Skip to content

refactor(agents)!: action-verb agent roster, audit-claude removal, agents TUI + HUD fixes - #287

Merged
dean0x merged 97 commits into
mainfrom
wave/agent-roster
Aug 19, 2026
Merged

dean0x merged 97 commits into
mainfrom
wave/agent-roster

Conversation

@dean0x

@dean0x dean0x commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Do not merge yet. Two verification steps remain that cannot be run from an automated session — see Before this can merge. Everything else is done and evidenced.

What this is

An integration branch carrying five related changes to devflow's agent system. Four were built as separate, individually-reviewable PRs (#282, #283, #284, #285) that each merged into this branch rather than into main, because they are individually reviewable but only jointly verifiable — PR 4's checks exercise a column PR 3 added, and PR 2's count changes ripple into PR 4's documentation. Reviewing them as one diff against main is the intended path.

A fifth change — a fix to the HUD status line — was found while reviewing this work and is merged in as fix/hud-base-branch. It is described below alongside the others.

main has been untouched throughout. If this branch is abandoned there is nothing to revert.

Why

Three unrelated problems, bundled because they all land in the same blast radius:

  1. The agent roster read as actor-nouns (coder, designer, reviewer) where action-verbs were wanted (code, design, review). Cosmetic in isolation, but renaming an agent in this codebase is genuinely dangerous — see the next section.
  2. The CLAUDE.md audit feature was unwanted and needed removing outright.
  3. The devflow agents TUI had four defects, found by running it in a real terminal: aliases and full model IDs both appeared as separate picker stops for the same model; GPT selections were silently accepted while inert; there was no install-state column; and agent names rendered lowercase.

The one thing a reviewer must understand first

A devflow agent's identity is three independent strings, and nothing in the codebase links them.

Form Example Authoritative in
A — slug bug-analyzer filenames, DEVFLOW_PLUGINS[].agents[], ~/.claude/agents/devflow/{slug}.md, agent-models.json keys, devflow agents --set {slug}
B — spawn key BugAnalyzer the frontmatter name: on line 2 of each agent file
C — prose "the Coder agent" agent bodies, skills, the orchestrator charter, rosters, docs

Form B is not derived from form A, and it diverged in the shipped tree: bug-analyzer.md declared name: BugAnalyzer (hyphen dropped), with the slug appearing nowhere in the file.

The critical part: nothing in src/ ever parses that name: field. Only Claude Code does, at spawn time. So a rename that correctly updates the filename, the registry, and every subagent_type literal — but misses line 2 of one agent file — leaves every test green and every spawn broken at runtime. There was no guard for this; the equivalent guard existed for skills but had never been written for agents.

That risk is why PR 4 lands guards before it renames anything, and why the guard work is worth reviewing more carefully than the rename itself.


What's in it

PR #282 — install/uninstall lifecycle (9 files)

Foundation for the rest. Cleanup was previously asymmetric: skills got a registry-diff sweep (gated to full installs only), while agents and commands relied on hand-maintained legacy name lists.

  • New shared src/core/orphan-sweep.ts; one compute site used by both the installer and the uninstaller.
  • The sweep is now ungated and covers agents, commands, and skills — it runs on every install shape, including --plugin partial installs.
  • Wired up getAllCommandNames(), which existed but was unused anywhere in src/.

The safety property to check: getAllAgentNames() / getAllCommandNames() / getAllSkillNames() span all plugins regardless of which are selected. That is what lets assets belonging to unselected plugins survive a partial install — the sweep deletes only names that left the registry entirely. If any code path intersects with the selected-plugin subset, that is a data-loss bug.

This is also why PRs 2 and 4 add zero legacy-list entries: retired names are swept automatically once they leave the registry.

Uninstall fixes in the same PR: removeAllDevFlow / removeSelectedPlugins / isDevFlowInstalled are now exported (previously unexported and therefore untestable); the selective path sweeps retired assets and calls revertExternalAgents; --keep-docs is honored in resolveDevflowDirCleanup (previously an active data-loss path that could prompt to wipe skill shadows and preference-profile.md); the artifact list is completed; and a containment precondition prevents any artifact path from resolving to ~/.devflow itself or above it.

A classification invariant was introduced and is enforced by a test: enumerateUserDevFlowContent (user state, survives unless explicitly confirmed) and the install-artifact list (removed on every path, including decline, cancel, and --keep-docs) must be disjoint. A name in both makes the confirmation prompt untruthful — it is listed as user content that removal would take, then deleted regardless of the answer. agent-models.json and migrations.json are artifacts; hud.json is user state.

PR #283 — remove the audit-claude feature (23 files)

Deletes the devflow-audit-claude plugin, the claude-md-auditor agent, the /audit-claude command, and the now-dead non-selectable-optional carry mechanism.

Why the carry was deleted rather than retargeted: devflow-audit-claude was the only plugin both optional: true and inside partitionSelectablePlugins's EXCLUDED, so once it is gone the helper's only reachable return value is [] — a guard for a now-impossible state. Keeping it would have meant retargeting its 17 it() blocks at synthetic fixtures, asserting the helper's algebra against plugins that do not exist. It is replaced by a recorded JSDoc invariant at the surviving seam: an optional plugin added to EXCLUDED needs a re-init carry.

Documentation counts were re-derived, not decremented — the previous "23 plugins (12 core + 10 optional language/ecosystem + 1 optional workflow)" was already wrong on both the split and the language-plugin count. Now 22 plugins (12 core + 10 optional) and 16 agents.

PR #284 — agents TUI (11 files)

  • Aliases-only picker, rendered bare. Per model in registry order: contribute all of its aliases, and its canonical ID only if it has none. sol (gpt-5.6-sol) now renders as sol. renderModelCell drops from four branches to three. The other two parentheticals are deliberately kept — default (opus) reports the shipped default and (unavailable) is a status marker, not an identifier.
  • The rule is entirely catalog-driven and requires no devflow-side maintenance. Nothing is hardcoded; new models from subswitch appear automatically, and a model that later gains an alias starts rendering as the alias with no code change.
  • Inertness at selection time — the dormancy marker now appears on the keypress (and, after review, correctly clears on one).
  • A fourth STATE column (budget 2+18+32+13+13 = 78 ≤ 80), plus orphan agent-models.json keys rendered as editable rows.
  • Capitalized names in the TUI only. --list deliberately stays lowercase: its AGENT column is an identifier users copy into --set, which exact-matches.

src/core/model-discovery.ts is byte-identical on purpose — its selectableNames doubles as the --set validation allowlist, so narrowing it would have rejected --set coder --model gpt-5.6-sol.

PR #285 — the rename (97 files)

Old New Old New
coder code simplifier simplify
designer design skimmer skim
evaluator evaluate synthesizer synthesize
researcher research tester test
reviewer review triager triage
scrutinizer scrutinize validator validate
bug-analyzer diagnose

Unchanged: git, knowledge, learning.

/bug-analysis is NOT renamed — the plugin, the command, the skill, .devflow/docs/bug-analysis/ and tests/bug-analysis/ all stay. Only the agent moved. (bug-analyzer and bug-analysis share a prefix; the only safe discriminator is the trailing er/zer.)

Six guards were added, five of them before any rename, and each was individually falsified — mutated until it went red with the assertion captured, then restored:

  • GAP-1 form A ↔ form B for every agent, via an exception map that shrank to empty and is now asserted empty.
  • GAP-2 agentType: coverage over the dynamic-* commands — roughly 40 spawn sites that no test read at all before this. Tolerates the no-space agentType:"X" form.
  • GAP-3 the orchestrator charter names only live agents, plus a size assertion: the hook that injects it fails open and silent past its cap, so an oversized charter is simply never injected with no error.
  • GAP-4 roster model tiers vs frontmatter model:.
  • GAP-5 a retired-name sweep using maximal recall (case-insensitive, no word boundary) over a corpus asserted non-empty at 248 files (src/assets + dist/commands + docs/reference + the git-tracked .devflow/features/ knowledge bases), with a bidirectional allowlist.
  • GAP-6 pins the 13 shipped key mappings and their invariants.

All dist-reading guards now fail loudly naming npm run build when dist/ is absent, rather than skipping. Two pre-existing fail-open guards were fixed the same way.

Key migration: LEGACY_AGENT_KEYS maps the 13 old slugs to canonical names, applied by one pure canonicaliseAgentKeys used by both consumers — readAgentMapping and a scope:'global' migration entry — so they cannot drift. It parses raw JSON rather than routing through readAgentMapping, which drops invalid values and would silently delete user data on a round trip. Prototype-safe throughout (Object.create(null) + Object.hasOwn), which also closes an argv trap where --set constructor would otherwise return a function into path.join.

No LEGACY_AGENT_NAMES entries were added — PR #282's registry-diff sweep removes the old installed files automatically once the names leave the registry.

fix/hud-base-branch — HUD status-line base detection (2 files)

Found while reviewing the above: the status line reported 3 changed files on a branch that actually had 119.

detectBaseBranch() in src/hud/git.ts scanned the HEAD reflog for the branch most recently checked out from and treated that as the merge base. A reflog entry is not a merge base — here it selected a feature branch already fast-forwarded into the current one, so the "base" collapsed to roughly HEAD and the counter collapsed with it. A locally merged or deleted branch poisons it, and because logs/HEAD is per-worktree, the same branch reported different numbers from different checkouts.

