session-orchestrator 3.20.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 (114) 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 +414 -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/docs/migration-v3.md +9 -6
  23. package/docs/persona-panel.md +3 -1
  24. package/docs/scope-collision-guard.md +167 -0
  25. package/hooks/enforce-scope.mjs +103 -3
  26. package/hooks/hooks-codex.json +1 -1
  27. package/hooks/hooks.json +11 -1
  28. package/hooks/on-session-end.mjs +52 -5
  29. package/hooks/on-stop.mjs +127 -12
  30. package/hooks/post-bash-write-verify.mjs +8 -32
  31. package/hooks/pre-task-scope-disjoint.mjs +1042 -0
  32. package/package.json +1 -1
  33. package/pi/prompts/release.md +12 -0
  34. package/scripts/autopilot.mjs +3 -1
  35. package/scripts/emit-session.mjs +42 -0
  36. package/scripts/export-hw-learnings.mjs +61 -2
  37. package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
  38. package/scripts/lib/backlog-scan.mjs +106 -15
  39. package/scripts/lib/build-live-signals.mjs +7 -3
  40. package/scripts/lib/ci-status-banner.mjs +207 -23
  41. package/scripts/lib/git-config-drift.mjs +471 -0
  42. package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
  43. package/scripts/lib/io.mjs +193 -7
  44. package/scripts/lib/learnings/select.mjs +157 -3
  45. package/scripts/lib/memory-cleanup-stamp.mjs +132 -8
  46. package/scripts/lib/mirror-issues-banner.mjs +266 -0
  47. package/scripts/lib/named-vault-resolver.mjs +105 -16
  48. package/scripts/lib/peer-cards/schema.mjs +6 -2
  49. package/scripts/lib/reconcile/writer.mjs +69 -30
  50. package/scripts/lib/redact-spans.mjs +89 -0
  51. package/scripts/lib/scope-baseline.mjs +77 -17
  52. package/scripts/lib/scope-gate.mjs +658 -0
  53. package/scripts/lib/secret-masker.mjs +262 -0
  54. package/scripts/lib/session-lock.mjs +34 -10
  55. package/scripts/lib/session-registry.mjs +9 -1
  56. package/scripts/lib/state-md/mission-status.mjs +21 -12
  57. package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
  58. package/scripts/lib/validate/check-agents.mjs +77 -5
  59. package/scripts/lib/validate/check-commands.mjs +2 -20
  60. package/scripts/lib/validate/check-doc-cli-commands.mjs +514 -0
  61. package/scripts/lib/validate/check-hooks-symmetry.mjs +18 -0
  62. package/scripts/lib/validate/check-owner-leakage.mjs +185 -17
  63. package/scripts/lib/validate/check-skills.mjs +191 -0
  64. package/scripts/lib/validate/check-test-git-config-target.mjs +665 -0
  65. package/scripts/lib/validate/check-unicode-safety.mjs +22 -2
  66. package/scripts/lib/validate/check-untracked-test-deps.mjs +925 -0
  67. package/scripts/lib/validate/check-unwired-features.mjs +219 -11
  68. package/scripts/lib/validate/check-vcs-repo-flag.mjs +965 -0
  69. package/scripts/lib/validate/frontmatter-block.mjs +61 -0
  70. package/scripts/lib/validate/tier-inference.mjs +46 -8
  71. package/scripts/lib/vault-mirror/namespace.mjs +146 -1
  72. package/scripts/lib/vault-mirror/process.mjs +264 -31
  73. package/scripts/lib/vault-mirror/render-sessions.mjs +115 -4
  74. package/scripts/lib/vault-status/board-writer.mjs +300 -56
  75. package/scripts/lib/vault-status/narrative-mirror.mjs +119 -5
  76. package/scripts/lib/vcs-repo-spec.mjs +500 -19
  77. package/scripts/print-learnings-index.mjs +30 -3
  78. package/scripts/release.mjs +616 -61
  79. package/scripts/site-numbers.mjs +1049 -0
  80. package/scripts/validate-plugin.mjs +61 -0
  81. package/scripts/validate-wave-scope.mjs +281 -12
  82. package/scripts/vault-mirror.mjs +26 -1
  83. package/skills/_shared/monitor-patterns.md +24 -4
  84. package/skills/_shared/state-ownership.md +17 -0
  85. package/skills/claude-md-drift-check/SKILL.md +9 -1
  86. package/skills/debug/SKILL.md +4 -1
  87. package/skills/discovery/issue-templates.md +4 -4
  88. package/skills/discovery/probes-code.md +2 -2
  89. package/skills/discovery/probes-feature.md +6 -6
  90. package/skills/discovery/probes-infra.md +2 -2
  91. package/skills/discovery/probes-session.md +5 -5
  92. package/skills/dispatcher/SKILL.md +10 -1
  93. package/skills/frontmatter-guard/SKILL.md +9 -1
  94. package/skills/gitlab-ops/SKILL.md +49 -39
  95. package/skills/gitlab-portfolio/SKILL.md +10 -1
  96. package/skills/memory-cleanup/SKILL.md +18 -5
  97. package/skills/npm-publish/SKILL.md +22 -50
  98. package/skills/persona-panel/SKILL.md +3 -1
  99. package/skills/plan/mode-new.md +23 -5
  100. package/skills/repo-audit/SKILL.md +10 -1
  101. package/skills/session-end/SKILL.md +44 -2
  102. package/skills/session-end/metrics-collection.md +1 -1
  103. package/skills/session-end/session-metrics-write.md +4 -10
  104. package/skills/session-plan/SKILL.md +2 -2
  105. package/skills/session-plan/wave-template.md +1 -1
  106. package/skills/session-start/SKILL.md +13 -1
  107. package/skills/spinout/SKILL.md +5 -1
  108. package/skills/sunset-review/SKILL.md +11 -1
  109. package/skills/tmux-layout/SKILL.md +7 -2
  110. package/skills/vault-mirror/SKILL.md +10 -1
  111. package/skills/vault-sync/SKILL.md +10 -1
  112. package/skills/vault-sync/validator.mjs +55 -6
  113. package/skills/wave-executor/wave-loop.md +42 -12
  114. package/scripts/lib/mission-status-schema.mjs +0 -114
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "Loop engineering for AI coding agents — turn ad-hoc sessions into a repeatable research → plan → wave-execute → close loop with verification gates. Runs on Claude Code, Codex CLI, Cursor, and Pi.",
10
- "version": "3.20.0"
10
+ "version": "3.21.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "session-orchestrator",
4
- "version": "3.20.0",
4
+ "version": "3.21.0",
5
5
  "description": "Loop engineering for AI coding agents — turn ad-hoc sessions into a repeatable research → plan → wave-execute → close loop with verification gates. Runs on Claude Code, Codex CLI, Cursor, and Pi.",
