Skip to content

feat: deterministic PreToolUse enforcement of the reviewer read-scope bound (2.3.4) - #545

Merged
apackeer merged 9 commits into
v2from
fix/issue-539-reviewer-scope-hook
Jul 12, 2026
Merged

feat: deterministic PreToolUse enforcement of the reviewer read-scope bound (2.3.4)#545
apackeer merged 9 commits into
v2from
fix/issue-539-reviewer-scope-hook

Conversation

@apackeer

@apackeer apackeer commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Deterministic PreToolUse enforcement of the reviewer read-scope bound

Fixes #539.

Problem

The per-unit reviewer read-scope bound shipped in #538 (2.2.16) is prose on six surfaces. The field evidence on that PR's thread shows why prose alone loses against this persona: the U03 transcript carried 14 recursive greps with cross-unit globs (construction/*/*/*.md) that bypassed the first wording, and the reporter's latency table shows per-call time degrading ~30x once sibling reads push context past the ~100k-token knee - so the damage from each violation is superlinear. Per the framework's own layering (determinism belongs in tools/hooks, knowledge in agents, judgment with humans), the bound needs a deterministic twin.

Design (D1-D5 outcomes)

D1 - how the hook learns the dispatch. The conductor writes <record>/.aidlc-reviewer-dispatch.json ({reviewer, stage, unit, exempt[]}) immediately before invoking a per-unit reviewer (stage-protocol 12a step 1) and deletes it the moment the verdict is read (step 3). The record is the enforcement window. Staleness is bounded by mtime + a 6h TTL with a janitor delete (the compose-marker discipline); the worst observed pre-fix review ran ~3h, so 6h covers the pathological case. Concurrent swarm clones each have their own record tree (state is forked per worktree), so no sharding is needed. A reviewer-agent sighting with NO record is never blocked - it records an advisory drop surfaced by /aidlc --doctor (the conductor forgot the step-1 write).

D2 - enforcement matrix. The matcher is an exported pure function (evaluateReviewerScope, pinned by t220's 20-case decision table). It scans path fields (Read/Edit/Write), pattern/glob + search-root fields (Glob/Grep), and the whole command string (Bash) for construction/<seg> tokens: the dispatched unit passes; a wildcard segment or bare construction/ sweep root blocks; a concrete sibling blocks unless the full token exactly matches an exempt entry's construction/ suffix. Grep's content regex is deliberately NOT scanned (matching file content is not a file access). The spot-check carve-out became dispatch-time data: the conductor resolves the explicitly named integration point and puts that one owning file on exempt when writing the record - the reviewer can no longer self-expand scope, which is exactly the prose-vs-persona contest the issue documents (the reporter's transcripts confirm the conductor already names sibling invariants by ID at dispatch time). A shell variable in the unit segment (construction/$UNIT/...) blocks conservatively (the matcher cannot resolve it; the refusal message says to use the literal unit name).

D3 - hard-block vs advisory per harness (user decision: hard-block where possible):