Two further defects in the same file:

  • Range semantics were mixed. The diff used git diff <ref> (working tree vs ref) while the ahead/behind arrow fifteen lines above used three-dot <ref>...HEAD. When the base carries commits not in HEAD, the diff bleeds in reverse changes.
  • An uncached gh pr view network call sat on the render path, executed on every status-line render and bounded only by a 1s timeout. The docstring claimed that layer was cached; it was not.

Now the default branch resolves deterministically via origin/HEAD, falling back through origin/main|master|develop|trunk then local equivalents, with no network call. When nothing resolves the counter renders as absent rather than as a confidently wrong number. The reflog heuristic and the gh layer are deleted outright rather than kept as fallbacks — a fallback that fires in exactly the confusing cases is worse than none. --fork-point was deliberately avoided: it depends on reflog data and degrades in fresh clones and worktrees, the very fragility being removed.

The diff and the arrow now share a merge base. The diff still includes uncommitted work (useful in a status line); the arrow stays commits-only. That asymmetry is deliberate and commented.

Why its test lives in tests/integration/: src/hud/git.ts had zero coverage — every HUD test fed hand-written values into the rendering layer, and tests/hud-render.test.ts hardcoded ahead: 2, filesChanged: 3, which is precisely the wrong output this bug produced. The new test uses real temporary git repositories, so it belongs in the integration suite the repo already excludes from the default run. Placed in the unit suite it deterministically broke 14 unrelated timeout-prone tests through git-subprocess contention (measured: main green twice at ~21s; branch red 3/3 at ~58s with an identical failure set). Relocated, the unit suite returns to the main baseline exactly.


How to review this

Suggested order, roughly by risk:

  1. src/core/orphan-sweep.ts and its two call sites. This is the only new deletion primitive. Confirm it never intersects with the selected-plugin subset.
  2. The uninstall classification invariant in src/cli/commands/uninstall.ts — the disjointness of user-state vs install-artifact, and the containment precondition.
  3. tests/agent-name-guards.test.ts. This is the safety net for the rename and the highest-value file in the branch. Worth asking of each guard: can this actually fail?
  4. src/core/agent-models.ts + src/core/migrations.ts — the key migration, especially the raw-JSON parse and the prototype safety.
  5. The rename diff itself. Large but mechanical. git diff main...wave/agent-roster -- src/assets/agents/ shows the 13 renames as git renames rather than delete+add.
  6. src/cli/agents-view/ for the TUI changes.
  7. src/hud/git.ts for the status-line fix — small, self-contained, and its test is in tests/integration/.

Two things that look wrong but are deliberate:

  • LEGACY_SKILL_NAMES and the LEGACY_SKILLS_* lists are untouched and must stay so. They are deletion manifests for pre-namespace bare directories at ~/.claude/skills/{name}/, which sit outside the swept namespace. A prefixed entry there would delete a live install; a bare entry for a post-namespace skill would delete a same-named foreign directory.
  • Reviewer Focus Areas (5 sites, 3 files) is intentionally NOT renamed. It is a cross-file contract — plan.mds emits the heading and code.md parses it — and also a PR-description section name. All five or none.

Verification

main this branch
Unit test files 84 85
Unit tests 2,693 2,767
Unit failures 0 0
Integration files 3 4
Integration tests 16 37
Integration failures 0 0

Post-review cleanup (4 commits, 9eaba10..0cd3922) adjusted two of these numbers: residual retired-name occurrences were purged from docs/reference/ and the tracked .devflow/features/ knowledge bases, the GAP-5 corpus was extended to cover both directories (233 → 248 files), and the dead LEGACY_AGENT_NAMES list was deleted — its two self-asserting tests account for the 2,769 → 2,767 unit-test delta. Suite re-run green at 85 files / 2,767 tests after the cleanup.

The integration suite was run twice at the end, both green. It is excluded from the default vitest run, so it is easy to forget — and it holds subagent-skill-preload, the only end-to-end exercise of the frontmatter name: field.

One test-harness fix was needed there: pack-install.test.ts's --version step had a 15s budget against an 0.08s true cost, and under parallel load had been measuring 2.9s–15.0s. It tipped over once. runSync also converted the resulting SIGTERM into a bare exit 1 with empty stderr, which made a timeout look exactly like a CLI crash. The budget is now 60s and the signal is surfaced. Packaging integrity was separately confirmed against the real tarball: 16 renamed agents present, deleted files absent, orphan-sweep.js compiled in, files[] and bin unchanged, and --version printing 2.0.0 on both main and this branch.

Build and tsc --noEmit clean. The integration suite — which the default vitest config excludes, and which nothing in this work had previously exercised — also passes (3 files / 16 tests), including subagent-skill-preload, the only end-to-end check of form B.

Behaviors verified end-to-end against a seeded temporary HOME:

  • Disk migration rewrites keys with values byte-identical, records the migration id, and a second init is a no-op.
  • The read-path alias resolves old keys with the file's sha256 unchanged, proving the read path and not the disk path produced the result.
  • Malformed values survive verbatim{"model":"not-a-real-model","effort":"bogus"} is preserved rather than dropped to {}. (Routing through readAgentMapping would have silently deleted them while a loose assertion still passed.)
  • --set coder exits 0 and writes code.
  • A partial install yields exactly 16 agent files, never 26.
  • Spawn-key closure over freshly built dist/: 66 raw matches, all resolving to a live agent's form-B name or the Claude Code built-in Explore.

Provenance against main: .github/, .claude/, .devflow/docs/, .devflow/memory/ and .devflow/learning/ are byte-identical. The only change under scripts/ is the removal of one element from scripts/build-mds.ts for the audit-claude deletion.

Defects caught during review

Recorded because they are the kind a reviewer should look for elsewhere:

  • The scripted prose pass corrupted a spawn contract. _partials/_preamble.mds enumerates the valid agentType string values; the PascalCase pass rewrote them to Code agent, Validate agent, …. That partial compiles into every dynamic command, so a workflow authored from it would have emitted agentType: "Code agent" and resolved to nothing.
  • LEGACY_AGENT_KEYS briefly shipped empty because source was edited after the last build — the suite was green against stale compiled output while every dist-reading guard validated the old artifact.
  • No test exercised the shipped key mappings. Every test emptied the production map first and injected synthetic entries; emptying the real map left all 92 related tests green. That is why the empty map above went unnoticed. GAP-6 closes it.
  • A guard-export change silently disarmed three existing guards. Lifting EXCLUDED to a module export so an invariant could be asserted destroyed the test file's independent literal oracle, turning three working guards into tautologies that move with production.

Breaking changes

  1. The 13 agent renames. Any subagent_type / agentType value in a user's own custom commands or agents must be updated by hand — devflow cannot migrate files it does not own.
  2. --plugin=audit-claude is now rejected. Stale devflow-audit-claude entries in existing manifests are inert and pruned on the next partial reinstall.
  3. Per-agent model overrides silently stop applying on downgrade. Retroactive version detection is impossible: readAgentMapping never reads the version field, and a test pins that it is ignored. There is no mechanism that could warn a user, so it is documented instead.
  4. A session left open across the upgrade holds a stale orchestrator charter naming the old agents, and should be restarted.

Known gaps, deliberately out of scope

  • revertExternalAgents on the selective uninstall path reverts every installed agent, not only those being removed, so surviving agents lose GPT frontmatter until the next devflow init. Fixing it is an API change to RevertOptions.
  • devflow agents --list does not surface orphan agent-models.json keys; only the TUI gained them.
  • An orphan holding a GPT model while routing is off cannot be cleared through the TUI, because dormancy suppresses the dirty flag at both default and the dormant value. Fixing it would change dormancy semantics, which this work deliberately holds constant.
  • Header/footer lines other than the keybindings line remain unbounded below 60 columns (pre-existing).
  • Test files are not typechecked — tsconfig.json scopes to src/**/* (pre-existing).
  • A failed canonicalise-agent-keys-v1 migration is silently marked applied rather than retried, because runGlobalMigration marks any non-throwing return as applied and this migration returns I/O failures as warnings. Net impact is low: readAgentMapping re-canonicalises on every read and the file self-heals on the next write.

Before this can merge

Two checks cannot be performed from an automated session and are genuinely outstanding:

  1. Live-spawn verification across all 8 workflows. This is the only check that catches a broken subagent_type at runtime, because nothing in src/ parses the frontmatter name: field. It cannot be run from a session that is itself using the old agents — installing the renamed agents mid-session breaks the running session. Sequence: node dist/cli.js init, then start a fresh session, then exercise the workflows. /dynamic-build is the only end-to-end check of agentType: spawns.
    "All N agents succeeded, 0 errors" is not evidence — agents can return with zero tool uses while an orchestrator counts them as succeeded. Check per-agent tool-use counts are greater than zero.
  2. A TTY pass on the changes from fix(agents): aliases-only picker, install state, and orphan visibility in the agents TUI #284 — 9 picker stops, STATE flipping on keypress, no wrapped rows at 80 and 60 columns, Bug-Analyzer displayed while the JSON key stays lowercase. Preconditions, or the check is vacuous: warm model cache, proxy.json enabled: false, at least one registry agent absent from the install directory, and agent-models.json seeded with both a canonical-ID pin and an orphan key.

One further item is not objectively verifiable and needs a human judgment call rather than a test: whether the orchestrator, in practice, conflates Test the agent with "test" the noun, or Review the agent with "review" the verb. There is no ground truth for this; it wants a read of the live transcripts.

Built-in collision check: all 13 new names were verified clear against the Claude Code build current at the time of this work — the built-in agent types are Explore and Plan, and none of the new names collide (Plan is taken, but devflow has no plan agent). This must be re-checked on each major Claude Code upgrade: a future built-in could silently shadow a devflow agent, and no in-repo guard can detect that.

