session-orchestrator 3.19.0 → 3.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/030-wave-execution.mdc +10 -8
- package/CHANGELOG.md +494 -0
- package/README.md +16 -11
- package/agents/analyst.md +1 -1
- package/agents/architect-reviewer.md +1 -1
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +1 -1
- package/agents/dialectic-deriver.md +1 -1
- package/agents/docs-writer.md +1 -1
- package/agents/memory-proposal-collector.md +1 -1
- package/agents/qa-strategist.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/session-reviewer.md +42 -1
- package/agents/skill-applied-judge.md +1 -1
- package/agents/test-writer.md +1 -1
- package/agents/ui-developer.md +1 -1
- package/agents/ux-evaluator.md +1 -1
- package/commands/release.md +60 -0
- package/commands/session.md +6 -2
- package/docs/USER-GUIDE.md +1 -1
- package/docs/instruction-delivery.md +350 -0
- package/docs/migration-v3.md +9 -6
- package/docs/persona-panel.md +3 -1
- package/docs/scope-collision-guard.md +167 -0
- package/docs/session-config-reference.md +1 -41
- package/docs/session-config-template.md +0 -23
- package/hooks/_lib/guard-source-loader.mjs +304 -91
- package/hooks/enforce-commands.mjs +216 -17
- package/hooks/enforce-scope.mjs +236 -12
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +11 -1
- package/hooks/on-session-end.mjs +52 -5
- package/hooks/on-session-start.mjs +7 -4
- package/hooks/on-stop.mjs +127 -12
- package/hooks/post-bash-write-verify.mjs +8 -32
- package/hooks/pre-bash-destructive-guard.mjs +146 -59
- package/hooks/pre-bash-sessions-ledger-guard.mjs +493 -66
- package/hooks/pre-task-scope-disjoint.mjs +1042 -0
- package/package.json +2 -2
- package/pi/prompts/release.md +12 -0
- package/scripts/autopilot.mjs +3 -1
- package/scripts/backfill-learnings-from-vault.mjs +967 -0
- package/scripts/emit-session.mjs +45 -40
- package/scripts/export-hw-learnings.mjs +61 -2
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/backlog-scan.mjs +106 -15
- package/scripts/lib/build-live-signals.mjs +7 -3
- package/scripts/lib/ci-status-banner.mjs +207 -23
- package/scripts/lib/command-blocker.mjs +322 -62
- package/scripts/lib/git-config-drift.mjs +471 -0
- package/scripts/lib/hardening.mjs +9 -9
- package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
- package/scripts/lib/io.mjs +193 -7
- package/scripts/lib/learnings/affinity.mjs +434 -0
- package/scripts/lib/learnings/candidates.mjs +736 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +408 -53
- package/scripts/lib/learnings/judgment.mjs +782 -0
- package/scripts/lib/learnings/kebab.mjs +128 -0
- package/scripts/lib/learnings/select.mjs +704 -0
- package/scripts/lib/memory-cleanup-stamp.mjs +132 -8
- package/scripts/lib/mirror-issues-banner.mjs +266 -0
- package/scripts/lib/named-vault-resolver.mjs +105 -16
- package/scripts/lib/peer-cards/schema.mjs +6 -2
- package/scripts/lib/reconcile/emitter.mjs +107 -22
- package/scripts/lib/reconcile/engine.mjs +9 -15
- package/scripts/lib/reconcile/renderer.mjs +141 -25
- package/scripts/lib/reconcile/sanitize.mjs +518 -0
- package/scripts/lib/reconcile/writer.mjs +134 -1
- package/scripts/lib/redact-spans.mjs +89 -0
- package/scripts/lib/scope-baseline.mjs +77 -17
- package/scripts/lib/scope-gate.mjs +852 -72
- package/scripts/lib/secret-masker.mjs +262 -0
- package/scripts/lib/session-close-backfill.mjs +2 -2
- package/scripts/lib/session-lock.mjs +34 -10
- package/scripts/lib/session-record-repair.mjs +551 -0
- package/scripts/lib/session-registry.mjs +9 -1
- package/scripts/lib/session-schema/serializer.mjs +54 -0
- package/scripts/lib/session-schema.mjs +1 -0
- package/scripts/lib/session-token-rollup.mjs +68 -6
- package/scripts/lib/soul-resolve.mjs +12 -0
- package/scripts/lib/state-md/mission-status.mjs +21 -12
- package/scripts/lib/tmux-layout/telemetry.mjs +43 -10
- package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
- package/scripts/lib/validate/check-agents.mjs +77 -5
- package/scripts/lib/validate/check-banner-parity.mjs +376 -0
- package/scripts/lib/validate/check-commands.mjs +2 -20
- package/scripts/lib/validate/check-doc-cli-commands.mjs +514 -0
- package/scripts/lib/validate/check-guard-requires-parity.mjs +1148 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +18 -0
- package/scripts/lib/validate/check-learning-provenance.mjs +511 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +188 -20
- package/scripts/lib/validate/check-rules.mjs +31 -5
- package/scripts/lib/validate/check-skills.mjs +191 -0
- package/scripts/lib/validate/check-test-git-config-target.mjs +665 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +22 -2
- package/scripts/lib/validate/check-untracked-test-deps.mjs +925 -0
- package/scripts/lib/validate/check-unwired-features.mjs +757 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +965 -0
- package/scripts/lib/validate/frontmatter-block.mjs +61 -0
- package/scripts/lib/validate/tier-inference.mjs +46 -8
- package/scripts/lib/vault-mirror/namespace.mjs +146 -1
- package/scripts/lib/vault-mirror/process.mjs +264 -31
- package/scripts/lib/vault-mirror/render-sessions.mjs +115 -4
- package/scripts/lib/vault-status/board-writer.mjs +300 -56
- package/scripts/lib/vault-status/narrative-mirror.mjs +119 -5
- package/scripts/lib/vcs-repo-spec.mjs +500 -19
- package/scripts/print-applicable-rules.mjs +170 -7
- package/scripts/print-learnings-index.mjs +501 -0
- package/scripts/release.mjs +616 -61
- package/scripts/repair-invalid-sessions.mjs +209 -0
- package/scripts/site-numbers.mjs +1049 -0
- package/scripts/sweep-expired-learnings.mjs +192 -32
- package/scripts/validate-plugin.mjs +82 -0
- package/scripts/validate-wave-scope.mjs +281 -12
- package/scripts/vault-mirror.mjs +26 -1
- package/skills/_shared/monitor-patterns.md +24 -4
- package/skills/_shared/state-ownership.md +17 -0
- package/skills/brainstorm/soul.md +47 -1
- package/skills/claude-md-drift-check/SKILL.md +9 -1
- package/skills/debug/SKILL.md +4 -1
- package/skills/discovery/issue-templates.md +4 -4
- package/skills/discovery/probes-code.md +2 -2
- package/skills/discovery/probes-feature.md +6 -6
- package/skills/discovery/probes-infra.md +2 -2
- package/skills/discovery/probes-session.md +5 -5
- package/skills/dispatcher/SKILL.md +10 -1
- package/skills/evolve/SKILL.md +116 -18
- package/skills/frontmatter-guard/SKILL.md +9 -1
- package/skills/gitlab-ops/SKILL.md +54 -39
- package/skills/gitlab-portfolio/SKILL.md +10 -1
- package/skills/grill/soul.md +44 -1
- package/skills/memory-cleanup/SKILL.md +18 -5
- package/skills/npm-publish/SKILL.md +22 -50
- package/skills/persona-panel/SKILL.md +3 -1
- package/skills/plan/mode-new.md +23 -5
- package/skills/plan/soul.md +46 -3
- package/skills/repo-audit/SKILL.md +10 -1
- package/skills/session-end/SKILL.md +45 -26
- package/skills/session-end/metrics-collection.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +30 -1
- package/skills/session-end/plan-verification.md +1 -5
- package/skills/session-end/session-metrics-write.md +6 -10
- package/skills/session-plan/SKILL.md +2 -2
- package/skills/session-plan/wave-template.md +1 -1
- package/skills/session-start/SKILL.md +15 -1
- package/skills/session-start/soul.md +41 -1
- package/skills/spinout/SKILL.md +5 -1
- package/skills/sunset-review/SKILL.md +11 -1
- package/skills/tmux-layout/SKILL.md +7 -2
- package/skills/vault-mirror/SKILL.md +10 -1
- package/skills/vault-sync/SKILL.md +10 -1
- package/skills/vault-sync/validator.mjs +55 -6
- package/skills/wave-executor/SKILL.md +1 -5
- package/skills/wave-executor/wave-loop.md +77 -82
- 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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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).
|
|
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,500 @@ 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
|
+
|
|
424
|
+
## [3.20.0] - 2026-08-13
|
|
425
|
+
|
|
426
|
+
Memory-pipeline line. The learning store had been accumulating for 233 sessions and delivering
|
|
427
|
+
to nobody: 100 learnings, and not one read path into a dispatched agent. Closing that gap
|
|
428
|
+
surfaced two defects underneath it — agent-authored text reaching every agent's project
|
|
429
|
+
instructions unfiltered, and a prune path that deleted learnings into neither the store nor
|
|
430
|
+
the archive. The headline is not a feature; it is that the review panel then found the same
|
|
431
|
+
class **inside the fix**: this line hardened one delivery channel and shipped a second,
|
|
432
|
+
unhardened one alongside it.
|
|
433
|
+
|
|
434
|
+
### Added
|
|
435
|
+
|
|
436
|
+
- **Learnings reach wave agents (#1014)** — a per-agent index in the dispatch prompt, selected
|
|
437
|
+
from the agent's own declared file scope. Two-tier with **split** caps (scope-matched, then
|
|
438
|
+
top-scored fill), because only 17 of 100 learnings carry `file_paths` and a single shared cap
|
|
439
|
+
lets the global tier crowd out the per-agent guarantee that is the acceptance criterion.
|
|
440
|
+
Rides the channel the coordinator already composes — `docs/instruction-delivery.md` §5
|
|
441
|
+
measured a *separate* injection path at **+72.3%**; this one costs **+0.69% to +1.15%** of a
|
|
442
|
+
178,096-byte baseline. Factor 92. `LEARNINGS_INDEX_MAX_CHARS = 2000` is derived (1.12% of the
|
|
443
|
+
measured baseline, 0.92× the median rule file), not chosen. Delivery is observable rather than
|
|
444
|
+
assumed: `orchestrator.learnings.index.injected` carries entry count, scope-matched split and
|
|
445
|
+
byte size, so "did the injector run?" is a grep instead of an inference.
|
|
446
|
+
- **Semantic dedup + contradiction detection (#1016)** — IDF-Dice top-K candidate pool
|
|
447
|
+
(K=8, FLOOR=0.085), per-seed, bounded, deliberately **non-transitive**: even at K=3 the
|
|
448
|
+
similarity graph collapses into a 99-of-100 giant component, so a clustering pass returns
|
|
449
|
+
"the corpus". `type` neither filters nor boosts — both strongest ground-truth links are
|
|
450
|
+
cross-type, and a type gate drops connectivity 6/6 → 4/6 while still retaining 26% of pairs.
|
|
451
|
+
A German stoplist is mandatory, not cosmetic: without it the top-scoring pair in the whole
|
|
452
|
+
corpus is two records sharing function words. The judgment layer is fail-closed and atomic
|
|
453
|
+
(one malformed decision voids the batch, never the valid subset), and enforces structurally
|
|
454
|
+
that **rendering an AUQ from an unreadable judgment is itself a write** — the AUQ renderer
|
|
455
|
+
sits in the same effect map as the archive writer, behind one gate.
|
|
456
|
+
- **`scripts/print-learnings-index.mjs`**, **`scripts/backfill-learnings-from-vault.mjs`**,
|
|
457
|
+
**`scripts/lib/validate/check-learning-provenance.mjs`**, and a `--prune` mode on
|
|
458
|
+
`sweep-expired-learnings.mjs`. Five new leaf modules under `scripts/lib/learnings/`
|
|
459
|
+
(`affinity`, `select`, `candidates`, `judgment`, `kebab`).
|
|
460
|
+
|
|
461
|
+
### Fixed
|
|
462
|
+
|
|
463
|
+
- **Agent-authored text is neutralised at the render point (#1015)** — the reconcile renderer
|
|
464
|
+
interpolated it verbatim into `.claude/rules/<slug>.md`, a file Claude Code then delivers to
|
|
465
|
+
every agent in every session as a project instruction, with no revocation: the frontmatter
|
|
466
|
+
gates are not a containment boundary for the body. Machine values now **reject** (a repaired
|
|
467
|
+
`learning-key` breaks idempotency; a dropped `globs[]` element can leave `globs: []`, which
|
|
468
|
+
the loader excludes everywhere — silently dead beats loudly rejected); prose is framed and
|
|
469
|
+
capped without a meaning filter, because a blocklist is the guard that looks green and does
|
|
470
|
+
not bite. Two premises in the issue text did not survive verification and are corrected in
|
|
471
|
+
the issue: `description` was already defended against newlines, and the frontmatter parser is
|
|
472
|
+
hand-rolled — so the newline is the *only* escape and a YAML-shaped sanitiser would have been
|
|
473
|
+
both wrong and destructive (14 `description` lines in the corpus carry a second colon).
|
|
474
|
+
- **The learning store is durable again (#1017)** — `/evolve` pruned by rewriting the store with
|
|
475
|
+
**no archive append**; 11 of 13 provenance pointers in generated rules resolved to nothing, in
|
|
476
|
+
neither store nor archive nor any backup. Both callers now share one `archiveThenRewrite()`
|
|
477
|
+
(KEEP-probe, archive **before** rewrite so a crash leaves the record in both places and never
|
|
478
|
+
in neither, `.bak` snapshot), and `pruneLearnings()` archives any record the caller's next
|
|
479
|
+
generation omits — reconciled by `id`, or by content fingerprint when a record carries no
|
|
480
|
+
usable one. **11 of 11 lost records were recovered** from the vault mirror with per-field
|
|
481
|
+
origin labelling; dangling pointers **11 → 0**.
|
|
482
|
+
- **Rule-block delivery boundaries were unrecoverable without any adversarial input** — rule
|
|
483
|
+
`content` is byte-identical to disk *including* its YAML fence, so joining rules on `---`
|
|
484
|
+
produced 56 separator-shaped lines for 18 rules. Replaced with per-rule fence tags carrying a
|
|
485
|
+
content-derived token and the source path (information an agent previously could not get at
|
|
486
|
+
all). An existing test had pinned the broken contract.
|
|
487
|
+
- **`learning_key` was derived five ways** on a *stored* contract, with the writer kebabbing the
|
|
488
|
+
`type` half and all four readers not. One shared derivation now; proven byte-identical across
|
|
489
|
+
107 records, all 13 stamped keys reproduced.
|
|
490
|
+
- **Two tracked `.mjs` files carried NUL bytes** and were therefore skipped **silently** by every
|
|
491
|
+
grep-based audit — exit 1, no output, which reads exactly like a clean result.
|
|
492
|
+
|
|
493
|
+
### Notes
|
|
494
|
+
|
|
495
|
+
- The review panel (security, qa, architect, independently) returned **FIX_REQUIRED** with four
|
|
496
|
+
HIGH blockers; all four were closed in one fix cycle, which found three further holes of the
|
|
497
|
+
same class. Every new guard is proven by fake regression, not by a green test.
|
|
498
|
+
- `CANDIDATE_FLOOR` is deliberately **unchanged** despite its justification no longer
|
|
499
|
+
reproducing: this line's own backfill moved every IDF weight, and the wave-1 ground-truth
|
|
500
|
+
labels were never persisted. A constant whose evidence has evaporated is not re-guessed from
|
|
501
|
+
the evaporated evidence (#1021).
|
|
502
|
+
- Follow-ups: #1018, #1019, #1020, #1021 — each with a named revisit trigger.
|
|
503
|
+
|
|
10
504
|
## [3.19.0] - 2026-08-04
|
|
11
505
|
|
|
12
506
|
Guard-hardening and release-mechanics line. Headline: the destructive-command guard closed
|