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
|
@@ -415,6 +415,337 @@ appears.
|
|
|
415
415
|
|
|
416
416
|
---
|
|
417
417
|
|
|
418
|
+
## 7. The learnings index is not a second delivery path (GitLab #1014)
|
|
419
|
+
|
|
420
|
+
> Measured **2026-08-13** at commit `c87102b`, branch `feat/memory-pipeline-1015-1014-1016`.
|
|
421
|
+
> `.claude/rules/` and `CLAUDE.md` were **clean at HEAD** for every measurement below
|
|
422
|
+
> (`git status --porcelain -- .claude/rules CLAUDE.md` → 0 lines); the dirty paths in the
|
|
423
|
+
> working tree were sibling agents' `scripts/` and `tests/` files, none of which this
|
|
424
|
+
> section measures.
|
|
425
|
+
|
|
426
|
+
§5 recommends against building a second delivery path, because injecting a scoped rule
|
|
427
|
+
block alongside undiminished native loading costs **+72%**. Issue #1014 then shipped an
|
|
428
|
+
injected block. This section exists to answer the obvious question — *is the learnings
|
|
429
|
+
index the thing §5 forbids?* — with numbers rather than with an assurance.
|
|
430
|
+
|
|
431
|
+
**It is not, by a factor of 92.** The forbidden path re-sends 122,875 bytes the agent is
|
|
432
|
+
already receiving. The index sends **1,405–1,727 bytes the agent receives nowhere else**:
|
|
433
|
+
**+0.79% to +0.97%** of the re-measured baseline.
|
|
434
|
+
|
|
435
|
+
> **⚠ Amendment, 2026-08-13 — every byte figure in §7 below is PRE-FRAMING.**
|
|
436
|
+
>
|
|
437
|
+
> §7 was measured at `c87102b`. A review panel then found that this block shipped
|
|
438
|
+
> agent-authored text with none of the neutralisation `<APPLICABLE-RULES>` had received in
|
|
439
|
+
> the same session — no fence, no wrapper-forgery rejection, no invisible-character
|
|
440
|
+
> stripping. The fix added a content-derived block fence and a framing preamble, at a
|
|
441
|
+
> **constant +320 B** independent of entry count.
|
|
442
|
+
>
|
|
443
|
+
> Re-measured by the coordinator after the fix (2 scopes, `wc -c` on the real CLI):
|
|
444
|
+
> `scripts/lib/reconcile/**` → **2,047 B** (was 1,727); `docs/**` → **1,234 B** (was 914).
|
|
445
|
+
>
|
|
446
|
+
> | | §7 as measured | after framing |
|
|
447
|
+
> |---|---:|---:|
|
|
448
|
+
> | band | 912–1,727 B | **1,234–2,047 B** |
|
|
449
|
+
> | share of the 178,096 B baseline | +0.51% – +0.97% | **+0.69% – +1.15%** |
|
|
450
|
+
> | ceiling (entry caps lifted) | 2,221 B / +1.25% | **2,541 B / +1.43%** |
|
|
451
|
+
>
|
|
452
|
+
> The 92× argument is unaffected — the comparison is against +122,875 B, and +320 B does not
|
|
453
|
+
> move it. `LEARNINGS_INDEX_MAX_CHARS = 2000` is likewise untouched: the framing sits outside
|
|
454
|
+
> the capped body, as the header and retrieval pointer already did.
|
|
455
|
+
>
|
|
456
|
+
> The per-scope table in §7.2 and the figures in §7.4 are left at their measured values rather
|
|
457
|
+
> than overwritten, so each keeps the SHA it was taken at. Read them as pre-framing.
|
|
458
|
+
|
|
459
|
+
### 7.1 The baseline, re-measured — and it did not drift
|
|
460
|
+
|
|
461
|
+
The wave-1 baseline was captured at `0fbea29`. Re-measuring at `c87102b` (3 commits later,
|
|
462
|
+
`git rev-list --count 0fbea29..HEAD` → `3`) gives a **byte-identical** rule corpus:
|
|
463
|
+
|
|
464
|
+
```console
|
|
465
|
+
$ find .claude/rules -name '*.md' | wc -l
|
|
466
|
+
29
|
|
467
|
+
$ find .claude/rules -name '*.md' -exec cat {} + | wc -c
|
|
468
|
+
165097
|
|
469
|
+
$ wc -c CLAUDE.md
|
|
470
|
+
9666
|
|
471
|
+
$ wc -c ~/.claude/CLAUDE.md
|
|
472
|
+
1220
|
|
473
|
+
$ sed -n '266,286p' skills/wave-executor/SKILL.md | wc -c # the fenced memory.propose block
|
|
474
|
+
2113
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
| Component | Bytes @ `0fbea29` | Bytes @ `c87102b` | Drift | Fires |
|
|
478
|
+
|---|---:|---:|---:|---|
|
|
479
|
+
| `.claude/rules/**` (29 files) | 165,097 | 165,097 | 0 | every agent, native project-instruction loading |
|
|
480
|
+
| `CLAUDE.md` / `AGENTS.md` (root) | 9,666 | 9,666 | 0 | every agent |
|
|
481
|
+
| `~/.claude/CLAUDE.md` (user-level) | 1,220 | 1,220 | 0 | every agent on this host, outside repo control |
|
|
482
|
+
| `memory.propose` boilerplate | 2,112 | **2,113** | +1 | every Impl-Core / Impl-Polish / Quality agent |
|
|
483
|
+
| **TOTAL** | **178,095** | **178,096** | **+1** | per Impl-wave agent, before task text |
|
|
484
|
+
|
|
485
|
+
The corpus figures are reproducible from the git object store rather than from the working
|
|
486
|
+
tree, which is what makes the zero-drift claim checkable:
|
|
487
|
+
|
|
488
|
+
```console
|
|
489
|
+
$ git ls-tree -r --name-only 0fbea29 -- .claude/rules | grep '\.md$' \
|
|
490
|
+
| while read f; do git cat-file blob "0fbea29:$f"; done | wc -c
|
|
491
|
+
165097
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
The single byte is the boilerplate's trailing newline: the block is lines 266–286 of
|
|
495
|
+
`skills/wave-executor/SKILL.md`, and wave 1 evidently measured it without the final `\n`.
|
|
496
|
+
Recording it rather than rounding it away is the point — a figure that reproduces to ±1
|
|
497
|
+
byte across two SHAs is a measurement; one that "matches" is an assertion.
|
|
498
|
+
|
|
499
|
+
**The rules corpus did not change this session.** The 100-record learnings corpus did. Those
|
|
500
|
+
are different corpora, and only the second one is what §7 adds.
|
|
501
|
+
|
|
502
|
+
### 7.2 What the index actually costs, per scope
|
|
503
|
+
|
|
504
|
+
Four **disjoint** agent file scopes, each naming real tracked files
|
|
505
|
+
(`git ls-files --error-unmatch` → TRACKED for all eight paths):
|
|
506
|
+
|
|
507
|
+
```console
|
|
508
|
+
$ echo '["hooks/pre-bash-destructive-guard.mjs","hooks/on-session-start.mjs"]' > /tmp/scope-hooks.json
|
|
509
|
+
$ node scripts/print-learnings-index.mjs --file-scope /tmp/scope-hooks.json --no-event | wc -c
|
|
510
|
+
1405
|
|
511
|
+
$ node scripts/print-learnings-index.mjs --file-scope /tmp/scope-hooks.json --no-event --json # count + scopeMatched
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
| Agent file scope | Bytes | Entries | Scoped | Global fill | % of 178,096 |
|
|
515
|
+
|---|---:|---:|---:|---:|---:|
|
|
516
|
+
| `hooks/**` (2 files) | 1,405 | 7 | 3 | 4 | +0.789% |
|
|
517
|
+
| `scripts/lib/reconcile/**` (2 files) | 1,727 | 9 | 5 | 4 | +0.970% |
|
|
518
|
+
| `tests/**` (2 files) | 912 | 4 | 0 | 4 | +0.512% |
|
|
519
|
+
| `docs/**` (2 files — this agent's own scope) | 914 | 4 | 0 | 4 | +0.513% |
|
|
520
|
+
| *control:* `tests/**` naming two paths the corpus does carry | 1,240 | 6 | 2 | 4 | +0.696% |
|
|
521
|
+
|
|
522
|
+
The corpus behind these numbers, measured rather than quoted:
|
|
523
|
+
|
|
524
|
+
```console
|
|
525
|
+
$ wc -l < .orchestrator/metrics/learnings.jsonl
|
|
526
|
+
100
|
|
527
|
+
$ node -e '…JSON.parse each line; count Array.isArray(r.file_paths) && r.file_paths.length>0…'
|
|
528
|
+
total=100 with_file_paths=17 pct=17.0%
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
**17 of 100 records carry `file_paths`.** That fraction, not the CLI, is what governs the
|
|
532
|
+
scoped column — and the `tests/**` row is the honest demonstration. It returned **0 scoped
|
|
533
|
+
matches** even though the corpus holds 6 `tests/` path entries, because those 6 name
|
|
534
|
+
different files (`tests/lib/session-registry.test.mjs`,
|
|
535
|
+
`tests/scripts/gates/gate-helpers.test.mjs`, …) than the two a plausible agent declared.
|
|
536
|
+
The control row re-runs the same scope against two paths the corpus *does* carry and
|
|
537
|
+
recovers 2 scoped matches. Matching is exact-path; the 0 is correct behaviour, not a defect.
|
|
538
|
+
|
|
539
|
+
The practical reading: **at 17% `file_paths` coverage, a majority of scopes will fall back
|
|
540
|
+
entirely to the global fill.** The index degrades to "top 4 general learnings" rather than
|
|
541
|
+
to nothing, which is the right failure mode, but it is not yet per-agent for most agents.
|
|
542
|
+
The lever is `--file-paths` adoption at proposal time, not the selector.
|
|
543
|
+
|
|
544
|
+
### 7.3 The delta, and the comparison that settles the question
|
|
545
|
+
|
|
546
|
+
Absolute: **+912 to +1,727 bytes**, ceiling **+2,221** (§7.4). As a share of the
|
|
547
|
+
re-measured 178,096-byte baseline: **+0.51% to +0.97%**, ceiling **+1.25%**.
|
|
548
|
+
|
|
549
|
+
| Path | Added bytes | As % of baseline |
|
|
550
|
+
|---|---:|---:|
|
|
551
|
+
| §5's forbidden path — scoped rule block alongside native delivery | +122,875 | **+72.3%** |
|
|
552
|
+
| §7's learnings index — measured worst case of four scopes | +1,727 | **+0.97%** |
|
|
553
|
+
|
|
554
|
+
**92×.** Two structural facts produce that gap, and each is checkable:
|
|
555
|
+
|
|
556
|
+
1. **It rides a channel the repo already writes itself.** The index is prepended to the
|
|
557
|
+
dispatch prompt — the same channel that already carries the task text and the 2,113-byte
|
|
558
|
+
`memory.propose` boilerplate. It does not add a delivery mechanism; it adds a block to a
|
|
559
|
+
prompt the coordinator was already composing. §5's +72% is entirely the cost of
|
|
560
|
+
*duplicating* a channel that keeps delivering regardless.
|
|
561
|
+
2. **Learnings have no native delivery to duplicate.** `.orchestrator/metrics/learnings.jsonl`
|
|
562
|
+
is not a `.md` file under `.claude/rules/`, and neither `CLAUDE.md` nor its Codex-CLI
|
|
563
|
+
alias `AGENTS.md` imports it (`grep -c "^@" CLAUDE.md` → `0`, unchanged from §1). Nothing
|
|
564
|
+
in the 178,096-byte baseline carries this content. The index is the *first* delivery of
|
|
565
|
+
it, not the second.
|
|
566
|
+
|
|
567
|
+
#### 7.3.1 …except for 13 records, and the index does not exclude them
|
|
568
|
+
|
|
569
|
+
Fact 2 is true of the corpus but not of every record in it. `/reconcile` converts qualifying
|
|
570
|
+
learnings into `.claude/rules/*.md` files — which **are** natively delivered. Those rules
|
|
571
|
+
carry their source in provenance, so the overlap is measurable:
|
|
572
|
+
|
|
573
|
+
```console
|
|
574
|
+
$ grep -rlha "learning-id:" .claude/rules/*.md | wc -l
|
|
575
|
+
13
|
|
576
|
+
$ grep -rha "^- learning-id:" .claude/rules/*.md | sed 's/.*`\(.*\)`.*/\1/' > /tmp/rule-lids.txt
|
|
577
|
+
$ # then: for each scope, intersect the index's learning ids with /tmp/rule-lids.txt
|
|
578
|
+
hooks: entries=7 already_a_rule=1
|
|
579
|
+
scripts: entries=9 already_a_rule=0
|
|
580
|
+
tests: entries=4 already_a_rule=0
|
|
581
|
+
docs: entries=4 already_a_rule=1
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
**13 of 100 learnings have already become natively-delivered rules, and the selector does
|
|
585
|
+
not filter them out.** In two of four scopes, one indexed entry was content the agent was
|
|
586
|
+
already receiving in full — a genuine, if small, second delivery of that record. The
|
|
587
|
+
duplicated unit is one ~150-byte index line against a multi-KB rule file, so the waste is
|
|
588
|
+
bounded and far below the measurement noise of §7.1; it does not change the +0.97% figure
|
|
589
|
+
or the 92× conclusion. But it is a real instance of the exact pattern §5 forbids, found by
|
|
590
|
+
looking for it, and it is cheap to close: the selector has each record's id and the rule
|
|
591
|
+
files carry theirs. **Filed as a follow-up rather than fixed here — this section's file
|
|
592
|
+
scope is documentation, and the fix is a change to `scripts/lib/learnings/select.mjs`.**
|
|
593
|
+
|
|
594
|
+
### 7.4 The 2,000-character cap is slack; the entry-count caps bind
|
|
595
|
+
|
|
596
|
+
`LEARNINGS_INDEX_MAX_CHARS = 2000` (`scripts/lib/learnings/select.mjs:89`) is a hard cap on
|
|
597
|
+
the rendered **body**. Under the shipped defaults it is never reached:
|
|
598
|
+
|
|
599
|
+
```console
|
|
600
|
+
$ node scripts/print-learnings-index.mjs --file-scope /tmp/scope-hooks.json --no-event | wc -c
|
|
601
|
+
1405
|
|
602
|
+
$ node scripts/print-learnings-index.mjs --file-scope /tmp/scope-hooks.json --no-event \
|
|
603
|
+
--max-scoped 100 --max-global 100 | wc -c
|
|
604
|
+
2221
|
|
605
|
+
$ node scripts/print-learnings-index.mjs --file-scope /tmp/scope-hooks.json --no-event \
|
|
606
|
+
--max-scoped 100 --max-global 100 --max-chars 100000 | wc -c
|
|
607
|
+
15588
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
| Constraint | Result | Binds? |
|
|
611
|
+
|---|---:|---|
|
|
612
|
+
| default caps (8 scoped / 4 global / 2,000 chars) | 1,405–1,727 B | entry caps bind |
|
|
613
|
+
| entry caps lifted, char cap at 2,000 | 2,221 B | **char cap binds** |
|
|
614
|
+
| both lifted (whole corpus as an index) | 15,588 B | nothing binds |
|
|
615
|
+
|
|
616
|
+
The binding constraint under defaults is **`--max-global 4`**, not the char cap. The
|
|
617
|
+
telemetry's `truncated: true` says so precisely: it is set at `select.mjs:389` by
|
|
618
|
+
`scoped.length > maxScoped || global.length > maxGlobal` — an **entry-count** overflow, not
|
|
619
|
+
a character overflow. Reading `truncated: true` on a 1,405-byte block as "the 2,000 cap bit"
|
|
620
|
+
would be wrong.
|
|
621
|
+
|
|
622
|
+
Slack, stated plainly: **816 bytes of body headroom on the `hooks` scope, 494 on `scripts`**
|
|
623
|
+
(the rendered block carries a 221-byte header outside the capped body, which is why the
|
|
624
|
+
lifted-caps run reports 2,221 rather than 2,000). The cap is a backstop against a corpus
|
|
625
|
+
that grows an order of magnitude — at 100 records it never fires. The number that would
|
|
626
|
+
make it fire is 15,588: the whole corpus rendered as an index is **7.8×** the cap, so the
|
|
627
|
+
cap is doing real work as a ceiling even while slack today.
|
|
628
|
+
|
|
629
|
+
### 7.5 The per-agent claim, verified empirically
|
|
630
|
+
|
|
631
|
+
Not read off the code — run, on two disjoint scopes, comparing the emitted subjects:
|
|
632
|
+
|
|
633
|
+
```console
|
|
634
|
+
$ # hooks scope
|
|
635
|
+
## Learnings Index (selected for your file scope)
|
|
636
|
+
|
|
637
|
+
7 entries (3 matched your declared file scope, 4 general). One line each — this is an INDEX, not the corpus.
|
|
638
|
+
Full text of any line: `grep -F '"subject":"<subject>"' .orchestrator/metrics/learnings.jsonl`
|
|
639
|
+
|
|
640
|
+
- anti-pattern/an inline @returns-never warn helper at a rule-loop warn site flips a later block to ALLOW: …
|
|
641
|
+
- anti-pattern/policy rule with a new type field must ship with its consuming hook branch in the same change: …
|
|
642
|
+
- anti-pattern/release() proof gate hinges on proof !== undefined — every call site must spread-guard: …
|
|
643
|
+
[4 more]
|
|
644
|
+
|
|
645
|
+
$ # scripts scope
|
|
646
|
+
## Learnings Index (selected for your file scope)
|
|
647
|
+
|
|
648
|
+
9 entries (5 matched your declared file scope, 4 general). One line each — this is an INDEX, not the corpus.
|
|
649
|
+
Full text of any line: `grep -F '"subject":"<subject>"' .orchestrator/metrics/learnings.jsonl`
|
|
650
|
+
|
|
651
|
+
- anti-pattern/A hardcoded expected value that coincides with a clamp/floor for one day is a green that proves nothing: …
|
|
652
|
+
- proven-pattern/security-floor policy loaders must merge, not first-hit-resolve: …
|
|
653
|
+
- recurring-issue/Eine Session ohne durchgelaufenes /close hinterlaesst unpushed Commits …
|
|
654
|
+
[6 more]
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
Set-compared rather than eyeballed:
|
|
658
|
+
|
|
659
|
+
```console
|
|
660
|
+
$ # intersect the two --json subject lists
|
|
661
|
+
hooks entries: 7 | scripts entries: 9
|
|
662
|
+
intersection: 2 | hooks-only: 5 | scripts-only: 7 | Jaccard: 0.143
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
**Two disjoint scopes share 2 of 14 distinct entries (Jaccard 0.143).** Both shared entries
|
|
666
|
+
are global fill; the scoped selections are fully disjoint. The block is genuinely
|
|
667
|
+
per-agent, not a constant wearing a scope's name — which is the claim §3.1 could *not* make
|
|
668
|
+
about glob-scoped rules, where the "scoped" saving turned out to be a constant 40,254 bytes
|
|
669
|
+
of tier filtering.
|
|
670
|
+
|
|
671
|
+
### 7.6 The telemetry answers "did the injector ever run?"
|
|
672
|
+
|
|
673
|
+
§1 could establish that `rule-loader.mjs` does not run at delivery time only by grepping for
|
|
674
|
+
the absence of a caller. That is an argument from silence, and §1.2 records how it failed:
|
|
675
|
+
a census keyed on the payload missed the prose call site entirely. The index does not
|
|
676
|
+
require that argument — it emits an event:
|
|
677
|
+
|
|
678
|
+
```console
|
|
679
|
+
$ grep -ac 'orchestrator.learnings.index.injected' .orchestrator/metrics/events.jsonl
|
|
680
|
+
1
|
|
681
|
+
$ node scripts/print-learnings-index.mjs --file-scope /tmp/scope-hooks.json > /dev/null
|
|
682
|
+
$ grep -ac 'orchestrator.learnings.index.injected' .orchestrator/metrics/events.jsonl
|
|
683
|
+
2
|
|
684
|
+
$ grep -a 'orchestrator.learnings.index.injected' .orchestrator/metrics/events.jsonl | tail -1
|
|
685
|
+
{
|
|
686
|
+
"timestamp": "2026-08-13T05:53:29.824Z",
|
|
687
|
+
"event": "orchestrator.learnings.index.injected",
|
|
688
|
+
"count": 7,
|
|
689
|
+
"scope_matched": 3,
|
|
690
|
+
"global_count": 4,
|
|
691
|
+
"candidates": 94,
|
|
692
|
+
"truncated": true,
|
|
693
|
+
"bytes": 1405,
|
|
694
|
+
"scope_source": "file-scope"
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
The counter moves 1 → 2 on one invocation, and the record's `bytes: 1405` is independently
|
|
699
|
+
equal to this section's `wc -c`. "Did it run, on what scope, and how big was it" is now a
|
|
700
|
+
`grep` over `events.jsonl` instead of an inference. `candidates: 94` also shows the pool is
|
|
701
|
+
the 94 *active* records of the 100 on disk — 6 are filtered before ranking.
|
|
702
|
+
|
|
703
|
+
### 7.7 What is NOT established — explicitly unbelegt
|
|
704
|
+
|
|
705
|
+
Four gaps. Each would move the denominator, and none is closed by anything above.
|
|
706
|
+
|
|
707
|
+
- **Whether this Claude Code build loads a root `AGENTS.md`.** One exists on disk —
|
|
708
|
+
`ls -la AGENTS.md` → 14,661 bytes, **untracked** (`git ls-files AGENTS.md | wc -l` → `0`).
|
|
709
|
+
The coordinator of this session confirmed from its own context window that it is *not*
|
|
710
|
+
delivered to the coordinator. **Whether a dispatched subagent receives it is unverified**,
|
|
711
|
+
and this agent cannot settle it: a subagent introspecting its own context is the
|
|
712
|
+
self-report §1.1 already flagged as corroborating-not-load-bearing. If subagents do
|
|
713
|
+
receive it, the baseline is 192,757 rather than 178,096 and the index's share *falls* to
|
|
714
|
+
+0.73%. Tracked as issue **#973**.
|
|
715
|
+
- **Whether the `<APPLICABLE-RULES>` block fires in practice.** `wave-loop.md` makes the
|
|
716
|
+
pre-dispatch injection a SHOULD, not a gate. In **this** session it was deliberately
|
|
717
|
+
skipped and logged as a deviation, so the 178,096 figure **excludes it**. If it fires, the
|
|
718
|
+
denominator grows by up to the 122,875 bytes §3 measured for a live wave scope and the
|
|
719
|
+
index's share falls correspondingly — to roughly +0.57%. Every percentage in §7.3 is
|
|
720
|
+
therefore a *conservative* upper bound on the index's share: the honest reading is
|
|
721
|
+
"≤1% under the smallest defensible denominator".
|
|
722
|
+
- **Whether 17% `file_paths` coverage is representative.** It is the coverage of a 100-record
|
|
723
|
+
corpus, 11 of whose records were recovered from a data-loss incident. Whether the recovered
|
|
724
|
+
records are systematically poorer in `file_paths` than organically-proposed ones was not
|
|
725
|
+
measured, and it would bias the scoped column if so.
|
|
726
|
+
- **Whether the §7.3.1 rule/learning overlap grows.** Measured once, at 13/100, on four
|
|
727
|
+
scopes. `/reconcile` promotes learnings into rules continuously, so this ratio rises by
|
|
728
|
+
construction. Nothing currently measures it on a schedule.
|
|
729
|
+
|
|
730
|
+
### 7.8 Correction to §1.3
|
|
731
|
+
|
|
732
|
+
§1.3 records a second delivery source outside this repo — the parent workspace's
|
|
733
|
+
`.claude/rules/parallel-sessions.md`, 5,623 bytes — and concludes the real delivered corpus
|
|
734
|
+
is 175,584 rather than 169,961. **That file no longer exists on this host:**
|
|
735
|
+
|
|
736
|
+
```console
|
|
737
|
+
$ ls -la "$(dirname "$PWD")"/.claude/rules/
|
|
738
|
+
ls: /Users/…/Projects/.claude/rules/: No such file or directory
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
§7's baseline therefore does not carry a parent-workspace term. This is a host-local
|
|
742
|
+
observation about one machine at one date, not proof the mechanism is gone — the directory
|
|
743
|
+
could be recreated at any time, and §1.3's structural point (that
|
|
744
|
+
`print-applicable-rules.mjs` reads only `<repoRoot>/.claude/rules` and cannot see such a
|
|
745
|
+
file) stands unchanged.
|
|
746
|
+
|
|
747
|
+
---
|
|
748
|
+
|
|
418
749
|
## Reproducing this document
|
|
419
750
|
|
|
420
751
|
```bash
|
|
@@ -434,6 +765,23 @@ node scripts/print-applicable-rules.mjs --wave-scope /tmp/scope.json | wc -c #
|
|
|
434
765
|
|
|
435
766
|
# blast radius
|
|
436
767
|
git grep -l "\.claude/rules/" | wc -l
|
|
768
|
+
|
|
769
|
+
# §7 — baseline re-measure, reproducible from the object store at any SHA
|
|
770
|
+
git ls-tree -r --name-only <sha> -- .claude/rules | grep '\.md$' \
|
|
771
|
+
| while read f; do git cat-file blob "<sha>:$f"; done | wc -c
|
|
772
|
+
sed -n '266,286p' skills/wave-executor/SKILL.md | wc -c # memory.propose boilerplate
|
|
773
|
+
|
|
774
|
+
# §7 — the learnings index for one agent scope (never writes; --no-event suppresses telemetry)
|
|
775
|
+
echo '["hooks/on-session-start.mjs"]' > /tmp/scope.json
|
|
776
|
+
node scripts/print-learnings-index.mjs --file-scope /tmp/scope.json --no-event | wc -c
|
|
777
|
+
node scripts/print-learnings-index.mjs --file-scope /tmp/scope.json --no-event --json
|
|
778
|
+
|
|
779
|
+
# §7.4 — which cap binds
|
|
780
|
+
node scripts/print-learnings-index.mjs --file-scope /tmp/scope.json --no-event \
|
|
781
|
+
--max-scoped 100 --max-global 100 --max-chars 100000 | wc -c
|
|
782
|
+
|
|
783
|
+
# §7.6 — did the injector run?
|
|
784
|
+
grep -ac 'orchestrator.learnings.index.injected' .orchestrator/metrics/events.jsonl
|
|
437
785
|
```
|
|
438
786
|
|
|
439
787
|
## See also
|
|
@@ -442,3 +790,5 @@ git grep -l "\.claude/rules/" | wc -l
|
|
|
442
790
|
- `scripts/print-applicable-rules.mjs` — the CLI bridge; reads only `<repoRoot>/.claude/rules`
|
|
443
791
|
- `scripts/lib/instruction-budget-guard.mjs` — the existing measurement + ceiling instrument
|
|
444
792
|
- `docs/rule-authoring.md` — frontmatter contract (and the stale claim at line 8)
|
|
793
|
+
- `scripts/print-learnings-index.mjs` — §7's per-agent index CLI; emits `orchestrator.learnings.index.injected`
|
|
794
|
+
- `scripts/lib/learnings/select.mjs` — the selector; `LEARNINGS_INDEX_MAX_CHARS = 2000` at line 89, `truncated` at line 389
|
package/docs/migration-v3.md
CHANGED
|
@@ -32,12 +32,15 @@ Optional but recommended:
|
|
|
32
32
|
### 3a. Claude Code
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
# 1.
|
|
36
|
-
|
|
37
|
-
git
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
# 1. Update the plugin — run this INSIDE Claude Code, not in a shell:
|
|
36
|
+
# /plugin update session-orchestrator@kanevry
|
|
37
|
+
# A marketplace-installed plugin lives in a managed cache, not in a git checkout,
|
|
38
|
+
# so `git pull` does not apply to it.
|
|
39
|
+
|
|
40
|
+
# 2. Install Node dependencies. Claude Code has no `plugin dir` subcommand,
|
|
41
|
+
# so resolve the install path from the cache:
|
|
42
|
+
SO_DIR="$(dirname "$(find ~/.claude/plugins/cache -path '*session-orchestrator*' -name package.json 2>/dev/null | head -1)")"
|
|
43
|
+
cd "$SO_DIR" && npm install
|
|
41
44
|
|
|
42
45
|
# 3. Restart Claude Code so hooks.json is re-read
|
|
43
46
|
```
|
package/docs/persona-panel.md
CHANGED
|
@@ -33,7 +33,9 @@ mkdir -p .claude/personas
|
|
|
33
33
|
Templates ship with the plugin at `templates/personas/`. Copy one as a starting point:
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
|
|
36
|
+
# Claude Code has no `plugin dir` subcommand — resolve the install path from the cache.
|
|
37
|
+
SO_DIR="$(dirname "$(find ~/.claude/plugins/cache -path '*session-orchestrator*' -name package.json 2>/dev/null | head -1)")"
|
|
38
|
+
cp "$SO_DIR/templates/personas/klima-physicist.v1.md" \
|
|
37
39
|
.claude/personas/klima-physicist.md
|
|
38
40
|
```
|
|
39
41
|
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Scope-Collision Guard — Pre-Dispatch File-Scope Deconfliction
|
|
2
|
+
|
|
3
|
+
> Reference for the mechanism that stops a wave from handing the SAME file to two agents (issue #1020).
|
|
4
|
+
> Four moving parts: the per-agent scope files, `scripts/validate-wave-scope.mjs` (`--assert-disjoint` / `--union`), `findScopeCollisions()` + `unionFileScopes()` in [`scripts/lib/scope-gate.mjs`](../scripts/lib/scope-gate.mjs), and the `PreToolUse` hook [`hooks/pre-task-scope-disjoint.mjs`](../hooks/pre-task-scope-disjoint.mjs).
|
|
5
|
+
> The coordinator-side **runbook** is `skills/wave-executor/wave-loop.md` § Scope Manifest 3.1–3.3 — this document does not restate it. What lives here instead: how the mechanism works, how it fails, what it deliberately does not see, and how to debug it.
|
|
6
|
+
|
|
7
|
+
## 1. What the pre-existing gates could not see
|
|
8
|
+
|
|
9
|
+
`assertFileScopeSubset()` (#796) checks each agent's scope **⊆** the wave's `allowedPaths` union, and `wave-scope-commit-guard` checks writes against that same union. A file claimed by two agents is a subset **twice over**, and the union grants it exactly once — so a double assignment is structurally invisible to both. It surfaced only afterwards, from an agent's own PSA-002 report (`findScopeCollisions()` header: `tests/scripts/sweep-expired-learnings-cli.test.mjs` handed to two agents of one wave). Per `.claude/rules/parallel-sessions.md` § Decision Tree, a file inside two declared scopes of one dispatch round is never a benign sibling signal — it is a deconfliction gap.
|
|
10
|
+
|
|
11
|
+
Two things follow, and both are the point of #1020:
|
|
12
|
+
|
|
13
|
+
- `allowedPaths` is **computed** from the per-agent declarations (`unionFileScopes()`), not transcribed by hand.
|
|
14
|
+
- Disjointness is asserted on the **declarations**, before the union exists.
|
|
15
|
+
|
|
16
|
+
## 2. The chain, in order
|
|
17
|
+
|
|
18
|
+
| # | Step | Artefact | Mechanism |
|
|
19
|
+
|---|------|----------|-----------|
|
|
20
|
+
| 1 | Declare | `<state-dir>/filescopes/wave-<N>/<agent-id>.json` (one per agent, plus `coordinator.json`) | written verbatim from the session plan |
|
|
21
|
+
| 2 | Assert disjointness | the sidecar array `[{id, files}, …]` | `validate-wave-scope.mjs --assert-disjoint` → `findScopeCollisions()` |
|
|
22
|
+
| 3 | Compute the union | stdout of `--union` → `allowedPaths` | `expandTestSiblings(unionFileScopes(scopes), { role })` |
|
|
23
|
+
| 4 | Inject | `FILE-SCOPE — exactly these:` + a fenced block in each agent prompt | the per-agent file from step 1 |
|
|
24
|
+
| 5 | Dispatch | `.orchestrator/wave-dispatch-scopes.json` (ledger) | `hooks/pre-task-scope-disjoint.mjs`, `PreToolUse` matcher `Agent` |
|
|
25
|
+
|
|
26
|
+
`<state-dir>` is the first of `.pi` / `.cursor` / `.codex` / `.claude` that carries a `wave-scope.json` — the same precedence `findScopeFile()` and the hook's `waveKeyOf()` use.
|
|
27
|
+
|
|
28
|
+
### 2.1 Why `--union` runs last
|
|
29
|
+
|
|
30
|
+
A union computed over colliding scopes **launders the defect into the artefact meant to prevent it**: `allowedPaths` then grants the contested file, and every later gate — `--assert-subset`, `enforce-scope` Gate 7, the commit guard — sees a perfectly legal write. `validate()` in `validate-wave-scope.mjs` enforces the order in code: `--assert-subset` → `--assert-disjoint` → `--union`, and `--union` returns early because it is a QUERY MODE that replaces the manifest echo on stdout.
|
|
31
|
+
|
|
32
|
+
The same ordering argument applies one level up: step 2 runs on the **declared** scopes, before step 3 expands test siblings. See § 6 for the limit that buys.
|
|
33
|
+
|
|
34
|
+
### 2.2 Why the scope files are not temp files
|
|
35
|
+
|
|
36
|
+
Steps 1, 2, 3, 4 and the `--assert-subset` assertion all read the *same* file, addressed by wave and agent id. A `$TMPDIR` copy is the one failure in this chain that **costs no error**: the injector finds nothing, no `FILE-SCOPE` block reaches the prompt, `extractScopeFromPrompt()` returns `[]`, and the hook allows the dispatch exactly as it did before #1020 — signal-free (matrix rows 5/6 below). The scope files are control state like `wave-scope.json` itself, never a wave territory; writing them legitimately trips `bash-write-verify` once per wave rollover, and widening `allowedPaths` to silence that would grant agents write access to the deconfliction record.
|
|
37
|
+
|
|
38
|
+
The coordinator's **own** planned direct edits belong in `coordinator.json` in the identical form. They are not dispatches, so the hook can never see them (§ 6); the CLI check is the only gate that covers them.
|
|
39
|
+
|
|
40
|
+
## 3. The collision algorithm
|
|
41
|
+
|
|
42
|
+
`findScopeCollisions(agentScopes, { knownFiles })` compares every cross-agent entry pair through `classifyEntryCollision()`, in three binding stages:
|
|
43
|
+
|
|
44
|
+
1. **Exact string equality** → kind `concrete`. The commonest real case, and the only stage that works for a file that **does not exist yet**.
|
|
45
|
+
2. **Concrete vs glob** via `pathMatchesPattern(concrete, glob)` → kind `concrete`. Exact and I/O-free. Two *distinct concrete* paths are disjoint and return immediately.
|
|
46
|
+
3. **Glob ∩ glob**, in two sub-stages:
|
|
47
|
+
- **3a — witness:** expand both entries against `KNOWN = knownFiles ∪ {every concrete entry of every agent}`; a non-empty intersection is `glob-expanded`. The second half of that union matters — a file the wave is about to *create* is not in `git ls-files`, but a concrete claim by one agent can still witness another's glob.
|
|
48
|
+
- **3b — prefix fallback:** for the intersection that exists only in files not yet on disk. Requires literal-prefix containment in either direction, at least one **recursive** entry (`**`, or a trailing `/`, which `pathMatchesPattern` matches by `startsWith` at any depth), and compatible literal suffixes. The suffix filter is a necessary condition, so it adds no false negative while removing `scripts/**/*.ts` vs `scripts/**/*.mjs`.
|
|
49
|
+
|
|
50
|
+
`knownFiles` is **injected, never discovered**: `scope-gate.mjs` is hook-safe (pure, sync, no I/O, no spawn) because `enforce-scope.mjs` reaches it on a hot path, and under the exit-0/stdout-JSON protocol a throw there reads as "no decision" = ALLOW. The CLI spawns `git ls-files` in `knownRepoFiles()`; the hook does the same in `listTrackedFiles()`, both resolving `git rev-parse --show-toplevel` first so a session started in a subdirectory produces repo-relative paths on both sides.
|
|
51
|
+
|
|
52
|
+
Duplicate agent ids are reported separately (`duplicateIds`), not as a self-collision. A record with no usable id runs as `<unnamed#i>` rather than being dropped — an unreviewed scope is exactly the one that collides.
|
|
53
|
+
|
|
54
|
+
### 3.1 Why `pathMatchesPattern` alone cannot do stage 3
|
|
55
|
+
|
|
56
|
+
The matcher is **directed**: argument 2 is compiled into a regex, argument 1 is tested as a literal string. Measured in this working tree on 2026-08-14:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
$ node --input-type=module -e "import { pathMatchesPattern } from './scripts/lib/scope-gate.mjs';
|
|
60
|
+
console.log(pathMatchesPattern('scripts/**/*.mjs','scripts/lib/*.mjs'));
|
|
61
|
+
console.log(pathMatchesPattern('scripts/lib/x.mjs','scripts/**/*.mjs'),
|
|
62
|
+
pathMatchesPattern('scripts/lib/x.mjs','scripts/lib/*.mjs'));"
|
|
63
|
+
false
|
|
64
|
+
true true
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Both globs match `scripts/lib/x.mjs`, yet the direct comparison says `false`. For `assertFileScopeSubset()` that inexactness is *safe*: its glob branch reduces to verbatim presence plus literal-prefix coverage and therefore **over-approximates coverage**, which at worst accepts a union it could not fully prove. For a **collision** check the sign flips — the same over-approximation becomes a **false negative**, i.e. a missed collision, i.e. the incident. That is why the two exact stages decide first and stage 3 is reached only for pairs neither can settle.
|
|
68
|
+
|
|
69
|
+
## 4. The hook
|
|
70
|
+
|
|
71
|
+
`hooks/pre-task-scope-disjoint.mjs` is a `PreToolUse` hook on matcher **`Agent`** (registered in `hooks/hooks.json`). It blocks a wave from handing the same file to two agents at the moment of dispatch, before either has written a byte.
|
|
72
|
+
|
|
73
|
+
It cannot compare a batch of siblings directly — the header records why, measured against 12 archived transcripts of this repo (147 dispatch `tool_use` blocks, 51 batches, measured 2026-08-14): the dispatch tool is named `Agent` and not `Task`; the payload carries **no** structured file scope (`files` / `file_scope` / `scope` → 441 probes, zero hits), so the scope exists only as prose inside `prompt`; and the not-yet-dispatched siblings of a batch are not visible in the transcript at dispatch time. What remains is a **ledger**: each dispatch records its scope under a wave key, and the next dispatch is checked against everything already recorded.
|
|
74
|
+
|
|
75
|
+
- **Ledger:** `.orchestrator/wave-dispatch-scopes.json` (gitignored), keyed `<session-id>|w<wave>|<role>` from `<state-dir>/wave-scope.json`.
|
|
76
|
+
- **Lock:** `.orchestrator/wave-dispatch-scopes.lock` — the read-modify-write cycle runs under `withFileLock()` (the primitive behind the PSA-005 STATE.md lock). `writeJsonAtomicSync` makes the write atomic, never the *cycle*.
|
|
77
|
+
- **Scope extraction:** `extractScopeFromPrompt()` finds a scope marker line and takes the FIRST fenced block after it, accepting only lines that survive a deliberately strict `looksLikeRepoPath()`. `normalizeScopeEntry()` then folds `./`, `//` and `/./` spellings together, and `promoteDirEntries()` rewrites `scripts/lib` → `scripts/lib/` **on evidence** (not a tracked file itself, at least one tracked file beneath it) — both because two spellings of one path previously compared as disjoint.
|
|
78
|
+
- **DENY** fires on exactly one condition: a collision involving THIS dispatch with a prior agent of the same wave that is **still in flight** (§ 5). The reason names the agent pair, the collision kind and the evidence paths; the suggestion is "give the file exactly ONE owner, or wait for the named agent(s) and re-dispatch", plus the ledger path to delete if the state is stale. The ledger is deliberately **not** persisted on a deny — the dispatch did not happen, so recording it would make the retry-after-fix look like a duplicate.
|
|
79
|
+
|
|
80
|
+
### 4.1 Error-class matrix — deliberately fail-**open**
|
|
81
|
+
|
|
82
|
+
The blast radius is asymmetric. A false positive on the dispatch path blocks every agent of the session — the guard becomes a session outage, and nothing downstream catches a dispatch that never happened. A false negative is a double assignment that three later gates still catch (`validate-wave-scope.mjs`, `enforce-scope.mjs` at write time, the W5 verification pass). Fail-closed is right for a WRITE guard (`enforce-scope.mjs` is), and wrong here. Each row is a choice, not an oversight:
|
|
83
|
+
|
|
84
|
+
| # | Condition | Decision |
|
|
85
|
+
|---|-----------|----------|
|
|
86
|
+
| 1 | disabled via profile/env | exit 0, silent |
|
|
87
|
+
| 2 | repo module failed to load | ALLOW + `GUARD INACTIVE` banner on stderr |
|
|
88
|
+
| 3 | stdin empty / not JSON | ALLOW |
|
|
89
|
+
| 4 | `tool_name` is not `Agent` | ALLOW |
|
|
90
|
+
| 5 | prompt carries no scope marker | ALLOW |
|
|
91
|
+
| 6 | scope block present but unparseable | ALLOW |
|
|
92
|
+
| 7 | ledger unreadable / corrupt | WARN + ALLOW + **self-heal** (the verdict carries a fresh ledger) |
|
|
93
|
+
| 8 | `git ls-files` failed | ALLOW, degraded (stage 3a loses witnesses; concrete collisions still found) |
|
|
94
|
+
| 9 | `findScopeCollisions` not evaluable | WARN + ALLOW |
|
|
95
|
+
| 10 | same agent id re-dispatched | ALLOW, ledger record replaced (a retry must not self-lock) |
|
|
96
|
+
| 10a | collision, but every colliding prior agent has FINISHED | ALLOW + prune those records |
|
|
97
|
+
| 11 | collision with a prior agent still IN FLIGHT | **DENY** |
|
|
98
|
+
| 12 | unexpected throw in `main()` | ALLOW + stderr |
|
|
99
|
+
| 13 | liveness probe throws / no evidence at all | treated as IN FLIGHT (bounded by the TTL, § 5) |
|
|
100
|
+
| 14 | ledger lock not acquirable within its budget | run the cycle UNLOCKED (degraded), never deny |
|
|
101
|
+
|
|
102
|
+
Two structural rules keep this matrix honest, both recorded in the hook header:
|
|
103
|
+
|
|
104
|
+
- `decide()` is a **pure function returning a verdict**; the module emits exactly once, at the end. `emitWarn`/`emitDeny` call `process.exit(0)` and never return, so a warn emitted from inside the checking flow would terminate the process before a later collision could be denied — and would skip the lock's release `finally`.
|
|
105
|
+
- Row 9's discriminator is **not** `ok !== true`. `ok` means *disjoint*, so `ok === false` is the normal result of a real collision; "not evaluable" is `ok === false` with BOTH result arrays empty. Reading `ok` as evaluability would turn every genuine collision into a warn, i.e. an allow — the exact fail-open the hook exists to prevent.
|
|
106
|
+
|
|
107
|
+
## 5. The liveness probe
|
|
108
|
+
|
|
109
|
+
A ledger with no notion of completion denies the wrong thing. Measured over 38 archived transcripts of this repo (346 `Agent` dispatch blocks; hook header, 2026-08-14): 0 of 4 same-batch overlaps and **2 of 2 cross-dispatch overlaps** would have been denied — and both cross-dispatch pairs were legitimate **sequential repair passes** (a dispatch and its later fix). Because a deny deliberately does not persist the ledger, the re-dispatch would have met the same stale record: a permanent block.
|
|
110
|
+
|
|
111
|
+
The discriminator is therefore neither time nor the agent's name, but whether the recorded agent is **still in flight**. Two transcript shapes carry that:
|
|
112
|
+
|
|
113
|
+
- **Synchronous dispatch** — the `tool_result` for the dispatch's `tool_use` id arrives when the agent is done (measured: five `Agent` rows within 0.44 s, their results 5–11 minutes later). At the fifth agent's `PreToolUse` none of the first four has a result → all in flight → a real same-batch overlap still denies.
|
|
114
|
+
- **Asynchronous dispatch** — the `tool_result` arrives in ~0.2 s and reads `Async agent launched successfully`. **That text is a launch receipt, not a completion.** Treating it as one would let every real background-batch collision through. The completion arrives later as a `<task-notification>` record carrying `<tool-use-id>` and `<status>completed</status>` (measured: launch 14:14:26.768 → notification 14:24:39.360).
|
|
115
|
+
|
|
116
|
+
`buildTranscriptIndex()` reads all three record shapes and counts a description as finished only when **every** one of its dispatch ids is finished. Cost containment: the transcript is read **only once a collision has already been found** — i.e. only on the path that is about to deny; the no-collision path pays nothing. Transcripts above 256 MiB are treated as *no evidence*, never as a completion.
|
|
117
|
+
|
|
118
|
+
**Blind fallback and its named ceiling (BV-004).** With no transcript, or none carrying a record of that agent, liveness falls back to the ledger entry's own age with `IN_FLIGHT_TTL_MS = 30 min`. The ceiling is derived: the largest **measured** same-batch dispatch spread is 95.7 s, so 30 min is ~19× headroom against the false-ALLOW direction, while both measured sequential repair gaps (36 min, 49 min) sit above it. **Revisit trigger:** a same-batch spread above ~5 min appearing in `.orchestrator/metrics/`, or a harness change that stops writing `transcript_path` — either invalidates the headroom the number rests on.
|
|
119
|
+
|
|
120
|
+
## 6. Named limits
|
|
121
|
+
|
|
122
|
+
Complete list of what this guard does **not** see, or sees only approximately:
|
|
123
|
+
|
|
124
|
+
1. **The blind TTL window.** Without transcript evidence the only liveness signal is the 30-minute TTL above. Inside that window a finished agent still blocks (false deny, recoverable by deleting the ledger); outside it a running agent no longer blocks (false allow). Revisit trigger as stated in § 5.
|
|
125
|
+
2. **Test-sibling collisions.** Disjointness is asserted on the **declared** scopes (step 2), before `expandTestSiblings()` runs (step 3). Two agents whose production files share a basename receive the *same* emitted sibling glob (`tests/**/{basename}*.test.mjs`), which a declared-scope check cannot see. Revisit if a wave is ever scoped by basename family instead of by directory.
|
|
126
|
+
3. **Prose extraction fails only toward ALLOW.** The hook's only channel is the `FILE-SCOPE` prose block. A missing marker, a missing fence, a decorated path that fails `looksLikeRepoPath()` — all resolve to allow (rows 5/6). Measured: 42 of 147 archived prompts (28.6 %) carried a scope marker at all, so denying the non-extractable case would have denied ~7 dispatches in 10. The CLI check (step 2) is the gate that does not depend on prose.
|
|
127
|
+
4. **Coordinator-direct edits are invisible to the hook.** They are not dispatches, so no ledger entry exists for them. They participate in the CLI check via `coordinator.json` only — and 2 of the 5 divergences that motivated #1020 were coordinator-direct edits.
|
|
128
|
+
5. **The wave-key fallback.** With no readable `wave-scope.json`, `waveKeyOf()` degrades to `<session>|w?|?` and the ledger spans the whole session, so a wave-3 dispatch is compared against wave-1 records. Bounded, not eliminated, by the liveness probe: a prior record binds only while its agent is in flight.
|
|
129
|
+
6. **Glob ∩ glob without witnesses.** Stage 3a needs tracked files; with git unavailable (row 8) or for files not yet on disk, only stage 3b's prefix fallback carries the load — and it requires at least one recursive entry, so two non-recursive globs that intersect only in an unborn file are not detected.
|
|
130
|
+
7. **Only collisions involving the current dispatch are actionable.** A pair among already-dispatched agents was either denied at its own dispatch or predates the guard; re-denying it would block an innocent third agent.
|
|
131
|
+
8. **Lock loss reopens the race.** On lock timeout the cycle runs unlocked (row 14) — two dispatches starting together can then read the same ledger state and one record is lost. That is the pre-lock behaviour, chosen over denying on a lock-file problem.
|
|
132
|
+
|
|
133
|
+
## 7. Debugging
|
|
134
|
+
|
|
135
|
+
**A dispatch was denied and you do not believe it.** Read `.orchestrator/wave-dispatch-scopes.json`: it carries `waveKey`, `updated`, and one `{id, desc, files, at}` record per already-dispatched agent. The deny reason names the other agent — find its record and compare its `files` to the ones in your prompt's `FILE-SCOPE` block. Three outcomes:
|
|
136
|
+
|
|
137
|
+
- The other agent is genuinely running and the overlap is real → fix the ownership in the session plan (one file, one agent), rewrite the affected `filescopes/wave-<N>/*.json`, re-assert, re-dispatch.
|
|
138
|
+
- The other agent has finished, but the ledger still binds it → the transcript carried no evidence (§ 5) and you are inside the TTL window. Delete `.orchestrator/wave-dispatch-scopes.json`; the next dispatch rebuilds it.
|
|
139
|
+
- The `waveKey` names an older wave → the `<session>|w?|?` fallback (limit 5). Check that `<state-dir>/wave-scope.json` exists and is readable, then delete the ledger.
|
|
140
|
+
|
|
141
|
+
**A dispatch was NOT denied and should have been.** Work down the allow rows: is `FILE-SCOPE` present in the prompt with a fenced block right after it (rows 5/6)? Is the hook armed at all (`GUARD INACTIVE` on stderr = row 2)? Did a `systemMessage` warning appear (rows 7/9)? Cross-check the same scopes through the CLI, which does not depend on prose:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
node scripts/validate-wave-scope.mjs --assert-disjoint "$WAVE_SCOPES_SIDECAR" < <state-dir>/wave-scope.json
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Exit 1 prints one `ERROR:` line per collision (`agents "A" and "B" both claim [...]`) and one per duplicate id. Exit 0 — nothing on stderr, the manifest echoed back on stdout — means the declared scopes really are disjoint and the hook was right to allow; the divergence is then in the prompt, not in the plan.
|
|
148
|
+
|
|
149
|
+
**Reset.** Delete `.orchestrator/wave-dispatch-scopes.json` (and `.orchestrator/wave-dispatch-scopes.lock` if a dead holder is suspected). Both are gitignored, both are rebuilt on the next dispatch, and neither is shared with any other mechanism. After the final wave, `<state-dir>/filescopes/` is deleted along with `wave-scope.json` — a stale `wave-<N>/` directory left behind is a scope claim nobody re-verified.
|
|
150
|
+
|
|
151
|
+
**Disable.** The hook honours the repo's profile gate (`shouldRunHook('pre-task-scope-disjoint')`, row 1) — a silent exit 0, no decision at all.
|
|
152
|
+
|
|
153
|
+
## 8. Provenance of the numbers
|
|
154
|
+
|
|
155
|
+
Every figure above is quoted from a measurement recorded next to the code that carries it, with its date:
|
|
156
|
+
|
|
157
|
+
- Transcript-shape figures (147 dispatch blocks / 12 transcripts / 51 batches / 441 zero-hit probes / 28.6 % marker coverage; 346 blocks / 38 transcripts for liveness; the 95.7 s, 36 min and 49 min spreads; the 0.44 s and 10-minute observations) — header of `hooks/pre-task-scope-disjoint.mjs`, measured 2026-08-14 against this project's archived transcripts.
|
|
158
|
+
- The directedness transcript in § 3.1 — run in this working tree on 2026-08-14; the command is printed with it.
|
|
159
|
+
|
|
160
|
+
Re-measure before citing any of these downstream. A count re-briefed later is a claim about the past (`.claude/rules/parallel-sessions.md` § PSA-006).
|
|
161
|
+
|
|
162
|
+
## See Also
|
|
163
|
+
|
|
164
|
+
- `skills/wave-executor/wave-loop.md` § Scope Manifest — the coordinator runbook (steps 3.1–3.3) and § Pre-Dispatch: File-Scope Injection (the prompt block shape).
|
|
165
|
+
- `.claude/rules/parallel-sessions.md` § Decision Tree (why a file in two declared scopes is never a benign sibling signal), § PSA-006 (measurement discipline).
|
|
166
|
+
- `hooks/enforce-scope.mjs` — the write-time gate, fail-**closed**; the deliberate inversion of this hook's posture.
|
|
167
|
+
- [`docs/adr/0011-guard-degradation-semantics.md`](adr/0011-guard-degradation-semantics.md) — the exit-0 hook protocol (#906) and why a truncated stdout envelope reads as no-decision, i.e. as ALLOW.
|
|
@@ -74,7 +74,7 @@ Some sub-configs live in dedicated policy files under `.orchestrator/policy/`:
|
|
|
74
74
|
|
|
75
75
|
| Field | Type | Default | Description |
|
|
76
76
|
|-------|------|---------|-------------|
|
|
77
|
-
| `agents-per-wave` | integer or integer with overrides | `6` | Maximum parallel subagents per wave. Supports session-type overrides: `6 (deep: 18)` outputs `{"default": 6, "deep": 18}`. Plain integers remain plain. |
|
|
77
|
+
| `agents-per-wave` | integer or integer with overrides | `6` | Maximum parallel subagents per wave. Supports session-type overrides: `6 (deep: 18)` outputs `{"default": 6, "deep": 18}`. Plain integers remain plain. The override key names a session type but does **not** create one: there is no `session-type:` Session Config key — `parseSessionConfig()` emits none, so writing one into a repo's `## Session Config` block is inert prose. The session type comes from the `/session` argument (default `deep`, see `commands/session.md`) and is persisted to STATE.md frontmatter as `session-type:`, which is the only live read (`scripts/print-applicable-rules.mjs` rule mode-gating). |
|
|
78
78
|
| `agent-mapping` | object | null | Optional mapping of role keys to agent names for explicit agent binding. Keys: `impl`, `test`, `db`, `ui`, `security`, `compliance`, `docs`, `perf`. Example: `{ impl: code-editor, test: test-specialist }`. Overrides auto-discovery when present. |
|
|
79
79
|
| `waves` | integer | `5` | Number of execution waves for feature and deep sessions. |
|
|
80
80
|
| `recent-commits` | integer | `20` | Number of recent commits to display during session start git analysis. |
|
|
@@ -1638,46 +1638,6 @@ Leave disabled (default) when:
|
|
|
1638
1638
|
- `skills/wave-executor/wave-loop.md` § 3b — the wave-executor hook contract.
|
|
1639
1639
|
- `agents/schemas/persona-panel-sidecar.schema.json` — sidecar JSON Schema enforced before write.
|
|
1640
1640
|
|
|
1641
|
-
## Compact Nudge (#620)
|
|
1642
|
-
|
|
1643
|
-
Advisory-only checkpoint surfaced at inter-wave boundaries in the wave-executor loop. Never auto-compacts — `/compact` is a user slash-command, and the coordinator/operator decides when to invoke it. When the gate conditions are met, the wave-executor appends ONE advisory bullet to the wave progress update suggesting a `/compact` before the next wave.
|
|
1644
|
-
|
|
1645
|
-
All fields live under a top-level `compact-nudge` object in your Session Config host file (`CLAUDE.md` or `AGENTS.md`), for example:
|
|
1646
|
-
|
|
1647
|
-
```yaml
|
|
1648
|
-
compact-nudge:
|
|
1649
|
-
enabled: false # opt-in advisory /compact nudge at inter-wave checkpoints (never auto-compacts)
|
|
1650
|
-
after: [discovery, impl] # wave boundaries that may fire the nudge — subset of {discovery, impl, failed-wave}
|
|
1651
|
-
mode: warn # warn (surface one bullet in the wave progress update) | off (silent no-op)
|
|
1652
|
-
```
|
|
1653
|
-
|
|
1654
|
-
| Field | Type | Default | Description |
|
|
1655
|
-
|-------|------|---------|-------------|
|
|
1656
|
-
| `compact-nudge.enabled` | boolean | `false` | Master toggle. When `false` (or the block is absent), the nudge never fires — zero behaviour change. |
|
|
1657
|
-
| `compact-nudge.after` | list of `"discovery"` \| `"impl"` \| `"failed-wave"` | `[discovery, impl]` | Wave boundaries that may fire the nudge. `discovery`/`impl` are wave ROLES, matched against the just-completed wave's role string. `failed-wave` is not a role — it keys off the wave's failure OUTCOME (any wave that did not pass its quality gate), so it can fire after a wave of any role. |
|
|
1658
|
-
| `compact-nudge.mode` | `warn` \| `off` | `warn` | `warn` emits the advisory bullet in the wave progress update. `off` is a silent no-op even when `enabled: true`. |
|
|
1659
|
-
|
|
1660
|
-
**Used by:** `skills/wave-executor/wave-loop.md` § 3c "Strategic Compact-Nudge". Issue #620. See `.claude/rules/loop-and-monitor.md` for the broader `/loop` vs `/goal` vs Monitor routing this nudge composes with.
|
|
1661
|
-
|
|
1662
|
-
## Goal Integration (#636)
|
|
1663
|
-
|
|
1664
|
-
Opt-in advisory continuation anchor that surfaces a suggested `/goal` command at named seams — the inter-wave fix-loop (`inter-wave-fixloop`) and the session-end backlog drain (`session-end-backlog`). Never auto-invokes `/goal`, never blocks forward progress; `/goal` remains a user slash-command the operator chooses to run. Per ADR-0010, `/goal` provides CONTINUATION, never JUDGMENT — the suggested condition always references freshly-run deterministic gate output and embeds a bound (e.g. "or stop after N attempts"); the exit-code result of the underlying gate stays the authority.
|
|
1665
|
-
|
|
1666
|
-
All fields live under a top-level `goal-integration` object in your Session Config host file (`CLAUDE.md` or `AGENTS.md`), for example:
|
|
1667
|
-
|
|
1668
|
-
```yaml
|
|
1669
|
-
goal-integration:
|
|
1670
|
-
enabled: false # opt-in advisory; default off — zero behaviour change when absent
|
|
1671
|
-
seams: [session-end-backlog, inter-wave-fixloop] # subset of {session-end-backlog, inter-wave-fixloop}; one goal per session — pick ONE seam at a time
|
|
1672
|
-
```
|
|
1673
|
-
|
|
1674
|
-
| Field | Type | Default | Description |
|
|
1675
|
-
|-------|------|---------|-------------|
|
|
1676
|
-
| `goal-integration.enabled` | boolean | `false` | Master toggle. When `false` (or the block is absent), no `/goal` suggestion is ever surfaced — zero behaviour change. |
|
|
1677
|
-
| `goal-integration.seams` | list of `"session-end-backlog"` \| `"inter-wave-fixloop"` | `[session-end-backlog, inter-wave-fixloop]` | Which seam(s) may surface the advisory `/goal` suggestion. Only ONE `/goal` can be active per session — if both seams are listed, the operator picks a single seam to actually invoke; the two cannot hold simultaneous active goals. |
|
|
1678
|
-
|
|
1679
|
-
**Used by:** `skills/wave-executor/wave-loop.md` § "/goal Continuation Anchor" (inter-wave-fixloop seam), `skills/session-end/SKILL.md` § 1.3a "Optional /goal Backlog-Drain" (session-end-backlog seam). Lever 5 / issue #636. See `.claude/rules/loop-and-monitor.md` § LM-008 for the full continuation-vs-judgment contract.
|
|
1680
|
-
|
|
1681
1641
|
## Skill Evolution (#646)
|
|
1682
1642
|
|
|
1683
1643
|
Opt-in configuration for the Skill Self-Evolution Foundation (Epic #643, Sub-issue #646). Controls whether `/evolve` surfaces skill health signals for operator review only (`advisory`) or additionally applies deterministic repairs to local config artifacts behind an evidence gate (`autonomous-gated`). The default is `off` — no behavior change for repos that omit this block.
|