dean0x and others added 30 commits August 18, 2026 01:35
…istry-diff sweeps

- Add shared `sweepOrphanedAssets` helper — single readdir/filter/unlink
  implementation reused by all three asset-type sweeps (PF-009 per-item
  isolation, PF-011 sweep-only/no-rewrite, missing-dir is a no-op).

- Replace the LEGACY_AGENT_NAMES deletion loop with an ungated registry-diff
  sweep over ~/.claude/agents/devflow/ using getAllAgentNames(). Names that
  leave the registry are pruned automatically on every install shape.

- Add equivalent registry-diff sweep for commands over
  ~/.claude/commands/devflow/ using getAllCommandNames() — the function
  existed but was previously unused by the installer.

- Ungate the existing skills sweep: it previously ran only on full installs;
  it now runs on every install shape including --plugin partial installs.

All three sweeps use getAllAgentNames/getAllCommandNames/getAllSkillNames which
span ALL plugins — assets from uninstalled plugins survive a partial run.

Tests: inverted the stale-agent/command partial-install test; added a
comprehensive partial-install vacuity test (retired asset removed, known-asset
from uninstalled plugin survives, corpus non-empty guard). Vacuity verified:
test went RED with sweep disabled, GREEN with sweep restored.

Co-Authored-By: Claude <noreply@anthropic.com>
…dule

Move the registry-diff sweep helper from installer.ts (module-private) to
src/core/orphan-sweep.ts (exported) so both the install pipeline and the
uninstall pipeline can use the same implementation without duplication.

installer.ts imports from core/orphan-sweep.js; the helper's semantics and
per-item failure isolation contract are unchanged (avoids PF-009, PF-011).

Co-Authored-By: Claude <noreply@anthropic.com>
Phase 2 of the install/uninstall lifecycle wave. Closes the gap between
what the install side cleans up and what the uninstall side actually removes.

Exports (testability):
- Export removeAllDevFlow, removeSelectedPlugins, isDevFlowInstalled so
  end-to-end tmpdir tests can drive them directly (PF-018 compliant — no CLI
  spawn needed, no ~/.claude guard required in these tests).

Registry-diff sweep on selective uninstall:
- removeSelectedPlugins now calls sweepOrphanedAssets (imported from
  core/orphan-sweep.ts) after its per-agent and per-command removal loops.
  Orphaned agents/commands whose names left the registry are swept away;
  knownNames spans ALL plugins so assets from other installed plugins survive.

revertExternalAgents on selective uninstall path:
- Mirrors the existing full-uninstall call. Runs BEFORE removeSelectedPlugins
  so agent .md files are still present when GPT model lines are stripped.
  Non-fatal: missing agents dir or revert errors are silently ignored.

artifact list extended (removeDevFlowInstallArtifacts):
- agent-models.json — cleared on every uninstall so reinstall starts with
  shipped defaults (was silently stranded, causing stale model assignments)
- migrations.json — cleared so migrations run cleanly on reinstall
- hud.json — HUD configuration
- costs/sessions/ and costs/archive.jsonl — auto-generated cost history
- logs/ — entire logs tree (project-slug dirs + global logs including proxy.log)
- cache/ parent instead of cache/models only — covers any future sub-dirs
- isDir === true discipline maintained throughout (avoids the silent TypeError
  gotcha in the per-item catch)

keepDocs honored in resolveDevflowDirCleanup:
- Adds keepDocs?: boolean to the function signature; returns 'artifacts-only'
  before reaching the precondition guard or isTTY check when keepDocs is true.
  Previously --keep-docs could still trigger the ~/.devflow wipe prompt via
  skill shadows or preference-profile.md — active data-loss path fixed.

enumerateUserDevFlowContent extended:
- Adds hud.json to the warning enumeration (user sets enable/disable via CLI)

isDevFlowInstalled fixed:
- Now checks agents/devflow and any devflow:* skill directory in addition to
  commands/devflow. A commandless plugin install (agents + skills only) no
  longer returns "No Devflow installation found" and exits 1.

Dry run made faithful:
- Selective path: unchanged (registry-computed, accurate)
- Full path: scans actual on-disk directories and reports what would really be
  removed (including legacy/orphaned assets), rather than computing from the
  registry

Tests:
- 9a: removeAllDevFlow empties agents/devflow including retired + user-dropped files
- 9b: removeSelectedPlugins sweep removes orphaned names not in any plugin
- 9c: ~/.devflow residue EQUALS user-authored allow-list (equality, not subset);
  non-vacuousness proved — adding mystery-artifact.json caused RED, removing it
  restored GREEN
- 9d: resolveDevflowDirCleanup returns 'artifacts-only' when keepDocs is true
- 9e: isDevFlowInstalled returns true for agents/skills with no commands present
- step 10: repoint devflow-audit-claude → devflow-bug-analysis/bug-analyzer
  + non-empty assertion to prevent vacuous pass on an empty selection

avoids PF-009, PF-014, applies ADR-003

Co-Authored-By: Claude <noreply@anthropic.com>
- Stop deleting user-owned agent-models.json/hud.json on the artifact-only path.
  Both were enumerated by enumerateUserDevFlowContent (shown in the full-wipe
  confirm prompt) AND listed in removeDevFlowInstallArtifacts, which also runs
  on the decline, cancel, non-interactive and --keep-docs paths — so they were
  deleted regardless of the user's answer.
- Derive the cache/ removal target from hudCacheDir() instead of
  path.dirname(modelCacheDir()); add a containment invariant so no artifact
  path can ever resolve to devflowDir itself (avoids PF-013).
- Collapse costs/sessions + costs/archive.jsonl into the costs/ parent so the
  removal site cannot drift from src/hud/cost-history.ts.
- Drop the unreachable 'Nothing to remove.' branch in the full dry-run plan
  (applies ADR-003) and correct the now-inaccurate 'scripts and manifest only'
  messages.
- Use SKILL_NAMESPACE instead of a hardcoded 'devflow:' literal (2 sites).
- Add tests/orphan-sweep.test.ts: direct coverage for the new shared sweep
  helper (missing dir, non-directory path, predicate-skip, recursive dir
  removal, EACCES isolation) with scanned-count non-vacuity assertions.
- Add (9f) disjointness test and extend (9c) so the enumerate/artifact overlap
  regression is caught; both verified RED against the reintroduced bug.
…p hud.json as user state

Resolves four misalignments from the self-review pass:

- MISALIGNMENT 1: agent-models.json is added back to installArtifacts in
  removeDevFlowInstallArtifacts (AC-P1-F4) and removed from
  enumerateUserDevFlowContent. Stale per-agent model overrides keyed to
  renamed/deleted agents would silently re-apply on reinstall without this.

- MISALIGNMENT 2: hud.json stays absent from installArtifacts and present
  in enumerateUserDevFlowContent — the reviewer's classification is correct.

- MISALIGNMENT 3: the @d8 invariant comment now names agent-models.json as
  an artifact and hud.json as user state, matching the corrected code. The
  disjointness test (9f) proved real: adding agent-models.json back into
  enumerateUserDevFlowContent causes it to go RED (length 6 vs expected 5),
  removing it restores GREEN.

- MISALIGNMENT 4: the comment in tests/plugins.test.ts (~:228) is rewritten
  to describe the registry-diff sweep mechanism (sweepOrphanedAssets) rather
  than the now-false statement that LEGACY_AGENT_NAMES drives stale-file cleanup.
  List contents are unchanged.

After this fix a user who declines the full ~/.devflow wipe keeps:
  skills/ (shadows), rules/ (shadows), preference-profile.md, learning.json, hud.json

And loses regardless of answer:
  agent-models.json, migrations.json, manifest.json, proxy artifacts, logs/, costs/, cache/
The devflow-audit-claude plugin was the only plugin that was both
optional and excluded from the init UI. Its removal makes the
EXCLUDED ∩ optional === ∅ invariant vacuously true — the helper
was guarding a now-impossible state.

Changes:
- Delete src/assets/agents/claude-md-auditor.md and
  src/assets/commands/audit-claude.md
- Remove devflow-audit-claude from DEVFLOW_PLUGINS registry,
  WORKFLOW_ORDER, and EXCLUDED set in plugins.ts
- Lift EXCLUDED to module-level ReadonlySet export so the
  invariant EXCLUDED ∩ optional === ∅ is assertable in tests
- Add DELETED_PLUGIN_NAMES to prune the stale manifest entry on
  partial reinstalls; wired into resolvePluginList in manifest.ts
- Remove resolveNonSelectableOptionalCarry and applyNonSelectableCarry
  from init-seed.ts (only ever returned [] or carried audit-claude;
  applies ADR-003 — end-state not transition)
- Remove the call site from init.ts
- Remove audit-claude.md from the static-copy list in build-mds.ts
  (1 hand-authored file now, not 2)
- Update all affected tests:
  - tests/plugins.test.ts: remove audit-claude refs; import EXCLUDED;
    add invariant test with falsification evidence (RED→GREEN verified)
  - tests/init-nonselectable-carry.test.ts: deleted entirely (8 tests)
  - tests/init-seed.test.ts: remove carry imports and describe block
  - tests/agent-frontmatter.test.ts: 17→16 agent files
  - tests/skill-references.test.ts: delete AGENTS_WITHOUT_SKILLS guard;
    add non-empty corpus assertion
  - tests/build-mds.test.ts: 16→15 command count
  - tests/packaging.test.ts: 16→15 command count

