diff --git a/.devflow/features/installer-shadowing/KNOWLEDGE.md b/.devflow/features/installer-shadowing/KNOWLEDGE.md index e4058d80..58acfb57 100644 --- a/.devflow/features/installer-shadowing/KNOWLEDGE.md +++ b/.devflow/features/installer-shadowing/KNOWLEDGE.md @@ -1,11 +1,11 @@ --- feature: installer-shadowing name: Installer & Skill/Rule Shadowing -description: "Use when modifying the install pipeline (installViaFileCopy, installAllRules, composeScripts, InstallReport), adding or changing skill/rule shadow override logic, touching uninstall scope (enumerateUserDevFlowContent, removeDevFlowInstallArtifacts, resolveDevflowDirCleanup) or install-artifact cleanup, extending the CLI skills/rules management commands, working with asset directory accessors (rulesDir, skillsDir, commandsDir) and package-root resolution, or modifying the init seeding layer (resolveInitSeed, resolveSeedFeatures, resolveSeedFlags, resolveSeedPlugins, --reset, knownFlags, knownPlugins, readConfigIfPresent, resolveExistingViewMode, getAllCommandNames, applyNonSelectableCarry, proxy). Keywords: installViaFileCopy, installAllRules, composeScripts, InstallReport, RuleInstallOutcome, SkillShadowState, RuleShadowState, shadow, unshadow, validateSkillShadow, validateRuleShadow, seedRuleShadow, prefixSkillName, unprefixSkillName, devflow:, skills, rules, uninstall, EISDIR, enumerateUserDevFlowContent, removeDevFlowInstallArtifacts, resolveDevflowDirCleanup, getPackageRoot, rulesDir, skillsDir, agentsDir, commandsDir, scriptsDir, LEGACY_SKILL_NAMES, LEGACY_AGENT_NAMES, orphan sweep, getAllSkillNames, getAllCommandNames, resolveInitSeed, resolveSeedFeatures, resolveSeedFlags, resolveSeedPlugins, resolveResetGatedInputs, resolveNonSelectableOptionalCarry, applyNonSelectableCarry, applyCliToggles, knownFlags, knownPlugins, readConfigIfPresent, resolveExistingViewMode, resolveFinalViewMode, reset, init-seed, proxy, reapplyAgentMapping, revertExternalAgents, agent-models.json, proxy.json, proxy-routing.json, proxy.pid, applyDisableToSettings, buildRealPreflightDeps." +description: "Use when modifying the install pipeline (installViaFileCopy, installAllRules, composeScripts, InstallReport), adding or changing skill/rule shadow override logic, touching uninstall scope (enumerateUserDevFlowContent, removeDevFlowInstallArtifacts, resolveDevflowDirCleanup) or install-artifact cleanup, extending the CLI skills/rules management commands, working with asset directory accessors (rulesDir, skillsDir, commandsDir) and package-root resolution, or modifying the init seeding layer (resolveInitSeed, resolveSeedFeatures, resolveSeedFlags, resolveSeedPlugins, --reset, knownFlags, knownPlugins, readConfigIfPresent, resolveExistingViewMode, getAllCommandNames, proxy). Keywords: installViaFileCopy, installAllRules, composeScripts, InstallReport, RuleInstallOutcome, SkillShadowState, RuleShadowState, shadow, unshadow, validateSkillShadow, validateRuleShadow, seedRuleShadow, prefixSkillName, unprefixSkillName, devflow:, skills, rules, uninstall, EISDIR, enumerateUserDevFlowContent, removeDevFlowInstallArtifacts, resolveDevflowDirCleanup, getPackageRoot, rulesDir, skillsDir, agentsDir, commandsDir, scriptsDir, LEGACY_SKILL_NAMES, LEGACY_AGENT_NAMES, orphan sweep, getAllSkillNames, getAllCommandNames, getAllAgentNames, DELETED_PLUGIN_NAMES, EXCLUDED, resolveInitSeed, resolveSeedFeatures, resolveSeedFlags, resolveSeedPlugins, resolveResetGatedInputs, applyCliToggles, knownFlags, knownPlugins, readConfigIfPresent, resolveExistingViewMode, resolveFinalViewMode, reset, init-seed, proxy, reapplyAgentMapping, revertExternalAgents, agent-models.json, proxy.json, proxy-routing.json, proxy.pid, applyDisableToSettings, buildRealPreflightDeps." category: architecture -directories: [src/targets/claude-code/installer.ts, src/targets/claude-code/legacy.ts, src/cli/commands/init.ts, src/cli/commands/init-seed.ts, src/cli/commands/uninstall.ts, src/cli/commands/rules.ts, src/cli/commands/skills.ts, src/core/plugins.ts, src/core/assets.ts, src/core/paths.ts, src/core/manifest.ts, src/core/flags.ts, src/core/feature-config.ts] +directories: [src/targets/claude-code/installer.ts, src/targets/claude-code/legacy.ts, src/cli/commands/init.ts, src/cli/commands/init-seed.ts, src/cli/commands/uninstall.ts, src/cli/commands/rules.ts, src/cli/commands/skills.ts, src/core/plugins.ts, src/core/assets.ts, src/core/paths.ts, src/core/manifest.ts, src/core/flags.ts, src/core/feature-config.ts, src/core/orphan-sweep.ts] created: 2026-07-13 -updated: 2026-07-25 +updated: 2026-08-18 --- # Installer & Skill/Rule Shadowing @@ -57,11 +57,15 @@ All four asset types now **throw** when a declared source is absent — there ar Shadow paths remain tolerant: invalid/missing shadows warn-and-install-source (applies ADR-010). The hard-error policy applies only to declared Devflow sources. -### Orphan Sweep (full install only) +### Orphan Sweep -On full (non-partial) install, `installViaFileCopy` reads `~/.claude/skills/` and removes any `devflow:*` directory whose bare name is absent from `getAllSkillNames()` (the live registry). This is the mechanism for cleaning up renamed or deleted skills across upgrades without requiring manual removal. +`installViaFileCopy` runs three ungated registry-diff sweeps — all run on every install shape, including partial installs: -Bare (pre-namespace) dirs are **not touched** by the sweep — they are handled exclusively by the frozen `LEGACY_SKILLS_*` lists in `legacy.ts` (avoids PF-012). Shadow dirs (`~/.devflow/skills/`) are keyed by bare registry name and are unaffected. +- **Skills**: reads `~/.claude/skills/` and removes any `devflow:*` directory whose bare name is absent from `getAllSkillNames()`. Bare (pre-namespace) dirs are **not touched** — handled exclusively by the frozen `LEGACY_SKILLS_*` lists in `legacy.ts` (avoids PF-012). Shadow dirs (`~/.devflow/skills/`) are keyed by bare registry name and are unaffected. +- **Commands**: reads `~/.claude/commands/devflow/` and removes any `.md` file whose command name is absent from `getAllCommandNames()`. +- **Agents**: reads `~/.claude/agents/devflow/` and removes any `.md` file whose agent name is absent from `getAllAgentNames()`. + +All three `knownNames` sets span ALL plugins — not just the selected subset — so assets from uninstalled plugins survive a partial run; only assets that are completely absent from the registry are removed. Separate from the sweeps, `installViaFileCopy` still performs a **full directory wipe** of `commands/devflow/`, `agents/devflow/`, and `rules/devflow/` before reinstalling on full (non-partial) installs. The sweeps handle partial installs cleanly without that wipe. ### InstallReport @@ -154,16 +158,17 @@ Skills install under `~/.claude/skills/devflow:{name}` (prefixed). The `devflow: All skills from ALL plugins install regardless of plugin selection. `skillsMap` passed to `installViaFileCopy` is built by `buildFullSkillsMap` which covers every `DEVFLOW_PLUGINS` entry — not just the selected subset. Rules, by contrast, are plugin-scoped (only selected plugins' rules install). -### LEGACY_* Symbol Split +### LEGACY_* and DELETED_* Symbol Split -Legacy cleanup lists are split across two files: +Legacy cleanup lists and deleted-plugin tracking are split across two files: | Symbol | File | |--------|------| | `LEGACY_AGENT_NAMES`, `LEGACY_SKILL_NAMES` (+ `LEGACY_SKILLS_PRE_V1`, `LEGACY_SKILLS_V2`, `LEGACY_SKILLS_V2X`) | `src/targets/claude-code/legacy.ts` | | `LEGACY_PLUGIN_NAMES`, `LEGACY_COMMAND_NAMES`, `LEGACY_RULE_NAMES` | `src/core/plugins.ts` | +| `DELETED_PLUGIN_NAMES` | `src/core/plugins.ts` | -The split keeps target-specific delete lists separate from the plugin registry consumed by cross-cutting CLI commands. (avoids PF-012) +`DELETED_PLUGIN_NAMES` is a separate array (not a rename map) for plugins that have been removed entirely with no successor. `resolvePluginList` in `manifest.ts` filters deleted plugin names from prior manifest entries on partial reinstalls — stale entries are silently dropped. The split keeps target-specific delete lists separate from the plugin registry consumed by cross-cutting CLI commands. (avoids PF-012) ## Component Interactions @@ -218,9 +223,9 @@ Returns `'artifacts-only'` or `'prompt'`: - `'artifacts-only'` — non-interactive session, no user content, or precondition guard failure; runs `removeDevFlowInstallArtifacts` only. - `'prompt'` — interactive session with user content present; prompt states full scope (listed user-authored items, plus logs and install metadata). Confirm → `fs.rm(devflowDir, {recursive: true, force: true})`; decline OR cancel → falls through to `removeDevFlowInstallArtifacts` (clean end-state — never `process.exit()` here; applies ADR-003, avoids PF-014). -`enumerateUserDevFlowContent(devflowDir)` checks for: `devflowDir/skills/` (skill shadows), `devflowDir/rules/` (rule shadows), `devflowDir/preference-profile.md`, `devflowDir/learning.json`, and `devflowDir/agent-models.json` (agent model assignments). Returns a human-readable label for each that exists. Pure I/O — no side effects. +`enumerateUserDevFlowContent(devflowDir)` checks for: `devflowDir/skills/` (skill shadows), `devflowDir/rules/` (rule shadows), `devflowDir/preference-profile.md`, and `devflowDir/learning.json`. Returns a human-readable label for each that exists. Pure I/O — no side effects. -`removeDevFlowInstallArtifacts(devflowDir, verbose)` removes `manifest.json` (install state) plus proxy install artifacts non-fatally: `proxy.json`, `proxy-routing.json`, `proxy.pid`, `.proxy-spawn.lock/` (directory), `logs/proxy.log`, and `cache/models/` (directory — external model catalog cache entries). Before removing `proxy.pid`, it reads the PID and checks process existence via `process.kill(pid, 0)` — if the relay is still running, a warning is emitted with a manual kill hint. **The relay is never killed by uninstall** — informational only. Scripts are already gone via `removeAllDevFlow`. Per-artifact failures are silently ignored (avoids PF-009). +`removeDevFlowInstallArtifacts(devflowDir, verbose)` removes `manifest.json` (install state), `agent-models.json` (per-agent model overrides — reclassified as an install artifact so stale overrides are cleaned on uninstall without prompting the user), plus proxy install artifacts non-fatally: `proxy.json`, `proxy-routing.json`, `proxy.pid`, `.proxy-spawn.lock/` (directory), `logs/proxy.log`, and `cache/models/` (directory — external model catalog cache entries). Before removing `proxy.pid`, it reads the PID and checks process existence via `process.kill(pid, 0)` — if the relay is still running, a warning is emitted with a manual kill hint. **The relay is never killed by uninstall** — informational only. Scripts are already gone via `removeAllDevFlow`. Per-artifact failures are silently ignored (avoids PF-009). Settings cleanup in uninstall (the settings read-modify-write pass) calls `applyDisableToSettings(parsedSettings, managedPort)` in a single parse-mutate-serialize pass — the same helper `runDisable` uses — instead of separate `removeProxyHooks` + `stripProxyEnv` calls. `managedPort` is read from `proxy.json` (falling back to `DEFAULT_PROXY_PORT`) so only the `ANTHROPIC_BASE_URL` for Devflow's managed port is stripped; a user's own localhost URL on any other port is left untouched. @@ -246,7 +251,6 @@ A dedicated pure-function module (`src/cli/commands/init-seed.ts`) computes the - Old manifest (no `knownPlugins`): split existing into workflow/language buckets, adopt nothing. - Re-init with `knownPlugins`: split + adopt newly-added non-optional selectable plugins ∉ knownPlugins. -**Non-selectable optional carry**: `resolveNonSelectableOptionalCarry(manifestPlugins, allPlugins)` identifies optional plugins from the prior manifest (e.g. `devflow-audit-claude`) that are excluded from the selectable buckets by `partitionSelectablePlugins`. Internally uses a name→plugin `Map` for O(1) lookup (was a `.find()`-in-`.filter()` O(n·m) scan). `applyNonSelectableCarry(isPartialInstall, manifestPlugins, pluginsToInstall, allPlugins)` (pure exported helper) encapsulates the `!options.plugin` gate + carry call + dedup-merge loop; `init.ts` delegates to it. Without the carry, a full re-init would silently drop non-selectable optional plugins. **Reset gate** (`resolveResetGatedInputs`): `--reset` zeroes seedManifest, seedConfig, AND settingsSnapshot (the empty settings string prevents `resolveExistingViewMode` from surfacing an externally-set viewMode and defeating the factory reset). The real manifest/settings are still used for security deny-state detection and `installedAt` preservation. @@ -316,7 +320,7 @@ Exports: `hasRuleShadow(ruleName, devflowDir?)`, `listShadowedRules(devflowDir?) - **Skills are cleaned before install on every run.** `installViaFileCopy` removes both the legacy unprefixed and current prefixed skill directories for all known skills before reinstalling. Partial installs (via `--plugin`) still clean all skills universally. -- **Orphan sweep runs only on full installs.** The `devflow:*` stale-dir sweep in `~/.claude/skills/` is skipped on partial installs (`isPartialInstall === true`). A partial reinstall does not prune orphaned skills from the registry. +- **Orphan sweeps (skills, commands, agents) run on every install shape.** All three registry-diff sweeps are ungated — they run on full and partial installs alike. A partial reinstall still prunes assets absent from the full registry. The `knownNames` sets span ALL plugins so assets from plugins not included in the current run are preserved. - **`seedRuleShadow` tier 2 requires a built package root.** `rulesDir()` calls `getPackageRoot()`, which resolves from `dist/core/paths.js` depth and throws loudly if `package.json` is absent at the resolved root. Running `devflow rules shadow` without a built `dist/` causes a loud throw on tier-2 fallback. @@ -338,12 +342,12 @@ Exports: `hasRuleShadow(ruleName, devflowDir?)`, `listShadowedRules(devflowDir?) ## Key Files -- `src/targets/claude-code/installer.ts` — `installViaFileCopy`, `installAllRules`, `installRuleFile`, `composeScripts`, `validateSkillShadow`, `validateRuleShadow`, `InstallReport`, `ShadowSkip`, `RuleInstallOutcome`, `SkillShadowState`, `RuleShadowState`, `copyDirectory`, `chmodRecursive`; orphan sweep on full install +- `src/targets/claude-code/installer.ts` — `installViaFileCopy`, `installAllRules`, `installRuleFile`, `composeScripts`, `validateSkillShadow`, `validateRuleShadow`, `InstallReport`, `ShadowSkip`, `RuleInstallOutcome`, `SkillShadowState`, `RuleShadowState`, `copyDirectory`, `chmodRecursive`; ungated orphan sweeps for skills (`getAllSkillNames`), commands (`getAllCommandNames`), and agents (`getAllAgentNames`) run on every install shape - `src/core/assets.ts` — `skillsDir`, `agentsDir`, `rulesDir`, `scriptsDir`, `commandsDir` accessors; single source of truth for all asset source paths - `src/core/paths.ts` — `getPackageRoot()` with hard `package.json` assertion; 2-level-up resolution from `dist/core/paths.js` - `src/targets/claude-code/legacy.ts` — `LEGACY_AGENT_NAMES`, `LEGACY_SKILL_NAMES` (composed from `LEGACY_SKILLS_PRE_V1`, `LEGACY_SKILLS_V2`, `LEGACY_SKILLS_V2X`); target-specific delete lists for upgrade cleanup - `src/cli/commands/init.ts` — consumes `InstallReport` and `InitSeed`; calls `installViaFileCopy`; proxy preflight block using `buildRealPreflightDeps` factory from `proxy.ts` (`swallowSettingsReadError: true`); `reapplyAgentMapping` call (ordering load-bearing, guarded when mapping is empty AND proxy is off); proxy hooks + env in settings mutation pass; exhaustive `ShadowSkipReason` switch with `never` guard -- `src/cli/commands/init-seed.ts` — pure seeding helpers: `resolveInitSeed`, `resolveSeedFeatures` (proxy in manifest group), `resolveSeedFlags`, `resolveSeedPlugins`, `resolveResetGatedInputs`, `resolveNonSelectableOptionalCarry`, `applyNonSelectableCarry`, `applyCliToggles` (proxy toggle), `FEATURE_DEFAULTS` (proxy: false) +- `src/cli/commands/init-seed.ts` — pure seeding helpers: `resolveInitSeed`, `resolveSeedFeatures` (proxy in manifest group), `resolveSeedFlags`, `resolveSeedPlugins`, `resolveResetGatedInputs`, `applyCliToggles` (proxy toggle), `FEATURE_DEFAULTS` (proxy: false) - `src/cli/commands/uninstall.ts` — `removeAllDevFlow` (internal), `enumerateUserDevFlowContent` (includes agent-models.json), `removeDevFlowInstallArtifacts` (proxy artifacts + relay PID check; `isDir === true` strict equality), `revertExternalAgents` (before removeAllDevFlow), `computeAssetsToRemove`, `resolveSecurityRemovalDecision`, `resolveDevflowDirCleanup`; settings cleanup calls `applyDisableToSettings(settings, managedPort)` (port-scoped, single-pass) - `src/cli/commands/proxy.ts` — `applyDisableToSettings` (single-pass hook+env strip used by both runDisable and uninstall), `buildRealPreflightDeps` (factory for init and runEnable preflight deps), `ProxyPreflightDeps`, `addProxyHooks`, `removeProxyHooks`, `applyProxyEnv`, `stripProxyEnv` - `src/cli/commands/rules.ts` — `rulesCommand` positional dispatch, `seedRuleShadow` (3-tier), `handleRuleShadow`, `handleRuleUnshadow`, `buildRuleShadowTag`, `printRulesList`, `hasRuleShadow`, `listShadowedRules` @@ -351,7 +355,7 @@ Exports: `hasRuleShadow(ruleName, devflowDir?)`, `listShadowedRules(devflowDir?) - `src/core/manifest.ts` — `ManifestData` (with `knownPlugins`, `features.knownFlags`, `features.proxy`), `readManifest` (self-heals snapshots via `asStringArray`; proxy absent→false), `writeManifest`, `syncManifestFeature`, `resolvePluginList` - `src/core/flags.ts` — `FLAG_REGISTRY`, `resolveExistingViewMode`, `resolveFinalViewMode`, `applyFlags`, `stripFlags`, `getDefaultFlags` - `src/core/feature-config.ts` — `readConfig`, `readConfigIfPresent`, `writeConfig`, `updateFeature` -- `src/core/plugins.ts` — `prefixSkillName`, `unprefixSkillName`, `SKILL_NAMESPACE`, `DEVFLOW_PLUGINS`, `buildFullSkillsMap`, `buildRulesMap`, `getAllSkillNames`, `getAllCommandNames`, `partitionSelectablePlugins`, `LEGACY_PLUGIN_NAMES`, `LEGACY_COMMAND_NAMES`, `LEGACY_RULE_NAMES` +- `src/core/plugins.ts` — `prefixSkillName`, `unprefixSkillName`, `SKILL_NAMESPACE`, `DEVFLOW_PLUGINS`, `buildFullSkillsMap`, `buildRulesMap`, `getAllSkillNames`, `getAllCommandNames`, `getAllAgentNames`, `partitionSelectablePlugins`, `EXCLUDED`, `LEGACY_PLUGIN_NAMES`, `LEGACY_COMMAND_NAMES`, `LEGACY_RULE_NAMES`, `DELETED_PLUGIN_NAMES` - `tests/init-proxy.test.ts` — pins the reapply-after-preflight ordering invariant and the empty-mapping guard - `tests/uninstall-logic.test.ts` — pins proxy artifact removal, `isDir === true` correctness, live-PID warn-never-kill, stale PID, and per-item non-fatal behavior diff --git a/CHANGELOG.md b/CHANGELOG.md index 329bb188..87e16d26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **`devflow uninstall`**: Now removes proxy artifacts on uninstall — `ensure-proxy` hook registrations, `ANTHROPIC_BASE_URL` from `settings.json`, and the model discovery cache (`~/.devflow/cache/models/`) — in addition to standard command/agent/skill/rule removal. ### Removed +- **`devflow-audit-claude` plugin and `/audit-claude` command** (BREAKING): The CLAUDE.md audit plugin is removed. `--plugin=audit-claude` is now rejected by `devflow init`; stale `devflow-audit-claude` entries in existing manifests are silently pruned by `DELETED_PLUGIN_NAMES` on the next partial reinstall. The `claude-md-auditor` agent and `audit-claude.md` command are deleted; the orphan sweep removes any previously installed copies automatically. +- **Non-selectable optional carry mechanism**: `resolveNonSelectableOptionalCarry` and `applyNonSelectableCarry` deleted from `init-seed.ts`. The carry was guarding a now-impossible state (the only non-selectable optional plugin was `devflow-audit-claude`). A structural invariant test (`EXCLUDED ∩ optional === ∅`) ensures this state stays impossible. No behavior change for users. - **1.x migration registry and helper modules** (BREAKING): all 20 run-once 1.x upgrade migrations removed from `MIGRATIONS`; helper modules `legacy-decisions-purge.ts`, `decisions-ledger-migration.ts`, `marketplace-cleanup.ts`, and `mkdir-lock.ts` deleted. The migration framework stays for future 2.x entries. No 1.x → 2.0 upgrade path. - **Native `claude plugin install` path** (BREAKING): the `claude plugin install` code path is removed; `installViaFileCopy` (file copy) is the sole install mechanism for all Devflow assets. - **`extraKnownMarketplaces` registration from settings template**: the Devflow marketplace entry is no longer written to `~/.claude/settings.json` on install. diff --git a/CLAUDE.md b/CLAUDE.md index 36992339..0e9a5825 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,7 @@ Devflow enhances Claude Code with intelligent development workflows. Modificatio ## Architecture Overview -Registry-driven CLI tool with 23 plugins (12 core + 10 optional language/ecosystem + 1 optional workflow). Plugins are entries in DEVFLOW_PLUGINS in `src/core/plugins.ts` — each entry declares its `commands`, `agents`, `skills`, and `rules` arrays. All assets live once in `src/assets/` and install directly; the only compile step is `.mds` command sources → `dist/commands/` via `npm run build:mds`. +Registry-driven CLI tool with 22 plugins (12 core + 10 optional). Plugins are entries in DEVFLOW_PLUGINS in `src/core/plugins.ts` — each entry declares its `commands`, `agents`, `skills`, and `rules` arrays. All assets live once in `src/assets/` and install directly; the only compile step is `.mds` command sources → `dist/commands/` via `npm run build:mds`. | Plugin | Purpose | |--------|---------| @@ -28,7 +28,6 @@ Registry-driven CLI tool with 23 plugins (12 core + 10 optional language/ecosyst | `devflow-bug-analysis` | Proactive bug finding with static and semantic analysis | | `devflow-ambient` | Ambient mode — orchestrator charter + plan handoff | | `devflow-core-skills` | Auto-activating quality enforcement | -| `devflow-audit-claude` | Audit CLAUDE.md files (optional) | | `devflow-dynamic` | Dynamic workflow recipes — dependency-aware tickets→plan→build delivery pipeline (optional) | | `devflow-typescript` | TypeScript language patterns (optional) | | `devflow-react` | React framework patterns (optional) | @@ -84,9 +83,9 @@ devflow/ │ ├── targets/claude-code/ # Claude Code install target (installer, hooks.ts, post-install, claude-paths, legacy, templates/) │ └── assets/ # All installable assets (single source of truth) │ ├── skills/ # 41 skills -│ ├── agents/ # 17 agents (16 shared + 1 plugin-specific claude-md-auditor) +│ ├── agents/ # 16 agents │ ├── rules/ # 13 rules (flat .md files) -│ ├── commands/ # MDS command sources (14 hosts + 10 partials in _partials/; 2 static .md) +│ ├── commands/ # MDS command sources (14 hosts + 10 partials in _partials/; 1 static .md) │ └── scripts/hooks/ # Capture + memory + learning + ambient + proxy hooks (capture-prompt, capture-turn, capture-question, queue-append, memory-worker, background-memory-update [Stop-hook worker], learning-lock, session-start-memory, session-start-context, session-start-orchestrator, pre-compact-memory, preamble, ensure-proxy [SessionStart+UserPromptSubmit, registered/removed by addProxyHooks/removeProxyHooks], git-marker [sourced git-repo helper], get-mtime, hook-bootstrap, hook-log-init) │ └── assets/ # Static prose assets shipped with hooks (orchestrator-charter.md) ├── scripts/ # Dev tooling (build-mds.ts, bump-version.ts) @@ -231,12 +230,9 @@ Per-project runtime files live under `.devflow/`: - `/research` — Researcher agents + Skimmer + Synthesizer + Knowledge; multi-type research with trust-aware synthesis - `/release` — Git agent + Validator + Synthesizer; adaptive release with learned configuration - `/bug-analysis` — BugAnalyzer agents + Git + Synthesizer; proactive bug finding with static and semantic analysis, incremental by default -- `/audit-claude` — CLAUDE.md audit (optional plugin) **Shared agents** (16): git, synthesizer, skimmer, simplifier, coder, reviewer, triager, evaluator, tester, scrutinizer, validator, designer, knowledge, researcher, bug-analyzer, learning -**Plugin-specific agents** (1): claude-md-auditor - ## Key Conventions ### Skills diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 832ae8e2..a17836fa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,9 +25,9 @@ After setup, Devflow commands (`/code-review`, `/implement`, etc.) are available ``` devflow/ ├── src/assets/skills/ # 41 skills (single source of truth) -├── src/assets/agents/ # 17 agents (single source of truth) +├── src/assets/agents/ # 16 agents (single source of truth) ├── src/assets/rules/ # 13 rules (single source of truth, flat .md files) -├── src/assets/commands/ # Command sources (.mds + 2 static .md; partials in _partials/) +├── src/assets/commands/ # Command sources (.mds + 1 static .md; partials in _partials/) ├── src/assets/scripts/hooks/ # Working Memory + ambient hooks ├── src/core/ # Shared CLI logic (DEVFLOW_PLUGINS registry, paths, flags, …) ├── src/cli/ # TypeScript CLI command modules (init, list, uninstall, …) diff --git a/README.md b/README.md index 08f57884..eeacf1b3 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ you: add rate limiting to the /api/upload endpoint **Full lifecycle.** `/devflow:plan` takes a feature idea through codebase exploration, gap analysis, design review, and outputs a plan document ready for `/devflow:implement`. `/devflow:implement` accepts that plan document (or an issue or task description directly) and drives it through coding, validation, and refinement to a PR. `/devflow:debug` investigates bugs with competing hypotheses in parallel. `/devflow:self-review` runs Simplifier + Scrutinizer quality passes. -**Everything is composable.** 23 plugins (12 core + 10 language/ecosystem + 1 optional workflow recipes). Install only what you need. +**Everything is composable.** 22 plugins (12 core + 10 optional). Install only what you need. **HUD.** A persistent status line updates on every prompt — project, branch, diff stats, context usage, model, cost with weekly/monthly totals, quota reset timers, and configuration counts at a glance. diff --git a/docs/cli-reference.md b/docs/cli-reference.md index cb8f3596..f9a28820 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -60,7 +60,6 @@ npx devflow-kit init --plugin=implement,code-review # Install multiple | `devflow-bug-analysis` | Core | Proactive bug finding with static and semantic analysis | | `devflow-ambient` | Core | Ambient mode (orchestrator charter + plan handoff) | | `devflow-core-skills` | Core | Auto-activating quality skills | -| `devflow-audit-claude` | Optional | CLAUDE.md file audit | | `devflow-typescript` | Language | TypeScript patterns | | `devflow-react` | Language | React patterns | | `devflow-accessibility` | Language | Web accessibility patterns | diff --git a/docs/reference/agent-design.md b/docs/reference/agent-design.md index 7fd18fef..f57ac2e8 100644 --- a/docs/reference/agent-design.md +++ b/docs/reference/agent-design.md @@ -124,4 +124,4 @@ npx devflow-kit agents --reset --yes # Skip confirmation prom All agents live in `src/assets/agents/` — there is no separate per-plugin agent directory. For an agent used by only one plugin, add it to `src/assets/agents/` and declare it in only that plugin's `agents` array in DEVFLOW_PLUGINS. -**Note:** `src/assets/agents/` is the single source of truth for all agents (e.g., `git.md`, `coder.md`, `designer.md`, `claude-md-auditor.md`). No build step distributes agents — they install directly at `node dist/cli.js init` time. +**Note:** `src/assets/agents/` is the single source of truth for all agents (e.g., `git.md`, `coder.md`, `designer.md`). No build step distributes agents — they install directly at `node dist/cli.js init` time. diff --git a/docs/reference/file-organization.md b/docs/reference/file-organization.md index 592f8dfd..45a8965d 100644 --- a/docs/reference/file-organization.md +++ b/docs/reference/file-organization.md @@ -44,7 +44,7 @@ devflow/ │ │ │ └── references/ │ │ ├── software-design/ │ │ └── ... -│ ├── agents/ # 17 agents (16 shared + claude-md-auditor) +│ ├── agents/ # 16 agents │ │ ├── git.md │ │ ├── synthesizer.md │ │ ├── coder.md @@ -55,7 +55,7 @@ devflow/ │ │ └── ... │ ├── commands/ # Command sources │ │ ├── *.mds # 14 MDS host files (compiled to dist/commands/ by build:mds) -│ │ ├── *.md # 2 static command files +│ │ ├── *.md # 1 static command file │ │ └── _partials/ # 10 MDS partial files (no output-dir:, never compiled directly) │ └── scripts/hooks/ # Capture + memory + learning + ambient hooks │ ├── capture-prompt # UserPromptSubmit hook: appends user turn to memory + learning queues (independently gated) @@ -151,10 +151,9 @@ Assets live once in `src/assets/` and install directly to the user's `~/.claude/ 2. Add agent name to the plugin entry's `agents` array in DEVFLOW_PLUGINS 3. Run `node dist/cli.js init` to install -### Shared vs Plugin-Specific Agents +### Agents -- **Shared** (16): `git`, `synthesizer`, `skimmer`, `simplifier`, `coder`, `reviewer`, `triager`, `evaluator`, `tester`, `scrutinizer`, `validator`, `designer`, `knowledge`, `researcher`, `bug-analyzer`, `learning` -- **Plugin-specific** (1): `claude-md-auditor` — committed directly in `src/assets/agents/` +All 16 agents (`git`, `synthesizer`, `skimmer`, `simplifier`, `coder`, `reviewer`, `triager`, `evaluator`, `tester`, `scrutinizer`, `validator`, `designer`, `knowledge`, `researcher`, `bug-analyzer`, `learning`) are shared — committed directly in `src/assets/agents/`. ## Settings Override diff --git a/scripts/build-mds.ts b/scripts/build-mds.ts index 41f817a4..d943e72a 100644 --- a/scripts/build-mds.ts +++ b/scripts/build-mds.ts @@ -257,9 +257,8 @@ async function main(): Promise { process.exit(1); } - // Copy 2 hand-authored command files verbatim into dist/commands/ + // Copy 1 hand-authored command file verbatim into dist/commands/ const handAuthored = [ - path.join(ROOT, 'src', 'assets', 'commands', 'audit-claude.md'), path.join(ROOT, 'src', 'assets', 'commands', 'release.md'), ]; const commandsDest = path.join(ROOT, 'dist', 'commands'); diff --git a/src/assets/agents/claude-md-auditor.md b/src/assets/agents/claude-md-auditor.md deleted file mode 100644 index b133d8ac..00000000 --- a/src/assets/agents/claude-md-auditor.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -name: claude-md-auditor -description: Audits CLAUDE.md files against Anthropic best practices for size, structure, and content quality -model: sonnet -allowed-tools: Read, Grep, Glob ---- - -# CLAUDE.md Auditor - -You are a strict auditor that evaluates CLAUDE.md files against Anthropic's official guidance. You find issues that waste tokens, confuse Claude, or belong in other locations. You never soften findings. - -## Input Context - -- `file_path`: Absolute path to the CLAUDE.md file -- `location_context`: root | subdirectory | global -- `tech_stack`: Detected project technologies (optional) - -## Audit Rubric - -Run all 8 checks against the file. Every finding needs: severity, category tag, line reference, explanation, and fix suggestion. - -### 1. Size Limits [SIZE] - -| Context | Line Limit | Token Estimate | -|---------|-----------|----------------| -| Root CLAUDE.md | < 300 | ~5KB | -| Subdirectory CLAUDE.md | < 150 | ~2.5KB | -| Global ~/.claude/CLAUDE.md | < 200 | ~3.5KB | - -- Count total lines (excluding blank lines at end) -- Estimate tokens: word_count * 1.3 -- Flag sections exceeding 50 lines -- **Critical** if file exceeds limit. **Medium** if any section exceeds 50 lines. - -### 2. Required Sections [MISSING] - -For root CLAUDE.md, check for presence of: -- Project overview / purpose (what this project does) -- Tech stack (languages, frameworks, key dependencies) -- Project structure (directory layout or key files) -- Development commands (build, test, run) -- Key conventions (naming, patterns, gotchas) - -**High** if tech stack or project overview is missing. **Medium** for others. - -### 3. Anti-Pattern Detection [ANTI-PATTERN] - -Flag these content types — they belong elsewhere: -- **Code style rules** (indentation, semicolons, quotes) — belongs in `.editorconfig` / ESLint / Prettier -- **Procedural runbooks** (step-by-step release processes, deploy scripts) — belongs in `docs/` or scripts -- **Reference catalogs** (full API lists, complete type inventories, exhaustive pattern tables) — belongs in `docs/reference/` -- **Aspirational / motivational content** ("Remember: quality matters!") — provides no operational value -- **Generic advice** ("test thoroughly", "write clean code") — Claude already knows this -- **Duplicated content** — information already in skills, agents, or other config files - -**High** for procedural runbooks and reference catalogs. **Medium** for others. - -### 4. Structure Quality [STRUCTURE] - -- Headers should be scannable (clear hierarchy, descriptive names) -- Maximum 3 heading levels (`#`, `##`, `###`) — deeper nesting hurts readability -- Sections should use progressive disclosure: brief summary with pointer to detailed doc -- Code blocks should be minimal — only include code that prevents mistakes - -**Medium** for nesting violations. **Low** for style issues. - -### 5. Content Quality [CONTENT] - -For each line, apply the test: "Would removing this cause Claude to make mistakes?" -- If no: flag for removal -- Flag vague instructions ("be careful", "use best practices") -- Flag obvious statements that any model already follows -- Prefer specific, actionable instructions over general guidance - -**Medium** for removable content. **Low** for vagueness. - -### 6. Hierarchical Usage [HIERARCHY] - -- Root CLAUDE.md: project-wide guidance only -- Subdirectory CLAUDE.md: module-specific overrides only -- Global CLAUDE.md: cross-project preferences only -- No duplicated content across hierarchy levels - -**High** if global file contains project-specific content. **Medium** for duplication. - -### 7. Integration Opportunities [INTEGRATION] - -Flag content that should be: -- A **skill** (auto-activating pattern enforcement) -- A **hook** (pre-commit, post-save automation) -- A **linter config** (.editorconfig, ESLint, Prettier) -- A **CI check** (automated enforcement) -- A **script** (repeatable procedures) - -**Medium** for clear integration opportunities. - -### 8. Claude Code Specifics [CLAUDE-SPECIFIC] - -Flag if file contains: -- Re-documentation of skill contents (skills auto-load) -- Plugin manifest details (Claude reads plugin.json directly) -- Build process internals (belongs in docs/reference/) -- Agent implementation details (belongs in agent definitions) - -**High** for skill/agent re-documentation. **Medium** for build/manifest details. - -## Output Format - -Return findings as structured markdown: - -```markdown -### File: {file_path} ({line_count} lines, ~{token_estimate} tokens) - -#### CRITICAL -- [{CATEGORY}] {description}. **Fix**: {suggestion}. - -#### HIGH -- [{CATEGORY}] Lines {start}-{end}: {description}. **Fix**: {suggestion}. - -#### MEDIUM -- [{CATEGORY}] {description}. **Fix**: {suggestion}. - -#### LOW -- [{CATEGORY}] {description}. **Fix**: {suggestion}. -``` - -Omit severity sections that have no findings. Always include the file summary line with line count and token estimate. - -## Boundaries - -- **Read-only**: Never modify CLAUDE.md files -- **No false positives**: Only flag issues you're confident about -- **Severity accuracy**: Don't inflate severity to seem thorough -- **Escalate**: If file structure is so unusual you can't assess it, say so diff --git a/src/assets/commands/audit-claude.md b/src/assets/commands/audit-claude.md deleted file mode 100644 index c3080c19..00000000 --- a/src/assets/commands/audit-claude.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -description: Audit CLAUDE.md files against Anthropic best practices ---- - -# Command: /audit-claude - -## Description - -Audit CLAUDE.md files against Anthropic's best practices. Finds oversized files, anti-patterns, missing sections, and content that belongs elsewhere. Reports issues with severity levels and fix suggestions. - -## Usage - -`/audit-claude [path]` - -- No argument: audits all CLAUDE.md files in project + `~/.claude/CLAUDE.md` -- With path: audits the specific file - -## Implementation - -### Phase 1: Discovery - -**Produces:** CLAUDE_FILES - -Find all CLAUDE.md files to audit: - -```bash -# Project CLAUDE.md files -CLAUDE_FILES=$(find . -name "CLAUDE.md" -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null) - -# Global CLAUDE.md -GLOBAL_CLAUDE="$HOME/.claude/CLAUDE.md" -if [ -f "$GLOBAL_CLAUDE" ]; then - CLAUDE_FILES="$CLAUDE_FILES $GLOBAL_CLAUDE" -fi -``` - -If a specific path argument was provided, use only that file. - -### Phase 2: Analysis - -**Produces:** AUDIT_RESULTS -**Requires:** CLAUDE_FILES - -For each discovered file, spawn a `claude-md-auditor` agent: - -``` -Task( - subagent_type: "claude-md-auditor", - prompt: "Audit this CLAUDE.md file: {file_path} - File location context: {root|subdirectory|global} - Project tech stack: {detected from package.json, go.mod, etc.} - - Return your findings as structured markdown." -) -``` - -Run agents in parallel when multiple files are found. - -### Phase 3: Report - -**Requires:** AUDIT_RESULTS - -Combine all agent outputs into a single report: - -```markdown -## CLAUDE.md Audit Report - -### File: ./CLAUDE.md ({line_count} lines, ~{token_estimate} tokens) - -#### CRITICAL -- [{CATEGORY}] {description}. {fix suggestion}. - -#### HIGH -- [{CATEGORY}] Lines {start}-{end}: {description}. {fix suggestion}. - -#### MEDIUM -- [{CATEGORY}] {description}. {fix suggestion}. - -#### LOW -- [{CATEGORY}] {description}. {fix suggestion}. - -### Summary -- {N} Critical, {N} High, {N} Medium, {N} Low -- Estimated reducible lines: ~{N} (move to docs/reference/) -- Estimated token cost per session: ~{N} tokens -``` - -Present the report directly to the user. Do NOT write to `.devflow/docs/` — this is a diagnostic command. - -## Output Format - -Findings grouped by severity (Critical > High > Medium > Low), each with: -- **Category tag** in brackets: SIZE, MISSING, ANTI-PATTERN, STRUCTURE, CONTENT, HIERARCHY, INTEGRATION, CLAUDE-SPECIFIC -- **Line reference** when applicable -- **Explanation** of why it's an issue -- **Fix suggestion** with concrete action diff --git a/src/cli/commands/init-seed.ts b/src/cli/commands/init-seed.ts index 36ef09a5..357e1276 100644 --- a/src/cli/commands/init-seed.ts +++ b/src/cli/commands/init-seed.ts @@ -155,9 +155,8 @@ export function resolveSeedFlags( * - Otherwise → split + adopt newly-added non-optional selectable plugins * whose name is ∉ knownPlugins and ∉ manifestPlugins * - * Always-installed plugins (devflow-core-skills, devflow-ambient) and - * non-selectable optional plugins (devflow-audit-claude) are filtered out by - * partitionSelectablePlugins and never appear in the returned buckets. + * Always-installed plugins (devflow-core-skills, devflow-ambient) are filtered + * out by partitionSelectablePlugins and never appear in the returned buckets. */ export function resolveSeedPlugins( manifestPlugins: string[] | null, @@ -268,85 +267,6 @@ export function resolveResetGatedInputs( return { seedManifest: manifest, seedConfig: projectConfig, seedSettings: settingsJson }; } -/** - * Identify non-selectable optional plugins from the prior manifest that should be - * carried forward on a plugin-less full re-init. - * - * Non-selectable optional plugins (e.g. devflow-audit-claude) are excluded from - * the init prompt buckets by partitionSelectablePlugins and therefore never appear - * in the seed's workflowPlugins/languagePlugins. Without an explicit carry, a - * plugin-less full re-init would silently drop them — violating the - * "re-init preserves all existing state" acceptance criterion. - * - * Rules: - * - null manifestPlugins (fresh install OR --reset) → empty carry set - * - Otherwise: carry set = manifestPlugins ∩ optional ∩ not-in-selectable-buckets - * - Unknown/stale names (not in allPlugins) are excluded - * - * The caller is responsible for injecting the carry set into pluginsToInstall only - * on full (plugin-less) re-inits. Partial installs (--plugin flag) already merge - * via resolvePluginList. --reset produces null seedManifest → null manifestPlugins - * so the carry is empty by construction (factory reset drops them, as intended). - * - * Pure function — no I/O, no side effects. - */ -export function resolveNonSelectableOptionalCarry( - manifestPlugins: string[] | null, - allPlugins: PluginDefinition[], -): string[] { - if (manifestPlugins === null || manifestPlugins.length === 0) return []; - - const { workflow, language } = partitionSelectablePlugins(allPlugins); - const selectableNames = new Set([ - ...workflow.map(p => p.name), - ...language.map(p => p.name), - ]); - - // Build a name→plugin Map for O(1) lookup, replacing an O(n·m) find-in-filter. - const pluginMap = new Map(allPlugins.map(p => [p.name, p])); - - // Carry only OPTIONAL non-selectable plugins that exist in the registry. - // Non-optional always-installed plugins (core-skills, ambient) are excluded — - // they are guaranteed to be in pluginsToInstall by other mechanisms. - return manifestPlugins.filter(name => { - const plugin = pluginMap.get(name); - return plugin !== undefined && plugin.optional && !selectableNames.has(name); - }); -} - -/** - * Apply the non-selectable optional plugin carry on a full (plugin-less) re-init. - * - * Encapsulates the `!options.plugin` gate + resolveNonSelectableOptionalCarry call - * + dedup-merge loop from initAction, extracted as a pure function to make the - * carry wiring independently testable without spinning up the full init action. - * - * @param isPartialInstall - true when --plugin was passed (carry is skipped) - * @param manifestPlugins - Plugin list from seedManifest (null on fresh/--reset) - * @param pluginsToInstall - Current install list (not mutated; returns new array) - * @param allPlugins - Full plugin registry - * @returns Updated plugin list with carry plugins merged in (deduped, order preserved) - * - * Pure function — no I/O, no side effects. - */ -export function applyNonSelectableCarry( - isPartialInstall: boolean, - manifestPlugins: string[] | null, - pluginsToInstall: PluginDefinition[], - allPlugins: PluginDefinition[], -): PluginDefinition[] { - if (isPartialInstall) return pluginsToInstall; - const carryNames = resolveNonSelectableOptionalCarry(manifestPlugins, allPlugins); - const result = [...pluginsToInstall]; - for (const name of carryNames) { - const plugin = allPlugins.find(p => p.name === name); - if (plugin && !result.includes(plugin)) { - result.push(plugin); - } - } - return result; -} - /** * Apply CLI-explicit feature toggles on top of a seed's features. * diff --git a/src/cli/commands/init.ts b/src/cli/commands/init.ts index 407bd445..c35de7b0 100644 --- a/src/cli/commands/init.ts +++ b/src/cli/commands/init.ts @@ -46,7 +46,7 @@ import { applyFlags, stripFlags, applyViewMode, stripViewMode, FLAG_REGISTRY, Vi import { addContextHook, removeContextHook, hasContextHook } from './context.js'; import { writeFileAtomicExclusive } from '../../core/fs-atomic.js'; import { writeConfig, readConfigIfPresent, type FeatureConfig } from '../../core/feature-config.js'; -import { resolveInitSeed, applyCliToggles, resolveResetGatedInputs, applyNonSelectableCarry } from './init-seed.js'; +import { resolveInitSeed, applyCliToggles, resolveResetGatedInputs } from './init-seed.js'; import { getPendingTurnsPath, getPendingTurnsProcessingPath } from '../../core/project-paths.js'; import * as os from 'os'; @@ -1050,23 +1050,6 @@ export const initCommand = new Command('init') pluginsToInstall.push(ambientPlugin); } - // Carry non-selectable optional plugins (e.g. devflow-audit-claude) from the prior - // install on full re-inits. These plugins are excluded from the init prompt buckets - // by partitionSelectablePlugins, so they never appear in selectedPlugins and would - // otherwise be silently dropped on any plugin-less re-init. - // - // --reset: seedManifest is null (resolveResetGatedInputs zeros it) → carry is empty. - // Factory reset correctly drops non-selectable optional plugins. - // --plugin X partial install: resolvePluginList already merges the full manifest list - // at the manifest-write step; physical dirs are preserved (no full wipe on partial). - // Skip carry here to avoid force-reinstalling plugins the user didn't target. - pluginsToInstall = applyNonSelectableCarry( - !!options.plugin, - seedManifest?.plugins ?? null, - pluginsToInstall, - DEVFLOW_PLUGINS, - ); - // Skills: install ALL from ALL plugins (skills are tiny markdown files; // commands need skills from other plugins to function) const skillsMap = buildFullSkillsMap(); diff --git a/src/core/manifest.ts b/src/core/manifest.ts index c818e6cc..98646637 100644 --- a/src/core/manifest.ts +++ b/src/core/manifest.ts @@ -1,6 +1,6 @@ import { promises as fs } from 'fs'; import * as path from 'path'; -import { LEGACY_PLUGIN_NAMES } from './plugins.js'; +import { LEGACY_PLUGIN_NAMES, DELETED_PLUGIN_NAMES } from './plugins.js'; import { VIEW_MODES, ViewMode } from './flags.js'; /** @@ -228,7 +228,10 @@ export function resolvePluginList( isPartialInstall: boolean, ): string[] { if (existingManifest && isPartialInstall) { - const cleaned = existingManifest.plugins.map(p => LEGACY_PLUGIN_NAMES[p] ?? p); + const deletedSet = new Set(DELETED_PLUGIN_NAMES); + const cleaned = existingManifest.plugins + .filter(p => !deletedSet.has(p)) + .map(p => LEGACY_PLUGIN_NAMES[p] ?? p); return mergeManifestPlugins(cleaned, installedPluginNames); } return installedPluginNames; diff --git a/src/core/plugins.ts b/src/core/plugins.ts index 5cc838cc..76a1df53 100644 --- a/src/core/plugins.ts +++ b/src/core/plugins.ts @@ -181,15 +181,6 @@ export const DEVFLOW_PLUGINS: PluginDefinition[] = [ ], rules: [], }, - { - name: 'devflow-audit-claude', - description: 'Audit CLAUDE.md files against Anthropic best practices', - commands: ['/audit-claude'], - agents: ['claude-md-auditor'], - skills: [], - optional: true, - rules: [], - }, { name: 'devflow-dynamic', description: 'Dynamic workflow recipes - dependency-aware tickets→plan→build delivery pipeline', @@ -292,6 +283,16 @@ export const LEGACY_PLUGIN_NAMES: Record = { 'devflow-specify': 'devflow-plan', }; +/** + * Plugin names that have been deleted from the registry. + * Used during init to prune stale entries from users' manifests on partial + * reinstalls — the full-reinstall path writes installedPluginNames directly + * and cannot carry a deleted name. Entries can be removed after 2 major versions. + */ +export const DELETED_PLUGIN_NAMES: string[] = [ + 'devflow-audit-claude', +]; + /** * Parse a comma-separated plugin selection string into normalized plugin names. * Validates against known plugins; returns invalid names as errors. @@ -462,17 +463,29 @@ export const LEGACY_RULE_NAMES: string[] = []; /** * Canonical display order for workflow commands shown at end of init. * Mirrors the user-facing pipeline: research → explore → plan → implement → - * code-review → resolve → self-review → bug-analysis → debug → release → audit-claude → + * code-review → resolve → self-review → bug-analysis → debug → release → * dynamic pipeline (dynamic-tickets → dynamic-plan → dynamic-build → dynamic-wave → dynamic-profile). * Export so init.ts can import it rather than keeping a local copy. */ export const WORKFLOW_ORDER: string[] = [ '/research', '/explore', '/plan', '/implement', '/code-review', '/resolve', '/self-review', '/bug-analysis', - '/debug', '/release', '/audit-claude', + '/debug', '/release', '/dynamic-tickets', '/dynamic-plan', '/dynamic-build', '/dynamic-wave', '/dynamic-profile', ]; +/** + * Plugin names excluded from the init multiselect buckets. + * These are always installed regardless of user selection: + * - devflow-core-skills (always installed, non-optional) + * - devflow-ambient (always installed, non-optional) + * + * Invariant: EXCLUDED ∩ optional === ∅ — no optional plugin may be excluded from + * the init UI without a re-init carry mechanism to preserve it across full reinstalls. + * Guarded by the structural invariant test in tests/plugins.test.ts. + */ +export const EXCLUDED: ReadonlySet = new Set(['devflow-core-skills', 'devflow-ambient']); + /** * Partition the selectable plugins into workflow (command-bearing) and language * (command-less, optional language/ecosystem) buckets for the two-step init UI. @@ -480,7 +493,6 @@ export const WORKFLOW_ORDER: string[] = [ * Excluded from both buckets (not selectable at init): * - devflow-core-skills (always installed) * - devflow-ambient (always installed) - * - devflow-audit-claude (installable via --plugin only) * * Pure function — does not mutate the input array; preserves DEVFLOW_PLUGINS * ordering within each bucket; deterministic; no I/O. @@ -489,7 +501,6 @@ export function partitionSelectablePlugins(plugins: PluginDefinition[]): { workflow: PluginDefinition[]; language: PluginDefinition[]; } { - const EXCLUDED = new Set(['devflow-core-skills', 'devflow-ambient', 'devflow-audit-claude']); const workflow: PluginDefinition[] = []; const language: PluginDefinition[] = []; diff --git a/tests/agent-frontmatter.test.ts b/tests/agent-frontmatter.test.ts index f1ed3fdc..4cfb8441 100644 --- a/tests/agent-frontmatter.test.ts +++ b/tests/agent-frontmatter.test.ts @@ -5,7 +5,7 @@ * Protocol: RED → GREEN → REFACTOR. * * Coverage: - * - All 17 real shipped agent files (verbatim round-trips) + * - All 16 real shipped agent files (verbatim round-trips) * - Synthetic edge cases: CRLF, missing frontmatter, unterminated frontmatter, * model: in body, duplicate model lines, effort add/replace/remove */ @@ -32,10 +32,9 @@ async function readAgent(name: string): Promise { // Real agent files — verbatim round-trips // --------------------------------------------------------------------------- -describe('rewriteAgentFrontmatter — all 17 real agent files', () => { +describe('rewriteAgentFrontmatter — all 16 real agent files', () => { const AGENTS = [ 'bug-analyzer.md', - 'claude-md-auditor.md', 'coder.md', 'designer.md', 'evaluator.md', diff --git a/tests/build-mds.test.ts b/tests/build-mds.test.ts index d2773778..1f541a1d 100644 --- a/tests/build-mds.test.ts +++ b/tests/build-mds.test.ts @@ -406,14 +406,14 @@ describe('expected-command-set guard (C2)', () => { } }); - it('dist/commands/ contains exactly 16 .md files (14 compiled + 2 hand-authored)', async () => { - // The 2 hand-authored files are audit-claude.md and release.md, copied verbatim by build-mds.ts. + it('dist/commands/ contains exactly 15 .md files (14 compiled + 1 hand-authored)', async () => { + // The 1 hand-authored file is release.md, copied verbatim by build-mds.ts. const files = await fs.readdir(path.join(ROOT, 'dist', 'commands')); const mdFiles = files.filter(f => f.endsWith('.md')); expect( mdFiles.length, - `Expected 16 .md files in dist/commands/ (14 compiled + 2 hand-authored), got ${mdFiles.length}: ${mdFiles.sort().join(', ')}`, - ).toBe(16); + `Expected 15 .md files in dist/commands/ (14 compiled + 1 hand-authored), got ${mdFiles.length}: ${mdFiles.sort().join(', ')}`, + ).toBe(15); }); }); diff --git a/tests/init-nonselectable-carry.test.ts b/tests/init-nonselectable-carry.test.ts deleted file mode 100644 index 1040b5e2..00000000 --- a/tests/init-nonselectable-carry.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { applyNonSelectableCarry } from '../src/cli/commands/init-seed.js'; -import { DEVFLOW_PLUGINS } from '../src/core/plugins.js'; -import { type PluginDefinition } from '../src/core/plugins.js'; - -// ── Fixtures ────────────────────────────────────────────────────────────────── - -/** Minimal plugin stubs for wiring tests that don't need the full registry. */ -function makePlugin(name: string, optional = false): PluginDefinition { - return { name, description: '', commands: [], agents: [], skills: [], rules: [], optional }; -} - -// Grab the real audit-claude entry from the registry (non-selectable optional plugin). -const auditClaude = DEVFLOW_PLUGINS.find(p => p.name === 'devflow-audit-claude')!; -const implement = DEVFLOW_PLUGINS.find(p => p.name === 'devflow-implement')!; - -// ── applyNonSelectableCarry wiring ──────────────────────────────────────────── - -describe('applyNonSelectableCarry wiring', () => { - // ── Gate: partial install (--plugin) skips carry ────────────────────────── - - it('isPartialInstall=true → carry gate skips, returns same list unchanged', () => { - const list = [implement]; - const result = applyNonSelectableCarry( - true, - ['devflow-implement', 'devflow-audit-claude'], - list, - DEVFLOW_PLUGINS, - ); - // Reference equality: same array instance returned (no carry applied) - expect(result).toBe(list); - }); - - it('isPartialInstall=true → audit-claude NOT added even when in manifest', () => { - const result = applyNonSelectableCarry( - true, - ['devflow-implement', 'devflow-audit-claude'], - [implement], - DEVFLOW_PLUGINS, - ); - expect(result.map(p => p.name)).not.toContain('devflow-audit-claude'); - }); - - // ── Full re-init with null manifest (fresh install / --reset) ───────────── - - it('isPartialInstall=false, manifestPlugins=null → carry empty, list unchanged', () => { - const list = [implement]; - const result = applyNonSelectableCarry(false, null, list, DEVFLOW_PLUGINS); - expect(result).toEqual(list); - expect(result.map(p => p.name)).not.toContain('devflow-audit-claude'); - }); - - // ── Full re-init: audit-claude in prior manifest → carried ──────────────── - - it('isPartialInstall=false, audit-claude in manifest → appended to install list', () => { - const result = applyNonSelectableCarry( - false, - ['devflow-implement', 'devflow-audit-claude'], - [implement], - DEVFLOW_PLUGINS, - ); - expect(result.map(p => p.name)).toContain('devflow-audit-claude'); - // Original entries preserved - expect(result.map(p => p.name)).toContain('devflow-implement'); - }); - - // ── Deduplication: already-present plugin not added twice ──────────────── - - it('isPartialInstall=false, audit-claude already in install list → not duplicated', () => { - const result = applyNonSelectableCarry( - false, - ['devflow-implement', 'devflow-audit-claude'], - [implement, auditClaude], - DEVFLOW_PLUGINS, - ); - const names = result.map(p => p.name); - const auditCount = names.filter(n => n === 'devflow-audit-claude').length; - expect(auditCount).toBe(1); - }); - - // ── Unknown name in manifest → safely excluded ─────────────────────────── - - it('isPartialInstall=false, stale/unknown plugin name in manifest → excluded', () => { - const result = applyNonSelectableCarry( - false, - ['devflow-implement', 'devflow-obsolete-2024'], - [implement], - DEVFLOW_PLUGINS, - ); - expect(result.map(p => p.name)).not.toContain('devflow-obsolete-2024'); - }); - - // ── Input list not mutated (immutable-return contract) ─────────────────── - - it('does not mutate the pluginsToInstall argument', () => { - const list = [implement]; - const before = [...list]; - applyNonSelectableCarry( - false, - ['devflow-implement', 'devflow-audit-claude'], - list, - DEVFLOW_PLUGINS, - ); - expect(list).toEqual(before); - }); - - // ── Isolated stub-registry: verifies gate + merge loop independently ────── - - it('isolated registry: carries optional non-selectable plugin, skips selectable optional', () => { - // Build a minimal registry: one selectable optional, one non-selectable optional. - // partitionSelectablePlugins filters by presence of commands — non-selectable - // optional plugins (like audit-claude) have no entry in the selectable buckets. - // Use the real DEVFLOW_PLUGINS but limit manifestPlugins to known names. - const result = applyNonSelectableCarry( - false, - // include a selectable optional (e.g. devflow-typescript) + audit-claude - ['devflow-typescript', 'devflow-audit-claude', 'devflow-implement'], - [implement], - DEVFLOW_PLUGINS, - ); - const names = result.map(p => p.name); - // audit-claude is non-selectable optional → carried - expect(names).toContain('devflow-audit-claude'); - // devflow-typescript is selectable optional → NOT carried (excluded by carry helper) - expect(names).not.toContain('devflow-typescript'); - }); -}); diff --git a/tests/init-seed.test.ts b/tests/init-seed.test.ts index 147d3086..ea84ac08 100644 --- a/tests/init-seed.test.ts +++ b/tests/init-seed.test.ts @@ -6,7 +6,6 @@ import { resolveInitSeed, applyCliToggles, resolveResetGatedInputs, - resolveNonSelectableOptionalCarry, FEATURE_DEFAULTS, type FeatureSeed, } from '../src/cli/commands/init-seed.js'; @@ -216,12 +215,11 @@ describe('resolveSeedPlugins', () => { expect(workflowPlugins).toContain('devflow-implement'); }); - it('fresh never includes excluded always-installed plugins (core-skills, ambient, audit-claude)', () => { + it('fresh never includes excluded always-installed plugins (core-skills, ambient)', () => { const { workflowPlugins, languagePlugins } = resolveSeedPlugins(null, undefined, DEVFLOW_PLUGINS); const all = [...workflowPlugins, ...languagePlugins]; expect(all).not.toContain('devflow-core-skills'); expect(all).not.toContain('devflow-ambient'); - expect(all).not.toContain('devflow-audit-claude'); }); it('knownPlugins === undefined → split existing into buckets, adopt nothing', () => { @@ -508,70 +506,6 @@ describe('resolveResetGatedInputs', () => { }); }); -// ── resolveNonSelectableOptionalCarry ───────────────────────────────────────── - -describe('resolveNonSelectableOptionalCarry', () => { - it('null manifestPlugins (fresh install) → empty carry', () => { - const carry = resolveNonSelectableOptionalCarry(null, DEVFLOW_PLUGINS); - expect(carry).toEqual([]); - }); - - it('null manifestPlugins (--reset simulation via resolveResetGatedInputs) → empty carry', () => { - // --reset sets seedManifest to null; callers pass seedManifest?.plugins ?? null - const carry = resolveNonSelectableOptionalCarry(null, DEVFLOW_PLUGINS); - expect(carry).toEqual([]); - }); - - it('devflow-audit-claude in manifest → carried (non-selectable optional plugin preserved)', () => { - const manifestPlugins = ['devflow-implement', 'devflow-code-review', 'devflow-audit-claude']; - const carry = resolveNonSelectableOptionalCarry(manifestPlugins, DEVFLOW_PLUGINS); - expect(carry).toContain('devflow-audit-claude'); - }); - - it('only selectable plugins in manifest → empty carry', () => { - const manifestPlugins = ['devflow-implement', 'devflow-code-review', 'devflow-typescript']; - const carry = resolveNonSelectableOptionalCarry(manifestPlugins, DEVFLOW_PLUGINS); - expect(carry).toEqual([]); - }); - - it('devflow-core-skills (non-optional always-installed) → not in carry', () => { - // core-skills is excluded from selection buckets but it is NOT optional — should not be carried - const manifestPlugins = ['devflow-core-skills', 'devflow-implement']; - const carry = resolveNonSelectableOptionalCarry(manifestPlugins, DEVFLOW_PLUGINS); - expect(carry).not.toContain('devflow-core-skills'); - }); - - it('devflow-ambient (non-optional always-installed) → not in carry', () => { - const manifestPlugins = ['devflow-ambient', 'devflow-implement']; - const carry = resolveNonSelectableOptionalCarry(manifestPlugins, DEVFLOW_PLUGINS); - expect(carry).not.toContain('devflow-ambient'); - }); - - it('carry contains only devflow-audit-claude when both selectable and non-selectable plugins present', () => { - const manifestPlugins = [ - 'devflow-implement', - 'devflow-code-review', - 'devflow-typescript', - 'devflow-core-skills', - 'devflow-ambient', - 'devflow-audit-claude', - ]; - const carry = resolveNonSelectableOptionalCarry(manifestPlugins, DEVFLOW_PLUGINS); - expect(carry).toEqual(['devflow-audit-claude']); - }); - - it('unknown/stale plugin name in manifest → excluded from carry', () => { - const manifestPlugins = ['devflow-implement', 'devflow-obsolete-plugin']; - const carry = resolveNonSelectableOptionalCarry(manifestPlugins, DEVFLOW_PLUGINS); - expect(carry).not.toContain('devflow-obsolete-plugin'); - }); - - it('empty manifestPlugins array → empty carry', () => { - const carry = resolveNonSelectableOptionalCarry([], DEVFLOW_PLUGINS); - expect(carry).toEqual([]); - }); -}); - // ── proxy seeding (resolveSeedFeatures + applyCliToggles) ───────────────────── describe('proxy seeding', () => { diff --git a/tests/packaging.test.ts b/tests/packaging.test.ts index 3464f6f1..e1d15680 100644 --- a/tests/packaging.test.ts +++ b/tests/packaging.test.ts @@ -278,7 +278,7 @@ describe('Guard 5 (files[] coverage): package.json includes required directories * AC-C3: The published tarball must: * (a) Contain no plugins/ or shared/ source-tree paths — these directories * only exist in the git repo and must never be published. - * (b) Contain exactly 16 dist/commands/*.md files — one per registered command. + * (b) Contain exactly 15 dist/commands/*.md files — one per registered command. * If the count changes, this guard forces an intentional update. * * Per PF-008: assert on parsed `npm pack --dry-run --json` output (structured @@ -320,15 +320,15 @@ describe('Guard 6 (tarball contents): npm pack --dry-run output excludes source ).toHaveLength(0); }); - it('tarball contains exactly 16 dist/commands/*.md files (AC-C3)', () => { + it('tarball contains exactly 15 dist/commands/*.md files (AC-C3)', () => { const files = getPackFiles(); if (files.length === 0) return; // pre-build: skip const commandMds = files.filter(f => /^dist\/commands\/[^/]+\.md$/.test(f)); expect( commandMds, - `Expected 16 dist/commands/*.md files in tarball, got ${commandMds.length}.\n` + + `Expected 15 dist/commands/*.md files in tarball, got ${commandMds.length}.\n` + `Files found: ${commandMds.join(', ')}\n` + `If a command was added or removed, update this count intentionally.`, - ).toHaveLength(16); + ).toHaveLength(15); }); }); diff --git a/tests/plugins.test.ts b/tests/plugins.test.ts index 92c7c12a..67239ee7 100644 --- a/tests/plugins.test.ts +++ b/tests/plugins.test.ts @@ -8,6 +8,9 @@ import { partitionSelectablePlugins, prefixSkillName, WORKFLOW_ORDER, + EXCLUDED, + DELETED_PLUGIN_NAMES, + LEGACY_PLUGIN_NAMES, type PluginDefinition, } from '../src/core/plugins.js'; import { @@ -177,8 +180,8 @@ describe('optional plugin flag', () => { } }); - it('non-language plugins do not have optional: true (except audit-claude, dynamic, compliance)', () => { - const allowedOptional = new Set([...languagePluginNames, 'devflow-audit-claude', 'devflow-dynamic', 'devflow-compliance']); + it('non-language plugins do not have optional: true (except dynamic, compliance)', () => { + const allowedOptional = new Set([...languagePluginNames, 'devflow-dynamic', 'devflow-compliance']); for (const plugin of DEVFLOW_PLUGINS) { if (!allowedOptional.has(plugin.name)) { expect(plugin.optional, `${plugin.name} should not be optional`).toBeFalsy(); @@ -193,15 +196,6 @@ describe('optional plugin flag', () => { } }); - it('audit-claude is excluded from init multiselect choices', () => { - // partitionSelectablePlugins excludes devflow-audit-claude from both buckets - const { workflow, language } = partitionSelectablePlugins(DEVFLOW_PLUGINS); - const selectableNames = [...workflow, ...language].map(pl => pl.name); - expect(selectableNames).not.toContain('devflow-audit-claude'); - // But it still exists in the registry (installable via --plugin=audit-claude) - expect(DEVFLOW_PLUGINS.find(p => p.name === 'devflow-audit-claude')).toBeDefined(); - }); - it('devflow-ambient declares review/resolve skill dependencies', () => { const ambient = DEVFLOW_PLUGINS.find(p => p.name === 'devflow-ambient'); expect(ambient).toBeDefined(); @@ -287,6 +281,37 @@ describe('optional plugin flag', () => { }); }); +describe('DELETED_PLUGIN_NAMES consistency', () => { + // DELETED_PLUGIN_NAMES is a PRUNING manifest, not an install-naming list: + // resolvePluginList drops every listed name from the user's manifest.plugins on + // partial reinstall. A LIVE plugin name listed here is therefore silently erased + // from the user's recorded selection, and resolveSeedPlugins then omits it from + // the next re-init seed — a plugin the user chose disappears with no error. + // Same misread class as PF-012 (deletion manifest that reads like a naming list); + // guarded here because the state-aware-init contract forbids it. applies ADR-014 + it('contains no name that is still a live plugin in the registry', () => { + const liveNames = new Set(DEVFLOW_PLUGINS.map(p => p.name)); + expect(liveNames.size, 'registry must be non-empty or this guard is vacuous').toBeGreaterThan(0); + + const stillLive = DELETED_PLUGIN_NAMES.filter(name => liveNames.has(name)); + expect( + stillLive, + 'DELETED_PLUGIN_NAMES lists a plugin that still exists in DEVFLOW_PLUGINS — ' + + 'it would be pruned from every user manifest on partial reinstall and dropped from the re-init seed', + ).toEqual([]); + }); + + it('does not overlap LEGACY_PLUGIN_NAMES keys (deletion would pre-empt the rename)', () => { + // resolvePluginList filters deleted names BEFORE applying the rename map, so a + // name in both lists is dropped rather than migrated to its new name. + const overlap = DELETED_PLUGIN_NAMES.filter(name => name in LEGACY_PLUGIN_NAMES); + expect( + overlap, + 'a name cannot be both renamed and deleted — the delete filter runs first and the rename would never apply', + ).toEqual([]); + }); +}); + describe('LEGACY_AGENT_NAMES consistency', () => { it('no legacy agent name appears in any current plugin agents array', () => { const currentAgents = getAllAgentNames(); @@ -334,7 +359,24 @@ describe('LEGACY_SKILL_NAMES consistency', () => { }); describe('partitionSelectablePlugins', () => { - const EXCLUDED = new Set(['devflow-core-skills', 'devflow-ambient', 'devflow-audit-claude']); + // Independent oracle. Every other test in this describe derives its expectation + // from the imported EXCLUDED, so they move with production and cannot detect a + // change to the excluded set itself (dropping devflow-ambient leaves them green + // while exposing an always-installed plugin as an uncheckable-by-accident entry + // in the init language multiselect). This literal is the only assertion that + // pins WHICH plugins are excluded, so a deliberate change must land here. avoids PF-018 + it('EXCLUDED pins exactly the always-installed plugins (independent oracle)', () => { + expect([...EXCLUDED].sort()).toEqual(['devflow-ambient', 'devflow-core-skills']); + }); + + it('EXCLUDED ∩ optional === ∅ — no optional plugin is non-selectable (structural invariant)', () => { + // This invariant ensures that every optional plugin is reachable via the init UI. + // Adding an optional plugin to EXCLUDED would silently drop it on full re-inits + // without a carry mechanism. Guards the structural requirement. + const optionalNames = new Set(DEVFLOW_PLUGINS.filter(p => p.optional).map(p => p.name)); + const intersection = [...EXCLUDED].filter(name => optionalNames.has(name)); + expect(intersection, 'EXCLUDED contains an optional plugin — add a re-init carry mechanism to preserve it across full reinstalls').toEqual([]); + }); it('command-bearing plugins land in workflow bucket', () => { const { workflow } = partitionSelectablePlugins(DEVFLOW_PLUGINS); @@ -439,8 +481,7 @@ describe('WORKFLOW_ORDER', () => { }); it('every WORKFLOW_ORDER entry corresponds to a real command in the registry (reverse regression guard)', () => { - // Build the full set of all commands across ALL plugins (including excluded ones like - // devflow-audit-claude, which owns /audit-claude and is intentionally in WORKFLOW_ORDER). + // Build the full set of all commands across ALL plugins. const allCommands = new Set(DEVFLOW_PLUGINS.flatMap(pl => pl.commands)); for (const cmd of WORKFLOW_ORDER) { expect( diff --git a/tests/registry-integrity.test.ts b/tests/registry-integrity.test.ts index b259dc93..aa354325 100644 --- a/tests/registry-integrity.test.ts +++ b/tests/registry-integrity.test.ts @@ -265,8 +265,8 @@ describe('Guard 5 (build-gated): spawned agents ↔ plugin agent declarations', /** * Normalize an agent name for comparison across naming conventions: - * - Registry uses filenames: bug-analyzer, claude-md-auditor - * - subagent_type uses frontmatter name field: BugAnalyzer, claude-md-auditor + * - Registry uses filenames: bug-analyzer, coder + * - subagent_type uses frontmatter name field: BugAnalyzer, Coder * Strip hyphens and lowercase so both representations collapse to the same key. */ const normalize = (name: string) => name.replace(/-/g, '').toLowerCase(); diff --git a/tests/skill-references.test.ts b/tests/skill-references.test.ts index ae8fd6e5..5c450f18 100644 --- a/tests/skill-references.test.ts +++ b/tests/skill-references.test.ts @@ -135,7 +135,6 @@ const COMMAND_REFS = new Set([ 'debug', 'implement', 'self-review', - 'audit-claude', 'plan', 'review', 'pipeline', @@ -184,16 +183,11 @@ describe('Format 2: Agent frontmatter skills', () => { }); it('every shared agent declares at least one skill in frontmatter', () => { - // Agents from plugins with skills: [] legitimately have no skills to declare. - // These are simple, fully self-contained agents whose logic is inline. - const AGENTS_WITHOUT_SKILLS = new Set(['claude-md-auditor']); - const agentFiles = readdirSync(path.join(ROOT, 'src', 'assets', 'agents')).filter(f => f.endsWith('.md')); - for (const file of agentFiles) { - const name = file.replace(/\.md$/, ''); - if (AGENTS_WITHOUT_SKILLS.has(name)) continue; + expect(agentFiles.length, 'agent corpus must be non-empty — a rename emptying the directory would pass vacuously').toBeGreaterThan(0); + for (const file of agentFiles) { const filePath = path.join(ROOT, 'src', 'assets', 'agents', file); const content = readFileSync(filePath, 'utf-8'); const skillNames = parseFrontmatterSkills(content);