From 53380dcb6b5a249b6191777aaf82aaf608762495 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 13 Mar 2026 00:42:53 +0200 Subject: [PATCH 01/12] feat: add search-first skill for research-before-building (#111) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New skill enforcing a 4-phase research loop (Need Analysis → Search → Evaluate → Decide) before writing custom utility code. Delegates research to Explore subagent to keep main session context clean. Four decision outcomes: Adopt, Extend, Compose, Build. Added to core-skills plugin and ambient-router BUILD primary skills. --- .../.claude-plugin/plugin.json | 1 + shared/skills/ambient-router/SKILL.md | 2 +- .../references/skill-catalog.md | 1 + shared/skills/search-first/SKILL.md | 133 ++++++++++++++++++ .../references/evaluation-criteria.md | 101 +++++++++++++ src/cli/plugins.ts | 2 +- 6 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 shared/skills/search-first/SKILL.md create mode 100644 shared/skills/search-first/references/evaluation-criteria.md diff --git a/plugins/devflow-core-skills/.claude-plugin/plugin.json b/plugins/devflow-core-skills/.claude-plugin/plugin.json index 52dc45b..dbfaae9 100644 --- a/plugins/devflow-core-skills/.claude-plugin/plugin.json +++ b/plugins/devflow-core-skills/.claude-plugin/plugin.json @@ -24,6 +24,7 @@ "git-workflow", "github-patterns", "input-validation", + "search-first", "test-driven-development", "test-patterns" ] diff --git a/shared/skills/ambient-router/SKILL.md b/shared/skills/ambient-router/SKILL.md index ef1812f..ecae954 100644 --- a/shared/skills/ambient-router/SKILL.md +++ b/shared/skills/ambient-router/SKILL.md @@ -54,7 +54,7 @@ Based on classified intent, read the following skills to inform your response. | Intent | Primary Skills | Secondary (if file type matches) | |--------|---------------|----------------------------------| -| **BUILD** | test-driven-development, implementation-patterns | typescript (.ts), react (.tsx/.jsx), go (.go), java (.java), python (.py), rust (.rs), frontend-design (CSS/UI), input-validation (forms/API), security-patterns (auth/crypto) | +| **BUILD** | test-driven-development, implementation-patterns, search-first | typescript (.ts), react (.tsx/.jsx), go (.go), java (.java), python (.py), rust (.rs), frontend-design (CSS/UI), input-validation (forms/API), security-patterns (auth/crypto) | | **DEBUG** | test-patterns, core-patterns | git-safety (if git operations involved) | | **REVIEW** | self-review, core-patterns | test-patterns | | **PLAN** | implementation-patterns | core-patterns | diff --git a/shared/skills/ambient-router/references/skill-catalog.md b/shared/skills/ambient-router/references/skill-catalog.md index 3b6fcde..652d1bd 100644 --- a/shared/skills/ambient-router/references/skill-catalog.md +++ b/shared/skills/ambient-router/references/skill-catalog.md @@ -12,6 +12,7 @@ These skills may be loaded during GUIDED-depth ambient routing. |-------|-------------|---------------| | test-driven-development | Always for BUILD | `*.ts`, `*.tsx`, `*.js`, `*.jsx`, `*.py` | | implementation-patterns | Always for BUILD | Any code file | +| search-first | Always for BUILD | Any code file | | typescript | TypeScript files in scope | `*.ts`, `*.tsx` | | react | React components in scope | `*.tsx`, `*.jsx` | | frontend-design | UI/styling work | `*.css`, `*.scss`, `*.tsx` with styling keywords | diff --git a/shared/skills/search-first/SKILL.md b/shared/skills/search-first/SKILL.md new file mode 100644 index 0000000..5069404 --- /dev/null +++ b/shared/skills/search-first/SKILL.md @@ -0,0 +1,133 @@ +--- +name: search-first +description: >- + This skill should be used when the user asks to "add a utility", "create a helper", + "implement parsing", "build a wrapper", or writes infrastructure/utility code that + may already exist as a well-maintained package. Enforces research before building. +user-invocable: false +allowed-tools: Read, Grep, Glob +--- + +# Search-First + +Research before building. Check if a battle-tested solution exists before writing custom utility code. + +## Iron Law + +> **RESEARCH BEFORE BUILDING** +> +> Never write custom utility code without first checking if a battle-tested solution +> exists. The best code is code you don't write. A maintained package with thousands +> of users will always beat a hand-rolled utility in reliability, edge cases, and +> long-term maintenance. + +## When This Skill Activates + +**Triggers** — creating or modifying code that: +- Parses, formats, or validates data (dates, URLs, emails, UUIDs, etc.) +- Implements common algorithms (sorting, diffing, hashing, encoding) +- Wraps HTTP clients, retries, rate limiting, caching +- Handles file system operations beyond basic read/write +- Implements CLI argument parsing, logging, or configuration +- Creates test utilities (mocking, fixtures, assertions) + +**Does NOT trigger** for: +- Domain-specific business logic unique to this project +- Glue code connecting existing components +- Trivial operations (< 5 lines, single-use) +- Code that intentionally avoids external dependencies (e.g., zero-dep libraries) + +--- + +## Research Process + +### Phase 1: Need Analysis + +Before searching, define what you actually need: + +``` +Need: {one-sentence description of the capability} +Constraints: {runtime, bundle size, license, zero-dep requirement} +Must-haves: {non-negotiable requirements} +Nice-to-haves: {optional features} +``` + +### Phase 2: Search + +Delegate research to an Explore subagent to keep main session context clean. + +**Spawn an Explore agent** with this prompt template: + +``` +Task(subagent_type="Explore"): +"Research existing solutions for: {need description} + +Search for: +1. npm/PyPI/crates packages that solve this (check package.json/requirements.txt for ecosystem) +2. Existing utilities in this codebase (grep for related function names) +3. Framework built-ins that already handle this + +For each candidate, find: +- Package name and weekly downloads (if applicable) +- Last publish date and maintenance status +- Bundle size / dependency count +- API surface relevant to our need +- License compatibility + +Return top 3 candidates with pros/cons, or confirm nothing suitable exists." +``` + +### Phase 3: Evaluate + +Score each candidate against evaluation criteria. See `references/evaluation-criteria.md` for the full matrix. + +Quick checklist: +- [ ] Last published within 12 months +- [ ] Weekly downloads > 1,000 (npm) or equivalent traction +- [ ] No known vulnerabilities (check Snyk/npm audit) +- [ ] API fits the use case without heavy wrapping +- [ ] License compatible with project (MIT/Apache/BSD preferred) +- [ ] Bundle size acceptable for the project context + +### Phase 4: Decide + +Choose one of four outcomes: + +| Decision | When | Action | +|----------|------|--------| +| **Adopt** | Exact match, well-maintained, good API | Install and use directly | +| **Extend** | Partial match, needs thin wrapper | Install + write minimal adapter | +| **Compose** | No single package fits, but 2-3 small ones combine well | Install multiple, write glue code | +| **Build** | Nothing fits, or dependency cost exceeds value | Write custom, document why | + +**Document the decision** in a code comment at the usage site: + +```typescript +// search-first: Adopted date-fns for date formatting (2M weekly downloads, 30KB) +// search-first: Built custom — no package handles our specific wire format +``` + +--- + +## Anti-Patterns + +| Anti-Pattern | Correct Approach | +|-------------|-----------------| +| Adding a dependency for 5 lines of trivial code | Build — dependency overhead exceeds value | +| Choosing the most popular package without checking fit | Evaluate API fit, not just popularity | +| Wrapping a package so heavily it obscures the original | If wrapping > 50% of original API, reconsider | +| Skipping research because "I know how to build this" | Research anyway — maintenance cost matters more than initial build | +| Installing a massive framework for one utility function | Look for focused, single-purpose packages | + +## Scope Limiter + +This skill concerns **utility and infrastructure code** only: +- Data transformation, validation, formatting +- Network operations, retries, caching +- CLI tooling, logging, configuration +- Test utilities and helpers + +It does NOT apply to **domain-specific business logic** where: +- The logic encodes unique business rules +- No generic solution could exist +- The code is inherently project-specific diff --git a/shared/skills/search-first/references/evaluation-criteria.md b/shared/skills/search-first/references/evaluation-criteria.md new file mode 100644 index 0000000..2bd59cb --- /dev/null +++ b/shared/skills/search-first/references/evaluation-criteria.md @@ -0,0 +1,101 @@ +# Search-First — Evaluation Criteria + +Detailed package evaluation criteria and decision matrix for the 4-outcome model. + +## Evaluation Matrix + +Score each candidate on these axes (1-5 scale): + +| Criterion | Weight | 1 (Poor) | 3 (Acceptable) | 5 (Excellent) | +|-----------|--------|-----------|-----------------|----------------| +| **Maintenance** | High | No commits in 2+ years | Active, yearly releases | Regular releases, responsive maintainer | +| **Adoption** | Medium | < 100 weekly downloads | 1K-10K weekly downloads | > 100K weekly downloads | +| **API Fit** | High | Needs heavy wrapping | Partial fit, thin adapter needed | Direct use, clean API | +| **Bundle Size** | Medium | > 500KB | 50-500KB | < 50KB | +| **Security** | High | Known vulnerabilities | No known issues, few dependencies | Audited, zero/minimal dependencies | +| **License** | Required | GPL/AGPL (restrictive) | LGPL (conditional) | MIT/Apache/BSD (permissive) | + +**Minimum thresholds**: License must be compatible. Security must be ≥ 3. All others are trade-offs. + +## Decision Matrix + +### Adopt (score ≥ 20/25, API Fit ≥ 4) + +The package directly solves the problem with minimal integration code. + +**Example**: Using `zod` for schema validation — exact fit, massive adoption, tiny bundle. + +``` +✅ Adopt: zod v3.22 +- Maintenance: 5 (monthly releases) +- Adoption: 5 (4M weekly downloads) +- API Fit: 5 (direct use for all validation) +- Bundle Size: 4 (57KB) +- Security: 5 (zero dependencies) +- Total: 24/25 +``` + +### Extend (score ≥ 15/25, API Fit ≥ 2) + +The package handles 60-80% of the need. Write a thin adapter for the rest. + +**Example**: Using `got` for HTTP but wrapping it with project-specific retry and auth logic. + +``` +✅ Extend: got v14 +- Maintenance: 4 (active) +- Adoption: 5 (8M weekly downloads) +- API Fit: 3 (need custom retry wrapper) +- Bundle Size: 3 (150KB) +- Security: 4 (minimal deps) +- Total: 19/25 +Adapter: ~30 lines wrapping retry + auth headers +``` + +### Compose (no single package fits, but small packages combine) + +Two or three focused packages together solve the problem better than one large framework. + +**Example**: `ms` (time parsing) + `p-retry` (retry logic) + `quick-lru` (caching) instead of a monolithic HTTP client framework. + +**Rules for Compose**: +- Maximum 3 packages in a composition +- Each package must be focused (single responsibility) +- Total combined bundle < what a monolithic alternative would cost +- Glue code should be < 50 lines + +### Build (nothing fits, or dependency cost > value) + +Write custom code when: +- No package scores ≥ 15/25 +- The code is < 50 lines and trivial +- Zero-dependency constraint is explicit +- The domain is too specific for generic packages + +**Required**: Document why Build was chosen: + +```typescript +// search-first: Built custom — our wire format uses non-standard +// ISO-8601 extensions that no date library handles correctly. +// Evaluated: date-fns (no custom format support), luxon (500KB overhead), +// dayjs (close but missing timezone edge case). +``` + +## Ecosystem-Specific Hints + +### Node.js / TypeScript +- Check npm: `https://www.npmjs.com/package/{name}` +- Bundle size: `https://bundlephobia.com/package/{name}` +- Check if Node.js built-ins handle it (`node:crypto`, `node:url`, `node:path`) + +### Python +- Check PyPI: `https://pypi.org/project/{name}` +- Check if stdlib handles it (`urllib`, `json`, `pathlib`, `dataclasses`) + +### Rust +- Check crates.io: `https://crates.io/crates/{name}` +- Check if std handles it + +### Go +- Check pkg.go.dev +- Go standard library is extensive — check stdlib first diff --git a/src/cli/plugins.ts b/src/cli/plugins.ts index c87199d..063c999 100644 --- a/src/cli/plugins.ts +++ b/src/cli/plugins.ts @@ -24,7 +24,7 @@ export const DEVFLOW_PLUGINS: PluginDefinition[] = [ description: 'Auto-activating quality enforcement (foundation layer)', commands: [], agents: [], - skills: ['core-patterns', 'docs-framework', 'git-safety', 'git-workflow', 'github-patterns', 'input-validation', 'test-driven-development', 'test-patterns'], + skills: ['core-patterns', 'docs-framework', 'git-safety', 'git-workflow', 'github-patterns', 'input-validation', 'search-first', 'test-driven-development', 'test-patterns'], }, { name: 'devflow-specify', From 7879fc619146e35dc80d2cad7e34bdcc1822edf6 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 13 Mar 2026 00:43:05 +0200 Subject: [PATCH 02/12] feat: add reviewer confidence thresholds and consolidation rules (#113) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each review finding now includes a visible confidence score (0-100%). Only findings ≥80% appear in main report sections; 60-79% go to a capped Suggestions section; <60% are dropped entirely. Adds consolidation rules: group similar issues, skip stylistic preferences, skip unchanged code unless CRITICAL. Fixes synthesizer review glob (was *-report.*.md, matched zero files) to *.md with self-exclusion. Adds confidence-aware aggregation with cross-reviewer boosting. Updates Git agent PR comment instructions to only create inline comments for ≥80% confidence findings. --- .../commands/code-review.md | 4 +- shared/agents/reviewer.md | 41 ++++++++++++++++--- shared/agents/synthesizer.md | 16 +++++--- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/plugins/devflow-code-review/commands/code-review.md b/plugins/devflow-code-review/commands/code-review.md index 86026ed..9a60979 100644 --- a/plugins/devflow-code-review/commands/code-review.md +++ b/plugins/devflow-code-review/commands/code-review.md @@ -95,7 +95,9 @@ IMPORTANT: Write report to .docs/reviews/{branch-slug}/{focus}.md using Write to Task(subagent_type="Git", run_in_background=false): "OPERATION: comment-pr Read reviews from .docs/reviews/{branch-slug}/ -Create inline PR comments, deduplicate, consolidate skipped into summary" +Create inline PR comments for findings with ≥80% confidence only. +Lower-confidence suggestions (60-79%) go in the summary comment, not as inline comments. +Deduplicate findings across reviewers, consolidate skipped into summary." ``` **Synthesizer Agent**: diff --git a/shared/agents/reviewer.md b/shared/agents/reviewer.md index f66fce1..aa0f8a6 100644 --- a/shared/agents/reviewer.md +++ b/shared/agents/reviewer.md @@ -46,8 +46,32 @@ The orchestrator provides: 3. **Apply 3-category classification** - Sort issues by where they occur 4. **Apply focus-specific analysis** - Use pattern skill detection rules from the loaded skill file 5. **Assign severity** - CRITICAL, HIGH, MEDIUM, LOW based on impact -6. **Generate report** - File:line references with suggested fixes -7. **Determine merge recommendation** - Based on blocking issues +6. **Assess confidence** - Assign 0-100% confidence to each finding (see Confidence Scale below) +7. **Filter by confidence** - Only report findings ≥80% in main sections; lower-confidence items go to Suggestions +8. **Consolidate similar issues** - Group related findings to reduce noise (see Consolidation Rules) +9. **Generate report** - File:line references with suggested fixes +10. **Determine merge recommendation** - Based on blocking issues + +## Confidence Scale + +Assess how certain you are that each finding is a real issue (not a false positive): + +| Range | Label | Meaning | +|-------|-------|---------| +| 90-100% | Certain | Clearly a bug, vulnerability, or violation — no ambiguity | +| 80-89% | High | Very likely an issue, but minor chance of false positive | +| 60-79% | Medium | Plausible issue, but depends on context you may not fully see | +| < 60% | Low | Possible concern, but likely a matter of style or interpretation | + +**Threshold**: Only report findings with ≥80% confidence in Blocking, Should-Fix, and Pre-existing sections. Findings with 60-79% confidence go to the Suggestions section. Findings < 60% are dropped entirely. + +## Consolidation Rules + +Before writing your report, apply these noise reduction rules: + +1. **Group similar issues** — If 3+ instances of the same pattern appear (e.g., "missing error handling" in multiple functions), consolidate into 1 finding listing all locations rather than N separate findings +2. **Skip stylistic preferences** — Do not flag formatting, naming style, or code organization choices unless they violate explicit project conventions found in CLAUDE.md, .editorconfig, or linter configs +3. **Skip issues in unchanged code** — Pre-existing issues in lines you did NOT change should only be reported if CRITICAL severity (security vulnerabilities, data loss risks) ## Issue Categories (from review-methodology) @@ -76,17 +100,24 @@ Report format for `{output_path}`: ### CRITICAL **{Issue}** - `file.ts:123` +**Confidence**: {n}% - Problem: {description} - Fix: {suggestion with code} ### HIGH -{issues...} +{issues with **Confidence**: {n}% each...} ## Issues in Code You Touched (Should Fix) -{issues with file:line...} +{issues with file:line and **Confidence**: {n}% each...} ## Pre-existing Issues (Not Blocking) -{informational issues...} +{informational issues with **Confidence**: {n}% each...} + +## Suggestions (Lower Confidence) + +{Max 3 items with 60-79% confidence. Brief description only — no code fixes.} + +- **{Issue}** - `file.ts:456` (Confidence: {n}%) — {brief description} ## Summary | Category | CRITICAL | HIGH | MEDIUM | LOW | diff --git a/shared/agents/synthesizer.md b/shared/agents/synthesizer.md index fe5fba5..e7005be 100644 --- a/shared/agents/synthesizer.md +++ b/shared/agents/synthesizer.md @@ -128,10 +128,13 @@ Analyze 3 axes to determine strategy: Synthesize outputs from multiple Reviewer agents. Apply strict merge rules. **Process:** -1. Read all review reports from `${REVIEW_BASE_DIR}/*-report.*.md` -2. Categorize issues into 3 buckets (from review-methodology) -3. Count by severity (CRITICAL, HIGH, MEDIUM, LOW) -4. Determine merge recommendation based on blocking issues +1. Read all review reports from `${REVIEW_BASE_DIR}/*.md` (exclude your own output `review-summary.*.md`) +2. Extract confidence percentages from each finding +3. Apply confidence-aware aggregation: when multiple reviewers flag the same file:line, boost confidence by 10% per additional reviewer (cap at 100%) +4. Maintain ≥80% confidence threshold in final output +5. Categorize issues into 3 buckets (from review-methodology) +6. Count by severity (CRITICAL, HIGH, MEDIUM, LOW) +7. Determine merge recommendation based on blocking issues **Issue Categories:** - **Blocking** (Category 1): Issues in YOUR changes - CRITICAL/HIGH must block @@ -172,7 +175,10 @@ Report format: | Pre-existing | - | - | {n} | {n} | {n} | ## Blocking Issues -{List with file:line and suggested fix} +{List with file:line, confidence %, and suggested fix} + +## Suggestions (Lower Confidence) +{Max 5 items across all reviewers with 60-79% confidence. Brief descriptions only.} ## Action Plan 1. {Priority fix} From 47c4a67be75b15816d5753adb762a8cbde72dc47 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 13 Mar 2026 00:43:20 +0200 Subject: [PATCH 03/12] feat: add version manifest for upgrade tracking (#91) New manifest.json tracks installed version, plugins, scope, and feature flags. Written to devflowDir after successful init. - readManifest/writeManifest/mergeManifestPlugins/detectUpgrade utilities - init.ts: detects upgrade/reinstall via manifest, writes after install - list.ts: shows install status, version, features from manifest - Partial installs merge plugins instead of overwriting - 17 new tests for all manifest operations --- src/cli/commands/init.ts | 27 ++++++ src/cli/commands/list.ts | 47 +++++++++- src/cli/utils/manifest.ts | 107 ++++++++++++++++++++++ tests/manifest.test.ts | 186 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 src/cli/utils/manifest.ts create mode 100644 tests/manifest.test.ts diff --git a/src/cli/commands/init.ts b/src/cli/commands/init.ts index 9e57843..aac3a00 100644 --- a/src/cli/commands/init.ts +++ b/src/cli/commands/init.ts @@ -24,6 +24,7 @@ import { detectPlatform, detectShell, getProfilePath, getSafeDeleteInfo, hasSafe import { generateSafeDeleteBlock, isAlreadyInstalled, installToProfile, removeFromProfile, getInstalledVersion, SAFE_DELETE_BLOCK_VERSION } from '../utils/safe-delete-install.js'; import { addAmbientHook, removeAmbientHook, hasAmbientHook } from './ambient.js'; import { addMemoryHooks, removeMemoryHooks, hasMemoryHooks } from './memory.js'; +import { readManifest, writeManifest, mergeManifestPlugins, detectUpgrade } from '../utils/manifest.js'; // Re-export pure functions for tests (canonical source is post-install.ts) export { substituteSettingsTemplate, computeGitignoreAppend, applyTeamsConfig, stripTeamsConfig, mergeDenyList } from '../utils/post-install.js'; @@ -292,6 +293,17 @@ export const initCommand = new Command('init') process.exit(1); } + // Check existing manifest for upgrade detection + const existingManifest = await readManifest(devflowDir); + if (existingManifest) { + const upgrade = detectUpgrade(version, existingManifest.version); + if (upgrade.isUpgrade) { + s.message(`Upgrading from v${upgrade.previousVersion} to v${version}`); + } else if (upgrade.isSameVersion) { + s.message('Reinstalling same version'); + } + } + // Validate target directory s.message('Validating target directory'); @@ -596,5 +608,20 @@ export const initCommand = new Command('init') p.log.info(`Deduplication: ${agentsMap.size} unique agents (from ${totalAgentDeclarations} declarations)`); } + // Write installation manifest for upgrade tracking + const installedPluginNames = pluginsToInstall.map(pl => pl.name); + const now = new Date().toISOString(); + const manifestData = { + version, + plugins: existingManifest && options.plugin + ? mergeManifestPlugins(existingManifest.plugins, installedPluginNames) + : installedPluginNames, + scope, + features: { teams: teamsEnabled, ambient: ambientEnabled, memory: memoryEnabled }, + installedAt: existingManifest?.installedAt ?? now, + updatedAt: now, + }; + await writeManifest(devflowDir, manifestData); + p.outro(color.green('Ready! Run any command in Claude Code to get started.')); }); diff --git a/src/cli/commands/list.ts b/src/cli/commands/list.ts index c8ee9d4..07f3988 100644 --- a/src/cli/commands/list.ts +++ b/src/cli/commands/list.ts @@ -2,21 +2,64 @@ import { Command } from 'commander'; import * as p from '@clack/prompts'; import color from 'picocolors'; import { DEVFLOW_PLUGINS } from '../plugins.js'; +import { getDevFlowDirectory } from '../utils/paths.js'; +import { getGitRoot } from '../utils/git.js'; +import { readManifest } from '../utils/manifest.js'; +import * as path from 'path'; export const listCommand = new Command('list') .description('List available DevFlow plugins') - .action(() => { + .action(async () => { p.intro(color.bgCyan(color.black(' DevFlow Plugins '))); + // Try to read manifest from user scope and local scope + const userDevflowDir = getDevFlowDirectory(); + const gitRoot = await getGitRoot(); + const localDevflowDir = gitRoot ? path.join(gitRoot, '.devflow') : null; + + const userManifest = await readManifest(userDevflowDir); + const localManifest = localDevflowDir ? await readManifest(localDevflowDir) : null; + const manifest = localManifest ?? userManifest; + + // Show install status if manifest exists + if (manifest) { + const installedAt = new Date(manifest.installedAt).toLocaleDateString(); + const updatedAt = new Date(manifest.updatedAt).toLocaleDateString(); + const scope = localManifest ? 'local' : 'user'; + const features = [ + manifest.features.teams ? 'teams' : null, + manifest.features.ambient ? 'ambient' : null, + manifest.features.memory ? 'memory' : null, + ].filter(Boolean).join(', ') || 'none'; + + p.note( + `${color.dim('Version:')} ${color.cyan(`v${manifest.version}`)}\n` + + `${color.dim('Scope:')} ${scope}\n` + + `${color.dim('Features:')} ${features}\n` + + `${color.dim('Installed:')} ${installedAt}` + + (installedAt !== updatedAt ? ` ${color.dim('Updated:')} ${updatedAt}` : ''), + 'Installation', + ); + } + + const installedPlugins = new Set(manifest?.plugins ?? []); const maxNameLen = Math.max(...DEVFLOW_PLUGINS.map(p => p.name.length)); const pluginList = DEVFLOW_PLUGINS .map(plugin => { const cmds = plugin.commands.length > 0 ? plugin.commands.join(', ') : '(skills only)'; const optionalTag = plugin.optional ? color.dim(' (optional)') : ''; - return `${color.cyan(plugin.name.padEnd(maxNameLen + 2))}${color.dim(plugin.description)}${optionalTag}\n${' '.repeat(maxNameLen + 2)}${color.yellow(cmds)}`; + const installedTag = manifest + ? (installedPlugins.has(plugin.name) ? color.green(' ✓') : color.dim(' ✗')) + : ''; + return `${color.cyan(plugin.name.padEnd(maxNameLen + 2))}${color.dim(plugin.description)}${optionalTag}${installedTag}\n${' '.repeat(maxNameLen + 2)}${color.yellow(cmds)}`; }) .join('\n\n'); p.note(pluginList, 'Available plugins'); + + if (!manifest) { + p.log.info(color.dim('Run `devflow init` for install tracking')); + } + p.outro(color.dim('Install with: npx devflow-kit init --plugin=')); }); diff --git a/src/cli/utils/manifest.ts b/src/cli/utils/manifest.ts new file mode 100644 index 0000000..8c7d5e8 --- /dev/null +++ b/src/cli/utils/manifest.ts @@ -0,0 +1,107 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; + +/** + * Manifest data tracked for each DevFlow installation. + */ +export interface ManifestData { + version: string; + plugins: string[]; + scope: 'user' | 'local'; + features: { + teams: boolean; + ambient: boolean; + memory: boolean; + }; + installedAt: string; + updatedAt: string; +} + +/** + * Read and parse the manifest file. Returns null if missing or corrupt. + */ +export async function readManifest(devflowDir: string): Promise { + const manifestPath = path.join(devflowDir, 'manifest.json'); + try { + const content = await fs.readFile(manifestPath, 'utf-8'); + const data = JSON.parse(content) as ManifestData; + if (!data.version || !Array.isArray(data.plugins) || !data.scope) { + return null; + } + return data; + } catch { + return null; + } +} + +/** + * Write manifest to disk. Creates parent directory if needed. + */ +export async function writeManifest(devflowDir: string, data: ManifestData): Promise { + await fs.mkdir(devflowDir, { recursive: true }); + const manifestPath = path.join(devflowDir, 'manifest.json'); + await fs.writeFile(manifestPath, JSON.stringify(data, null, 2) + '\n', 'utf-8'); +} + +/** + * Merge new plugins into existing plugin list (union, no duplicates). + * Preserves order: existing plugins first, then new ones appended. + */ +export function mergeManifestPlugins(existing: string[], newPlugins: string[]): string[] { + const merged = [...existing]; + for (const plugin of newPlugins) { + if (!merged.includes(plugin)) { + merged.push(plugin); + } + } + return merged; +} + +/** + * Compare two semver strings. Returns -1, 0, or 1. + * Handles simple x.y.z versions; returns null for unparseable input. + */ +function compareSemver(a: string, b: string): number | null { + const parse = (v: string): number[] => { + const match = v.match(/^v?(\d+)\.(\d+)\.(\d+)/); + return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : []; + }; + + const pa = parse(a); + const pb = parse(b); + + if (pa.length === 0 || pb.length === 0) return null; + + for (let i = 0; i < 3; i++) { + if (pa[i] > pb[i]) return 1; + if (pa[i] < pb[i]) return -1; + } + return 0; +} + +/** + * Detect upgrade/downgrade/same version status. + */ +export interface UpgradeInfo { + isUpgrade: boolean; + isDowngrade: boolean; + isSameVersion: boolean; + previousVersion: string | null; +} + +export function detectUpgrade(currentVersion: string, installedVersion: string | null): UpgradeInfo { + if (!installedVersion) { + return { isUpgrade: false, isDowngrade: false, isSameVersion: false, previousVersion: null }; + } + + const cmp = compareSemver(currentVersion, installedVersion); + if (cmp === null) { + return { isUpgrade: false, isDowngrade: false, isSameVersion: false, previousVersion: installedVersion }; + } + return { + isUpgrade: cmp > 0, + isDowngrade: cmp < 0, + isSameVersion: cmp === 0, + previousVersion: installedVersion, + }; +} diff --git a/tests/manifest.test.ts b/tests/manifest.test.ts new file mode 100644 index 0000000..98594ec --- /dev/null +++ b/tests/manifest.test.ts @@ -0,0 +1,186 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { promises as fs } from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import { readManifest, writeManifest, mergeManifestPlugins, detectUpgrade, type ManifestData } from '../src/cli/utils/manifest.js'; + +describe('readManifest', () => { + let tmpDir: string; + + beforeEach(async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'devflow-manifest-')); + }); + + afterEach(async () => { + await fs.rm(tmpDir, { recursive: true, force: true }); + }); + + it('returns null for missing file', async () => { + const result = await readManifest(tmpDir); + expect(result).toBeNull(); + }); + + it('returns null for corrupt JSON', async () => { + await fs.writeFile(path.join(tmpDir, 'manifest.json'), 'not-json{{{', 'utf-8'); + const result = await readManifest(tmpDir); + expect(result).toBeNull(); + }); + + it('returns null for invalid shape (missing required fields)', async () => { + await fs.writeFile(path.join(tmpDir, 'manifest.json'), JSON.stringify({ foo: 'bar' }), 'utf-8'); + const result = await readManifest(tmpDir); + expect(result).toBeNull(); + }); + + it('returns parsed manifest for valid data', async () => { + const data: ManifestData = { + version: '1.4.0', + plugins: ['devflow-core-skills', 'devflow-implement'], + scope: 'user', + features: { teams: false, ambient: true, memory: true }, + installedAt: '2026-03-01T00:00:00.000Z', + updatedAt: '2026-03-13T00:00:00.000Z', + }; + await fs.writeFile(path.join(tmpDir, 'manifest.json'), JSON.stringify(data), 'utf-8'); + const result = await readManifest(tmpDir); + expect(result).toEqual(data); + }); +}); + +describe('writeManifest', () => { + let tmpDir: string; + + beforeEach(async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'devflow-manifest-')); + }); + + afterEach(async () => { + await fs.rm(tmpDir, { recursive: true, force: true }); + }); + + it('creates manifest file', async () => { + const data: ManifestData = { + version: '1.4.0', + plugins: ['devflow-core-skills'], + scope: 'user', + features: { teams: false, ambient: true, memory: true }, + installedAt: '2026-03-13T00:00:00.000Z', + updatedAt: '2026-03-13T00:00:00.000Z', + }; + await writeManifest(tmpDir, data); + const content = await fs.readFile(path.join(tmpDir, 'manifest.json'), 'utf-8'); + expect(JSON.parse(content)).toEqual(data); + }); + + it('overwrites existing manifest', async () => { + const old: ManifestData = { + version: '1.0.0', + plugins: ['devflow-core-skills'], + scope: 'user', + features: { teams: false, ambient: false, memory: false }, + installedAt: '2026-01-01T00:00:00.000Z', + updatedAt: '2026-01-01T00:00:00.000Z', + }; + await writeManifest(tmpDir, old); + + const updated: ManifestData = { ...old, version: '1.4.0', updatedAt: '2026-03-13T00:00:00.000Z' }; + await writeManifest(tmpDir, updated); + + const result = await readManifest(tmpDir); + expect(result?.version).toBe('1.4.0'); + }); + + it('creates parent directory if needed', async () => { + const nestedDir = path.join(tmpDir, 'nested', 'devflow'); + const data: ManifestData = { + version: '1.4.0', + plugins: [], + scope: 'local', + features: { teams: false, ambient: false, memory: false }, + installedAt: '2026-03-13T00:00:00.000Z', + updatedAt: '2026-03-13T00:00:00.000Z', + }; + await writeManifest(nestedDir, data); + const result = await readManifest(nestedDir); + expect(result?.version).toBe('1.4.0'); + }); +}); + +describe('mergeManifestPlugins', () => { + it('returns union of existing and new plugins', () => { + const result = mergeManifestPlugins( + ['devflow-core-skills', 'devflow-implement'], + ['devflow-code-review', 'devflow-debug'], + ); + expect(result).toEqual(['devflow-core-skills', 'devflow-implement', 'devflow-code-review', 'devflow-debug']); + }); + + it('does not create duplicates', () => { + const result = mergeManifestPlugins( + ['devflow-core-skills', 'devflow-implement'], + ['devflow-implement', 'devflow-code-review'], + ); + expect(result).toEqual(['devflow-core-skills', 'devflow-implement', 'devflow-code-review']); + }); + + it('preserves order (existing first)', () => { + const result = mergeManifestPlugins( + ['b', 'a'], + ['c', 'a'], + ); + expect(result).toEqual(['b', 'a', 'c']); + }); +}); + +describe('detectUpgrade', () => { + it('detects fresh install (no previous version)', () => { + const result = detectUpgrade('1.4.0', null); + expect(result).toEqual({ + isUpgrade: false, + isDowngrade: false, + isSameVersion: false, + previousVersion: null, + }); + }); + + it('detects same version', () => { + const result = detectUpgrade('1.4.0', '1.4.0'); + expect(result.isSameVersion).toBe(true); + expect(result.isUpgrade).toBe(false); + expect(result.isDowngrade).toBe(false); + expect(result.previousVersion).toBe('1.4.0'); + }); + + it('detects upgrade (newer version)', () => { + const result = detectUpgrade('2.0.0', '1.4.0'); + expect(result.isUpgrade).toBe(true); + expect(result.isDowngrade).toBe(false); + expect(result.isSameVersion).toBe(false); + expect(result.previousVersion).toBe('1.4.0'); + }); + + it('detects minor upgrade', () => { + const result = detectUpgrade('1.5.0', '1.4.0'); + expect(result.isUpgrade).toBe(true); + }); + + it('detects patch upgrade', () => { + const result = detectUpgrade('1.4.1', '1.4.0'); + expect(result.isUpgrade).toBe(true); + }); + + it('detects downgrade', () => { + const result = detectUpgrade('1.0.0', '1.4.0'); + expect(result.isDowngrade).toBe(true); + expect(result.isUpgrade).toBe(false); + expect(result.previousVersion).toBe('1.4.0'); + }); + + it('handles unparseable versions gracefully', () => { + const result = detectUpgrade('not-a-version', '1.4.0'); + expect(result.isUpgrade).toBe(false); + expect(result.isDowngrade).toBe(false); + expect(result.isSameVersion).toBe(false); + expect(result.previousVersion).toBe('1.4.0'); + }); +}); From 9e316a7be843995fa7f3c6e834574ae1bdbcfd88 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 13 Mar 2026 00:43:27 +0200 Subject: [PATCH 04/12] docs: update changelog and skill count for Wave 1 features --- CHANGELOG.md | 8 ++++++++ CLAUDE.md | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a76e80b..2f8b4a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- **Search-first skill** (#111) — New skill enforcing research before building custom utility code. 4-phase loop: Need Analysis → Search (via Explore subagent) → Evaluate → Decide (Adopt/Extend/Compose/Build) +- **Reviewer confidence thresholds** (#113) — Each review finding now includes a visible confidence score (0-100%). Only ≥80% findings appear in main sections; lower-confidence items go to a capped Suggestions section. Adds consolidation rules to group similar issues and skip stylistic preferences +- **Version manifest** (#91) — Tracks installed version, plugins, and features in `manifest.json`. Enables upgrade detection during `devflow init` and shows install status in `devflow list` + +### Fixed +- **Synthesizer review glob** — Fixed `${REVIEW_BASE_DIR}/*-report.*.md` glob that matched zero reviewer files; now uses `${REVIEW_BASE_DIR}/*.md` with self-exclusion + --- ## [1.4.0] - 2026-03-09 diff --git a/CLAUDE.md b/CLAUDE.md index 21b3fc2..300c763 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,7 +44,7 @@ Commands with Teams Variant ship as `{name}.md` (parallel subagents) and `{name} ``` devflow/ -├── shared/skills/ # 30 skills (single source of truth) +├── shared/skills/ # 31 skills (single source of truth) ├── shared/agents/ # 10 shared agents (single source of truth) ├── plugins/devflow-*/ # 17 plugins (9 core + 8 optional language/ecosystem) ├── docs/reference/ # Detailed reference documentation From cd315f52664f74575160ef4244f6fd85b0853213 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 13 Mar 2026 01:12:25 +0200 Subject: [PATCH 05/12] chore: untrack gitignored build artifacts in devflow-specify --- plugins/devflow-specify/agents/skimmer.md | 88 -------- plugins/devflow-specify/agents/synthesizer.md | 204 ------------------ 2 files changed, 292 deletions(-) delete mode 100644 plugins/devflow-specify/agents/skimmer.md delete mode 100644 plugins/devflow-specify/agents/synthesizer.md diff --git a/plugins/devflow-specify/agents/skimmer.md b/plugins/devflow-specify/agents/skimmer.md deleted file mode 100644 index 284cdd7..0000000 --- a/plugins/devflow-specify/agents/skimmer.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -name: Skimmer -description: Codebase orientation using skim to identify relevant files, functions, and patterns for a feature or task -model: inherit ---- - -# Skimmer Agent - -You are a codebase orientation specialist using `skim` to efficiently understand codebases. Extract structure without implementation noise - find entry points, data flow, and integration points quickly. - -## Input Context - -You receive from orchestrator: -- **TASK_DESCRIPTION**: What feature/task needs to be implemented or understood - -## Responsibilities - -1. **Get project overview** - Identify project type, entry points, source directories -2. **Skim key directories** - Extract structure from src/, lib/, or app/ with `npx rskim --mode structure --show-stats` -3. **Search for task-relevant code** - Find files matching task keywords -4. **Identify integration points** - Exports, entry points, import patterns -5. **Generate orientation summary** - Structured output for implementation planning - -## Tool Invocation - -Always invoke skim via `npx rskim`. This works whether or not skim is globally installed — npx downloads and caches it transparently. - -## Skim Modes - -| Mode | Use When | Command | -|------|----------|---------| -| `structure` | High-level overview | `npx rskim src/ --mode structure` | -| `signatures` | Need API/function details | `npx rskim src/ --mode signatures` | -| `types` | Working with type definitions | `npx rskim src/ --mode types` | - -## Output - -```markdown -## Codebase Orientation - -### Project Type -{Language/framework from package.json, Cargo.toml, etc.} - -### Token Statistics -{From skim --show-stats: original vs skimmed tokens} - -### Directory Structure -| Directory | Purpose | -|-----------|---------| -| src/ | {description} | -| lib/ | {description} | - -### Relevant Files for Task -| File | Purpose | Key Exports | -|------|---------|-------------| -| `path/file.ts` | {description} | {functions, types} | - -### Key Functions/Types -{Specific functions, classes, or types related to task} - -### Integration Points -{Where new code connects to existing code} - -### Patterns Observed -{Existing patterns to follow} - -### Suggested Approach -{Brief recommendation based on codebase structure} -``` - -## Principles - -1. **Speed over depth** - Get oriented quickly, don't deep dive everything -2. **Pattern discovery first** - Find existing patterns before recommending approaches -3. **Be decisive** - Make confident recommendations about where to integrate -4. **Token efficiency** - Use skim stats to show compression ratio -5. **Task-focused** - Only explore what's relevant to the task - -## Boundaries - -**Handle autonomously:** -- Directory structure exploration -- Pattern identification -- Generating orientation summaries - -**Escalate to orchestrator:** -- No source directories found (ask user for structure) -- Ambiguous project structure (report findings, ask for clarification) diff --git a/plugins/devflow-specify/agents/synthesizer.md b/plugins/devflow-specify/agents/synthesizer.md deleted file mode 100644 index fe5fba5..0000000 --- a/plugins/devflow-specify/agents/synthesizer.md +++ /dev/null @@ -1,204 +0,0 @@ ---- -name: Synthesizer -description: Combines outputs from multiple agents into actionable summaries (modes: exploration, planning, review) -model: haiku -skills: review-methodology, docs-framework ---- - -# Synthesizer Agent - -You are a synthesis specialist. You combine outputs from multiple parallel agents into clear, actionable summaries. You operate in three modes: exploration, planning, and review. - -## Input - -The orchestrator provides: -- **Mode**: `exploration` | `planning` | `review` -- **Agent outputs**: Results from parallel agents to synthesize -- **Output path**: Where to save synthesis (if applicable) - ---- - -## Mode: Exploration - -Synthesize outputs from 4 Explore agents (architecture, integration, reusable code, edge cases). - -**Process:** -1. Extract key findings from each explorer -2. Identify patterns that appear across multiple explorations -3. Resolve conflicts if explorers found contradictory patterns -4. Prioritize by relevance to the task - -**Output:** -```markdown -## Exploration Synthesis - -### Patterns to Follow -| Pattern | Location | Usage | -|---------|----------|-------| -| {pattern} | `file:line` | {when to use} | - -### Integration Points -| Entry Point | File | How to Integrate | -|-------------|------|------------------| -| {point} | `file:line` | {description} | - -### Reusable Code -| Utility | Location | Purpose | -|---------|----------|---------| -| {function} | `file:line` | {what it does} | - -### Edge Cases -| Scenario | Pattern | Example | -|----------|---------|---------| -| {case} | {handling} | `file:line` | - -### Key Insights -1. {insight} -2. {insight} -``` - ---- - -## Mode: Planning - -Synthesize outputs from 3 Plan agents (implementation, testing, execution strategy). - -**Process:** -1. Merge implementation steps with testing strategy -2. Apply execution strategy analysis to determine Coder deployment -3. Identify dependencies between steps -4. Assess context risk based on file count and module breadth - -**Execution Strategy Decision:** - -Analyze 3 axes to determine strategy: - -| Axis | Signals | Impact | -|------|---------|--------| -| **Artifact Independence** | Shared contracts? Integration points? Cross-file dependencies? | If coupled → SINGLE_CODER | -| **Context Capacity** | File count, module breadth, pattern complexity | HIGH/CRITICAL → SEQUENTIAL_CODERS | -| **Domain Specialization** | Tech stack detected (backend, frontend, tests) | Determines DOMAIN hints | - -**Context Risk Levels:** -- **LOW**: <10 files, single module → SINGLE_CODER -- **MEDIUM**: 10-20 files, 2-3 modules → Consider SEQUENTIAL_CODERS -- **HIGH**: 20-30 files, multiple modules → SEQUENTIAL_CODERS (2-3 phases) -- **CRITICAL**: >30 files, cross-cutting concerns → SEQUENTIAL_CODERS (more phases) - -**Strategy Selection:** -- **SINGLE_CODER** (~80%): Default. Coherent A→Z implementation. Best for consistency in naming, patterns, error handling. -- **SEQUENTIAL_CODERS** (~15%): Context overflow risk or layered dependencies. Split into phases with handoff summaries. -- **PARALLEL_CODERS** (~5%): True artifact independence - no shared contracts, no integration points. Rare. - -**Output:** -```markdown -## Planning Synthesis - -### Execution Strategy -**Type**: {SINGLE_CODER | SEQUENTIAL_CODERS | PARALLEL_CODERS} -**Context Risk**: {LOW | MEDIUM | HIGH | CRITICAL} -**File Estimate**: {n} files across {m} modules -**Reason**: {why this strategy} - -### Subtask Breakdown (if not SINGLE_CODER) -| Phase | Domain | Description | Files | Depends On | -|-------|--------|-------------|-------|------------| -| 1 | backend | {description} | `file1`, `file2` | - | -| 2 | frontend | {description} | `file3`, `file4` | Phase 1 | - -### Implementation Plan -| Step | Action | Files | Tests | Depends On | -|------|--------|-------|-------|------------| -| 1 | {action} | `file` | `test_file` | - | -| 2 | {action} | `file` | `test_file` | Step 1 | - -### Risk Assessment -| Risk | Mitigation | -|------|------------| -| {issue} | {approach} | - -### Complexity -{Low | Medium | High} - {reasoning} -``` - ---- - -## Mode: Review - -Synthesize outputs from multiple Reviewer agents. Apply strict merge rules. - -**Process:** -1. Read all review reports from `${REVIEW_BASE_DIR}/*-report.*.md` -2. Categorize issues into 3 buckets (from review-methodology) -3. Count by severity (CRITICAL, HIGH, MEDIUM, LOW) -4. Determine merge recommendation based on blocking issues - -**Issue Categories:** -- **Blocking** (Category 1): Issues in YOUR changes - CRITICAL/HIGH must block -- **Should-Fix** (Category 2): Issues in code you touched - HIGH/MEDIUM -- **Pre-existing** (Category 3): Legacy issues - informational only - -**Merge Rules:** -| Condition | Recommendation | -|-----------|----------------| -| Any CRITICAL in blocking | BLOCK MERGE | -| Any HIGH in blocking | CHANGES REQUESTED | -| Only MEDIUM in blocking | APPROVED WITH COMMENTS | -| No blocking issues | APPROVED | - -**Output:** -**CRITICAL**: Write the summary to disk using the Write tool: -1. Create directory: `mkdir -p ${REVIEW_BASE_DIR}` -2. Write to `${REVIEW_BASE_DIR}/review-summary.${TIMESTAMP}.md` using Write tool -3. Confirm file written in final message - -Report format: - -```markdown -# Code Review Summary - -**Branch**: {branch} -> {base} -**Date**: {timestamp} - -## Merge Recommendation: {BLOCK | CHANGES_REQUESTED | APPROVED} - -{Brief reasoning} - -## Issue Summary -| Category | CRITICAL | HIGH | MEDIUM | LOW | Total | -|----------|----------|------|--------|-----|-------| -| Blocking | {n} | {n} | {n} | - | {n} | -| Should Fix | - | {n} | {n} | - | {n} | -| Pre-existing | - | - | {n} | {n} | {n} | - -## Blocking Issues -{List with file:line and suggested fix} - -## Action Plan -1. {Priority fix} -2. {Next fix} -``` - ---- - -## Principles - -1. **No new research** - Only synthesize what agents found -2. **Preserve references** - Keep file:line from source agents -3. **Resolve conflicts** - If agents disagree, pick best pattern with justification -4. **Actionable output** - Results must be executable by next phase -5. **Accurate counts** - Issue counts must match reality (review mode) -6. **Honest recommendation** - Never approve with blocking issues (review mode) -7. **Be decisive** - Make confident synthesis choices - -## Boundaries - -**Handle autonomously:** -- Combining agent outputs -- Resolving conflicts between agents -- Generating structured summaries -- Determining merge recommendations - -**Escalate to orchestrator:** -- Fundamental disagreements between agents that need user input -- Missing critical agent outputs From 7fddd4b4722e4ffdb6215821f569305424365c49 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sat, 14 Mar 2026 00:04:46 +0200 Subject: [PATCH 06/12] docs: add Unreleased comparison link to CHANGELOG.md Add missing [Unreleased] link reference per keep-a-changelog convention. Points to comparison from v1.4.0 (latest release) to HEAD. Co-Authored-By: Claude --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f8b4a1..a39864a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -865,6 +865,7 @@ devflow init --- +[Unreleased]: https://github.com/dean0x/devflow/compare/v1.4.0...HEAD [1.4.0]: https://github.com/dean0x/devflow/compare/v1.3.3...v1.4.0 [1.3.3]: https://github.com/dean0x/devflow/compare/v1.3.2...v1.3.3 [1.3.2]: https://github.com/dean0x/devflow/compare/v1.3.1...v1.3.2 From ac3a64023780ef3f1c1953042211e69420de111a Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sat, 14 Mar 2026 00:04:52 +0200 Subject: [PATCH 07/12] docs: update README skill counts and add missing search-first entry - Update total skill count from 30 to 31 and core auto-activating count from 8 to 9 to reflect the addition of search-first - Add search-first row to auto-activating skills table Co-Authored-By: Claude --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2092d4d..0bbea98 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ DevFlow adds structured commands that handle the full lifecycle: specify feature - **Full-lifecycle implementation** — spec, explore, plan, code, validate, refine in one command - **Automatic session memory** — survives restarts, `/clear`, and context compaction - **Parallel debugging** — competing hypotheses investigated simultaneously -- **30 quality skills** — 8 auto-activating core, 8 optional language/ecosystem, plus specialized review and agent skills +- **31 quality skills** — 9 auto-activating core, 8 optional language/ecosystem, plus specialized review and agent skills ## Quick Start @@ -120,6 +120,7 @@ The `devflow-core-skills` plugin provides quality enforcement skills that activa | `test-driven-development` | Implementing new features (RED-GREEN-REFACTOR) | | `test-patterns` | Writing or modifying tests | | `input-validation` | Creating API endpoints | +| `search-first` | Adding utilities, helpers, or infrastructure code | ## Language & Ecosystem Plugins From 4305801b71d25e0b5d8d33a2deff74f3362dc294 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sat, 14 Mar 2026 00:05:30 +0200 Subject: [PATCH 08/12] docs: add consolidated findings template and confidence threshold cross-refs - Add report template example for grouped/consolidated findings in reviewer.md so agents know how to format N-occurrence issues - Add cross-reference HTML comments in reviewer.md, synthesizer.md, and code-review.md linking the three locations where the 80% confidence threshold is documented, reducing sync drift risk Resolves: batch-6 (B7, S6) Co-Authored-By: Claude --- .../commands/code-review.md | 1 + shared/agents/reviewer.md | 6 +++ shared/agents/synthesizer.md | 1 + src/cli/utils/manifest.ts | 32 ++++++++++++++- tests/manifest.test.ts | 40 +++++++++++++++++++ 5 files changed, 79 insertions(+), 1 deletion(-) diff --git a/plugins/devflow-code-review/commands/code-review.md b/plugins/devflow-code-review/commands/code-review.md index 9a60979..674f244 100644 --- a/plugins/devflow-code-review/commands/code-review.md +++ b/plugins/devflow-code-review/commands/code-review.md @@ -95,6 +95,7 @@ IMPORTANT: Write report to .docs/reviews/{branch-slug}/{focus}.md using Write to Task(subagent_type="Git", run_in_background=false): "OPERATION: comment-pr Read reviews from .docs/reviews/{branch-slug}/ + Create inline PR comments for findings with ≥80% confidence only. Lower-confidence suggestions (60-79%) go in the summary comment, not as inline comments. Deduplicate findings across reviewers, consolidate skipped into summary." diff --git a/shared/agents/reviewer.md b/shared/agents/reviewer.md index aa0f8a6..95c4f5a 100644 --- a/shared/agents/reviewer.md +++ b/shared/agents/reviewer.md @@ -63,6 +63,7 @@ Assess how certain you are that each finding is a real issue (not a false positi | 60-79% | Medium | Plausible issue, but depends on context you may not fully see | | < 60% | Low | Possible concern, but likely a matter of style or interpretation | + **Threshold**: Only report findings with ≥80% confidence in Blocking, Should-Fix, and Pre-existing sections. Findings with 60-79% confidence go to the Suggestions section. Findings < 60% are dropped entirely. ## Consolidation Rules @@ -104,6 +105,11 @@ Report format for `{output_path}`: - Problem: {description} - Fix: {suggestion with code} +**{Issue Title} ({N} occurrences)** — Confidence: {n}% +- `file1.ts:12`, `file2.ts:45`, `file3.ts:89` +- Problem: {description of the shared pattern} +- Fix: {suggestion that applies to all occurrences} + ### HIGH {issues with **Confidence**: {n}% each...} diff --git a/shared/agents/synthesizer.md b/shared/agents/synthesizer.md index e7005be..bffa931 100644 --- a/shared/agents/synthesizer.md +++ b/shared/agents/synthesizer.md @@ -131,6 +131,7 @@ Synthesize outputs from multiple Reviewer agents. Apply strict merge rules. 1. Read all review reports from `${REVIEW_BASE_DIR}/*.md` (exclude your own output `review-summary.*.md`) 2. Extract confidence percentages from each finding 3. Apply confidence-aware aggregation: when multiple reviewers flag the same file:line, boost confidence by 10% per additional reviewer (cap at 100%) + 4. Maintain ≥80% confidence threshold in final output 5. Categorize issues into 3 buckets (from review-methodology) 6. Count by severity (CRITICAL, HIGH, MEDIUM, LOW) diff --git a/src/cli/utils/manifest.ts b/src/cli/utils/manifest.ts index 8c7d5e8..229925c 100644 --- a/src/cli/utils/manifest.ts +++ b/src/cli/utils/manifest.ts @@ -25,7 +25,18 @@ export async function readManifest(devflowDir: string): Promise { @@ -89,6 +103,22 @@ export interface UpgradeInfo { previousVersion: string | null; } +/** + * Resolve the final plugin list for a manifest write. + * On partial installs (--plugin flag), merge newly installed plugins into + * the existing manifest's plugin list. On full installs, replace entirely. + */ +export function resolvePluginList( + installedPluginNames: string[], + existingManifest: ManifestData | null, + isPartialInstall: boolean, +): string[] { + if (existingManifest && isPartialInstall) { + return mergeManifestPlugins(existingManifest.plugins, installedPluginNames); + } + return installedPluginNames; +} + export function detectUpgrade(currentVersion: string, installedVersion: string | null): UpgradeInfo { if (!installedVersion) { return { isUpgrade: false, isDowngrade: false, isSameVersion: false, previousVersion: null }; diff --git a/tests/manifest.test.ts b/tests/manifest.test.ts index 98594ec..49c98da 100644 --- a/tests/manifest.test.ts +++ b/tests/manifest.test.ts @@ -32,6 +32,46 @@ describe('readManifest', () => { expect(result).toBeNull(); }); + it('returns null when features object is missing', async () => { + const partial = { + version: '1.4.0', + plugins: ['devflow-core-skills'], + scope: 'user', + installedAt: '2026-03-13T00:00:00.000Z', + updatedAt: '2026-03-13T00:00:00.000Z', + }; + await fs.writeFile(path.join(tmpDir, 'manifest.json'), JSON.stringify(partial), 'utf-8'); + const result = await readManifest(tmpDir); + expect(result).toBeNull(); + }); + + it('returns null when installedAt is missing', async () => { + const partial = { + version: '1.4.0', + plugins: ['devflow-core-skills'], + scope: 'user', + features: { teams: false, ambient: true, memory: true }, + updatedAt: '2026-03-13T00:00:00.000Z', + }; + await fs.writeFile(path.join(tmpDir, 'manifest.json'), JSON.stringify(partial), 'utf-8'); + const result = await readManifest(tmpDir); + expect(result).toBeNull(); + }); + + it('returns null when features has wrong types', async () => { + const partial = { + version: '1.4.0', + plugins: ['devflow-core-skills'], + scope: 'user', + features: { teams: 'yes', ambient: true, memory: true }, + installedAt: '2026-03-13T00:00:00.000Z', + updatedAt: '2026-03-13T00:00:00.000Z', + }; + await fs.writeFile(path.join(tmpDir, 'manifest.json'), JSON.stringify(partial), 'utf-8'); + const result = await readManifest(tmpDir); + expect(result).toBeNull(); + }); + it('returns parsed manifest for valid data', async () => { const data: ManifestData = { version: '1.4.0', From aa577d1bfc48ef00905bd9ce38b448adb207d7f2 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sat, 14 Mar 2026 00:06:45 +0200 Subject: [PATCH 09/12] fix(list): extract pure logic into testable functions and parallelize I/O - Extract formatFeatures, resolveScope, getPluginInstallStatus, and formatPluginCommands as exported pure functions from list.ts handler - Parallelize getGitRoot() and readManifest() with Promise.all since they are independent I/O operations - Add 14 unit tests in list-logic.test.ts covering all extracted functions Co-Authored-By: Claude --- src/cli/commands/list.ts | 70 +++++++++++++++++++++++++------- tests/list-logic.test.ts | 87 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 15 deletions(-) create mode 100644 tests/list-logic.test.ts diff --git a/src/cli/commands/list.ts b/src/cli/commands/list.ts index 07f3988..71e57d6 100644 --- a/src/cli/commands/list.ts +++ b/src/cli/commands/list.ts @@ -1,23 +1,65 @@ import { Command } from 'commander'; import * as p from '@clack/prompts'; import color from 'picocolors'; -import { DEVFLOW_PLUGINS } from '../plugins.js'; +import { DEVFLOW_PLUGINS, type PluginDefinition } from '../plugins.js'; import { getDevFlowDirectory } from '../utils/paths.js'; import { getGitRoot } from '../utils/git.js'; -import { readManifest } from '../utils/manifest.js'; +import { readManifest, type ManifestData } from '../utils/manifest.js'; import * as path from 'path'; +/** + * Format manifest feature flags into a human-readable comma-separated string. + * Returns 'none' when no features are enabled. + */ +export function formatFeatures(features: ManifestData['features']): string { + return [ + features.teams ? 'teams' : null, + features.ambient ? 'ambient' : null, + features.memory ? 'memory' : null, + ].filter(Boolean).join(', ') || 'none'; +} + +/** + * Determine effective installation scope based on which manifest was found. + * Local scope takes precedence when a local manifest exists. + */ +export function resolveScope(localManifest: ManifestData | null): 'local' | 'user' { + return localManifest ? 'local' : 'user'; +} + +/** + * Compute the install status indicator for a plugin. + * Returns 'installed', 'not_installed', or 'unknown' (when no manifest exists). + */ +export function getPluginInstallStatus( + pluginName: string, + installedPlugins: ReadonlySet, + hasManifest: boolean, +): 'installed' | 'not_installed' | 'unknown' { + if (!hasManifest) return 'unknown'; + return installedPlugins.has(pluginName) ? 'installed' : 'not_installed'; +} + +/** + * Format the commands portion of a plugin entry. + * Returns the comma-separated command list or '(skills only)' for skill-only plugins. + */ +export function formatPluginCommands(commands: string[]): string { + return commands.length > 0 ? commands.join(', ') : '(skills only)'; +} + export const listCommand = new Command('list') .description('List available DevFlow plugins') .action(async () => { p.intro(color.bgCyan(color.black(' DevFlow Plugins '))); - // Try to read manifest from user scope and local scope + // Resolve user manifest and git root in parallel (independent I/O) const userDevflowDir = getDevFlowDirectory(); - const gitRoot = await getGitRoot(); + const [gitRoot, userManifest] = await Promise.all([ + getGitRoot(), + readManifest(userDevflowDir), + ]); const localDevflowDir = gitRoot ? path.join(gitRoot, '.devflow') : null; - - const userManifest = await readManifest(userDevflowDir); const localManifest = localDevflowDir ? await readManifest(localDevflowDir) : null; const manifest = localManifest ?? userManifest; @@ -25,12 +67,8 @@ export const listCommand = new Command('list') if (manifest) { const installedAt = new Date(manifest.installedAt).toLocaleDateString(); const updatedAt = new Date(manifest.updatedAt).toLocaleDateString(); - const scope = localManifest ? 'local' : 'user'; - const features = [ - manifest.features.teams ? 'teams' : null, - manifest.features.ambient ? 'ambient' : null, - manifest.features.memory ? 'memory' : null, - ].filter(Boolean).join(', ') || 'none'; + const scope = resolveScope(localManifest); + const features = formatFeatures(manifest.features); p.note( `${color.dim('Version:')} ${color.cyan(`v${manifest.version}`)}\n` + @@ -43,13 +81,15 @@ export const listCommand = new Command('list') } const installedPlugins = new Set(manifest?.plugins ?? []); + const hasManifest = manifest !== null; const maxNameLen = Math.max(...DEVFLOW_PLUGINS.map(p => p.name.length)); const pluginList = DEVFLOW_PLUGINS .map(plugin => { - const cmds = plugin.commands.length > 0 ? plugin.commands.join(', ') : '(skills only)'; + const cmds = formatPluginCommands(plugin.commands); const optionalTag = plugin.optional ? color.dim(' (optional)') : ''; - const installedTag = manifest - ? (installedPlugins.has(plugin.name) ? color.green(' ✓') : color.dim(' ✗')) + const status = getPluginInstallStatus(plugin.name, installedPlugins, hasManifest); + const installedTag = status === 'installed' ? color.green(' ✓') + : status === 'not_installed' ? color.dim(' ✗') : ''; return `${color.cyan(plugin.name.padEnd(maxNameLen + 2))}${color.dim(plugin.description)}${optionalTag}${installedTag}\n${' '.repeat(maxNameLen + 2)}${color.yellow(cmds)}`; }) diff --git a/tests/list-logic.test.ts b/tests/list-logic.test.ts new file mode 100644 index 0000000..8ddf46f --- /dev/null +++ b/tests/list-logic.test.ts @@ -0,0 +1,87 @@ +import { describe, it, expect } from 'vitest'; +import { + formatFeatures, + resolveScope, + getPluginInstallStatus, + formatPluginCommands, +} from '../src/cli/commands/list.js'; +import type { ManifestData } from '../src/cli/utils/manifest.js'; + +describe('formatFeatures', () => { + it('returns all enabled features comma-separated', () => { + const features: ManifestData['features'] = { teams: true, ambient: true, memory: true }; + expect(formatFeatures(features)).toBe('teams, ambient, memory'); + }); + + it('returns subset of enabled features', () => { + const features: ManifestData['features'] = { teams: false, ambient: true, memory: true }; + expect(formatFeatures(features)).toBe('ambient, memory'); + }); + + it('returns single enabled feature', () => { + const features: ManifestData['features'] = { teams: true, ambient: false, memory: false }; + expect(formatFeatures(features)).toBe('teams'); + }); + + it('returns "none" when no features are enabled', () => { + const features: ManifestData['features'] = { teams: false, ambient: false, memory: false }; + expect(formatFeatures(features)).toBe('none'); + }); + + it('preserves feature order: teams, ambient, memory', () => { + const features: ManifestData['features'] = { teams: true, ambient: false, memory: true }; + expect(formatFeatures(features)).toBe('teams, memory'); + }); +}); + +describe('resolveScope', () => { + it('returns "local" when local manifest exists', () => { + const localManifest: ManifestData = { + version: '1.0.0', + plugins: [], + scope: 'local', + features: { teams: false, ambient: false, memory: false }, + installedAt: '2026-01-01T00:00:00.000Z', + updatedAt: '2026-01-01T00:00:00.000Z', + }; + expect(resolveScope(localManifest)).toBe('local'); + }); + + it('returns "user" when local manifest is null', () => { + expect(resolveScope(null)).toBe('user'); + }); +}); + +describe('getPluginInstallStatus', () => { + const installedPlugins = new Set(['devflow-core-skills', 'devflow-implement']); + + it('returns "installed" for a plugin in the installed set', () => { + expect(getPluginInstallStatus('devflow-core-skills', installedPlugins, true)).toBe('installed'); + }); + + it('returns "not_installed" for a plugin not in the installed set', () => { + expect(getPluginInstallStatus('devflow-debug', installedPlugins, true)).toBe('not_installed'); + }); + + it('returns "unknown" when no manifest exists', () => { + expect(getPluginInstallStatus('devflow-core-skills', installedPlugins, false)).toBe('unknown'); + }); + + it('returns "unknown" when no manifest, even with empty set', () => { + expect(getPluginInstallStatus('devflow-implement', new Set(), false)).toBe('unknown'); + }); +}); + +describe('formatPluginCommands', () => { + it('returns comma-separated commands', () => { + expect(formatPluginCommands(['/implement'])).toBe('/implement'); + }); + + it('joins multiple commands with comma', () => { + expect(formatPluginCommands(['/code-review', '/resolve'])).toBe('/code-review, /resolve'); + }); + + it('returns "(skills only)" for empty commands array', () => { + expect(formatPluginCommands([])).toBe('(skills only)'); + }); +}); From 59c77ffb49a29e7a1347fa11df191e7f255e6174 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sat, 14 Mar 2026 00:07:17 +0200 Subject: [PATCH 10/12] fix(init): add error handling for manifest write and extract resolvePluginList - Wrap writeManifest call in try/catch so a permissions or disk error after a successful install emits a non-fatal warning instead of crashing the CLI - Use resolvePluginList to encapsulate partial-vs-full install merge decision, replacing inline conditional - Add 5 tests for resolvePluginList covering full install, partial install merge, deduplication, and missing manifest edge cases Co-Authored-By: Claude --- src/cli/commands/init.ts | 14 +++++----- tests/manifest.test.ts | 58 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/src/cli/commands/init.ts b/src/cli/commands/init.ts index aac3a00..1a99b3e 100644 --- a/src/cli/commands/init.ts +++ b/src/cli/commands/init.ts @@ -24,7 +24,7 @@ import { detectPlatform, detectShell, getProfilePath, getSafeDeleteInfo, hasSafe import { generateSafeDeleteBlock, isAlreadyInstalled, installToProfile, removeFromProfile, getInstalledVersion, SAFE_DELETE_BLOCK_VERSION } from '../utils/safe-delete-install.js'; import { addAmbientHook, removeAmbientHook, hasAmbientHook } from './ambient.js'; import { addMemoryHooks, removeMemoryHooks, hasMemoryHooks } from './memory.js'; -import { readManifest, writeManifest, mergeManifestPlugins, detectUpgrade } from '../utils/manifest.js'; +import { readManifest, writeManifest, mergeManifestPlugins, resolvePluginList, detectUpgrade } from '../utils/manifest.js'; // Re-export pure functions for tests (canonical source is post-install.ts) export { substituteSettingsTemplate, computeGitignoreAppend, applyTeamsConfig, stripTeamsConfig, mergeDenyList } from '../utils/post-install.js'; @@ -608,20 +608,22 @@ export const initCommand = new Command('init') p.log.info(`Deduplication: ${agentsMap.size} unique agents (from ${totalAgentDeclarations} declarations)`); } - // Write installation manifest for upgrade tracking + // Write installation manifest for upgrade tracking (non-fatal — install already succeeded) const installedPluginNames = pluginsToInstall.map(pl => pl.name); const now = new Date().toISOString(); const manifestData = { version, - plugins: existingManifest && options.plugin - ? mergeManifestPlugins(existingManifest.plugins, installedPluginNames) - : installedPluginNames, + plugins: resolvePluginList(installedPluginNames, existingManifest, !!options.plugin), scope, features: { teams: teamsEnabled, ambient: ambientEnabled, memory: memoryEnabled }, installedAt: existingManifest?.installedAt ?? now, updatedAt: now, }; - await writeManifest(devflowDir, manifestData); + try { + await writeManifest(devflowDir, manifestData); + } catch (error) { + p.log.warn(`Failed to write installation manifest (install succeeded): ${error instanceof Error ? error.message : error}`); + } p.outro(color.green('Ready! Run any command in Claude Code to get started.')); }); diff --git a/tests/manifest.test.ts b/tests/manifest.test.ts index 49c98da..36c5a66 100644 --- a/tests/manifest.test.ts +++ b/tests/manifest.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { promises as fs } from 'fs'; import * as path from 'path'; import * as os from 'os'; -import { readManifest, writeManifest, mergeManifestPlugins, detectUpgrade, type ManifestData } from '../src/cli/utils/manifest.js'; +import { readManifest, writeManifest, mergeManifestPlugins, resolvePluginList, detectUpgrade, type ManifestData } from '../src/cli/utils/manifest.js'; describe('readManifest', () => { let tmpDir: string; @@ -224,3 +224,59 @@ describe('detectUpgrade', () => { expect(result.previousVersion).toBe('1.4.0'); }); }); + +describe('resolvePluginList', () => { + const existingManifest: ManifestData = { + version: '1.0.0', + plugins: ['devflow-core-skills', 'devflow-implement'], + scope: 'user', + features: { teams: false, ambient: true, memory: true }, + installedAt: '2026-01-01T00:00:00.000Z', + updatedAt: '2026-01-01T00:00:00.000Z', + }; + + it('replaces plugin list on full install (no existing manifest)', () => { + const result = resolvePluginList( + ['devflow-core-skills', 'devflow-code-review'], + null, + false, + ); + expect(result).toEqual(['devflow-core-skills', 'devflow-code-review']); + }); + + it('replaces plugin list on full install (existing manifest present)', () => { + const result = resolvePluginList( + ['devflow-core-skills', 'devflow-code-review'], + existingManifest, + false, + ); + expect(result).toEqual(['devflow-core-skills', 'devflow-code-review']); + }); + + it('merges plugins on partial install with existing manifest', () => { + const result = resolvePluginList( + ['devflow-code-review', 'devflow-debug'], + existingManifest, + true, + ); + expect(result).toEqual(['devflow-core-skills', 'devflow-implement', 'devflow-code-review', 'devflow-debug']); + }); + + it('does not duplicate plugins on partial install merge', () => { + const result = resolvePluginList( + ['devflow-implement', 'devflow-code-review'], + existingManifest, + true, + ); + expect(result).toEqual(['devflow-core-skills', 'devflow-implement', 'devflow-code-review']); + }); + + it('replaces plugin list on partial install without existing manifest', () => { + const result = resolvePluginList( + ['devflow-code-review'], + null, + true, + ); + expect(result).toEqual(['devflow-code-review']); + }); +}); From 71e48a1f01a67111399836825fdb96c6f40c91c9 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sat, 14 Mar 2026 00:09:06 +0200 Subject: [PATCH 11/12] test(manifest): add v-prefix semver and edge case coverage Add missing test coverage for manifest utilities: - v-prefixed version strings in compareSemver (via detectUpgrade) - Unparseable installed version in detectUpgrade (symmetric case) - mergeManifestPlugins with empty array boundaries Co-Authored-By: Claude --- tests/manifest.test.ts | 50 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/manifest.test.ts b/tests/manifest.test.ts index 36c5a66..ce41039 100644 --- a/tests/manifest.test.ts +++ b/tests/manifest.test.ts @@ -170,6 +170,21 @@ describe('mergeManifestPlugins', () => { ); expect(result).toEqual(['b', 'a', 'c']); }); + + it('returns empty array when both inputs are empty', () => { + const result = mergeManifestPlugins([], []); + expect(result).toEqual([]); + }); + + it('returns new plugins when existing is empty', () => { + const result = mergeManifestPlugins([], ['devflow-core-skills', 'devflow-debug']); + expect(result).toEqual(['devflow-core-skills', 'devflow-debug']); + }); + + it('returns existing plugins when new is empty', () => { + const result = mergeManifestPlugins(['devflow-core-skills', 'devflow-debug'], []); + expect(result).toEqual(['devflow-core-skills', 'devflow-debug']); + }); }); describe('detectUpgrade', () => { @@ -223,6 +238,41 @@ describe('detectUpgrade', () => { expect(result.isSameVersion).toBe(false); expect(result.previousVersion).toBe('1.4.0'); }); + + it('handles unparseable installed version gracefully', () => { + const result = detectUpgrade('1.4.0', 'garbage'); + expect(result.isUpgrade).toBe(false); + expect(result.isDowngrade).toBe(false); + expect(result.isSameVersion).toBe(false); + expect(result.previousVersion).toBe('garbage'); + }); + + it('detects upgrade with v-prefixed current version', () => { + const result = detectUpgrade('v2.0.0', '1.4.0'); + expect(result.isUpgrade).toBe(true); + expect(result.isDowngrade).toBe(false); + expect(result.previousVersion).toBe('1.4.0'); + }); + + it('detects upgrade with v-prefixed installed version', () => { + const result = detectUpgrade('2.0.0', 'v1.4.0'); + expect(result.isUpgrade).toBe(true); + expect(result.isDowngrade).toBe(false); + expect(result.previousVersion).toBe('v1.4.0'); + }); + + it('detects same version with both v-prefixed', () => { + const result = detectUpgrade('v1.4.0', 'v1.4.0'); + expect(result.isSameVersion).toBe(true); + expect(result.isUpgrade).toBe(false); + expect(result.isDowngrade).toBe(false); + }); + + it('detects downgrade with v-prefixed versions', () => { + const result = detectUpgrade('v1.0.0', 'v1.4.0'); + expect(result.isDowngrade).toBe(true); + expect(result.isUpgrade).toBe(false); + }); }); describe('resolvePluginList', () => { From f819ba0e45dc2224cf305e87d2f5f6a0c3d8230c Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sat, 14 Mar 2026 00:12:55 +0200 Subject: [PATCH 12/12] refactor: simplify resolution fixes Remove dead mergeManifestPlugins import from init.ts (now called internally by resolvePluginList in manifest.ts) and align resolveScope return type order to 'user' | 'local' matching codebase convention. --- src/cli/commands/init.ts | 2 +- src/cli/commands/list.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/commands/init.ts b/src/cli/commands/init.ts index 1a99b3e..54eecf2 100644 --- a/src/cli/commands/init.ts +++ b/src/cli/commands/init.ts @@ -24,7 +24,7 @@ import { detectPlatform, detectShell, getProfilePath, getSafeDeleteInfo, hasSafe import { generateSafeDeleteBlock, isAlreadyInstalled, installToProfile, removeFromProfile, getInstalledVersion, SAFE_DELETE_BLOCK_VERSION } from '../utils/safe-delete-install.js'; import { addAmbientHook, removeAmbientHook, hasAmbientHook } from './ambient.js'; import { addMemoryHooks, removeMemoryHooks, hasMemoryHooks } from './memory.js'; -import { readManifest, writeManifest, mergeManifestPlugins, resolvePluginList, detectUpgrade } from '../utils/manifest.js'; +import { readManifest, writeManifest, resolvePluginList, detectUpgrade } from '../utils/manifest.js'; // Re-export pure functions for tests (canonical source is post-install.ts) export { substituteSettingsTemplate, computeGitignoreAppend, applyTeamsConfig, stripTeamsConfig, mergeDenyList } from '../utils/post-install.js'; diff --git a/src/cli/commands/list.ts b/src/cli/commands/list.ts index 71e57d6..6b5c97a 100644 --- a/src/cli/commands/list.ts +++ b/src/cli/commands/list.ts @@ -23,7 +23,7 @@ export function formatFeatures(features: ManifestData['features']): string { * Determine effective installation scope based on which manifest was found. * Local scope takes precedence when a local manifest exists. */ -export function resolveScope(localManifest: ManifestData | null): 'local' | 'user' { +export function resolveScope(localManifest: ManifestData | null): 'user' | 'local' { return localManifest ? 'local' : 'user'; }