Falsification evidence for invariant test:
  Adding devflow-dynamic to EXCLUDED → test fails RED with
  "expected ['devflow-dynamic'] to deeply equal []"
  Reverting → 46 tests pass GREEN

Co-Authored-By: Claude <noreply@anthropic.com>
- CLAUDE.md: 23 plugins → 22 (12 core + 10 optional); remove plugin table row;
  17 agents → 16; 2 static .md → 1; remove /audit-claude command entry;
  remove "Plugin-specific agents (1): claude-md-auditor" line
- README.md: 23 plugins → 22 (12 core + 10 optional)
- CONTRIBUTING.md: 17 agents → 16; 2 static .md → 1 static .md
- docs/cli-reference.md: remove devflow-audit-claude plugin table row
- docs/reference/file-organization.md: 17 agents → 16; 2 static → 1;
  replace "Shared vs Plugin-Specific Agents" section with plain "Agents" list
- docs/reference/agent-design.md: remove claude-md-auditor.md from example list
- tests/registry-integrity.test.ts: replace deleted claude-md-auditor
  with coder in naming-convention comment
- .devflow/features/installer-shadowing/KNOWLEDGE.md: fix three PR 1 stale
  entries (orphan sweep now ungated on all install shapes; DELETED_PLUGIN_NAMES
  added to LEGACY_* section; agent-models.json moved to install artifact);
  remove resolveNonSelectableOptionalCarry / applyNonSelectableCarry references
  (deleted in Commit 1)
Under ### Removed:
- devflow-audit-claude plugin and /audit-claude command (BREAKING)
- Non-selectable optional carry mechanism (internal, no user-facing change)
Foundation A: export `classifyAgentState` / `AgentState` from external-models.ts
(single source of truth for --list and TUI state); delete local `RowState` in
agents.ts, route `buildListRows` through `classifyAgentState`.

Foundation B: add `readInstalledAgentNames(installDir)` to agent-models.ts —
one `readdir` call replaces N `fs.access` calls in `buildListRows`.

Fix 1: picker cycle now uses `pickerNames(catalog.models)` (aliases only; canonical
ID only when model has no aliases) instead of `catalog.selectableNames`. Aliases
like 'sol' render bare in the TUI — no "(gpt-5.6-sol)" annotation. Canonical IDs
stored via --set are normalised to their picker name on load via `buildPickerNameMap`
without writing to disk. `catalog.aliasToId` is no longer referenced in render.ts.

Tests: T1 (live-cache exact cycle), T2 (two-alias fixture), T3 (zero-maintenance
custom catalog), T4/T14-rewrite (bare alias rendering, red→green falsification
recorded), T5 (static guard: aliasToId absent from render.ts).
Extract `mergeTuiRowsIntoMapping` as an exported pure helper from
`applyTuiSave` (agents.ts). Only dirty rows modify the mapping — untouched
rows (including dormant GPT entries) are byte-identical preserved. This is
the inertness guarantee: a row never edited produces no write.

Add `rowState(row, proxyEnabled)` to state.ts — pure helper delegating to
`classifyAgentState` using the persisted model name (dormantModel when set,
otherwise configuredModel) so dormant rows are correctly classified as
'saved-inactive'. Commit 3 will thread installed/inRegistry from AgentRow.

Drop "Saved. " prefix from TUI save outro to match --set wording verbatim.

Tests: T6 (rowState: active/saved-inactive variants), T12 (mergeTuiRowsIntoMapping:
inertness, dirty model, reset to default, dirty effort, pure function).
Fix pre-existing test: 'gpt-test-1' canonical id must not appear in picker
cycle (alias 'test1' takes its slot — Fix 1 regression in agents-command.test.ts).
Add required `installed` and `inRegistry` fields to AgentRow and optional
counterparts to InitRowInput (default true). buildRow populates them;
rowState now uses row.installed and row.inRegistry (not hardcoded true/true).

buildTuiState: pass installDir, call readInstalledAgentNames once (Foundation B),
thread installed per-row, append orphan rows (mapping.agents keys absent from
the registry) with inRegistry=false at the end.

render.ts: 4th STATE column (AGENT 18, MODEL 32, EFFORT 13, STATE 13 = 78 ≤ 80);
renderStateCell(row, proxyEnabled) delegates to rowState; stripAnsi(row.name)
mandatory before name-cell rendering (security — arbitrary JSON keys may inject
ANSI escape sequences).

Tests: T7 (STATE in header), T8 (active state), T9 (not-installed state),
T10 (orphan/unknown state), T11 (stripAnsi on name cell).
Add `formatAgentName(name: string): string` to render.ts — capitalizes the
first character of the agent name for TUI display. Exactly ONE call site in
the row renderer (applied after stripAnsi). The `--list` path in agents.ts
does not import or call formatAgentName; raw lowercase names are preserved
there.

Tests: T13 (formatAgentName unit, TUI capitalization, --list static guard).
Update all render-test row-finder calls from lowercase to capitalized names
(e.g. 'coder' → 'Coder') to match the new TUI output.
M1 — InitRowInput.installed and .inRegistry are now REQUIRED (not optional).
tsc enumerates every construction site; the two production sites in agents.ts
already passed explicit values (no change needed there). All 10 buildRow calls
in tests/agents-state.test.ts receive explicit installed:true, inRegistry:true
— the correct value for normal registry agents. The ?? true fallbacks in
buildRow are removed; undefined would have silently produced 'unknown' state.

M2a — Width guarantee tests at 80 and 60 cols (AC-P3-WIDTH, 2 new tests in
agents-render.test.ts). At 60 cols the responsive-scale block produces column
widths agent=13, model=24, effort=10, state=10 (total row = 59 ≤ 60). The
keybindingsLine (77 chars) previously exceeded 60 cols — fixed in render.ts
by slicing the raw text to dims.cols before applying dim(), so the line is
never wider than the terminal at any width.

M2b — --list AGENT cell format and round-trip (AC-P3-LIST, 2 new tests in
agents-command.test.ts). Every name from buildListRows matches ^[a-z0-9-]+$
(lowercase identifiers only; capitalization is TUI-only via formatAgentName).
Round-trip test verifies every name from --list is present in getAllAgentNames()
and therefore accepted by --set validation.

M3 — T14 falsification confirmed concretely. Temporarily restored the
alias→canonical-ID annotation branch in renderModelCell (passing aliasToId
through RenderModelCellOptions and renderFrame), ran tests/agents-render.test.ts,
captured exit code 1 with assertion:
  "expected '❯ Coder             sol (gpt-5.6-sol)…' not to contain 'sol (gpt-5.6-sol)'"
Reverted, re-ran: exit code 0, 50/50 pass. T14 is not vacuous.

Suite: 84 files / 2727 tests / 0 failures (+4 tests vs 2723 baseline).
tsc --noEmit: clean. npm run build: clean.

Co-Authored-By: Claude <noreply@anthropic.com>
Two defects in code added by this PR, both confirmed by falsification
(production behavior temporarily reverted, new tests observed RED).

1. STATE column reported a stale dormancy marker (state.ts rowState).
   rowState keyed on `dormantModel ?? configuredModel`, so a dormant row
   the user cycled onto a live Claude model kept rendering
   'saved-inactive' even though mergeTuiRowsIntoMapping would persist
   'opus'. The marker appeared on the keypress but never cleared on one.
   Now classifies the model the row WILL persist, mirroring the merge
   rule exactly: configuredModel when dirty, else dormantModel ??
   configuredModel. Also drops a "Commit 3 will..." transition comment
   describing work that already landed (applies ADR-003).

2. Orphan row names could break the frame (render.ts). Fix 3 newly
   renders arbitrary agent-models.json keys, and stripAnsi preserves TAB
   and LF by contract — so a key containing \n emitted an embedded
   newline into a frame line, violating renderFrame's one-string-per-line
   contract and desyncing terminal.ts's redraw, while \t measured as one
   char in padToVisible but occupied up to eight terminal columns. New
   sanitizeCell() collapses both at the display boundary; the raw key is
   untouched so the save-path merge still targets the real mapping key.

Also: keybindings footer now truncates via truncateVisible instead of a
bespoke .slice(), so the renderer has one truncation primitive.

Tests (+5 net, 171 -> 176 across the three agents files):
 - rowState: in-session GPT selection with proxy off and nothing saved
   (fixture where configuredModel and originalModel DIFFER and disagree
   on the answer, so it distinguishes them); marker clears when a dormant
   row is cycled onto a Claude model; stays on for a different GPT model;
   stays on when cycled back onto its own saved value.
   Replaces an exact-duplicate rowState case (avoids PF-018 mechanism 7,
   label-only scenario).
 - render: newline and tab in an orphan name never reach the frame, and
   STATE stays at its declared column offset.

Suite: 84 files / 2732 tests / 0 failures (2727 baseline, +5 accounted
per-file). tsc --noEmit clean, npm run build clean.
…g-Analyzer)

Title-case each hyphen-separated segment rather than uppercasing only the
first character, so multi-segment names render correctly in the TUI:
  bug-analyzer → Bug-Analyzer
  my-custom-agent → My-Custom-Agent
  claude-md-auditor → Claude-Md-Auditor
  code → Code (single-segment: unchanged)

`--list` output remains raw lowercase — the AGENT column is an identifier
users copy into `--set`, which exact-matches.

