Skip to content

refactor(install): registry-diff asset sweep + uninstall lifecycle parity - #282

Merged
dean0x merged 5 commits into
wave/agent-rosterfrom
feat/install-uninstall-lifecycle
Aug 17, 2026
Merged

dean0x merged 5 commits into
wave/agent-rosterfrom
feat/install-uninstall-lifecycle

Conversation

@dean0x

@dean0x dean0x commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Summary

PR 1 of 4 in the wave/agent-roster integration wave. This is the foundation PR: it replaces hand-maintained legacy-name cleanup with a self-maintaining registry-diff sweep, so the later PRs in this wave (removing audit-claude, renaming 13 agents) need no legacy-list entries at all.

Install side

  • New shared sweepOrphanedAssets helper in src/core/orphan-sweep.ts; one compute site, consumed by both the installer and uninstaller.
  • Replaced the LEGACY_AGENT_NAMES deletion loop with an ungated registry-diff sweep over ~/.claude/agents/devflow/.
  • Added the equivalent sweep for commands, wiring up getAllCommandNames() — which existed but was previously unused anywhere in src/.
  • Ungated the existing skills sweep so it runs on partial (--plugin) installs too.

Safe because getAllAgentNames()/getAllCommandNames() span all plugins regardless of selection: assets belonging to unselected plugins survive, and only names that left the registry entirely are removed.

Uninstall side

  • removeAllDevFlow, removeSelectedPlugins and isDevFlowInstalled are now exported and therefore testable.
  • Registry-diff sweep added to the selective uninstall path, which previously left retired assets behind permanently.
  • revertExternalAgents now runs on the selective path, before agent files are removed.
  • --keep-docs is honored in resolveDevflowDirCleanup — previously an active data-loss path that could prompt to wipe skill shadows and preference-profile.md.
  • isDevFlowInstalled no longer keys off commands/devflow alone, so a commandless plugin set is detected instead of exiting 1.
  • Artifact list completed: migrations.json, agent-models.json, costs/, logs/, cache/ parent, proxy artifacts.
  • A containment precondition now prevents any artifact path from resolving to ~/.devflow itself or above it.

Classification invariant

enumerateUserDevFlowContent (user state, survives unless explicitly confirmed) and the install-artifact list (removed on every path, including decline/cancel/--keep-docs) are now disjoint, enforced by a test. agent-models.json is an install artifact — leaving it behind meant a reinstall silently re-applied stale per-agent overrides. hud.json is user state.

Testing

85 files / 2,716 tests / 0 failures (baseline 84 / 2,693; +23 fully accounted per-file). Build and tsc --noEmit clean.

New tests deliberately target the partial-install path — on a full install the pre-existing wholesale wipe deletes planted files anyway, so a green test there would prove nothing about the sweep. Each sweep test also asserts an unselected plugin's asset survived, which is what distinguishes "the sweep worked" from "the wipe ran". Non-vacuity was proven by red→green falsification for the sweep tests, the residue allow-list equality test, and the disjointness test.

Notes

  • LEGACY_SKILL_NAMES and the LEGACY_SKILLS_* lists are untouched and must stay so: they are deletion manifests for pre-namespace bare dirs outside the swept namespace.
  • LEGACY_AGENT_NAMES now has no production consumer. Left in place deliberately rather than churned in this PR.
  • Known limitation, out of scope: revertExternalAgents on the selective path reverts every installed agent rather than only those being removed, so surviving agents lose GPT frontmatter until the next devflow init. Fixing it is an API change to RevertOptions.

dean0x and others added 5 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/
@dean0x
dean0x merged commit 0682a42 into wave/agent-roster Aug 17, 2026
1 check passed
@dean0x
dean0x deleted the feat/install-uninstall-lifecycle branch August 17, 2026 23:22
dean0x added a commit that referenced this pull request Aug 19, 2026
…ents TUI + HUD fixes (#287)

> **Do not merge yet.** Two verification steps remain that cannot be run
from an automated session — see [Before this can
merge](#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 #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.

---------

Co-authored-by: Claude <noreply@anthropic.com>
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