6
6
  "author": {
7
7
  "name": "Bernhard Goetzendorfer",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "session-orchestrator",
3
- "version": "3.20.0+codex.20260813195914",
3
+ "version": "3.21.0+codex.20260819195349",
4
4
  "description": "Loop engineering for AI coding agents — turn ad-hoc sessions into a repeatable research → plan → wave-execute → close loop with verification gates. Runs on Claude Code, Codex CLI, Cursor, and Pi.",
5
5
  "keywords": [
6
6
  "session",
@@ -73,17 +73,19 @@ Role-specific scope rules:
73
73
  - **Quality Phase 1 (Simplification)**: production files changed this session only (no test files)
74
74
  - **Quality Phase 2 (Tests)**: test file patterns only (`**/*.test.*`, `**/*.spec.*`, `**/__tests__/**`)
75
75
 
76
- **Test-sibling expansion (#970):** a scope entry naming a production file but not its test sibling makes the task's own regression test unwritable the guard then enforces exactly the inconsistency the quality gate exists to catch. Before writing `allowedPaths`, expand the union through the shared helper — do not hand-derive the paths:
76
+ **`allowedPaths` is COMPUTED, never hand-transcribed (#1020).** Hand-transcribing the union produced 5 scope divergences in one session. Three steps, in this order:
77
77
 
78
- ```js
79
- import { expandTestSiblings } from '$CURSOR_RULES_DIR/../scripts/lib/scope-gate.mjs';
80
- // Pass this wave's role verbatim — the helper GATES on it.
81
- const allowedPaths = expandTestSiblings(unionScopes, { role });
82
- ```
78
+ 1. Write each task's file scope verbatim as a JSON array of strings to `.cursor/filescopes/wave-<N>/<task-id>.json` — that path IS `$TASK_FILESCOPE_JSON`, reused verbatim by the subset assertion below; never a `$TMPDIR` temp file, which no later step can address. Expect a `bash-write-verify … OUTSIDE the wave's allowedPaths` notice here: `.cursor/filescopes/` is control state like `.cursor/wave-scope.json`, and this step runs before the union exists. Your OWN planned direct edits go into `coordinator.json` in the same directory and take part in both steps below (2 of those 5 divergences were coordinator-direct edits, for which no task scope file exists by construction).
79
+ 2. Build the sidecar — an ARRAY of `{id, files}` records, never an object map, so a duplicated id stays visible — and assert disjointness **before** the union, or the union launders a double assignment into the artefact meant to prevent it: `node "$CURSOR_RULES_DIR/../scripts/validate-wave-scope.mjs" --assert-disjoint "$WAVE_SCOPES_SIDECAR" < .cursor/wave-scope.json`. Exit 1 = two tasks were handed the same file; fix the plan, never widen the union.
80
+ 3. `--union <sidecar>` is a QUERY MODE that still needs a schema-valid manifest on stdin: write the skeleton with `"allowedPaths": []`, run `node "$CURSOR_RULES_DIR/../scripts/validate-wave-scope.mjs" --union "$WAVE_SCOPES_SIDECAR" < .cursor/wave-scope.json`, and insert the printed array as `allowedPaths`. It prints that array **instead of** the manifest echo one JSON document per run. It already applies test-sibling expansion, so do not run the helper by hand as well.
81
+
82
+ After the final wave, delete `.cursor/wave-scope.json` **and** `.cursor/filescopes/`.
83
+
84
+ **Test-sibling expansion (#970):** a scope entry naming a production file but not its test sibling makes the task's own regression test unwritable — the guard then enforces exactly the inconsistency the quality gate exists to catch. Step 3 above expands it for you via `expandTestSiblings(unionFileScopes(scopes), { role })`; do not hand-derive the paths.
83
85
 
84
- It emits a **glob** from the production basename (`foo.mjs` → `tests/**/foo*.test.mjs`), skips absolute entries, and is a strict no-op on Discovery's `[]`. **Which roles it fires for is decided inside the helper** — `TEST_SIBLING_EXPANSION_ROLES` in `scripts/lib/scope-gate.mjs` (currently Impl-Core / Impl-Polish; Quality Phase 1 is off, Phase 2 inert). Pass the role; do not pre-filter by role here. An absent or unrecognised role does **not** expand (fail-closed). Expand before the overlap check and before the file is written, in one pass. Full rule, the measured hit-rate and its ~15% residual, and the per-repo configurability: `skills/wave-executor/wave-loop.md` § Scope Manifest #3.
86
+ It emits a **glob** from the production basename (`foo.mjs` → `tests/**/foo*.test.mjs`), skips absolute entries, and is a strict no-op on Discovery's `[]`. **Which roles it fires for is decided inside the helper** — `TEST_SIBLING_EXPANSION_ROLES` in `scripts/lib/scope-gate.mjs` (currently Impl-Core / Impl-Polish; Quality Phase 1 is off, Phase 2 inert). Pass the role; do not pre-filter by role here. An absent or unrecognised role does **not** expand (fail-closed). The disjointness check (step 2) runs on the declared scopes, before the union expands anything; expansion happens before the file is written, in one pass. Full rule, the measured hit-rate and its ~15% residual, and the per-repo configurability: `skills/wave-executor/wave-loop.md` § Scope Manifest #3.
85
87
 
86
- Validate with: `node "$CURSOR_RULES_DIR/../scripts/validate-wave-scope.mjs" < .cursor/wave-scope.json` — fix JSON if it exits 1. Before each task batch, also assert the task's file scope against the manifest, carrying the mechanical half of the rule above (gated on the manifest's `role`, so it self-skips where expansion does not fire):
88
+ Validate with: `node "$CURSOR_RULES_DIR/../scripts/validate-wave-scope.mjs" < .cursor/wave-scope.json` — fix JSON if it exits 1. Before each task batch, also assert the task's file scope — `$TASK_FILESCOPE_JSON`, i.e. the step-1 file itself — against the manifest, carrying the mechanical half of the rule above (gated on the manifest's `role`, so it self-skips where expansion does not fire). This assertion is unchanged by #1020 and keeps running: it checks scope ⊆ union, a different property than disjointness — a file claimed twice is a subset twice over, so a double assignment is structurally invisible to it.
87
89
 
88
90
  ```bash
89
91
  node "$CURSOR_RULES_DIR/../scripts/validate-wave-scope.mjs" \
package/CHANGELOG.md CHANGED
@@ -7,6 +7,420 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.21.0] - 2026-08-19
11
+
12
+ Twenty-one commits (12 `fix`, 4 `feat`, 4 `docs`, 1 `chore`; 209 files, +28,906/−2,893), no
13
+ `BREAKING CHANGE:` footer and no `!` subject. Three strands — guard mechanics, the public
14
+ site, cross-session messaging — that turned out to be one strand: **a fact maintained in two
15
+ places, and a guard that is green without biting.** The sharpest instances are always the
16
+ quiet ones. **12 of 46 `SKILL.md`** and **14 of 16 `agents/*.md`** carried frontmatter that is
17
+ not YAML while every frontmatter checker in the tree had reported them clean for as long as
18
+ they existed. The v2 vault renderer's branch condition was **structurally false from its first
19
+ commit** and matched **0 of 253** session records. And the scope guard shipped, in the agent's
20
+ own channel, the command that disarms it.
21
+
22
+ ### Added
23
+
24
+ - **Cross-session messaging is wired (#1049, #1050, #1047 — Epic #1048, waves 1–2 of 5).**
25
+ New always-on rule `.claude/rules/cross-session-messaging.md` with CSM-001..005, and the PSA
26
+ decision tree gained a peer-inform branch that hangs **below** the sibling check on the
27
+ foreign-scope leg — so it can mask neither the sibling branch nor the PSA-002 pause. That is
28
+ structural, not prose. The two-axis rationale is re-founded rather than restated: the moat
29
+ was never "peer sessions cannot talk to each other" — two independent sessions in one working
30
+ copy exchanged full round-trips on 2026-08-16 — it is that they share **one working copy**,
31
+ and what crosses the channel is information, never isolation. `READ_ONLY_TOOLS` gained
32
+ `SendMessage` + `ListAgents`, the mechanical precondition for upward agent escalation;
33
+ `validateTierConsistency` filtered on `WRITE_TOOLS` and therefore named **no culprit at all**
34
+ for an unknown tool.
35
+ - **The public site is rebuilt, and it now proves its own numbers (#1043–#1046).** The live
36
+ page had served v3.19.0 against v3.20.0 in the repo for six days, all five legal paths were
37
+ 404, and the `Measured` block — the honesty argument of the whole page — carried five wrong
38
+ numbers.
39
+ - The load-bearing element is a band whose station height **is** the file count a wave may
40
+ write: `--unit: 15px` = one file. Discovery is therefore not a box of height 0 but a stroke
41
+ on the baseline — the moment the limit becomes visibly geometric rather than requested.
42
+ - Mobile had **no** navigation at all (`display:none`, no hamburger, 20 viewports of scroll);
43
+ 6 links are now visible at 390px. Twelve sections in identical scoring and 27 equally loud
44
+ cards became 10 sections with three loud blocks, cutting rendered height to **52.4%**
45
+ (10,350 → 5,428px — the factor is the durable figure, the viewport count is not: 5.98
46
+ viewports holds at 908px window height and becomes 6.03 at 900px). State no longer rides on
47
+ red/green alone but on shape **and** glyph. Cold-load acceptance without `<script>`: 115
48
+ content-bearing elements, 0 invisible, identical height — the page hangs on no condition.
49
+ - **`vercel.json`** carries the deploy configuration **versioned** instead of clicked into a
50
+ dashboard: `outputDirectory: "site"`, CSP/HSTS/`frame-ancestors` headers, www→apex redirect.
51
+ The Vercel Git integration is connected, so a push to the GitHub mirror publishes the site.
52
+ In `scripts/release.mjs` the checklist line `cd site && vercel --prod` is replaced by
53
+ `verifyLiveSite()` with four **distinguished** outcomes (version mismatch / success /
54
+ network error / surface moved), not collapsed onto a flat "not ok".
55
+ - **`scripts/site-numbers.mjs`** — 13 metrics read from the repo and written into
56
+ `data-metric` spans, ending the hand-maintained number block. Its first run against the new
57
+ page caught three real errors, one of them the coordinator's (`skills: page says 47, repo
58
+ says 46` — `skills/_shared/` has no `SKILL.md` and is not a skill) and one a contract defect
59
+ that would have produced `vv3.20.0` at the next release. Wired into `--set-version`,
60
+ deliberately **not** into CI: `sessions` and `learnings` grow every session, so a pipeline
61
+ gate on them would be permanently red.
62
+ - **`/guide`** — the four install paths, the first session with the honest number (7–10
63
+ questions, 2–4 minutes, not the claimed "one question"), and a section on recognising that
64
+ a session failed silently. The reason is a census, not a hunch: **all four issues opened by
65
+ people outside this repo are install or environment failures, not one a feature request**
66
+ (`gh issue list -R Kanevry/session-orchestrator --state all`; authors ≠ owner: #53, #54,
67
+ #62, #63).
68
+ - **`/impressum` and `/datenschutz`.** The privacy policy is not copied but written from the
69
+ measured processing: 0 external requests, no analytics, self-hosted fonts (3 files,
70
+ 55,052 B), Vercel server logs only. The sibling site's Vercel-Analytics sections are absent
71
+ on purpose — `grep` shows they are not active here. Sitemap 1 → 4 URLs.
72
+ - **Two catchers for the untracked-test-dependency class (#1081), because one was structurally
73
+ blind.** A differential run compares test *status*, and a test that swallows a missing file
74
+ and passes in **both** trees produces no difference at all. Statically:
75
+ `scripts/lib/validate/check-untracked-test-deps.mjs`, rules R2 (a statically resolved repo
76
+ root passed into an import closure that names an untracked path) and R4 (cwd-relative read),
77
+ measured 2/2 recall at 0 false positives where the obvious variants sat at 98–99% FP.
78
+ Structurally: `.husky/pre-push` materialises the sha being pushed via `git clone
79
+ --no-hardlinks` (1.04 s; `git archive` is unusable — without `.git`, 21 tests fail with "not a
80
+ git repository") and runs the gate **there**. It came within one function of being worthless:
81
+ `findProjectRoot()` checks `CLAUDE_PROJECT_DIR` and five siblings **before** cwd, so the gate
82
+ would have started in the tmp tree and read the working-tree files anyway.
83
+ - **A tracked census snapshot, `site/_census.json`, with ledger precedence** — per-metric opt-in
84
+ for exactly `sessions`, `learnings`, `counted-sha`, never a blanket fallback, which would have
85
+ made `collect()` blind to "wrong root". `optional: true` was considered and rejected by name:
86
+ it would have let the two tiles fall silently back into the hand-maintained state while
87
+ `--check` still reported the page as current — verbatim the silent-failure class the script
88
+ was written against. A known metric id without a value is now a third contract violation
89
+ (`unresolved`).
90
+ - **`tests/setup/scrub-git-env.mjs`, `scripts/lib/git-config-drift.mjs`, and
91
+ `check-test-git-config-target.mjs`** — the mechanism against this session's own accident (see
92
+ Notes). The setup file removes nine redirect-capable git variables once per worker; that is
93
+ the root, because the two lines that caused the damage passed a **correct** cwd, and `GIT_DIR`
94
+ beats cwd, `-C`, and even the positional of `git init <dir>`. The drift probe covers the
95
+ surface `git status` does not show, and runs with a filtered environment — a set `GIT_DIR`
96
+ would otherwise redirect the probe itself into a foreign repo and call this one clean. The
97
+ lint rule is WARN-only out of measurement, not caution: its first formulation had 11 hits,
98
+ **all** false positives (each a `git init <dir>` where the positional **is** the target); it
99
+ ends at 150/150 with a target.
100
+ - **`scripts/lib/validate/check-skills.mjs` — a real parser over all 46 `SKILL.md`**,
101
+ registered in `validate-plugin.mjs`. The sibling `check-agents.mjs` validated frontmatter
102
+ with line-oriented regexes, which is precisely how the 12 broken blocks stayed invisible:
103
+ an unquoted `description:` containing a `: ` (`"Iron Law: NO FIXES"`) is not YAML, but a
104
+ regex looking for `^description:` sees nothing wrong, and Claude Code's own loader is
105
+ lenient enough that the defect never surfaced at runtime. A `js-yaml` `CORE_SCHEMA` parse
106
+ is now rule R8 and the five field rules hang off it — none of them can be evaluated on a
107
+ block that does not parse. Deliberately **not** implemented: a block-scalar ban (see
108
+ below), and length ceilings on `name`/`description`, because no spec vendored in this repo
109
+ states one and an invented requirement is worse than none.
110
+ - **`scripts/lib/validate/frontmatter-block.mjs` — shared extraction, deliberately unshared
111
+ rules.** The extractor stood verbatim in two checkers, so the next change to the block
112
+ format would have landed in one copy and one gate would have started accepting what the
113
+ other rejects — with no test able to see it, because each gate tested its own copy. The
114
+ module owns the byte range and nothing else, and its header records why the three checkers
115
+ must stay **contradictory**: `check-agents.mjs` bans `description: >` because the agent
116
+ loader cannot read a folded scalar, while `check-skills.mjs` must tolerate it — for
117
+ `SKILL.md` the folded form is the only shape that makes the `: ` collision structurally
118
+ impossible, 35 of 46 files now use it, and porting the agent ban here would red 35 of 46
119
+ and forbid the very fix that made the gate green. The divergence is the requirement, not
120
+ drift.
121
+ - **`scripts/lib/mirror-issues-banner.mjs` — the GitHub-mirror blind spot**, wired into
122
+ `skills/session-start/SKILL.md` Phase 4. VCS auto-detection picks exactly one platform, so
123
+ in a GitLab-origin repo with a public GitHub mirror no code path ever read the mirror's
124
+ issues: everything filed by an external reporter was structurally invisible to every
125
+ session. The probe asks the other side, hard-pinned to `github` (auto-detecting here would
126
+ reproduce the defect it exists to compensate for) and **self-disabling** — no `github`
127
+ remote resolves to `undefined` → `null` → no spawn, no network call. No new Session Config
128
+ key by design: the spec comes from `git remote`, and a key would be a second SSOT drifting
129
+ against it. The return is three-valued, not two — `null` means *never asked* or *asked and
130
+ clean*; a `degraded` field with a closed enum means *the query failed and the state is
131
+ unknown*. Collapsing that third state into `null` is what makes a dead probe
132
+ indistinguishable from a healthy repo, which is the next section's recurring theme.
133
+ - **`atomicWriteWithBackup()` in `scripts/lib/io.mjs`** — the tmp-write/rename/`.bak-<ISO>`
134
+ primitive that `writeJsonAtomicSync` now delegates to, ending a second copy of the same
135
+ sequence (#734).
136
+ - **`check-unwired-features` signal S3 `orphaned-prose-module`** — a config key is not the
137
+ only thing prose can promise; a document can also assert that a module does a job nothing
138
+ calls. S3 fires only where the claim is in the **passive voice with a bare filename and no
139
+ exported symbol**: "…*are validated* against `foo.mjs`" asserts that something happens by
140
+ itself, whereas "dispatch via `runWavePool()`" addresses a reader who will do it. The
141
+ broader check — every export with no non-test importer — was measured and rejected: 1366
142
+ exports, 779 unimported, 93.2% false positives naive and still 81.2% after four exclusion
143
+ rules. A gate that prints 282 lines is switched off in week two, which is this file's own
144
+ disease one level up. Measured 2026-08-14, the cascade narrowed 452 production modules to
145
+ **2**; one of those is deleted below, so the live report now stands at 1. Read a near-empty
146
+ report as designed, not broken — S3 is a relapse guard, not a cleanup tool.
147
+ - **[ADR-0012](docs/adr/0012-pseudonym-map-privacy.md)** — host-local pseudonym map for
148
+ owner-leaky repo namespaces. An ADR rather than a code comment precisely because every
149
+ element reads like removable defensiveness at its call site while being load-bearing for a
150
+ property no test can observe directly: a green suite is fully compatible with the leak.
151
+
152
+ ### Fixed
153
+
154
+ - **The documented Claude Code install path was guaranteed broken.** `claude plugin dir` is not
155
+ a subcommand: `claude plugin dir session-orchestrator` answers `error: unknown command 'dir'`
156
+ and exits 1 (measured on Claude Code 2.1.235; `claude plugin --help` lists 14 subcommands and
157
+ `dir` is none of them). The `||` fallback therefore fired **every** time and pointed at
158
+ `~/.claude/plugins/session-orchestrator`, a directory that does not exist — so `cd` failed,
159
+ `&&` aborted, `npm install` never ran, and the hooks could not find `zx`. That is the cause
160
+ behind external issues #62/#63, whose reporters described the symptom. Replaced by a
161
+ layout-independent resolution through the plugin cache, extracted from the **shipped**
162
+ `site/index.html`, entity-decoded and executed verbatim: exit 0, correct path. Second defect
163
+ in `migration-v3.md` fixed alongside — a marketplace-installed plugin sits in a managed cache,
164
+ not a git checkout, so `git pull` there does nothing; replaced by `/plugin update`. Five sites
165
+ in five files; the two remaining matches for `claude plugin dir` are deliberate quotes on the
166
+ site explaining the breakage.
167
+ - **The v2 vault renderer was dead at birth (#1074).** `total_agents` stands in
168
+ `REQUIRED_FIELDS`, so the branch condition `total_agents === undefined` is structurally false
169
+ — written 4h23m before the first record that could have matched it, and matching **0 of 253**
170
+ records since. The obvious fix would have been **worse than the bug**: `agents` is polymorphic
171
+ — measured over 599 wave objects, **210× a number and 14× an array** — so a naive `??` would
172
+ have written `[object Object]` into the vault. A type-aware alias chain repairs 239 agent, 5
173
+ files and 280 quality cells at changed-existing = 0. The fixtures are now harvested golden
174
+ records; the old `makeV2Entry` invented a record no production writer can produce.
175
+ - **Four blind probes and one active fail-open (#1039).** `backlog-scan` folded "no remote" and
176
+ "query failed" into a silent `null`, from which the mode selector read "contributes 0 delta" —
177
+ a 40-issue backlog with critical labels looked like an empty one. `ci-status-banner` forbade in
178
+ its own comment at ~line 490 the very form it still used. `named-vault-resolver` had one
179
+ `source: 'fallback'` covering two causes (honestly relativised by the agent:
180
+ `resolveNamedVault` has zero production callers, so the defect was real but not yet reachable).
181
+ `scope-baseline` hardcoded `'origin/main...HEAD'` twice, and the damage sat elsewhere than in
182
+ the name: `wave-loop.md:874` renders `skipped: 'unresolvable-ref'` **silently**, so the
183
+ scope-drift tripwire was permanently inert in every non-origin repo and never said so.
184
+ `vault-mirror/namespace` `deriveRepo()` degraded silently to `basename(cwd)` and wrote that as
185
+ repo **identity** into the vault — which then happened for real this session. And
186
+ `harness-audit` category 6 was an active fail-open: outside a git repository it awarded **2/2
187
+ with "no github mirror remote configured — skipped"**; a non-measurable query now fails 0/2,
188
+ full marks only for genuine absence. Census correction: **14** executable origin hardcodings,
189
+ not the 4 the issue named.
190
+ - **The scope guard shipped the instructions for its own disarmament (#1057).** The
191
+ stale-manifest branch ended on ``remove it with `rm -f <path>` and let the coordinator write a
192
+ fresh manifest`` — and that string travels through `emitDeny()` in `permissionDecisionReason`,
193
+ i.e. into the context of the very agent whose edit was just denied. Reproduced end to end from
194
+ the coordinator: `rm -f` is **allowed** by `pre-bash-destructive-guard.mjs` (14 rules, only
195
+ `rm-rf-destructive` bites); with the manifest removed, an in-project `Edit` is ALLOW **and** a
196
+ `Write` to `/etc/` is ALLOW — not only gate 7 (allowedPaths) falls but gate 6 (containment),
197
+ because under the exit-0 protocol the hook then emits no decision at all and exit 0 is not a
198
+ veto. The guard was off for the rest of the session. Not theoretical: a **parallel** session in
199
+ the same working copy received this suggestion for the **live** `wave-scope.json` of this one,
200
+ and declined only on an indicator (mtime 7 minutes old), not on knowledge — at one hour of age
201
+ the same indicator points the other way. The new text names both cases instead of guessing one
202
+ and routes to `blocked` per PSA-001/003/007. `systemMessage` was examined and deliberately not
203
+ used: `io.mjs:329` says the operator sees that field, `hook-development/SKILL.md:108` says it
204
+ is "shown to Claude" — a session-wide guard disarmament does not belong on a channel whose
205
+ reader the repo cannot name consistently.
206
+ - **The owner-leakage scanner was blind to `.html` (#1076) — and its first sighted run found a
207
+ real defect.** The naive fix would have excluded **nothing**, because the sanctioned form
208
+ carries no `www.` while the site uses `www.` throughout; the result would have been a
209
+ permanently red gate including pre-commit. Then the eighth planted defect: seven forms had been
210
+ driven against the new coverage and all seven bit, but all seven sat in the CP1 corridor, and
211
+ CP1 is the **only** one of the eleven rules that runs through `canonicalizeLine()` — revived,
212
+ in other words, for 1 of 11. The eighth is an entity-encoded dot in the **domain**, which a
213
+ browser resolves and the scanner did not. CP2/CP3/CP7/CP8 now additionally check the canonical
214
+ form, and the discriminator is measured rather than guessed: dot-anchored rules gain
215
+ (canonicalisation can never fabricate a dot from a separator) while slash- and slug-anchored
216
+ rules stay raw (canon folds `-` onto `/`, which flips CP4/CP10 false→true and shreds CP6).
217
+ `isAllowlisted()` on the canonical form would have been **wrong** — canon breaks its own
218
+ allowlist — so the design uses an occurrence counter instead: if the canonical form carries
219
+ more domain tokens than the raw one, the surplus came from a decoding and can never be the
220
+ sanctioned publication. Cost repo-wide: 0 new findings.
221
+ - **Admitting `.html` to `check-unicode-safety.mjs` immediately found a live legal defect.**
222
+ `site/datenschutz/index.html:277` carried a `U+00AD` at end of line. A soft hyphen does **not**
223
+ suppress the break — HTML collapses newline plus indentation into a space — so the page
224
+ rendered "Auftragsverarbeitungs vereinbarung", visibly broken, on a legally required page.
225
+ Deleting the `U+00AD` alone would not have fixed it; the space would have remained.
226
+ - **Six findings from the review panel against the site, the most important a false promise
227
+ (#1043/#1044/#1046).** The privacy policy described a network request that does not exist — a
228
+ reload of `leaderboard.json` including `credentials: 'omit'` — while a grep for
229
+ `fetch(|XMLHttpRequest|sendBeacon|WebSocket|EventSource|import(|new Image` across all four
230
+ pages returns 0 hits. The direction is harmless (over- rather than under-declared); the place
231
+ is not, because §10 expressly invites the reader to open the network tab and check. It was the
232
+ single statement on the page a reader could falsify in two minutes — on a page whose thesis is
233
+ checkability. Struck without replacement. The other five: a `TODO` placeholder shipped live in
234
+ public source (`index.html:665`), a `/guide` canonical that contradicted itself three ways,
235
+ missing og/twitter tags on all three subpages (a shared guide link rendered as a bare URL in
236
+ Slack, X and Discord — for a product distributed through shared links, the one classically
237
+ SEO-adjacent item with real return), `offers` struck from the JSON-LD, and an unquantified
238
+ absolute ("Every issue an outside user has ever opened" → "All four issues opened by people
239
+ outside it") two paragraphs above where the number already stood correctly.
240
+ - **A character allowlist in the census generator, and a `ReferenceError` on the path that
241
+ enforces it.** `rewrite()` checked `/[<>]/` against the **old** cell content, never the new
242
+ value. Eleven of the thirteen metrics are digits or hex by construction, but `version` is
243
+ whatever `package.json` says and `readPackageVersion` only checks "non-empty string" — a
244
+ prepared version literal could have closed the `<span>` and opened a tag. The precondition is
245
+ write access to `package.json`, which in this repo's trust model already means full access, so
246
+ this is defence in depth rather than an open hole — but it is the one place where `script-src
247
+ 'unsafe-inline'` would stop being theoretical. A rejection now refuses the **whole** file, not
248
+ just the value, and the allowlist hangs on the write rather than on HTML-span presence (before,
249
+ `"rules": "../../etc/passwd"` landed unchecked in the shipped file). Building it caught a defect
250
+ of its own: the error path called a `writeStderrLine` that never existed in that file — a
251
+ `ReferenceError` exactly where the guard bites. Error paths have to be executed, not read.
252
+ - **Accessibility: one real AA violation, and controls at 1.71:1.** `/guide` had no live region —
253
+ the copy button only changed its visible text, so a screen-reader user got **nothing** on
254
+ success and nothing on the error path, where "Press Ctrl+C" is the only way forward (WCAG
255
+ 4.1.3). Seven copy buttons shared one name and were indistinguishable in a screen reader's
256
+ element list, the one place they are read outside their context; names are now derived from the
257
+ nearest label in **document order** plus the first non-comment line, measured 7 of 7 unique
258
+ against 1 of 7 before. `button.copy` and `a.btn-2` sat at 1.71:1, under the 3:1 a control
259
+ boundary needs (WCAG 1.4.11); raising `--line-2` globally would have been a design change, since
260
+ the same token draws decorative hairlines where 3:1 is loud, so a separate `--line-ui` token
261
+ carries the purpose: measured 3.40:1 / 3.57:1 / 3.57:1 on the rendered element. Two comments
262
+ that claimed too much were corrected — the print comment said "only the two moving 1px lines are
263
+ dropped" while the rule below hides five selectors including the whole six-link navigation.
264
+ - **Page and receipt came from two different measurements.** `site/index.html` said
265
+ 252/135/`6fa214d`, `site/_census.json` said 253/140/`6f6bf58` — both tracked, both publicly
266
+ shipped, `--check` reporting 3 drifted. The earlier instance had the same shape and a
267
+ coordinator cause: a `--write` step run against a file still inside a live agent's file scope —
268
+ the PSA-002 scope collision this repo built `--assert-disjoint` against, one level above the
269
+ wave mechanics. The new coupling test compares the two **committed** artefacts against each
270
+ other rather than against the repo, so "both stale together" is green and only a divergence is
271
+ red; it asserts on drift **or** stale, because `counted-sha` is `provenance: true` and a
272
+ drift-only assertion would have been green against exactly the state it forbids.
273
+ - **The mirror push had a fourth state, and it was fail-open.** Today's own fix distinguished
274
+ three (no remote / push ok / push failed). Outside a git repository, `git remote get-url github`
275
+ fails with `fatal: not a git repository` — by exit code indistinguishable from "no such remote" —
276
+ so the block reported `no 'github' remote configured — skipping (not an error)` on stdout and
277
+ exited **0**: a broken environment reported as a healthy one, inside the fix that was closing
278
+ fail-open. Found by a reviewer explicitly briefed to **refute** the measurements; not by the
279
+ author, not by the test, not by the gate. Fixed with a `git rev-parse --git-dir` guard, all four
280
+ states executed individually in throwaway repos, `bash -n` **and** `sh -n` green (the bash-3.2
281
+ trap from this repo's own rule file).
282
+ - **CI had been red for six consecutive pipelines (#1081).**
283
+ `tests/scripts/site-numbers.test.mjs` calls `collect(REPO_ROOT)` and requires `missing === []`;
284
+ two of the 13 metrics read `.orchestrator/metrics/*.jsonl`, untracked per `.gitignore:40`.
285
+ Locally 23/23, on CI `23 tests | 23 skipped`, because a throwing top-level `beforeAll` takes the
286
+ whole file with it in Vitest — the blast radius was 23, not 9. Measured now: #7276, #7277,
287
+ #7280, #7281, #7286 and #7288 red, green again from #7334 (`glab ci list --per-page 20`).
288
+ - **The new pre-push gate produced three defects of its own before it worked**, each measured
289
+ rather than reasoned. (1) `git clone <path>` points the clone's origin at a **filesystem path**
290
+ while CI's checkout points at the real remote URL, so any code that *parses* that URL behaves
291
+ differently in the two trees — the opposite of the hook's purpose: `deriveRepo()` returned
292
+ `unknown-repo` where working tree and CI both return `session-orchestrator`, and
293
+ `namespace.test.mjs:199` asserts exactly against that. The real origin URL is now carried into
294
+ the clone. (2) A repo-pointing git environment beats both `-C <path>` and cwd, so
295
+ `git -C "$tree" checkout --detach` ran against the **original** repository. (3)
296
+ `GIT_CONFIG_PARAMETERS` propagates into every child once any `-c` was passed anywhere: pushing
297
+ with `git -c core.hooksPath=.husky push` carried that override into the materialised clone,
298
+ where every throwaway repo the gate's own test suite creates then fired the repository's
299
+ **real** pre-commit hooks — `test: fail, total: 0` three times in a row, green the moment the
300
+ `-c` was dropped. Both scrub sites are now namespace sweeps with a justified keep-set instead
301
+ of name lists, because the denylist has shipped with a hole **twice in a row**, each time at
302
+ the sibling form of the name just closed (`GIT_CONFIG_COUNT` without `GIT_CONFIG_PARAMETERS`,
303
+ then the exact mirror image). The source-derived census found **six of nine** unscrubbed
304
+ platform variables where the panel had reported three of four — including `PLUGIN_ROOT` (rung
305
+ **one** of `resolvePluginRoot`, above every `CLAUDE_`/`CODEX_`/`PI_` name the old list already
306
+ unset) and the Cursor plugin root, which is not `CURSOR_PLUGIN_ROOT` but `CURSOR_RULES_DIR`;
307
+ adding the guessed name would have read in review as a complete fix and closed nothing.
308
+ `GIT_EDITOR` and `GIT_EXEC_PATH` are kept by measurement, not hygiene: Claude Code sets
309
+ `GIT_EDITOR=true`, and a blind sweep drops git back to `vi` and hangs the worker.
310
+ - **Two silent instrument errors (#979, #1062).** `scanBacklog` read 50 of 89 open issues and
311
+ reported the window nowhere: `limit 50 => total 50, critical 0, high 10, stale 0` against
312
+ `limit 100 => total 89, critical 1, high 18, stale 5`. All three numbers wrong, and the mode
313
+ selector at Phase 7.5 computed with them — `staleCount 0` meant not one stale issue was visible
314
+ to it. The cause was the window alone, not the `priority::`/`priority:` split. Separately,
315
+ `memory_cleanup_at` hung on a prose instruction that failed once: `stampMemoryCleanup()` had
316
+ zero production callers, every reference was text asking the coordinator LLM to remember. On
317
+ 2026-08-14 a `/memory-cleanup` ran with documented yield, the step was skipped, all three
318
+ session records of that day carried `memory_cleanup_at: null`, and the banner reported "last
319
+ cleanup 29 days ago" against the 3 days of its own notes — with **no** mechanical signal
320
+ anywhere: zero `orchestrator.memory.cleanup*` events in 22,887. `/memory-cleanup` now emits the
321
+ event and `emit-session.mjs` derives the stamp from it.
322
+ - **The host registry lost living sessions (#1047).** `hooks/on-stop.mjs` deleted the registry
323
+ entry and refreshed the lock heartbeat 12 lines further down — same `if` block, at every turn
324
+ end — while `heartbeat()` had had zero production callers since its birth commit. The fix is
325
+ atomic (deletion out **and** heartbeat wired), because either half alone would have been worse
326
+ than the bug: entries would survive but never age, drop out of `detectPeers` after 15 minutes
327
+ and be swept as "stale" after 60. Deregistration moved to `hooks/on-session-end.mjs`. The
328
+ platform census in the comment was wrong at first and corrected by review — pi does deregister,
329
+ Cursor never registers and so cannot leak; Codex is the only affected platform.
330
+ - **`gh repo view -R` broke the GitHub CI banner for every external user of this plugin
331
+ (#1022).** `gh repo view` takes the repository as a **positional** argument and has no
332
+ `-R`/`--repo` flag at all, so the host-pinning added in #872 made `gh` exit 1 with
333
+ `unknown shorthand flag: 'R'` — an error `checkCiStatus`'s outer catch swallowed to
334
+ `null`, leaving the session-start Phase 4 banner silently dead on every GitHub repo. Fixed
335
+ positionally, and the swallow narrowed: a CLI that is *present* but fails now reports
336
+ `degraded` instead of `null`. The asymmetry across the three neighbouring call sites is
337
+ real and must not be unified — `glab repo view` **does** take `-R`, and `gh api`/`glab
338
+ api` take neither `-R` nor a positional, only `--hostname`.
339
+ - **`hooks/on-stop.mjs` printed a 10-frame stack trace at every turn end when `node_modules`
340
+ was absent** (interrupted install, EPERM sandbox, half-synced plugin cache): a static
341
+ `import { $ } from 'zx'` fails at module-load time, with no hint that `npm install` is the
342
+ fix. `zx` is now imported lazily and the failure degrades to one rate-limited stderr line
343
+ per 6h window, mirroring the missing-`node` degradation in `hooks/run-node.sh`.
344
+ (GH Kanevry/session-orchestrator#63.)
345
+ - **A board row inherited one legacy entry's terminal status to every same-named repo,
346
+ permanently (#871).** `_active-sessions.md` rows were keyed by `path.basename(repoRoot)`,
347
+ so two repos with the same directory name under different parents were one row — and both
348
+ are enumerable, since the dispatcher walks to depth 2. Rows are now keyed by a path-derived
349
+ hash, case-folded on APFS/NTFS so `…/Some-Repo` and `…/some-repo` do not split back into
350
+ the duplicate rows #719 had already fixed at the name layer. The key length carries a named
351
+ ceiling and a revisit trigger, not an intention to revisit.
352
+ - **Two vault write channels published unmasked secrets (#974/#1025)** —
353
+ `scripts/lib/vault-status/narrative-mirror.mjs` and `scripts/lib/vault-mirror/process.mjs`,
354
+ both writing tracked, pushed artefacts. The masker's needle set is a function of the
355
+ caller's env, which is not a defect but does mean two runs over the same records mask
356
+ differently: a consumer comparing a written artefact against a fresh candidate must treat
357
+ an already-redacted span as a **wildcard** (`matchesModuloRedaction`), or a later
358
+ partially-populated run rewrites the raw value it had already redacted. The tempting fix —
359
+ persist the needle set — is rejected: it puts a plaintext secrets file on disk to defend
360
+ against secrets on disk. Masking runs **after** `extractNarrative`, never before, because
361
+ masking the raw string first lets `[REDACTED]` land inside the structure and silently
362
+ delete a whole table from the mirrored file.
363
+ - **`agents/*.md` frontmatter is parsed, not pattern-matched.** `check-agents.mjs` gained the
364
+ same `js-yaml` `CORE_SCHEMA` rule, reported alone with no fall-through to the field rules —
365
+ a field rule evaluated on an unparseable block is a guess. Its existing block-scalar ban
366
+ stays: that rule forbids one particular *valid* YAML form, which is a different question
367
+ from whether the block is YAML at all.
368
+ - **Deleted `scripts/lib/mission-status-schema.mjs`** — 4 exports, zero production callers,
369
+ while three prose locations promised its application. Found by S3 above, on the day S3 was
370
+ written.
371
+
372
+ ### Notes
373
+
374
+ - **3.18.0 is missing from npm.** The tag `v3.18.0`, the GitHub release and the CHANGELOG entry
375
+ all exist; the registry has never seen the version — `npm view session-orchestrator versions`
376
+ returns `…, "3.16.0", "3.17.0", "3.19.0", "3.20.0"`. That gap is the incident #978 names and
377
+ the reason `scripts/release.mjs --publish` now creates the annotated tag only **after** a
378
+ registry-verified publish. Nothing in this line republishes it; the version number stays
379
+ skipped rather than reused.
380
+ - **This session damaged its own repository, and the first recovery missed half the damage.** A
381
+ coordinator diagnostic command exported `GIT_DIR` and ran the suite. In the real `.git`: HEAD
382
+ detached, three fixture commits, plus a foreign remote and the fixture identity in
383
+ `.git/config` — which then authored two commits. The first recovery checked HEAD, the index and
384
+ all 1614 files and **overlooked `.git/config` entirely**, because `git status` does not show it;
385
+ an agent measuring something else found it two hours later. History rewriting failed on both
386
+ remotes' branch protection (GitLab "not allowed to force push", GitHub GH006), so authorship
387
+ was corrected via `.mailmap` — which incidentally refutes part of this session's own #1079
388
+ finding: `enforce_admins: false` does **not** make the protection bypassable for force-push.
389
+ One correction to the record: the claim "git runs every hook with `GIT_DIR` set", written in
390
+ `334ea2d`, is **false** — measured on git 2.x / macOS, `pre-push`, `pre-commit`, `commit-msg`
391
+ and `post-commit` all run with `GIT_DIR` unset. The scrub remains right as defence in depth;
392
+ its stated reason was not.
393
+ - The sharpest instances of the frontmatter class are the ones where the check was green. A
394
+ hand-rolled `getDescription` regex in
395
+ `tests/lib/validate/skill-description-quality.test.mjs` terminated on the first folded line,
396
+ measuring **97 characters where the real YAML value is 329** (`session-start`; `autopilot`
397
+ 107 vs 555, `bootstrap` 98 vs 341) — which made its own `>= 250` assertion **vacuous** for
398
+ every skill already using the block-scalar form. It surfaced only because repairing the 12
399
+ unparseable blocks moved files across the threshold. `yaml.load` is now the single reader.
400
+ - **Found and deliberately not fixed here**, because both sit outside this line's scope and
401
+ both are the same shape as the bugs above: `agents/eval-judge.md` is **valid** YAML and
402
+ loses **96%** of its description — 1180 raw characters, 51 visible to a parser — because
403
+ ` #803` opens a YAML comment at column 51, so no parse rule can catch it. And
404
+ `skills/discovery/probes-arch.md:39` calls
405
+ `npx madge --circular --extensions ts,tsx,js,jsx src/` in an all-`.mjs` repo that has no
406
+ `src/` directory: it processes zero files and reports success.
407
+ - **Open, deliberately.** `site/llms-full.txt` still carries its numbers by hand — the generator
408
+ only touches HTML. They were corrected and cross-checked against the generator, but that is
409
+ exactly the defect this line fixes. Revisit trigger: as soon as a second text file carries
410
+ measured values. And the `scanBacklog` fix has no test of its own; it is covered only by the
411
+ 171 existing tests of the touched modules staying green, with the fake regression outstanding.
412
+ - One reported number did not survive review and is recorded here rather than quietly dropped:
413
+ "274 elements checked" for the site's contrast sweep is **not reproducible** — two independent
414
+ checkers measure 260 / 282 / 288 depending on an inclusion rule that was never shipped with the
415
+ number. The **value** 4.81:1 (worst text contrast) is stable across all three counting rules;
416
+ the count is not. A count without its rule is worthless — the same lesson #979 taught once
417
+ already in this line.
418
+ - Gate at `8984224`, re-run for this entry rather than quoted: **587 test files, 14,244 passed
419
+ / 0 failed / 11 skipped** (`npm test`, exit 0), typecheck **404 files OK** (exit 0),
420
+ owner-leakage **1548 files / 0 findings**, unicode-safety 0 findings. The release commit also
421
+ reports `validate-plugin` 172/0 and lint 0 at that sha; both are quoted from it, not
422
+ re-measured here.
423
+
10
424
  ## [3.20.0] - 2026-08-13
11
425
 
12
426
  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.21.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.21.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.21.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 site that proves its own numbers (#1043–#1046)** — the live page had served v3.19.0 against v3.20.0 in the repo for six days, all five legal paths were 404, and the `Measured` block the honesty argument of the whole page carried five wrong numbers. It is now generated: `scripts/site-numbers.mjs` reads **13 metrics** from the repo into `data-metric` spans, and its very first run caught three real errors, one of them the coordinator's own (`skills: page says 47, repo says 46`) and one a contract defect that would have shipped `vv3.20.0` at the next release. Deploy config moved from a dashboard into a versioned `vercel.json`. Rendered height fell to **52.4%** of the old page (10,350 5,428px), and mobile navigation — previously `display:none` with no hamburger — exists.
139
+ - **The documented Claude Code install path was guaranteed broken** — `claude plugin dir` is not a subcommand; it exits 1, the fallback path does not exist, `cd` fails, `npm install` never runs, and every hook afterwards cannot find `zx`. Measured on Claude Code 2.1.235. That is the cause behind external issues #62/#63, whose reporters described the symptom — and **all four issues opened by people outside this repo are install or environment failures, not one a feature request**. The replacement was extracted from the shipped HTML, entity-decoded and executed verbatim.
140
+ - **The scope guard shipped the instructions for its own disarmament (#1057)** — the stale-manifest denial ended on ``remove it with `rm -f <path>` ``, and that string travels in `permissionDecisionReason` into the context of the agent just denied. `rm -f` is not blocked (14 rules, only `rm-rf-destructive` bites), and with the manifest gone both allowedPaths **and** project-root containment fall. A parallel session in the same working copy received this suggestion for the live manifest of this one, and declined on an indicator rather than on knowledge.
141
+ - **Two dead instruments, found by measuring rather than reading** — the v2 vault renderer's branch condition was **structurally false from its first commit** and matched **0 of 253** session records; the obvious fix would have been worse than the bug, because `agents` is polymorphic (210× a number, 14× an array over 599 wave objects) and a naive `??` would have written `[object Object]` into the vault. And `scanBacklog` read 50 of 89 open issues while reporting the window nowhere: `critical 0, high 10, stale 0` where the truth was `1 / 18 / 5`.
142
+ - **Cross-session messaging is wired (Epic #1048, waves 1–2)** — a new always-on rule with CSM-001..005, and a peer-inform branch hung **below** the sibling check in the PSA decision tree so it can mask neither that branch nor the PSA-002 pause. The moat was never "peer sessions cannot talk to each other" they demonstrably can it is that they share one working copy, and what crosses the channel is information, never isolation.
143
+ - **Guards that were green without biting** — the owner-leakage scanner did not read `.html` at all; admitting the class immediately found a live `U+00AD` breaking a word visibly on a legally required page. `harness-audit` category 6 awarded **2/2** outside a git repository. Six consecutive CI pipelines were red because a test depended on untracked ledger files: locally 23/23, on CI `23 tests | 23 skipped`. Each is now closed by a mechanism, and the review panel found further holes inside the fixes themselves — including one fail-open state inside the fix that was closing fail-open.
139
144
 
140
- Previous line (v3.19.0): guard hardening and release mechanics six wrapper bypasses closed (#982), blocked-commands floor overlay (#972), session-lock ownership proof, and release as one dispatch (#978).
145
+ Previous line (v3.20.0): the memory pipeline 233 sessions of learnings that reached no agent (#1014), agent-authored text neutralised at the render point (#1015), and a learning store made durable after 11 of 13 provenance pointers resolved to nothing (#1017).
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