Update all 7 locations that pinned the first-char-only behavior: docstring,
CHANGELOG Fix 4 entry, and 5 test groups (T10 orphan row, T11 ANSI strip,
T13 direct unit assertions, T13 TUI integration, frame "shows all three
agents" and "non-cursor rows" checks).

Co-Authored-By: Claude <noreply@anthropic.com>
…e 1 of 4)

GAP-1: assert every agent's frontmatter name: matches its filename slug
  using capitalizeFirst(slug) as the default, with a SHRINKING exception map
  that starts with one entry (bug-analyzer → BugAnalyzer). The map must be
  empty after phase 4 — that is the wave's acceptance criterion.

GAP-2: agentType: coverage — dynamic commands (which use agentType: not
  subagent_type) were unverified. The new guard asserts set-equality between
  the 11-agent _roster.mds table and actual agentType: values in dist, plus
  roster ⊆ registry. Tolerates the no-space form agentType:"X". Excludes
  the Explore built-in. Fails loud when dist is absent.

GAP-3: orchestrator charter byte-size cap — the session-start-orchestrator
  hook silently skips injection past 4 096 chars. Assert ≤ 75% of cap (3 072).
  Also sweeps for retired agent names (vacuous today; active from phase 4).

GAP-4: roster model tiers — assert that _roster.mds model column matches
  each agent's frontmatter model:. Reads from compiled dist/commands/dynamic-
  build.md (fail-loud when dist absent). All 11 currently match.

GAP-5: retired-name sweep — generalizes the dream→learning guard with maximal
  recall (case-insensitive, no trailing boundary), filesScanned >= 220 corpus
  guard, and form B derived from frontmatter (not Capitalize(slug)). Ships with
  an empty retired-names list; populated in phase 4.

Fix two existing fail-open sites:
  - registry-integrity.test.ts Guard 5: was `if (!distExists) return;`
  - skill-references.test.ts:865: was `continue; // dist not yet built`
  Both now throw with a message naming `npm run build`.
…-migration mechanism

Key-migration infrastructure — INERT until phase 4 populates LEGACY_AGENT_KEYS.

- LEGACY_AGENT_KEYS: exported Object.create(null) map of old-key → canonical-key.
  Prototype-null + Object.hasOwn() throughout to prevent __proto__ injection.
  Ships empty; populated in phase 4 when agent renames are applied.

- canonicaliseAgentKeys(): pure function that renames legacy keys in a raw agents
  map. Fast-path no-op when the map is empty (common case). Idempotent under
  concurrent execution — two worktrees applying simultaneously produce identical
  results (no lock required). Guards __proto__ on both old-key and new-key sides.

- Wire into readAgentMapping (all 4 call sites at once). Also fixes the agents: []
  array trap: typeof [] === 'object' && [] !== null, so Array.isArray() check added.

- Wire into MIGRATIONS as first scope:'global' entry
  ('canonicalise-agent-keys-v1'). Reads raw JSON (not readAgentMapping) to avoid
  silently dropping user data on round-trip. Handles BOM, empty file, invalid JSON,
  missing/null/array agents field. Known issue documented: retries forever on
  failure (no cap/backoff) — out of scope for this wave.

- Wire into agents.ts --set: accepts old keys with an info line pointing to the
  canonical name.

Tests: 12 new canonicaliseAgentKeys unit tests (fast-path, rename, collision,
idempotency, __proto__ guards, LEGACY_AGENT_KEYS prototype-null assertion, array
trap, integration via readAgentMapping). 11 new migration unit tests (fast-path,
rename + file write, ENOENT, missing agents field, no legacy keys present, invalid
JSON, BOM strip, agents array warning, envelope preservation, idempotency). Updated
MIGRATIONS registry test ('is empty' → asserts canonicalise-agent-keys-v1 entry).
…m A+B)

Atomic identity rename across Form A (slug/filename) and Form B
(frontmatter name: field + spawn key strings) for all 13 non-exempt agents.

Renames:
  coder → code         (Coder → Code)
  designer → design    (Designer → Design)
  evaluator → evaluate (Evaluator → Evaluate)
  researcher → research(Researcher → Research)
  reviewer → review    (Reviewer → Review)
  scrutinizer → scrutinize (Scrutinizer → Scrutinize)
  simplifier → simplify(Simplifier → Simplify)
  skimmer → skim       (Skimmer → Skim)
  synthesizer → synthesize (Synthesizer → Synthesize)
  tester → test        (Tester → Test)
  triager → triage     (Triager → Triage)
  validator → validate (Validator → Validate)
  bug-analyzer → diagnose (BugAnalyzer → Diagnose)

Unchanged: git/Git, knowledge/Knowledge, learning/Learning.
Unchanged: /bug-analysis plugin, command, skill.
Preserved: all five "Reviewer Focus Areas" cross-file contract sites.
Preserved: all subagent_type="Explore" Claude Code built-in sites.
Deferred: Form-C prose rewrites and contract variable renames (Phase 3).

Changes:
- git mv all 13 agent files; update frontmatter name: in each
- Update plugins.ts agents[] arrays with new slugs
- Update all spawn strings in .mds and .md command sources
- Add _roster.mds table entries with new names
- Update reviewerThunks → reviewThunks in dynamic-build.mds + test assertion
- Update evaluateVerdict/testVerdict identifiers in _engine.mds
- Empty SLUG_TO_NAME_EXCEPTIONS (diagnose = capitalizeFirst('diagnose'))
- Rename tests/skimmer-agent.test.ts → tests/skim-agent.test.ts
- Update all test fixtures, path literals, and assertions to use new names

Verification: 85 files / 2760 tests / 0 failures; all 5 GAP guards GREEN.
…e 3 — Commit 4)

Mechanical renames (new name reads fine as-is):
  SCRUTINIZER_OUTPUT → SCRUTINIZE_OUTPUT  (implement.mds, 2 sites)
  SIMPLIFIER_COMMITS → SIMPLIFY_COMMITS   (self-review.mds, 1 site)
  SKIMMER_CONTEXT    → SKIM_CONTEXT       (plan.mds, 4 sites)
  simplifierTranscript → simplifyTranscript (subagent-skill-preload.test.ts, 2 sites)

Disambiguated renames (bare verb form reads wrong):
  CODER_OUTPUT       → CODE_AGENT_OUTPUT  (implement.mds, 1 site)
  CODER_RESULTS      → CODE_AGENT_RESULTS (resolve.mds, 7 sites)
  REVIEWER_LIST      → REVIEW_FOCUS_LIST  (code-review.mds, 3 sites)
  REVIEWER_OUTPUTS   → REVIEW_FOCUS_OUTPUTS (code-review.mds, 2 sites)
  SINGLE_CODER       → SINGLE_CODE_AGENT  (implement.mds + plan.mds + synthesize.md, 20 sites)
  SEQUENTIAL_CODERS  → SEQUENTIAL_CODE_AGENTS (13 sites across same files)
  PARALLEL_CODERS    → PARALLEL_CODE_AGENTS   (10 sites across same files)

Preserved: ANALYZER_OUTPUTS (generic focus analyzers, not the agent)
Preserved: parseReviewFocusAreas (already correct from phase 2)
…rences (Phase 3 — Commit 5)

Replace all PascalCase prose references to old noun-form agent names with
action-verb equivalents throughout src/assets/ and source TypeScript/test files.

Convention applied: write "the Code agent", "each Review agent" (append " agent");
use bare verb only in spawn literals (agentType: "Review") and roster tables.

Scope:
- 15 agent .md files: headings corrected (e.g. "# Scrutinize agent")
- 14 command .mds files + 6 partials: Coder→Code agent, Reviewer→Review agent,
  Scrutinizer→Scrutinize agent, Simplifier→Simplify agent, Skimmer→Skim agent,
  Designer→Design agent, Researcher→Research agent, Triager→Triage agent,
  BugAnalyzer→Diagnose agent, Validator→Validate agent, Evaluator→Evaluate agent,
  Synthesizer→Synthesize agent, Tester→Test agent
- Orchestrator charter: routing tier entries updated (2,177 bytes, well within 3,072 cap)
- 9 skill files: Researcher→Research agent, Reviewer→Review agent, etc.
- src/core/plugins.ts, src/cli/commands/init.ts: display strings updated
- src/cli/agents-view/render.ts: JSDoc example updated
- tests/plugins.test.ts, tests/registry-integrity.test.ts: test descriptions updated

Protected invariants:
- "Reviewer Focus Areas" preserved at all 5 sites (cross-file contract)
- invalidator (3 sites), validator in skills (~17 lib refs), /tmp/devflow-tester- (2 sites) untouched
- LEGACY_AGENT_KEYS / old-key test data untouched
- ANALYZER_OUTPUTS untouched

Verified: 85 test files / 2,760 tests pass; all 7 GAP guards green; 0 "agent agents" duplications
… 4 — Commit 6a)

Phase 3 left the 13 renamed agent files with lowercase-agent H1 headings
(e.g. `# Code agent`) while the three untouched agents (git, knowledge,
learning) kept title case (`# Git Agent`). This commit makes all 16
consistent using the pre-existing title-case pattern:

  Code agent    → Code Agent
  Design agent  → Design Agent
  Diagnose agent → Diagnose Agent
  Evaluate agent → Evaluate Agent
  Research agent → Research Agent
  Review agent  → Review Agent
  Scrutinize agent → Scrutinize Agent
  Simplify agent → Simplify Agent
  Skim agent    → Skim Agent
  Synthesize agent → Synthesize Agent
  Test agent    → Test Agent
  Triage agent  → Triage Agent
  Validate agent → Validate Agent