Harness Registration Identity Posture
Claude Code settings.json, first PreToolUse entry (matcher Read|Edit|Write|Glob|Grep|Bash, quoted per #522) payload agent_type (probe-verified) Hard-block (exit 2 + stderr, live-verified)
Kiro CLI the adapter's new reviewer-scope target, wired inside BOTH reviewer agents' own JSON configs the scoped registration IS the identity Hard-block (exit-2 contract, live-verified incl. graceful in-subagent handling)
Codex CLI hooks.json PreToolUse row + one new trust entry payload agent_type (verified on 0.142.5) Hard-block (live-verified; the duplicate-delivery replay cache now carries stderr so a block replays faithfully)
Kiro IDE none (deliberate) n/a The reworked IDE adapter (2.2.17) receives hook context via USER_PROMPT with toolArgs always empty - a pre-tool matcher has nothing to inspect, so per the porting-guide policy no dead hook ships; the 12a prose bound governs. t220 pins the deliberate absence.

Safety rails everywhere: fail-open on any ambiguity (no/stale/malformed record, unknown tool, non-reviewer agent, malformed stdin, any throw), TTY guard, health heartbeat, recordHookDrop diagnostics, and the deterministic off-switch AIDLC_DISABLE_REVIEWER_SCOPE_HOOK=1 for false-positive storms (e.g. a source tree with its own construction/ directory).

D4 - builder coverage: out of scope. The field evidence is reviewer-specific; builders have a weaker temptation, no transcripts, and a wider legitimate read surface. The record + matcher design generalizes if evidence appears.

D5 - audit. Every refusal emits REVIEWER_SCOPE_BLOCKED (Tool, Target, Stage, Unit) through the standard appendAuditEntry path; taxonomy registered across aidlc-audit.ts, audit-format.md (70 -> 71 events), 12-state-machine.md, and the hooks chapter. Audit failure never changes the block decision.

What the reviewer experiences

The blocked call never runs; the harness hands the reviewer the hook's stderr, which names the scope, the offending token, and the sanctioned alternative ("verify cross-unit claims against the passed contracts"). All three live probes showed the agent reading the reason, not retrying, and continuing the review. The human sees nothing in-session; the audit shard records each refusal.

Test evidence

  • t220 (new, unit): 46 tests - the matcher decision table (28 cases incl. the adversarial-review shapes) + parse/reason helpers (in-process, pure), the dispatch-record lifecycle against the SHIPPED hook as a subprocess (block/allow/no-record advisory drop/stale janitor/malformed fail-open/off-switch/garbage stdin/audit emit), and the registration + protocol prose pins across all four harnesses (including the deliberate Kiro IDE absence).
  • Collateral pins updated: t01/t02 hook roster 11 -> 12 (+ plan parity 67 -> 68), t150 codex hooks.json events (+PreToolUse), t219 quoting count 13 -> 14, coverage-registry allowlist. t48 (audit taxonomy drift), t68 (version trio), t148, t217 all green with the additions.
  • Live probes (design phase, all three CLI harnesses): PreToolUse exit-2 blocking verified end to end on Claude Code (subagent agent_type in payload; block reason relayed), Codex 0.142.5 (subagent agent_type = spawned agent name; block inside a collab subagent handled gracefully), and Kiro CLI 2.7.0 (per-agent hook registration fires for the subagent's own calls only; PreToolHook blocked the tool execution relayed). Probe artifacts under the effort's tmp dir.
  • Deterministic tier: smoke+unit+integration slice-evidence block in the PR conversation (run per the repo's test discipline).

Review

Two independent adversarial reviewers ran on the committed diff; all findings were reproduced before fixing.

Round 1 (codex-review, gpt-5.5) found three matcher bypasses, now pinned in t220's decision table:

  1. Dot-dot traversal - construction/U03/../U01/design.md was judged on the first segment after construction/ and allowed; path components are now normalized with .. collapsed against parents before judging.
  2. Bare search roots - grep -rn TODO construction / find construction name the whole tree with no construction/ literal, so the token scan missed them; a quote-stripped pass now blocks any token ending in the bare construction component while the word inside quoted content regexes stays content.
  3. Codex apply_patch Delete/Move - *** Delete File: / *** Move to: directives were not fanned out to the scope check; they now forward as Edit-class mutations.

Round 2 (fresh Claude agent, verdict APPROVE-WITH-FIXES) found no blockers; all minors fixed:

  1. Kiro identity precision - each Kiro registration now passes its own agent name as an argv token, forwarded as agent_type, so the record's reviewer field is compared on Kiro too (a stale record naming a different reviewer fails open instead of scoping the wrong agent).
  2. Block-reason recovery hints - a reviewer blocked on a shell variable (construction/$U/) or an unquoted bare-word grep is now told exactly how to self-correct (literal unit name / quote the pattern); both are conservative blocks, not sibling reads.
  3. SKILL.md antecedent - the verdict sentence now reads "the ## Review section verdict from the primary artifact" (the inserted delete step had left "its" pointing at the dispatch record).
  4. Codex replay symmetry - the duplicate-delivery cache persists the answered exit code (2 or 0), never a raw crash code.
  5. Two doc-count stragglers - the shipped onboarding template (projects into CLAUDE.md/AGENTS.md) and the architecture doc's test-levels row now say 12 hooks.

Round 1's consolidated report (delivered after round 2) added five more items, all folded in:

  1. Troubleshooting discoverability - the guide now documents the refusal message, the off-switch, and the stale-record cleanup path (the false-positive class it names: repos with their own construction/ source directories).
  2. Kiro batch writes - the adapter's write side now iterates operations[] like the read side.
  3. Bounded audit emit - the blocked-call audit row acquires the lock with a 250ms budget (emit-unlocked inside) instead of the standard 5s retry, so a contended Bolt fan-out cannot slow the refuse; a starved lock drops the row and records a hook drop.
  4. Rate-bounded advisory - the missing-dispatch-record drop is deduped to one line per 10 minutes.
  5. Parallel-review dispatch sharding - correctly out of reach today (serial conductor loop); follow-up issue drafted to shard the record per unit before any parallel per-unit review lands.

The reviews also flagged pre-existing stale "68-event taxonomy" references in eight doc files (wrong on the v2 base already); left for a follow-up sweep rather than folded into this PR.

Base note

Originally stacked on #538; that PR (and the 2.2.17-2.2.19 wave) merged while this was in flight, so the branch is rebased onto current v2 with re-slots: version 2.2.20, test t220. The Kiro IDE registration present in the pre-rebase design was dropped after the 2.2.17 IDE hook rework made the seam unenforceable (empty toolArgs); the drop is deliberate, documented, and pinned.

Credits

The deterministic-hook proposal comes from the issue-534 reporter's transcript evidence on the #538 thread; the .reviewerignore-style negative filter was independently suggested on the issue thread - this PR is that idea at the framework's native enforcement point.

@apackeer

apackeer commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Slice evidence - issue 539 final tree (tip 132c44a)

All runs from the worktree, bash tests/run-tests.sh --debug -P 8 <tier>, logs
redirected to ROOT tmp/issue-539-reviewer-scope-hook/.

Slice 1: smoke+unit

STAMP:    tests/logs/2026-07-09T15-34-28Z
TRACES:   none expected (smoke/unit tiers produce no driver ndjson)
SUMMARY:  2026-07-09T15-34-28Z/summary.txt  Result: FAIL, Failed files: 1 / 152, Failed assertions: 1 / 2567
RESULT:   smoke+unit . 151/1 . reds: t205-gate-revision-backstop (scenario 11, two-shard anchor timing) . live vars set: none (none required) . invariant grep hits: 0 (path-excluded)

Red disposition: t205 scenario 11 green-alone (bun test tests/unit/t205-gate-revision-backstop.test.ts -> 11 pass / 0 fail); known contention-sensitive scenario (shard-mtime anchor under -P 8), untouched by this diff (t205 file not in the diff).

Slice 2: integration

STAMP:    tests/logs/2026-07-09T15-34-54Z
TRACES:   tests/logs/2026-07-09T15-34-54Z/*.ndjson  (23 files, sdk-drive)
SUMMARY:  2026-07-09T15-34-54Z/summary.txt  Result: FAIL, Failed files: 2 / 99
RESULT:   integration . 97/2 . reds: t183-codekb-placement-reverify.sdk (Bedrock 'unexpected error during processing' API error at minute ~10), t72-stage-reverse-engineering (live-agent latency, 887s) . live vars set: none (Claude-SDK tier needs only claude on PATH) . invariant grep hits: 0 (path-excluded)

Red disposition: both green-alone in the filtered rerun below; both are live-SDK
tests untouched by this diff (upstream API error + latency flake).

Slice 3: red rerun (filtered, green-alone gate)

STAMP:    tests/logs/2026-07-09T15-58-08Z
TRACES:   tests/logs/2026-07-09T15-58-08Z/*.ndjson  (2 files, sdk-drive)
SUMMARY:  2026-07-09T15-58-08Z/summary.txt  Result: PASS, Failed files: 0 / 2
RESULT:   integration --filter "t72-stage-reverse-engineering|t183-codekb" . 2/0 . reds: none . live vars set: none . invariant grep hits: 0

Earlier full-tier context

An earlier full smoke+unit+integration run on the pre-review-fix tree
(tests/logs/2026-07-09T10-15-18Z, 251 files) had 3 reds: t28/t81 (real
collateral pins, fixed by bumping the event count to 71) and t185 (7/16 red
green-alone AND identically red on a clean origin/v2 baseline worktree -
pre-existing environment issue, not this branch). Files affected by the
post-run fixes were re-run explicitly (223 pass / 0 fail across 17 files)
before the final slices above.

apackeer added 8 commits July 10, 2026 13:27
… bound

The per-unit reviewer read-scope bound (stage-protocol 12a) gains a
deterministic twin: core/hooks/aidlc-reviewer-scope.ts, the framework's
second flow-altering hook. While a conductor-written dispatch record
(<record>/.aidlc-reviewer-dispatch.json, written at 12a step 1, deleted
at step 3, 6h staleness janitor) is fresh, the dispatched reviewer's
tool calls that reach into sibling units' construction/ paths - file
reads, writes, and grep/glob/shell patterns that span siblings - are
refused (exit 2 + a redirecting stderr reason) unless the target is on
the record's exempt list (consumes contracts, stage file, Q&A file, and
any conductor-named integration-point file). Every refusal emits a
REVIEWER_SCOPE_BLOCKED audit event. Fail-open on every ambiguity, with
AIDLC_DISABLE_REVIEWER_SCOPE_HOOK=1 as the deterministic off-switch.

Registration per harness: Claude Code settings.json gains its first
PreToolUse entry (agent_type identity, probe-verified); Kiro CLI wires
the adapter's new reviewer-scope target inside the two reviewer agents'
own JSON configs (registration IS the identity); Kiro IDE ships a new
aidlc-reviewer-scope.kiro.hook (window-scoped best-effort); Codex adds
a PreToolUse row to HOOK_WIRING + a trust entry (agent_type verified on
0.142.5), with stderr now riding the duplicate-delivery replay cache.
t218 pins the pure matcher (20-case decision table), the dispatch-record
lifecycle against the shipped hook (block/allow/stale-janitor/off-switch/
audit emit), and the four harness registrations + the 12a prose. Collateral
pins updated: t01/t02 hook roster (11 -> 12), t01 plan parity (67 -> 68),
t150 codex hooks.json events (+PreToolUse), coverage-registry allowlist.
Docs: hooks chapter gains the reviewer-scope section and the second
flow-altering contract; audit taxonomy 70 -> 71 across audit-format.md,
12-state-machine.md, 06-hooks-and-tools.md; hook-count sweeps in AGENTS.md
+ guide pages; porting guide notes the two block channels and the
scoped-registration identity pattern. CHANGELOG 2.2.17 + README badge.
The base PRs merged while this branch was in flight: PR 538 (2.2.16, the
prose bound), the Kiro IDE hook rework (2.2.17: USER_PROMPT context with
toolArgs always empty), and the CLAUDE_PROJECT_DIR quoting fix (2.2.19).
Re-slots: version 2.2.17 -> 2.2.20, test t218 -> t220 (t218/t219 taken).
Consequences absorbed: the Claude PreToolUse entry now quotes
CLAUDE_PROJECT_DIR (t219 pin bumped 13 -> 14); the Kiro IDE registration
is DROPPED rather than rebased - the reworked IDE adapter's payloads
carry no tool inputs, so a pre-tool matcher has nothing to inspect there;
t220 pins the deliberate absence and the docs/CHANGELOG state the gap.
Codex-review findings, all reproduced before fixing and pinned in t220:
1. Dot-dot traversal: construction/U03/../U01/design.md was judged on the
   first segment after construction/ and allowed. Path components are now
   normalized with .. collapsed against parents before judging.
2. Bare search roots: 'grep -rn TODO construction' / 'find construction'
   name the whole tree with no construction/ literal, so the token scan
   missed them. A quote-stripped pass now blocks any token ending in the
   bare construction component; the word inside quoted content regexes
   stays content.
3. Codex apply_patch Delete File / Move to directives were not fanned out
   to the scope check (patchedFiles covers only Add/Update for the audit
   surface); the reviewer-scope target now includes them as Edit-class
   mutations.
REVIEWER_SCOPE_BLOCKED is event 71; both baseline pins updated with the
lineage comment extended.
Fresh-eyes Claude review verdict APPROVE-WITH-FIXES; all findings fixed:
- Kiro registrations now pass their own agent name as argv[3], forwarded
  as agent_type, so the core hook compares against the dispatch record's
  reviewer field on Kiro too - a stale record naming a different reviewer
  fails open instead of scoping the wrong agent (bare scoped_registration
  remains as the no-arg fallback).
- blockReason now tells a reviewer blocked on a shell variable to use the
  literal unit name, and one blocked on an unquoted bare word to quote the
  pattern; the matcher comment no longer claims variables are invisible
  (they block conservatively).
- The four SKILL.md verdict sentences read the Review section 'from the
  primary artifact' (the inserted delete step had left 'its' pointing at
  the dispatch record).
- Codex adapter persists the ANSWERED exit code (2 or 0) so a duplicate
  delivery replays exactly what the original answered even if the core
  hook ever crashed with exit 1.
- Doc-count stragglers: core/templates/onboarding.md (shipped CLAUDE.md/
  AGENTS.md) and docs/reference/01-architecture.md test-levels row bumped
  to 12 hooks; stale 67 comment in t01.
- Troubleshooting guide: hook roster names all 12 (adds reviewer-scope),
  the settings.json check names PreToolUse, and a new section documents
  the reviewer read-scope refusal message, the
  AIDLC_DISABLE_REVIEWER_SCOPE_HOOK=1 off-switch for repos with their own
  construction/ source dirs, and the stale-record cleanup path.
- Kiro CLI adapter: the write side of the reviewer-scope target now
  iterates a batch operations[] collection like the read side, so a
  batched fs_write across siblings cannot bypass on a missing top-level
  path.
- The blocked-call audit emit is time-bounded (5 x 50ms lock budget,
  emit-unlocked inside) so a lock-starved Bolt fan-out cannot stretch a
  fast refuse to 5s; a contended lock drops the advisory row and records
  a hook drop instead.
- The missing-dispatch-record advisory is rate-bounded to one drop line
  per 10 minutes (marker mtime) so a conductor bug plus a chatty reviewer
  cannot flood the drops file.
- t220 pins the bare 'construction' Grep search root; stage-protocol 12a
  notes the record's stage field lands verbatim in the audit row.
- Follow-up drafted (tmp): shard the dispatch record per unit before any
  parallel per-unit review lands.
Version re-bumped 2.2.20 -> 2.3.4 (2.3.1-2.3.3 landed while this PR was in
review) and the unit test file re-slotted t220 -> t221 (the tier-projection
PR took t220), including the coverage-registry pin. The two kiro reviewer
agent JSONs merged with the tunable-tier projection: the authored files keep
this PR's preToolUse hooks and drop the authored model field (the model dial
is projection-owned since 2.3.1 - the build stamps claude-sonnet-4.5 for the
balanced tier). The troubleshooting doc unions this PR's 12-hook count with
the 2.3.0 hook-drops prose.
@apackeer
apackeer force-pushed the fix/issue-539-reviewer-scope-hook branch from 6d51552 to 66f9c4b Compare July 10, 2026 03:39
@apackeer apackeer changed the title feat: deterministic PreToolUse enforcement of the reviewer read-scope bound (2.2.20) feat: deterministic PreToolUse enforcement of the reviewer read-scope bound (2.3.4) Jul 10, 2026

@leandrodamascena leandrodamascena left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CHANGES REQUESTED.

I like where this is going — a deterministic backstop for a bound that prose lost is the right idea. But I extracted evaluateReviewerScope from the hook and ran it under bun with a {unit:"U03", exempt:[]} record, and I got a dispatched reviewer to read a sibling unit's construction/ content through about a dozen inputs, several of them the way a reviewer would naturally type the command. The round-1 hardening does hold against the shapes it was written for — I confirmed construction/U01/design.md, construction/*/*/*.md, the single-token .., and the whole-path quote all block. The problem is everything one step to the side of it.

The root cause is one design choice: the matcher is a lexical scan for the literal token construction, but the bound it defends is a path-containment property. Those aren't the same thing, and the gap is every finding below. All of these return block:false on the real matcher (I ran each):

  • Recursive search rooted above construction/. grep -rn X ., rg Contract docs/aidlc, find docs/aidlc -name design.md -exec cat {} +. The construction subtree lives under .../intents/<intent>/construction/<unit>/, so any recursion rooted at an ancestor carries no construction token and is allowed — while descending into every sibling. This is the exact "14 recursive greps" the PR names as motivation; the reviewer just roots one level up instead of typing construction/*/.
  • Pathless Grep/Glob. Grep {pattern:"PaymentGateway"} with no path/glob contributes zero candidate strings, so the matcher loop never runs and the tool recurses cwd. The reviewer JSONs ship execute_bash/fs_read, so it's reachable.
  • Case. Read {file_path:"Construction/U02/design.md"} — one capital letter opens the real lowercase file on macOS/Windows. No toLowerCase anywhere.
  • Wildcard in the word / quote-split / cd+relative / bare relative. cat construction*/U01/x, cat "construction"/U01/x, cd construction/U03 && cat ../U01/design.md, Read {file_path:"../U01/design.md"} — all allow. The shell resolves them; the matcher never sees a contiguous construction/<sibling> token.

There's also a fail-closed twin of the case bug: scoped to U01, Read {file_path:"construction/u01/design.md"} returns block:true — a reviewer's own-unit read refused. Lower impact (§12a hands it the literal path), but same missing FS-case normalization, so fix both together.

My suggestion is to move the decision off the string and onto the resolved path: the hook already has the record's unit and path, so judge whether a resolved path or a search root lands inside a sibling construction/<other-unit>/, rather than whether the string spells construction. Add case-insensitive comparison, treat a pathless recursive Grep/Glob as rooted at cwd, and widen the tool allow-list (NotebookRead and anything outside the six-name matcher fail open today). Then re-pin t221 with the ancestor-root, pathless, case-variant, and cd-relative shapes — every current block case names construction explicitly, which is why the suite passed.

One structural note, not a blocker: the only writer of .aidlc-reviewer-dispatch.json is prose in the four SKILL.md orchestrators. If the conductor skips step 1 the hook fails open. I think that's acceptable — it's defense-in-depth over the reviewer's own prose bound, fail-open is the safe direction, and forgetting the write falls back to today's baseline, not worse. But it means this is a second layer, not a replacement, and the matcher coverage is what has to change before it earns the "deterministic twin" framing.

Non-blocking: Kiro IDE prose-only is the right call and well documented; product-lead wires the hook but reviews no per-unit stage so it always fails open (harmless, but dead wiring); the block-reason text tells a blocked reviewer to "quote the pattern," which nudges toward the exact quoting that flips a block to an allow — worth changing regardless.

Replace the lexical construction-token matcher with path/root containment checks for reviewer-scope enforcement. Block ancestor-root recursion, pathless Grep/Glob, case variants, wildcard or quote-split construction components, and cd-relative sibling reads while keeping current-unit and exact-exempt paths allowed.

Widen the Claude PreToolUse matcher to file/search/shell surfaces, update docs/CHANGELOG, regenerate dist, and pin the review bypass shapes in t221.
@apackeer

Copy link
Copy Markdown
Contributor Author

Addressed the blocking reviewer-scope findings in 12112403.

What changed:

  • Replaced the lexical construction token scan with path/root containment checks keyed off the dispatch record root.
  • Blocks ancestor-root recursive searches (grep -rn X ., rg ... aidlc/spaces/default/intents, find ...), pathless Grep/Glob, case variants, wildcard or quote-split construction components, cd + relative sibling traversal, and bare relative sibling reads from the current-unit cwd.
  • Keeps current-unit paths and exact exempt files allowed.
  • Widened the Claude/core reviewer-scope tool surface to file/search/shell tools (NotebookRead, MultiEdit, NotebookEdit, LS included) and removed the old block-reason nudge to "quote the pattern".
  • Updated t221, docs, CHANGELOG, and regenerated dist/.

Verification:

  • bun test tests/unit/t221-reviewer-scope-hook.test.ts -> 61 pass
  • bun scripts/package.ts --check -> all harness trees in sync
  • bun test tests/unit/t150-codex-packaging.test.ts tests/unit/t149-codex-hook-adapter.test.ts tests/unit/t147-kiro-hook-adapter.test.ts -> 42 pass
  • bun test tests/unit/t68-version-changelog-sync.test.ts tests/unit/t132-hooks-doc-count-sync.test.ts tests/smoke/t01-file-structure.test.ts tests/smoke/t02-hook-executability.test.ts tests/smoke/t03-settings-json.test.ts -> 62 pass
  • bash tests/run-tests.sh --debug -P 8 --smoke --unit -> PASS, 153 files / 2632 assertions, logs at tests/logs/2026-07-12T00-28-21Z

@apackeer
apackeer merged commit 3c76878 into v2 Jul 12, 2026
@apackeer
apackeer deleted the fix/issue-539-reviewer-scope-hook branch July 12, 2026 12:20
apackeer added a commit that referenced this pull request Jul 12, 2026
PR #545 took 2.3.4 and the t221 slot on v2. Version trio re-bumped to
2.3.5 (aidlc-version.ts + CHANGELOG heading + README badge); the four
plugin-selection tests re-slot to t222-t225 (t221 is the reviewer-scope
hook's); audit-event pins merge both sides' additions (72 events, 19
categories: REVIEWER_SCOPE_BLOCKED from #545 + PLUGIN_SELECTION_CHANGED
from this branch); coverage registry + ratchet regenerated; dist regen
in sync.
apackeer added a commit that referenced this pull request Jul 13, 2026
…me plugin selection (2.3.5) (#550)

* feat: rename plugin ownership frontmatter from bundle: to plugin:

One noun everywhere: the distribution unit, the ownership tag on
stages/contributions, and (in later commits) the selection surface are
all "plugin". "bundle" was legacy naming from the extension-mechanism
era; a plugin defines exactly one ownership tag in every real case, so
the two-word split bought nothing and confused authors. The word
"bundle" is deliberately left free for a possible future collection-of-
plugins concept.

- Schema: plugin: is canonical; bundle: remains a deprecated read-side
  alias (both keys known). Both present with different values fails
  validation; the validator returns a normalized clone exposing only
  plugin. parseStageFrontmatter synthesizes plugin from an alias-only
  file; emitStageFrontmatter writes plugin: only (and now carries
  number/name/plugin in FIELD_ORDER so plugin-authored frontmatter
  round-trips).
- Compose hook: contributions read plugin: first, fall back to bundle:,
  drop-log a conflict. Fragment sentinel markers were already
  <!-- plugin:... --> and are byte-unchanged.
- test-pro, docs 18 + 10, and fixtures author plugin:; t188 pins the
  alias contract (one bundle:-only fixture still composes; a conflicting
  pair drop-logs), t62 pins schema accept/reject, t64 pins parse->emit->
  parse round-trip for plugin metadata and the alias.

No behavioral change for installs authored with bundle:.

* feat(plugins): project and compose scopes, agents, and knowledge buckets

A plugin can now ship scope definitions, agent personas, and per-agent
knowledge alongside stages/sensors/tools/contributions. Proven at scale
by a customer pilot (a real plugin shipping 3 scopes, 18 personas, 90
knowledge files) before landing here.

- packager: scopes/agents/knowledge join the plugin projection
  contentDirs; walk() is recursive so knowledge/<agent-slug>/ subtrees
  project without special cases.
- compose hook: three copyTreeNoClobber calls route the buckets into the
  harness scopes/agents/knowledge dirs - additive-only, same no-clobber +
  {{HARNESS_DIR}} substitution + collision drop-log as stages. A plugin
  ADDS a persona; it never overwrites a core one.
- naming: plugin scope files are <plugin>-<name>.md and agent files
  <plugin>-<role>-agent.md - the plugin prefix replaces core's aidlc-
  filename prefix, stem == frontmatter name.
- test-pro gains a scope, an agent, and a knowledge dir as living proof;
  its content test's agent roster now unions the plugin's own agents/
  bucket so a stage naming a plugin-shipped persona validates at author
  time (previously only the core roster counted - a plugin could never
  satisfy it).
- stage-body-non-empty guard in the plugin content tests AND t188: a
  frontmatter-only stage file compiles, routes, and passes every other
  check while being behaviorally dead (a real pilot regression shipped
  23 such stages); now it fails loudly at author and compose time.
- doc 18 Section 6 rows flip from deferred to implemented for the three
  buckets; memory/ stays deferred deliberately (it targets the workspace
  method tree where core ships org/team.md - real collision semantics
  that warrant their own design pass).

* feat: plugin ownership through compile, plugin-namespaced runner skills, compose-time runner regen

Wires plugin: ownership end to end - previously the schema accepted it
and the compiler dropped it, so nothing downstream could act on it.

- compile: plugin: carries from stage frontmatter onto the compiled
  stage-graph.json node (absent on core nodes, so core emission is
  byte-identical). Invariants: a plugin-owned slug must start with
  "<plugin>-", and plugin: aidlc is rejected (core-ness = omitting the
  field). Authored number/name stay on the existing re-seeded path -
  carrying them is deliberately out of scope here.
- runner skills: a plugin-owned stage's runner is named by its own
  already-prefixed slug (/test-pro-integration); core stages keep
  aidlc-<slug> byte-identically. Drift detection parses the driven slug
  from the skill body instead of assuming the aidlc- prefix.
- scope runners: the hardcoded FIRST_BATCH constant is replaced by scope
  frontmatter - runner: true selects a scope into the default batch. The
  four shipped scopes carry the flag so shipped output is unchanged;
  FIRST_BATCH remains as a derived compatibility export. Plugin-owned
  scopes (plugin: in frontmatter) get bare-name runners.
- compose hook: after a successful recompile it now regenerates stage
  runners (and scope runners when the plugin ships scopes/), so a
  composed plugin stage's slash command appears the moment it composes
  in - previously routable but not typeable. Failures drop-log; installs
  without a skills dir (codex-style, skills emitted at package time)
  skip with a note. New self-heal probe: plugin stage in the graph but
  missing its runner dir triggers regeneration even when nothing else
  changed.
- t221-plugin-runner-naming pins carry-through, both invariants, the
  naming rules, and data-driven batch selection; t188 asserts composed
  runner skills exist and core runners are untouched.

* feat: enforce filename==identity at compile + duplicate-name guards in loaders

The naming conventions the docs present as binding were enforced for
sensors only; stages, scopes, and agents silently tolerated mismatches
that break path-derived lookups (runtime stage_file paths, knowledge
dirs, drift guards) or last-write-win. With plugins now composing
stages/scopes/agents into shared install dirs, both hazards became
plugin-vs-core collisions - enforcement is load-bearing.

- compile: a stage file whose name stem differs from its frontmatter
  slug fails loudly (file, stem, slug all named). The schema comment
  that punted this check to "the parser" now points here.
- loaders: duplicate scope names and duplicate agent slugs across two
  files throw at load naming BOTH files (previously silent last-write-
  wins), mirroring the sensor loader's discipline. New AIDLC_AGENTS_DIR
  env seam + _resetAgentsForTests, matching the scopes/sensors seams.
- doctor: advisory rows for agent/scope files whose stem does not match
  their declared name (plugin-owned files must match exactly; core
  scopes accept the aidlc- filename prefix). Advisory, not hard-fail: a
  mismatch is recoverable misconfiguration.
- plugin content tests gain stem==name assertions so plugin authors
  catch it before compose; t222-naming-enforcement pins the compile
  error, both duplicate guards, the env seam, and the doctor advisory.

* feat: generated Stage Graph table in SKILL.md + state-template becomes the contract

Two generated-surface cleanups that unblock install-time plugin
selection: every surface that enumerates stages must derive from the
compiled graph, because selection filters that graph.

- stage-table: new utility verb mirroring scope-table - renders the
  SKILL.md Stage Graph table from compiled stage-graph.json inside
  BEGIN/END markers, --check byte-compares for drift. All four harness
  SKILL.md copies now carry the generated region. The hand-maintained
  table had already drifted in two Mode cells (reverse-engineering and
  code-generation carried prose annotations the compiled graph does not
  have; the agent names they duplicated are already in the Lead/Support
  columns) - the generator is authoritative.
- compose hook refreshes both generated regions (scope-table +
  stage-table) after a successful recompile, so composed plugin stages
  appear in the table the moment they compose in; missing SKILL.md or
  marker skips with an advisory drop. t188 asserts test-pro's stages
  land in the composed region.
- t32 repointed: bidirectional hand-sync check becomes stage-table
  --check drift guard + a coverage assertion that the rendered table
  names every compiled stage exactly once.
- state-template.md now documents the state-file section/field contract
  only (headings, field bullets, checkbox legend, one placeholder row);
  the engine is the sole source of stage enumeration - the template's
  hand-enumerated stage list was dead data that had drifted. state-init
  and the reference/troubleshooting docs point at the compiled grid and
  the --doctor resync path instead of the template.

* feat: install-time plugin selection - select-plugins, closure guard, transactional regen

Plugins add, the install selects. An install can now choose which
plugins' content its users see: a plugins list in tools/data/
harness.json filters the graph compile to enabled plugins (core is the
implicit "aidlc" plugin; the three initialization stages are always
included), and every generated surface (runner skills, scope grid,
SKILL.md stage/scope tables, state rows, doctor) follows the filtered
graph automatically. A test-pro-only install shows only /test-pro-*
commands and the test-pro scope while core remains installed and
re-enableable. An absent plugins key means everything is enabled, so
existing installs are unaffected and the shipped dist is byte-identical.

- select-plugins utility verb: validates requested names against the
  known plugin set, writes the selection, recompiles, regenerates
  runners (pruning newly-disabled ones) and both SKILL.md generated
  regions - one step, TRANSACTIONAL: harness.json + stage-graph.json +
  scope-grid.json snapshot before mutating; any late-step failure
  restores all three and re-runs the regeneration chain against the
  restored selection, with a loud recovery message if that also fails.
- compiled graph persists the FULL stage set; disabled nodes carry
  enabled: false (key omitted when enabled). loadStageGraph() filters
  for all runtime consumers; loadStageGraphAll() serves doctor and
  selection tooling. Stage numbers are seeded over the full graph, so a
  disable/re-enable round trip is number-stable (pinned byte-for-byte).
- closure guard: a selection leaving an enabled stage consuming a
  required artifact whose only producers are disabled fails compile
  naming consumer, artifact, producers, and the plugin(s) to enable.
- scope fallbacks degrade gracefully under a core-disabled selection:
  sole-enabled-plugin installs fall back to that plugin's first scope;
  multi-plugin selections keep the hard error.
- compose does not auto-enable: composing a plugin into an install with
  an active selection that excludes it lands the files and records an
  advisory drop naming the select-plugins command; the self-heal probes
  respect enabled flags so a disabled stage is not a compile trigger.
- doctor: enabled plugins + per-plugin stage counts; hard-fails on
  selection/graph disagreement and on torn-run leftovers, naming the
  recovery command.
- test-pro's cross-plugin consumes edges relax to required: false so a
  test-pro-only selection satisfies closure (its stage bodies already
  describe those inputs as use-when-present).
- t223-plugin-selection: full journey over the shipped test-pro plugin -
  select, verify surfaces, re-enable with byte-identical numbers, prune,
  unknown-name error, rollback on late-step failure, closure-guard
  message; t188 gains the compose-not-enabled advisory case.

* feat: decouple the walking-skeleton ceremony from core scope names

The engine's last behavior keyed on hardcoded core scope NAMES was the
walking-skeleton stance: SKELETON_ON_SCOPES, a six-name code constant a
plugin scope could never join without a code edit. The stance now
resolves through the active scope's own file.

- new optional scope frontmatter skeleton: on|off, read through
  loadScopeMetadata; absent = off (composed scopes reshape an existing
  plan and must not conjure a skeleton Bolt; plugin scopes opt in
  explicitly). Invalid values throw naming the file. All nine core
  scopes declare the field explicitly - the same six resolve on as the
  deleted constant, pinned by test.
- env-scope fallback ADOPTED, not just validated: resolve-env-scope and
  orchestrate's resolveScope previously let an AWS_AIDLC_DEFAULT_SCOPE
  naming a disabled/unknown scope through to a generic unknown-scope
  death; both now route through selectionAwareDefaultScope, so a
  plugin-only install with the shipped core default starts from plain
  /aidlc. selectionAwareDefaultScope itself now keys off scope OWNERSHIP
  (core vs plugin) rather than owner-bucket count, fixing a wrong
  fallback when core scopes were enabled but the preferred one disabled.
- prose decoupled: conductor.md, org.md, rules-reading.md, and the four
  SKILL.md copies describe the ceremony via the skeleton: field instead
  of name lists; stage-protocol.md's hardcoded per-scope stage-count
  table is replaced by a pointer at the compiled grid (scope-table).
- t224-scope-name-decoupling: a static probe (comment-stripped source
  scan) asserts no core tool regrows a 3+ core-scope-name literal;
  fixture tests pin on/off/absent/invalid parsing; the env fallback is
  pinned under a plugin-only selection.

* fix: doctor selection coverage hard-fails only under an active selection + help-text harness seam

Three tier reds, two defects:

- The new Enabled-stage-compile-coverage doctor check hard-failed on ANY
  uncompiled stage file, breaking the pre-existing contract that an
  uncompiled stage without a selection is a deliberate authoring state
  surfaced as an advisory (t184's pin). The hard-fail is now gated on an
  active selection - there a missing node means a torn select-plugins
  run; without one, the pre-existing advisory row owns the case.
- The select-plugins help-text example hardcoded .claude/tools instead
  of the ${harnessDir()} seam (t153 in core, t150 after packaging
  propagated it to the codex tree verbatim).

* fix: integration-tier reds - protocol depth table, export fixture, dupe-slug fixture route

Four integration reds after the branch's feature commits, all
adaptation gaps rather than engine defects:

- t34: WP7's prose decoupling deleted the per-scope stage-count table
  wholesale, but t34's contract (from the original .sh) is that the
  depth section lists every scope. Restore a scope->default-depth table
  (mirroring the scope files' depth: frontmatter - names and depths
  only, no stage counts, which stay banned in this protocol).
- t66: the designer-export golden fixture predates the runner/skeleton
  scope frontmatter; regenerate it (delta is exactly the new fields on
  the scopes block).
- t-custom-harness-compile E5: the duplicate-slug fixture wrote the
  dupe under a mismatched filename, which now trips the new stem==slug
  guard before the duplicate-slug guard it pins. Route the dupe through
  a different phase dir with the slug as its stem so the intended guard
  fires.
- t78 red was a parallel-load flake (5s timeout): green alone, no
  change.

* chore: version 2.3.4 - changelog, badge, dist regen

Consolidated entry for the plugin-selection feature set: plugin content
buckets (scopes/agents/knowledge), plugin ownership through compile,
namespaced runners, install-time selection with transactional
regeneration and the closure guard, generated stage table, state
contract, skeleton frontmatter decoupling, and the bundle->plugin
rename. t68 pins version/changelog/badge agreement.

* feat: reject the renamed bundle: key outright - no read-side alias

The bundle->plugin rename originally kept bundle: as a deprecated
read-side alias in three layers (parser synthesis, schema
normalization, compose fallback), each with slightly different
semantics. The alias is gone: one word, one key, everywhere.

- schema: a stage carrying bundle: fails validation with 'bundle: was
  renamed; write plugin: for ownership' - a targeted error naming the
  fix, not a generic unknown-key.
- parser: back to a dumb extractor; no alias synthesis. The schema is
  the single normalization boundary.
- compose hook: a contribution carrying bundle: (alone or beside
  plugin:) is skipped with a drop naming the rename - a stale plugin
  tree fails visibly instead of composing under wrong or ambiguous
  ownership.
- t62/t64/t188 flip from alias-accepted pins to rejected-with-named-fix
  pins; docs and CHANGELOG drop the alias language. 'bundle' remains
  reserved for a possible future collection-of-plugins concept and is
  otherwise absent from the mechanism.

* fix: version-skew guard on plugin-owned content copy

Composing a plugin that ships plugin:-keyed stages/scopes/agents onto an
installed engine whose schema predates the plugin: ownership key broke that
install's graph compile permanently (the retry marker re-failed every
SessionStart). compose now probes the INSTALLED schema for plugin:
acceptance up front; on rejection it skips the plugin-owned content copy
with a degraded drop naming the remediation and does not write the retry
marker for that cause (retrying cannot fix an old engine). knowledge/,
sensors/, and tools/ still copy. t188 gains an old-engine fixture proving
no copy, the drop, no marker, and a still-green compile.

* fix: frontmatter-name collision guard in compose + statusline fail-open

copyTreeNoClobber was filename-only: a plugin agents/scopes file with a
unique filename but a frontmatter name duplicating an installed agent/scope
landed cleanly, then the duplicate-name throws in loadAgents and
loadScopeMetadataAll bricked compile, orchestrate, help, and the statusline
on every prompt with no drop attributing the plugin. compose now parses each
agents/scopes file's frontmatter name and checks it against the installed
roster before copying; a collision skips that file with a degraded drop
naming the plugin, the file, and the colliding installed file. The
statusline's agentDisplayMap additionally degrades to slug display when
loadAgents throws (hooks fail open by design). t188 gains a synthetic
collision fixture proving skip + drop + compile green + statusline exit 0;
t61 gains a duplicate-name slug-fallback case.

* fix: composed scopes survive plugin selection unconditionally

A composed scope (runtime-approved user state: a scopes/aidlc-<name>.md with
no plugin: field plus a hand-appended grid entry no stage frontmatter
produces) was destroyed by plugin selection twice over: filterScopeGrid
stripped it from the written grid when core was deselected (unrecoverable -
the transpose cannot re-derive it), and even with core enabled the transpose
seeded its name from enabledScopeNames and fabricated a flattened init-only
twin that shadowed the real on-disk entry in mergeComposedScopes. The grid
pipeline now derives the composed set (on-disk grid keys with no stage
frontmatter producer over the FULL stage list), excludes them from transpose
seeding, and passes them to filterScopeGrid as an explicit exempt set - a
composed entry round-trips byte-identical through any select-plugins run.
discoverScopes keeps a composed scope's runner via written-grid membership
so the select-plugins regen chain no longer prunes it. t223 gains a
composed-scope journey: seed, select test-pro only, re-enable both,
byte-identical grid entry each time, runner intact, and next --scope routes
a run-stage directive for the real first EXECUTE stage.

* fix: runner-prune provenance, empty-batch gate, lazy scope batch

Three runner-gen hazards closed. (1) Both prune sites deleted ANY skills dir
whose SKILL.md matched the command signature - a user-authored skill that
documents those commands was rm -rf'd silently. Generated runners now carry
a generated-by: aidlc-runner-gen frontmatter marker; pruning requires
signature AND marker, with a one-release legacy rule for pre-marker installs
(dir named exactly aidlc-<slug>, or the bare slug when it is plugin-owned).
A signature-matching dir that is neither is left alone and listed on stderr
as unmanaged. (2) An empty scope batch pruned every scope runner and exited
0 success-shaped, indistinguishable from a stale-scopes upgrade or a
mispointed AIDLC_SCOPES_DIR; prune-on-empty now runs only under an active
selection, otherwise it warns naming the two likely causes and preserves
runners; discoverScopes warns on an unreadable scope dir. (3) The eager
FIRST_BATCH module const ran scope-dir I/O at import and threw on one
malformed scope file, killing even write; deleted - importers (t123 twins,
t130, codex emit) call defaultScopeBatch() at use time, and a malformed
scope file no longer kills write. t221 gains six cases pinning all three.

* fix: skills-dir guard, env-fallback note, doctor parse-fail coverage, covers headers

Four small hardening fixes. select-plugins no longer errors on a Codex-style
install with no harness skills/ dir: regenerateSelectionSurfaces skips the
two runner-gen spawns with a note naming the skipped surface and the real
path (mirrors compose's advisory; never mkdirs). The
AWS_AIDLC_DEFAULT_SCOPE sole-plugin fallback is no longer silent: the lib
result carries a note field and both env call sites print it to stderr
naming the substituted scope; exit code and stdout unchanged. Doctor's
enabled-stage-coverage walk no longer skips a stage file whose frontmatter
fails to parse when a selection is active - it cannot be proven in the
graph, so it counts as missingEnabled with the parse error in the fix
message; the two issue-number references in doctor comments are rewritten
as prose contracts. t223/t224 covers: headers rewritten in the registry's
class:id grammar; the regenerated coverage registry flips
subcommand aidlc-utility select-plugins, function:selectionAwareDefaultScope,
function:pluginsEnabled, and function:mergeComposedScopes to covered.

* refactor: cleanup set - table-check dedup, shared predicate, marker grammar, fixtures hoist, doctor dedup, selection audit event

Six review cleanups. The stage-table/scope-table --check twins collapse
into one checkGeneratedTableRegion helper, and replaceGeneratedRegion now
shares the same marker-locating core (findGeneratedRegion) so all three
surfaces validate duplicate/out-of-order markers identically. The
stage-enabled-by-selection predicate is exported once from aidlc-lib
(stageEnabledBySelection) and used by graph compile and doctor; compose
keeps its self-contained copy with a sync comment. The scope-table BEGIN
marker drops its em dash for the hyphen grammar the stage-table already
uses (constants, compose copies, all four harness SKILL.md files; an
installed SKILL.md still carrying the old marker degrades to compose's
existing missing-BEGIN advisory drop, no error). The triplicated test
scaffolding hoists into fixtures.ts withEnvAndFreshCaches (delete-aware
env restore + all five cache resets) with t221/t222/t223/t224 converted.
Doctor reuses a parameterized artifactsRegistryFor and a shared
frontmatterBlock helper instead of hand-rolled twins. select-plugins'
set-mode emits a PLUGIN_SELECTION_CHANGED audit event (previous + new
selection) - registered in VALID_EVENT_TYPES, documented in the state-
machine chapter and audit-format registry, pinned by the count tests
(70 -> 71) and asserted end-to-end in t223.

* docs: fold the review-fix behavior changes into the 2.3.4 changelog entry

The compose skew guard, the frontmatter-name collision guard, the runner
provenance marker + empty-batch gate, composed-scope selection survival,
the PLUGIN_SELECTION_CHANGED audit event, the env-fallback stderr note,
the doctor parse-fail coverage rule, the Codex skills-dir note, and the
scope-table marker grammar change are all user-visible amendments to the
same unreleased 2.3.4 - no new version.

* fix: lazy-require core cache resets in test fixtures

The withEnvAndFreshCaches hoist gave fixtures.ts module-load-time imports of
core/tools/aidlc-graph.ts and aidlc-lib.ts. t52's drift meta-test runs t48
inside a sandbox copy that carries dist + docs + tests but no core/, so
importing fixtures.ts there failed module resolution and broke all six t52
cases. The resets now load the two modules with createRequire at call time
(no sandbox test calls them); resolution identity with the tests' own ESM
imports verified behaviorally via the t221/t222/t223/t224 consumers.

* chore: rebase onto v2 2.3.4 - re-bump to 2.3.5, re-slot tests t222-t225

PR #545 took 2.3.4 and the t221 slot on v2. Version trio re-bumped to
2.3.5 (aidlc-version.ts + CHANGELOG heading + README badge); the four
plugin-selection tests re-slot to t222-t225 (t221 is the reviewer-scope
hook's); audit-event pins merge both sides' additions (72 events, 19
categories: REVIEWER_SCOPE_BLOCKED from #545 + PLUGIN_SELECTION_CHANGED
from this branch); coverage registry + ratchet regenerated; dist regen
in sync.

* fix(test): t225 bracket scan - linear scanner replaces backtracking regex

The array-literal probe's regex form backtracked past the 5s test timeout
on the rebased aidlc-lib.ts and, worse, could silently skip literals when
a match starting at an earlier bracket spanned them inside a quoted-string
alternative. A linear bracket-stack scanner is O(n) and found one real
coupling the regex missed: the workspace-detection greenfield advisory's
three incremental scope names. That advisory predates the probe, so it is
exempted by exact signature; any new literal (or that one growing) still
fails.

* fix: schema-precheck plugin stage files at compose copy time

Graph compile is all-or-nothing: aidlc-graph.ts throws on the first
schema-invalid stage file, so one bad plugin stage copied into the
install (e.g. a stale tree still authoring the renamed bundle: key)
bricked every later compile of the whole install with no self-heal.
The compose stage copy now validates each plugin stage file against
the INSTALLED engine's parser + validator before it lands: an invalid
file (or a frontmatter-only empty body) is skip-and-dropped with the
file and errors named, the rest of the plugin composes normally, and
the self-heal probe excludes deliberately-dropped slugs so it doesn't
force a futile recompile every session. Fails open when the installed
lib can't be loaded (a partial install already can't compile).

* fix: reserve the aidlc- prefix for core - plugin names may not claim it

runnerDirName returns the bare slug for plugin stages but aidlc-<slug>
for core, so a plugin named aidlc-<x> generated runner dirs identical
to core runner paths and runner-gen write clobbered them silently -
/aidlc-<slug> could route to the wrong stage. Four chokepoints now
refuse the name: graph compile rejects plugin: aidlc-* stage
frontmatter (the install-path enforcement point), the compose stage
precheck mirrors the same invariants so a bad stage file drops instead
of bricking compile, loadScopeMetadataAll rejects aidlc-* plugin: in
scope frontmatter (covers scope-runner dirs), and the packager refuses
an aidlc/aidlc-* plugin directory at discover + plugin-build time.

* fix: select-plugins refuses to strand an active workflow; doctor flags a stranded one

Disabling a plugin an active workflow depends on hard-errored every
later /aidlc on that workflow (the state file's scope out-ranks
--scope, so 'Unknown scope' had no in-band recovery) while doctor
stayed green. select-plugins set-mode now enumerates every
non-complete workflow across all spaces and refuses when the new
selection would disable (a) the plugin owning the workflow's scope or
(b) a plugin owning a pending EXECUTE stage in its plan, naming each
dependency and the remediation (complete or park the workflow, or
keep the plugin). Doctor gains a matching row that fails when the
CURRENT selection (pre-guard or hand-edited) already strands one.

* fix: disabling a plugin now strips its merged contributions from core stages

select-plugins disable removed a plugin's own stages/scopes/runners but
left its merged produces/sensors/consumes/required_sections and spliced
prose welded into CORE stage source and the compiled graph - a disabled
plugin kept steering enabled stages. Compose now records what it
ACTUALLY added per target stage in a per-plugin sidecar
(tools/data/plugin-contrib-<key>.json; actually-added entries only, so
removal can never strip a value core already had), and select-plugins
strips on disable: structural adds via the sidecar, prose fragments via
their existing sentinel markers, with stripped stage files joining the
transaction's rollback snapshots. Re-enabling restores everything on the
next session start (the plugin's compose hook re-merges; round-trip is
byte-identical). Compose also stops merging contributions for a plugin
the current selection disables - stage copies stay (runtime-filtered),
but contribution merges into unfiltered core source would have undone
the disable-time strip every session.

* fix: non-blocking review set - adds parse shortfall log, dropped ordering-edge advisory, packager manifest error, changelog + doc accuracy

Four smaller review items plus the doc updates the bigger fixes need:
- adds.produces/sensors entries that fail the 4-space parse now
  drop-log a parsed-N-of-M shortfall naming the indentation rule,
  mirroring the consumes parser (they truncated silently).
- An enabled stage whose requires_stage names a selection-disabled
  stage is surfaced by doctor as an advisory listing each dropped
  ordering edge (not a closure error: the edge is vacuous when the dep
  never runs, and plugin-only installs legitimately order plugin
  stages after core ones).
- A malformed plugins/<name>/.aidlc-plugin/plugin.json fails the
  packager with the plugin and file named instead of a raw JSON.parse
  stack.
- CHANGELOG: the empty-body stage guard is now accurately described
  as the compose stage-file precheck (it was claimed as a runtime
  compose guard while test-only); the entry also gains bullets for the
  contribution strip, the strand guard, and the aidlc- name
  reservation. Doc 18 documents strip/restore semantics, the
  not-filtered agents/knowledge surfaces, the dropped-edge advisory,
  and the reserved names; the CLI guide documents the strip + refusal.

* chore: regenerate coverage registry after review-fix test additions

* style: hyphens for em dashes in lines added by this review-fix series

* chore: bun.lock configVersion metadata from the current bun

A newer bun adds a configVersion field to the lockfile on install,
leaving every fresh checkout permanently dirty on git status. One-line
metadata; no dependency changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants