refactor(agents)!: action-verb agent roster, audit-claude removal, agents TUI + HUD fixes - #287
Conversation
…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
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.
Review cycle 2 (2026-08-19_1756) — incremental over 46 finding-resolution commitsConvergence: 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)
Deferred for triage (pre-existing or policy — not introduced by branch)
Reports & VerdictFull reports in |
…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.
|
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 — 2. Explicit 16 MiB maxBuffer on HUD git subprocess (499aabe)
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 Still deferred (out of scope for this wave): |
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.
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 againstmainis 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.mainhas 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:
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.devflow agentsTUI 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.
bug-analyzerDEVFLOW_PLUGINS[].agents[],~/.claude/agents/devflow/{slug}.md,agent-models.jsonkeys,devflow agents --set {slug}BugAnalyzername:on line 2 of each agent fileForm B is not derived from form A, and it diverged in the shipped tree:
bug-analyzer.mddeclaredname: BugAnalyzer(hyphen dropped), with the slug appearing nowhere in the file.The critical part: nothing in
src/ever parses thatname:field. Only Claude Code does, at spawn time. So a rename that correctly updates the filename, the registry, and everysubagent_typeliteral — 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.
src/core/orphan-sweep.ts; one compute site used by both the installer and the uninstaller.--pluginpartial installs.getAllCommandNames(), which existed but was unused anywhere insrc/.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/isDevFlowInstalledare now exported (previously unexported and therefore untestable); the selective path sweeps retired assets and callsrevertExternalAgents;--keep-docsis honored inresolveDevflowDirCleanup(previously an active data-loss path that could prompt to wipe skill shadows andpreference-profile.md); the artifact list is completed; and a containment precondition prevents any artifact path from resolving to~/.devflowitself 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.jsonandmigrations.jsonare artifacts;hud.jsonis user state.PR #283 — remove the audit-claude feature (23 files)
Deletes the
devflow-audit-claudeplugin, theclaude-md-auditoragent, the/audit-claudecommand, and the now-dead non-selectable-optional carry mechanism.Why the carry was deleted rather than retargeted:
devflow-audit-claudewas the only plugin bothoptional: trueand insidepartitionSelectablePlugins'sEXCLUDED, 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 17it()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 toEXCLUDEDneeds 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)and16 agents.PR #284 — agents TUI (11 files)
sol (gpt-5.6-sol)now renders assol.renderModelCelldrops 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.agent-models.jsonkeys rendered as editable rows.--listdeliberately stays lowercase: its AGENT column is an identifier users copy into--set, which exact-matches.src/core/model-discovery.tsis byte-identical on purpose — itsselectableNamesdoubles as the--setvalidation allowlist, so narrowing it would have rejected--set coder --model gpt-5.6-sol.PR #285 — the rename (97 files)
codercodesimplifiersimplifydesignerdesignskimmerskimevaluatorevaluatesynthesizersynthesizeresearcherresearchtestertestreviewerreviewtriagertriagescrutinizerscrutinizevalidatorvalidatebug-analyzerdiagnoseUnchanged:
git,knowledge,learning./bug-analysisis NOT renamed — the plugin, the command, the skill,.devflow/docs/bug-analysis/andtests/bug-analysis/all stay. Only the agent moved. (bug-analyzerandbug-analysisshare a prefix; the only safe discriminator is the trailinger/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:
agentType:coverage over thedynamic-*commands — roughly 40 spawn sites that no test read at all before this. Tolerates the no-spaceagentType:"X"form.model:..devflow/features/knowledge bases), with a bidirectional allowlist.All dist-reading guards now fail loudly naming
npm run buildwhendist/is absent, rather than skipping. Two pre-existing fail-open guards were fixed the same way.Key migration:
LEGACY_AGENT_KEYSmaps the 13 old slugs to canonical names, applied by one purecanonicaliseAgentKeysused by both consumers —readAgentMappingand ascope:'global'migration entry — so they cannot drift. It parses raw JSON rather than routing throughreadAgentMapping, 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 constructorwould otherwise return a function intopath.join.No
LEGACY_AGENT_NAMESentries 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()insrc/hud/git.tsscanned 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 becauselogs/HEADis per-worktree, the same branch reported different numbers from different checkouts.Two further defects in the same file:
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.gh pr viewnetwork 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 throughorigin/main|master|develop|trunkthen 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 theghlayer are deleted outright rather than kept as fallbacks — a fallback that fires in exactly the confusing cases is worse than none.--fork-pointwas 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.tshad zero coverage — every HUD test fed hand-written values into the rendering layer, andtests/hud-render.test.tshardcodedahead: 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:maingreen twice at ~21s; branch red 3/3 at ~58s with an identical failure set). Relocated, the unit suite returns to themainbaseline exactly.How to review this
Suggested order, roughly by risk:
src/core/orphan-sweep.tsand its two call sites. This is the only new deletion primitive. Confirm it never intersects with the selected-plugin subset.src/cli/commands/uninstall.ts— the disjointness of user-state vs install-artifact, and the containment precondition.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?src/core/agent-models.ts+src/core/migrations.ts— the key migration, especially the raw-JSON parse and the prototype safety.git diff main...wave/agent-roster -- src/assets/agents/shows the 13 renames as git renames rather than delete+add.src/cli/agents-view/for the TUI changes.src/hud/git.tsfor the status-line fix — small, self-contained, and its test is intests/integration/.Two things that look wrong but are deliberate:
LEGACY_SKILL_NAMESand theLEGACY_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.mdsemits the heading andcode.mdparses it — and also a PR-description section name. All five or none.Verification
mainPost-review cleanup (4 commits,
9eaba10..0cd3922) adjusted two of these numbers: residual retired-name occurrences were purged fromdocs/reference/and the tracked.devflow/features/knowledge bases, the GAP-5 corpus was extended to cover both directories (233 → 248 files), and the deadLEGACY_AGENT_NAMESlist 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 holdssubagent-skill-preload, the only end-to-end exercise of the frontmattername:field.One test-harness fix was needed there:
pack-install.test.ts's--versionstep 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.runSyncalso converted the resulting SIGTERM into a bareexit 1with 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.jscompiled in,files[]andbinunchanged, and--versionprinting2.0.0on bothmainand this branch.Build and
tsc --noEmitclean. The integration suite — which the default vitest config excludes, and which nothing in this work had previously exercised — also passes (3 files / 16 tests), includingsubagent-skill-preload, the only end-to-end check of form B.Behaviors verified end-to-end against a seeded temporary
HOME:initis a no-op.{"model":"not-a-real-model","effort":"bogus"}is preserved rather than dropped to{}. (Routing throughreadAgentMappingwould have silently deleted them while a loose assertion still passed.)--set coderexits 0 and writescode.dist/: 66 raw matches, all resolving to a live agent's form-B name or the Claude Code built-inExplore.Provenance against
main:.github/,.claude/,.devflow/docs/,.devflow/memory/and.devflow/learning/are byte-identical. The only change underscripts/is the removal of one element fromscripts/build-mds.tsfor the audit-claude deletion.Defects caught during review
Recorded because they are the kind a reviewer should look for elsewhere:
_partials/_preamble.mdsenumerates the validagentTypestring values; the PascalCase pass rewrote them toCode agent, Validate agent, …. That partial compiles into every dynamic command, so a workflow authored from it would have emittedagentType: "Code agent"and resolved to nothing.LEGACY_AGENT_KEYSbriefly 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.EXCLUDEDto 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
subagent_type/agentTypevalue in a user's own custom commands or agents must be updated by hand — devflow cannot migrate files it does not own.--plugin=audit-claudeis now rejected. Staledevflow-audit-claudeentries in existing manifests are inert and pruned on the next partial reinstall.readAgentMappingnever reads theversionfield, and a test pins that it is ignored. There is no mechanism that could warn a user, so it is documented instead.Known gaps, deliberately out of scope
revertExternalAgentson the selective uninstall path reverts every installed agent, not only those being removed, so surviving agents lose GPT frontmatter until the nextdevflow init. Fixing it is an API change toRevertOptions.devflow agents --listdoes not surface orphanagent-models.jsonkeys; only the TUI gained them.defaultand the dormant value. Fixing it would change dormancy semantics, which this work deliberately holds constant.tsconfig.jsonscopes tosrc/**/*(pre-existing).canonicalise-agent-keys-v1migration is silently marked applied rather than retried, becauserunGlobalMigrationmarks any non-throwing return as applied and this migration returns I/O failures as warnings. Net impact is low:readAgentMappingre-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:
subagent_typeat runtime, because nothing insrc/parses the frontmattername: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-buildis the only end-to-end check ofagentType: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.
Bug-Analyzerdisplayed while the JSON key stays lowercase. Preconditions, or the check is vacuous: warm model cache,proxy.jsonenabled: false, at least one registry agent absent from the install directory, andagent-models.jsonseeded 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
Testthe agent with "test" the noun, orReviewthe 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
ExploreandPlan, and none of the new names collide (Planis taken, but devflow has noplanagent). 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.