Body prose keeps the settled convention "the Code agent" / "each Review
agent" — only the H1 line changes.
Update every remaining reference to old agent names across docs and
knowledge bases. All 13 renamed agents now use their new action-verb
form throughout:

  Coder       → Code agent / Code
  Designer    → Design agent / Design
  Evaluator   → Evaluate agent / Evaluate
  Researcher  → Research agent / Research
  Reviewer    → Review agent / Review
  Scrutinizer → Scrutinize agent / Scrutinize
  Simplifier  → Simplify agent / Simplify
  Skimmer     → Skim agent / Skim
  Synthesizer → Synthesize agent / Synthesize
  Tester      → Test agent / Test
  Triager     → Triage agent / Triage
  Validator   → Validate agent / Validate
  BugAnalyzer → Diagnose agent / Diagnose

Files updated:
- CLAUDE.md — model strategy, shared agents roster, orchestration
  commands, persisting agents, handoff artifact, file-tree comments
- README.md — pipeline steps, command table, feature descriptions
- docs/cli-reference.md — plugin descriptions
- docs/commands.md — command pipeline steps
- docs/working-memory.md — KB descriptions and file-tree comments
- docs/reference/agent-design.md — frontmatter example, length table
- docs/reference/file-organization.md — agents list (line 156)
- docs/reference/skills-architecture.md — "Used By" columns, section
  headings, frontmatter example
- docs/reference/skill-catalog.md — compliance skill description
- .devflow/features/*/KNOWLEDGE.md — all four shared knowledge bases
  (resolve-pipeline, dynamic-workflow-engine, compliance-plugin,
  ambient-orchestrator)
…se 4 — Commit 6c)

Documents the full breaking change for the agent rename wave:
- Table of all 13 old → new slug (Form A) and name (Form B) mappings
- What devflow migrates automatically (agent files, agent-models.json keys)
- What users must migrate by hand (their own custom commands/agents)
- Downgrade warning: per-agent model overrides silently stop applying
  (readAgentMapping never reads the version field — test-pinned)
- Open-session warning: stale orchestrator charter after upgrade
- Claude Code built-in name collision check: verified clear against
  claude-sonnet-4-6 build 2026-08-18; note to re-check on each major
  Claude Code upgrade
…S (Phase 4 — Commit 7)

Populate the three stubs left empty after phase 1:

RETIRED_AGENT_FORM_B (agent-name-guards.test.ts): 13 old Form-B names
(Coder, Designer, Evaluator, Researcher, Reviewer, Scrutinizer, Simplifier,
Skimmer, Synthesizer, Tester, Triager, Validator, BugAnalyzer) — activates
the GAP-5 retired-name sweep across src/assets/**/*.{md,mds} and
dist/commands/*.md.

RETIRED_ALLOWLIST (25 entries, bidirectional): every collateral hit is
explicitly categorised (URL_LINK, DATA_FIELD, CONTRACT, CONCEPT, THIRD_PARTY,
EXAMPLE_CODE) and each entry is verified to match at least one live corpus
hit. Protected cross-file contracts (Reviewer Focus Areas — 5 sites, 3 files)
are preserved intentionally. A new it-block asserts every allowlist entry
remains live (stale entries fail the suite).

SLUG_TO_NAME_EXCEPTIONS empty-assertion (GAP-1): new it-block asserts the
exception map is empty — D-RI-1 wave acceptance criterion.

LEGACY_AGENT_KEYS (agent-models.ts): 13 old-slug → new-slug entries enable
the canonicalise-agent-keys-v1 migration to rewrite ~/.devflow/agent-models.json
on user's first init post-upgrade (coder→code, reviewer→review, etc.).

Test harness fix (agent-models.test.ts, migrations.test.ts): both describe
blocks now use beforeEach/afterEach save-clear-restore instead of afterEach-
only-clear, so each test starts from a known-empty map and shipped entries are
restored afterward.

Genuine missed renames from phase 3, fixed rather than allowlisted:
  - SIMPLIFIER_OUTPUT → SIMPLIFY_OUTPUT (implement.mds)
  - SCRUTINIZER_STATUS/CHANGES/modified_files → SCRUTINIZE_* (self-review.mds)

GAP-5 red/green proof: injecting "<!-- PROBE: Simplifier -->" into
src/assets/skills/security/SKILL.md turned the sweep RED; removal restored GREEN.

85 test files / 2762 tests / 0 failures.
P0 — corrupted agentType literals (Form C scripted pass):
_preamble.mds enumerated the valid `agentType` string values as
"Code agent, Validate agent, ..." — contradicting both the code example
three lines above it and _roster.mds. A workflow script authored from
that partial would emit agentType: "Code agent", which does not resolve.
This partial is included by every dynamic command. Restored bare values,
and the same corruption in the model-tier mapping on the next line.

P1 — vacuous test coverage of the shipped rename map (avoids PF-018):
Both canonicaliseAgentKeys suites delete every key from the exported
LEGACY_AGENT_KEYS in beforeEach and inject synthetic entries, so no test
ever exercised the real 13 mappings. Verified by experiment: emptying the
shipped map left all 92 tests in the three relevant files green — the
exact stale-dist failure mode this wave already hit once. Added GAP-6 in
agent-name-guards.test.ts (a file that never mutates the map): pins the
13 pairs, asserts keys are disjoint from the live registry, every value
is a live agent, the map is single-hop, and it applies end-to-end against
the genuine map. Proven non-vacuous — emptying the map fails 3 of them.
Documented the scope limit in both mutating suites.

P1 — migration deferral note was inverted:
The note claimed a failed canonicalise-agent-keys-v1 would retry forever.
It cannot: runGlobalMigration marks any non-throwing return as applied,
and that migration catches every I/O failure and returns it as a warning.
Probed directly — an unreadable agent-models.json records the migration
applied with legacy keys still on disk, never retried. Corrected the note
to describe the real behaviour and why it is survivable (readAgentMapping
re-canonicalises on every read; the file self-heals on next write), and
added the missing test for the untested EACCES read path.

P1 — meaning inversion in a load-bearing orchestration contract:
"re-Validate agent" reads as an instruction to re-validate the agent
rather than to re-run the Validate agent. Now "re-run Validate agent".
Also repaired broken pseudo-code (`Code agent(agentType:"Code", ...)`)
and a broken report heading (`## Code agent Report:` →
`## Implementation Report:`, matching every sibling agent's convention).

applies ADR-003 · avoids PF-018 · avoids PF-019
1. skim.md: reword :126 "Skim for structure" → "rskim for structure"
   to match the correct phrasing at :66 and avoid ambiguity with the
   agent's own name.

2. agent-models.ts / migrations.ts / agents.ts: remove five stale
   comments that referenced "phase 1", "phase 4", or "LEGACY_AGENT_KEYS
   is empty" — describe current state only.

3. CHANGELOG.md: correct the built-in collision list. The previous list
   named devflow's own pre-rename agents as "Claude Code built-ins".
   The actual built-ins are only Explore and Plan; updated accordingly.

4. _engine.mds / code.md: reword "multi-Code agent" → "multiple Code
   agents" / "chaining multiple Code agents" to eliminate parse ambiguity.

5. diagnose.md, triage.md, git/SKILL.md, _ticket_template.mds,
   dynamic-plan.mds, resolve.mds: fix missing articles and number
   disagreements left by the scripted prose pass.

6. CLAUDE.md: normalise roster block to bare verb names throughout
   ("Review agents" → "Review", "Git agent" → "Git", etc.).

7. project-paths.ts + project-paths.cjs: update JSDoc on both sides of
   the mirror pair from "coder phase handoff artifact" to "Code agent
   phase handoff artifact". Both files changed identically.

8. plan.mds: rename prose references "Explorer" → "Explore" at three
   sites (lines 101, 128, 255). All three are prose labels, not
   subagent_type= literals — verified before changing.
Replaces the three-layer heuristic in detectBaseBranch() with a
deterministic default-branch resolver that makes no network call:

  (a) git symbolic-ref --short refs/remotes/origin/HEAD (clone-set)
  (b) First of origin/main, origin/master, origin/develop, origin/trunk
  (c) Local main, master, develop, trunk (no-remote fallback)
  (d) null → caller renders counter absent rather than wrong

Removes the HEAD reflog heuristic (Layer 2) that was the root cause of
the bug: on wave/agent-roster, it selected refactor/agent-action-verbs
(a branch already fast-forwarded in) and reported 3 files where the
true surface was 119 files / +4972 / -2132.

Also removes the `gh pr view` call (Layer 3) from the render path — a
network call on a status-line render is not acceptable and is now
redundant.

Fixes the diff range semantics: replaces `git diff --shortstat <baseRef>`
(working tree vs ref tip, bleeds reverse changes when base diverges) with
explicit `git merge-base <baseRef> HEAD` followed by
`git diff --shortstat <mergeBase>`. This ensures the counter covers only
this branch's changes, while deliberately retaining uncommitted working-
tree changes in the figure (committed + uncommitted = PR surface + WIP).

Adds 21 tests using real temporary git repos (not mocks): layer-by-layer
resolution, the regression scenario, worktree equivalence, detached HEAD,
no-remote, no-commits, not-a-git-repo, diff/ahead-behind reference-point
agreement, and no-gh-invocation on the render path.

Closes: fix/hud-base-branch
dean0x and others added 22 commits August 19, 2026 16:26
src/core/noop.ts had zero consumers anywhere in src/, tests/, scripts/ or
docs/ — the only references were its own definition and its own test. It is
also outside the three clusters this branch covers (uninstall/installer
observability, agent-models/migrations/TUI truthfulness, HUD trunk comparison).

Dead code, deleted.
--no-optional-locks is a git-level option, not a `status` option. Passing it
after the subcommand makes git exit non-zero with "unknown option", shellExec
turns that into '', and the HUD reports every tree as clean. The branch had
dropped the flag entirely and attributed the failure to Apple git; the real
cause was argument position, and dropping it let the status line write
.git/index on every prompt.

Moved to `git --no-optional-locks status --porcelain` and pinned the ordering
with an argv assertion plus a live dirty/staged check. Falsified: with the flag
after the subcommand both the new pin and the existing Shape M dirty assertion
go red.

Also strips the transition prose ("replaces the previous design of up to 11
sequential rev-parse calls", "the prior design used git rev-parse --verify",
"fixing the prior asymmetry") from detectBaseBranch — applies ADR-003, the
comments now describe the end state only.
InstallReport.sweptOrphans and .sweepFailures were populated by all three
install sweeps and then read by nobody — no init summary line, no warning. The
uninstall-side sweep was worse: it logged removals only under --verbose and
discarded sweep.failed entirely.

Both halves of the data matter. A silent removal from ~/.claude/agents/ is
indistinguishable from an asset that was never installed, and a FAILED removal
leaves a retired agent or command still loading in Claude Code with no
diagnostic at all.

- init.ts gains formatSweepSummary(), a pure InstallReport -> SummaryLine[]
  formatter, wired into the post-install summary next to the shadow reporting.
- sweepDevflowNamespaces now warns on every failed removal regardless of
  verbosity, and keeps removals behind --verbose.
runCleanupPhase took a cwd for ".devflow/ and the .claudeignore fallback" but
called getGitRoot() with no argument, so the git root — the PRIMARY .claudeignore
path — still came from process.cwd(). Half the phase acted on the injected
directory and half on the process directory. Its five confirm prompts also read
process.stdin.isTTY directly, even though they can remove the user's
.claudeignore, strip the security deny list from ~/.claude/settings.json, and
edit the shell profile. Under a TTY-attached runner (npm run test:watch) the A8
tests would point those prompts at the developer's real files; the suite's own
output already shows it reaching /Users/<user>/.zshrc.

- getGitRoot gains an optional cwd (defaults to process.cwd(), all other callers
  unchanged); runCleanupPhase passes its injected cwd.
- runCleanupPhase and runFullPhaseForScope take isTTY as an input, matching the
  resolveDevflowDirCleanup({isTTY}) pattern already used one call deeper.
- Drops selectedPluginNames from runSelectivePhaseForScope — it was accepted,
  never read, and silenced with `void`.

Also converts the revert-before-remove ordering invariant from a prose comment
into two executable guards. The wrong order leaves no filesystem trace (the file
is deleted either way and revertExternalAgents fails silently down skippedMissing),
so the source order is the only thing that can be asserted.

Falsified: restoring getGitRoot() reddens the git-root test; restoring any
process.stdin.isTTY read inside the phase reddens the isTTY guard.
tests/paths.test.ts computed `abs` and then discarded it with `void abs` — the
assertion it was meant to make (relative and absolute parents resolve alike) is
now actually made. uninstall.ts had a doubled blank line after uninstallCommand.
…aliseAgentKeys

Both __proto__ guard paths previously misreported their outcome:

- oldKey === '__proto__': pushed to dropped[] without deleting the own
  property from result or setting didMutate=true (output claimed a drop
  without performing one).
- newKey === '__proto__': pushed oldKey to dropped[] (collision bucket),
  but the real reason is prototype-pollution guard, not a canonical-key
  collision — the migration warning "canonical key already present,
  existing value kept" was factually wrong for this path.

Fix: introduce guardDropped[] as a distinct return field for the two
pollution-guard paths. dropped[] now exclusively tracks collision drops
(verbatim warning preserved). Reflect.deleteProperty safely removes the
own '__proto__' property spread may have copied. Migration emits a
separate warning for guardDropped with accurate language.

Tests: update both __proto__ guard tests to assert didMutate=true,
dropped=[], and guardDropped=[key] for their respective paths.

Co-Authored-By: Claude <noreply@anthropic.com>
Plan item B6 required one test per parser arm; the export was previously
covered only transitively through the migration harness.

Add a dedicated describe block with 7 tests covering every discriminant:
  ok   — {"agents":{"x":42}} → kind=ok, rawAgents.x === 42
  skip — ENOENT, empty file, BOM-only (U+FEFF stripped to "")
  warn — invalid JSON ("invalid JSON"), unreadable file ("cannot read"),
         non-object agents field ("non-object agents field")

Message substrings are pinned to match what the migration tests already rely
on. Root guard (running as root skips the EACCES test — mirrors the pattern
in migrations.test.ts).

Co-Authored-By: Claude <noreply@anthropic.com>
…lers

The "When it runs" heading cited only uninstall.ts: sweepDevflowNamespaces,
but the first bullet described install-time sweeping in installViaFileCopy.
Update the heading to list both actual call sites:
  installer.ts: installViaFileCopy (install-time)
  uninstall.ts: sweepDevflowNamespaces (selective-uninstall-time)

The two bullets and their descriptions are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Replace the hand-written inline string in runDryRunPhase with a new
exported enumerateDryRunExtras(claudeDir, devflowDir) that derives its
path list from installArtifactPaths (agent-models.json, etc.) and
enumerates ALL skill variants — both devflow:-namespaced prefixed dirs
and bare legacy dirs — so the dry-run preview matches what removal
actually removes.

Also:
- Fix installArtifactPaths docstring overclaim ("complete list" → notes
  that manifest.json is removed separately)
- Renumber runCleanupPhase inline step comments (1,4,5,6,7 → 1,2,3,4,5)
- Add regression tests for agent-models.json and bare legacy skill dir
  coverage in the enumerated path set
…; add proxy-off suffix to --list (B12, B11)

B12: mergeTuiRowsIntoMapping now uses persistedModelFor(row) and
persistedEffortFor(row) (single-source helpers) instead of inlining
row.configuredModel / row.configuredEffort, so TUI saves and STATE column
share the same persisted-value predicate. Update persistedEffortFor JSDoc
to reflect its new caller.

B11: --list renders "saved-inactive (proxy off)" not bare "saved-inactive"
via the new (proxy off) suffix in the formatListOutput switch; TUI STATE
column stays bare.  Export formatListOutput for direct test coverage.
- Add missing CLAUDE.md hand-migration bullet (users must update their
  own files — devflow never edits CLAUDE.md)
- Fix STATE column count: 14 = 79 ≤ 80 (was incorrectly stated as 13 = 78)
- Replace phantom devflow:apply-knowledge with the real skill names
  devflow:apply-decisions and devflow:apply-feature-knowledge
- Replace phantom LEGACY_AGENT_NAMES with "orphan sweep" (the actual
  mechanism used)
- Remove four incorrect Removed bullets that described live features or
  fabricated removals (self-learning system, devflow learn --purge,
  debug logging, knowledge citations)
- Update old agent name list in Changed bullets to current canonical names
…ions JSDoc (A9, C5)

A9 (file-organization.md):
- "Nothing outside these four namespaces is written by devflow init" was
  false; replaced with accurate description including settings.json and
  ~/.devflow/ state files
- "sweeps … after copying new files" was false; corrected to pre-install
  sweep

C5 (migrations.ts):
- Two JSDoc bodies claimed MIGRATIONS is empty; updated to describe the
  actual state with the canonicalise-agent-keys-v1 global migration

C5 (commands.md):
- Remove fabricated "effort estimate" and "machine-readable tracking issue"
  details from /dynamic-tickets; the tracking issue body is prose written
  by the Synthesize agent
… (FIX 7)

Add tests for:
- Top-level JSON non-object (bare number) → kind='warn' with message
  containing "not a JSON object"
- agents field null/undefined → kind='skip' (no agents to migrate)

These two branches (lines 247-249, 253-255 of parseRawEnvelope) had no
test coverage on this branch.
…n collision path

F1: readAgentMapping now routes through parseAgentMappingEnvelope for BOM
(U+FEFF) stripping, shared JSON-parse error handling, and skip/warn/ok
dispatching. Special case: 'non-object agents field' warn arm returns
Ok(empty) for backward compat with files that have agents:[].

F8: canonicaliseAgentKeys collision path drops the warn() call — the
structured dropped array is the sole channel; migrations.ts surfaces it.
Updated unit test from toHaveLength(1) to toHaveLength(0).

F13: adds a traversal-key test for reapplyAgentMapping's containment
guard: a key whose target exists outside installDir is blocked with a
warning and leaves the target file untouched.

Applies PF-009 (degrade-not-throw) across all three fixes.
F2: removeDevFlowInstallArtifacts uses isContainedIn() from paths.ts
instead of an inline containment check, sharing the single source of truth.

F5: remove the bulk settings.hooks deletion block that was erroneously
scoped — it only fired when foreign hooks remained, deleting them.
Devflow's surgical strippers (removeAmbientHook etc.) already handle
Devflow-owned hooks; remaining entries are third-party and must survive.

F6: pre-capture managed proxy ports (scope→port map) before runFullPhaseForScope
runs, since removeDevFlowInstallArtifacts inside that phase deletes proxy.json.
runCleanupPhase now receives managedProxyPorts in opts and uses the pre-read
port for URL stripping instead of a post-deletion read fallback.

F7: enumerateDryRunExtras step 4 guards each installArtifactPaths entry with
fs.access before pushing — paths that never existed are omitted from the
dry-run preview list.

F9: removeAllDevFlow calls sweepDevflowNamespaces after its registry+legacy
loop so orphaned devflow:* skill dirs (retired, renamed, or deleted from
the registry) are cleaned up on full uninstall as well as selective.

F10: introduce preservedLogged flag in the .devflow/ prompt branch so the
cancel path (which already emits a specific message) does not also emit
the generic '.devflow/ preserved' line, eliminating the duplicate.

Static guards added for F5 and F6; behavioral tests for F7, F9, F11.
F12: mergeTuiRowsIntoMapping must not allocate a new object for a row
that is inert (dormant GPT model, proxy off). The new Object.is()
assertion pins that the original entry reference is returned unchanged,
making the inertness contract explicit and detectable.
…for rule paths

F14: extract recordSweep(report, kind, sweep) helper from the three
identical inline push blocks in installViaFileCopy, eliminating the
repetition and ensuring the kind tag is always populated.

F15: change InstallReport.sweptOrphans from string[] to SweptOrphan[]
({ kind: 'skill'|'command'|'agent', name: string }) so formatSweepSummary
can render 'agent git' instead of 'git', disambiguating assets with the
same registry name across different types. formatSweepSummary updated to
format each entry as '{kind} {name}'. Tests updated to pass { kind, name }
objects; new F15 test asserts the kind field on swept orphan entries.
Existing toContain(name) assertion updated to .some(o => o.name === name).

F16: three rule path constructions in installOneRule now use mdFileName()
instead of the inline template literal, sharing the single source of truth
with every other .md path in the installer.
…nization.md

F17 comment corrections:

hud/git.ts: resolveComparisonRef is called from layers (a) and (b) only,
not 'all three'. Layer (c) (local-only fallback) returns a branch name
directly without invoking the function. Two comment blocks corrected.

agent-models.ts: containment guard comment cited PF-014 (cancel-path
cleanup) but the guard follows PF-009 (degrade-not-throw). Corrected.

docs/reference/file-organization.md: 'pre-install sweep' description was
inaccurate — skills are swept before their copy phase, but commands and
agents are swept after theirs. Also updated to document that
sweepDevflowNamespaces now runs on full uninstall via removeAllDevFlow
(F9 wiring).
…-sections bullet

F3: investigation of the three items flagged as missing from ### Removed
(self-learning system, devflow learning --purge, ## Knowledge Citations
rename) found that none are current-branch removals — they were earlier-
version removals correctly excluded by commit 154834f.

The one genuine omission: the ## Decisions Citations section was added
as part of the Triage + Code split (feat(resolve)! commit 5903547) but
was absent from the '### Added' new-sections bullet. It is restored here:
the Triage agent aggregates cited ADR-NNN / PF-NNN IDs into this section.
… changes

F4: two KB updates after F1 and F15 land:

external-model-routing: correct the parseAgentMappingEnvelope description
which previously claimed 'readAgentMapping does NOT call
parseAgentMappingEnvelope'. F1 changed that — readAgentMapping now routes
through it for BOM tolerance and shared error handling. Document the
'non-object agents' special case and the backward-compat Ok(empty) return.

installer-shadowing: update InstallReport.sweptOrphans from string[] to
SweptOrphan[] with the { kind, name } shape introduced in F15. Document
the recordSweep helper added in F14. Both the interface block and the
prose sentence above it are updated.
@dean0x

dean0x commented Aug 19, 2026

Copy link
Copy Markdown
Owner Author

Review cycle 2 (2026-08-19_1756) — incremental over 46 finding-resolution commits

Convergence: Zero cycle-1 findings re-introduced; all by-design deferrals held (per all 11 reviewers).

Resolved in follow-up commits a2e3c44..0c03f34 (pushed with this comment)

  • readAgentMapping wired through shared envelope parser (BOM divergence closed); cast pair removed
  • delete settings.hooks block removed (third-party hooks now survive uninstall)
  • Managed proxy port captured before artifact removal (non-default ANTHROPIC_BASE_URL now stripped properly)
  • Dry-run artifact list existence-filtered; test de-vacuized
  • Full uninstall now sweeps orphaned devflow:* skill dirs
  • Duplicate collision warning removed
  • CHANGELOG Decisions-Citations record restored
  • KB parser/type/char-count corrections (saved-inactive length corrected to 14 chars)
  • recordSweep extraction and helper consolidation
  • SweptOrphan kind tags added for agent/command/skill discrimination
  • mdFileName adoption in rule paths
  • Comment/doc corrections (resolveComparisonRef layer count, reference doc false pre-install claim)
  • Skill-sweep test now verifies registry-skill survival
  • 8 new discriminating test assertions added

Deferred for triage (pre-existing or policy — not introduced by branch)

  • Bare-name skill deletion sourced from live registry vs frozen legacy list (PF-012 tension, 3 sites, pre-existing behavior)
  • shellExec has no maxBuffer (for-each-ref/status overflow would silently blank HUD segments — pre-existing module)
  • Three sequential git spawns at top of gatherGitStatus (~27ms wall time, parallelization deferred)
  • TRUNK_BRANCHESSKILL.md sync guard hardcodes a third copy instead of parsing markdown
  • readInstalledAgentNames swallows EACCES silently (deliberate plan decision — degrade-not-throw)
  • runCleanupPhase further extraction needed (240 lines, 5 responsibilities, deferred as structural refactor)
  • LEGACY_AGENT_KEYS identity-entry module-load assertion
  • Low-severity test-shape items (export-shape tests, runDryRunPhase branch observation, installer sweep kind coverage)

Reports & Verdict

Full reports in .devflow/docs/reviews/wave-agent-roster/2026-08-19_1756/ — synthesis verdict was CHANGES_REQUESTED against 6e08ba5. The blocking subset is resolved by the pushed follow-up commits a2e3c44..0c03f34; remaining items are the deferred-triage list above (pre-existing patterns, low-touch structural improvements for follow-up).


dean0x added 3 commits August 19, 2026 20:52
…LL_NAMES only

The live-registry union let install/uninstall rm -rf a user's own
~/.claude/skills/{name} for any current registry skill name. All four
sites (installViaFileCopy, removeAllDevFlow, removeSelectedPlugins,
enumerateDryRunExtras) now bare-sweep from the frozen legacy list only;
the installer's bare pass is deleted outright as redundant with init's
legacy cleanup pass, which solely owns bare legacy removal. Discriminating
tests seed a foreign skills/security/ dir (RED-proven against the old
code) and a non-vacuity anchor guards the hazard set. Docs updated to
the split-pass contract. Avoids PF-012.
shellExec relied on Node's 1MiB default; git for-each-ref output scales
with ref count, and overflow was swallowed into '', silently blanking the
ahead/behind and diff-stats segments. Explicit GIT_MAXBUFFER at the
single execFile choke point; regression test simulates
ERR_CHILD_PROCESS_STDIO_MAXBUFFER and pins graceful degradation.
The guard test pinned its own hardcoded third copy of the branch list,
so SKILL.md edits left CI green and the subset-only check missed extras.
The test now parses the Protected Branches canonical list from
worktree-support SKILL.md and asserts set-equality in both directions,
including release/* -> TRUNK_BRANCH_PREFIXES.
@dean0x

dean0x commented Aug 19, 2026

Copy link
Copy Markdown
Owner Author

Three cycle-2 deferred-triage items resolved on this branch:

1. Bare-name skill deletion now sourced exclusively from frozen LEGACY_SKILL_NAMES (5562b13)

All four affected sites — installViaFileCopy, removeAllDevFlow, removeSelectedPlugins, and enumerateDryRunExtras — now sweep bare names from the frozen legacy list only. A fourth site (removeSelectedPlugins) was identified during validation beyond the three flagged in the review. The installer's own bare pass is deleted outright as redundant with init's legacy cleanup pass, which solely owns bare legacy removal. Discriminating tests seed a foreign skills/security/ dir (RED-proven against the old code); a non-vacuity anchor guards the hazard set. Avoids PF-012.

2. Explicit 16 MiB maxBuffer on HUD git subprocess (499aabe)

shellExec relied on Node's 1 MiB default; git for-each-ref output scales with ref count, and overflow was swallowed into '', silently blanking the ahead/behind and diff-stats segments. GIT_MAXBUFFER is now set at the single execFile choke point. Regression test simulates ERR_CHILD_PROCESS_STDIO_MAXBUFFER and pins graceful degradation.

3. TRUNK_BRANCHES guard now parses the SKILL.md canonical list (cf8c137)

The old guard test pinned its own hardcoded third copy of the branch list, so SKILL.md edits left CI green and the subset-only check missed extras. The test now parses the Protected Branches canonical list from worktree-support SKILL.md and asserts set-equality in both directions, including release/*TRUNK_BRANCH_PREFIXES.


Still deferred (out of scope for this wave): runCleanupPhase extraction and low-severity test-shape items noted in the review.

@dean0x
dean0x merged commit 9db3d85 into main Aug 19, 2026
2 checks passed
@dean0x
dean0x deleted the wave/agent-roster branch August 19, 2026 19:46
dean0x added a commit that referenced this pull request Sep 7, 2026
Updated fixture documentation from 11 to 14 shapes to reflect the current
test setup where Shape L is split into L1 (fully-pushed develop) and L2
(develop +1 unpushed), and Shape M (dirty-tree asymmetry) was added during
PR #287 integration.

Shapes defined: A–K (11), L1, L2 (L split into 2 variants), M = 14 IIFEs.
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.

1 participant