From cb04031d8ac58c5a388a4f21003ad77d9e6b387c Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Thu, 23 Apr 2026 17:30:21 +0300 Subject: [PATCH 01/44] feat(feature-kb): add per-feature knowledge base foundation layer Runtime module, two skills, one agent, CLI command, plugin registration, init integration, and tests for the per-feature knowledge base system. - scripts/hooks/lib/feature-kb.cjs: CJS runtime for KB CRUD, staleness detection via git log, mkdir-based lock protocol, and CLI interface - shared/skills/feature-kb/SKILL.md: 4-phase KB creation skill (Scan, Extract, Distill, Forge) with category templates and quality checks - shared/skills/apply-feature-kb/SKILL.md: 3-step consumption algorithm for FEATURE_KNOWLEDGE variable with staleness handling and skip guard - shared/agents/kb-builder.md: Sonnet agent that structures exploration outputs into KNOWLEDGE.md files under .features/{slug}/ - src/cli/commands/kb.ts: devflow kb list|create|check|refresh|remove subcommands with @clack/prompts TUI and claude -p integration - Plugin registration: feature-kb + apply-feature-kb skills added to core-skills, plan, ambient; apply-feature-kb to implement, code-review, resolve, debug, self-review; kb-builder agent to plan and ambient - Init integration: creates .features/index.json on local-scope installs; adds .features/.kb.lock to .gitignore entries - 41 new tests across 4 test files (all 1108 tests pass) - Security: execFileSync with array args for git commands (fixes CWE-78) Co-Authored-By: Claude --- CLAUDE.md | 7 +- .../.claude-plugin/plugin.json | 7 +- .../.claude-plugin/plugin.json | 3 +- .../.claude-plugin/plugin.json | 2 + .../devflow-debug/.claude-plugin/plugin.json | 3 +- .../.claude-plugin/plugin.json | 3 +- .../devflow-plan/.claude-plugin/plugin.json | 7 +- .../.claude-plugin/plugin.json | 3 +- .../.claude-plugin/plugin.json | 3 +- scripts/hooks/lib/feature-kb.cjs | 434 ++++++++++++++++++ shared/agents/kb-builder.md | 57 +++ shared/skills/apply-feature-kb/SKILL.md | 71 +++ shared/skills/feature-kb/SKILL.md | 130 ++++++ src/cli/cli.ts | 2 + src/cli/commands/init.ts | 15 + src/cli/commands/kb.ts | 368 +++++++++++++++ src/cli/plugins.ts | 23 +- src/cli/utils/post-install.ts | 2 +- .../feature-kb/apply-feature-kb-skill.test.ts | 42 ++ tests/feature-kb/feature-kb.test.ts | 249 ++++++++++ tests/feature-kb/fixtures.ts | 100 ++++ tests/feature-kb/kb-builder-agent.test.ts | 31 ++ tests/feature-kb/kb-command.test.ts | 59 +++ tests/skill-references.test.ts | 1 + 24 files changed, 1601 insertions(+), 21 deletions(-) create mode 100644 scripts/hooks/lib/feature-kb.cjs create mode 100644 shared/agents/kb-builder.md create mode 100644 shared/skills/apply-feature-kb/SKILL.md create mode 100644 shared/skills/feature-kb/SKILL.md create mode 100644 src/cli/commands/kb.ts create mode 100644 tests/feature-kb/apply-feature-kb-skill.test.ts create mode 100644 tests/feature-kb/feature-kb.test.ts create mode 100644 tests/feature-kb/fixtures.ts create mode 100644 tests/feature-kb/kb-builder-agent.test.ts create mode 100644 tests/feature-kb/kb-command.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 9cfa12b..0d7ce49 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,6 +46,8 @@ Commands with Teams Variant ship as `{name}.md` (parallel subagents) and `{name} **Claude Code Flags**: Typed registry (`src/cli/utils/flags.ts`) for managing Claude Code feature flags (env vars and top-level settings). Pure functions `applyFlags`/`stripFlags`/`getDefaultFlags` follow the `applyTeamsConfig`/`stripTeamsConfig` pattern. Initial flags: `tool-search`, `lsp`, `clear-context-on-plan` (default ON), `brief`, `disable-1m-context` (default OFF). Manageable via `devflow flags --enable/--disable/--status/--list`. Stored in manifest `features.flags: string[]`. +**Feature Knowledge Bases**: Per-feature `.features/` directory containing KNOWLEDGE.md files that capture area-specific patterns, conventions, architecture, and gotchas. KBs are created as side-effects of planning (plan:orch Phase 8.5), loaded automatically across all workflows via `FEATURE_KNOWLEDGE` variable (companion to `KNOWLEDGE_CONTEXT`), and use staleness detection via git log against `referencedFiles`. Index at `.features/index.json` (object keyed by slug). Managed via `devflow kb list|create|check|refresh|remove`. KB Builder agent (sonnet) structures exploration outputs into KNOWLEDGE.md. `apply-feature-kb` skill provides consumption algorithm for agents. `.features/.kb.lock` guards concurrent index writes (gitignored). `devflow kb list` — List all feature KBs with staleness status. `devflow kb create ` — Create a new KB via claude -p exploration. `devflow kb check` — Check all KBs for staleness. `devflow kb refresh [slug]` — Refresh stale KB(s). `devflow kb remove ` — Remove a KB and its index entry. + **Two-Mode Init**: `devflow init` offers Recommended (sensible defaults, quick setup) or Advanced (full interactive flow) after plugin selection. `--recommended` / `--advanced` CLI flags for non-interactive use. Recommended applies: ambient ON, memory ON, learn ON, HUD ON, teams OFF, default-ON flags, .claudeignore ON, auto-install safe-delete if trash CLI detected, user-mode security deny list. **Migrations**: Run-once migrations execute automatically on `devflow init`, tracked at `~/.devflow/migrations.json` (scope-independent; single file regardless of user-scope vs local-scope installs). Registry: append an entry to `MIGRATIONS` in `src/cli/utils/migrations.ts`. Scopes: `global` (runs once per machine, no project context) vs `per-project` (sweeps all discovered Claude-enabled projects in parallel). Failures are non-fatal — migrations retry on next init. Currently registered per-project migrations include `purge-legacy-knowledge-v2` (removes 4 hardcoded pre-v2 ADR/PF IDs and orphan `PROJECT-PATTERNS.md`) and `purge-legacy-knowledge-v3` (v3: sweeps all remaining pre-v2 seeded entries using the `- **Source**: self-learning:` format discriminator — any ADR/PF section lacking this marker is removed; entries the user edited to include the marker survive). **D37 edge case**: a project cloned *after* migrations have run won't be swept (the marker is global, not per-project). Recovery: `rm ~/.devflow/migrations.json` forces a re-sweep on next `devflow init`. @@ -60,9 +62,10 @@ devflow/ ├── docs/reference/ # Detailed reference documentation ├── scripts/ # Helper scripts (statusline, docs-helpers) │ └── hooks/ # Working Memory + ambient + learning hooks (prompt-capture-memory, stop-update-memory, background-memory-update, session-start-memory, session-start-classification, pre-compact-memory, preamble, session-end-learning, stop-update-learning [deprecated], background-learning, get-mtime) -├── src/cli/ # TypeScript CLI (init, list, uninstall, ambient, learn, flags) +├── src/cli/ # TypeScript CLI (init, list, uninstall, ambient, learn, flags, kb) ├── .claude-plugin/ # Marketplace registry ├── .docs/ # Project docs (reviews, design) — per-project +├── .features/ # Per-feature knowledge bases (committed to git) └── .memory/ # Working memory files — per-project ``` @@ -150,7 +153,7 @@ Working memory files live in a dedicated `.memory/` directory: - `/self-review` — Simplifier then Scrutinizer (sequential); consumes knowledge via index + on-demand Read via `devflow:apply-knowledge` - `/audit-claude` — CLAUDE.md audit (optional plugin) -**Shared agents** (12): git, synthesizer, skimmer, simplifier, coder, reviewer, resolver, evaluator, tester, scrutinizer, validator, designer +**Shared agents** (13): git, synthesizer, skimmer, simplifier, coder, reviewer, resolver, evaluator, tester, scrutinizer, validator, designer, kb-builder **Plugin-specific agents** (1): claude-md-auditor diff --git a/plugins/devflow-ambient/.claude-plugin/plugin.json b/plugins/devflow-ambient/.claude-plugin/plugin.json index acd4cdf..c9492e4 100644 --- a/plugins/devflow-ambient/.claude-plugin/plugin.json +++ b/plugins/devflow-ambient/.claude-plugin/plugin.json @@ -27,7 +27,8 @@ "git", "synthesizer", "resolver", - "designer" + "designer", + "kb-builder" ], "skills": [ "router", @@ -53,6 +54,8 @@ "qa", "worktree-support", "gap-analysis", - "design-review" + "design-review", + "feature-kb", + "apply-feature-kb" ] } diff --git a/plugins/devflow-code-review/.claude-plugin/plugin.json b/plugins/devflow-code-review/.claude-plugin/plugin.json index f5e2c71..282bf24 100644 --- a/plugins/devflow-code-review/.claude-plugin/plugin.json +++ b/plugins/devflow-code-review/.claude-plugin/plugin.json @@ -33,6 +33,7 @@ "review-methodology", "security", "testing", - "worktree-support" + "worktree-support", + "apply-feature-kb" ] } diff --git a/plugins/devflow-core-skills/.claude-plugin/plugin.json b/plugins/devflow-core-skills/.claude-plugin/plugin.json index 1e1a8cf..5ce1c4b 100644 --- a/plugins/devflow-core-skills/.claude-plugin/plugin.json +++ b/plugins/devflow-core-skills/.claude-plugin/plugin.json @@ -19,6 +19,8 @@ "agents": [], "skills": [ "apply-knowledge", + "apply-feature-kb", + "feature-kb", "software-design", "docs-framework", "git", diff --git a/plugins/devflow-debug/.claude-plugin/plugin.json b/plugins/devflow-debug/.claude-plugin/plugin.json index 3daf04b..1737a67 100644 --- a/plugins/devflow-debug/.claude-plugin/plugin.json +++ b/plugins/devflow-debug/.claude-plugin/plugin.json @@ -21,6 +21,7 @@ "skills": [ "agent-teams", "git", - "worktree-support" + "worktree-support", + "apply-feature-kb" ] } diff --git a/plugins/devflow-implement/.claude-plugin/plugin.json b/plugins/devflow-implement/.claude-plugin/plugin.json index 2b1d465..0f1badb 100644 --- a/plugins/devflow-implement/.claude-plugin/plugin.json +++ b/plugins/devflow-implement/.claude-plugin/plugin.json @@ -30,6 +30,7 @@ "patterns", "qa", "quality-gates", - "worktree-support" + "worktree-support", + "apply-feature-kb" ] } diff --git a/plugins/devflow-plan/.claude-plugin/plugin.json b/plugins/devflow-plan/.claude-plugin/plugin.json index 9e443db..2374028 100644 --- a/plugins/devflow-plan/.claude-plugin/plugin.json +++ b/plugins/devflow-plan/.claude-plugin/plugin.json @@ -19,13 +19,16 @@ "git", "skimmer", "synthesizer", - "designer" + "designer", + "kb-builder" ], "skills": [ "agent-teams", "gap-analysis", "design-review", "patterns", - "worktree-support" + "worktree-support", + "feature-kb", + "apply-feature-kb" ] } diff --git a/plugins/devflow-resolve/.claude-plugin/plugin.json b/plugins/devflow-resolve/.claude-plugin/plugin.json index d90eec8..5dc311e 100644 --- a/plugins/devflow-resolve/.claude-plugin/plugin.json +++ b/plugins/devflow-resolve/.claude-plugin/plugin.json @@ -24,6 +24,7 @@ "agent-teams", "patterns", "security", - "worktree-support" + "worktree-support", + "apply-feature-kb" ] } diff --git a/plugins/devflow-self-review/.claude-plugin/plugin.json b/plugins/devflow-self-review/.claude-plugin/plugin.json index b6142f4..3eca193 100644 --- a/plugins/devflow-self-review/.claude-plugin/plugin.json +++ b/plugins/devflow-self-review/.claude-plugin/plugin.json @@ -22,6 +22,7 @@ "skills": [ "quality-gates", "software-design", - "worktree-support" + "worktree-support", + "apply-feature-kb" ] } diff --git a/scripts/hooks/lib/feature-kb.cjs b/scripts/hooks/lib/feature-kb.cjs new file mode 100644 index 0000000..48fe7e2 --- /dev/null +++ b/scripts/hooks/lib/feature-kb.cjs @@ -0,0 +1,434 @@ +// scripts/hooks/lib/feature-kb.cjs +// Runtime module for per-feature knowledge base management. +// +// DESIGN: Feature KBs live under .features/{slug}/KNOWLEDGE.md with a central +// index at .features/index.json (keyed by slug). This module is the single +// source of truth for all KB operations — loading, staleness detection, index +// mutation, and listing. A mkdir-based lock guards concurrent index writes. +// +// ARCHITECTURE EXCEPTION: This is a developer-facing CLI tool invoked exclusively +// by trusted orchestration scripts within Claude Code. The worktree path argument +// is controlled by the Claude Code session, not by end users. Path traversal +// analysis (CWE-23) flags the worktreePath→fs I/O flow as a risk, but this is +// inherent to a tool whose sole purpose is to manage files in a git worktree. +// The same pattern exists in scripts/hooks/lib/knowledge-context.cjs (accepted). +// For command execution, we use execFileSync with array args (not shell strings) +// to prevent injection attacks from index content. +// +// CLI interface (see if-require.main block at bottom): +// node feature-kb.cjs list +// node feature-kb.cjs stale [slug] +// node feature-kb.cjs update-index --slug=X --name=Y ... +// node feature-kb.cjs mark-stale [file2...] +// node feature-kb.cjs remove + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +/** + * @typedef {{ + * name: string, + * description: string, + * directories: string[], + * referencedFiles: string[], + * category: string, + * lastUpdated: string, + * createdBy: string + * }} FeatureEntry + */ + +/** + * Load and parse .features/index.json from a worktree path. + * Returns null when the file is absent or contains invalid JSON. + * + * @param {string} worktreePath + * @returns {{ version: number, features: Record } | null} + */ +function loadIndex(worktreePath) { + const indexPath = path.join(worktreePath, '.features', 'index.json'); + try { + const raw = fs.readFileSync(indexPath, 'utf8'); + return JSON.parse(raw); + } catch { + return null; + } +} + +/** + * Load KB content for a given slug. + * Returns null when the KNOWLEDGE.md file is absent. + * + * @param {string} worktreePath + * @param {string} slug + * @returns {string | null} + */ +function loadKBContent(worktreePath, slug) { + const kbPath = path.join(worktreePath, '.features', slug, 'KNOWLEDGE.md'); + try { + return fs.readFileSync(kbPath, 'utf8'); + } catch { + return null; + } +} + +/** + * Check if a KB is stale by comparing lastUpdated against git log of referencedFiles. + * Returns { stale: false } for non-git repos or when the entry is not found. + * + * @param {string} worktreePath + * @param {string} slug + * @returns {{ stale: boolean, changedFiles: string[] }} + */ +function checkStaleness(worktreePath, slug) { + const index = loadIndex(worktreePath); + if (!index || !index.features[slug]) return { stale: false, changedFiles: [] }; + + const entry = index.features[slug]; + const { execFileSync } = require('child_process'); + + try { + // Check if in git repo — use execFileSync to avoid shell injection + execFileSync('git', ['rev-parse', '--git-dir'], { cwd: worktreePath, stdio: 'pipe' }); + } catch { + return { stale: false, changedFiles: [] }; // Non-git fallback + } + + const files = entry.referencedFiles || []; + if (files.length === 0) return { stale: false, changedFiles: [] }; + + try { + // Use execFileSync with array args to prevent command injection. + // lastUpdated comes from the index (a controlled ISO timestamp), but we + // avoid string interpolation into a shell command as a defense-in-depth measure. + const result = execFileSync( + 'git', + ['log', `--after=${entry.lastUpdated}`, '--name-only', '--pretty=format:', '--', ...files], + { cwd: worktreePath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] } + ); + const changedFiles = [...new Set(result.split('\n').map(l => l.trim()).filter(Boolean))]; + return { stale: changedFiles.length > 0, changedFiles }; + } catch { + return { stale: false, changedFiles: [] }; + } +} + +/** + * Check staleness for all KBs in the index. + * + * @param {string} worktreePath + * @returns {Record} + */ +function checkAllStaleness(worktreePath) { + const index = loadIndex(worktreePath); + if (!index) return {}; + const results = {}; + for (const slug of Object.keys(index.features)) { + results[slug] = checkStaleness(worktreePath, slug); + } + return results; +} + +/** + * Acquire a mkdir-based lock. Follows the same pattern as .memory/.knowledge.lock. + * Returns true when the lock is acquired within timeoutMs, false otherwise. + * + * @param {string} lockPath + * @param {number} [timeoutMs=30000] + * @param {number} [staleMs=60000] + * @returns {boolean} + */ +function acquireLock(lockPath, timeoutMs = 30000, staleMs = 60000) { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + try { + fs.mkdirSync(lockPath); + return true; + } catch { + // Check whether the lock is stale + try { + const stat = fs.statSync(lockPath); + if (Date.now() - stat.mtimeMs > staleMs) { + try { fs.rmdirSync(lockPath); } catch { /* ignore */ } + continue; + } + } catch { + continue; // lock disappeared — retry + } + // Wait 100ms before retrying (Atomics.wait avoids shell dependency) + try { + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100); + } catch { /* Node < 16 fallback: busy-wait */ } + } + } + return false; +} + +/** + * Release a mkdir-based lock. + * + * @param {string} lockPath + */ +function releaseLock(lockPath) { + try { fs.rmdirSync(lockPath); } catch { /* ignore */ } +} + +/** + * Create or update an entry in index.json with the mkdir-based lock protocol. + * + * @param {string} worktreePath + * @param {{ + * slug: string, + * name: string, + * description?: string, + * directories: string[], + * referencedFiles: string[], + * category: string, + * createdBy?: string + * }} entry + */ +function updateIndex(worktreePath, entry) { + const featuresDir = path.join(worktreePath, '.features'); + const lockPath = path.join(featuresDir, '.kb.lock'); + const indexPath = path.join(featuresDir, 'index.json'); + + if (!acquireLock(lockPath)) { + throw new Error('Failed to acquire .features/.kb.lock within timeout'); + } + + try { + let index = { version: 1, features: {} }; + try { + index = JSON.parse(fs.readFileSync(indexPath, 'utf8')); + } catch { /* start fresh */ } + + const existing = index.features[entry.slug] || {}; + index.features[entry.slug] = { + name: entry.name, + description: entry.description !== undefined ? entry.description : (existing.description || ''), + directories: entry.directories, + referencedFiles: entry.referencedFiles, + category: entry.category, + lastUpdated: new Date().toISOString(), + createdBy: entry.createdBy || existing.createdBy || 'manual', + }; + + fs.writeFileSync(indexPath, JSON.stringify(index, null, 2) + '\n'); + } finally { + releaseLock(lockPath); + } +} + +/** + * Mark KBs as stale whose referencedFiles overlap with the given file list. + * Unlike checkStaleness (git-based), this programmatically flags overlap. + * Returns the list of slugs that have overlapping referenced files. + * + * @param {string} worktreePath + * @param {string[]} changedFiles + * @returns {string[]} slugs that have overlapping referenced files + */ +function markStale(worktreePath, changedFiles) { + const index = loadIndex(worktreePath); + if (!index) return []; + + const staleSlugsList = []; + for (const [slug, entry] of Object.entries(index.features)) { + const refs = entry.referencedFiles || []; + const overlap = refs.some(ref => + changedFiles.some(f => f === ref || f.startsWith(ref) || ref.startsWith(f)) + ); + if (overlap) staleSlugsList.push(slug); + } + return staleSlugsList; +} + +/** + * Remove a KB entry from index.json and delete its directory. + * No-op if the slug does not exist in the index. + * + * @param {string} worktreePath + * @param {string} slug + */ +function removeEntry(worktreePath, slug) { + const featuresDir = path.join(worktreePath, '.features'); + const lockPath = path.join(featuresDir, '.kb.lock'); + const indexPath = path.join(featuresDir, 'index.json'); + + if (!acquireLock(lockPath)) { + throw new Error('Failed to acquire .features/.kb.lock within timeout'); + } + + try { + let index = { version: 1, features: {} }; + try { + index = JSON.parse(fs.readFileSync(indexPath, 'utf8')); + } catch { + return; // nothing to remove + } + + delete index.features[slug]; + fs.writeFileSync(indexPath, JSON.stringify(index, null, 2) + '\n'); + + // Remove KB directory + const kbDir = path.join(featuresDir, slug); + try { + fs.rmSync(kbDir, { recursive: true, force: true }); + } catch { /* ignore */ } + } finally { + releaseLock(lockPath); + } +} + +/** + * List all KBs with their metadata (slug + FeatureEntry fields). + * + * @param {string} worktreePath + * @returns {Array<{ slug: string } & FeatureEntry>} + */ +function listKBs(worktreePath) { + const index = loadIndex(worktreePath); + if (!index) return []; + return Object.entries(index.features).map(([slug, entry]) => ({ slug, ...entry })); +} + +// --------------------------------------------------------------------------- +// CLI interface +// +// Usage: +// node feature-kb.cjs list +// node feature-kb.cjs stale [slug] +// node feature-kb.cjs update-index --slug=X --name=Y --directories='[...]' --referencedFiles='[...]' --category=X [--description=Y] [--createdBy=Z] +// node feature-kb.cjs mark-stale [file2...] +// node feature-kb.cjs remove +// --------------------------------------------------------------------------- + +if (require.main === module) { + const argv = process.argv.slice(2); + const subcommand = argv[0]; + + /** + * Parse --key=value style arguments from an argv array. + * @param {string[]} args + * @returns {Record} + */ + function parseKeyValue(args) { + const result = {}; + for (const arg of args) { + const m = arg.match(/^--([^=]+)=(.*)$/s); + if (m) result[m[1]] = m[2]; + } + return result; + } + + const USAGE = [ + 'Usage:', + ' node feature-kb.cjs list ', + ' node feature-kb.cjs stale [slug]', + ' node feature-kb.cjs update-index --slug=X --name=Y --directories=\'[...]\' --referencedFiles=\'[...]\' --category=X [--description=Y] [--createdBy=Z]', + ' node feature-kb.cjs mark-stale [file2...]', + ' node feature-kb.cjs remove ', + ].join('\n'); + + if (!subcommand) { + process.stderr.write(USAGE + '\n'); + process.exit(1); + } + + if (subcommand === 'list') { + const worktreePath = argv[1] ? path.resolve(argv[1]) : null; + if (!worktreePath) { + process.stderr.write('Error: missing worktree argument\n' + USAGE + '\n'); + process.exit(1); + } + const entries = listKBs(worktreePath); + process.stderr.write(`[feature-kb] mode=list worktree=${worktreePath} count=${entries.length}\n`); + process.stdout.write(JSON.stringify(entries, null, 2) + '\n'); + process.exit(0); + } + + if (subcommand === 'stale') { + const worktreePath = argv[1] ? path.resolve(argv[1]) : null; + if (!worktreePath) { + process.stderr.write('Error: missing worktree argument\n' + USAGE + '\n'); + process.exit(1); + } + const slug = argv[2]; + if (slug) { + const result = checkStaleness(worktreePath, slug); + process.stderr.write(`[feature-kb] mode=stale worktree=${worktreePath} slug=${slug} stale=${result.stale}\n`); + process.stdout.write(JSON.stringify(result, null, 2) + '\n'); + } else { + const result = checkAllStaleness(worktreePath); + process.stderr.write(`[feature-kb] mode=stale worktree=${worktreePath} all=true\n`); + process.stdout.write(JSON.stringify(result, null, 2) + '\n'); + } + process.exit(0); + } + + if (subcommand === 'update-index') { + const worktreePath = argv[1] ? path.resolve(argv[1]) : null; + if (!worktreePath) { + process.stderr.write('Error: missing worktree argument\n' + USAGE + '\n'); + process.exit(1); + } + const kv = parseKeyValue(argv.slice(2)); + if (!kv.slug || !kv.name || !kv.directories || !kv.referencedFiles || !kv.category) { + process.stderr.write('Error: missing required flags (slug, name, directories, referencedFiles, category)\n' + USAGE + '\n'); + process.exit(1); + } + let directories; + let referencedFiles; + try { + directories = JSON.parse(kv.directories); + referencedFiles = JSON.parse(kv.referencedFiles); + } catch (e) { + process.stderr.write(`Error: --directories and --referencedFiles must be valid JSON arrays: ${e.message}\n`); + process.exit(1); + } + updateIndex(worktreePath, { + slug: kv.slug, + name: kv.name, + description: kv.description, + directories, + referencedFiles, + category: kv.category, + createdBy: kv.createdBy, + }); + process.stderr.write(`[feature-kb] mode=update-index worktree=${worktreePath} slug=${kv.slug}\n`); + process.stdout.write(JSON.stringify({ ok: true, slug: kv.slug }) + '\n'); + process.exit(0); + } + + if (subcommand === 'mark-stale') { + const worktreePath = argv[1] ? path.resolve(argv[1]) : null; + if (!worktreePath) { + process.stderr.write('Error: missing worktree argument\n' + USAGE + '\n'); + process.exit(1); + } + const changedFiles = argv.slice(2); + const stale = markStale(worktreePath, changedFiles); + process.stderr.write(`[feature-kb] mode=mark-stale worktree=${worktreePath} staleCount=${stale.length}\n`); + process.stdout.write(JSON.stringify(stale, null, 2) + '\n'); + process.exit(0); + } + + if (subcommand === 'remove') { + const worktreePath = argv[1] ? path.resolve(argv[1]) : null; + const slug = argv[2]; + if (!worktreePath || !slug) { + process.stderr.write('Error: missing worktree or slug argument\n' + USAGE + '\n'); + process.exit(1); + } + removeEntry(worktreePath, slug); + process.stderr.write(`[feature-kb] mode=remove worktree=${worktreePath} slug=${slug}\n`); + process.stdout.write(JSON.stringify({ ok: true, slug }) + '\n'); + process.exit(0); + } + + process.stderr.write(`Error: unknown subcommand '${subcommand}'\n` + USAGE + '\n'); + process.exit(1); +} + +module.exports = { loadIndex, loadKBContent, checkStaleness, checkAllStaleness, updateIndex, markStale, removeEntry, listKBs }; diff --git a/shared/agents/kb-builder.md b/shared/agents/kb-builder.md new file mode 100644 index 0000000..25d480f --- /dev/null +++ b/shared/agents/kb-builder.md @@ -0,0 +1,57 @@ +--- +name: KB Builder +description: Structures codebase exploration into a feature knowledge base +model: sonnet +skills: + - devflow:feature-kb + - devflow:worktree-support +tools: + - Read + - Grep + - Glob + - Bash + - Write +--- + +# KB Builder Agent + +## Input Context + +- **FEATURE_SLUG** (required): Kebab-case identifier for the feature area (e.g., `cli-commands`) +- **FEATURE_NAME** (required): Human-readable name (e.g., "CLI Command System") +- **EXPLORATION_OUTPUTS** (required): Combined findings from Skimmer + Explore agents +- **DIRECTORIES** (required): Directory prefixes defining the feature area scope +- **KNOWLEDGE_CONTEXT** (optional): Existing ADR/PF index for cross-referencing +- **EXISTING_KB** (optional): Current KNOWLEDGE.md content when refreshing a stale KB +- **CHANGED_FILES** (optional): Files that changed since last KB update (for refresh) +- **WORKTREE_PATH** (optional): Worktree root for path resolution + +## Responsibilities + +1. **Resolve worktree path**: Use `devflow:worktree-support` to determine the working directory +2. **Orient on feature area**: Read EXPLORATION_OUTPUTS to understand the feature's architecture, patterns, and boundaries +3. **Follow the feature-kb skill**: Execute the 4-phase process (Scan → Extract → Distill → Forge) from `devflow:feature-kb` +4. **Cross-reference knowledge**: If KNOWLEDGE_CONTEXT is provided, reference relevant ADR/PF entries in the KB's "Related" section +5. **Handle refresh**: If EXISTING_KB is provided, update stale sections based on CHANGED_FILES while preserving any manually added content (user edits). Don't regenerate from scratch. +6. **Write KNOWLEDGE.md**: Write to `.features/{FEATURE_SLUG}/KNOWLEDGE.md` (create directory if needed) +7. **Update index**: Run `node scripts/hooks/lib/feature-kb.cjs update-index` with all required fields +8. **Report**: Output what was created/updated + +## Output + +``` +KB_STATUS: created | refreshed +KB_PATH: .features/{slug}/KNOWLEDGE.md +KB_SLUG: {slug} +KB_NAME: {name} +SECTIONS: [list of sections written] +REFERENCED_FILES: [files selected for staleness tracking] +CROSS_REFERENCES: [ADR/PF entries referenced, if any] +``` + +## Boundaries + +- **Only writes to `.features/` directory** — never modify source code +- **Never delete existing KBs** — only create new or refresh existing +- **500-line cap** — if KB exceeds 500 lines, split into focused sub-KBs (each gets own index entry) +- **No push, no external API calls** — local filesystem operations only diff --git a/shared/skills/apply-feature-kb/SKILL.md b/shared/skills/apply-feature-kb/SKILL.md new file mode 100644 index 0000000..291d9f3 --- /dev/null +++ b/shared/skills/apply-feature-kb/SKILL.md @@ -0,0 +1,71 @@ +--- +name: apply-feature-kb +description: Consumption algorithm for FEATURE_KNOWLEDGE variable — pre-computed feature context +trigger: agent-loaded +allowed-tools: + - Read + - Grep + - Glob +--- + +# Apply Feature Knowledge + +## Iron Law + +> **Pre-computed context, not a cage. Verify against current code when assumptions seem outdated.** +> +> A feature KB captures patterns AS THEY WERE when last updated. Code evolves. +> Use the KB as a starting point, not gospel truth. + +--- + +## 3-Step Algorithm + +### Step 1: Read the KB + +When `FEATURE_KNOWLEDGE` is provided and is not `(none)`: + +1. Read each KB section (separated by `--- Feature KB: {slug} ---` headers) +2. Absorb: architecture, data flow, key patterns, anti-patterns, gotchas +3. Note integration points that relate to your current task + +### Step 2: Apply to Current Task + +1. **Patterns as defaults**: Follow documented patterns unless you have a specific reason not to +2. **Anti-patterns as warnings**: Check your work against documented anti-patterns +3. **Gotchas as checklists**: Verify each gotcha doesn't apply to your changes +4. **Integration points**: Ensure your changes respect documented boundaries +5. **Key files**: Use as starting points for exploration + +### Step 3: Supplement as Needed + +The KB may not cover everything: +- If the KB doesn't address your specific area, explore further +- If the KB seems outdated (marked `[STALE]`), verify against current code +- If you discover new patterns, note them — they may become KB updates + +--- + +## Skip Guard + +When `FEATURE_KNOWLEDGE` is `(none)`, empty, or not provided — skip this skill entirely. +Do not mention feature knowledge or its absence in your output. + +## Staleness Handling + +KBs marked with `[STALE — referenced files changed since last update. Verify against current code.]`: +- Treat as **lower-confidence** context +- Verify key assertions against current code before relying on them +- Don't assume anti-patterns or gotchas are still valid +- Still use as a starting point — stale context is better than no context + +## Concatenation Format + +Multiple KBs are concatenated with slug headers: +``` +--- Feature KB: payments --- +[full KNOWLEDGE.md content] + +--- Feature KB: auth --- +[full KNOWLEDGE.md content] +``` diff --git a/shared/skills/feature-kb/SKILL.md b/shared/skills/feature-kb/SKILL.md new file mode 100644 index 0000000..e2b3c33 --- /dev/null +++ b/shared/skills/feature-kb/SKILL.md @@ -0,0 +1,130 @@ +--- +name: feature-kb +description: Structures codebase exploration into a feature knowledge base +trigger: agent-loaded +allowed-tools: + - Read + - Grep + - Glob + - Bash + - Write +--- + +# Feature Knowledge Base Creation + +## Iron Law + +> **Knowledge that can't be derived from reading one file — capture cross-cutting understanding.** +> +> A KB exists to save the NEXT agent from rediscovering patterns that span multiple files, +> modules, or layers. If it's obvious from a single file read, don't capture it. + +--- + +## Four-Phase Process + +### Phase 1: Scan + +Identify the feature area boundaries: +- Directory prefixes (e.g., `src/payments/`, `src/auth/`) +- Key entry points and exports +- Configuration and wiring files +- Test directories + +### Phase 2: Extract + +For each key file, extract: +- **Architecture**: Module boundaries, dependency graph, data flow +- **Conventions**: Naming patterns, file organization, API style +- **Component Patterns**: Reusable structures, composition patterns +- **Domain Knowledge**: Business rules, invariants, edge cases +- **Integration Points**: How this area connects to other areas + +### Phase 3: Distill + +Compress findings into actionable knowledge: +- Remove obvious/derivable information +- Highlight non-obvious patterns and gotchas +- Cross-reference with ADR/PF entries where relevant +- Identify anti-patterns specific to this area + +### Phase 4: Forge + +Write the KNOWLEDGE.md file with this structure: + +```markdown +--- +feature: {slug} +name: {human-readable name} +category: {architecture|conventions|component-patterns|domain-knowledge|lessons-learned} +directories: [{dir prefixes}] +referencedFiles: [{key files for staleness tracking}] +created: {ISO date} +updated: {ISO date} +--- + +# {Feature Area Name} + +## Overview +[2-3 sentence summary of what this area does and why it exists] + +## Architecture +[Module boundaries, key abstractions, dependency direction] + +## Data Flow +[How data moves through this area — inputs, transformations, outputs] + +## Key Patterns +[Patterns unique to this area that agents should follow] + +## Integration Points +[How this area connects to other areas of the codebase] + +## Anti-Patterns +[Things that look right but are wrong in this specific area] + +## Gotchas +[Non-obvious behaviors, edge cases, things that break silently] + +## Key Files +[Most important files with one-line descriptions] + +## Related +[Links to ADR/PF entries, other KBs, external docs] +``` + +--- + +## Category Templates + +| Category | Focus | Example | +|----------|-------|---------| +| `architecture` | Module boundaries, dependency graph | "Payment Processing Architecture" | +| `conventions` | Naming, file org, API style | "CLI Command Conventions" | +| `component-patterns` | Reusable structures, composition | "React Form Patterns" | +| `domain-knowledge` | Business rules, invariants | "Billing Domain Rules" | +| `lessons-learned` | Post-incident, migration lessons | "Auth Migration Lessons" | + +## Quality Self-Checks + +| Red Flag | Fix | +|----------|-----| +| KB > 500 lines | Split into focused sub-areas | +| Restates what's obvious from one file | Remove — KB is for cross-cutting knowledge | +| No anti-patterns section | Every area has gotchas — dig deeper | +| No integration points | How does this connect to rest of system? | +| Broad directories (e.g., `src/`) | Focus on specific subdirectories | +| No referenced files for staleness | Pick 5-10 key files that signal changes | + +## Integration + +After writing KNOWLEDGE.md, update the index: +```bash +node scripts/hooks/lib/feature-kb.cjs update-index "{worktree}" \ + --slug="{slug}" --name="{name}" \ + --directories='["{dir1}", "{dir2}"]' \ + --referencedFiles='["{file1}", "{file2}"]' \ + --category="{category}" \ + --description="Use when {trigger description}" \ + --createdBy="{source}" +``` diff --git a/src/cli/cli.ts b/src/cli/cli.ts index a704d40..7ec1596 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -13,6 +13,7 @@ import { skillsCommand } from './commands/skills.js'; import { hudCommand } from './commands/hud.js'; import { learnCommand } from './commands/learn.js'; import { flagsCommand } from './commands/flags.js'; +import { kbCommand } from './commands/kb.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -41,6 +42,7 @@ program.addCommand(skillsCommand); program.addCommand(hudCommand); program.addCommand(learnCommand); program.addCommand(flagsCommand); +program.addCommand(kbCommand); // Handle no command program.action(() => { diff --git a/src/cli/commands/init.ts b/src/cli/commands/init.ts index e98e3de..4cd7728 100644 --- a/src/cli/commands/init.ts +++ b/src/cli/commands/init.ts @@ -945,6 +945,21 @@ export const initCommand = new Command('init') await migrateMemoryFiles(verbose); } + // Create .features/ directory with empty index (feature knowledge bases) + if (scope === 'local' && gitRoot) { + const featuresDir = path.join(gitRoot, '.features'); + await fs.mkdir(featuresDir, { recursive: true }); + const featuresIndexPath = path.join(featuresDir, 'index.json'); + try { + await fs.access(featuresIndexPath); + } catch { + await fs.writeFile(featuresIndexPath, JSON.stringify({ version: 1, features: {} }, null, 2) + '\n'); + if (verbose) { + p.log.success('.features/index.json created'); + } + } + } + // Configure HUD const existingHud = loadHudConfig(); saveHudConfig({ enabled: hudEnabled, detail: existingHud.detail }); diff --git a/src/cli/commands/kb.ts b/src/cli/commands/kb.ts new file mode 100644 index 0000000..43cc8ef --- /dev/null +++ b/src/cli/commands/kb.ts @@ -0,0 +1,368 @@ +import { Command } from 'commander'; +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { execFileSync, execSync } from 'child_process'; +import * as p from '@clack/prompts'; +import color from 'picocolors'; +import { createRequire } from 'module'; +import { fileURLToPath } from 'url'; +import { isClaudeCliAvailable } from '../utils/cli.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +/** @internal */ +const _require = createRequire(import.meta.url); + +/** + * Resolve the path to feature-kb.cjs relative to this file's dist location. + * In the build, cli.js lands in dist/cli/, so we go up two levels to reach + * the project root (where scripts/ lives). + */ +function getFeatureKbPath(): string { + // dist/cli/commands/kb.js → ../../.. → project root + return path.join(__dirname, '..', '..', '..', 'scripts', 'hooks', 'lib', 'feature-kb.cjs'); +} + +/** + * Load the feature-kb CJS module functions. + */ +function loadFeatureKb(): { + listKBs: (worktreePath: string) => Array<{ slug: string; name: string; category: string; directories: string[]; lastUpdated: string }>; + checkAllStaleness: (worktreePath: string) => Record; + checkStaleness: (worktreePath: string, slug: string) => { stale: boolean; changedFiles: string[] }; + removeEntry: (worktreePath: string, slug: string) => void; +} { + return _require(getFeatureKbPath()); +} + +/** + * Get the git root for the current directory, or cwd if not in a git repo. + */ +function getWorktreePath(): string { + try { + const result = execSync('git rev-parse --show-toplevel', { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + }); + return result.trim(); + } catch { + return process.cwd(); + } +} + +export const kbCommand = new Command('kb') + .description('Manage per-feature knowledge bases'); + +// --------------------------------------------------------------------------- +// devflow kb list +// --------------------------------------------------------------------------- + +kbCommand + .command('list') + .description('List all feature KBs with staleness status') + .action(async () => { + p.intro(color.cyan('Feature Knowledge Bases')); + + const worktreePath = getWorktreePath(); + + let kbs: ReturnType['listKBs']>; + let staleness: Record; + + try { + const featureKb = loadFeatureKb(); + kbs = featureKb.listKBs(worktreePath); + staleness = featureKb.checkAllStaleness(worktreePath); + } catch (err) { + p.log.error(`Failed to load feature KBs: ${err instanceof Error ? err.message : String(err)}`); + process.exit(1); + } + + if (kbs.length === 0) { + p.log.info( + 'No feature KBs found. KBs are created automatically during planning, or manually via ' + + color.cyan('devflow kb create ') + '.' + ); + p.outro(''); + return; + } + + p.log.info(`Found ${kbs.length} feature KB${kbs.length === 1 ? '' : 's'} in ${color.dim(worktreePath)}`); + console.log(''); + + for (const kb of kbs) { + const staleInfo = staleness[kb.slug]; + const isStale = staleInfo?.stale ?? false; + const statusBadge = isStale + ? color.yellow('[STALE]') + : color.green('[current]'); + + console.log(` ${color.bold(kb.name)} ${statusBadge}`); + console.log(` slug: ${color.dim(kb.slug)}`); + console.log(` category: ${color.dim(kb.category)}`); + console.log(` updated: ${color.dim(kb.lastUpdated)}`); + console.log(` dirs: ${color.dim(kb.directories.join(', '))}`); + if (isStale && staleInfo.changedFiles.length > 0) { + console.log(` changed: ${color.yellow(staleInfo.changedFiles.slice(0, 3).join(', '))}${staleInfo.changedFiles.length > 3 ? ` +${staleInfo.changedFiles.length - 3} more` : ''}`); + } + console.log(''); + } + + p.outro(`Run ${color.cyan('devflow kb check')} to see staleness details`); + }); + +// --------------------------------------------------------------------------- +// devflow kb check +// --------------------------------------------------------------------------- + +kbCommand + .command('check') + .description('Check all KBs for staleness') + .action(async () => { + p.intro(color.cyan('KB Staleness Check')); + + const worktreePath = getWorktreePath(); + + let staleness: Record; + let kbs: ReturnType['listKBs']>; + + try { + const featureKb = loadFeatureKb(); + kbs = featureKb.listKBs(worktreePath); + staleness = featureKb.checkAllStaleness(worktreePath); + } catch (err) { + p.log.error(`Failed to check KBs: ${err instanceof Error ? err.message : String(err)}`); + process.exit(1); + } + + if (kbs.length === 0) { + p.log.info('No feature KBs found.'); + p.outro(''); + return; + } + + let staleCount = 0; + + for (const kb of kbs) { + const staleInfo = staleness[kb.slug]; + const isStale = staleInfo?.stale ?? false; + if (isStale) { + staleCount++; + p.log.warn(`${kb.name} (${kb.slug}) is stale`); + for (const f of staleInfo.changedFiles.slice(0, 5)) { + console.log(` ${color.yellow('•')} ${f}`); + } + if (staleInfo.changedFiles.length > 5) { + console.log(` ${color.yellow('•')} ...and ${staleInfo.changedFiles.length - 5} more`); + } + } else { + p.log.success(`${kb.name} (${kb.slug}) is current`); + } + } + + if (staleCount > 0) { + p.outro(`${staleCount} KB${staleCount === 1 ? '' : 's'} need refresh. Run: ${color.cyan('devflow kb refresh')}`); + } else { + p.outro('All KBs are current'); + } + }); + +// --------------------------------------------------------------------------- +// devflow kb create +// --------------------------------------------------------------------------- + +kbCommand + .command('create ') + .description('Create a new KB via claude -p exploration') + .action(async (slug: string) => { + p.intro(color.cyan(`Create Feature KB: ${slug}`)); + + if (!isClaudeCliAvailable()) { + p.log.error('claude CLI not found on PATH. Install Claude Code first.'); + process.exit(1); + } + + const worktreePath = getWorktreePath(); + + const name = await p.text({ + message: 'Feature name (human-readable)', + placeholder: 'e.g., CLI Command System', + validate: (v) => (v.trim().length < 3 ? 'Name must be at least 3 characters' : undefined), + }); + if (p.isCancel(name)) { p.cancel('Cancelled.'); return; } + + const directoriesRaw = await p.text({ + message: 'Directories (comma-separated, e.g., src/cli/commands/,src/cli/utils/)', + placeholder: 'src/feature/', + validate: (v) => (v.trim().length === 0 ? 'Enter at least one directory' : undefined), + }); + if (p.isCancel(directoriesRaw)) { p.cancel('Cancelled.'); return; } + + const directories = (directoriesRaw as string).split(',').map((d) => d.trim()).filter(Boolean); + const dirList = directories.map((d) => `"${d}"`).join(', '); + + const s = p.spinner(); + s.start('Running KB Builder agent...'); + + const prompt = [ + `You are the KB Builder agent. Create a feature knowledge base for the following area:`, + ``, + `FEATURE_SLUG: ${slug}`, + `FEATURE_NAME: ${name as string}`, + `DIRECTORIES: [${dirList}]`, + `WORKTREE_PATH: ${worktreePath}`, + ``, + `Follow the devflow:feature-kb skill's 4-phase process:`, + `1. Scan the directories to identify key files and entry points`, + `2. Extract architecture, conventions, patterns, integration points`, + `3. Distill into actionable cross-cutting knowledge`, + `4. Write .features/${slug}/KNOWLEDGE.md with all required sections`, + ``, + `Then run: node scripts/hooks/lib/feature-kb.cjs update-index "${worktreePath}" \\`, + ` --slug="${slug}" --name="${name as string}" \\`, + ` --directories='${JSON.stringify(directories)}' \\`, + ` --referencedFiles='[]' \\`, + ` --category="component-patterns" \\`, + ` --createdBy="devflow-kb"`, + ``, + `Create the directory if needed. Report KB_STATUS when done.`, + ].join('\n'); + + try { + execFileSync('claude', [ + '-p', prompt, + '--allowedTools', 'Read,Grep,Glob,Write,Bash', + '--dangerously-skip-permissions', + ], { + cwd: worktreePath, + stdio: 'pipe', + encoding: 'utf8', + }); + s.stop('KB created successfully'); + p.log.success(`KB written to .features/${slug}/KNOWLEDGE.md`); + } catch (err) { + s.stop('KB creation failed'); + p.log.error(`claude exited with error: ${err instanceof Error ? err.message : String(err)}`); + process.exit(1); + } + + p.outro(`Run ${color.cyan(`devflow kb list`)} to see all KBs`); + }); + +// --------------------------------------------------------------------------- +// devflow kb refresh [slug] +// --------------------------------------------------------------------------- + +kbCommand + .command('refresh [slug]') + .description('Refresh stale KB(s). Omit slug to refresh all stale KBs.') + .action(async (slug?: string) => { + p.intro(color.cyan(slug ? `Refresh KB: ${slug}` : 'Refresh Stale KBs')); + + if (!isClaudeCliAvailable()) { + p.log.error('claude CLI not found on PATH. Install Claude Code first.'); + process.exit(1); + } + + const worktreePath = getWorktreePath(); + const featureKb = loadFeatureKb(); + + // Determine which slugs to refresh + let slugsToRefresh: string[]; + if (slug) { + slugsToRefresh = [slug]; + } else { + const staleness = featureKb.checkAllStaleness(worktreePath); + slugsToRefresh = Object.entries(staleness) + .filter(([, info]) => info.stale) + .map(([s]) => s); + } + + if (slugsToRefresh.length === 0) { + p.log.success('No stale KBs found — everything is current.'); + p.outro(''); + return; + } + + p.log.info(`Refreshing ${slugsToRefresh.length} KB${slugsToRefresh.length === 1 ? '' : 's'}: ${slugsToRefresh.join(', ')}`); + + for (const kbSlug of slugsToRefresh) { + const s = p.spinner(); + s.start(`Refreshing ${kbSlug}...`); + + const staleInfo = featureKb.checkStaleness(worktreePath, kbSlug); + const kbPath = path.join(worktreePath, '.features', kbSlug, 'KNOWLEDGE.md'); + let existingContent = ''; + try { + existingContent = await fs.readFile(kbPath, 'utf8'); + } catch { /* new KB */ } + + const prompt = [ + `You are the KB Builder agent refreshing a stale feature knowledge base.`, + ``, + `FEATURE_SLUG: ${kbSlug}`, + `WORKTREE_PATH: ${worktreePath}`, + `CHANGED_FILES: ${JSON.stringify(staleInfo.changedFiles)}`, + ``, + existingContent ? `EXISTING_KB:\n${existingContent}` : '', + ``, + `Instructions:`, + `- Update the stale sections based on CHANGED_FILES`, + `- Preserve any manually added content`, + `- Do not regenerate from scratch`, + `- Write the updated KB to .features/${kbSlug}/KNOWLEDGE.md`, + `- Run update-index to refresh lastUpdated timestamp`, + ].filter(Boolean).join('\n'); + + try { + execFileSync('claude', [ + '-p', prompt, + '--allowedTools', 'Read,Grep,Glob,Write,Bash', + '--dangerously-skip-permissions', + ], { + cwd: worktreePath, + stdio: 'pipe', + encoding: 'utf8', + }); + s.stop(`${kbSlug} refreshed`); + } catch (err) { + s.stop(`${kbSlug} refresh failed`); + p.log.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + } + } + + p.outro('Refresh complete'); + }); + +// --------------------------------------------------------------------------- +// devflow kb remove +// --------------------------------------------------------------------------- + +kbCommand + .command('remove ') + .description('Remove a KB and its index entry') + .action(async (slug: string) => { + p.intro(color.cyan(`Remove KB: ${slug}`)); + + const confirmed = await p.confirm({ + message: `Remove KB '${slug}' and its KNOWLEDGE.md? This cannot be undone.`, + initialValue: false, + }); + if (p.isCancel(confirmed) || !confirmed) { + p.cancel('Removal cancelled.'); + return; + } + + const worktreePath = getWorktreePath(); + + try { + const featureKb = loadFeatureKb(); + featureKb.removeEntry(worktreePath, slug); + p.log.success(`KB '${slug}' removed`); + } catch (err) { + p.log.error(`Failed to remove KB: ${err instanceof Error ? err.message : String(err)}`); + process.exit(1); + } + + p.outro('Done'); + }); diff --git a/src/cli/plugins.ts b/src/cli/plugins.ts index 7be2872..2e0ebbf 100644 --- a/src/cli/plugins.ts +++ b/src/cli/plugins.ts @@ -47,55 +47,55 @@ export const DEVFLOW_PLUGINS: PluginDefinition[] = [ description: 'Auto-activating quality enforcement skills - foundation layer for all Devflow plugins', commands: [], agents: [], - skills: ['apply-knowledge', 'software-design', 'docs-framework', 'git', 'boundary-validation', 'research', 'test-driven-development', 'testing'], + skills: ['apply-knowledge', 'apply-feature-kb', 'feature-kb', 'software-design', 'docs-framework', 'git', 'boundary-validation', 'research', 'test-driven-development', 'testing'], }, { name: 'devflow-plan', description: 'Unified design planning with gap analysis and design review', commands: ['/plan'], - agents: ['git', 'skimmer', 'synthesizer', 'designer'], - skills: ['agent-teams', 'gap-analysis', 'design-review', 'patterns', 'worktree-support'], + agents: ['git', 'skimmer', 'synthesizer', 'designer', 'kb-builder'], + skills: ['agent-teams', 'gap-analysis', 'design-review', 'patterns', 'worktree-support', 'feature-kb', 'apply-feature-kb'], }, { name: 'devflow-implement', description: 'Complete task implementation workflow - accepts plan documents, issues, or task descriptions', commands: ['/implement'], agents: ['git', 'coder', 'simplifier', 'scrutinizer', 'evaluator', 'tester', 'validator'], - skills: ['agent-teams', 'patterns', 'qa', 'quality-gates', 'worktree-support'], + skills: ['agent-teams', 'patterns', 'qa', 'quality-gates', 'worktree-support', 'apply-feature-kb'], }, { name: 'devflow-code-review', description: 'Comprehensive code review with parallel specialized agents', commands: ['/code-review'], agents: ['git', 'reviewer', 'synthesizer'], - skills: ['agent-teams', 'architecture', 'complexity', 'consistency', 'database', 'dependencies', 'documentation', 'performance', 'regression', 'review-methodology', 'security', 'testing', 'worktree-support'], + skills: ['agent-teams', 'architecture', 'complexity', 'consistency', 'database', 'dependencies', 'documentation', 'performance', 'regression', 'review-methodology', 'security', 'testing', 'worktree-support', 'apply-feature-kb'], }, { name: 'devflow-resolve', description: 'Process and fix code review issues with risk assessment', commands: ['/resolve'], agents: ['git', 'resolver', 'simplifier'], - skills: ['agent-teams', 'patterns', 'security', 'worktree-support'], + skills: ['agent-teams', 'patterns', 'security', 'worktree-support', 'apply-feature-kb'], }, { name: 'devflow-debug', description: 'Debugging workflows with competing hypothesis investigation using agent teams', commands: ['/debug'], agents: ['git', 'synthesizer'], - skills: ['agent-teams', 'git', 'worktree-support'], + skills: ['agent-teams', 'git', 'worktree-support', 'apply-feature-kb'], }, { name: 'devflow-self-review', description: 'Self-review workflow: Simplifier + Scrutinizer for code quality', commands: ['/self-review'], agents: ['simplifier', 'scrutinizer', 'validator'], - skills: ['quality-gates', 'software-design', 'worktree-support'], + skills: ['quality-gates', 'software-design', 'worktree-support', 'apply-feature-kb'], }, { name: 'devflow-ambient', description: 'Ambient mode — intent classification with proportional agent orchestration', commands: ['/ambient'], - agents: ['coder', 'validator', 'simplifier', 'scrutinizer', 'evaluator', 'tester', 'skimmer', 'reviewer', 'git', 'synthesizer', 'resolver', 'designer'], + agents: ['coder', 'validator', 'simplifier', 'scrutinizer', 'evaluator', 'tester', 'skimmer', 'reviewer', 'git', 'synthesizer', 'resolver', 'designer', 'kb-builder'], skills: [ 'router', 'implement:orch', @@ -121,6 +121,8 @@ export const DEVFLOW_PLUGINS: PluginDefinition[] = [ 'worktree-support', 'gap-analysis', 'design-review', + 'feature-kb', + 'apply-feature-kb', ], }, { @@ -393,6 +395,9 @@ export const LEGACY_SKILL_NAMES: string[] = [ 'design-review', // v2.x knowledge index pattern: new shared skill bare name for pre-namespace installs 'apply-knowledge', + // v2.x feature knowledge bases: new skills bare names for pre-namespace installs + 'feature-kb', + 'apply-feature-kb', ]; /** diff --git a/src/cli/utils/post-install.ts b/src/cli/utils/post-install.ts index 968a50e..0175330 100644 --- a/src/cli/utils/post-install.ts +++ b/src/cli/utils/post-install.ts @@ -473,7 +473,7 @@ export async function updateGitignore( ): Promise { try { const gitignorePath = path.join(gitRoot, '.gitignore'); - const entriesToAdd = ['.claude/', '.devflow/', '.memory/', '.docs/']; + const entriesToAdd = ['.claude/', '.devflow/', '.memory/', '.docs/', '.features/.kb.lock']; let gitignoreContent = ''; try { diff --git a/tests/feature-kb/apply-feature-kb-skill.test.ts b/tests/feature-kb/apply-feature-kb-skill.test.ts new file mode 100644 index 0000000..494a791 --- /dev/null +++ b/tests/feature-kb/apply-feature-kb-skill.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'fs'; +import * as path from 'path'; + +const ROOT = path.resolve(import.meta.dirname, '../..'); + +describe('feature-kb skill', () => { + const content = readFileSync(path.join(ROOT, 'shared/skills/feature-kb/SKILL.md'), 'utf8'); + + it('has iron law', () => { expect(content).toContain('## Iron Law'); }); + it('has 4-phase process', () => { + expect(content).toContain('### Phase 1: Scan'); + expect(content).toContain('### Phase 2: Extract'); + expect(content).toContain('### Phase 3: Distill'); + expect(content).toContain('### Phase 4: Forge'); + }); + it('has category templates', () => { expect(content).toContain('## Category Templates'); }); + it('has quality self-checks', () => { expect(content).toContain('## Quality Self-Checks'); }); + it('has KB format template with required sections', () => { + expect(content).toContain('## Overview'); + expect(content).toContain('## Architecture'); + expect(content).toContain('## Data Flow'); + expect(content).toContain('## Key Patterns'); + expect(content).toContain('## Anti-Patterns'); + expect(content).toContain('## Gotchas'); + expect(content).toContain('## Key Files'); + }); +}); + +describe('apply-feature-kb skill', () => { + const content = readFileSync(path.join(ROOT, 'shared/skills/apply-feature-kb/SKILL.md'), 'utf8'); + + it('has iron law', () => { expect(content).toContain('## Iron Law'); }); + it('has 3-step algorithm', () => { + expect(content).toContain('### Step 1: Read the KB'); + expect(content).toContain('### Step 2: Apply to Current Task'); + expect(content).toContain('### Step 3: Supplement as Needed'); + }); + it('has skip guard', () => { expect(content).toContain('## Skip Guard'); }); + it('has staleness handling', () => { expect(content).toContain('## Staleness Handling'); }); + it('references (none) skip', () => { expect(content).toContain('(none)'); }); +}); diff --git a/tests/feature-kb/feature-kb.test.ts b/tests/feature-kb/feature-kb.test.ts new file mode 100644 index 0000000..9afea90 --- /dev/null +++ b/tests/feature-kb/feature-kb.test.ts @@ -0,0 +1,249 @@ +import { describe, it, expect, afterAll } from 'vitest'; +import * as path from 'path'; +import { createRequire } from 'module'; +import { writeFileSync, mkdirSync, readFileSync, existsSync } from 'fs'; +import { + SAMPLE_INDEX, + SAMPLE_KB_CONTENT, + makeTmpFeatureWorktree, + cleanupTmpFeatureWorktrees, +} from './fixtures'; + +afterAll(() => cleanupTmpFeatureWorktrees()); + +const ROOT = path.resolve(import.meta.dirname, '../..'); +const require = createRequire(import.meta.url); + +const { + loadIndex, + loadKBContent, + checkStaleness, + checkAllStaleness, + updateIndex, + markStale, + removeEntry, + listKBs, +} = require(path.join(ROOT, 'scripts/hooks/lib/feature-kb.cjs')) as { + loadIndex: (worktreePath: string) => { version: number; features: Record } | null; + loadKBContent: (worktreePath: string, slug: string) => string | null; + checkStaleness: (worktreePath: string, slug: string) => { stale: boolean; changedFiles: string[] }; + checkAllStaleness: (worktreePath: string) => Record; + updateIndex: (worktreePath: string, entry: Record) => void; + markStale: (worktreePath: string, changedFiles: string[]) => string[]; + removeEntry: (worktreePath: string, slug: string) => void; + listKBs: (worktreePath: string) => Array<{ slug: string } & Record>; +}; + +// --------------------------------------------------------------------------- +// loadIndex +// --------------------------------------------------------------------------- + +describe('loadIndex', () => { + it('returns parsed object for valid JSON', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + const result = loadIndex(tmp); + expect(result).not.toBeNull(); + expect(result!.version).toBe(1); + expect(result!.features['cli-commands']).toBeDefined(); + }); + + it('returns null for missing directory', () => { + const tmp = makeTmpFeatureWorktree(); // no index written + // Remove the .features dir to simulate completely missing + const { rmSync } = require('fs'); + rmSync(path.join(tmp, '.features'), { recursive: true, force: true }); + expect(loadIndex(tmp)).toBeNull(); + }); + + it('returns null for invalid JSON', () => { + const tmp = makeTmpFeatureWorktree(); + writeFileSync(path.join(tmp, '.features', 'index.json'), 'not-json'); + expect(loadIndex(tmp)).toBeNull(); + }); +}); + +// --------------------------------------------------------------------------- +// loadKBContent +// --------------------------------------------------------------------------- + +describe('loadKBContent', () => { + it('returns content string when KNOWLEDGE.md exists', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX, { 'cli-commands': SAMPLE_KB_CONTENT }); + const content = loadKBContent(tmp, 'cli-commands'); + expect(content).not.toBeNull(); + expect(content).toContain('# CLI Command System'); + }); + + it('returns null for missing KB', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + expect(loadKBContent(tmp, 'cli-commands')).toBeNull(); + }); +}); + +// --------------------------------------------------------------------------- +// checkStaleness +// --------------------------------------------------------------------------- + +describe('checkStaleness', () => { + it('returns stale: false when entry is not found in index', () => { + const tmp = makeTmpFeatureWorktree({ version: 1, features: {} }); + const result = checkStaleness(tmp, 'nonexistent'); + expect(result.stale).toBe(false); + expect(result.changedFiles).toEqual([]); + }); + + it('returns stale: false for non-git repos', () => { + // tmp dir has no git init, so it is a non-git directory + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + const result = checkStaleness(tmp, 'cli-commands'); + expect(result.stale).toBe(false); + expect(result.changedFiles).toEqual([]); + }); +}); + +// --------------------------------------------------------------------------- +// updateIndex +// --------------------------------------------------------------------------- + +describe('updateIndex', () => { + it('creates a new entry in an empty index', () => { + const tmp = makeTmpFeatureWorktree({ version: 1, features: {} }); + updateIndex(tmp, { + slug: 'payments', + name: 'Payment Processing', + directories: ['src/payments/'], + referencedFiles: ['src/payments/checkout.ts'], + category: 'component-patterns', + createdBy: 'test', + }); + const index = loadIndex(tmp); + expect(index!.features['payments']).toBeDefined(); + const entry = index!.features['payments'] as Record; + expect(entry.name).toBe('Payment Processing'); + expect(entry.category).toBe('component-patterns'); + }); + + it('upserts an existing entry, preserving createdBy', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + updateIndex(tmp, { + slug: 'cli-commands', + name: 'CLI Command System Updated', + directories: ['src/cli/'], + referencedFiles: ['src/cli/cli.ts'], + category: 'conventions', + }); + const index = loadIndex(tmp); + const entry = index!.features['cli-commands'] as Record; + expect(entry.name).toBe('CLI Command System Updated'); + expect(entry.category).toBe('conventions'); + // createdBy should be preserved from original + expect(entry.createdBy).toBe('plan:orch'); + }); + + it('sets lastUpdated to a current ISO timestamp', () => { + const before = new Date().toISOString(); + const tmp = makeTmpFeatureWorktree({ version: 1, features: {} }); + updateIndex(tmp, { + slug: 'test-slug', + name: 'Test', + directories: [], + referencedFiles: [], + category: 'architecture', + }); + const after = new Date().toISOString(); + const index = loadIndex(tmp); + const entry = index!.features['test-slug'] as Record; + const updated = entry.lastUpdated as string; + expect(updated >= before).toBe(true); + expect(updated <= after).toBe(true); + }); +}); + +// --------------------------------------------------------------------------- +// removeEntry +// --------------------------------------------------------------------------- + +describe('removeEntry', () => { + it('removes entry from index and deletes its directory', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX, { 'cli-commands': SAMPLE_KB_CONTENT }); + const kbDir = path.join(tmp, '.features', 'cli-commands'); + expect(existsSync(kbDir)).toBe(true); + + removeEntry(tmp, 'cli-commands'); + + const index = loadIndex(tmp); + expect(index!.features['cli-commands']).toBeUndefined(); + expect(existsSync(kbDir)).toBe(false); + }); + + it('is a no-op for a non-existent slug', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + // Should not throw + expect(() => removeEntry(tmp, 'nonexistent')).not.toThrow(); + // Original entry should still exist + const index = loadIndex(tmp); + expect(index!.features['cli-commands']).toBeDefined(); + }); +}); + +// --------------------------------------------------------------------------- +// markStale +// --------------------------------------------------------------------------- + +describe('markStale', () => { + it('identifies KBs whose referencedFiles overlap with changed files', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + const stale = markStale(tmp, ['src/cli/cli.ts', 'some/other/file.ts']); + expect(stale).toContain('cli-commands'); + }); + + it('returns empty array when no overlap', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + const stale = markStale(tmp, ['src/payments/checkout.ts', 'src/unrelated.ts']); + expect(stale).toEqual([]); + }); + + it('returns empty array for missing index', () => { + const tmp = makeTmpFeatureWorktree(); + const stale = markStale(tmp, ['src/cli/cli.ts']); + expect(stale).toEqual([]); + }); +}); + +// --------------------------------------------------------------------------- +// listKBs +// --------------------------------------------------------------------------- + +describe('listKBs', () => { + it('returns all entries with their slugs', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + const entries = listKBs(tmp); + expect(entries).toHaveLength(1); + expect(entries[0].slug).toBe('cli-commands'); + expect(entries[0].name).toBe('CLI Command System'); + }); + + it('returns empty array for missing index', () => { + const tmp = makeTmpFeatureWorktree(); + expect(listKBs(tmp)).toEqual([]); + }); +}); + +// --------------------------------------------------------------------------- +// checkAllStaleness +// --------------------------------------------------------------------------- + +describe('checkAllStaleness', () => { + it('returns empty object for missing index', () => { + const tmp = makeTmpFeatureWorktree(); + expect(checkAllStaleness(tmp)).toEqual({}); + }); + + it('returns an entry per slug', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + const result = checkAllStaleness(tmp); + expect(result['cli-commands']).toBeDefined(); + expect(result['cli-commands']).toHaveProperty('stale'); + expect(result['cli-commands']).toHaveProperty('changedFiles'); + }); +}); diff --git a/tests/feature-kb/fixtures.ts b/tests/feature-kb/fixtures.ts new file mode 100644 index 0000000..c7c5be8 --- /dev/null +++ b/tests/feature-kb/fixtures.ts @@ -0,0 +1,100 @@ +// Shared test fixtures for feature-kb module tests. + +import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'fs'; +import * as path from 'path'; +import * as os from 'os'; + +export const SAMPLE_INDEX = { + version: 1, + features: { + 'cli-commands': { + name: 'CLI Command System', + description: 'Use when adding CLI subcommands, modifying plugin registration, or changing the init flow.', + directories: ['src/cli/commands/', 'src/cli/utils/'], + referencedFiles: ['src/cli/cli.ts', 'src/cli/plugins.ts'], + category: 'component-patterns', + lastUpdated: '2026-04-20T14:30:00Z', + createdBy: 'plan:orch', + }, + }, +}; + +export const SAMPLE_KB_CONTENT = `--- +feature: cli-commands +name: CLI Command System +category: component-patterns +directories: + - src/cli/commands/ + - src/cli/utils/ +referencedFiles: + - src/cli/cli.ts + - src/cli/plugins.ts +created: 2026-04-20T14:30:00Z +updated: 2026-04-20T14:30:00Z +--- + +# CLI Command System + +## Overview +Commander.js-based CLI with @clack/prompts for interactive UX. + +## Architecture +Each command is a separate file in src/cli/commands/ exporting a Command instance. + +## Key Patterns +- Commander.js option chain +- @clack/prompts for TUI dialogs + +## Anti-Patterns +- Don't use inquirer (project uses @clack/prompts) + +## Gotchas +- Always register new commands in cli.ts + +## Key Files +- src/cli/cli.ts — command registration +- src/cli/plugins.ts — plugin registry +`; + +const createdTmpDirs: string[] = []; + +/** + * Create a temporary worktree directory with optional .features/ index and KB files. + * Returns the absolute path to the tmpdir root. + * Directories are tracked — call `cleanupTmpFeatureWorktrees()` in afterAll. + */ +export function makeTmpFeatureWorktree( + indexContent?: object, + kbs?: Record, +): string { + const tmp = mkdtempSync(path.join(os.tmpdir(), 'feature-kb-test-')); + createdTmpDirs.push(tmp); + + const featuresDir = path.join(tmp, '.features'); + mkdirSync(featuresDir, { recursive: true }); + + if (indexContent) { + writeFileSync(path.join(featuresDir, 'index.json'), JSON.stringify(indexContent, null, 2)); + } + + if (kbs) { + for (const [slug, content] of Object.entries(kbs)) { + const kbDir = path.join(featuresDir, slug); + mkdirSync(kbDir, { recursive: true }); + writeFileSync(path.join(kbDir, 'KNOWLEDGE.md'), content); + } + } + + return tmp; +} + +/** + * Remove all temporary worktree directories created by `makeTmpFeatureWorktree`. + * Call in `afterAll(() => cleanupTmpFeatureWorktrees())`. + */ +export function cleanupTmpFeatureWorktrees(): void { + for (const dir of createdTmpDirs) { + try { rmSync(dir, { recursive: true, force: true }); } catch { /* best-effort */ } + } + createdTmpDirs.length = 0; +} diff --git a/tests/feature-kb/kb-builder-agent.test.ts b/tests/feature-kb/kb-builder-agent.test.ts new file mode 100644 index 0000000..d990355 --- /dev/null +++ b/tests/feature-kb/kb-builder-agent.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'fs'; +import * as path from 'path'; + +const ROOT = path.resolve(import.meta.dirname, '../..'); + +describe('kb-builder agent', () => { + const content = readFileSync(path.join(ROOT, 'shared/agents/kb-builder.md'), 'utf8'); + + it('has correct model', () => { expect(content).toContain('model: sonnet'); }); + it('has feature-kb skill', () => { expect(content).toContain('devflow:feature-kb'); }); + it('has worktree-support skill', () => { expect(content).toContain('devflow:worktree-support'); }); + it('has required tools', () => { + expect(content).toContain('Read'); + expect(content).toContain('Grep'); + expect(content).toContain('Glob'); + expect(content).toContain('Bash'); + expect(content).toContain('Write'); + }); + it('documents input contract', () => { + expect(content).toContain('FEATURE_SLUG'); + expect(content).toContain('FEATURE_NAME'); + expect(content).toContain('EXPLORATION_OUTPUTS'); + expect(content).toContain('DIRECTORIES'); + expect(content).toContain('KNOWLEDGE_CONTEXT'); + }); + it('constrains writes to .features/', () => { + expect(content).toContain('.features/'); + expect(content).toContain('Boundaries'); + }); +}); diff --git a/tests/feature-kb/kb-command.test.ts b/tests/feature-kb/kb-command.test.ts new file mode 100644 index 0000000..f5043a0 --- /dev/null +++ b/tests/feature-kb/kb-command.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect, afterAll } from 'vitest'; +import { execSync } from 'child_process'; +import * as path from 'path'; +import { readFileSync, rmSync } from 'fs'; +import { makeTmpFeatureWorktree, cleanupTmpFeatureWorktrees, SAMPLE_INDEX } from './fixtures'; + +afterAll(() => cleanupTmpFeatureWorktrees()); + +const ROOT = path.resolve(import.meta.dirname, '../..'); +const CJS_PATH = path.join(ROOT, 'scripts/hooks/lib/feature-kb.cjs'); + +describe('feature-kb.cjs CLI', () => { + it('list shows entries', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + const result = execSync(`node ${CJS_PATH} list ${tmp}`, { encoding: 'utf8' }); + const entries = JSON.parse(result); + expect(entries).toHaveLength(1); + expect(entries[0].slug).toBe('cli-commands'); + expect(entries[0].name).toBe('CLI Command System'); + }); + + it('list returns empty array for missing index', () => { + const tmp = makeTmpFeatureWorktree(); + // Remove the index file so index is missing + try { rmSync(path.join(tmp, '.features', 'index.json')); } catch { /* ignore */ } + const result = execSync(`node ${CJS_PATH} list ${tmp}`, { encoding: 'utf8' }); + expect(JSON.parse(result)).toEqual([]); + }); + + it('stale returns staleness for slug', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + const result = execSync(`node ${CJS_PATH} stale ${tmp} cli-commands`, { encoding: 'utf8' }); + const parsed = JSON.parse(result); + expect(parsed).toHaveProperty('stale'); + expect(parsed).toHaveProperty('changedFiles'); + }); + + it('exits 1 with usage error on no args', () => { + expect(() => execSync(`node ${CJS_PATH}`, { encoding: 'utf8', stdio: 'pipe' })).toThrow(); + }); + + it('update-index creates entry', () => { + const tmp = makeTmpFeatureWorktree({ version: 1, features: {} }); + execSync( + `node ${CJS_PATH} update-index ${tmp} --slug=payments --name="Payment Processing" --directories='["src/payments/"]' --referencedFiles='["src/payments/checkout.ts"]' --category=component-patterns`, + { encoding: 'utf8' } + ); + const index = JSON.parse(readFileSync(path.join(tmp, '.features', 'index.json'), 'utf8')); + expect(index.features.payments).toBeDefined(); + expect(index.features.payments.name).toBe('Payment Processing'); + }); + + it('remove deletes entry and directory', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX, { 'cli-commands': '# Test KB' }); + execSync(`node ${CJS_PATH} remove ${tmp} cli-commands`, { encoding: 'utf8' }); + const index = JSON.parse(readFileSync(path.join(tmp, '.features', 'index.json'), 'utf8')); + expect(index.features['cli-commands']).toBeUndefined(); + }); +}); diff --git a/tests/skill-references.test.ts b/tests/skill-references.test.ts index b5428eb..640648d 100644 --- a/tests/skill-references.test.ts +++ b/tests/skill-references.test.ts @@ -828,6 +828,7 @@ describe('Completeness: reviewer.md Focus Areas vs code-review plugin', () => { 'knowledge-persistence', 'review-methodology', 'worktree-support', + 'apply-feature-kb', // consumption meta-skill, not a review focus ]); const reviewerContent = readFileSync( From 14339680c501e9acd95061c156bf72d2ad86dd45 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Thu, 23 Apr 2026 17:46:01 +0300 Subject: [PATCH 02/44] feat(feature-kb): wire FEATURE_KNOWLEDGE through all orchestration skills, agents, and commands Integrates the feature-kb system into all 7 orchestration skills, 5 shared agents, 6 base commands, and 5 teams command variants. Each component loads relevant feature KBs from .features/index.json based on task context, passes FEATURE_KNOWLEDGE to downstream agents, and marks KBs stale after files change. - plan:orch: Phase 0.5 loads KBs; Phase 8.5 spawns KB Builder for new areas - implement:orch: Phase 1.5 loads KBs; Phase 6 marks stale after changes - review:orch: Phase 2b loads KBs; Reviewers receive FEATURE_KNOWLEDGE - resolve:orch: Phase 1.5 loads KBs; Resolvers receive FEATURE_KNOWLEDGE - debug:orch, explore:orch: load KBs orchestrator-local (not passed to workers) - quality-gates: P0-Design pillar checks FEATURE_KNOWLEDGE compliance - Agents (coder, reviewer, designer, resolver, scrutinizer): add apply-feature-kb skill + input field --- .../commands/code-review-teams.md | 26 ++++--- .../commands/code-review.md | 12 +++- plugins/devflow-debug/commands/debug-teams.md | 8 ++- plugins/devflow-debug/commands/debug.md | 8 ++- .../commands/implement-teams.md | 17 ++++- .../devflow-implement/commands/implement.md | 17 ++++- plugins/devflow-plan/agents/designer.md | 2 + plugins/devflow-plan/commands/plan-teams.md | 32 ++++++++- plugins/devflow-plan/commands/plan.md | 34 +++++++++- .../devflow-resolve/commands/resolve-teams.md | 12 +++- plugins/devflow-resolve/commands/resolve.md | 13 +++- .../commands/self-review.md | 14 +++- shared/agents/coder.md | 4 +- shared/agents/designer.md | 2 + shared/agents/resolver.md | 2 + shared/agents/reviewer.md | 2 + shared/agents/scrutinizer.md | 2 + shared/skills/debug:orch/SKILL.md | 11 ++- shared/skills/explore:orch/SKILL.md | 19 +++++- shared/skills/implement:orch/SKILL.md | 22 +++++- shared/skills/plan:orch/SKILL.md | 68 ++++++++++++++++--- shared/skills/quality-gates/SKILL.md | 2 + shared/skills/resolve:orch/SKILL.md | 13 +++- shared/skills/review:orch/SKILL.md | 13 +++- 24 files changed, 309 insertions(+), 46 deletions(-) diff --git a/plugins/devflow-code-review/commands/code-review-teams.md b/plugins/devflow-code-review/commands/code-review-teams.md index 0a0eea3..31a73c2 100644 --- a/plugins/devflow-code-review/commands/code-review-teams.md +++ b/plugins/devflow-code-review/commands/code-review-teams.md @@ -95,7 +95,7 @@ Per worktree, detect file types in diff using `DIFF_RANGE` to determine conditio ### Phase 1b: Load Knowledge Index -**Produces:** KNOWLEDGE_CONTEXT +**Produces:** KNOWLEDGE_CONTEXT, FEATURE_KNOWLEDGE Load the knowledge index for the current worktree before spawning the review team: @@ -105,6 +105,14 @@ KNOWLEDGE_CONTEXT=$(node scripts/hooks/lib/knowledge-context.cjs index "{worktre This produces a compact index of active ADR/PF entries. Pass `KNOWLEDGE_CONTEXT` to each reviewer teammate prompt. Reviewers use `devflow:apply-knowledge` to Read full entry bodies on demand. +**Load Feature Knowledge:** +1. Read `.features/index.json` if it exists +2. Based on changed files from Phase 1 analysis, identify relevant KBs (match file paths against KB `directories` and `referencedFiles`) +3. For each match: check staleness via `node scripts/hooks/lib/feature-kb.cjs stale "{worktree}" {slug}`, read `.features/{slug}/KNOWLEDGE.md` +4. Set `FEATURE_KNOWLEDGE` (or `(none)` if no KBs exist or none are relevant) + +Pass `FEATURE_KNOWLEDGE` to each reviewer teammate alongside `KNOWLEDGE_CONTEXT`. + ### Phase 2: Spawn Review Team **Produces:** REVIEWER_OUTPUTS @@ -143,16 +151,18 @@ Spawn review teammates. For each teammate, compose a self-contained prompt using You are reviewing PR #{pr_number} on branch {branch} (base: {base_branch}). WORKTREE_PATH: {worktree_path} (omit if cwd) KNOWLEDGE_CONTEXT: {knowledge_context} + FEATURE_KNOWLEDGE: {feature_knowledge} 1. Read your skill(s): `Read {SKILL_PATHS}` 2. Read review methodology: `Read ~/.claude/skills/devflow:review-methodology/SKILL.md` 3. Follow devflow:apply-knowledge to scan KNOWLEDGE_CONTEXT index and Read full ADR/PF bodies on demand. Skip if (none). - 4. Get the diff: `git -C {WORKTREE_PATH} diff {DIFF_RANGE}` - 5. Apply the 6-step review process from devflow:review-methodology - 6. Focus: {FOCUS} - 7. Classify each finding: 🔴 BLOCKING / ⚠️ SHOULD-FIX / ℹ️ PRE-EXISTING - 8. Include file:line references for every finding - 9. Write your report: `Write to {worktree_path}/.docs/reviews/{branch_slug}/{timestamp}/{REPORT_NAME}.md` - 10. Report completion: SendMessage(type: "message", recipient: "team-lead", summary: "{SUMMARY}") + 4. Follow devflow:apply-feature-kb for FEATURE_KNOWLEDGE — feature-specific patterns and anti-patterns inform findings. Skip if (none). + 5. Get the diff: `git -C {WORKTREE_PATH} diff {DIFF_RANGE}` + 6. Apply the 6-step review process from devflow:review-methodology + 7. Focus: {FOCUS} + 8. Classify each finding: 🔴 BLOCKING / ⚠️ SHOULD-FIX / ℹ️ PRE-EXISTING + 9. Include file:line references for every finding + 10. Write your report: `Write to {worktree_path}/.docs/reviews/{branch_slug}/{timestamp}/{REPORT_NAME}.md` + 11. Report completion: SendMessage(type: "message", recipient: "team-lead", summary: "{SUMMARY}") **Core reviewers (always spawn):** diff --git a/plugins/devflow-code-review/commands/code-review.md b/plugins/devflow-code-review/commands/code-review.md index 9b6efff..6dde5e1 100644 --- a/plugins/devflow-code-review/commands/code-review.md +++ b/plugins/devflow-code-review/commands/code-review.md @@ -102,7 +102,7 @@ Per worktree, detect file types in diff using `DIFF_RANGE` to determine conditio ### Phase 1b: Load Knowledge Index -**Produces:** KNOWLEDGE_CONTEXT +**Produces:** KNOWLEDGE_CONTEXT, FEATURE_KNOWLEDGE While file analysis runs (or just before spawning reviewers), load the knowledge index for the current worktree: @@ -112,6 +112,14 @@ KNOWLEDGE_CONTEXT=$(node scripts/hooks/lib/knowledge-context.cjs index "{worktre This produces a compact index of active ADR/PF entries. Pass `KNOWLEDGE_CONTEXT` to all Reviewer agents. Reviewers use `devflow:apply-knowledge` to Read full entry bodies on demand. +**Load Feature Knowledge:** +1. Read `.features/index.json` if it exists +2. Based on changed files from Phase 1 analysis, identify relevant KBs (match file paths against KB `directories` and `referencedFiles`) +3. For each match: check staleness via `node scripts/hooks/lib/feature-kb.cjs stale "{worktree}" {slug}`, read `.features/{slug}/KNOWLEDGE.md` +4. Set `FEATURE_KNOWLEDGE` (or `(none)` if no KBs exist or none are relevant) + +Pass `FEATURE_KNOWLEDGE` to all Reviewer agents alongside `KNOWLEDGE_CONTEXT`. + ### Phase 2: Run Reviews (Parallel) **Produces:** REVIEWER_OUTPUTS @@ -149,7 +157,9 @@ PR: #{pr_number}, Base: {base_branch} WORKTREE_PATH: {worktree_path} (omit if cwd) DIFF_COMMAND: git -C {WORKTREE_PATH} diff {DIFF_RANGE} (omit -C flag if no WORKTREE_PATH) KNOWLEDGE_CONTEXT: {knowledge_context} +FEATURE_KNOWLEDGE: {feature_knowledge} Follow devflow:apply-knowledge to scan the index and Read full ADR/PF bodies on demand. +Follow devflow:apply-feature-kb for FEATURE_KNOWLEDGE — feature-specific patterns and anti-patterns inform findings. IMPORTANT: Write report to {worktree_path}/.docs/reviews/{branch-slug}/{timestamp}/{focus}.md using Write tool" ``` diff --git a/plugins/devflow-debug/commands/debug-teams.md b/plugins/devflow-debug/commands/debug-teams.md index 961271d..a0cf56b 100644 --- a/plugins/devflow-debug/commands/debug-teams.md +++ b/plugins/devflow-debug/commands/debug-teams.md @@ -25,7 +25,7 @@ Investigate bugs by spawning a team of agents, each pursuing a different hypothe ### Phase 1: Load Knowledge Index (Orchestrator-Local) -**Produces:** KNOWLEDGE_CONTEXT +**Produces:** KNOWLEDGE_CONTEXT, FEATURE_KNOWLEDGE Before hypothesizing, load the knowledge index: @@ -35,6 +35,12 @@ KNOWLEDGE_CONTEXT=$(node scripts/hooks/lib/knowledge-context.cjs index "{worktre The orchestrator uses `KNOWLEDGE_CONTEXT` locally when generating hypotheses (Phase 2) — prior pitfalls and decisions can suggest specific root causes to investigate. Follow `devflow:apply-knowledge` to Read full entry bodies on demand. **Do NOT pass `KNOWLEDGE_CONTEXT` to investigator teammates** — knowledge context stays in the orchestrator; teammates examine code directly. +**Load Feature Knowledge:** +1. Read `.features/index.json` if it exists +2. Based on the bug description, identify relevant KBs +3. For each match: check staleness via `node scripts/hooks/lib/feature-kb.cjs stale "{worktree}" {slug}`, read `.features/{slug}/KNOWLEDGE.md` +4. Use `FEATURE_KNOWLEDGE` **locally only** for hypothesis generation — feature-specific gotchas and anti-patterns suggest root causes. **Do NOT pass to investigator teammates.** + ### Phase 2: Context Gathering **Produces:** HYPOTHESES, BUG_CONTEXT diff --git a/plugins/devflow-debug/commands/debug.md b/plugins/devflow-debug/commands/debug.md index 12ec57e..9cf8c36 100644 --- a/plugins/devflow-debug/commands/debug.md +++ b/plugins/devflow-debug/commands/debug.md @@ -33,7 +33,7 @@ Investigate bugs by spawning parallel agents, each pursuing a different hypothes ### Phase 1: Load Knowledge Index (Orchestrator-Local) -**Produces:** KNOWLEDGE_CONTEXT +**Produces:** KNOWLEDGE_CONTEXT, FEATURE_KNOWLEDGE Before hypothesizing, load the knowledge index: @@ -43,6 +43,12 @@ KNOWLEDGE_CONTEXT=$(node scripts/hooks/lib/knowledge-context.cjs index "{worktre The orchestrator uses `KNOWLEDGE_CONTEXT` locally when generating hypotheses (Phase 2) — prior pitfalls and decisions can suggest specific root causes to investigate. Follow `devflow:apply-knowledge` to Read full entry bodies on demand. **Do NOT pass `KNOWLEDGE_CONTEXT` to Explore investigators** — knowledge context stays in the orchestrator; investigators examine code directly. +**Load Feature Knowledge:** +1. Read `.features/index.json` if it exists +2. Based on the bug description, identify relevant KBs +3. For each match: check staleness via `node scripts/hooks/lib/feature-kb.cjs stale "{worktree}" {slug}`, read `.features/{slug}/KNOWLEDGE.md` +4. Use `FEATURE_KNOWLEDGE` **locally only** for hypothesis generation — feature-specific gotchas and anti-patterns suggest root causes. **Do NOT pass to Explore investigators.** + ### Phase 2: Context Gathering **Produces:** HYPOTHESES, BUG_CONTEXT diff --git a/plugins/devflow-implement/commands/implement-teams.md b/plugins/devflow-implement/commands/implement-teams.md index 915e3f2..6684492 100644 --- a/plugins/devflow-implement/commands/implement-teams.md +++ b/plugins/devflow-implement/commands/implement-teams.md @@ -29,7 +29,7 @@ Orchestrate a single task through implementation by spawning specialized agent t ### Phase 1: Setup -**Produces:** TASK_ID, BASE_BRANCH, EXECUTION_PLAN +**Produces:** TASK_ID, BASE_BRANCH, EXECUTION_PLAN, FEATURE_KNOWLEDGE Record the current branch name as `BASE_BRANCH` - this will be the PR target. @@ -60,6 +60,12 @@ Return the branch setup summary." 5. Use extracted content as EXECUTION_PLAN for the Coder phase (replaces exploration/planning output) 6. Captured values override defaults from Git agent where present +**Load Feature Knowledge:** +1. Read `.features/index.json` if it exists +2. Based on task description and file targets, identify relevant KBs +3. For each match: check staleness via `node scripts/hooks/lib/feature-kb.cjs stale "{worktree}" {slug}`, read `.features/{slug}/KNOWLEDGE.md` +4. Set `FEATURE_KNOWLEDGE` (or `(none)` if no KBs exist or none are relevant) + ### Phase 2: Implement **Produces:** CODER_OUTPUT, FILES_CHANGED @@ -89,7 +95,8 @@ BASE_BRANCH: {base branch} EXECUTION_PLAN: {full plan from setup context} PATTERNS: {patterns from plan document or empty} CREATE_PR: true -DOMAIN: {detected domain or 'fullstack'}" +DOMAIN: {detected domain or 'fullstack'} +FEATURE_KNOWLEDGE: {feature_knowledge}" ``` --- @@ -219,6 +226,7 @@ After Simplifier completes, spawn Scrutinizer as final quality gate: Agent(subagent_type="Scrutinizer"): "TASK_DESCRIPTION: {task description} FILES_CHANGED: {list of files from Coder output} +FEATURE_KNOWLEDGE: {feature_knowledge} Evaluate 9 pillars, fix P0/P1 issues, report status" ``` @@ -391,6 +399,11 @@ Design and execute scenario-based acceptance tests. Report PASS or FAIL with evi **Requires:** VALIDATION_RESULT, ALIGNMENT_RESULT, QA_RESULT, PR_URL +After quality gates pass, mark stale feature KBs based on changed files: +```bash +node scripts/hooks/lib/feature-kb.cjs mark-stale "{worktree}" {files_changed...} +``` + Display completion summary with phase status, PR info, and next steps. ## Phase 1.5: Load Project Knowledge -**Produces:** KNOWLEDGE_CONTEXT +**Produces:** KNOWLEDGE_CONTEXT, FEATURE_KNOWLEDGE **Requires:** REVIEW_DIR Run `node scripts/hooks/lib/knowledge-context.cjs index "{worktree}"` to produce a compact index of active ADR/PF entries from `decisions.md` and `pitfalls.md`, with Deprecated/Superseded entries already stripped. Falls back to `(none)` when both files are absent or all entries are filtered. Pass `KNOWLEDGE_CONTEXT` to every Resolver agent in Phase 4. Resolver agents use `devflow:apply-knowledge` to Read full entry bodies on demand — no fan-out of the full corpus. +Also load feature knowledge: +1. Read `.features/index.json` if it exists +2. Based on file paths from review report issue entries, identify relevant KBs +3. Read matching `.features/{slug}/KNOWLEDGE.md` files, check staleness via `node scripts/hooks/lib/feature-kb.cjs stale "{worktree}" {slug}` +4. Concatenate as `FEATURE_KNOWLEDGE` (or `(none)`) + ## Phase 2: Parse Issues **Produces:** ISSUES @@ -70,7 +76,7 @@ Determine execution: batches with no shared files can run in parallel. ## Phase 4: Resolve (Parallel) **Produces:** RESOLUTION_RESULTS -**Requires:** BATCHES, KNOWLEDGE_CONTEXT, BRANCH_SLUG +**Requires:** BATCHES, KNOWLEDGE_CONTEXT, FEATURE_KNOWLEDGE, BRANCH_SLUG Spawn `Agent(subagent_type="Resolver")` agents — one per batch, parallel where possible. @@ -79,6 +85,7 @@ Each receives: - **BRANCH**: Branch slug - **BATCH_ID**: Identifier for this batch - **KNOWLEDGE_CONTEXT**: Knowledge index from Phase 1.5 (or `(none)`). Resolvers follow `devflow:apply-knowledge` to Read full ADR/PF bodies on demand. +- **FEATURE_KNOWLEDGE**: Feature area context from Phase 1.5 (or `(none)`). Follow `devflow:apply-feature-kb` for consumption algorithm. Resolvers follow a 3-tier risk approach: - **Standard fixes**: Applied directly @@ -122,7 +129,7 @@ Report to user: Before reporting results, verify every phase was announced: - [ ] Phase 1: Target Review Directory → REVIEW_DIR captured -- [ ] Phase 1.5: Load Project Knowledge → KNOWLEDGE_CONTEXT captured +- [ ] Phase 1.5: Load Project Knowledge → KNOWLEDGE_CONTEXT captured, FEATURE_KNOWLEDGE loaded (or skipped if `.features/` absent) - [ ] Phase 2: Parse Issues → ISSUES captured (or stopped: no actionable issues) - [ ] Phase 3: Analyze & Batch → BATCHES captured - [ ] Phase 4: Resolve → RESOLUTION_RESULTS captured per batch diff --git a/shared/skills/review:orch/SKILL.md b/shared/skills/review:orch/SKILL.md index 3577e02..25d0b7c 100644 --- a/shared/skills/review:orch/SKILL.md +++ b/shared/skills/review:orch/SKILL.md @@ -46,7 +46,7 @@ Create directory: `mkdir -p .docs/reviews/{branch_slug}/{timestamp}` ## Phase 2b: Load Knowledge Index -**Produces:** KNOWLEDGE_CONTEXT +**Produces:** KNOWLEDGE_CONTEXT, FEATURE_KNOWLEDGE **Requires:** REVIEW_DIR After incremental detection, load the knowledge index: @@ -57,6 +57,12 @@ KNOWLEDGE_CONTEXT=$(node scripts/hooks/lib/knowledge-context.cjs index "{worktre This produces a compact index of active ADR/PF entries. Pass `KNOWLEDGE_CONTEXT` to all Reviewer agents. Reviewers use `devflow:apply-knowledge` to Read full entry bodies on demand. +Also load feature knowledge: +1. Read `.features/index.json` if it exists +2. Based on changed files from Phase 3 file analysis, identify relevant KBs (match file paths against KB `directories` and `referencedFiles`) +3. For each match: check staleness via `node scripts/hooks/lib/feature-kb.cjs stale "{worktree}" {slug}`, read `.features/{slug}/KNOWLEDGE.md` +4. Concatenate as `FEATURE_KNOWLEDGE` (or `(none)`) + ## Phase 3: File Analysis **Produces:** REVIEWER_LIST @@ -83,7 +89,7 @@ Detect conditional reviewers from file types: ## Phase 4: Reviews (Parallel) **Produces:** REVIEWER_OUTPUTS -**Requires:** DIFF_RANGE, REVIEW_DIR, TIMESTAMP, KNOWLEDGE_CONTEXT, REVIEWER_LIST +**Requires:** DIFF_RANGE, REVIEW_DIR, TIMESTAMP, KNOWLEDGE_CONTEXT, FEATURE_KNOWLEDGE, REVIEWER_LIST Spawn all reviewers in a single message (parallel execution): @@ -99,6 +105,7 @@ Each reviewer receives: - **Output path**: `.docs/reviews/{branch_slug}/{timestamp}/{focus}.md` - **DIFF_COMMAND**: `git diff {DIFF_RANGE}` (incremental or full) - **KNOWLEDGE_CONTEXT**: compact index from Phase 2b (or `(none)` when absent) — follow `devflow:apply-knowledge` to Read full ADR/PF bodies on demand +- **FEATURE_KNOWLEDGE**: feature area context from Phase 2b (or `(none)`) — follow `devflow:apply-feature-kb` for consumption algorithm ## Phase 5: Synthesis (Parallel) @@ -134,7 +141,7 @@ Before reporting results, verify every phase was announced: - [ ] Phase 1: Pre-flight → BRANCH_INFO, PR_INFO captured - [ ] Phase 2: Incremental Detection → DIFF_RANGE, REVIEW_DIR, TIMESTAMP captured -- [ ] Phase 2b: Load Knowledge Index → KNOWLEDGE_CONTEXT captured +- [ ] Phase 2b: Load Knowledge Index → KNOWLEDGE_CONTEXT captured, FEATURE_KNOWLEDGE loaded (or skipped if `.features/` absent) - [ ] Phase 3: File Analysis → REVIEWER_LIST captured - [ ] Phase 4: Reviews → REVIEWER_OUTPUTS written to disk - [ ] Phase 5: Synthesis → review-summary.md written From 5b61071ac19950b7168340fe82d026a19492b889 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Thu, 23 Apr 2026 18:03:25 +0300 Subject: [PATCH 03/44] fix: address self-review issues Security (P0): Add validateSlug() with kebab-case enforcement to prevent path traversal via malicious slugs in loadKBContent, checkStaleness, updateIndex, removeEntry. CLI commands (kb create, kb remove) validate at boundary before any filesystem operations. Functionality (P0): Remove scope === 'local' guard on .features/ directory creation in init.ts -- .features/ is committed to the project repo and should be created whenever a gitRoot exists, regardless of install scope. Documentation (P2): Update CLAUDE.md skill count (41 -> 44) and agent count (12 -> 13) to reflect new feature-kb/apply-feature-kb skills and kb-builder agent. Tests: Add 6 validateSlug tests covering valid slugs, path traversal, slashes, dot-prefix, and non-kebab-case rejection. --- CLAUDE.md | 4 +- scripts/hooks/lib/feature-kb.cjs | 36 +++++++++- src/cli/commands/init.ts | 3 +- src/cli/commands/kb.ts | 100 +++++++++++----------------- tests/feature-kb/feature-kb.test.ts | 45 +++++++++++++ 5 files changed, 121 insertions(+), 67 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0d7ce49..f00717e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,8 +56,8 @@ Commands with Teams Variant ship as `{name}.md` (parallel subagents) and `{name} ``` devflow/ -├── shared/skills/ # 41 skills (single source of truth) -├── shared/agents/ # 12 shared agents (single source of truth) +├── shared/skills/ # 44 skills (single source of truth) +├── shared/agents/ # 13 shared agents (single source of truth) ├── plugins/devflow-*/ # 17 plugins (8 core + 9 optional language/ecosystem) ├── docs/reference/ # Detailed reference documentation ├── scripts/ # Helper scripts (statusline, docs-helpers) diff --git a/scripts/hooks/lib/feature-kb.cjs b/scripts/hooks/lib/feature-kb.cjs index 48fe7e2..bd38737 100644 --- a/scripts/hooks/lib/feature-kb.cjs +++ b/scripts/hooks/lib/feature-kb.cjs @@ -26,6 +26,35 @@ const fs = require('fs'); const path = require('path'); +const { execFileSync } = require('child_process'); + +/** + * Validate that a slug is safe for use as a directory name. + * Rejects path traversal attempts (e.g., '../etc'), absolute paths, + * and characters unsafe for filesystem use. + * + * D52: Defense-in-depth — even though callers are trusted orchestration + * scripts, validate at the boundary closest to the filesystem operation. + * + * @param {string} slug + * @returns {void} + * @throws {Error} if slug is invalid + */ +function validateSlug(slug) { + if (!slug || typeof slug !== 'string') { + throw new Error('Slug must be a non-empty string'); + } + if (slug.includes('..') || slug.includes('/') || slug.includes('\\')) { + throw new Error(`Invalid slug '${slug}': must not contain '..', '/', or '\\'`); + } + if (slug.startsWith('.')) { + throw new Error(`Invalid slug '${slug}': must not start with '.'`); + } + // Only allow kebab-case identifiers: lowercase letters, digits, hyphens + if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) { + throw new Error(`Invalid slug '${slug}': must be kebab-case (lowercase letters, digits, hyphens)`); + } +} /** * @typedef {{ @@ -65,6 +94,7 @@ function loadIndex(worktreePath) { * @returns {string | null} */ function loadKBContent(worktreePath, slug) { + validateSlug(slug); const kbPath = path.join(worktreePath, '.features', slug, 'KNOWLEDGE.md'); try { return fs.readFileSync(kbPath, 'utf8'); @@ -82,11 +112,11 @@ function loadKBContent(worktreePath, slug) { * @returns {{ stale: boolean, changedFiles: string[] }} */ function checkStaleness(worktreePath, slug) { + validateSlug(slug); const index = loadIndex(worktreePath); if (!index || !index.features[slug]) return { stale: false, changedFiles: [] }; const entry = index.features[slug]; - const { execFileSync } = require('child_process'); try { // Check if in git repo — use execFileSync to avoid shell injection @@ -189,6 +219,7 @@ function releaseLock(lockPath) { * }} entry */ function updateIndex(worktreePath, entry) { + validateSlug(entry.slug); const featuresDir = path.join(worktreePath, '.features'); const lockPath = path.join(featuresDir, '.kb.lock'); const indexPath = path.join(featuresDir, 'index.json'); @@ -252,6 +283,7 @@ function markStale(worktreePath, changedFiles) { * @param {string} slug */ function removeEntry(worktreePath, slug) { + validateSlug(slug); const featuresDir = path.join(worktreePath, '.features'); const lockPath = path.join(featuresDir, '.kb.lock'); const indexPath = path.join(featuresDir, 'index.json'); @@ -431,4 +463,4 @@ if (require.main === module) { process.exit(1); } -module.exports = { loadIndex, loadKBContent, checkStaleness, checkAllStaleness, updateIndex, markStale, removeEntry, listKBs }; +module.exports = { loadIndex, loadKBContent, checkStaleness, checkAllStaleness, updateIndex, markStale, removeEntry, listKBs, validateSlug }; diff --git a/src/cli/commands/init.ts b/src/cli/commands/init.ts index 4cd7728..8cb5f1c 100644 --- a/src/cli/commands/init.ts +++ b/src/cli/commands/init.ts @@ -946,7 +946,8 @@ export const initCommand = new Command('init') } // Create .features/ directory with empty index (feature knowledge bases) - if (scope === 'local' && gitRoot) { + // .features/ is committed to the project repo (not scope-dependent) + if (gitRoot) { const featuresDir = path.join(gitRoot, '.features'); await fs.mkdir(featuresDir, { recursive: true }); const featuresIndexPath = path.join(featuresDir, 'index.json'); diff --git a/src/cli/commands/kb.ts b/src/cli/commands/kb.ts index 43cc8ef..e8c0188 100644 --- a/src/cli/commands/kb.ts +++ b/src/cli/commands/kb.ts @@ -1,12 +1,13 @@ import { Command } from 'commander'; import { promises as fs } from 'fs'; import * as path from 'path'; -import { execFileSync, execSync } from 'child_process'; +import { execFileSync } from 'child_process'; import * as p from '@clack/prompts'; import color from 'picocolors'; import { createRequire } from 'module'; import { fileURLToPath } from 'url'; import { isClaudeCliAvailable } from '../utils/cli.js'; +import { getGitRoot } from '../utils/git.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -14,41 +15,24 @@ const __dirname = path.dirname(__filename); /** @internal */ const _require = createRequire(import.meta.url); -/** - * Resolve the path to feature-kb.cjs relative to this file's dist location. - * In the build, cli.js lands in dist/cli/, so we go up two levels to reach - * the project root (where scripts/ lives). - */ -function getFeatureKbPath(): string { - // dist/cli/commands/kb.js → ../../.. → project root - return path.join(__dirname, '..', '..', '..', 'scripts', 'hooks', 'lib', 'feature-kb.cjs'); -} - -/** - * Load the feature-kb CJS module functions. - */ -function loadFeatureKb(): { +interface FeatureKbModule { listKBs: (worktreePath: string) => Array<{ slug: string; name: string; category: string; directories: string[]; lastUpdated: string }>; checkAllStaleness: (worktreePath: string) => Record; checkStaleness: (worktreePath: string, slug: string) => { stale: boolean; changedFiles: string[] }; removeEntry: (worktreePath: string, slug: string) => void; -} { - return _require(getFeatureKbPath()); + validateSlug: (slug: string) => void; } +// dist/cli/commands/kb.js → ../../.. → project root (where scripts/ lives) +const featureKb: FeatureKbModule = _require( + path.join(__dirname, '..', '..', '..', 'scripts', 'hooks', 'lib', 'feature-kb.cjs') +); + /** * Get the git root for the current directory, or cwd if not in a git repo. */ -function getWorktreePath(): string { - try { - const result = execSync('git rev-parse --show-toplevel', { - encoding: 'utf8', - stdio: ['pipe', 'pipe', 'pipe'], - }); - return result.trim(); - } catch { - return process.cwd(); - } +async function getWorktreePath(): Promise { + return (await getGitRoot()) ?? process.cwd(); } export const kbCommand = new Command('kb') @@ -64,19 +48,9 @@ kbCommand .action(async () => { p.intro(color.cyan('Feature Knowledge Bases')); - const worktreePath = getWorktreePath(); - - let kbs: ReturnType['listKBs']>; - let staleness: Record; - - try { - const featureKb = loadFeatureKb(); - kbs = featureKb.listKBs(worktreePath); - staleness = featureKb.checkAllStaleness(worktreePath); - } catch (err) { - p.log.error(`Failed to load feature KBs: ${err instanceof Error ? err.message : String(err)}`); - process.exit(1); - } + const worktreePath = await getWorktreePath(); + const kbs = featureKb.listKBs(worktreePath); + const staleness = featureKb.checkAllStaleness(worktreePath); if (kbs.length === 0) { p.log.info( @@ -93,9 +67,7 @@ kbCommand for (const kb of kbs) { const staleInfo = staleness[kb.slug]; const isStale = staleInfo?.stale ?? false; - const statusBadge = isStale - ? color.yellow('[STALE]') - : color.green('[current]'); + const statusBadge = isStale ? color.yellow('[STALE]') : color.green('[current]'); console.log(` ${color.bold(kb.name)} ${statusBadge}`); console.log(` slug: ${color.dim(kb.slug)}`); @@ -103,7 +75,9 @@ kbCommand console.log(` updated: ${color.dim(kb.lastUpdated)}`); console.log(` dirs: ${color.dim(kb.directories.join(', '))}`); if (isStale && staleInfo.changedFiles.length > 0) { - console.log(` changed: ${color.yellow(staleInfo.changedFiles.slice(0, 3).join(', '))}${staleInfo.changedFiles.length > 3 ? ` +${staleInfo.changedFiles.length - 3} more` : ''}`); + const shown = staleInfo.changedFiles.slice(0, 3).join(', '); + const overflow = staleInfo.changedFiles.length > 3 ? ` +${staleInfo.changedFiles.length - 3} more` : ''; + console.log(` changed: ${color.yellow(shown)}${overflow}`); } console.log(''); } @@ -121,19 +95,9 @@ kbCommand .action(async () => { p.intro(color.cyan('KB Staleness Check')); - const worktreePath = getWorktreePath(); - - let staleness: Record; - let kbs: ReturnType['listKBs']>; - - try { - const featureKb = loadFeatureKb(); - kbs = featureKb.listKBs(worktreePath); - staleness = featureKb.checkAllStaleness(worktreePath); - } catch (err) { - p.log.error(`Failed to check KBs: ${err instanceof Error ? err.message : String(err)}`); - process.exit(1); - } + const worktreePath = await getWorktreePath(); + const kbs = featureKb.listKBs(worktreePath); + const staleness = featureKb.checkAllStaleness(worktreePath); if (kbs.length === 0) { p.log.info('No feature KBs found.'); @@ -175,6 +139,13 @@ kbCommand .command('create ') .description('Create a new KB via claude -p exploration') .action(async (slug: string) => { + try { + featureKb.validateSlug(slug); + } catch (err) { + p.log.error(err instanceof Error ? err.message : String(err)); + process.exit(1); + } + p.intro(color.cyan(`Create Feature KB: ${slug}`)); if (!isClaudeCliAvailable()) { @@ -182,7 +153,7 @@ kbCommand process.exit(1); } - const worktreePath = getWorktreePath(); + const worktreePath = await getWorktreePath(); const name = await p.text({ message: 'Feature name (human-readable)', @@ -264,8 +235,7 @@ kbCommand process.exit(1); } - const worktreePath = getWorktreePath(); - const featureKb = loadFeatureKb(); + const worktreePath = await getWorktreePath(); // Determine which slugs to refresh let slugsToRefresh: string[]; @@ -342,6 +312,13 @@ kbCommand .command('remove ') .description('Remove a KB and its index entry') .action(async (slug: string) => { + try { + featureKb.validateSlug(slug); + } catch (err) { + p.log.error(err instanceof Error ? err.message : String(err)); + process.exit(1); + } + p.intro(color.cyan(`Remove KB: ${slug}`)); const confirmed = await p.confirm({ @@ -353,10 +330,9 @@ kbCommand return; } - const worktreePath = getWorktreePath(); + const worktreePath = await getWorktreePath(); try { - const featureKb = loadFeatureKb(); featureKb.removeEntry(worktreePath, slug); p.log.success(`KB '${slug}' removed`); } catch (err) { diff --git a/tests/feature-kb/feature-kb.test.ts b/tests/feature-kb/feature-kb.test.ts index 9afea90..b164f6b 100644 --- a/tests/feature-kb/feature-kb.test.ts +++ b/tests/feature-kb/feature-kb.test.ts @@ -23,6 +23,7 @@ const { markStale, removeEntry, listKBs, + validateSlug, } = require(path.join(ROOT, 'scripts/hooks/lib/feature-kb.cjs')) as { loadIndex: (worktreePath: string) => { version: number; features: Record } | null; loadKBContent: (worktreePath: string, slug: string) => string | null; @@ -32,6 +33,7 @@ const { markStale: (worktreePath: string, changedFiles: string[]) => string[]; removeEntry: (worktreePath: string, slug: string) => void; listKBs: (worktreePath: string) => Array<{ slug: string } & Record>; + validateSlug: (slug: string) => void; }; // --------------------------------------------------------------------------- @@ -247,3 +249,46 @@ describe('checkAllStaleness', () => { expect(result['cli-commands']).toHaveProperty('changedFiles'); }); }); + +// --------------------------------------------------------------------------- +// validateSlug +// --------------------------------------------------------------------------- + +describe('validateSlug', () => { + it('accepts valid kebab-case slugs', () => { + expect(() => validateSlug('cli-commands')).not.toThrow(); + expect(() => validateSlug('payments')).not.toThrow(); + expect(() => validateSlug('my-feature-123')).not.toThrow(); + expect(() => validateSlug('a')).not.toThrow(); + }); + + it('rejects path traversal attempts', () => { + expect(() => validateSlug('../etc')).toThrow(/must not contain/); + expect(() => validateSlug('../../dangerous')).toThrow(/must not contain/); + expect(() => validateSlug('foo/../bar')).toThrow(/must not contain/); + }); + + it('rejects slugs with slashes', () => { + expect(() => validateSlug('foo/bar')).toThrow(/must not contain/); + expect(() => validateSlug('foo\\bar')).toThrow(/must not contain/); + }); + + it('rejects slugs starting with a dot', () => { + expect(() => validateSlug('.hidden')).toThrow(/must not start with/); + }); + + it('rejects non-kebab-case slugs', () => { + expect(() => validateSlug('MyFeature')).toThrow(/kebab-case/); + expect(() => validateSlug('my_feature')).toThrow(/kebab-case/); + expect(() => validateSlug('MY-FEATURE')).toThrow(/kebab-case/); + expect(() => validateSlug('')).toThrow(/non-empty/); + }); + + it('rejects empty and non-string values', () => { + expect(() => validateSlug('')).toThrow(); + // @ts-expect-error testing runtime behavior + expect(() => validateSlug(null)).toThrow(); + // @ts-expect-error testing runtime behavior + expect(() => validateSlug(undefined)).toThrow(); + }); +}); From b70a1a6d78ce5e2e562c4b1bbd170f3b96ebfbcf Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Thu, 23 Apr 2026 18:18:24 +0300 Subject: [PATCH 04/44] fix: add FEATURE_KNOWLEDGE delegation note to pipeline:orch --- shared/skills/pipeline:orch/SKILL.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shared/skills/pipeline:orch/SKILL.md b/shared/skills/pipeline:orch/SKILL.md index c668bc6..600d094 100644 --- a/shared/skills/pipeline:orch/SKILL.md +++ b/shared/skills/pipeline:orch/SKILL.md @@ -17,6 +17,10 @@ Meta-orchestrator chaining implement → review → resolve with status reportin --- +## Feature Knowledge + +`FEATURE_KNOWLEDGE` loading is handled by each sub-orchestrator (implement:orch Phase 1.5, review:orch Phase 2b, resolve:orch Phase 1.5). Pipeline:orch does NOT load KBs itself — it delegates to the inner skills which handle loading, staleness checks, and agent distribution independently. + ## Cost Communication Classification statement must warn about scope: From b2f9cc7efe76459c0c7b653db642cb2d0854aa15 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Thu, 23 Apr 2026 19:40:38 +0300 Subject: [PATCH 05/44] feat(feature-kb): add KB Builder agent to ambient and plan plugins Distributes KB Builder agent to devflow-ambient and devflow-plan plugins at build time. Co-Authored-By: Claude --- plugins/devflow-ambient/agents/kb-builder.md | 57 ++++++++++++++++++++ plugins/devflow-plan/agents/kb-builder.md | 57 ++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 plugins/devflow-ambient/agents/kb-builder.md create mode 100644 plugins/devflow-plan/agents/kb-builder.md diff --git a/plugins/devflow-ambient/agents/kb-builder.md b/plugins/devflow-ambient/agents/kb-builder.md new file mode 100644 index 0000000..25d480f --- /dev/null +++ b/plugins/devflow-ambient/agents/kb-builder.md @@ -0,0 +1,57 @@ +--- +name: KB Builder +description: Structures codebase exploration into a feature knowledge base +model: sonnet +skills: + - devflow:feature-kb + - devflow:worktree-support +tools: + - Read + - Grep + - Glob + - Bash + - Write +--- + +# KB Builder Agent + +## Input Context + +- **FEATURE_SLUG** (required): Kebab-case identifier for the feature area (e.g., `cli-commands`) +- **FEATURE_NAME** (required): Human-readable name (e.g., "CLI Command System") +- **EXPLORATION_OUTPUTS** (required): Combined findings from Skimmer + Explore agents +- **DIRECTORIES** (required): Directory prefixes defining the feature area scope +- **KNOWLEDGE_CONTEXT** (optional): Existing ADR/PF index for cross-referencing +- **EXISTING_KB** (optional): Current KNOWLEDGE.md content when refreshing a stale KB +- **CHANGED_FILES** (optional): Files that changed since last KB update (for refresh) +- **WORKTREE_PATH** (optional): Worktree root for path resolution + +## Responsibilities + +1. **Resolve worktree path**: Use `devflow:worktree-support` to determine the working directory +2. **Orient on feature area**: Read EXPLORATION_OUTPUTS to understand the feature's architecture, patterns, and boundaries +3. **Follow the feature-kb skill**: Execute the 4-phase process (Scan → Extract → Distill → Forge) from `devflow:feature-kb` +4. **Cross-reference knowledge**: If KNOWLEDGE_CONTEXT is provided, reference relevant ADR/PF entries in the KB's "Related" section +5. **Handle refresh**: If EXISTING_KB is provided, update stale sections based on CHANGED_FILES while preserving any manually added content (user edits). Don't regenerate from scratch. +6. **Write KNOWLEDGE.md**: Write to `.features/{FEATURE_SLUG}/KNOWLEDGE.md` (create directory if needed) +7. **Update index**: Run `node scripts/hooks/lib/feature-kb.cjs update-index` with all required fields +8. **Report**: Output what was created/updated + +## Output + +``` +KB_STATUS: created | refreshed +KB_PATH: .features/{slug}/KNOWLEDGE.md +KB_SLUG: {slug} +KB_NAME: {name} +SECTIONS: [list of sections written] +REFERENCED_FILES: [files selected for staleness tracking] +CROSS_REFERENCES: [ADR/PF entries referenced, if any] +``` + +## Boundaries + +- **Only writes to `.features/` directory** — never modify source code +- **Never delete existing KBs** — only create new or refresh existing +- **500-line cap** — if KB exceeds 500 lines, split into focused sub-KBs (each gets own index entry) +- **No push, no external API calls** — local filesystem operations only diff --git a/plugins/devflow-plan/agents/kb-builder.md b/plugins/devflow-plan/agents/kb-builder.md new file mode 100644 index 0000000..25d480f --- /dev/null +++ b/plugins/devflow-plan/agents/kb-builder.md @@ -0,0 +1,57 @@ +--- +name: KB Builder +description: Structures codebase exploration into a feature knowledge base +model: sonnet +skills: + - devflow:feature-kb + - devflow:worktree-support +tools: + - Read + - Grep + - Glob + - Bash + - Write +--- + +# KB Builder Agent + +## Input Context + +- **FEATURE_SLUG** (required): Kebab-case identifier for the feature area (e.g., `cli-commands`) +- **FEATURE_NAME** (required): Human-readable name (e.g., "CLI Command System") +- **EXPLORATION_OUTPUTS** (required): Combined findings from Skimmer + Explore agents +- **DIRECTORIES** (required): Directory prefixes defining the feature area scope +- **KNOWLEDGE_CONTEXT** (optional): Existing ADR/PF index for cross-referencing +- **EXISTING_KB** (optional): Current KNOWLEDGE.md content when refreshing a stale KB +- **CHANGED_FILES** (optional): Files that changed since last KB update (for refresh) +- **WORKTREE_PATH** (optional): Worktree root for path resolution + +## Responsibilities + +1. **Resolve worktree path**: Use `devflow:worktree-support` to determine the working directory +2. **Orient on feature area**: Read EXPLORATION_OUTPUTS to understand the feature's architecture, patterns, and boundaries +3. **Follow the feature-kb skill**: Execute the 4-phase process (Scan → Extract → Distill → Forge) from `devflow:feature-kb` +4. **Cross-reference knowledge**: If KNOWLEDGE_CONTEXT is provided, reference relevant ADR/PF entries in the KB's "Related" section +5. **Handle refresh**: If EXISTING_KB is provided, update stale sections based on CHANGED_FILES while preserving any manually added content (user edits). Don't regenerate from scratch. +6. **Write KNOWLEDGE.md**: Write to `.features/{FEATURE_SLUG}/KNOWLEDGE.md` (create directory if needed) +7. **Update index**: Run `node scripts/hooks/lib/feature-kb.cjs update-index` with all required fields +8. **Report**: Output what was created/updated + +## Output + +``` +KB_STATUS: created | refreshed +KB_PATH: .features/{slug}/KNOWLEDGE.md +KB_SLUG: {slug} +KB_NAME: {name} +SECTIONS: [list of sections written] +REFERENCED_FILES: [files selected for staleness tracking] +CROSS_REFERENCES: [ADR/PF entries referenced, if any] +``` + +## Boundaries + +- **Only writes to `.features/` directory** — never modify source code +- **Never delete existing KBs** — only create new or refresh existing +- **500-line cap** — if KB exceeds 500 lines, split into focused sub-KBs (each gets own index entry) +- **No push, no external API calls** — local filesystem operations only From a15d49f4697287d60b74a38325f223cc9de56cdf Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 24 Apr 2026 12:44:26 +0300 Subject: [PATCH 06/44] fix(feature-kb): resolve runtime review issues (H5-H9, M2-M3, S2-S4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - H5: Fix N+1 in checkAllStaleness — load index + git-dir check once - H6: Fix updateIndex crash on missing .features/ with mkdirSync - H7: Fix removeEntry crash on missing .features/ with early return - H8/H9: Refactor CLI to dispatch table with requireWorktree validation - M2: Fix findOverlapping prefix matching to use directory boundary - M3: Simplify removeEntry control flow (no early return inside lock) - S2: Extract tryBreakStaleLock helper from acquireLock - S3: Rename markStale → findOverlapping (more accurate name) - S4: Add requireWorktree() to validate CLI worktree arg is a directory - T1: Add optional lockTimeoutMs param to updateIndex/removeEntry Co-Authored-By: Claude --- scripts/hooks/lib/feature-kb.cjs | 299 ++++++++++++++++++------------- 1 file changed, 177 insertions(+), 122 deletions(-) diff --git a/scripts/hooks/lib/feature-kb.cjs b/scripts/hooks/lib/feature-kb.cjs index bd38737..4078600 100644 --- a/scripts/hooks/lib/feature-kb.cjs +++ b/scripts/hooks/lib/feature-kb.cjs @@ -19,7 +19,7 @@ // node feature-kb.cjs list // node feature-kb.cjs stale [slug] // node feature-kb.cjs update-index --slug=X --name=Y ... -// node feature-kb.cjs mark-stale [file2...] +// node feature-kb.cjs find-overlapping [file2...] // node feature-kb.cjs remove 'use strict'; @@ -146,6 +146,7 @@ function checkStaleness(worktreePath, slug) { /** * Check staleness for all KBs in the index. + * Loads the index once and checks git-dir once to avoid N+1 overhead. * * @param {string} worktreePath * @returns {Record} @@ -153,13 +154,64 @@ function checkStaleness(worktreePath, slug) { function checkAllStaleness(worktreePath) { const index = loadIndex(worktreePath); if (!index) return {}; + + // Check git-dir once for the whole batch + try { + execFileSync('git', ['rev-parse', '--git-dir'], { cwd: worktreePath, stdio: 'pipe' }); + } catch { + // Non-git repo — all entries non-stale + const results = {}; + for (const slug of Object.keys(index.features)) { + results[slug] = { stale: false, changedFiles: [] }; + } + return results; + } + const results = {}; for (const slug of Object.keys(index.features)) { - results[slug] = checkStaleness(worktreePath, slug); + const entry = index.features[slug]; + const files = entry.referencedFiles || []; + if (files.length === 0) { + results[slug] = { stale: false, changedFiles: [] }; + continue; + } + try { + const result = execFileSync( + 'git', + ['log', `--after=${entry.lastUpdated}`, '--name-only', '--pretty=format:', '--', ...files], + { cwd: worktreePath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] } + ); + const changedFiles = [...new Set(result.split('\n').map(l => l.trim()).filter(Boolean))]; + results[slug] = { stale: changedFiles.length > 0, changedFiles }; + } catch { + results[slug] = { stale: false, changedFiles: [] }; + } } return results; } +/** + * Attempt to break a stale mkdir-based lock. + * Returns true when the lock is gone (either removed or already absent), + * false when the lock is still live (within staleMs). + * + * @param {string} lockPath + * @param {number} staleMs + * @returns {boolean} + */ +function tryBreakStaleLock(lockPath, staleMs) { + try { + const stat = fs.statSync(lockPath); + if (Date.now() - stat.mtimeMs > staleMs) { + try { fs.rmdirSync(lockPath); } catch { /* ignore */ } + return true; + } + } catch { + return true; // lock disappeared + } + return false; +} + /** * Acquire a mkdir-based lock. Follows the same pattern as .memory/.knowledge.lock. * Returns true when the lock is acquired within timeoutMs, false otherwise. @@ -176,20 +228,12 @@ function acquireLock(lockPath, timeoutMs = 30000, staleMs = 60000) { fs.mkdirSync(lockPath); return true; } catch { - // Check whether the lock is stale - try { - const stat = fs.statSync(lockPath); - if (Date.now() - stat.mtimeMs > staleMs) { - try { fs.rmdirSync(lockPath); } catch { /* ignore */ } - continue; - } - } catch { - continue; // lock disappeared — retry + if (!tryBreakStaleLock(lockPath, staleMs)) { + // Wait 100ms before retrying (Atomics.wait avoids shell dependency) + try { + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100); + } catch { /* Node < 16 fallback: busy-wait */ } } - // Wait 100ms before retrying (Atomics.wait avoids shell dependency) - try { - Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100); - } catch { /* Node < 16 fallback: busy-wait */ } } } return false; @@ -217,14 +261,16 @@ function releaseLock(lockPath) { * category: string, * createdBy?: string * }} entry + * @param {number} [lockTimeoutMs=30000] optional lock timeout for testability */ -function updateIndex(worktreePath, entry) { +function updateIndex(worktreePath, entry, lockTimeoutMs = 30000) { validateSlug(entry.slug); const featuresDir = path.join(worktreePath, '.features'); + fs.mkdirSync(featuresDir, { recursive: true }); const lockPath = path.join(featuresDir, '.kb.lock'); const indexPath = path.join(featuresDir, 'index.json'); - if (!acquireLock(lockPath)) { + if (!acquireLock(lockPath, lockTimeoutMs)) { throw new Error('Failed to acquire .features/.kb.lock within timeout'); } @@ -252,43 +298,45 @@ function updateIndex(worktreePath, entry) { } /** - * Mark KBs as stale whose referencedFiles overlap with the given file list. - * Unlike checkStaleness (git-based), this programmatically flags overlap. - * Returns the list of slugs that have overlapping referenced files. + * Find KBs whose referencedFiles overlap with the given changed file list. + * Uses directory-boundary matching to avoid false positives (e.g., `src/foo` + * matching `src/foobar`). Returns the list of slugs with overlapping files. * * @param {string} worktreePath * @param {string[]} changedFiles * @returns {string[]} slugs that have overlapping referenced files */ -function markStale(worktreePath, changedFiles) { +function findOverlapping(worktreePath, changedFiles) { const index = loadIndex(worktreePath); if (!index) return []; - const staleSlugsList = []; + const overlappingSlugs = []; for (const [slug, entry] of Object.entries(index.features)) { const refs = entry.referencedFiles || []; const overlap = refs.some(ref => - changedFiles.some(f => f === ref || f.startsWith(ref) || ref.startsWith(f)) + changedFiles.some(f => f === ref || f.startsWith(ref + '/') || ref.startsWith(f + '/')) ); - if (overlap) staleSlugsList.push(slug); + if (overlap) overlappingSlugs.push(slug); } - return staleSlugsList; + return overlappingSlugs; } /** * Remove a KB entry from index.json and delete its directory. - * No-op if the slug does not exist in the index. + * No-op if the slug does not exist in the index or if .features/ is absent. * * @param {string} worktreePath * @param {string} slug + * @param {number} [lockTimeoutMs=30000] optional lock timeout for testability */ -function removeEntry(worktreePath, slug) { +function removeEntry(worktreePath, slug, lockTimeoutMs = 30000) { validateSlug(slug); const featuresDir = path.join(worktreePath, '.features'); + if (!fs.existsSync(featuresDir)) return; const lockPath = path.join(featuresDir, '.kb.lock'); const indexPath = path.join(featuresDir, 'index.json'); - if (!acquireLock(lockPath)) { + if (!acquireLock(lockPath, lockTimeoutMs)) { throw new Error('Failed to acquire .features/.kb.lock within timeout'); } @@ -296,14 +344,11 @@ function removeEntry(worktreePath, slug) { let index = { version: 1, features: {} }; try { index = JSON.parse(fs.readFileSync(indexPath, 'utf8')); - } catch { - return; // nothing to remove - } + } catch { /* no index to modify */ } delete index.features[slug]; fs.writeFileSync(indexPath, JSON.stringify(index, null, 2) + '\n'); - // Remove KB directory const kbDir = path.join(featuresDir, slug); try { fs.rmSync(kbDir, { recursive: true, force: true }); @@ -332,7 +377,7 @@ function listKBs(worktreePath) { // node feature-kb.cjs list // node feature-kb.cjs stale [slug] // node feature-kb.cjs update-index --slug=X --name=Y --directories='[...]' --referencedFiles='[...]' --category=X [--description=Y] [--createdBy=Z] -// node feature-kb.cjs mark-stale [file2...] +// node feature-kb.cjs find-overlapping [file2...] // node feature-kb.cjs remove // --------------------------------------------------------------------------- @@ -359,108 +404,118 @@ if (require.main === module) { ' node feature-kb.cjs list ', ' node feature-kb.cjs stale [slug]', ' node feature-kb.cjs update-index --slug=X --name=Y --directories=\'[...]\' --referencedFiles=\'[...]\' --category=X [--description=Y] [--createdBy=Z]', - ' node feature-kb.cjs mark-stale [file2...]', + ' node feature-kb.cjs find-overlapping [file2...]', ' node feature-kb.cjs remove ', ].join('\n'); - if (!subcommand) { - process.stderr.write(USAGE + '\n'); - process.exit(1); - } - - if (subcommand === 'list') { - const worktreePath = argv[1] ? path.resolve(argv[1]) : null; - if (!worktreePath) { + /** + * Resolve and validate a worktree path argument. + * Exits with an error message if missing or not a valid directory. + * @param {string[]} cliArgv + * @returns {string} + */ + function requireWorktree(cliArgv) { + const p = cliArgv[1] ? path.resolve(cliArgv[1]) : null; + if (!p) { process.stderr.write('Error: missing worktree argument\n' + USAGE + '\n'); process.exit(1); } - const entries = listKBs(worktreePath); - process.stderr.write(`[feature-kb] mode=list worktree=${worktreePath} count=${entries.length}\n`); - process.stdout.write(JSON.stringify(entries, null, 2) + '\n'); - process.exit(0); - } - - if (subcommand === 'stale') { - const worktreePath = argv[1] ? path.resolve(argv[1]) : null; - if (!worktreePath) { - process.stderr.write('Error: missing worktree argument\n' + USAGE + '\n'); + if (!fs.existsSync(p) || !fs.statSync(p).isDirectory()) { + process.stderr.write(`Error: '${p}' is not a valid directory\n`); process.exit(1); } - const slug = argv[2]; - if (slug) { - const result = checkStaleness(worktreePath, slug); - process.stderr.write(`[feature-kb] mode=stale worktree=${worktreePath} slug=${slug} stale=${result.stale}\n`); - process.stdout.write(JSON.stringify(result, null, 2) + '\n'); - } else { - const result = checkAllStaleness(worktreePath); - process.stderr.write(`[feature-kb] mode=stale worktree=${worktreePath} all=true\n`); - process.stdout.write(JSON.stringify(result, null, 2) + '\n'); - } - process.exit(0); + return p; } - if (subcommand === 'update-index') { - const worktreePath = argv[1] ? path.resolve(argv[1]) : null; - if (!worktreePath) { - process.stderr.write('Error: missing worktree argument\n' + USAGE + '\n'); - process.exit(1); - } - const kv = parseKeyValue(argv.slice(2)); - if (!kv.slug || !kv.name || !kv.directories || !kv.referencedFiles || !kv.category) { - process.stderr.write('Error: missing required flags (slug, name, directories, referencedFiles, category)\n' + USAGE + '\n'); - process.exit(1); - } - let directories; - let referencedFiles; - try { - directories = JSON.parse(kv.directories); - referencedFiles = JSON.parse(kv.referencedFiles); - } catch (e) { - process.stderr.write(`Error: --directories and --referencedFiles must be valid JSON arrays: ${e.message}\n`); - process.exit(1); - } - updateIndex(worktreePath, { - slug: kv.slug, - name: kv.name, - description: kv.description, - directories, - referencedFiles, - category: kv.category, - createdBy: kv.createdBy, - }); - process.stderr.write(`[feature-kb] mode=update-index worktree=${worktreePath} slug=${kv.slug}\n`); - process.stdout.write(JSON.stringify({ ok: true, slug: kv.slug }) + '\n'); - process.exit(0); - } + /** @type {Record void>} */ + const dispatch = { + list() { + const worktreePath = requireWorktree(argv); + const entries = listKBs(worktreePath); + process.stderr.write(`[feature-kb] mode=list worktree=${worktreePath} count=${entries.length}\n`); + process.stdout.write(JSON.stringify(entries, null, 2) + '\n'); + process.exit(0); + }, + + stale() { + const worktreePath = requireWorktree(argv); + const slug = argv[2]; + if (slug) { + const result = checkStaleness(worktreePath, slug); + process.stderr.write(`[feature-kb] mode=stale worktree=${worktreePath} slug=${slug} stale=${result.stale}\n`); + process.stdout.write(JSON.stringify(result, null, 2) + '\n'); + } else { + const result = checkAllStaleness(worktreePath); + process.stderr.write(`[feature-kb] mode=stale worktree=${worktreePath} all=true\n`); + process.stdout.write(JSON.stringify(result, null, 2) + '\n'); + } + process.exit(0); + }, + + 'update-index'() { + const worktreePath = requireWorktree(argv); + const kv = parseKeyValue(argv.slice(2)); + if (!kv.slug || !kv.name || !kv.directories || !kv.referencedFiles || !kv.category) { + process.stderr.write('Error: missing required flags (slug, name, directories, referencedFiles, category)\n' + USAGE + '\n'); + process.exit(1); + } + let directories; + let referencedFiles; + try { + directories = JSON.parse(kv.directories); + referencedFiles = JSON.parse(kv.referencedFiles); + } catch (e) { + process.stderr.write(`Error: --directories and --referencedFiles must be valid JSON arrays: ${e.message}\n`); + process.exit(1); + } + updateIndex(worktreePath, { + slug: kv.slug, + name: kv.name, + description: kv.description, + directories, + referencedFiles, + category: kv.category, + createdBy: kv.createdBy, + }); + process.stderr.write(`[feature-kb] mode=update-index worktree=${worktreePath} slug=${kv.slug}\n`); + process.stdout.write(JSON.stringify({ ok: true, slug: kv.slug }) + '\n'); + process.exit(0); + }, + + 'find-overlapping'() { + const worktreePath = requireWorktree(argv); + const changedFiles = argv.slice(2); + const overlapping = findOverlapping(worktreePath, changedFiles); + process.stderr.write(`[feature-kb] mode=find-overlapping worktree=${worktreePath} overlappingCount=${overlapping.length}\n`); + process.stdout.write(JSON.stringify(overlapping, null, 2) + '\n'); + process.exit(0); + }, + + remove() { + const worktreePath = requireWorktree(argv); + const slug = argv[2]; + if (!slug) { + process.stderr.write('Error: missing slug argument\n' + USAGE + '\n'); + process.exit(1); + } + removeEntry(worktreePath, slug); + process.stderr.write(`[feature-kb] mode=remove worktree=${worktreePath} slug=${slug}\n`); + process.stdout.write(JSON.stringify({ ok: true, slug }) + '\n'); + process.exit(0); + }, + }; - if (subcommand === 'mark-stale') { - const worktreePath = argv[1] ? path.resolve(argv[1]) : null; - if (!worktreePath) { - process.stderr.write('Error: missing worktree argument\n' + USAGE + '\n'); - process.exit(1); - } - const changedFiles = argv.slice(2); - const stale = markStale(worktreePath, changedFiles); - process.stderr.write(`[feature-kb] mode=mark-stale worktree=${worktreePath} staleCount=${stale.length}\n`); - process.stdout.write(JSON.stringify(stale, null, 2) + '\n'); - process.exit(0); + if (!subcommand) { + process.stderr.write(USAGE + '\n'); + process.exit(1); } - if (subcommand === 'remove') { - const worktreePath = argv[1] ? path.resolve(argv[1]) : null; - const slug = argv[2]; - if (!worktreePath || !slug) { - process.stderr.write('Error: missing worktree or slug argument\n' + USAGE + '\n'); - process.exit(1); - } - removeEntry(worktreePath, slug); - process.stderr.write(`[feature-kb] mode=remove worktree=${worktreePath} slug=${slug}\n`); - process.stdout.write(JSON.stringify({ ok: true, slug }) + '\n'); - process.exit(0); + const handler = dispatch[subcommand]; + if (!handler) { + process.stderr.write(`Error: unknown subcommand '${subcommand}'\n` + USAGE + '\n'); + process.exit(1); } - - process.stderr.write(`Error: unknown subcommand '${subcommand}'\n` + USAGE + '\n'); - process.exit(1); + handler(); } -module.exports = { loadIndex, loadKBContent, checkStaleness, checkAllStaleness, updateIndex, markStale, removeEntry, listKBs, validateSlug }; +module.exports = { loadIndex, loadKBContent, checkStaleness, checkAllStaleness, updateIndex, findOverlapping, removeEntry, listKBs, validateSlug }; From e2d8a095641335785a68986529816b50b5dd65dd Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 24 Apr 2026 12:45:04 +0300 Subject: [PATCH 07/44] fix(kb): harden CLI permissions and validation (H1, H8, S6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - H1: Remove Bash from --allowedTools in kb create/refresh claude invocations (KB Builder only needs Read,Grep,Glob,Write — Bash is unnecessary surface) - H8: Add validateSlug() call in refresh action when slug is provided - Update FeatureKbModule interface to reflect findOverlapping rename Co-Authored-By: Claude --- src/cli/commands/kb.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/cli/commands/kb.ts b/src/cli/commands/kb.ts index e8c0188..1bae1f3 100644 --- a/src/cli/commands/kb.ts +++ b/src/cli/commands/kb.ts @@ -19,6 +19,7 @@ interface FeatureKbModule { listKBs: (worktreePath: string) => Array<{ slug: string; name: string; category: string; directories: string[]; lastUpdated: string }>; checkAllStaleness: (worktreePath: string) => Record; checkStaleness: (worktreePath: string, slug: string) => { stale: boolean; changedFiles: string[] }; + findOverlapping: (worktreePath: string, changedFiles: string[]) => string[]; removeEntry: (worktreePath: string, slug: string) => void; validateSlug: (slug: string) => void; } @@ -173,7 +174,7 @@ kbCommand const dirList = directories.map((d) => `"${d}"`).join(', '); const s = p.spinner(); - s.start('Running KB Builder agent...'); + s.start('Creating KB...'); const prompt = [ `You are the KB Builder agent. Create a feature knowledge base for the following area:`, @@ -202,7 +203,7 @@ kbCommand try { execFileSync('claude', [ '-p', prompt, - '--allowedTools', 'Read,Grep,Glob,Write,Bash', + '--allowedTools', 'Read,Grep,Glob,Write', '--dangerously-skip-permissions', ], { cwd: worktreePath, @@ -230,6 +231,15 @@ kbCommand .action(async (slug?: string) => { p.intro(color.cyan(slug ? `Refresh KB: ${slug}` : 'Refresh Stale KBs')); + if (slug) { + try { + featureKb.validateSlug(slug); + } catch (err) { + p.log.error(err instanceof Error ? err.message : String(err)); + process.exit(1); + } + } + if (!isClaudeCliAvailable()) { p.log.error('claude CLI not found on PATH. Install Claude Code first.'); process.exit(1); @@ -287,7 +297,7 @@ kbCommand try { execFileSync('claude', [ '-p', prompt, - '--allowedTools', 'Read,Grep,Glob,Write,Bash', + '--allowedTools', 'Read,Grep,Glob,Write', '--dangerously-skip-permissions', ], { cwd: worktreePath, From e14f99d487e6759cd37d172e4b28ad8a88b712d6 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 24 Apr 2026 12:45:35 +0300 Subject: [PATCH 08/44] fix(skills): align feature-kb skill consistency (M4-M6, S1) - M4: Remove feature-kb from devflow-core-skills (it belongs in plan/ambient) - M5: Remove trigger:agent-loaded from apply-feature-kb (no such trigger exists) - M6: Simplify apply-feature-kb allowed-tools to inline format matching apply-knowledge - S1: Add apply-feature-kb, apply-knowledge to kb-builder agent skills list Co-Authored-By: Claude --- shared/agents/kb-builder.md | 2 ++ shared/skills/apply-feature-kb/SKILL.md | 6 +----- src/cli/plugins.ts | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/shared/agents/kb-builder.md b/shared/agents/kb-builder.md index 25d480f..7b694ce 100644 --- a/shared/agents/kb-builder.md +++ b/shared/agents/kb-builder.md @@ -4,6 +4,8 @@ description: Structures codebase exploration into a feature knowledge base model: sonnet skills: - devflow:feature-kb + - devflow:apply-feature-kb + - devflow:apply-knowledge - devflow:worktree-support tools: - Read diff --git a/shared/skills/apply-feature-kb/SKILL.md b/shared/skills/apply-feature-kb/SKILL.md index 291d9f3..00db6bf 100644 --- a/shared/skills/apply-feature-kb/SKILL.md +++ b/shared/skills/apply-feature-kb/SKILL.md @@ -1,11 +1,7 @@ --- name: apply-feature-kb description: Consumption algorithm for FEATURE_KNOWLEDGE variable — pre-computed feature context -trigger: agent-loaded -allowed-tools: - - Read - - Grep - - Glob +allowed-tools: Read --- # Apply Feature Knowledge diff --git a/src/cli/plugins.ts b/src/cli/plugins.ts index 2e0ebbf..1176897 100644 --- a/src/cli/plugins.ts +++ b/src/cli/plugins.ts @@ -47,7 +47,7 @@ export const DEVFLOW_PLUGINS: PluginDefinition[] = [ description: 'Auto-activating quality enforcement skills - foundation layer for all Devflow plugins', commands: [], agents: [], - skills: ['apply-knowledge', 'apply-feature-kb', 'feature-kb', 'software-design', 'docs-framework', 'git', 'boundary-validation', 'research', 'test-driven-development', 'testing'], + skills: ['apply-knowledge', 'apply-feature-kb', 'software-design', 'docs-framework', 'git', 'boundary-validation', 'research', 'test-driven-development', 'testing'], }, { name: 'devflow-plan', From e603e6b8872dc20486ea1c94f6c0630083b94152 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 24 Apr 2026 12:45:53 +0300 Subject: [PATCH 09/44] fix(hooks): harden background scripts with --allowedTools (H1) Constrain background claude -p invocations to the minimum required tool set: - background-learning: --allowedTools 'Read' (analysis only, no writes) - background-memory-update: --allowedTools 'Read,Write' (reads context, writes WORKING-MEMORY.md) Pairing --dangerously-skip-permissions with --allowedTools provides a tighter permission boundary without requiring interactive confirmation prompts. Co-Authored-By: Claude --- scripts/hooks/background-learning | 1 + scripts/hooks/background-memory-update | 1 + 2 files changed, 2 insertions(+) diff --git a/scripts/hooks/background-learning b/scripts/hooks/background-learning index 1f2e327..60ffe65 100755 --- a/scripts/hooks/background-learning +++ b/scripts/hooks/background-learning @@ -391,6 +391,7 @@ run_sonnet_analysis() { DEVFLOW_BG_UPDATER=1 DEVFLOW_BG_LEARNER=1 "$CLAUDE_BIN" -p \ --model "$MODEL" \ --dangerously-skip-permissions \ + --allowedTools 'Read' \ --output-format text \ "$PROMPT" \ > "$RESPONSE_FILE" 2>> "$LOG_FILE" & diff --git a/scripts/hooks/background-memory-update b/scripts/hooks/background-memory-update index 6c9643a..0b5f349 100755 --- a/scripts/hooks/background-memory-update +++ b/scripts/hooks/background-memory-update @@ -268,6 +268,7 @@ TIMEOUT=120 DEVFLOW_BG_UPDATER=1 "$CLAUDE_BIN" -p \ --model haiku \ --dangerously-skip-permissions \ + --allowedTools 'Read,Write' \ --output-format text \ "$PROMPT" \ >> "$LOG_FILE" 2>&1 & From dbc0805ee9c1d7766c84728554b9a4594ec6514d Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 24 Apr 2026 12:46:53 +0300 Subject: [PATCH 10/44] =?UTF-8?q?fix(tests):=20update=20tests=20for=20mark?= =?UTF-8?q?Stale=E2=86=92findOverlapping=20rename=20and=20plugin.json=20sy?= =?UTF-8?q?nc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename markStale → findOverlapping in feature-kb.test.ts import and describe block - Add boundary-matching test case for findOverlapping (no false positives on prefix match) - Remove feature-kb from devflow-core-skills plugin.json (sync with plugins.ts fix) Co-Authored-By: Claude --- .../.claude-plugin/plugin.json | 1 - tests/feature-kb/feature-kb.test.ts | 27 ++++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/plugins/devflow-core-skills/.claude-plugin/plugin.json b/plugins/devflow-core-skills/.claude-plugin/plugin.json index 5ce1c4b..ba25180 100644 --- a/plugins/devflow-core-skills/.claude-plugin/plugin.json +++ b/plugins/devflow-core-skills/.claude-plugin/plugin.json @@ -20,7 +20,6 @@ "skills": [ "apply-knowledge", "apply-feature-kb", - "feature-kb", "software-design", "docs-framework", "git", diff --git a/tests/feature-kb/feature-kb.test.ts b/tests/feature-kb/feature-kb.test.ts index b164f6b..4fa3764 100644 --- a/tests/feature-kb/feature-kb.test.ts +++ b/tests/feature-kb/feature-kb.test.ts @@ -20,7 +20,7 @@ const { checkStaleness, checkAllStaleness, updateIndex, - markStale, + findOverlapping, removeEntry, listKBs, validateSlug, @@ -30,7 +30,7 @@ const { checkStaleness: (worktreePath: string, slug: string) => { stale: boolean; changedFiles: string[] }; checkAllStaleness: (worktreePath: string) => Record; updateIndex: (worktreePath: string, entry: Record) => void; - markStale: (worktreePath: string, changedFiles: string[]) => string[]; + findOverlapping: (worktreePath: string, changedFiles: string[]) => string[]; removeEntry: (worktreePath: string, slug: string) => void; listKBs: (worktreePath: string) => Array<{ slug: string } & Record>; validateSlug: (slug: string) => void; @@ -189,26 +189,33 @@ describe('removeEntry', () => { }); // --------------------------------------------------------------------------- -// markStale +// findOverlapping // --------------------------------------------------------------------------- -describe('markStale', () => { +describe('findOverlapping', () => { it('identifies KBs whose referencedFiles overlap with changed files', () => { const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); - const stale = markStale(tmp, ['src/cli/cli.ts', 'some/other/file.ts']); - expect(stale).toContain('cli-commands'); + const overlapping = findOverlapping(tmp, ['src/cli/cli.ts', 'some/other/file.ts']); + expect(overlapping).toContain('cli-commands'); }); it('returns empty array when no overlap', () => { const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); - const stale = markStale(tmp, ['src/payments/checkout.ts', 'src/unrelated.ts']); - expect(stale).toEqual([]); + const overlapping = findOverlapping(tmp, ['src/payments/checkout.ts', 'src/unrelated.ts']); + expect(overlapping).toEqual([]); }); it('returns empty array for missing index', () => { const tmp = makeTmpFeatureWorktree(); - const stale = markStale(tmp, ['src/cli/cli.ts']); - expect(stale).toEqual([]); + const overlapping = findOverlapping(tmp, ['src/cli/cli.ts']); + expect(overlapping).toEqual([]); + }); + + it('does not match on common prefix without directory boundary', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + // 'src/cli' should NOT match 'src/clitools/foo.ts' (no dir boundary) + const overlapping = findOverlapping(tmp, ['src/clitools/foo.ts']); + expect(overlapping).not.toContain('cli-commands'); }); }); From c49fb9e569e944c4e2ea4478d9be4ab748f4d962 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 24 Apr 2026 12:57:04 +0300 Subject: [PATCH 11/44] refactor(orch): renumber all phases to sequential integers (M7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename fractional and lettered phase designators to sequential integers across all 6 orch skills and 2 plan command files: - plan:orch: Phase 0→1, 0.5→2, 0.6→3, 1→4, 2→5, 3→6, 4→7, 5→8, 6→9, 7→10, 8→11, 8.5→12 - implement:orch: Phase 1.5→2, 2→3, 3→4, 4→5, 5→6, 6→7; also renames mark-stale to find-overlapping per updated CJS API - explore:orch: Phase 0.5→1, 1→2, 2→3, 3→4, 4→5 - resolve:orch: Phase 1.5→2, 2→3, 3→4, 4→5, 5→6, 6→7 - review:orch: Phase 2b→3, 3→4, 4→5, 5→6, 6→7 - debug:orch: Phase 0→1, 1→2, 2→3, 3→4, 4→5, 5→6 - plan.md, plan-teams.md: Phase 14.5→15 All cross-references, Phase Completion Checklists, and descriptive text updated to match. Tests updated to reference new phase numbers. Co-Authored-By: Claude --- plugins/devflow-ambient/agents/kb-builder.md | 2 + plugins/devflow-plan/agents/kb-builder.md | 2 + plugins/devflow-plan/commands/plan-teams.md | 2 +- plugins/devflow-plan/commands/plan.md | 2 +- shared/skills/debug:orch/SKILL.md | 24 ++--- shared/skills/explore:orch/SKILL.md | 20 ++-- shared/skills/implement:orch/SKILL.md | 42 ++++---- shared/skills/plan:orch/SKILL.md | 102 +++++++++---------- shared/skills/resolve:orch/SKILL.md | 30 +++--- shared/skills/review:orch/SKILL.md | 26 ++--- tests/knowledge/command-adoption.test.ts | 16 +-- tests/resolve/knowledge-citation.test.ts | 16 +-- 12 files changed, 144 insertions(+), 140 deletions(-) diff --git a/plugins/devflow-ambient/agents/kb-builder.md b/plugins/devflow-ambient/agents/kb-builder.md index 25d480f..7b694ce 100644 --- a/plugins/devflow-ambient/agents/kb-builder.md +++ b/plugins/devflow-ambient/agents/kb-builder.md @@ -4,6 +4,8 @@ description: Structures codebase exploration into a feature knowledge base model: sonnet skills: - devflow:feature-kb + - devflow:apply-feature-kb + - devflow:apply-knowledge - devflow:worktree-support tools: - Read diff --git a/plugins/devflow-plan/agents/kb-builder.md b/plugins/devflow-plan/agents/kb-builder.md index 25d480f..7b694ce 100644 --- a/plugins/devflow-plan/agents/kb-builder.md +++ b/plugins/devflow-plan/agents/kb-builder.md @@ -4,6 +4,8 @@ description: Structures codebase exploration into a feature knowledge base model: sonnet skills: - devflow:feature-kb + - devflow:apply-feature-kb + - devflow:apply-knowledge - devflow:worktree-support tools: - Read diff --git a/plugins/devflow-plan/commands/plan-teams.md b/plugins/devflow-plan/commands/plan-teams.md index 366c065..483249c 100644 --- a/plugins/devflow-plan/commands/plan-teams.md +++ b/plugins/devflow-plan/commands/plan-teams.md @@ -453,7 +453,7 @@ If the feature does not already have a GitHub issue, create via `gh issue create Display: artifact path, issue URL, gap analysis summary, design review summary, suggested next step (`/implement`). -#### Phase 14.5: Feature KB Generation (Conditional) +#### Phase 15: Feature KB Generation (Conditional) **Requires:** Phase 3 and Phase 8 exploration outputs diff --git a/plugins/devflow-plan/commands/plan.md b/plugins/devflow-plan/commands/plan.md index fd1ea48..71ce251 100644 --- a/plugins/devflow-plan/commands/plan.md +++ b/plugins/devflow-plan/commands/plan.md @@ -400,7 +400,7 @@ Display completion summary: - Design review summary (N anti-patterns found, M mitigated in plan) - Suggested next step: `/implement {artifact-path}` or `/implement #{issue-number}` -#### Phase 14.5: Feature KB Generation (Conditional) +#### Phase 15: Feature KB Generation (Conditional) **Requires:** Phase 3 and Phase 8 exploration outputs diff --git a/shared/skills/debug:orch/SKILL.md b/shared/skills/debug:orch/SKILL.md index 418ba9c..e8d0288 100644 --- a/shared/skills/debug:orch/SKILL.md +++ b/shared/skills/debug:orch/SKILL.md @@ -24,7 +24,7 @@ This is a lightweight variant of `/debug` for ambient ORCHESTRATED mode. Exclude If the orchestrator receives a `WORKTREE_PATH` context (e.g., from multi-worktree workflows), pass it through to all spawned agents. Each agent's "Worktree Support" section handles path resolution. -## Phase 0: Load Knowledge Index (Orchestrator-Local) +## Phase 1: Load Knowledge Index (Orchestrator-Local) **Produces:** KNOWLEDGE_CONTEXT, FEATURE_KNOWLEDGE @@ -43,7 +43,7 @@ Also load feature knowledge: 4. Use `FEATURE_KNOWLEDGE` **locally** for hypothesis generation — feature-specific gotchas and anti-patterns suggest root causes 5. **Do NOT pass to Explore sub-agents** (same asymmetric pattern as KNOWLEDGE_CONTEXT) -## Phase 1: Hypothesize +## Phase 2: Hypothesize **Produces:** HYPOTHESES **Requires:** KNOWLEDGE_CONTEXT @@ -56,7 +56,7 @@ Analyze the bug description, error messages, and conversation context. Generate If fewer than 3 hypotheses are possible, proceed with 2. -## Phase 2: Investigate (Parallel) +## Phase 3: Investigate (Parallel) **Produces:** INVESTIGATION_RESULTS **Requires:** HYPOTHESES @@ -67,7 +67,7 @@ Spawn one `Agent(subagent_type="Explore")` per hypothesis **in a single message* - Must provide file:line references for all evidence - Returns verdict: **CONFIRMED** | **DISPROVED** | **PARTIAL** (some evidence supports, some contradicts) -## Phase 3: Converge +## Phase 4: Converge **Produces:** CONVERGENCE_DECISION **Requires:** INVESTIGATION_RESULTS @@ -78,7 +78,7 @@ Evaluate investigation results: - **Multiple PARTIAL**: Look for a unifying root cause that explains all partial evidence - **All DISPROVED**: Report honestly — "No root cause identified from initial hypotheses." Generate 2-3 second-round hypotheses if conversation context suggests avenues not yet explored. -## Phase 4: Report +## Phase 5: Report **Produces:** ROOT_CAUSE_REPORT **Requires:** CONVERGENCE_DECISION, INVESTIGATION_RESULTS @@ -90,7 +90,7 @@ Present root cause analysis: - **Root cause**: Clear statement of what's wrong and why - **Recommended fix**: Specific changes with file references -## Phase 5: Offer Fix +## Phase 6: Offer Fix **Requires:** ROOT_CAUSE_REPORT @@ -108,11 +108,11 @@ Ask user via AskUserQuestion: "Want me to implement this fix?" Before reporting results, verify every phase was announced: -- [ ] Phase 0: Load Knowledge Index → KNOWLEDGE_CONTEXT captured, FEATURE_KNOWLEDGE loaded (orchestrator-local only, or skipped if `.features/` absent) -- [ ] Phase 1: Hypothesize → HYPOTHESES captured (3-5 distinct) -- [ ] Phase 2: Investigate → INVESTIGATION_RESULTS captured per hypothesis -- [ ] Phase 3: Converge → CONVERGENCE_DECISION captured -- [ ] Phase 4: Report → ROOT_CAUSE_REPORT presented -- [ ] Phase 5: Offer Fix → User asked, response handled +- [ ] Phase 1: Load Knowledge Index → KNOWLEDGE_CONTEXT captured, FEATURE_KNOWLEDGE loaded (orchestrator-local only, or skipped if `.features/` absent) +- [ ] Phase 2: Hypothesize → HYPOTHESES captured (3-5 distinct) +- [ ] Phase 3: Investigate → INVESTIGATION_RESULTS captured per hypothesis +- [ ] Phase 4: Converge → CONVERGENCE_DECISION captured +- [ ] Phase 5: Report → ROOT_CAUSE_REPORT presented +- [ ] Phase 6: Offer Fix → User asked, response handled If any phase is unchecked, execute it before proceeding. diff --git a/shared/skills/explore:orch/SKILL.md b/shared/skills/explore:orch/SKILL.md index bc4a05b..2fd9306 100644 --- a/shared/skills/explore:orch/SKILL.md +++ b/shared/skills/explore:orch/SKILL.md @@ -29,7 +29,7 @@ For GUIDED depth, the main session performs exploration directly: ## ORCHESTRATED Pipeline -### Phase 0.5: Load Feature Knowledge +### Phase 1: Load Feature Knowledge **Produces:** FEATURE_KNOWLEDGE @@ -40,7 +40,7 @@ For GUIDED depth, the main session performs exploration directly: **Explore agent framing**: "The KB is a baseline — your job is to VALIDATE, EXTEND, and CORRECT it, not repeat it. Focus on areas the KB doesn't cover and things that may have changed." -### Phase 1: Orient +### Phase 2: Orient **Produces:** ORIENT_OUTPUT @@ -50,7 +50,7 @@ Spawn `Agent(subagent_type="Skimmer")` to get codebase overview relevant to the - Entry points and key abstractions - Related patterns and conventions -### Phase 2: Explore +### Phase 3: Explore **Produces:** EXPLORE_OUTPUT **Requires:** ORIENT_OUTPUT @@ -63,7 +63,7 @@ Based on Skimmer findings, spawn 2-3 `Agent(subagent_type="Explore")` agents **i Adjust explorer focus based on the specific exploration question. -### Phase 3: Synthesize +### Phase 4: Synthesize **Produces:** MERGED_FINDINGS **Requires:** EXPLORE_OUTPUT @@ -74,7 +74,7 @@ Spawn `Agent(subagent_type="Synthesizer")` in `exploration` mode with combined f - Resolve any contradictions between explorer findings - Organize into the Output format below -### Phase 4: Present +### Phase 5: Present **Requires:** MERGED_FINDINGS @@ -105,10 +105,10 @@ Structured exploration findings with concrete code references: Before presenting findings, verify every phase was announced: -- [ ] Phase 0.5: Load Feature Knowledge → FEATURE_KNOWLEDGE captured (or skipped if `.features/` absent) -- [ ] Phase 1: Orient → ORIENT_OUTPUT captured -- [ ] Phase 2: Explore → EXPLORE_OUTPUT captured -- [ ] Phase 3: Synthesize → MERGED_FINDINGS captured -- [ ] Phase 4: Present → Findings delivered with file:line references +- [ ] Phase 1: Load Feature Knowledge → FEATURE_KNOWLEDGE captured (or skipped if `.features/` absent) +- [ ] Phase 2: Orient → ORIENT_OUTPUT captured +- [ ] Phase 3: Explore → EXPLORE_OUTPUT captured +- [ ] Phase 4: Synthesize → MERGED_FINDINGS captured +- [ ] Phase 5: Present → Findings delivered with file:line references If any phase is unchecked, execute it before proceeding. diff --git a/shared/skills/implement:orch/SKILL.md b/shared/skills/implement:orch/SKILL.md index 3444970..10b24cc 100644 --- a/shared/skills/implement:orch/SKILL.md +++ b/shared/skills/implement:orch/SKILL.md @@ -28,10 +28,10 @@ Before starting the full pipeline, check for re-validation context: If this condition is true → execute **Re-validation Path**: 1. **Branch safety check**: If current branch is protected (main, master, etc.), execute Phase 1 first to create/switch to a work branch. If already on a work branch, skip Phase 1. -2. Skip Phases 2-3 (no Coder needed) -3. Run Phase 4 (FILES_CHANGED Detection) using the existing branch -4. Run Phase 5 (Quality Gates) on detected changes -5. Proceed to Phase 6 (Completion) +2. Skip Phases 3-4 (no Coder needed) +3. Run Phase 5 (FILES_CHANGED Detection) using the existing branch +4. Run Phase 6 (Quality Gates) on detected changes +5. Proceed to Phase 7 (Completion) If not → proceed with the full pipeline below. @@ -56,7 +56,7 @@ Return the branch setup summary." Capture `branch name` and `BASE_BRANCH` from Git agent output for use throughout the pipeline. -## Phase 1.5: Load Feature Knowledge +## Phase 2: Load Feature Knowledge **Produces:** FEATURE_KNOWLEDGE @@ -66,7 +66,7 @@ Capture `branch name` and `BASE_BRANCH` from Git agent output for use throughout 4. For each relevant KB: check staleness via `node scripts/hooks/lib/feature-kb.cjs stale "{worktree}" {slug}`, read `.features/{slug}/KNOWLEDGE.md`, mark stale if needed. 5. Concatenate as `FEATURE_KNOWLEDGE` (or `(none)` if no matches). -## Phase 2: Plan Synthesis +## Phase 3: Plan Synthesis **Produces:** EXECUTION_PLAN **Requires:** FEATURE_BRANCH @@ -82,7 +82,7 @@ Format as structured markdown with: Goal, Steps, Files, Constraints, Decisions. If the orchestrator receives a `WORKTREE_PATH` context (e.g., from multi-worktree workflows), pass it through to all spawned agents. Each agent's "Worktree Support" section handles path resolution. -## Phase 3: Coder Execution +## Phase 4: Coder Execution **Produces:** CODER_COMMITS, PRE_CODER_SHA **Requires:** EXECUTION_PLAN, FEATURE_BRANCH @@ -93,11 +93,11 @@ Spawn `Agent(subagent_type="Coder")` with input variables: - **TASK_ID**: Generated from timestamp (e.g., `task-2026-03-19_1430`) - **TASK_DESCRIPTION**: From conversation context - **BASE_BRANCH**: Current branch (or newly created branch from Phase 1) -- **EXECUTION_PLAN**: From Phase 2 +- **EXECUTION_PLAN**: From Phase 3 - **PATTERNS**: Codebase patterns from conversation context - **CREATE_PR**: `false` (commit only, no push) - **DOMAIN**: Inferred from files in scope (`backend`, `frontend`, `tests`, `fullstack`) -- **FEATURE_KNOWLEDGE**: From Phase 1.5 (or `(none)`) +- **FEATURE_KNOWLEDGE**: From Phase 2 (or `(none)`) **Execution strategy**: Single sequential Coder by default. Parallel Coders only when tasks are self-contained — zero shared contracts, no integration points, different files/modules with no imports between them. @@ -107,7 +107,7 @@ If Coder returns **BLOCKED**, halt the pipeline and report to user. **Handoff artifact** (when HANDOFF_REQUIRED=true): After Coder completes, write the phase summary to `.docs/handoff.md` using the Write tool. The next Coder reads this on startup (see Coder agent Responsibility 1). This survives context compaction — unlike PRIOR_PHASE_SUMMARY which is context-mediated. -## Phase 4: FILES_CHANGED Detection +## Phase 5: FILES_CHANGED Detection **Produces:** FILES_CHANGED **Requires:** PRE_CODER_SHA @@ -120,7 +120,7 @@ git diff --name-only {starting_sha}...HEAD Pass FILES_CHANGED to all quality gate agents. -## Phase 5: Quality Gates +## Phase 6: Quality Gates **Produces:** GATE_RESULTS **Requires:** FILES_CHANGED, CODER_COMMITS @@ -129,22 +129,22 @@ Run sequentially — each gate must pass before the next: 1. `Agent(subagent_type="Validator")` (build + typecheck + lint + tests) — retry up to 2× on failure (Coder fixes between retries) 2. `Agent(subagent_type="Simplifier")` — code clarity and maintainability pass on FILES_CHANGED -3. `Agent(subagent_type="Scrutinizer")` — 9-pillar quality evaluation on FILES_CHANGED, with `FEATURE_KNOWLEDGE` from Phase 1.5 +3. `Agent(subagent_type="Scrutinizer")` — 9-pillar quality evaluation on FILES_CHANGED, with `FEATURE_KNOWLEDGE` from Phase 2 4. `Agent(subagent_type="Validator")` (re-validate after Simplifier/Scrutinizer changes) 5. `Agent(subagent_type="Evaluator")` — verify implementation matches original request — retry up to 2× if misalignment found 6. `Agent(subagent_type="Tester")` — scenario-based acceptance testing from user's perspective — retry up to 2× if QA fails If any gate exhausts retries, halt pipeline and report what passed and what failed. -## Phase 6: Completion +## Phase 7: Completion **Requires:** GATE_RESULTS, FILES_CHANGED, CODER_COMMITS Cleanup: delete `.docs/handoff.md` if it exists (no longer needed after pipeline completes). -After quality gates pass, check if FILES_CHANGED overlap with any KB's `referencedFiles` and mark stale: +After quality gates pass, check for overlapping KBs whose `referencedFiles` intersect FILES_CHANGED: ```bash -node scripts/hooks/lib/feature-kb.cjs mark-stale "{worktree}" {files_changed...} +node scripts/hooks/lib/feature-kb.cjs find-overlapping "{worktree}" {files_changed...} ``` This signals staleness for the next plan cycle. @@ -166,11 +166,11 @@ Report results: Before reporting results, verify every phase was announced: - [ ] Phase 1: Pre-flight → BASE_BRANCH, FEATURE_BRANCH captured -- [ ] Phase 1.5: Load Feature Knowledge → FEATURE_KNOWLEDGE captured (or skipped) -- [ ] Phase 2: Plan Synthesis → EXECUTION_PLAN captured -- [ ] Phase 3: Coder Execution → CODER_COMMITS, PRE_CODER_SHA captured -- [ ] Phase 4: FILES_CHANGED Detection → FILES_CHANGED captured -- [ ] Phase 5: Quality Gates → GATE_RESULTS captured (per gate: pass/fail) -- [ ] Phase 6: Completion → Results reported, stale KBs marked +- [ ] Phase 2: Load Feature Knowledge → FEATURE_KNOWLEDGE captured (or skipped) +- [ ] Phase 3: Plan Synthesis → EXECUTION_PLAN captured +- [ ] Phase 4: Coder Execution → CODER_COMMITS, PRE_CODER_SHA captured +- [ ] Phase 5: FILES_CHANGED Detection → FILES_CHANGED captured +- [ ] Phase 6: Quality Gates → GATE_RESULTS captured (per gate: pass/fail) +- [ ] Phase 7: Completion → Results reported, overlapping KBs checked If any phase is unchecked, execute it before proceeding. diff --git a/shared/skills/plan:orch/SKILL.md b/shared/skills/plan:orch/SKILL.md index 10eca2a..e501f79 100644 --- a/shared/skills/plan:orch/SKILL.md +++ b/shared/skills/plan:orch/SKILL.md @@ -24,8 +24,8 @@ This is a focused variant of the `/plan` command pipeline for ambient ORCHESTRAT For GUIDED depth, the main session performs planning directly: -0. **Discover** — If the planning question is open-ended, ask clarifying questions via AskUserQuestion and present 2-3 approaches with tradeoffs before orienting. Skip if the user's prompt is already specific. If the user says "skip" or "just proceed": skip remaining questions, present inferred scope for confirmation. -0.5. **Load Feature KBs** — Read `.features/index.json` if it exists. Based on the task, identify relevant KBs, read them, and use as context for direct planning. Set `FEATURE_KNOWLEDGE = (none)` if no KBs exist or none are relevant. +1. **Discover** — If the planning question is open-ended, ask clarifying questions via AskUserQuestion and present 2-3 approaches with tradeoffs before orienting. Skip if the user's prompt is already specific. If the user says "skip" or "just proceed": skip remaining questions, present inferred scope for confirmation. +2. **Load Feature KBs** — Read `.features/index.json` if it exists. Based on the task, identify relevant KBs, read them, and use as context for direct planning. Set `FEATURE_KNOWLEDGE = (none)` if no KBs exist or none are relevant. 1. **Spawn Skimmer** — `Agent(subagent_type="Skimmer")` targeting the area of interest. Use orientation output to ground design decisions in real file structures and patterns. 2. **Design** — Using Skimmer findings + loaded pattern/design skills + `FEATURE_KNOWLEDGE`, design the approach directly in main session. Apply `devflow:design-review` skill inline to check the plan for anti-patterns before presenting. 3. **Present** — Deliver structured plan using the Output format below. Use AskUserQuestion for ambiguous design choices. @@ -45,18 +45,18 @@ Before starting the full pipeline, check for prior planning context: **Override**: If the user explicitly requests a fresh plan ("start from scratch", "ignore the old plan", "new approach"), execute the full pipeline regardless of prior artifacts. -If EITHER condition is true (and no override) → execute **Refinement Path** instead of Phases 0-8: +If EITHER condition is true (and no override) → execute **Refinement Path** instead of Phases 1-12: 1. Read the existing plan (disk artifact or conversation context) -2. Run Phase 2 (Explore) targeting only areas affected by the new request +2. Run Phase 5 (Explore) targeting only areas affected by the new request 3. Update the plan with changes, preserving unchanged sections -4. Run Phase 6 (Design Review Lite) on updated sections only +4. Run Phase 9 (Design Review Lite) on updated sections only 5. Present the delta (what changed and why) -6. Proceed to Phase 8 (Persist) if updated plan is substantial +6. Proceed to Phase 11 (Persist) if updated plan is substantial If NEITHER condition is met → proceed with the full pipeline below. -## Phase 0: Load Knowledge Index +## Phase 1: Load Knowledge Index **Produces:** KNOWLEDGE_CONTEXT @@ -68,7 +68,7 @@ KNOWLEDGE_CONTEXT=$(node scripts/hooks/lib/knowledge-context.cjs index "{worktre This produces a compact index of active ADR/PF entries. Pass `KNOWLEDGE_CONTEXT` to Explorer and Designer agents — prior decisions constrain design, known pitfalls inform gap analysis. Agents use `devflow:apply-knowledge` to Read full entry bodies on demand. -## Phase 0.5: Load Feature Knowledge +## Phase 2: Load Feature Knowledge **Produces:** FEATURE_KNOWLEDGE @@ -91,7 +91,7 @@ This produces a compact index of active ADR/PF entries. Pass `KNOWLEDGE_CONTEXT` If no KBs exist or none are relevant, set `FEATURE_KNOWLEDGE = (none)`. -## Phase 0.6: Requirements Discovery +## Phase 3: Requirements Discovery **Produces:** CONSTRAINED_PROBLEM @@ -102,36 +102,36 @@ Before committing to an approach, surface ambiguity through focused Socratic que - Invoked from within another pipeline (pipeline:orch, implement:orch) - Single clear approach exists with no meaningful alternatives -**Skip examples** (proceed directly to Phase 1): +**Skip examples** (proceed directly to Phase 4): - "Add retry with exponential backoff to HttpClient in src/http.ts, max 3 retries, configurable timeout" — specific files, clear behavior, defined parameters - "Implement the design from .docs/design/caching.md" — pre-existing specification -**Discover examples** (run Phase 0.5): +**Discover examples** (run Phase 3): - "Add a caching layer" — open-ended, multiple valid approaches - "Improve the auth flow" — vague scope, unclear what aspects need improvement - "Design a notification system" — system-level, many architectural choices **Process:** -1. **Assess** — Does the request have meaningful ambiguity or multiple valid approaches? If not, skip to Phase 1. +1. **Assess** — Does the request have meaningful ambiguity or multiple valid approaches? If not, skip to Phase 4. 2. **Question** — Ask clarifying questions via AskUserQuestion. Prefer multiple choice (2-4 options) when tradeoffs exist. 3. **Propose approaches** — Present 2-3 options with explicit tradeoffs: - Lead with your recommended approach and why - Each option: 2-3 sentences + key tradeoff (complexity, performance, maintenance) - Final option: "Other — describe your preferred approach" -4. **Confirm** — Get user's choice, then proceed to Phase 1 with a constrained problem. +4. **Confirm** — Get user's choice, then proceed to Phase 4 with a constrained problem. -If the user says "skip", "just proceed", or signals impatience — skip remaining questions, present your inferred understanding (problem, scope, recommended approach) in one message for confirmation, then proceed to Phase 1 after confirmation. This matches /plan Gate 0 behavior. +If the user says "skip", "just proceed", or signals impatience — skip remaining questions, present your inferred understanding (problem, scope, recommended approach) in one message for confirmation, then proceed to Phase 4 after confirmation. This matches /plan Gate 0 behavior. **Question design:** - Ask about constraints and goals, not implementation details - Surface hidden assumptions ("Does this need to handle concurrent writes?") - Reveal scope boundaries ("Just the API layer, or the UI as well?") -## Phase 1: Orient +## Phase 4: Orient **Produces:** ORIENT_OUTPUT -**Requires:** CONSTRAINED_PROBLEM (or original prompt if Phase 0.5 skipped) +**Requires:** CONSTRAINED_PROBLEM (or original prompt if Phase 3 skipped) Spawn `Agent(subagent_type="Skimmer")` to get codebase overview relevant to the planning question: @@ -140,7 +140,7 @@ Spawn `Agent(subagent_type="Skimmer")` to get codebase overview relevant to the - Test patterns and coverage approach - Related prior implementations (similar features, analogous patterns) -## Phase 2: Explore +## Phase 5: Explore **Produces:** EXPLORE_OUTPUT **Requires:** ORIENT_OUTPUT, KNOWLEDGE_CONTEXT, FEATURE_KNOWLEDGE @@ -151,11 +151,11 @@ Based on Skimmer findings, spawn 2-3 `Agent(subagent_type="Explore")` agents **i - **Pattern explorer**: Find existing implementations of similar features to follow as templates - **Constraint explorer**: Identify constraints — test infrastructure, build system, CI requirements, deployment concerns -Each Explore agent receives `KNOWLEDGE_CONTEXT` (from Phase 0), `FEATURE_KNOWLEDGE` (from Phase 0.5), and the instructions: "follow `devflow:apply-knowledge` for KNOWLEDGE_CONTEXT" and "The FEATURE_KNOWLEDGE is a baseline — your job is to VALIDATE, EXTEND, and CORRECT it, not repeat it. Focus exploration on areas the KB doesn't cover and changes since it was last updated." +Each Explore agent receives `KNOWLEDGE_CONTEXT` (from Phase 1), `FEATURE_KNOWLEDGE` (from Phase 2), and the instructions: "follow `devflow:apply-knowledge` for KNOWLEDGE_CONTEXT" and "The FEATURE_KNOWLEDGE is a baseline — your job is to VALIDATE, EXTEND, and CORRECT it, not repeat it. Focus exploration on areas the KB doesn't cover and changes since it was last updated." Adjust explorer focus based on the specific planning question. -## Phase 3: Gap Analysis Lite +## Phase 6: Gap Analysis Lite **Produces:** GAP_OUTPUT **Requires:** EXPLORE_OUTPUT, ORIENT_OUTPUT, KNOWLEDGE_CONTEXT, FEATURE_KNOWLEDGE @@ -170,8 +170,8 @@ KNOWLEDGE_CONTEXT: {knowledge_context} FEATURE_KNOWLEDGE: {feature_knowledge} Artifacts: Planning question: {user's intent} - Exploration findings: {Phase 2 outputs} - Codebase context: {Phase 1 output} + Exploration findings: {Phase 5 outputs} + Codebase context: {Phase 4 output} Identify missing requirements, undefined error states, vague acceptance criteria. Follow devflow:apply-knowledge for KNOWLEDGE_CONTEXT." @@ -182,13 +182,13 @@ KNOWLEDGE_CONTEXT: {knowledge_context} FEATURE_KNOWLEDGE: {feature_knowledge} Artifacts: Planning question: {user's intent} - Exploration findings: {Phase 2 outputs} - Codebase context: {Phase 1 output} + Exploration findings: {Phase 5 outputs} + Codebase context: {Phase 4 output} Identify pattern violations, missing integration points, layering issues. Follow devflow:apply-knowledge for KNOWLEDGE_CONTEXT." ``` -## Phase 4: Synthesize +## Phase 7: Synthesize **Produces:** SYNTHESIS_OUTPUT **Requires:** GAP_OUTPUT, EXPLORE_OUTPUT @@ -198,11 +198,11 @@ Spawn `Agent(subagent_type="Synthesizer")` combining gap analysis and explore ou ``` Agent(subagent_type="Synthesizer"): "Mode: design -Designer outputs: {Phase 3 designer outputs} +Designer outputs: {Phase 6 designer outputs} Combine gap findings with exploration context into blocking vs. should-address categorization." ``` -## Phase 5: Plan +## Phase 8: Plan **Produces:** PLAN_OUTPUT **Requires:** ORIENT_OUTPUT, EXPLORE_OUTPUT, SYNTHESIS_OUTPUT, KNOWLEDGE_CONTEXT, FEATURE_KNOWLEDGE @@ -210,12 +210,12 @@ Combine gap findings with exploration context into blocking vs. should-address c Spawn `Agent(subagent_type="Plan")` with all findings, including `FEATURE_KNOWLEDGE`: - Design implementation approach with file-level specificity -- Reference existing patterns discovered in Phases 1-2 +- Reference existing patterns discovered in Phases 4-5 - Include: architecture decisions, file changes, new files needed, test strategy -- Integrate gap mitigations from Phase 4 into the relevant steps +- Integrate gap mitigations from Phase 7 into the relevant steps - Flag areas where existing patterns conflict with the proposed approach -## Phase 6: Design Review Lite +## Phase 9: Design Review Lite **Produces:** REVIEW_NOTES **Requires:** PLAN_OUTPUT @@ -231,19 +231,19 @@ Main session reviews the plan inline using the loaded `devflow:design-review` sk Note findings directly in the plan presentation. This is inline review — no agent spawn needed. -## Phase 7: Present +## Phase 10: Present **Requires:** PLAN_OUTPUT, SYNTHESIS_OUTPUT, REVIEW_NOTES Present plan to user with: - Implementation approach (file-level) -- Gap analysis findings (from Phase 4 synthesis) -- Design review notes (from Phase 6 inline check) +- Gap analysis findings (from Phase 7 synthesis) +- Design review notes (from Phase 9 inline check) - Risk areas Use AskUserQuestion for any ambiguous design choices that need user input before proceeding to IMPLEMENT. -## Phase 8: Persist +## Phase 11: Persist **Requires:** PLAN_OUTPUT @@ -253,29 +253,29 @@ If the plan is substantial (>10 implementation steps or HIGH/CRITICAL context ri Otherwise: plan stays in conversation context, ready for IMPLEMENT to consume directly. -## Phase 8.5: Feature KB Generation (Conditional) +## Phase 12: Feature KB Generation (Conditional) -If Phase 1-2 explored a feature area that does NOT have a matching KB: +If Phases 4-5 explored a feature area that does NOT have a matching KB: 1. Identify the feature area slug and name from the explored directories 2. Spawn Agent(subagent_type="KB Builder"): ``` "FEATURE_SLUG: {slug} FEATURE_NAME: {name} - EXPLORATION_OUTPUTS: {combined Phase 1 + Phase 2 outputs} + EXPLORATION_OUTPUTS: {combined Phase 4 + Phase 5 outputs} DIRECTORIES: {directory prefixes explored} - KNOWLEDGE_CONTEXT: {from Phase 0}" + KNOWLEDGE_CONTEXT: {from Phase 1}" ``` 3. Report: "Created feature KB: {slug}" Skip if all explored areas already have matching KBs. -If a stale KB was detected in Phase 0.5, also refresh it here — spawn KB Builder with `EXISTING_KB` content + `CHANGED_FILES` from staleness check. +If a stale KB was detected in Phase 2, also refresh it here — spawn KB Builder with `EXISTING_KB` content + `CHANGED_FILES` from staleness check. **Failure handling**: KB Builder failure is **non-blocking**. If it crashes, log the failure and complete the plan workflow normally. **Produces:** `.features/{slug}/KNOWLEDGE.md`, updated `.features/index.json` -**Requires:** Phase 1-2 exploration outputs +**Requires:** Phase 4-5 exploration outputs --- @@ -297,17 +297,17 @@ Structured plan ready to feed into IMPLEMENT/ORCHESTRATED if user proceeds: Before presenting output, verify every phase was announced: -- [ ] Phase 0: Load Knowledge Index → KNOWLEDGE_CONTEXT captured -- [ ] Phase 0.5: Load Feature Knowledge → FEATURE_KNOWLEDGE captured (or skipped if `.features/` absent) -- [ ] Phase 0.6: Requirements Discovery → CONSTRAINED_PROBLEM captured (or skipped with stated reason) -- [ ] Phase 1: Orient → ORIENT_OUTPUT captured -- [ ] Phase 2: Explore → EXPLORE_OUTPUT captured -- [ ] Phase 3: Gap Analysis Lite → GAP_OUTPUT captured -- [ ] Phase 4: Synthesize → SYNTHESIS_OUTPUT captured -- [ ] Phase 5: Plan → PLAN_OUTPUT captured -- [ ] Phase 6: Design Review Lite → REVIEW_NOTES captured -- [ ] Phase 7: Present → Output delivered to user -- [ ] Phase 8: Persist → Artifact written (or skipped with stated reason) -- [ ] Phase 8.5: Feature KB Generation → KB Builder spawned for new feature areas (or skipped if KB exists) +- [ ] Phase 1: Load Knowledge Index → KNOWLEDGE_CONTEXT captured +- [ ] Phase 2: Load Feature Knowledge → FEATURE_KNOWLEDGE captured (or skipped if `.features/` absent) +- [ ] Phase 3: Requirements Discovery → CONSTRAINED_PROBLEM captured (or skipped with stated reason) +- [ ] Phase 4: Orient → ORIENT_OUTPUT captured +- [ ] Phase 5: Explore → EXPLORE_OUTPUT captured +- [ ] Phase 6: Gap Analysis Lite → GAP_OUTPUT captured +- [ ] Phase 7: Synthesize → SYNTHESIS_OUTPUT captured +- [ ] Phase 8: Plan → PLAN_OUTPUT captured +- [ ] Phase 9: Design Review Lite → REVIEW_NOTES captured +- [ ] Phase 10: Present → Output delivered to user +- [ ] Phase 11: Persist → Artifact written (or skipped with stated reason) +- [ ] Phase 12: Feature KB Generation → KB Builder spawned for new feature areas (or skipped if KB exists) If any phase is unchecked, execute it before proceeding. diff --git a/shared/skills/resolve:orch/SKILL.md b/shared/skills/resolve:orch/SKILL.md index 454e1e9..b2a5b29 100644 --- a/shared/skills/resolve:orch/SKILL.md +++ b/shared/skills/resolve:orch/SKILL.md @@ -32,10 +32,10 @@ If no unresolved review found: halt with "No unresolved review found. Run a revi Extract branch slug from the directory path. - -## Phase 1.5: Load Project Knowledge +## Phase 2: Load Project Knowledge **Produces:** KNOWLEDGE_CONTEXT, FEATURE_KNOWLEDGE **Requires:** REVIEW_DIR @@ -48,7 +48,7 @@ Also load feature knowledge: 3. Read matching `.features/{slug}/KNOWLEDGE.md` files, check staleness via `node scripts/hooks/lib/feature-kb.cjs stale "{worktree}" {slug}` 4. Concatenate as `FEATURE_KNOWLEDGE` (or `(none)`) -## Phase 2: Parse Issues +## Phase 3: Parse Issues **Produces:** ISSUES **Requires:** REVIEW_DIR @@ -61,7 +61,7 @@ For each issue, extract: id (generated), file, line, severity, category (blockin If no actionable issues found: "Review is clean — no issues to resolve." → stop. -## Phase 3: Analyze & Batch +## Phase 4: Analyze & Batch **Produces:** BATCHES **Requires:** ISSUES @@ -73,7 +73,7 @@ Group issues by file/function for efficient resolution: Determine execution: batches with no shared files can run in parallel. -## Phase 4: Resolve (Parallel) +## Phase 5: Resolve (Parallel) **Produces:** RESOLUTION_RESULTS **Requires:** BATCHES, KNOWLEDGE_CONTEXT, FEATURE_KNOWLEDGE, BRANCH_SLUG @@ -84,15 +84,15 @@ Each receives: - **ISSUES**: Array of issues in the batch - **BRANCH**: Branch slug - **BATCH_ID**: Identifier for this batch -- **KNOWLEDGE_CONTEXT**: Knowledge index from Phase 1.5 (or `(none)`). Resolvers follow `devflow:apply-knowledge` to Read full ADR/PF bodies on demand. -- **FEATURE_KNOWLEDGE**: Feature area context from Phase 1.5 (or `(none)`). Follow `devflow:apply-feature-kb` for consumption algorithm. +- **KNOWLEDGE_CONTEXT**: Knowledge index from Phase 2 (or `(none)`). Resolvers follow `devflow:apply-knowledge` to Read full ADR/PF bodies on demand. +- **FEATURE_KNOWLEDGE**: Feature area context from Phase 2 (or `(none)`). Follow `devflow:apply-feature-kb` for consumption algorithm. Resolvers follow a 3-tier risk approach: - **Standard fixes**: Applied directly - **Careful fixes** (public API, shared state, >3 files): Systematic refactoring — understand context, plan, test, implement, verify - **Architectural overhaul**: Defer to tech debt (LAST RESORT — avoided at almost all costs, only when complete system redesign required) -## Phase 5: Collect & Simplify +## Phase 6: Collect & Simplify **Produces:** SIMPLIFICATION_RESULTS **Requires:** RESOLUTION_RESULTS @@ -102,7 +102,7 @@ Aggregate results from all Resolver agents: Spawn `Agent(subagent_type="Simplifier")` on all files modified by Resolvers. -## Phase 6: Report +## Phase 7: Report **Requires:** RESOLUTION_RESULTS, SIMPLIFICATION_RESULTS, REVIEW_DIR @@ -129,11 +129,11 @@ Report to user: Before reporting results, verify every phase was announced: - [ ] Phase 1: Target Review Directory → REVIEW_DIR captured -- [ ] Phase 1.5: Load Project Knowledge → KNOWLEDGE_CONTEXT captured, FEATURE_KNOWLEDGE loaded (or skipped if `.features/` absent) -- [ ] Phase 2: Parse Issues → ISSUES captured (or stopped: no actionable issues) -- [ ] Phase 3: Analyze & Batch → BATCHES captured -- [ ] Phase 4: Resolve → RESOLUTION_RESULTS captured per batch -- [ ] Phase 5: Collect & Simplify → SIMPLIFICATION_RESULTS captured -- [ ] Phase 6: Report → resolution-summary.md written +- [ ] Phase 2: Load Project Knowledge → KNOWLEDGE_CONTEXT captured, FEATURE_KNOWLEDGE loaded (or skipped if `.features/` absent) +- [ ] Phase 3: Parse Issues → ISSUES captured (or stopped: no actionable issues) +- [ ] Phase 4: Analyze & Batch → BATCHES captured +- [ ] Phase 5: Resolve → RESOLUTION_RESULTS captured per batch +- [ ] Phase 6: Collect & Simplify → SIMPLIFICATION_RESULTS captured +- [ ] Phase 7: Report → resolution-summary.md written If any phase is unchecked, execute it before proceeding. diff --git a/shared/skills/review:orch/SKILL.md b/shared/skills/review:orch/SKILL.md index 25d0b7c..f73814b 100644 --- a/shared/skills/review:orch/SKILL.md +++ b/shared/skills/review:orch/SKILL.md @@ -44,7 +44,7 @@ Check `.docs/reviews/{branch_slug}/.last-review-head`: Generate timestamp: `YYYY-MM-DD_HHMM` Create directory: `mkdir -p .docs/reviews/{branch_slug}/{timestamp}` -## Phase 2b: Load Knowledge Index +## Phase 3: Load Knowledge Index **Produces:** KNOWLEDGE_CONTEXT, FEATURE_KNOWLEDGE **Requires:** REVIEW_DIR @@ -59,11 +59,11 @@ This produces a compact index of active ADR/PF entries. Pass `KNOWLEDGE_CONTEXT` Also load feature knowledge: 1. Read `.features/index.json` if it exists -2. Based on changed files from Phase 3 file analysis, identify relevant KBs (match file paths against KB `directories` and `referencedFiles`) +2. Based on changed files from Phase 4 file analysis, identify relevant KBs (match file paths against KB `directories` and `referencedFiles`) 3. For each match: check staleness via `node scripts/hooks/lib/feature-kb.cjs stale "{worktree}" {slug}`, read `.features/{slug}/KNOWLEDGE.md` 4. Concatenate as `FEATURE_KNOWLEDGE` (or `(none)`) -## Phase 3: File Analysis +## Phase 4: File Analysis **Produces:** REVIEWER_LIST **Requires:** DIFF_RANGE @@ -86,7 +86,7 @@ Detect conditional reviewers from file types: | `package.json`, lock files | dependencies | | `*.md`, doc files | documentation | -## Phase 4: Reviews (Parallel) +## Phase 5: Reviews (Parallel) **Produces:** REVIEWER_OUTPUTS **Requires:** DIFF_RANGE, REVIEW_DIR, TIMESTAMP, KNOWLEDGE_CONTEXT, FEATURE_KNOWLEDGE, REVIEWER_LIST @@ -104,10 +104,10 @@ Each reviewer receives: - **Branch context**: branch → base_branch - **Output path**: `.docs/reviews/{branch_slug}/{timestamp}/{focus}.md` - **DIFF_COMMAND**: `git diff {DIFF_RANGE}` (incremental or full) -- **KNOWLEDGE_CONTEXT**: compact index from Phase 2b (or `(none)` when absent) — follow `devflow:apply-knowledge` to Read full ADR/PF bodies on demand -- **FEATURE_KNOWLEDGE**: feature area context from Phase 2b (or `(none)`) — follow `devflow:apply-feature-kb` for consumption algorithm +- **KNOWLEDGE_CONTEXT**: compact index from Phase 3 (or `(none)` when absent) — follow `devflow:apply-knowledge` to Read full ADR/PF bodies on demand +- **FEATURE_KNOWLEDGE**: feature area context from Phase 3 (or `(none)`) — follow `devflow:apply-feature-kb` for consumption algorithm -## Phase 5: Synthesis (Parallel) +## Phase 6: Synthesis (Parallel) **Requires:** REVIEWER_OUTPUTS, REVIEW_DIR, PR_INFO @@ -116,7 +116,7 @@ After all reviewers complete, spawn in parallel: 1. `Agent(subagent_type="Git")` with action `comment-pr` — post review summary as PR comment (deduplicate: check existing comments first) 2. `Agent(subagent_type="Synthesizer")` in review mode — reads all `{focus}.md` files from disk, writes `review-summary.md` -## Phase 6: Finalize +## Phase 7: Finalize **Requires:** BRANCH_INFO, REVIEW_DIR @@ -141,10 +141,10 @@ Before reporting results, verify every phase was announced: - [ ] Phase 1: Pre-flight → BRANCH_INFO, PR_INFO captured - [ ] Phase 2: Incremental Detection → DIFF_RANGE, REVIEW_DIR, TIMESTAMP captured -- [ ] Phase 2b: Load Knowledge Index → KNOWLEDGE_CONTEXT captured, FEATURE_KNOWLEDGE loaded (or skipped if `.features/` absent) -- [ ] Phase 3: File Analysis → REVIEWER_LIST captured -- [ ] Phase 4: Reviews → REVIEWER_OUTPUTS written to disk -- [ ] Phase 5: Synthesis → review-summary.md written -- [ ] Phase 6: Finalize → .last-review-head updated, results reported +- [ ] Phase 3: Load Knowledge Index → KNOWLEDGE_CONTEXT captured, FEATURE_KNOWLEDGE loaded (or skipped if `.features/` absent) +- [ ] Phase 4: File Analysis → REVIEWER_LIST captured +- [ ] Phase 5: Reviews → REVIEWER_OUTPUTS written to disk +- [ ] Phase 6: Synthesis → review-summary.md written +- [ ] Phase 7: Finalize → .last-review-head updated, results reported If any phase is unchecked, execute it before proceeding. diff --git a/tests/knowledge/command-adoption.test.ts b/tests/knowledge/command-adoption.test.ts index 48296a4..36d75a4 100644 --- a/tests/knowledge/command-adoption.test.ts +++ b/tests/knowledge/command-adoption.test.ts @@ -58,10 +58,10 @@ describe('debug:orch — knowledge is orchestrator-local, not fanned to Explore it('debug:orch Explore spawn blocks do NOT pass KNOWLEDGE_CONTEXT to sub-agents', () => { const content = loadFile('shared/skills/debug:orch/SKILL.md') - // Find the Phase 2 Investigate section (Explore spawns) - const phase2Section = extractSection(content, 'Phase 2: Investigate', '## Phase 3') + // Find the Phase 3 Investigate section (Explore spawns) + const phase3Section = extractSection(content, 'Phase 3: Investigate', '## Phase 4') // KNOWLEDGE_CONTEXT should NOT appear in Explore spawn block parameters - expect(phase2Section).not.toContain('KNOWLEDGE_CONTEXT') + expect(phase3Section).not.toContain('KNOWLEDGE_CONTEXT') }) }) @@ -234,8 +234,8 @@ describe('plan:orch — knowledge loading phase', () => { it('Explore spawn blocks receive KNOWLEDGE_CONTEXT', () => { const content = loadFile('shared/skills/plan:orch/SKILL.md') // The Explore phase section should mention KNOWLEDGE_CONTEXT - const phase2 = extractSection(content, 'Phase 2: Explore', '## Phase 3') - expect(phase2).toContain('KNOWLEDGE_CONTEXT') + const phase5 = extractSection(content, 'Phase 5: Explore', '## Phase 6') + expect(phase5).toContain('KNOWLEDGE_CONTEXT') }) }) @@ -249,9 +249,9 @@ describe('review:orch — knowledge loading phase', () => { expect(content).toMatch(/[Ll]oad.*[Kk]nowledge|[Kk]nowledge.*[Ll]oad/i) }) - it('Phase 4 Reviews section receives KNOWLEDGE_CONTEXT', () => { + it('Phase 5 Reviews section receives KNOWLEDGE_CONTEXT', () => { const content = loadFile('shared/skills/review:orch/SKILL.md') - const phase4 = extractSection(content, 'Phase 4: Reviews', '## Phase 5') - expect(phase4).toContain('KNOWLEDGE_CONTEXT') + const phase5 = extractSection(content, 'Phase 5: Reviews', '## Phase 6') + expect(phase5).toContain('KNOWLEDGE_CONTEXT') }) }) diff --git a/tests/resolve/knowledge-citation.test.ts b/tests/resolve/knowledge-citation.test.ts index 553f40d..203d480 100644 --- a/tests/resolve/knowledge-citation.test.ts +++ b/tests/resolve/knowledge-citation.test.ts @@ -160,18 +160,18 @@ describe('resolve-teams.md — teams variant parity', () => { describe('resolve:orch SKILL.md — ambient mode parity', () => { const content = loadFile('shared/skills/resolve:orch/SKILL.md'); - it('contains Phase 1.5: Load Project Knowledge between Phase 1 and Phase 2', () => { - expect(content).toMatch(/Phase 1\.5.*Load Project Knowledge/i); + it('contains Phase 2: Load Project Knowledge between Phase 1 and Phase 3', () => { + expect(content).toMatch(/Phase 2.*Load Project Knowledge/i); }); - it('Phase 4 spawn block includes KNOWLEDGE_CONTEXT', () => { - const phase4Section = extractSection(content, '## Phase 4', '## Phase 5'); - expect(phase4Section).toContain('KNOWLEDGE_CONTEXT'); + it('Phase 5 spawn block includes KNOWLEDGE_CONTEXT', () => { + const phase5Section = extractSection(content, '## Phase 5', '## Phase 6'); + expect(phase5Section).toContain('KNOWLEDGE_CONTEXT'); }); - it('Phase 6 (Report) mentions Knowledge Citations (D-B)', () => { - const phase6Section = extractSection(content, '## Phase 6', '## Error Handling'); - expect(phase6Section).toContain('Knowledge Citations'); + it('Phase 7 (Report) mentions Knowledge Citations (D-B)', () => { + const phase7Section = extractSection(content, '## Phase 7', '## Error Handling'); + expect(phase7Section).toContain('Knowledge Citations'); }); }); From 092319a19b1ed5db53e391ac332181b51e105bcb Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 24 Apr 2026 12:58:18 +0300 Subject: [PATCH 12/44] fix(pipeline:orch): update phase references to match renumbered orch skills --- shared/skills/pipeline:orch/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/skills/pipeline:orch/SKILL.md b/shared/skills/pipeline:orch/SKILL.md index 600d094..8ed13f4 100644 --- a/shared/skills/pipeline:orch/SKILL.md +++ b/shared/skills/pipeline:orch/SKILL.md @@ -19,7 +19,7 @@ Meta-orchestrator chaining implement → review → resolve with status reportin ## Feature Knowledge -`FEATURE_KNOWLEDGE` loading is handled by each sub-orchestrator (implement:orch Phase 1.5, review:orch Phase 2b, resolve:orch Phase 1.5). Pipeline:orch does NOT load KBs itself — it delegates to the inner skills which handle loading, staleness checks, and agent distribution independently. +`FEATURE_KNOWLEDGE` loading is handled by each sub-orchestrator (implement:orch Phase 2, review:orch Phase 3, resolve:orch Phase 2). Pipeline:orch does NOT load KBs itself — it delegates to the inner skills which handle loading, staleness checks, and agent distribution independently. ## Cost Communication From 6cd274145dafde6962b0d7e35fee0a0f88b47a6b Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 24 Apr 2026 13:01:10 +0300 Subject: [PATCH 13/44] fix(commands): add FEATURE_KNOWLEDGE to all Coder templates, update phase diagrams (C1, H3) --- .../devflow-implement/commands/implement-teams.md | 10 +++++++--- plugins/devflow-implement/commands/implement.md | 10 +++++++--- plugins/devflow-plan/commands/plan-teams.md | 14 +++++++++----- plugins/devflow-plan/commands/plan.md | 14 +++++++++----- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/plugins/devflow-implement/commands/implement-teams.md b/plugins/devflow-implement/commands/implement-teams.md index 6684492..dd32a9b 100644 --- a/plugins/devflow-implement/commands/implement-teams.md +++ b/plugins/devflow-implement/commands/implement-teams.md @@ -115,6 +115,7 @@ EXECUTION_PLAN: {phase 1 steps} PATTERNS: {patterns from plan document or empty} CREATE_PR: false DOMAIN: {phase 1 domain, e.g., 'backend'} +FEATURE_KNOWLEDGE: {feature_knowledge} HANDOFF_REQUIRED: true" ``` @@ -130,6 +131,7 @@ CREATE_PR: {true if last phase, false otherwise} DOMAIN: {phase N domain, e.g., 'frontend'} PRIOR_PHASE_SUMMARY: {summary from previous Coder} FILES_FROM_PRIOR_PHASE: {list of files created} +FEATURE_KNOWLEDGE: {feature_knowledge} HANDOFF_REQUIRED: {true if not last phase}" ``` @@ -149,7 +151,8 @@ BASE_BRANCH: {base branch} EXECUTION_PLAN: {subtask 1 steps} PATTERNS: {patterns} CREATE_PR: false -DOMAIN: {subtask 1 domain}" +DOMAIN: {subtask 1 domain} +FEATURE_KNOWLEDGE: {feature_knowledge}" Agent(subagent_type="Coder"): # Coder 2 (same message) "TASK_ID: {task-id}-part2 @@ -158,7 +161,8 @@ BASE_BRANCH: {base branch} EXECUTION_PLAN: {subtask 2 steps} PATTERNS: {patterns} CREATE_PR: false -DOMAIN: {subtask 2 domain}" +DOMAIN: {subtask 2 domain} +FEATURE_KNOWLEDGE: {feature_knowledge}" ``` **Independence criteria** (all must be true for PARALLEL_CODERS): @@ -401,7 +405,7 @@ Design and execute scenario-based acceptance tests. Report PASS or FAIL with evi After quality gates pass, mark stale feature KBs based on changed files: ```bash -node scripts/hooks/lib/feature-kb.cjs mark-stale "{worktree}" {files_changed...} +node scripts/hooks/lib/feature-kb.cjs find-overlapping "{worktree}" {files_changed...} ``` Display completion summary with phase status, PR info, and next steps. diff --git a/plugins/devflow-implement/commands/implement.md b/plugins/devflow-implement/commands/implement.md index 7b22beb..e86c634 100644 --- a/plugins/devflow-implement/commands/implement.md +++ b/plugins/devflow-implement/commands/implement.md @@ -122,6 +122,7 @@ EXECUTION_PLAN: {phase 1 steps} PATTERNS: {patterns from plan document or empty} CREATE_PR: false DOMAIN: {phase 1 domain, e.g., 'backend'} +FEATURE_KNOWLEDGE: {feature_knowledge} HANDOFF_REQUIRED: true" ``` @@ -137,6 +138,7 @@ CREATE_PR: {true if last phase, false otherwise} DOMAIN: {phase N domain, e.g., 'frontend'} PRIOR_PHASE_SUMMARY: {summary from previous Coder} FILES_FROM_PRIOR_PHASE: {list of files created} +FEATURE_KNOWLEDGE: {feature_knowledge} HANDOFF_REQUIRED: {true if not last phase}" ``` @@ -156,7 +158,8 @@ BASE_BRANCH: {base branch} EXECUTION_PLAN: {subtask 1 steps} PATTERNS: {patterns} CREATE_PR: false -DOMAIN: {subtask 1 domain}" +DOMAIN: {subtask 1 domain} +FEATURE_KNOWLEDGE: {feature_knowledge}" Agent(subagent_type="Coder"): # Coder 2 (same message) "TASK_ID: {task-id}-part2 @@ -165,7 +168,8 @@ BASE_BRANCH: {base branch} EXECUTION_PLAN: {subtask 2 steps} PATTERNS: {patterns} CREATE_PR: false -DOMAIN: {subtask 2 domain}" +DOMAIN: {subtask 2 domain} +FEATURE_KNOWLEDGE: {feature_knowledge}" ``` **Independence criteria** (all must be true for PARALLEL_CODERS): @@ -356,7 +360,7 @@ Design and execute scenario-based acceptance tests. Report PASS or FAIL with evi After quality gates pass, mark stale feature KBs based on changed files: ```bash -node scripts/hooks/lib/feature-kb.cjs mark-stale "{worktree}" {files_changed...} +node scripts/hooks/lib/feature-kb.cjs find-overlapping "{worktree}" {files_changed...} ``` Display completion summary with phase status, PR info, and next steps. diff --git a/plugins/devflow-plan/commands/plan-teams.md b/plugins/devflow-plan/commands/plan-teams.md index 483249c..b7168c7 100644 --- a/plugins/devflow-plan/commands/plan-teams.md +++ b/plugins/devflow-plan/commands/plan-teams.md @@ -520,11 +520,15 @@ If a stale KB was detected in Phase 2, also refresh it — spawn KB Builder with │ ├─ Phase 12: Designer agent (mode: design-review) │ └─ Phase 13: GATE 2 - Confirm Plan + Design Review ⛔ MANDATORY │ -└─ Block 6: Output - └─ Phase 14: Output - ├─ Store design artifact (.docs/design/) - ├─ Create GitHub issue (optional) - └─ Report summary + next step +├─ Block 6: Output +│ └─ Phase 14: Output +│ ├─ Store design artifact (.docs/design/) +│ ├─ Create GitHub issue (optional) +│ └─ Report summary + next step +│ +└─ Block 7: Feature KB (Conditional) + └─ Phase 15: Feature KB Generation + └─ KB Builder agent (if new/stale feature area) ``` ## Principles diff --git a/plugins/devflow-plan/commands/plan.md b/plugins/devflow-plan/commands/plan.md index 71ce251..56107a6 100644 --- a/plugins/devflow-plan/commands/plan.md +++ b/plugins/devflow-plan/commands/plan.md @@ -478,11 +478,15 @@ If a stale KB was detected in Phase 2, also refresh it — spawn KB Builder with │ └─ Phase 13: GATE 2 - Confirm Plan + Design Review ⛔ MANDATORY │ └─ AskUserQuestion: Final plan approval │ -└─ Block 6: Output - └─ Phase 14: Output - ├─ Store design artifact (.docs/design/) - ├─ Create GitHub issue (optional) - └─ Report summary + next step +├─ Block 6: Output +│ └─ Phase 14: Output +│ ├─ Store design artifact (.docs/design/) +│ ├─ Create GitHub issue (optional) +│ └─ Report summary + next step +│ +└─ Block 7: Feature KB (Conditional) + └─ Phase 15: Feature KB Generation + └─ KB Builder agent (if new/stale feature area) ``` ## Principles From 8c9c73a5a1df89fb19d760866d8dd81c3e2067df Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 24 Apr 2026 13:02:02 +0300 Subject: [PATCH 14/44] docs: update CLAUDE.md with kb.lock gitignore note, debug:orch pattern, phase renumbering (M8, P1) --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index f00717e..8e53574 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,7 +46,7 @@ Commands with Teams Variant ship as `{name}.md` (parallel subagents) and `{name} **Claude Code Flags**: Typed registry (`src/cli/utils/flags.ts`) for managing Claude Code feature flags (env vars and top-level settings). Pure functions `applyFlags`/`stripFlags`/`getDefaultFlags` follow the `applyTeamsConfig`/`stripTeamsConfig` pattern. Initial flags: `tool-search`, `lsp`, `clear-context-on-plan` (default ON), `brief`, `disable-1m-context` (default OFF). Manageable via `devflow flags --enable/--disable/--status/--list`. Stored in manifest `features.flags: string[]`. -**Feature Knowledge Bases**: Per-feature `.features/` directory containing KNOWLEDGE.md files that capture area-specific patterns, conventions, architecture, and gotchas. KBs are created as side-effects of planning (plan:orch Phase 8.5), loaded automatically across all workflows via `FEATURE_KNOWLEDGE` variable (companion to `KNOWLEDGE_CONTEXT`), and use staleness detection via git log against `referencedFiles`. Index at `.features/index.json` (object keyed by slug). Managed via `devflow kb list|create|check|refresh|remove`. KB Builder agent (sonnet) structures exploration outputs into KNOWLEDGE.md. `apply-feature-kb` skill provides consumption algorithm for agents. `.features/.kb.lock` guards concurrent index writes (gitignored). `devflow kb list` — List all feature KBs with staleness status. `devflow kb create ` — Create a new KB via claude -p exploration. `devflow kb check` — Check all KBs for staleness. `devflow kb refresh [slug]` — Refresh stale KB(s). `devflow kb remove ` — Remove a KB and its index entry. +**Feature Knowledge Bases**: Per-feature `.features/` directory containing KNOWLEDGE.md files that capture area-specific patterns, conventions, architecture, and gotchas. KBs are created as side-effects of planning (plan:orch Phase 12), loaded automatically across all workflows via `FEATURE_KNOWLEDGE` variable (companion to `KNOWLEDGE_CONTEXT`), and use staleness detection via git log against `referencedFiles`. Index at `.features/index.json` (object keyed by slug). Managed via `devflow kb list|create|check|refresh|remove`. KB Builder agent (sonnet) structures exploration outputs into KNOWLEDGE.md. `apply-feature-kb` skill provides consumption algorithm for agents. `.features/.kb.lock` is gitignored (transient lock directory for concurrent index writes, added automatically by `devflow init`). `devflow kb list` — List all feature KBs with staleness status. `devflow kb create ` — Create a new KB via claude -p exploration. `devflow kb check` — Check all KBs for staleness. `devflow kb refresh [slug]` — Refresh stale KB(s). `devflow kb remove ` — Remove a KB and its index entry. Note: debug:orch keeps FEATURE_KNOWLEDGE orchestrator-local (investigation workers examine code without pre-loaded context). **Two-Mode Init**: `devflow init` offers Recommended (sensible defaults, quick setup) or Advanced (full interactive flow) after plugin selection. `--recommended` / `--advanced` CLI flags for non-interactive use. Recommended applies: ambient ON, memory ON, learn ON, HUD ON, teams OFF, default-ON flags, .claudeignore ON, auto-install safe-delete if trash CLI detected, user-mode security deny list. From 365e0d6bc42fca2dbc7f7380d9e168889d353e62 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 24 Apr 2026 13:05:58 +0300 Subject: [PATCH 15/44] test(feature-kb): add lock failure, staleness, directory boundary, and CLI tests (T1-T6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - T1: lock failure test — throws when .kb.lock dir is pre-created - T2: positive staleness in a real git repo — detects changed referenced files - T3: directory boundary matching — referencedFiles prefix vs exact-file disambiguation - T4: updateIndex creates missing .features/ directory automatically - T5: removeEntry is a no-op when .features/ directory does not exist - T6: CLI tests for unknown subcommand, invalid worktree, find-overlapping Co-Authored-By: Claude --- tests/feature-kb/feature-kb.test.ts | 143 +++++++++++++++++++++++++++- tests/feature-kb/kb-command.test.ts | 22 +++++ 2 files changed, 162 insertions(+), 3 deletions(-) diff --git a/tests/feature-kb/feature-kb.test.ts b/tests/feature-kb/feature-kb.test.ts index 4fa3764..5c9264d 100644 --- a/tests/feature-kb/feature-kb.test.ts +++ b/tests/feature-kb/feature-kb.test.ts @@ -1,7 +1,8 @@ import { describe, it, expect, afterAll } from 'vitest'; import * as path from 'path'; import { createRequire } from 'module'; -import { writeFileSync, mkdirSync, readFileSync, existsSync } from 'fs'; +import { writeFileSync, mkdirSync, readFileSync, existsSync, rmSync, rmdirSync } from 'fs'; +import { execSync } from 'child_process'; import { SAMPLE_INDEX, SAMPLE_KB_CONTENT, @@ -29,9 +30,9 @@ const { loadKBContent: (worktreePath: string, slug: string) => string | null; checkStaleness: (worktreePath: string, slug: string) => { stale: boolean; changedFiles: string[] }; checkAllStaleness: (worktreePath: string) => Record; - updateIndex: (worktreePath: string, entry: Record) => void; + updateIndex: (worktreePath: string, entry: Record, lockTimeoutMs?: number) => void; findOverlapping: (worktreePath: string, changedFiles: string[]) => string[]; - removeEntry: (worktreePath: string, slug: string) => void; + removeEntry: (worktreePath: string, slug: string, lockTimeoutMs?: number) => void; listKBs: (worktreePath: string) => Array<{ slug: string } & Record>; validateSlug: (slug: string) => void; }; @@ -103,6 +104,62 @@ describe('checkStaleness', () => { }); }); +// --------------------------------------------------------------------------- +// checkStaleness (positive — git repo) +// --------------------------------------------------------------------------- + +// T2: Positive staleness detection in a real git repo +describe('checkStaleness (positive — git repo)', () => { + it('detects stale KB when referenced file changed after lastUpdated', () => { + const tmp = makeTmpFeatureWorktree(); + // Remove auto-created .features dir — we'll set it up after git init + rmSync(path.join(tmp, '.features'), { recursive: true, force: true }); + + // Initialize git repo with initial commit + execSync('git init', { cwd: tmp, stdio: 'pipe' }); + execSync('git config user.email "test@test.com"', { cwd: tmp, stdio: 'pipe' }); + execSync('git config user.name "Test"', { cwd: tmp, stdio: 'pipe' }); + + // Create a tracked file and commit it + const srcDir = path.join(tmp, 'src', 'cli'); + mkdirSync(srcDir, { recursive: true }); + writeFileSync(path.join(srcDir, 'cli.ts'), 'export const v = 1;'); + execSync('git add .', { cwd: tmp, stdio: 'pipe' }); + execSync('git commit -m "initial"', { cwd: tmp, stdio: 'pipe' }); + + // Set lastUpdated to just before now + const lastUpdated = new Date(Date.now() - 5000).toISOString(); + + // Create the index with a KB that references src/cli/cli.ts + const featuresDir = path.join(tmp, '.features'); + mkdirSync(featuresDir, { recursive: true }); + const index = { + version: 1, + features: { + 'my-feature': { + name: 'My Feature', + description: '', + directories: ['src/cli/'], + referencedFiles: ['src/cli/cli.ts'], + category: 'test', + lastUpdated, + createdBy: 'test', + }, + }, + }; + writeFileSync(path.join(featuresDir, 'index.json'), JSON.stringify(index, null, 2)); + + // Modify the file and commit + writeFileSync(path.join(srcDir, 'cli.ts'), 'export const v = 2;'); + execSync('git add .', { cwd: tmp, stdio: 'pipe' }); + execSync('git commit -m "update cli.ts"', { cwd: tmp, stdio: 'pipe' }); + + const result = checkStaleness(tmp, 'my-feature'); + expect(result.stale).toBe(true); + expect(result.changedFiles).toContain('src/cli/cli.ts'); + }); +}); + // --------------------------------------------------------------------------- // updateIndex // --------------------------------------------------------------------------- @@ -159,6 +216,47 @@ describe('updateIndex', () => { expect(updated >= before).toBe(true); expect(updated <= after).toBe(true); }); + + // T1: Lock failure + it('throws when lock cannot be acquired within timeout', () => { + const tmp = makeTmpFeatureWorktree({ version: 1, features: {} }); + const lockPath = path.join(tmp, '.features', '.kb.lock'); + // Pre-create lock directory to simulate a held lock + mkdirSync(lockPath); + + expect(() => updateIndex(tmp, { + slug: 'test-lock', + name: 'Test', + directories: [], + referencedFiles: [], + category: 'test', + }, 500)).toThrow(/lock/i); + + // Lock dir should still exist (not cleaned up by our failed attempt) + expect(existsSync(lockPath)).toBe(true); + // Clean up + rmdirSync(lockPath); + }); + + // T4: Creates missing .features/ directory + it('creates .features/ directory if missing', () => { + const tmp = makeTmpFeatureWorktree(); + // Remove the .features dir + rmSync(path.join(tmp, '.features'), { recursive: true, force: true }); + expect(existsSync(path.join(tmp, '.features'))).toBe(false); + + updateIndex(tmp, { + slug: 'new-feature', + name: 'New Feature', + directories: ['src/new/'], + referencedFiles: ['src/new/index.ts'], + category: 'component-patterns', + }); + + expect(existsSync(path.join(tmp, '.features'))).toBe(true); + const index = loadIndex(tmp); + expect(index!.features['new-feature']).toBeDefined(); + }); }); // --------------------------------------------------------------------------- @@ -186,6 +284,16 @@ describe('removeEntry', () => { const index = loadIndex(tmp); expect(index!.features['cli-commands']).toBeDefined(); }); + + // T5: No-op when .features/ directory is missing + it('is a no-op when .features/ directory does not exist', () => { + const tmp = makeTmpFeatureWorktree(); + rmSync(path.join(tmp, '.features'), { recursive: true, force: true }); + expect(existsSync(path.join(tmp, '.features'))).toBe(false); + + // Should not throw + expect(() => removeEntry(tmp, 'nonexistent')).not.toThrow(); + }); }); // --------------------------------------------------------------------------- @@ -217,6 +325,35 @@ describe('findOverlapping', () => { const overlapping = findOverlapping(tmp, ['src/clitools/foo.ts']); expect(overlapping).not.toContain('cli-commands'); }); + + // T3: Directory boundary matching + // referencedFiles uses no trailing slash so the startsWith(ref + '/') logic + // in findOverlapping correctly matches nested files while rejecting + // files that merely share a prefix (e.g. src/client vs src/cli). + it('matches files under a referenced directory prefix', () => { + const index = { + version: 1, + features: { + 'cli-feature': { + name: 'CLI', + description: '', + directories: ['src/cli/'], + referencedFiles: ['src/cli'], + category: 'test', + lastUpdated: new Date().toISOString(), + createdBy: 'test', + }, + }, + }; + const tmp = makeTmpFeatureWorktree(index); + + // File under the directory prefix — should match (src/cli is a prefix of src/cli/deep/file.ts) + expect(findOverlapping(tmp, ['src/cli/deep/file.ts'])).toContain('cli-feature'); + + // File NOT under the directory but sharing prefix — should NOT match + // (src/cli is NOT a prefix of src/client.ts since there's no / after cli) + expect(findOverlapping(tmp, ['src/client.ts'])).toEqual([]); + }); }); // --------------------------------------------------------------------------- diff --git a/tests/feature-kb/kb-command.test.ts b/tests/feature-kb/kb-command.test.ts index f5043a0..d79bc8e 100644 --- a/tests/feature-kb/kb-command.test.ts +++ b/tests/feature-kb/kb-command.test.ts @@ -56,4 +56,26 @@ describe('feature-kb.cjs CLI', () => { const index = JSON.parse(readFileSync(path.join(tmp, '.features', 'index.json'), 'utf8')); expect(index.features['cli-commands']).toBeUndefined(); }); + + // T6: Unknown command and invalid worktree + it('exits 1 for unknown subcommand', () => { + expect(() => execSync(`node ${CJS_PATH} unknown-command /tmp`, { encoding: 'utf8', stdio: 'pipe' })).toThrow(); + }); + + it('exits 1 for invalid worktree path', () => { + expect(() => execSync(`node ${CJS_PATH} list /nonexistent/path/that/does/not/exist`, { encoding: 'utf8', stdio: 'pipe' })).toThrow(); + }); + + it('find-overlapping returns overlapping slugs', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + const result = execSync(`node ${CJS_PATH} find-overlapping ${tmp} src/cli/cli.ts`, { encoding: 'utf8' }); + const slugs = JSON.parse(result); + expect(slugs).toContain('cli-commands'); + }); + + it('find-overlapping returns empty for non-overlapping files', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + const result = execSync(`node ${CJS_PATH} find-overlapping ${tmp} src/payments/checkout.ts`, { encoding: 'utf8' }); + expect(JSON.parse(result)).toEqual([]); + }); }); From 60b9196deb629e2759b6c49d7d84937a486d48fb Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 24 Apr 2026 13:18:50 +0300 Subject: [PATCH 16/44] fix: address self-review issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - P0: Restore Bash to KB_AGENT_TOOLS — the KB Builder agent needs Bash to run `node scripts/hooks/lib/feature-kb.cjs update-index` after writing KNOWLEDGE.md. Previous hardening (e2d8a09) incorrectly removed it, breaking the create/refresh CLI flows. - P1: Commit uncommitted Coder refactoring (NOT_STALE sentinel, parseGitChangedFiles helper, exitOnInvalidSlug DRY, nullish coalescing). - P2: Fix "mark stale" wording in implement commands to match the actual operation name "find-overlapping" (consistency with implement:orch). --- .../commands/implement-teams.md | 2 +- .../devflow-implement/commands/implement.md | 2 +- scripts/hooks/lib/feature-kb.cjs | 36 +++++++++------ src/cli/commands/kb.ts | 45 +++++++++---------- 4 files changed, 45 insertions(+), 40 deletions(-) diff --git a/plugins/devflow-implement/commands/implement-teams.md b/plugins/devflow-implement/commands/implement-teams.md index dd32a9b..b5da1fd 100644 --- a/plugins/devflow-implement/commands/implement-teams.md +++ b/plugins/devflow-implement/commands/implement-teams.md @@ -403,7 +403,7 @@ Design and execute scenario-based acceptance tests. Report PASS or FAIL with evi **Requires:** VALIDATION_RESULT, ALIGNMENT_RESULT, QA_RESULT, PR_URL -After quality gates pass, mark stale feature KBs based on changed files: +After quality gates pass, check for overlapping feature KBs whose `referencedFiles` intersect the changed files: ```bash node scripts/hooks/lib/feature-kb.cjs find-overlapping "{worktree}" {files_changed...} ``` diff --git a/plugins/devflow-implement/commands/implement.md b/plugins/devflow-implement/commands/implement.md index e86c634..7fba988 100644 --- a/plugins/devflow-implement/commands/implement.md +++ b/plugins/devflow-implement/commands/implement.md @@ -358,7 +358,7 @@ Design and execute scenario-based acceptance tests. Report PASS or FAIL with evi **Requires:** VALIDATION_RESULT, ALIGNMENT_RESULT, QA_RESULT, PR_URL -After quality gates pass, mark stale feature KBs based on changed files: +After quality gates pass, check for overlapping feature KBs whose `referencedFiles` intersect the changed files: ```bash node scripts/hooks/lib/feature-kb.cjs find-overlapping "{worktree}" {files_changed...} ``` diff --git a/scripts/hooks/lib/feature-kb.cjs b/scripts/hooks/lib/feature-kb.cjs index 4078600..0d2144c 100644 --- a/scripts/hooks/lib/feature-kb.cjs +++ b/scripts/hooks/lib/feature-kb.cjs @@ -28,6 +28,18 @@ const fs = require('fs'); const path = require('path'); const { execFileSync } = require('child_process'); +/** Sentinel returned whenever a KB is confirmed non-stale or a fallback is needed. */ +const NOT_STALE = Object.freeze({ stale: false, changedFiles: [] }); + +/** + * Parse git log output into a deduplicated list of changed file paths. + * @param {string} output - raw stdout from `git log --name-only` + * @returns {string[]} + */ +function parseGitChangedFiles(output) { + return [...new Set(output.split('\n').map(l => l.trim()).filter(Boolean))]; +} + /** * Validate that a slug is safe for use as a directory name. * Rejects path traversal attempts (e.g., '../etc'), absolute paths, @@ -114,7 +126,7 @@ function loadKBContent(worktreePath, slug) { function checkStaleness(worktreePath, slug) { validateSlug(slug); const index = loadIndex(worktreePath); - if (!index || !index.features[slug]) return { stale: false, changedFiles: [] }; + if (!index || !index.features[slug]) return NOT_STALE; const entry = index.features[slug]; @@ -122,11 +134,11 @@ function checkStaleness(worktreePath, slug) { // Check if in git repo — use execFileSync to avoid shell injection execFileSync('git', ['rev-parse', '--git-dir'], { cwd: worktreePath, stdio: 'pipe' }); } catch { - return { stale: false, changedFiles: [] }; // Non-git fallback + return NOT_STALE; // Non-git fallback } const files = entry.referencedFiles || []; - if (files.length === 0) return { stale: false, changedFiles: [] }; + if (files.length === 0) return NOT_STALE; try { // Use execFileSync with array args to prevent command injection. @@ -137,10 +149,10 @@ function checkStaleness(worktreePath, slug) { ['log', `--after=${entry.lastUpdated}`, '--name-only', '--pretty=format:', '--', ...files], { cwd: worktreePath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] } ); - const changedFiles = [...new Set(result.split('\n').map(l => l.trim()).filter(Boolean))]; + const changedFiles = parseGitChangedFiles(result); return { stale: changedFiles.length > 0, changedFiles }; } catch { - return { stale: false, changedFiles: [] }; + return NOT_STALE; } } @@ -160,11 +172,7 @@ function checkAllStaleness(worktreePath) { execFileSync('git', ['rev-parse', '--git-dir'], { cwd: worktreePath, stdio: 'pipe' }); } catch { // Non-git repo — all entries non-stale - const results = {}; - for (const slug of Object.keys(index.features)) { - results[slug] = { stale: false, changedFiles: [] }; - } - return results; + return Object.fromEntries(Object.keys(index.features).map(slug => [slug, NOT_STALE])); } const results = {}; @@ -172,7 +180,7 @@ function checkAllStaleness(worktreePath) { const entry = index.features[slug]; const files = entry.referencedFiles || []; if (files.length === 0) { - results[slug] = { stale: false, changedFiles: [] }; + results[slug] = NOT_STALE; continue; } try { @@ -181,10 +189,10 @@ function checkAllStaleness(worktreePath) { ['log', `--after=${entry.lastUpdated}`, '--name-only', '--pretty=format:', '--', ...files], { cwd: worktreePath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] } ); - const changedFiles = [...new Set(result.split('\n').map(l => l.trim()).filter(Boolean))]; + const changedFiles = parseGitChangedFiles(result); results[slug] = { stale: changedFiles.length > 0, changedFiles }; } catch { - results[slug] = { stale: false, changedFiles: [] }; + results[slug] = NOT_STALE; } } return results; @@ -283,7 +291,7 @@ function updateIndex(worktreePath, entry, lockTimeoutMs = 30000) { const existing = index.features[entry.slug] || {}; index.features[entry.slug] = { name: entry.name, - description: entry.description !== undefined ? entry.description : (existing.description || ''), + description: entry.description ?? existing.description ?? '', directories: entry.directories, referencedFiles: entry.referencedFiles, category: entry.category, diff --git a/src/cli/commands/kb.ts b/src/cli/commands/kb.ts index 1bae1f3..f8cece8 100644 --- a/src/cli/commands/kb.ts +++ b/src/cli/commands/kb.ts @@ -29,6 +29,22 @@ const featureKb: FeatureKbModule = _require( path.join(__dirname, '..', '..', '..', 'scripts', 'hooks', 'lib', 'feature-kb.cjs') ); +/** Tools passed to `claude -p` when spawning the KB Builder agent. */ +const KB_AGENT_TOOLS = 'Read,Grep,Glob,Write,Bash'; + +/** + * Validate a KB slug and exit with an error message if invalid. + * Centralises the repeated try/catch pattern across create/refresh/remove. + */ +function exitOnInvalidSlug(slug: string): void { + try { + featureKb.validateSlug(slug); + } catch (err) { + p.log.error(err instanceof Error ? err.message : String(err)); + process.exit(1); + } +} + /** * Get the git root for the current directory, or cwd if not in a git repo. */ @@ -140,13 +156,7 @@ kbCommand .command('create ') .description('Create a new KB via claude -p exploration') .action(async (slug: string) => { - try { - featureKb.validateSlug(slug); - } catch (err) { - p.log.error(err instanceof Error ? err.message : String(err)); - process.exit(1); - } - + exitOnInvalidSlug(slug); p.intro(color.cyan(`Create Feature KB: ${slug}`)); if (!isClaudeCliAvailable()) { @@ -203,7 +213,7 @@ kbCommand try { execFileSync('claude', [ '-p', prompt, - '--allowedTools', 'Read,Grep,Glob,Write', + '--allowedTools', KB_AGENT_TOOLS, '--dangerously-skip-permissions', ], { cwd: worktreePath, @@ -231,14 +241,7 @@ kbCommand .action(async (slug?: string) => { p.intro(color.cyan(slug ? `Refresh KB: ${slug}` : 'Refresh Stale KBs')); - if (slug) { - try { - featureKb.validateSlug(slug); - } catch (err) { - p.log.error(err instanceof Error ? err.message : String(err)); - process.exit(1); - } - } + if (slug) exitOnInvalidSlug(slug); if (!isClaudeCliAvailable()) { p.log.error('claude CLI not found on PATH. Install Claude Code first.'); @@ -297,7 +300,7 @@ kbCommand try { execFileSync('claude', [ '-p', prompt, - '--allowedTools', 'Read,Grep,Glob,Write', + '--allowedTools', KB_AGENT_TOOLS, '--dangerously-skip-permissions', ], { cwd: worktreePath, @@ -322,13 +325,7 @@ kbCommand .command('remove ') .description('Remove a KB and its index entry') .action(async (slug: string) => { - try { - featureKb.validateSlug(slug); - } catch (err) { - p.log.error(err instanceof Error ? err.message : String(err)); - process.exit(1); - } - + exitOnInvalidSlug(slug); p.intro(color.cyan(`Remove KB: ${slug}`)); const confirmed = await p.confirm({ From 3107d8f0ff407e038de35797ad917c3140934fcb Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 24 Apr 2026 13:25:57 +0300 Subject: [PATCH 17/44] docs: fix stale phase reference in file-organization.md --- docs/reference/file-organization.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/file-organization.md b/docs/reference/file-organization.md index 85da5b4..5baf667 100644 --- a/docs/reference/file-organization.md +++ b/docs/reference/file-organization.md @@ -180,8 +180,8 @@ Knowledge files in `.memory/knowledge/` capture decisions and pitfalls that agen | File | Format | Source | Purpose | |------|--------|--------|---------| -| `decisions.md` | ADR-NNN (sequential) | `/implement` Phase 11.5 | Architectural decisions — why choices were made | -| `pitfalls.md` | PF-NNN (sequential) | `/code-review`, `/debug`, `/resolve` | Known gotchas, fragile areas, past bugs | +| `decisions.md` | ADR-NNN (sequential) | `background-learning` | Architectural decisions — why choices were made | +| `pitfalls.md` | PF-NNN (sequential) | `background-learning` | Known gotchas, fragile areas, past bugs | Each file has a `` comment on line 1. SessionStart injects TL;DR headers only (~30-50 tokens). Agents read full files when relevant to their work. Cap: 50 entries per file. From d918bb730d3d2f9af896144469a7ccf5a881ca46 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 24 Apr 2026 17:41:13 +0300 Subject: [PATCH 18/44] docs(skills): fix stale phase numbers in plan:orch, pipeline:orch, review:orch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - plan:orch GUIDED section: renumber steps 1,2,3 → 3,4,5 to fix duplicate numbering after prior renumbering of first two steps - pipeline:orch: update all three sub-orchestrator descriptions from "Phases 1-6" to "Phases 1-7" and add the knowledge-loading phase to each phase name list - review:orch: fix Phase 5 conditional reviewer source reference from "Phase 3" to "Phase 4" after file analysis was renumbered --- shared/skills/pipeline:orch/SKILL.md | 6 +++--- shared/skills/plan:orch/SKILL.md | 6 +++--- shared/skills/review:orch/SKILL.md | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/shared/skills/pipeline:orch/SKILL.md b/shared/skills/pipeline:orch/SKILL.md index 8ed13f4..9beb27c 100644 --- a/shared/skills/pipeline:orch/SKILL.md +++ b/shared/skills/pipeline:orch/SKILL.md @@ -32,7 +32,7 @@ Classification statement must warn about scope: **Produces:** IMPLEMENT_RESULT -Load `devflow:implement:orch` via the Skill tool, then execute its full pipeline (Phases 1-6: pre-flight → plan synthesis → Coder → FILES_CHANGED detection → quality gates → completion). The quality gates are non-negotiable: Validator → Simplifier → Scrutinizer → re-Validate → Evaluator → Tester. +Load `devflow:implement:orch` via the Skill tool, then execute its full pipeline (Phases 1-7: pre-flight → feature knowledge → plan synthesis → Coder → FILES_CHANGED detection → quality gates → completion). The quality gates are non-negotiable: Validator → Simplifier → Scrutinizer → re-Validate → Evaluator → Tester. If implementation returns **BLOCKED**: halt entire pipeline, report blocker. @@ -53,7 +53,7 @@ Auto-proceed to Phase 3. **Produces:** REVIEW_RESULT **Requires:** IMPLEMENT_RESULT -Load `devflow:review:orch` via the Skill tool, then execute its full pipeline (Phases 1-6: pre-flight → incremental detection → file analysis → parallel reviewers (7 core + conditional) → synthesis → finalize). All 7 core reviewers (security, architecture, performance, complexity, consistency, testing, regression) are mandatory. +Load `devflow:review:orch` via the Skill tool, then execute its full pipeline (Phases 1-7: pre-flight → incremental detection → knowledge index → file analysis → parallel reviewers (7 core + conditional) → synthesis → finalize). All 7 core reviewers (security, architecture, performance, complexity, consistency, testing, regression) are mandatory. Report review results (merge recommendation, issue counts). @@ -75,7 +75,7 @@ If **no blocking issues**: **Produces:** RESOLVE_RESULT **Requires:** RESOLVE_DECISION, REVIEW_RESULT -Load `devflow:resolve:orch` via the Skill tool, then execute its full pipeline (Phases 1-6: target review directory → parse issues → analyze & batch → parallel resolvers → collect & simplify → report). +Load `devflow:resolve:orch` via the Skill tool, then execute its full pipeline (Phases 1-7: target review directory → project knowledge → parse issues → analyze & batch → parallel resolvers → collect & simplify → report). ## Phase 6: Summary diff --git a/shared/skills/plan:orch/SKILL.md b/shared/skills/plan:orch/SKILL.md index e501f79..45e0789 100644 --- a/shared/skills/plan:orch/SKILL.md +++ b/shared/skills/plan:orch/SKILL.md @@ -26,9 +26,9 @@ For GUIDED depth, the main session performs planning directly: 1. **Discover** — If the planning question is open-ended, ask clarifying questions via AskUserQuestion and present 2-3 approaches with tradeoffs before orienting. Skip if the user's prompt is already specific. If the user says "skip" or "just proceed": skip remaining questions, present inferred scope for confirmation. 2. **Load Feature KBs** — Read `.features/index.json` if it exists. Based on the task, identify relevant KBs, read them, and use as context for direct planning. Set `FEATURE_KNOWLEDGE = (none)` if no KBs exist or none are relevant. -1. **Spawn Skimmer** — `Agent(subagent_type="Skimmer")` targeting the area of interest. Use orientation output to ground design decisions in real file structures and patterns. -2. **Design** — Using Skimmer findings + loaded pattern/design skills + `FEATURE_KNOWLEDGE`, design the approach directly in main session. Apply `devflow:design-review` skill inline to check the plan for anti-patterns before presenting. -3. **Present** — Deliver structured plan using the Output format below. Use AskUserQuestion for ambiguous design choices. +3. **Spawn Skimmer** — `Agent(subagent_type="Skimmer")` targeting the area of interest. Use orientation output to ground design decisions in real file structures and patterns. +4. **Design** — Using Skimmer findings + loaded pattern/design skills + `FEATURE_KNOWLEDGE`, design the approach directly in main session. Apply `devflow:design-review` skill inline to check the plan for anti-patterns before presenting. +5. **Present** — Deliver structured plan using the Output format below. Use AskUserQuestion for ambiguous design choices. ## Worktree Support diff --git a/shared/skills/review:orch/SKILL.md b/shared/skills/review:orch/SKILL.md index f73814b..11278bf 100644 --- a/shared/skills/review:orch/SKILL.md +++ b/shared/skills/review:orch/SKILL.md @@ -96,7 +96,7 @@ Spawn all reviewers in a single message (parallel execution): **7 core reviewers** (always): - security, architecture, performance, complexity, consistency, testing, regression -**Conditional reviewers** (from Phase 3 file analysis): +**Conditional reviewers** (from Phase 4 file analysis): - typescript, react, database, dependencies, documentation, go, java, python, rust, accessibility, ui-design Each reviewer receives: From 031ba1770421476a561ced2193d64a9aae9fa6f4 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 24 Apr 2026 17:42:13 +0300 Subject: [PATCH 19/44] refactor(feature-kb): extract checkEntryFiles helper and fix removeEntry early-return MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract a private `checkEntryFiles(worktreePath, entry)` helper that runs the git-log staleness check for a single entry. Both `checkStaleness` and `checkAllStaleness` now delegate to it, eliminating the duplicated git-log + parseGitChangedFiles pattern. `checkAllStaleness` retains its git-dir-once optimization — only the per-entry loop body changes. Also restore early-return in `removeEntry`'s parse-failure catch block: when the index file is absent or corrupt, release the lock and return immediately rather than falling through to write an empty index that could clobber a recoverable file. Co-Authored-By: Claude --- plugins/devflow-ambient/agents/knowledge.md | 59 ++++++++++++++++++ plugins/devflow-plan/agents/knowledge.md | 59 ++++++++++++++++++ scripts/hooks/lib/feature-kb.cjs | 69 ++++++++++----------- shared/agents/knowledge.md | 59 ++++++++++++++++++ 4 files changed, 210 insertions(+), 36 deletions(-) create mode 100644 plugins/devflow-ambient/agents/knowledge.md create mode 100644 plugins/devflow-plan/agents/knowledge.md create mode 100644 shared/agents/knowledge.md diff --git a/plugins/devflow-ambient/agents/knowledge.md b/plugins/devflow-ambient/agents/knowledge.md new file mode 100644 index 0000000..7b694ce --- /dev/null +++ b/plugins/devflow-ambient/agents/knowledge.md @@ -0,0 +1,59 @@ +--- +name: KB Builder +description: Structures codebase exploration into a feature knowledge base +model: sonnet +skills: + - devflow:feature-kb + - devflow:apply-feature-kb + - devflow:apply-knowledge + - devflow:worktree-support +tools: + - Read + - Grep + - Glob + - Bash + - Write +--- + +# KB Builder Agent + +## Input Context + +- **FEATURE_SLUG** (required): Kebab-case identifier for the feature area (e.g., `cli-commands`) +- **FEATURE_NAME** (required): Human-readable name (e.g., "CLI Command System") +- **EXPLORATION_OUTPUTS** (required): Combined findings from Skimmer + Explore agents +- **DIRECTORIES** (required): Directory prefixes defining the feature area scope +- **KNOWLEDGE_CONTEXT** (optional): Existing ADR/PF index for cross-referencing +- **EXISTING_KB** (optional): Current KNOWLEDGE.md content when refreshing a stale KB +- **CHANGED_FILES** (optional): Files that changed since last KB update (for refresh) +- **WORKTREE_PATH** (optional): Worktree root for path resolution + +## Responsibilities + +1. **Resolve worktree path**: Use `devflow:worktree-support` to determine the working directory +2. **Orient on feature area**: Read EXPLORATION_OUTPUTS to understand the feature's architecture, patterns, and boundaries +3. **Follow the feature-kb skill**: Execute the 4-phase process (Scan → Extract → Distill → Forge) from `devflow:feature-kb` +4. **Cross-reference knowledge**: If KNOWLEDGE_CONTEXT is provided, reference relevant ADR/PF entries in the KB's "Related" section +5. **Handle refresh**: If EXISTING_KB is provided, update stale sections based on CHANGED_FILES while preserving any manually added content (user edits). Don't regenerate from scratch. +6. **Write KNOWLEDGE.md**: Write to `.features/{FEATURE_SLUG}/KNOWLEDGE.md` (create directory if needed) +7. **Update index**: Run `node scripts/hooks/lib/feature-kb.cjs update-index` with all required fields +8. **Report**: Output what was created/updated + +## Output + +``` +KB_STATUS: created | refreshed +KB_PATH: .features/{slug}/KNOWLEDGE.md +KB_SLUG: {slug} +KB_NAME: {name} +SECTIONS: [list of sections written] +REFERENCED_FILES: [files selected for staleness tracking] +CROSS_REFERENCES: [ADR/PF entries referenced, if any] +``` + +## Boundaries + +- **Only writes to `.features/` directory** — never modify source code +- **Never delete existing KBs** — only create new or refresh existing +- **500-line cap** — if KB exceeds 500 lines, split into focused sub-KBs (each gets own index entry) +- **No push, no external API calls** — local filesystem operations only diff --git a/plugins/devflow-plan/agents/knowledge.md b/plugins/devflow-plan/agents/knowledge.md new file mode 100644 index 0000000..7b694ce --- /dev/null +++ b/plugins/devflow-plan/agents/knowledge.md @@ -0,0 +1,59 @@ +--- +name: KB Builder +description: Structures codebase exploration into a feature knowledge base +model: sonnet +skills: + - devflow:feature-kb + - devflow:apply-feature-kb + - devflow:apply-knowledge + - devflow:worktree-support +tools: + - Read + - Grep + - Glob + - Bash + - Write +--- + +# KB Builder Agent + +## Input Context + +- **FEATURE_SLUG** (required): Kebab-case identifier for the feature area (e.g., `cli-commands`) +- **FEATURE_NAME** (required): Human-readable name (e.g., "CLI Command System") +- **EXPLORATION_OUTPUTS** (required): Combined findings from Skimmer + Explore agents +- **DIRECTORIES** (required): Directory prefixes defining the feature area scope +- **KNOWLEDGE_CONTEXT** (optional): Existing ADR/PF index for cross-referencing +- **EXISTING_KB** (optional): Current KNOWLEDGE.md content when refreshing a stale KB +- **CHANGED_FILES** (optional): Files that changed since last KB update (for refresh) +- **WORKTREE_PATH** (optional): Worktree root for path resolution + +## Responsibilities + +1. **Resolve worktree path**: Use `devflow:worktree-support` to determine the working directory +2. **Orient on feature area**: Read EXPLORATION_OUTPUTS to understand the feature's architecture, patterns, and boundaries +3. **Follow the feature-kb skill**: Execute the 4-phase process (Scan → Extract → Distill → Forge) from `devflow:feature-kb` +4. **Cross-reference knowledge**: If KNOWLEDGE_CONTEXT is provided, reference relevant ADR/PF entries in the KB's "Related" section +5. **Handle refresh**: If EXISTING_KB is provided, update stale sections based on CHANGED_FILES while preserving any manually added content (user edits). Don't regenerate from scratch. +6. **Write KNOWLEDGE.md**: Write to `.features/{FEATURE_SLUG}/KNOWLEDGE.md` (create directory if needed) +7. **Update index**: Run `node scripts/hooks/lib/feature-kb.cjs update-index` with all required fields +8. **Report**: Output what was created/updated + +## Output + +``` +KB_STATUS: created | refreshed +KB_PATH: .features/{slug}/KNOWLEDGE.md +KB_SLUG: {slug} +KB_NAME: {name} +SECTIONS: [list of sections written] +REFERENCED_FILES: [files selected for staleness tracking] +CROSS_REFERENCES: [ADR/PF entries referenced, if any] +``` + +## Boundaries + +- **Only writes to `.features/` directory** — never modify source code +- **Never delete existing KBs** — only create new or refresh existing +- **500-line cap** — if KB exceeds 500 lines, split into focused sub-KBs (each gets own index entry) +- **No push, no external API calls** — local filesystem operations only diff --git a/scripts/hooks/lib/feature-kb.cjs b/scripts/hooks/lib/feature-kb.cjs index 0d2144c..9329328 100644 --- a/scripts/hooks/lib/feature-kb.cjs +++ b/scripts/hooks/lib/feature-kb.cjs @@ -116,27 +116,14 @@ function loadKBContent(worktreePath, slug) { } /** - * Check if a KB is stale by comparing lastUpdated against git log of referencedFiles. - * Returns { stale: false } for non-git repos or when the entry is not found. + * Check staleness for a single feature entry by running git log against its referencedFiles. + * Callers are responsible for the git-dir check — this helper assumes the repo exists. * * @param {string} worktreePath - * @param {string} slug + * @param {FeatureEntry} entry * @returns {{ stale: boolean, changedFiles: string[] }} */ -function checkStaleness(worktreePath, slug) { - validateSlug(slug); - const index = loadIndex(worktreePath); - if (!index || !index.features[slug]) return NOT_STALE; - - const entry = index.features[slug]; - - try { - // Check if in git repo — use execFileSync to avoid shell injection - execFileSync('git', ['rev-parse', '--git-dir'], { cwd: worktreePath, stdio: 'pipe' }); - } catch { - return NOT_STALE; // Non-git fallback - } - +function checkEntryFiles(worktreePath, entry) { const files = entry.referencedFiles || []; if (files.length === 0) return NOT_STALE; @@ -156,6 +143,29 @@ function checkStaleness(worktreePath, slug) { } } +/** + * Check if a KB is stale by comparing lastUpdated against git log of referencedFiles. + * Returns { stale: false } for non-git repos or when the entry is not found. + * + * @param {string} worktreePath + * @param {string} slug + * @returns {{ stale: boolean, changedFiles: string[] }} + */ +function checkStaleness(worktreePath, slug) { + validateSlug(slug); + const index = loadIndex(worktreePath); + if (!index || !index.features[slug]) return NOT_STALE; + + try { + // Check if in git repo — use execFileSync to avoid shell injection + execFileSync('git', ['rev-parse', '--git-dir'], { cwd: worktreePath, stdio: 'pipe' }); + } catch { + return NOT_STALE; // Non-git fallback + } + + return checkEntryFiles(worktreePath, index.features[slug]); +} + /** * Check staleness for all KBs in the index. * Loads the index once and checks git-dir once to avoid N+1 overhead. @@ -177,23 +187,7 @@ function checkAllStaleness(worktreePath) { const results = {}; for (const slug of Object.keys(index.features)) { - const entry = index.features[slug]; - const files = entry.referencedFiles || []; - if (files.length === 0) { - results[slug] = NOT_STALE; - continue; - } - try { - const result = execFileSync( - 'git', - ['log', `--after=${entry.lastUpdated}`, '--name-only', '--pretty=format:', '--', ...files], - { cwd: worktreePath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] } - ); - const changedFiles = parseGitChangedFiles(result); - results[slug] = { stale: changedFiles.length > 0, changedFiles }; - } catch { - results[slug] = NOT_STALE; - } + results[slug] = checkEntryFiles(worktreePath, index.features[slug]); } return results; } @@ -349,10 +343,13 @@ function removeEntry(worktreePath, slug, lockTimeoutMs = 30000) { } try { - let index = { version: 1, features: {} }; + let index; try { index = JSON.parse(fs.readFileSync(indexPath, 'utf8')); - } catch { /* no index to modify */ } + } catch { + releaseLock(lockPath); + return; // nothing to remove — preserve existing (possibly corrupt) file + } delete index.features[slug]; fs.writeFileSync(indexPath, JSON.stringify(index, null, 2) + '\n'); diff --git a/shared/agents/knowledge.md b/shared/agents/knowledge.md new file mode 100644 index 0000000..bd47487 --- /dev/null +++ b/shared/agents/knowledge.md @@ -0,0 +1,59 @@ +--- +name: Knowledge +description: Structures codebase exploration into a feature knowledge base +model: sonnet +skills: + - devflow:feature-kb + - devflow:apply-feature-kb + - devflow:apply-knowledge + - devflow:worktree-support +tools: + - Read + - Grep + - Glob + - Bash + - Write +--- + +# KB Builder Agent + +## Input Context + +- **FEATURE_SLUG** (required): Kebab-case identifier for the feature area (e.g., `cli-commands`) +- **FEATURE_NAME** (required): Human-readable name (e.g., "CLI Command System") +- **EXPLORATION_OUTPUTS** (required): Combined findings from Skimmer + Explore agents +- **DIRECTORIES** (required): Directory prefixes defining the feature area scope +- **KNOWLEDGE_CONTEXT** (optional): Existing ADR/PF index for cross-referencing +- **EXISTING_KB** (optional): Current KNOWLEDGE.md content when refreshing a stale KB +- **CHANGED_FILES** (optional): Files that changed since last KB update (for refresh) +- **WORKTREE_PATH** (optional): Worktree root for path resolution + +## Responsibilities + +1. **Resolve worktree path**: Use `devflow:worktree-support` to determine the working directory +2. **Orient on feature area**: Read EXPLORATION_OUTPUTS to understand the feature's architecture, patterns, and boundaries +3. **Follow the feature-kb skill**: Execute the 4-phase process (Scan → Extract → Distill → Forge) from `devflow:feature-kb` +4. **Cross-reference knowledge**: If KNOWLEDGE_CONTEXT is provided, reference relevant ADR/PF entries in the KB's "Related" section +5. **Handle refresh**: If EXISTING_KB is provided, update stale sections based on CHANGED_FILES while preserving any manually added content (user edits). Don't regenerate from scratch. +6. **Write KNOWLEDGE.md**: Write to `.features/{FEATURE_SLUG}/KNOWLEDGE.md` (create directory if needed) +7. **Update index**: Run `node scripts/hooks/lib/feature-kb.cjs update-index` with all required fields +8. **Report**: Output what was created/updated + +## Output + +``` +KB_STATUS: created | refreshed +KB_PATH: .features/{slug}/KNOWLEDGE.md +KB_SLUG: {slug} +KB_NAME: {name} +SECTIONS: [list of sections written] +REFERENCED_FILES: [files selected for staleness tracking] +CROSS_REFERENCES: [ADR/PF entries referenced, if any] +``` + +## Boundaries + +- **Only writes to `.features/` directory** — never modify source code +- **Never delete existing KBs** — only create new or refresh existing +- **500-line cap** — if KB exceeds 500 lines, split into focused sub-KBs (each gets own index entry) +- **No push, no external API calls** — local filesystem operations only From 33048f3ae1d5483b19563a2b34e9cdc8cc6c982a Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 24 Apr 2026 17:44:50 +0300 Subject: [PATCH 20/44] refactor: rename kb-builder agent to knowledge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename shared/agents/kb-builder.md → knowledge.md and update all references across plugin manifests, CLI source, commands, and skills. Update shared agent count from 12 to 13 in file-organization.md. Co-Authored-By: Claude --- CLAUDE.md | 2 +- docs/reference/file-organization.md | 4 +- .../.claude-plugin/plugin.json | 2 +- plugins/devflow-ambient/agents/kb-builder.md | 59 ------------------- plugins/devflow-ambient/agents/knowledge.md | 4 +- .../devflow-plan/.claude-plugin/plugin.json | 2 +- plugins/devflow-plan/agents/kb-builder.md | 59 ------------------- plugins/devflow-plan/agents/knowledge.md | 4 +- plugins/devflow-plan/commands/plan-teams.md | 10 ++-- plugins/devflow-plan/commands/plan.md | 10 ++-- shared/agents/kb-builder.md | 59 ------------------- shared/agents/knowledge.md | 2 +- shared/skills/plan:orch/SKILL.md | 8 +-- src/cli/commands/kb.ts | 6 +- src/cli/plugins.ts | 4 +- 15 files changed, 29 insertions(+), 206 deletions(-) delete mode 100644 plugins/devflow-ambient/agents/kb-builder.md delete mode 100644 plugins/devflow-plan/agents/kb-builder.md delete mode 100644 shared/agents/kb-builder.md diff --git a/CLAUDE.md b/CLAUDE.md index 8e53574..813d8fc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -153,7 +153,7 @@ Working memory files live in a dedicated `.memory/` directory: - `/self-review` — Simplifier then Scrutinizer (sequential); consumes knowledge via index + on-demand Read via `devflow:apply-knowledge` - `/audit-claude` — CLAUDE.md audit (optional plugin) -**Shared agents** (13): git, synthesizer, skimmer, simplifier, coder, reviewer, resolver, evaluator, tester, scrutinizer, validator, designer, kb-builder +**Shared agents** (13): git, synthesizer, skimmer, simplifier, coder, reviewer, resolver, evaluator, tester, scrutinizer, validator, designer, knowledge **Plugin-specific agents** (1): claude-md-auditor diff --git a/docs/reference/file-organization.md b/docs/reference/file-organization.md index 5baf667..1c26687 100644 --- a/docs/reference/file-organization.md +++ b/docs/reference/file-organization.md @@ -15,7 +15,7 @@ devflow/ │ │ │ └── references/ │ │ ├── software-design/ │ │ └── ... -│ └── agents/ # SINGLE SOURCE OF TRUTH (12 shared agents) +│ └── agents/ # SINGLE SOURCE OF TRUTH (13 shared agents) │ ├── git.md │ ├── synthesizer.md │ ├── coder.md @@ -138,7 +138,7 @@ Skills and agents are **not duplicated** in git. Instead: ### Shared vs Plugin-Specific Agents -- **Shared** (12): `git`, `synthesizer`, `skimmer`, `simplifier`, `coder`, `reviewer`, `resolver`, `evaluator`, `tester`, `scrutinizer`, `validator`, `designer` +- **Shared** (13): `git`, `synthesizer`, `skimmer`, `simplifier`, `coder`, `reviewer`, `resolver`, `evaluator`, `tester`, `scrutinizer`, `validator`, `designer`, `knowledge` - **Plugin-specific** (1): `claude-md-auditor` — committed directly in its plugin ## Settings Override diff --git a/plugins/devflow-ambient/.claude-plugin/plugin.json b/plugins/devflow-ambient/.claude-plugin/plugin.json index c9492e4..dabdbfb 100644 --- a/plugins/devflow-ambient/.claude-plugin/plugin.json +++ b/plugins/devflow-ambient/.claude-plugin/plugin.json @@ -28,7 +28,7 @@ "synthesizer", "resolver", "designer", - "kb-builder" + "knowledge" ], "skills": [ "router", diff --git a/plugins/devflow-ambient/agents/kb-builder.md b/plugins/devflow-ambient/agents/kb-builder.md deleted file mode 100644 index 7b694ce..0000000 --- a/plugins/devflow-ambient/agents/kb-builder.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -name: KB Builder -description: Structures codebase exploration into a feature knowledge base -model: sonnet -skills: - - devflow:feature-kb - - devflow:apply-feature-kb - - devflow:apply-knowledge - - devflow:worktree-support -tools: - - Read - - Grep - - Glob - - Bash - - Write ---- - -# KB Builder Agent - -## Input Context - -- **FEATURE_SLUG** (required): Kebab-case identifier for the feature area (e.g., `cli-commands`) -- **FEATURE_NAME** (required): Human-readable name (e.g., "CLI Command System") -- **EXPLORATION_OUTPUTS** (required): Combined findings from Skimmer + Explore agents -- **DIRECTORIES** (required): Directory prefixes defining the feature area scope -- **KNOWLEDGE_CONTEXT** (optional): Existing ADR/PF index for cross-referencing -- **EXISTING_KB** (optional): Current KNOWLEDGE.md content when refreshing a stale KB -- **CHANGED_FILES** (optional): Files that changed since last KB update (for refresh) -- **WORKTREE_PATH** (optional): Worktree root for path resolution - -## Responsibilities - -1. **Resolve worktree path**: Use `devflow:worktree-support` to determine the working directory -2. **Orient on feature area**: Read EXPLORATION_OUTPUTS to understand the feature's architecture, patterns, and boundaries -3. **Follow the feature-kb skill**: Execute the 4-phase process (Scan → Extract → Distill → Forge) from `devflow:feature-kb` -4. **Cross-reference knowledge**: If KNOWLEDGE_CONTEXT is provided, reference relevant ADR/PF entries in the KB's "Related" section -5. **Handle refresh**: If EXISTING_KB is provided, update stale sections based on CHANGED_FILES while preserving any manually added content (user edits). Don't regenerate from scratch. -6. **Write KNOWLEDGE.md**: Write to `.features/{FEATURE_SLUG}/KNOWLEDGE.md` (create directory if needed) -7. **Update index**: Run `node scripts/hooks/lib/feature-kb.cjs update-index` with all required fields -8. **Report**: Output what was created/updated - -## Output - -``` -KB_STATUS: created | refreshed -KB_PATH: .features/{slug}/KNOWLEDGE.md -KB_SLUG: {slug} -KB_NAME: {name} -SECTIONS: [list of sections written] -REFERENCED_FILES: [files selected for staleness tracking] -CROSS_REFERENCES: [ADR/PF entries referenced, if any] -``` - -## Boundaries - -- **Only writes to `.features/` directory** — never modify source code -- **Never delete existing KBs** — only create new or refresh existing -- **500-line cap** — if KB exceeds 500 lines, split into focused sub-KBs (each gets own index entry) -- **No push, no external API calls** — local filesystem operations only diff --git a/plugins/devflow-ambient/agents/knowledge.md b/plugins/devflow-ambient/agents/knowledge.md index 7b694ce..c12779e 100644 --- a/plugins/devflow-ambient/agents/knowledge.md +++ b/plugins/devflow-ambient/agents/knowledge.md @@ -1,5 +1,5 @@ --- -name: KB Builder +name: Knowledge description: Structures codebase exploration into a feature knowledge base model: sonnet skills: @@ -15,7 +15,7 @@ tools: - Write --- -# KB Builder Agent +# Knowledge Agent ## Input Context diff --git a/plugins/devflow-plan/.claude-plugin/plugin.json b/plugins/devflow-plan/.claude-plugin/plugin.json index 2374028..10410a8 100644 --- a/plugins/devflow-plan/.claude-plugin/plugin.json +++ b/plugins/devflow-plan/.claude-plugin/plugin.json @@ -20,7 +20,7 @@ "skimmer", "synthesizer", "designer", - "kb-builder" + "knowledge" ], "skills": [ "agent-teams", diff --git a/plugins/devflow-plan/agents/kb-builder.md b/plugins/devflow-plan/agents/kb-builder.md deleted file mode 100644 index 7b694ce..0000000 --- a/plugins/devflow-plan/agents/kb-builder.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -name: KB Builder -description: Structures codebase exploration into a feature knowledge base -model: sonnet -skills: - - devflow:feature-kb - - devflow:apply-feature-kb - - devflow:apply-knowledge - - devflow:worktree-support -tools: - - Read - - Grep - - Glob - - Bash - - Write ---- - -# KB Builder Agent - -## Input Context - -- **FEATURE_SLUG** (required): Kebab-case identifier for the feature area (e.g., `cli-commands`) -- **FEATURE_NAME** (required): Human-readable name (e.g., "CLI Command System") -- **EXPLORATION_OUTPUTS** (required): Combined findings from Skimmer + Explore agents -- **DIRECTORIES** (required): Directory prefixes defining the feature area scope -- **KNOWLEDGE_CONTEXT** (optional): Existing ADR/PF index for cross-referencing -- **EXISTING_KB** (optional): Current KNOWLEDGE.md content when refreshing a stale KB -- **CHANGED_FILES** (optional): Files that changed since last KB update (for refresh) -- **WORKTREE_PATH** (optional): Worktree root for path resolution - -## Responsibilities - -1. **Resolve worktree path**: Use `devflow:worktree-support` to determine the working directory -2. **Orient on feature area**: Read EXPLORATION_OUTPUTS to understand the feature's architecture, patterns, and boundaries -3. **Follow the feature-kb skill**: Execute the 4-phase process (Scan → Extract → Distill → Forge) from `devflow:feature-kb` -4. **Cross-reference knowledge**: If KNOWLEDGE_CONTEXT is provided, reference relevant ADR/PF entries in the KB's "Related" section -5. **Handle refresh**: If EXISTING_KB is provided, update stale sections based on CHANGED_FILES while preserving any manually added content (user edits). Don't regenerate from scratch. -6. **Write KNOWLEDGE.md**: Write to `.features/{FEATURE_SLUG}/KNOWLEDGE.md` (create directory if needed) -7. **Update index**: Run `node scripts/hooks/lib/feature-kb.cjs update-index` with all required fields -8. **Report**: Output what was created/updated - -## Output - -``` -KB_STATUS: created | refreshed -KB_PATH: .features/{slug}/KNOWLEDGE.md -KB_SLUG: {slug} -KB_NAME: {name} -SECTIONS: [list of sections written] -REFERENCED_FILES: [files selected for staleness tracking] -CROSS_REFERENCES: [ADR/PF entries referenced, if any] -``` - -## Boundaries - -- **Only writes to `.features/` directory** — never modify source code -- **Never delete existing KBs** — only create new or refresh existing -- **500-line cap** — if KB exceeds 500 lines, split into focused sub-KBs (each gets own index entry) -- **No push, no external API calls** — local filesystem operations only diff --git a/plugins/devflow-plan/agents/knowledge.md b/plugins/devflow-plan/agents/knowledge.md index 7b694ce..c12779e 100644 --- a/plugins/devflow-plan/agents/knowledge.md +++ b/plugins/devflow-plan/agents/knowledge.md @@ -1,5 +1,5 @@ --- -name: KB Builder +name: Knowledge description: Structures codebase exploration into a feature knowledge base model: sonnet skills: @@ -15,7 +15,7 @@ tools: - Write --- -# KB Builder Agent +# Knowledge Agent ## Input Context diff --git a/plugins/devflow-plan/commands/plan-teams.md b/plugins/devflow-plan/commands/plan-teams.md index b7168c7..83c4eb7 100644 --- a/plugins/devflow-plan/commands/plan-teams.md +++ b/plugins/devflow-plan/commands/plan-teams.md @@ -457,10 +457,10 @@ Display: artifact path, issue URL, gap analysis summary, design review summary, **Requires:** Phase 3 and Phase 8 exploration outputs -If the exploration in earlier phases covered a feature area without an existing KB, spawn KB Builder agent to create one: +If the exploration in earlier phases covered a feature area without an existing KB, spawn Knowledge agent to create one: ``` -Agent(subagent_type="KB Builder"): +Agent(subagent_type="Knowledge"): "FEATURE_SLUG: {slug} FEATURE_NAME: {name} EXPLORATION_OUTPUTS: {combined exploration outputs from Phases 3+8} @@ -470,9 +470,9 @@ KNOWLEDGE_CONTEXT: {from Phase 2}" Skip if all explored areas already have matching KBs. -If a stale KB was detected in Phase 2, also refresh it — spawn KB Builder with `EXISTING_KB` content + `CHANGED_FILES` from staleness check. +If a stale KB was detected in Phase 2, also refresh it — spawn Knowledge agent with `EXISTING_KB` content + `CHANGED_FILES` from staleness check. -**Failure handling**: KB Builder failure is **non-blocking**. If it crashes, log the failure and complete the plan workflow normally. +**Failure handling**: Knowledge agent failure is **non-blocking**. If it crashes, log the failure and complete the plan workflow normally. --- @@ -528,7 +528,7 @@ If a stale KB was detected in Phase 2, also refresh it — spawn KB Builder with │ └─ Block 7: Feature KB (Conditional) └─ Phase 15: Feature KB Generation - └─ KB Builder agent (if new/stale feature area) + └─ Knowledge agent (if new/stale feature area) ``` ## Principles diff --git a/plugins/devflow-plan/commands/plan.md b/plugins/devflow-plan/commands/plan.md index 56107a6..e29aa46 100644 --- a/plugins/devflow-plan/commands/plan.md +++ b/plugins/devflow-plan/commands/plan.md @@ -404,10 +404,10 @@ Display completion summary: **Requires:** Phase 3 and Phase 8 exploration outputs -If the exploration in earlier phases covered a feature area without an existing KB, spawn KB Builder agent to create one: +If the exploration in earlier phases covered a feature area without an existing KB, spawn Knowledge agent to create one: ``` -Agent(subagent_type="KB Builder"): +Agent(subagent_type="Knowledge"): "FEATURE_SLUG: {slug} FEATURE_NAME: {name} EXPLORATION_OUTPUTS: {combined exploration outputs from Phases 3+8} @@ -417,9 +417,9 @@ KNOWLEDGE_CONTEXT: {from Phase 2}" Skip if all explored areas already have matching KBs. -If a stale KB was detected in Phase 2, also refresh it — spawn KB Builder with `EXISTING_KB` content + `CHANGED_FILES` from staleness check. +If a stale KB was detected in Phase 2, also refresh it — spawn Knowledge agent with `EXISTING_KB` content + `CHANGED_FILES` from staleness check. -**Failure handling**: KB Builder failure is **non-blocking**. If it crashes, log the failure and complete the plan workflow normally. +**Failure handling**: Knowledge agent failure is **non-blocking**. If it crashes, log the failure and complete the plan workflow normally. --- @@ -486,7 +486,7 @@ If a stale KB was detected in Phase 2, also refresh it — spawn KB Builder with │ └─ Block 7: Feature KB (Conditional) └─ Phase 15: Feature KB Generation - └─ KB Builder agent (if new/stale feature area) + └─ Knowledge agent (if new/stale feature area) ``` ## Principles diff --git a/shared/agents/kb-builder.md b/shared/agents/kb-builder.md deleted file mode 100644 index 7b694ce..0000000 --- a/shared/agents/kb-builder.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -name: KB Builder -description: Structures codebase exploration into a feature knowledge base -model: sonnet -skills: - - devflow:feature-kb - - devflow:apply-feature-kb - - devflow:apply-knowledge - - devflow:worktree-support -tools: - - Read - - Grep - - Glob - - Bash - - Write ---- - -# KB Builder Agent - -## Input Context - -- **FEATURE_SLUG** (required): Kebab-case identifier for the feature area (e.g., `cli-commands`) -- **FEATURE_NAME** (required): Human-readable name (e.g., "CLI Command System") -- **EXPLORATION_OUTPUTS** (required): Combined findings from Skimmer + Explore agents -- **DIRECTORIES** (required): Directory prefixes defining the feature area scope -- **KNOWLEDGE_CONTEXT** (optional): Existing ADR/PF index for cross-referencing -- **EXISTING_KB** (optional): Current KNOWLEDGE.md content when refreshing a stale KB -- **CHANGED_FILES** (optional): Files that changed since last KB update (for refresh) -- **WORKTREE_PATH** (optional): Worktree root for path resolution - -## Responsibilities - -1. **Resolve worktree path**: Use `devflow:worktree-support` to determine the working directory -2. **Orient on feature area**: Read EXPLORATION_OUTPUTS to understand the feature's architecture, patterns, and boundaries -3. **Follow the feature-kb skill**: Execute the 4-phase process (Scan → Extract → Distill → Forge) from `devflow:feature-kb` -4. **Cross-reference knowledge**: If KNOWLEDGE_CONTEXT is provided, reference relevant ADR/PF entries in the KB's "Related" section -5. **Handle refresh**: If EXISTING_KB is provided, update stale sections based on CHANGED_FILES while preserving any manually added content (user edits). Don't regenerate from scratch. -6. **Write KNOWLEDGE.md**: Write to `.features/{FEATURE_SLUG}/KNOWLEDGE.md` (create directory if needed) -7. **Update index**: Run `node scripts/hooks/lib/feature-kb.cjs update-index` with all required fields -8. **Report**: Output what was created/updated - -## Output - -``` -KB_STATUS: created | refreshed -KB_PATH: .features/{slug}/KNOWLEDGE.md -KB_SLUG: {slug} -KB_NAME: {name} -SECTIONS: [list of sections written] -REFERENCED_FILES: [files selected for staleness tracking] -CROSS_REFERENCES: [ADR/PF entries referenced, if any] -``` - -## Boundaries - -- **Only writes to `.features/` directory** — never modify source code -- **Never delete existing KBs** — only create new or refresh existing -- **500-line cap** — if KB exceeds 500 lines, split into focused sub-KBs (each gets own index entry) -- **No push, no external API calls** — local filesystem operations only diff --git a/shared/agents/knowledge.md b/shared/agents/knowledge.md index bd47487..c12779e 100644 --- a/shared/agents/knowledge.md +++ b/shared/agents/knowledge.md @@ -15,7 +15,7 @@ tools: - Write --- -# KB Builder Agent +# Knowledge Agent ## Input Context diff --git a/shared/skills/plan:orch/SKILL.md b/shared/skills/plan:orch/SKILL.md index 45e0789..9f88b1b 100644 --- a/shared/skills/plan:orch/SKILL.md +++ b/shared/skills/plan:orch/SKILL.md @@ -258,7 +258,7 @@ Otherwise: plan stays in conversation context, ready for IMPLEMENT to consume di If Phases 4-5 explored a feature area that does NOT have a matching KB: 1. Identify the feature area slug and name from the explored directories -2. Spawn Agent(subagent_type="KB Builder"): +2. Spawn Agent(subagent_type="Knowledge"): ``` "FEATURE_SLUG: {slug} FEATURE_NAME: {name} @@ -270,9 +270,9 @@ If Phases 4-5 explored a feature area that does NOT have a matching KB: Skip if all explored areas already have matching KBs. -If a stale KB was detected in Phase 2, also refresh it here — spawn KB Builder with `EXISTING_KB` content + `CHANGED_FILES` from staleness check. +If a stale KB was detected in Phase 2, also refresh it here — spawn Knowledge agent with `EXISTING_KB` content + `CHANGED_FILES` from staleness check. -**Failure handling**: KB Builder failure is **non-blocking**. If it crashes, log the failure and complete the plan workflow normally. +**Failure handling**: Knowledge agent failure is **non-blocking**. If it crashes, log the failure and complete the plan workflow normally. **Produces:** `.features/{slug}/KNOWLEDGE.md`, updated `.features/index.json` **Requires:** Phase 4-5 exploration outputs @@ -308,6 +308,6 @@ Before presenting output, verify every phase was announced: - [ ] Phase 9: Design Review Lite → REVIEW_NOTES captured - [ ] Phase 10: Present → Output delivered to user - [ ] Phase 11: Persist → Artifact written (or skipped with stated reason) -- [ ] Phase 12: Feature KB Generation → KB Builder spawned for new feature areas (or skipped if KB exists) +- [ ] Phase 12: Feature KB Generation → Knowledge agent spawned for new feature areas (or skipped if KB exists) If any phase is unchecked, execute it before proceeding. diff --git a/src/cli/commands/kb.ts b/src/cli/commands/kb.ts index f8cece8..8a47f94 100644 --- a/src/cli/commands/kb.ts +++ b/src/cli/commands/kb.ts @@ -29,7 +29,7 @@ const featureKb: FeatureKbModule = _require( path.join(__dirname, '..', '..', '..', 'scripts', 'hooks', 'lib', 'feature-kb.cjs') ); -/** Tools passed to `claude -p` when spawning the KB Builder agent. */ +/** Tools passed to `claude -p` when spawning the Knowledge agent. */ const KB_AGENT_TOOLS = 'Read,Grep,Glob,Write,Bash'; /** @@ -187,7 +187,7 @@ kbCommand s.start('Creating KB...'); const prompt = [ - `You are the KB Builder agent. Create a feature knowledge base for the following area:`, + `You are the Knowledge agent. Create a feature knowledge base for the following area:`, ``, `FEATURE_SLUG: ${slug}`, `FEATURE_NAME: ${name as string}`, @@ -281,7 +281,7 @@ kbCommand } catch { /* new KB */ } const prompt = [ - `You are the KB Builder agent refreshing a stale feature knowledge base.`, + `You are the Knowledge agent refreshing a stale feature knowledge base.`, ``, `FEATURE_SLUG: ${kbSlug}`, `WORKTREE_PATH: ${worktreePath}`, diff --git a/src/cli/plugins.ts b/src/cli/plugins.ts index 1176897..999c711 100644 --- a/src/cli/plugins.ts +++ b/src/cli/plugins.ts @@ -53,7 +53,7 @@ export const DEVFLOW_PLUGINS: PluginDefinition[] = [ name: 'devflow-plan', description: 'Unified design planning with gap analysis and design review', commands: ['/plan'], - agents: ['git', 'skimmer', 'synthesizer', 'designer', 'kb-builder'], + agents: ['git', 'skimmer', 'synthesizer', 'designer', 'knowledge'], skills: ['agent-teams', 'gap-analysis', 'design-review', 'patterns', 'worktree-support', 'feature-kb', 'apply-feature-kb'], }, { @@ -95,7 +95,7 @@ export const DEVFLOW_PLUGINS: PluginDefinition[] = [ name: 'devflow-ambient', description: 'Ambient mode — intent classification with proportional agent orchestration', commands: ['/ambient'], - agents: ['coder', 'validator', 'simplifier', 'scrutinizer', 'evaluator', 'tester', 'skimmer', 'reviewer', 'git', 'synthesizer', 'resolver', 'designer', 'kb-builder'], + agents: ['coder', 'validator', 'simplifier', 'scrutinizer', 'evaluator', 'tester', 'skimmer', 'reviewer', 'git', 'synthesizer', 'resolver', 'designer', 'knowledge'], skills: [ 'router', 'implement:orch', From 3bf20eb81675f69db432d6d6566be2d3ffb8264f Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 24 Apr 2026 17:47:27 +0300 Subject: [PATCH 21/44] test(feature-kb): batch-D test improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename kb-builder-agent.test.ts to knowledge-agent.test.ts (follows shared/agents/kb-builder.md → knowledge.md rename in Batch C) - Reduce lock timeout from 500ms to 200ms (2 retry cycles at 100ms sleep is sufficient; faster test suite) - Add test documenting removeEntry early-return on corrupt index.json - Add expect(index).not.toBeNull() assertions before each index! access block to make implicit null safety explicit in test output Co-Authored-By: Claude --- tests/feature-kb/feature-kb.test.ts | 16 +++++++++++++++- ...der-agent.test.ts => knowledge-agent.test.ts} | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) rename tests/feature-kb/{kb-builder-agent.test.ts => knowledge-agent.test.ts} (89%) diff --git a/tests/feature-kb/feature-kb.test.ts b/tests/feature-kb/feature-kb.test.ts index 5c9264d..c520aca 100644 --- a/tests/feature-kb/feature-kb.test.ts +++ b/tests/feature-kb/feature-kb.test.ts @@ -176,6 +176,7 @@ describe('updateIndex', () => { createdBy: 'test', }); const index = loadIndex(tmp); + expect(index).not.toBeNull(); expect(index!.features['payments']).toBeDefined(); const entry = index!.features['payments'] as Record; expect(entry.name).toBe('Payment Processing'); @@ -192,6 +193,7 @@ describe('updateIndex', () => { category: 'conventions', }); const index = loadIndex(tmp); + expect(index).not.toBeNull(); const entry = index!.features['cli-commands'] as Record; expect(entry.name).toBe('CLI Command System Updated'); expect(entry.category).toBe('conventions'); @@ -211,6 +213,7 @@ describe('updateIndex', () => { }); const after = new Date().toISOString(); const index = loadIndex(tmp); + expect(index).not.toBeNull(); const entry = index!.features['test-slug'] as Record; const updated = entry.lastUpdated as string; expect(updated >= before).toBe(true); @@ -230,7 +233,7 @@ describe('updateIndex', () => { directories: [], referencedFiles: [], category: 'test', - }, 500)).toThrow(/lock/i); + }, 200)).toThrow(/lock/i); // Lock dir should still exist (not cleaned up by our failed attempt) expect(existsSync(lockPath)).toBe(true); @@ -255,6 +258,7 @@ describe('updateIndex', () => { expect(existsSync(path.join(tmp, '.features'))).toBe(true); const index = loadIndex(tmp); + expect(index).not.toBeNull(); expect(index!.features['new-feature']).toBeDefined(); }); }); @@ -272,6 +276,7 @@ describe('removeEntry', () => { removeEntry(tmp, 'cli-commands'); const index = loadIndex(tmp); + expect(index).not.toBeNull(); expect(index!.features['cli-commands']).toBeUndefined(); expect(existsSync(kbDir)).toBe(false); }); @@ -282,6 +287,7 @@ describe('removeEntry', () => { expect(() => removeEntry(tmp, 'nonexistent')).not.toThrow(); // Original entry should still exist const index = loadIndex(tmp); + expect(index).not.toBeNull(); expect(index!.features['cli-commands']).toBeDefined(); }); @@ -294,6 +300,14 @@ describe('removeEntry', () => { // Should not throw expect(() => removeEntry(tmp, 'nonexistent')).not.toThrow(); }); + + it('preserves corrupt index.json on remove instead of overwriting', () => { + const tmp = makeTmpFeatureWorktree(); + writeFileSync(path.join(tmp, '.features', 'index.json'), 'not-valid-json'); + removeEntry(tmp, 'nonexistent'); + const raw = readFileSync(path.join(tmp, '.features', 'index.json'), 'utf8'); + expect(raw).toBe('not-valid-json'); + }); }); // --------------------------------------------------------------------------- diff --git a/tests/feature-kb/kb-builder-agent.test.ts b/tests/feature-kb/knowledge-agent.test.ts similarity index 89% rename from tests/feature-kb/kb-builder-agent.test.ts rename to tests/feature-kb/knowledge-agent.test.ts index d990355..e52176b 100644 --- a/tests/feature-kb/kb-builder-agent.test.ts +++ b/tests/feature-kb/knowledge-agent.test.ts @@ -4,8 +4,8 @@ import * as path from 'path'; const ROOT = path.resolve(import.meta.dirname, '../..'); -describe('kb-builder agent', () => { - const content = readFileSync(path.join(ROOT, 'shared/agents/kb-builder.md'), 'utf8'); +describe('knowledge agent', () => { + const content = readFileSync(path.join(ROOT, 'shared/agents/knowledge.md'), 'utf8'); it('has correct model', () => { expect(content).toContain('model: sonnet'); }); it('has feature-kb skill', () => { expect(content).toContain('devflow:feature-kb'); }); From 486a69837f434a9de8f2cd9e91aa67a1e6a49a69 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Fri, 24 Apr 2026 18:09:19 +0300 Subject: [PATCH 22/44] =?UTF-8?q?fix:=20final=20cleanup=20=E2=80=94=20miss?= =?UTF-8?q?ed=20rename=20in=20CLAUDE.md,=20simplifier=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLAUDE.md: "KB Builder agent" → "Knowledge agent" (missed in batch C) - feature-kb.cjs: remove redundant releaseLock before return (try/finally handles it) - feature-kb.test.ts: remove inline re-require of already-imported rmSync --- CLAUDE.md | 2 +- scripts/hooks/lib/feature-kb.cjs | 1 - tests/feature-kb/feature-kb.test.ts | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 813d8fc..4ea507a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,7 +46,7 @@ Commands with Teams Variant ship as `{name}.md` (parallel subagents) and `{name} **Claude Code Flags**: Typed registry (`src/cli/utils/flags.ts`) for managing Claude Code feature flags (env vars and top-level settings). Pure functions `applyFlags`/`stripFlags`/`getDefaultFlags` follow the `applyTeamsConfig`/`stripTeamsConfig` pattern. Initial flags: `tool-search`, `lsp`, `clear-context-on-plan` (default ON), `brief`, `disable-1m-context` (default OFF). Manageable via `devflow flags --enable/--disable/--status/--list`. Stored in manifest `features.flags: string[]`. -**Feature Knowledge Bases**: Per-feature `.features/` directory containing KNOWLEDGE.md files that capture area-specific patterns, conventions, architecture, and gotchas. KBs are created as side-effects of planning (plan:orch Phase 12), loaded automatically across all workflows via `FEATURE_KNOWLEDGE` variable (companion to `KNOWLEDGE_CONTEXT`), and use staleness detection via git log against `referencedFiles`. Index at `.features/index.json` (object keyed by slug). Managed via `devflow kb list|create|check|refresh|remove`. KB Builder agent (sonnet) structures exploration outputs into KNOWLEDGE.md. `apply-feature-kb` skill provides consumption algorithm for agents. `.features/.kb.lock` is gitignored (transient lock directory for concurrent index writes, added automatically by `devflow init`). `devflow kb list` — List all feature KBs with staleness status. `devflow kb create ` — Create a new KB via claude -p exploration. `devflow kb check` — Check all KBs for staleness. `devflow kb refresh [slug]` — Refresh stale KB(s). `devflow kb remove ` — Remove a KB and its index entry. Note: debug:orch keeps FEATURE_KNOWLEDGE orchestrator-local (investigation workers examine code without pre-loaded context). +**Feature Knowledge Bases**: Per-feature `.features/` directory containing KNOWLEDGE.md files that capture area-specific patterns, conventions, architecture, and gotchas. KBs are created as side-effects of planning (plan:orch Phase 12), loaded automatically across all workflows via `FEATURE_KNOWLEDGE` variable (companion to `KNOWLEDGE_CONTEXT`), and use staleness detection via git log against `referencedFiles`. Index at `.features/index.json` (object keyed by slug). Managed via `devflow kb list|create|check|refresh|remove`. Knowledge agent (sonnet) structures exploration outputs into KNOWLEDGE.md. `apply-feature-kb` skill provides consumption algorithm for agents. `.features/.kb.lock` is gitignored (transient lock directory for concurrent index writes, added automatically by `devflow init`). `devflow kb list` — List all feature KBs with staleness status. `devflow kb create ` — Create a new KB via claude -p exploration. `devflow kb check` — Check all KBs for staleness. `devflow kb refresh [slug]` — Refresh stale KB(s). `devflow kb remove ` — Remove a KB and its index entry. Note: debug:orch keeps FEATURE_KNOWLEDGE orchestrator-local (investigation workers examine code without pre-loaded context). **Two-Mode Init**: `devflow init` offers Recommended (sensible defaults, quick setup) or Advanced (full interactive flow) after plugin selection. `--recommended` / `--advanced` CLI flags for non-interactive use. Recommended applies: ambient ON, memory ON, learn ON, HUD ON, teams OFF, default-ON flags, .claudeignore ON, auto-install safe-delete if trash CLI detected, user-mode security deny list. diff --git a/scripts/hooks/lib/feature-kb.cjs b/scripts/hooks/lib/feature-kb.cjs index 9329328..dd878d7 100644 --- a/scripts/hooks/lib/feature-kb.cjs +++ b/scripts/hooks/lib/feature-kb.cjs @@ -347,7 +347,6 @@ function removeEntry(worktreePath, slug, lockTimeoutMs = 30000) { try { index = JSON.parse(fs.readFileSync(indexPath, 'utf8')); } catch { - releaseLock(lockPath); return; // nothing to remove — preserve existing (possibly corrupt) file } diff --git a/tests/feature-kb/feature-kb.test.ts b/tests/feature-kb/feature-kb.test.ts index c520aca..06aff1b 100644 --- a/tests/feature-kb/feature-kb.test.ts +++ b/tests/feature-kb/feature-kb.test.ts @@ -52,8 +52,6 @@ describe('loadIndex', () => { it('returns null for missing directory', () => { const tmp = makeTmpFeatureWorktree(); // no index written - // Remove the .features dir to simulate completely missing - const { rmSync } = require('fs'); rmSync(path.join(tmp, '.features'), { recursive: true, force: true }); expect(loadIndex(tmp)).toBeNull(); }); From 67f9ff906416ab67aa9d0f285174a65565149286 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sun, 26 Apr 2026 14:04:29 +0300 Subject: [PATCH 23/44] feat(agents): add KNOWLEDGE_CONTEXT to Coder, FEATURE_KNOWLEDGE to Evaluator Coder now accepts KNOWLEDGE_CONTEXT (compact ADR/PF index) and uses devflow:apply-knowledge to read full bodies on demand, falling back to direct file reads when absent. Evaluator now accepts FEATURE_KNOWLEDGE and verifies implementation against documented feature patterns and anti-patterns. Co-Authored-By: Claude --- shared/agents/coder.md | 7 +++++-- shared/agents/evaluator.md | 7 ++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/shared/agents/coder.md b/shared/agents/coder.md index 49af0cb..05e1496 100644 --- a/shared/agents/coder.md +++ b/shared/agents/coder.md @@ -12,6 +12,7 @@ skills: - devflow:boundary-validation - devflow:worktree-support - devflow:apply-feature-kb + - devflow:apply-knowledge --- # Coder Agent @@ -31,6 +32,8 @@ You receive from orchestrator: **Domain hint** (optional): - **DOMAIN**: `backend` | `frontend` | `tests` | `fullstack` - Load/apply relevant domain skills - **FEATURE_KNOWLEDGE** (optional): Pre-computed feature area context — patterns, architecture, anti-patterns, gotchas +- **KNOWLEDGE_CONTEXT** (optional): Compact index of active ADR/PF entries. + When provided, use `devflow:apply-knowledge` to Read full bodies on demand. **Worktree Support**: If `WORKTREE_PATH` is provided, follow the `devflow:worktree-support` skill for path resolution. If omitted, use cwd. @@ -47,8 +50,8 @@ You receive from orchestrator: - Cross-reference changed files against EXECUTION_PLAN to identify what's relevant to your task - Read those relevant files to understand interfaces, types, naming conventions, error handling, and testing patterns established by prior work - If PRIOR_PHASE_SUMMARY is provided, use it to validate your understanding — actual code is authoritative, summaries are supplementary - - If `.memory/knowledge/decisions.md` exists, read it. Apply prior architectural decisions relevant to this task. Avoid contradicting accepted decisions without documenting a new ADR. - - If `.memory/knowledge/pitfalls.md` exists, scan for pitfalls in files you're about to modify. + - If `KNOWLEDGE_CONTEXT` is provided, follow `devflow:apply-knowledge` to scan the index and Read full bodies on demand. Otherwise, if `.memory/knowledge/decisions.md` exists, read it directly. Apply prior architectural decisions relevant to this task. + - If `KNOWLEDGE_CONTEXT` is `(none)` or absent: if `.memory/knowledge/pitfalls.md` exists, scan for pitfalls in files you're about to modify. When you apply a decision from `.memory/knowledge/decisions.md` or avoid a pitfall from `.memory/knowledge/pitfalls.md`, cite the entry ID in your final summary (e.g., 'applying ADR-003' or 'per PF-002') so usage can be tracked for capacity reviews. diff --git a/shared/agents/evaluator.md b/shared/agents/evaluator.md index f448d14..5dc5f83 100644 --- a/shared/agents/evaluator.md +++ b/shared/agents/evaluator.md @@ -5,6 +5,7 @@ model: opus skills: - devflow:software-design - devflow:worktree-support + - devflow:apply-feature-kb --- # Evaluator Agent @@ -21,6 +22,10 @@ You receive from orchestrator: **Worktree Support**: If `WORKTREE_PATH` is provided, follow the `devflow:worktree-support` skill for path resolution. If omitted, use cwd. +- **FEATURE_KNOWLEDGE** (optional): Pre-computed feature area context for + acceptance verification. Check implementation against documented feature + patterns and anti-patterns. Follow `devflow:apply-feature-kb`. + ## Responsibilities 1. **Understand intent**: Read ORIGINAL_REQUEST and EXECUTION_PLAN to understand what was requested @@ -35,7 +40,7 @@ You receive from orchestrator: | Wired | Connected to running app | Route registered, imported, reachable | Flag anything at "Exists" without reaching "Wired" as `incomplete`. -5. **Check completeness**: Verify all plan steps implemented, all acceptance criteria met +5. **Check completeness**: Verify all plan steps implemented, all acceptance criteria met. If FEATURE_KNOWLEDGE is provided, verify implementation follows documented patterns and avoids documented anti-patterns for the feature area 6. **Check scope**: Identify out-of-scope additions not justified by design improvements 7. **Report misalignments**: Document issues with sufficient detail for Coder to fix From 01ce3a8acbaeb7b1a51c1a0c6253d152a77b9252 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sun, 26 Apr 2026 14:05:46 +0300 Subject: [PATCH 24/44] feat(explore:orch): add full knowledge loading with orchestrator-local pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 now loads both KNOWLEDGE_CONTEXT (ADR/PF index via knowledge-context.cjs) and FEATURE_KNOWLEDGE (feature KBs), keeping both orchestrator-local — not passed to Explore sub-agents. Mirrors the same asymmetric pattern established in debug:orch. Also updates implement:orch Phase 2 to load KNOWLEDGE_CONTEXT alongside FEATURE_KNOWLEDGE, pass KC to Coder and Scrutinizer, and pass FK to Evaluator. Co-Authored-By: Claude --- shared/skills/explore:orch/SKILL.md | 28 ++++++++++++++++++++------- shared/skills/implement:orch/SKILL.md | 19 ++++++++++++------ 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/shared/skills/explore:orch/SKILL.md b/shared/skills/explore:orch/SKILL.md index 2fd9306..47073c6 100644 --- a/shared/skills/explore:orch/SKILL.md +++ b/shared/skills/explore:orch/SKILL.md @@ -22,21 +22,35 @@ Agent pipeline for EXPLORE intent in ambient GUIDED and ORCHESTRATED modes. Code For GUIDED depth, the main session performs exploration directly: -1. **Load Feature KBs** — Read `.features/index.json` if it exists. Based on the exploration question, identify relevant KBs and read them as context. Set `FEATURE_KNOWLEDGE = (none)` if none are relevant. +1. **Load Knowledge** — Run `node scripts/hooks/lib/knowledge-context.cjs index "{worktree}"` for KNOWLEDGE_CONTEXT. Read `.features/index.json` if it exists. Based on the exploration question, identify relevant KBs and read them. Use both locally to frame exploration. Set `FEATURE_KNOWLEDGE = (none)` if none are relevant. 2. **Spawn Skimmer** — `Agent(subagent_type="Skimmer")` targeting the area of interest. Use orientation output to ground exploration in real file structures and patterns. 3. **Trace** — Using Skimmer findings + `FEATURE_KNOWLEDGE`, trace the flow or analyze the subsystem directly in main session. Follow call chains, read key files, map integration points. 4. **Present** — Deliver structured findings using the Output format below. Use AskUserQuestion to offer drill-down into specific areas. ## ORCHESTRATED Pipeline -### Phase 1: Load Feature Knowledge +### Phase 1: Load Knowledge (Orchestrator-Local) -**Produces:** FEATURE_KNOWLEDGE +**Produces:** KNOWLEDGE_CONTEXT, FEATURE_KNOWLEDGE -1. Read `.features/index.json` if it exists. If not, set `FEATURE_KNOWLEDGE = (none)` and skip. -2. Identify relevant KBs from the exploration question (match task intent against KB descriptions and directories). +Before exploring, load the knowledge index: + +```bash +KNOWLEDGE_CONTEXT=$(node scripts/hooks/lib/knowledge-context.cjs index "{worktree}") +``` + +The orchestrator uses `KNOWLEDGE_CONTEXT` locally when framing exploration — prior +decisions and pitfalls suggest specific areas to investigate. Follow +`devflow:apply-knowledge` to Read full entry bodies on demand. **Do NOT pass +`KNOWLEDGE_CONTEXT` to Explore sub-agents** — knowledge context stays in the +orchestrator, not in the investigation workers. + +Also load feature knowledge: +1. Read `.features/index.json` if it exists. If not, set `FEATURE_KNOWLEDGE = (none)`. +2. Identify relevant KBs (match task intent against KB descriptions and directories). 3. For each match: check staleness via `node scripts/hooks/lib/feature-kb.cjs stale "{worktree}" {slug}`, read `.features/{slug}/KNOWLEDGE.md`. -4. Pass as `FEATURE_KNOWLEDGE` to Explore agents. +4. Use `FEATURE_KNOWLEDGE` **locally** for exploration framing — feature-specific patterns and integration points guide where to focus. +5. **Do NOT pass to Explore sub-agents** (same asymmetric pattern as KNOWLEDGE_CONTEXT). **Explore agent framing**: "The KB is a baseline — your job is to VALIDATE, EXTEND, and CORRECT it, not repeat it. Focus on areas the KB doesn't cover and things that may have changed." @@ -105,7 +119,7 @@ Structured exploration findings with concrete code references: Before presenting findings, verify every phase was announced: -- [ ] Phase 1: Load Feature Knowledge → FEATURE_KNOWLEDGE captured (or skipped if `.features/` absent) +- [ ] Phase 1: Load Knowledge (Orchestrator-Local) → KNOWLEDGE_CONTEXT and FEATURE_KNOWLEDGE captured (orchestrator-local, not passed to workers) - [ ] Phase 2: Orient → ORIENT_OUTPUT captured - [ ] Phase 3: Explore → EXPLORE_OUTPUT captured - [ ] Phase 4: Synthesize → MERGED_FINDINGS captured diff --git a/shared/skills/implement:orch/SKILL.md b/shared/skills/implement:orch/SKILL.md index 10b24cc..412ab30 100644 --- a/shared/skills/implement:orch/SKILL.md +++ b/shared/skills/implement:orch/SKILL.md @@ -8,7 +8,7 @@ user-invocable: false Agent pipeline for IMPLEMENT intent in ambient ORCHESTRATED mode. Pre-flight checks, plan synthesis, Coder execution, and quality gates. -This is a lightweight variant of `/implement` for ambient ORCHESTRATED mode. Excluded: strategy selection (single/sequential/parallel Coders), retry loops, PR creation, knowledge loading. +This is a lightweight variant of `/implement` for ambient ORCHESTRATED mode. Excluded: strategy selection (single/sequential/parallel Coders), retry loops, PR creation. ## Iron Law @@ -56,9 +56,15 @@ Return the branch setup summary." Capture `branch name` and `BASE_BRANCH` from Git agent output for use throughout the pipeline. -## Phase 2: Load Feature Knowledge +## Phase 2: Load Knowledge -**Produces:** FEATURE_KNOWLEDGE +**Produces:** KNOWLEDGE_CONTEXT, FEATURE_KNOWLEDGE + +Load the knowledge index: +```bash +KNOWLEDGE_CONTEXT=$(node scripts/hooks/lib/knowledge-context.cjs index "{worktree}") +``` +Pass `KNOWLEDGE_CONTEXT` to Coder (Phase 4) and Scrutinizer (Phase 6). 1. Check if `.features/index.json` exists. If not, set `FEATURE_KNOWLEDGE = (none)` and skip. 2. Read `.features/index.json`. @@ -98,6 +104,7 @@ Spawn `Agent(subagent_type="Coder")` with input variables: - **CREATE_PR**: `false` (commit only, no push) - **DOMAIN**: Inferred from files in scope (`backend`, `frontend`, `tests`, `fullstack`) - **FEATURE_KNOWLEDGE**: From Phase 2 (or `(none)`) +- **KNOWLEDGE_CONTEXT**: From Phase 2 (or `(none)`) **Execution strategy**: Single sequential Coder by default. Parallel Coders only when tasks are self-contained — zero shared contracts, no integration points, different files/modules with no imports between them. @@ -129,9 +136,9 @@ Run sequentially — each gate must pass before the next: 1. `Agent(subagent_type="Validator")` (build + typecheck + lint + tests) — retry up to 2× on failure (Coder fixes between retries) 2. `Agent(subagent_type="Simplifier")` — code clarity and maintainability pass on FILES_CHANGED -3. `Agent(subagent_type="Scrutinizer")` — 9-pillar quality evaluation on FILES_CHANGED, with `FEATURE_KNOWLEDGE` from Phase 2 +3. `Agent(subagent_type="Scrutinizer")` — 9-pillar quality evaluation on FILES_CHANGED, with `KNOWLEDGE_CONTEXT` and `FEATURE_KNOWLEDGE` from Phase 2 4. `Agent(subagent_type="Validator")` (re-validate after Simplifier/Scrutinizer changes) -5. `Agent(subagent_type="Evaluator")` — verify implementation matches original request — retry up to 2× if misalignment found +5. `Agent(subagent_type="Evaluator")` — verify implementation matches original request, with `FEATURE_KNOWLEDGE` from Phase 2 — retry up to 2× if misalignment found 6. `Agent(subagent_type="Tester")` — scenario-based acceptance testing from user's perspective — retry up to 2× if QA fails If any gate exhausts retries, halt pipeline and report what passed and what failed. @@ -166,7 +173,7 @@ Report results: Before reporting results, verify every phase was announced: - [ ] Phase 1: Pre-flight → BASE_BRANCH, FEATURE_BRANCH captured -- [ ] Phase 2: Load Feature Knowledge → FEATURE_KNOWLEDGE captured (or skipped) +- [ ] Phase 2: Load Knowledge → KNOWLEDGE_CONTEXT and FEATURE_KNOWLEDGE captured (or skipped) - [ ] Phase 3: Plan Synthesis → EXECUTION_PLAN captured - [ ] Phase 4: Coder Execution → CODER_COMMITS, PRE_CODER_SHA captured - [ ] Phase 5: FILES_CHANGED Detection → FILES_CHANGED captured From bc79274a42ead1531bad4a0e175e9578dcea478d Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sun, 26 Apr 2026 14:06:55 +0300 Subject: [PATCH 25/44] feat(implement): add KNOWLEDGE_CONTEXT loading and pass to Coder, Scrutinizer, Evaluator Phase 1 now loads KNOWLEDGE_CONTEXT via knowledge-context.cjs alongside FEATURE_KNOWLEDGE. KC is passed to all Coder spawn sites (single, sequential, parallel) and to Scrutinizer in Phase 5. FEATURE_KNOWLEDGE is now also passed to Evaluator in Phase 7 for pattern-aware alignment verification. Co-Authored-By: Claude --- .../devflow-implement/commands/implement.md | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/plugins/devflow-implement/commands/implement.md b/plugins/devflow-implement/commands/implement.md index 7fba988..35e96c2 100644 --- a/plugins/devflow-implement/commands/implement.md +++ b/plugins/devflow-implement/commands/implement.md @@ -36,7 +36,7 @@ Orchestrate a single task through implementation by spawning specialized agents. ### Phase 1: Setup -**Produces:** TASK_ID, BASE_BRANCH, EXECUTION_PLAN, FEATURE_KNOWLEDGE +**Produces:** TASK_ID, BASE_BRANCH, EXECUTION_PLAN, KNOWLEDGE_CONTEXT, FEATURE_KNOWLEDGE Record the current branch name as `BASE_BRANCH` - this will be the PR target. @@ -67,6 +67,12 @@ Return the branch setup summary." 5. Use extracted content as EXECUTION_PLAN for the Coder phase (replaces exploration/planning output) 6. Captured values override defaults from Git agent where present +**Load Knowledge Context:** +```bash +KNOWLEDGE_CONTEXT=$(node scripts/hooks/lib/knowledge-context.cjs index "{worktree}") +``` +Pass to Coder (Phase 2) and Scrutinizer (Phase 5). + **Load Feature Knowledge:** 1. Read `.features/index.json` if it exists 2. Based on task description and file targets, identify relevant KBs @@ -103,7 +109,8 @@ EXECUTION_PLAN: {full plan from setup context} PATTERNS: {patterns from plan document or empty} CREATE_PR: true DOMAIN: {detected domain or 'fullstack'} -FEATURE_KNOWLEDGE: {feature_knowledge}" +FEATURE_KNOWLEDGE: {feature_knowledge} +KNOWLEDGE_CONTEXT: {knowledge_context}" ``` --- @@ -123,6 +130,7 @@ PATTERNS: {patterns from plan document or empty} CREATE_PR: false DOMAIN: {phase 1 domain, e.g., 'backend'} FEATURE_KNOWLEDGE: {feature_knowledge} +KNOWLEDGE_CONTEXT: {knowledge_context} HANDOFF_REQUIRED: true" ``` @@ -139,6 +147,7 @@ DOMAIN: {phase N domain, e.g., 'frontend'} PRIOR_PHASE_SUMMARY: {summary from previous Coder} FILES_FROM_PRIOR_PHASE: {list of files created} FEATURE_KNOWLEDGE: {feature_knowledge} +KNOWLEDGE_CONTEXT: {knowledge_context} HANDOFF_REQUIRED: {true if not last phase}" ``` @@ -159,7 +168,8 @@ EXECUTION_PLAN: {subtask 1 steps} PATTERNS: {patterns} CREATE_PR: false DOMAIN: {subtask 1 domain} -FEATURE_KNOWLEDGE: {feature_knowledge}" +FEATURE_KNOWLEDGE: {feature_knowledge} +KNOWLEDGE_CONTEXT: {knowledge_context}" Agent(subagent_type="Coder"): # Coder 2 (same message) "TASK_ID: {task-id}-part2 @@ -169,7 +179,8 @@ EXECUTION_PLAN: {subtask 2 steps} PATTERNS: {patterns} CREATE_PR: false DOMAIN: {subtask 2 domain} -FEATURE_KNOWLEDGE: {feature_knowledge}" +FEATURE_KNOWLEDGE: {feature_knowledge} +KNOWLEDGE_CONTEXT: {knowledge_context}" ``` **Independence criteria** (all must be true for PARALLEL_CODERS): @@ -237,6 +248,7 @@ After Simplifier completes, spawn Scrutinizer as final quality gate: Agent(subagent_type="Scrutinizer"): "TASK_DESCRIPTION: {task description} FILES_CHANGED: {list of files from Coder output} +KNOWLEDGE_CONTEXT: {knowledge_context} FEATURE_KNOWLEDGE: {feature_knowledge} Evaluate 9 pillars, fix P0/P1 issues, report status" ``` @@ -274,6 +286,7 @@ Agent(subagent_type="Evaluator"): EXECUTION_PLAN: {execution plan from Phase 1} FILES_CHANGED: {list of files from Coder output} ACCEPTANCE_CRITERIA: {extracted criteria if available} +FEATURE_KNOWLEDGE: {feature_knowledge} Validate alignment with request and plan. Report ALIGNED or MISALIGNED with details." ``` From c76bbcdfdf91e07648d9244ef2ef37ef3df82f97 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sun, 26 Apr 2026 14:15:49 +0300 Subject: [PATCH 26/44] fix(kb-cli): remove hardcoded referencedFiles/category from create prompt, fix refresh prompt Create prompt now instructs the agent to select appropriate referencedFiles and determine the best category itself, instead of hardcoding empty arrays and "component-patterns". Refresh prompt now passes FEATURE_NAME and DIRECTORIES from the index entry, and provides a proper update-index command template for the agent to run after writing. --- src/cli/commands/kb.ts | 227 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 219 insertions(+), 8 deletions(-) diff --git a/src/cli/commands/kb.ts b/src/cli/commands/kb.ts index 8a47f94..e7db2a0 100644 --- a/src/cli/commands/kb.ts +++ b/src/cli/commands/kb.ts @@ -8,6 +8,9 @@ import { createRequire } from 'module'; import { fileURLToPath } from 'url'; import { isClaudeCliAvailable } from '../utils/cli.js'; import { getGitRoot } from '../utils/git.js'; +import type { HookMatcher, Settings } from '../utils/hooks.js'; +import { getClaudeDirectory, getDevFlowDirectory } from '../utils/paths.js'; +import { readManifest, writeManifest } from '../utils/manifest.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -52,8 +55,199 @@ async function getWorktreePath(): Promise { return (await getGitRoot()) ?? process.cwd(); } +const KB_HOOK_MARKER = 'session-end-kb-refresh'; + +/** + * Add the KB SessionEnd hook to settings JSON. + * Idempotent — returns unchanged JSON if hook already exists. + */ +export function addKbHook(settingsJson: string, devflowDir: string): string { + if (hasKbHook(settingsJson)) { + return settingsJson; + } + + const cleanedJson = removeKbHook(settingsJson); + const settings: Settings = JSON.parse(cleanedJson); + + if (!settings.hooks) { + settings.hooks = {}; + } + + const hookCommand = path.join(devflowDir, 'scripts', 'hooks', 'run-hook') + ' session-end-kb-refresh'; + + const newEntry: HookMatcher = { + hooks: [ + { + type: 'command', + command: hookCommand, + timeout: 10, + }, + ], + }; + + if (!settings.hooks.SessionEnd) { + settings.hooks.SessionEnd = []; + } + + settings.hooks.SessionEnd.push(newEntry); + + return JSON.stringify(settings, null, 2) + '\n'; +} + +/** + * Remove the KB hook from settings JSON. + * Idempotent — returns unchanged JSON if hook not present. + * Preserves other hooks. Cleans empty arrays/objects. + */ +export function removeKbHook(settingsJson: string): string { + const settings: Settings = JSON.parse(settingsJson); + let changed = false; + + const matchers = settings.hooks?.SessionEnd; + if (matchers) { + const before = matchers.length; + settings.hooks!.SessionEnd = matchers.filter( + (m) => !m.hooks.some((h) => h.command.includes(KB_HOOK_MARKER)), + ); + if (settings.hooks!.SessionEnd!.length < before) changed = true; + if (settings.hooks!.SessionEnd!.length === 0) delete settings.hooks!.SessionEnd; + } + + if (!changed) { + return settingsJson; + } + + if (settings.hooks && Object.keys(settings.hooks).length === 0) { + delete settings.hooks; + } + + return JSON.stringify(settings, null, 2) + '\n'; +} + +/** + * Check if the KB hook is registered in settings JSON or parsed Settings object. + */ +export function hasKbHook(input: string | Settings): boolean { + const settings: Settings = typeof input === 'string' ? JSON.parse(input) : input; + return settings.hooks?.SessionEnd?.some((matcher) => + matcher.hooks.some((h) => h.command.includes(KB_HOOK_MARKER)), + ) ?? false; +} + export const kbCommand = new Command('kb') - .description('Manage per-feature knowledge bases'); + .description('Manage per-feature knowledge bases') + .option('--enable', 'Enable per-feature knowledge bases') + .option('--disable', 'Disable per-feature knowledge bases') + .option('--status', 'Show KB feature status') + .action(async (options: { enable?: boolean; disable?: boolean; status?: boolean }) => { + if (!options.enable && !options.disable && !options.status) return; + + if (options.enable) { + p.intro(color.cyan('Enable Feature Knowledge Bases')); + const worktreePath = await getWorktreePath(); + const claudeDir = getClaudeDirectory(); + const devflowDir = getDevFlowDirectory(); + + // Create .features/index.json if missing + const featuresDir = path.join(worktreePath, '.features'); + await fs.mkdir(featuresDir, { recursive: true }); + const indexPath = path.join(featuresDir, 'index.json'); + try { + await fs.access(indexPath); + } catch { + await fs.writeFile(indexPath, JSON.stringify({ version: 1, features: {} }, null, 2) + '\n'); + } + + // Remove .disabled sentinel + try { await fs.unlink(path.join(featuresDir, '.disabled')); } catch { /* doesn't exist */ } + + // Add SessionEnd hook + const settingsPath = path.join(claudeDir, 'settings.json'); + try { + const content = await fs.readFile(settingsPath, 'utf-8'); + const updated = addKbHook(content, devflowDir); + if (updated !== content) { + await fs.writeFile(settingsPath, updated, 'utf-8'); + } + } catch { /* settings.json may not exist */ } + + // Update manifest + const manifest = await readManifest(devflowDir); + if (manifest) { + manifest.features.kb = true; + manifest.updatedAt = new Date().toISOString(); + await writeManifest(devflowDir, manifest); + } + + p.log.success('Feature knowledge bases enabled'); + p.outro(`SessionEnd hook installed. Run ${color.cyan('devflow kb create ')} to create a KB.`); + + } else if (options.disable) { + p.intro(color.cyan('Disable Feature Knowledge Bases')); + const worktreePath = await getWorktreePath(); + const claudeDir = getClaudeDirectory(); + const devflowDir = getDevFlowDirectory(); + + // Create .disabled sentinel + const featuresDir = path.join(worktreePath, '.features'); + await fs.mkdir(featuresDir, { recursive: true }); + await fs.writeFile(path.join(featuresDir, '.disabled'), '', 'utf-8'); + + // Remove SessionEnd hook + const settingsPath = path.join(claudeDir, 'settings.json'); + try { + const content = await fs.readFile(settingsPath, 'utf-8'); + const updated = removeKbHook(content); + if (updated !== content) { + await fs.writeFile(settingsPath, updated, 'utf-8'); + } + } catch { /* settings.json may not exist */ } + + // Update manifest + const manifest = await readManifest(devflowDir); + if (manifest) { + manifest.features.kb = false; + manifest.updatedAt = new Date().toISOString(); + await writeManifest(devflowDir, manifest); + } + + p.log.success('Feature knowledge bases disabled'); + p.log.info('Existing KBs preserved. Manual commands (create/refresh) still work.'); + p.outro(''); + + } else if (options.status) { + p.intro(color.cyan('Feature KB Status')); + const worktreePath = await getWorktreePath(); + const claudeDir = getClaudeDirectory(); + const devflowDir = getDevFlowDirectory(); + + // Check hook + let hookPresent = false; + try { + const content = await fs.readFile(path.join(claudeDir, 'settings.json'), 'utf-8'); + hookPresent = hasKbHook(content); + } catch { /* settings.json may not exist */ } + + // Check sentinel + let disabled = false; + try { + await fs.access(path.join(worktreePath, '.features', '.disabled')); + disabled = true; + } catch { /* not disabled */ } + + // Count KBs + const kbs = featureKb.listKBs(worktreePath); + + const status = hookPresent && !disabled ? 'enabled' : 'disabled'; + p.log.info(`Status: ${status === 'enabled' ? color.green('enabled') : color.yellow('disabled')}`); + p.log.info(`Hook: ${hookPresent ? color.green('installed') : color.dim('not installed')}`); + p.log.info(`KBs: ${kbs.length}`); + if (disabled) { + p.log.info(`Sentinel: ${color.yellow('.features/.disabled present')}`); + } + p.outro(''); + } + }); // --------------------------------------------------------------------------- // devflow kb list @@ -200,12 +394,17 @@ kbCommand `3. Distill into actionable cross-cutting knowledge`, `4. Write .features/${slug}/KNOWLEDGE.md with all required sections`, ``, - `Then run: node scripts/hooks/lib/feature-kb.cjs update-index "${worktreePath}" \\`, - ` --slug="${slug}" --name="${name as string}" \\`, - ` --directories='${JSON.stringify(directories)}' \\`, - ` --referencedFiles='[]' \\`, - ` --category="component-patterns" \\`, - ` --createdBy="devflow-kb"`, + `After writing KNOWLEDGE.md, register it in the index:`, + `1. Select 5-10 key files from the explored directories for staleness tracking`, + `2. Determine the best category: architecture, conventions, component-patterns, domain-knowledge, or lessons-learned`, + `3. Write a one-line description starting with "Use when" for relevance matching`, + `4. Run: node scripts/hooks/lib/feature-kb.cjs update-index "${worktreePath}" \\`, + ` --slug="${slug}" --name="${name as string}" \\`, + ` --directories='${JSON.stringify(directories)}' \\`, + ` --referencedFiles='[]' \\`, + ` --category="" \\`, + ` --description="" \\`, + ` --createdBy="devflow-kb"`, ``, `Create the directory if needed. Report KB_STATUS when done.`, ].join('\n'); @@ -274,6 +473,10 @@ kbCommand s.start(`Refreshing ${kbSlug}...`); const staleInfo = featureKb.checkStaleness(worktreePath, kbSlug); + const kbs = featureKb.listKBs(worktreePath); + const kbEntry = kbs.find((k: { slug: string }) => k.slug === kbSlug); + const featureName = kbEntry?.name ?? kbSlug; + const kbDirectories = kbEntry?.directories ?? []; const kbPath = path.join(worktreePath, '.features', kbSlug, 'KNOWLEDGE.md'); let existingContent = ''; try { @@ -284,6 +487,8 @@ kbCommand `You are the Knowledge agent refreshing a stale feature knowledge base.`, ``, `FEATURE_SLUG: ${kbSlug}`, + `FEATURE_NAME: ${featureName}`, + `DIRECTORIES: ${JSON.stringify(kbDirectories)}`, `WORKTREE_PATH: ${worktreePath}`, `CHANGED_FILES: ${JSON.stringify(staleInfo.changedFiles)}`, ``, @@ -294,7 +499,13 @@ kbCommand `- Preserve any manually added content`, `- Do not regenerate from scratch`, `- Write the updated KB to .features/${kbSlug}/KNOWLEDGE.md`, - `- Run update-index to refresh lastUpdated timestamp`, + `- After writing, run update-index with updated metadata:`, + ` node scripts/hooks/lib/feature-kb.cjs update-index "${worktreePath}" \\`, + ` --slug="${kbSlug}" --name="${featureName}" \\`, + ` --directories='${JSON.stringify(kbDirectories)}' \\`, + ` --referencedFiles='[]' \\`, + ` --category="${kbEntry?.category ?? 'component-patterns'}" \\`, + ` --createdBy="devflow-kb"`, ].filter(Boolean).join('\n'); try { From 8d41b601096c3b4f24ee8439a57eebbbce2cf848 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sun, 26 Apr 2026 14:16:00 +0300 Subject: [PATCH 27/44] =?UTF-8?q?feat(kb):=20add=20toggle=20mechanism=20?= =?UTF-8?q?=E2=80=94=20manifest=20field,=20init=20flow,=20enable/disable/s?= =?UTF-8?q?tatus=20CLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add kb: boolean to ManifestData.features with backward-compat default false - Add addKbHook/removeKbHook/hasKbHook functions following learn.ts hook pattern - Add devflow kb --enable/--disable/--status top-level action to kbCommand - Wire kbEnabled into devflow init recommended/advanced modes (default ON) - Add --kb/--no-kb CLI flags to init command - KB hook removed on devflow uninstall - Gate .features/ creation on kbEnabled; .disabled sentinel manages off state - Add .features/.disabled and .features/.kb-last-refresh to gitignore entries - Add .disabled guard to plan:orch Phase 12 to skip KB generation when disabled - Tests for addKbHook/removeKbHook/hasKbHook; manifest test fixtures updated --- shared/skills/plan:orch/SKILL.md | 2 + src/cli/commands/init.ts | 48 ++++++++++- src/cli/commands/uninstall.ts | 2 + src/cli/utils/manifest.ts | 2 + src/cli/utils/post-install.ts | 2 +- tests/kb.test.ts | 140 +++++++++++++++++++++++++++++++ tests/manifest.test.ts | 26 ++++-- 7 files changed, 213 insertions(+), 9 deletions(-) create mode 100644 tests/kb.test.ts diff --git a/shared/skills/plan:orch/SKILL.md b/shared/skills/plan:orch/SKILL.md index 9f88b1b..08d3dfd 100644 --- a/shared/skills/plan:orch/SKILL.md +++ b/shared/skills/plan:orch/SKILL.md @@ -255,6 +255,8 @@ Otherwise: plan stays in conversation context, ready for IMPLEMENT to consume di ## Phase 12: Feature KB Generation (Conditional) +If `.features/.disabled` exists, skip KB generation entirely — the KB feature is disabled. + If Phases 4-5 explored a feature area that does NOT have a matching KB: 1. Identify the feature area slug and name from the explored directories diff --git a/src/cli/commands/init.ts b/src/cli/commands/init.ts index 8cb5f1c..0874a28 100644 --- a/src/cli/commands/init.ts +++ b/src/cli/commands/init.ts @@ -27,6 +27,7 @@ import { addAmbientHook, removeAmbientHook } from './ambient.js'; import { addMemoryHooks, removeMemoryHooks } from './memory.js'; import { addLearningHook, removeLearningHook } from './learn.js'; import { addHudStatusLine, removeHudStatusLine } from './hud.js'; +import { addKbHook, removeKbHook } from './kb.js'; import { loadConfig as loadHudConfig, saveConfig as saveHudConfig } from '../hud/config.js'; import { readManifest, writeManifest, resolvePluginList, detectUpgrade } from '../utils/manifest.js'; import { getDefaultFlags, applyFlags, stripFlags, FLAG_REGISTRY } from '../utils/flags.js'; @@ -129,6 +130,7 @@ interface InitOptions { memory?: boolean; learn?: boolean; hud?: boolean; + kb?: boolean; hudOnly?: boolean; recommended?: boolean; advanced?: boolean; @@ -149,6 +151,8 @@ export const initCommand = new Command('init') .option('--no-learn', 'Disable self-learning') .option('--hud', 'Enable HUD (git info, context usage, session stats)') .option('--no-hud', 'Disable HUD status line') + .option('--kb', 'Enable feature knowledge bases') + .option('--no-kb', 'Disable feature knowledge bases') .option('--hud-only', 'Install only the HUD (no plugins, hooks, or extras)') .option('--recommended', 'Apply recommended defaults after plugin selection (skip advanced prompts)') .option('--advanced', 'Show all configuration prompts') @@ -258,7 +262,7 @@ export const initCommand = new Command('init') version, plugins: [], scope, - features: { teams: false, ambient: false, memory: false, hud: true, learn: false, flags: [] }, + features: { teams: false, ambient: false, memory: false, hud: true, learn: false, kb: false, flags: [] }, installedAt: now, updatedAt: now, }); @@ -368,6 +372,7 @@ export const initCommand = new Command('init') let memoryEnabled = true; let learnEnabled = true; let hudEnabled = true; + let kbEnabled = true; let enabledFlags = getDefaultFlags(); let claudeignoreEnabled = !!earlyGitRoot; let discoveredProjects: string[] = []; @@ -395,6 +400,7 @@ export const initCommand = new Command('init') if (options.memory !== undefined) memoryEnabled = options.memory; if (options.learn !== undefined) learnEnabled = options.learn; if (options.hud !== undefined) hudEnabled = options.hud; + if (options.kb !== undefined) kbEnabled = options.kb; // Compute safe-delete block synchronously so we know whether to fetch installed version if (profilePath && safeDeleteAvailable) { @@ -427,6 +433,7 @@ export const initCommand = new Command('init') `Working memory: ${memoryEnabled ? 'enabled' : 'disabled'}`, `Self-learning: ${learnEnabled ? 'enabled' : 'disabled'}`, `HUD: ${hudEnabled ? 'enabled' : 'disabled'}`, + `Feature KBs: ${kbEnabled ? 'enabled' : 'disabled'}`, `Agent Teams: ${teamsEnabled ? 'enabled' : 'disabled'}`, `Claude Code flags: ${defaultFlagCount} enabled`, `${claudeignoreEnabled ? '.claudeignore: created' : ''}`, @@ -554,6 +561,26 @@ export const initCommand = new Command('init') hudEnabled = hudChoice; } + if (options.kb !== undefined) { + kbEnabled = options.kb; + } else { + p.note( + 'Per-feature knowledge bases capture cross-cutting patterns,\n' + + 'conventions, and gotchas. Auto-refreshed when files change.\n' + + 'Consumes a background agent session on staleness detection.', + 'Feature Knowledge Bases', + ); + const kbChoice = await p.confirm({ + message: 'Enable feature knowledge bases? (Recommended)', + initialValue: true, + }); + if (p.isCancel(kbChoice)) { + p.cancel('Installation cancelled.'); + process.exit(0); + } + kbEnabled = kbChoice; + } + // Claude Code flags multiselect (advanced only) if (process.stdin.isTTY) { const flagChoices = FLAG_REGISTRY.map(f => ({ @@ -925,6 +952,10 @@ export const initCommand = new Command('init') ? addHudStatusLine(content, devflowDir) : removeHudStatusLine(content); + // KB hook — remove-then-add for upgrade safety + const cleanedForKb = removeKbHook(content); + content = kbEnabled ? addKbHook(cleanedForKb, devflowDir) : cleanedForKb; + // Claude Code flags — strip all managed keys, then re-apply selected flags content = stripFlags(content); content = applyFlags(content, enabledFlags); @@ -947,7 +978,7 @@ export const initCommand = new Command('init') // Create .features/ directory with empty index (feature knowledge bases) // .features/ is committed to the project repo (not scope-dependent) - if (gitRoot) { + if (gitRoot && kbEnabled) { const featuresDir = path.join(gitRoot, '.features'); await fs.mkdir(featuresDir, { recursive: true }); const featuresIndexPath = path.join(featuresDir, 'index.json'); @@ -961,6 +992,17 @@ export const initCommand = new Command('init') } } + // Manage .disabled sentinel based on kbEnabled state + if (gitRoot) { + const disabledPath = path.join(gitRoot, '.features', '.disabled'); + if (kbEnabled) { + try { await fs.unlink(disabledPath); } catch { /* doesn't exist */ } + } else { + await fs.mkdir(path.join(gitRoot, '.features'), { recursive: true }); + await fs.writeFile(disabledPath, '', 'utf-8'); + } + } + // Configure HUD const existingHud = loadHudConfig(); saveHudConfig({ enabled: hudEnabled, detail: existingHud.detail }); @@ -1082,7 +1124,7 @@ export const initCommand = new Command('init') version, plugins: resolvePluginList(installedPluginNames, existingManifest, !!options.plugin), scope, - features: { teams: teamsEnabled, ambient: ambientEnabled, memory: memoryEnabled, learn: learnEnabled, hud: hudEnabled, flags: enabledFlags }, + features: { teams: teamsEnabled, ambient: ambientEnabled, memory: memoryEnabled, learn: learnEnabled, hud: hudEnabled, kb: kbEnabled, flags: enabledFlags }, installedAt: existingManifest?.installedAt ?? now, updatedAt: now, }; diff --git a/src/cli/commands/uninstall.ts b/src/cli/commands/uninstall.ts index 29e0845..2c4f1cf 100644 --- a/src/cli/commands/uninstall.ts +++ b/src/cli/commands/uninstall.ts @@ -13,6 +13,7 @@ import { removeAmbientHook } from './ambient.js'; import { removeMemoryHooks } from './memory.js'; import { removeLearningHook } from './learn.js'; import { removeHudStatusLine } from './hud.js'; +import { removeKbHook } from './kb.js'; import { listShadowed } from './skills.js'; import { detectShell, getProfilePath } from '../utils/safe-delete.js'; import { isAlreadyInstalled, removeFromProfile } from '../utils/safe-delete-install.js'; @@ -400,6 +401,7 @@ export const uninstallCommand = new Command('uninstall') settingsContent = removeMemoryHooks(settingsContent); settingsContent = removeLearningHook(settingsContent); settingsContent = removeHudStatusLine(settingsContent); + settingsContent = removeKbHook(settingsContent); settingsContent = stripFlags(settingsContent); if (settingsContent !== originalContent) { diff --git a/src/cli/utils/manifest.ts b/src/cli/utils/manifest.ts index 112b720..e0e5d55 100644 --- a/src/cli/utils/manifest.ts +++ b/src/cli/utils/manifest.ts @@ -15,6 +15,7 @@ export interface ManifestData { memory: boolean; learn: boolean; hud: boolean; + kb: boolean; flags: string[]; }; installedAt: string; @@ -55,6 +56,7 @@ export async function readManifest(devflowDir: string): Promise { try { const gitignorePath = path.join(gitRoot, '.gitignore'); - const entriesToAdd = ['.claude/', '.devflow/', '.memory/', '.docs/', '.features/.kb.lock']; + const entriesToAdd = ['.claude/', '.devflow/', '.memory/', '.docs/', '.features/.kb.lock', '.features/.disabled', '.features/.kb-last-refresh', '.features/.kb-refresh.lock']; let gitignoreContent = ''; try { diff --git a/tests/kb.test.ts b/tests/kb.test.ts new file mode 100644 index 0000000..5537ca4 --- /dev/null +++ b/tests/kb.test.ts @@ -0,0 +1,140 @@ +import { describe, it, expect } from 'vitest'; +import { addKbHook, removeKbHook, hasKbHook } from '../src/cli/commands/kb.js'; + +const DEVFLOW_DIR = '/home/user/.devflow'; + +describe('addKbHook', () => { + it('adds hook to empty settings', () => { + const result = addKbHook('{}', DEVFLOW_DIR); + const parsed = JSON.parse(result); + expect(parsed.hooks?.SessionEnd).toHaveLength(1); + expect(parsed.hooks.SessionEnd[0].hooks[0].command).toContain('session-end-kb-refresh'); + }); + + it('adds hook with correct run-hook path', () => { + const result = addKbHook('{}', '/custom/.devflow'); + const parsed = JSON.parse(result); + expect(parsed.hooks.SessionEnd[0].hooks[0].command).toContain('/custom/.devflow/scripts/hooks/run-hook session-end-kb-refresh'); + }); + + it('is idempotent — does not add duplicate', () => { + const first = addKbHook('{}', DEVFLOW_DIR); + const second = addKbHook(first, DEVFLOW_DIR); + const parsed = JSON.parse(second); + const kbHooks = parsed.hooks.SessionEnd.filter( + (m: { hooks: Array<{ command: string }> }) => + m.hooks.some((h) => h.command.includes('session-end-kb-refresh')) + ); + expect(kbHooks).toHaveLength(1); + }); + + it('adds alongside existing SessionEnd hooks', () => { + const input = JSON.stringify({ + hooks: { + SessionEnd: [ + { hooks: [{ type: 'command', command: '/path/to/other-hook', timeout: 10 }] }, + ], + }, + }); + const result = addKbHook(input, DEVFLOW_DIR); + const parsed = JSON.parse(result); + expect(parsed.hooks.SessionEnd).toHaveLength(2); + }); + + it('preserves other settings', () => { + const input = JSON.stringify({ theme: 'dark', model: 'claude-sonnet' }); + const result = addKbHook(input, DEVFLOW_DIR); + const parsed = JSON.parse(result); + expect(parsed.theme).toBe('dark'); + expect(parsed.model).toBe('claude-sonnet'); + }); + + it('hook entry has correct timeout', () => { + const result = addKbHook('{}', DEVFLOW_DIR); + const parsed = JSON.parse(result); + expect(parsed.hooks.SessionEnd[0].hooks[0].timeout).toBe(10); + expect(parsed.hooks.SessionEnd[0].hooks[0].type).toBe('command'); + }); +}); + +describe('removeKbHook', () => { + it('removes KB hook from SessionEnd', () => { + const withHook = addKbHook('{}', DEVFLOW_DIR); + const result = removeKbHook(withHook); + const parsed = JSON.parse(result); + expect(parsed.hooks).toBeUndefined(); + }); + + it('preserves other SessionEnd hooks', () => { + const input = JSON.stringify({ + hooks: { + SessionEnd: [ + { hooks: [{ type: 'command', command: '/path/to/other-hook', timeout: 10 }] }, + { hooks: [{ type: 'command', command: '/devflow/scripts/hooks/run-hook session-end-kb-refresh', timeout: 10 }] }, + ], + }, + }); + const result = removeKbHook(input); + const parsed = JSON.parse(result); + expect(parsed.hooks.SessionEnd).toHaveLength(1); + expect(parsed.hooks.SessionEnd[0].hooks[0].command).toContain('other-hook'); + }); + + it('cleans empty hooks object when last hook removed', () => { + const withHook = addKbHook('{}', DEVFLOW_DIR); + const result = removeKbHook(withHook); + const parsed = JSON.parse(result); + expect(parsed.hooks).toBeUndefined(); + }); + + it('is idempotent — removing absent hook returns same JSON', () => { + const input = JSON.stringify({ theme: 'dark' }); + const result = removeKbHook(input); + expect(JSON.parse(result)).toEqual(JSON.parse(input)); + }); + + it('preserves other hook event types', () => { + const input = JSON.stringify({ + hooks: { + UserPromptSubmit: [{ hooks: [{ type: 'command', command: '/path/preamble', timeout: 5 }] }], + SessionEnd: [{ hooks: [{ type: 'command', command: '/devflow/scripts/hooks/run-hook session-end-kb-refresh', timeout: 10 }] }], + }, + }); + const result = removeKbHook(input); + const parsed = JSON.parse(result); + expect(parsed.hooks.UserPromptSubmit).toHaveLength(1); + expect(parsed.hooks.SessionEnd).toBeUndefined(); + }); +}); + +describe('hasKbHook', () => { + it('returns true when hook present on SessionEnd', () => { + const withHook = addKbHook('{}', DEVFLOW_DIR); + expect(hasKbHook(withHook)).toBe(true); + }); + + it('returns false when hook absent', () => { + expect(hasKbHook('{}')).toBe(false); + }); + + it('returns false when only other SessionEnd hooks exist', () => { + const input = JSON.stringify({ + hooks: { + SessionEnd: [ + { hooks: [{ type: 'command', command: '/path/to/session-end-learning', timeout: 10 }] }, + ], + }, + }); + expect(hasKbHook(input)).toBe(false); + }); + + it('accepts parsed Settings object', () => { + const withHook = addKbHook('{}', DEVFLOW_DIR); + const parsed = JSON.parse(withHook); + expect(hasKbHook(parsed)).toBe(true); + }); + + it('returns false for empty hooks object', () => { + expect(hasKbHook(JSON.stringify({ hooks: {} }))).toBe(false); + }); +}); diff --git a/tests/manifest.test.ts b/tests/manifest.test.ts index 8c315bb..7e6ffda 100644 --- a/tests/manifest.test.ts +++ b/tests/manifest.test.ts @@ -77,7 +77,7 @@ describe('readManifest', () => { version: '1.4.0', plugins: ['devflow-core-skills', 'devflow-implement'], scope: 'user', - features: { teams: false, ambient: true, memory: true, learn: false, hud: false, flags: [] }, + features: { teams: false, ambient: true, memory: true, learn: false, hud: false, kb: false, flags: [] }, installedAt: '2026-03-01T00:00:00.000Z', updatedAt: '2026-03-13T00:00:00.000Z', }; @@ -100,8 +100,24 @@ describe('readManifest', () => { expect(result).not.toBeNull(); expect(result!.features.hud).toBe(false); expect(result!.features.learn).toBe(false); + expect(result!.features.kb).toBe(false); expect(result!.features.flags).toEqual([]); }); + + it('normalizes old manifest without kb to default false', async () => { + const oldData = { + version: '1.4.0', + plugins: ['devflow-core-skills'], + scope: 'user', + features: { teams: false, ambient: true, memory: true, learn: true, hud: true, flags: [] }, + installedAt: '2026-03-01T00:00:00.000Z', + updatedAt: '2026-03-13T00:00:00.000Z', + }; + await fs.writeFile(path.join(tmpDir, 'manifest.json'), JSON.stringify(oldData), 'utf-8'); + const result = await readManifest(tmpDir); + expect(result).not.toBeNull(); + expect(result!.features.kb).toBe(false); + }); }); describe('writeManifest', () => { @@ -120,7 +136,7 @@ describe('writeManifest', () => { version: '1.4.0', plugins: ['devflow-core-skills'], scope: 'user', - features: { teams: false, ambient: true, memory: true, learn: false, hud: false, flags: [] }, + features: { teams: false, ambient: true, memory: true, learn: false, hud: false, kb: false, flags: [] }, installedAt: '2026-03-13T00:00:00.000Z', updatedAt: '2026-03-13T00:00:00.000Z', }; @@ -134,7 +150,7 @@ describe('writeManifest', () => { version: '1.0.0', plugins: ['devflow-core-skills'], scope: 'user', - features: { teams: false, ambient: false, memory: false, learn: false, hud: false, flags: [] }, + features: { teams: false, ambient: false, memory: false, learn: false, hud: false, kb: false, flags: [] }, installedAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z', }; @@ -153,7 +169,7 @@ describe('writeManifest', () => { version: '1.4.0', plugins: [], scope: 'local', - features: { teams: false, ambient: false, memory: false, learn: false, hud: false, flags: [] }, + features: { teams: false, ambient: false, memory: false, learn: false, hud: false, kb: false, flags: [] }, installedAt: '2026-03-13T00:00:00.000Z', updatedAt: '2026-03-13T00:00:00.000Z', }; @@ -297,7 +313,7 @@ describe('resolvePluginList', () => { version: '1.0.0', plugins: ['devflow-core-skills', 'devflow-implement'], scope: 'user', - features: { teams: false, ambient: true, memory: true, learn: false, hud: false, flags: [] }, + features: { teams: false, ambient: true, memory: true, learn: false, hud: false, kb: false, flags: [] }, installedAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z', }; From ac0b0b6e40502b6165c03d80967b001fda1d1a49 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sun, 26 Apr 2026 14:22:17 +0300 Subject: [PATCH 28/44] feat(kb): add automated staleness refresh via SessionEnd hook - Add stale-slugs and refresh-context CLI subcommands to feature-kb.cjs for bash-friendly output (one slug per line, tab-separated metadata) - Add session-end-kb-refresh SessionEnd hook with 2h throttle guard, disabled-flag check, and DEVFLOW_BG_KB_REFRESH recursion prevention - Add background-kb-refresh script: mkdir-based lock, stale lock recovery, per-slug claude -p refresh with 180s watchdog, cap at 3 KBs per session - Register both new scripts in shell-hooks syntax check tests - Add 9 tests for stale-slugs and refresh-context CLI subcommands --- scripts/hooks/background-kb-refresh | 153 +++++++++++++++++++++++++++ scripts/hooks/lib/feature-kb.cjs | 40 +++++++ scripts/hooks/session-end-kb-refresh | 57 ++++++++++ tests/feature-kb/feature-kb.test.ts | 123 ++++++++++++++++++++- tests/shell-hooks.test.ts | 2 + 5 files changed, 374 insertions(+), 1 deletion(-) create mode 100755 scripts/hooks/background-kb-refresh create mode 100755 scripts/hooks/session-end-kb-refresh diff --git a/scripts/hooks/background-kb-refresh b/scripts/hooks/background-kb-refresh new file mode 100755 index 0000000..97f2555 --- /dev/null +++ b/scripts/hooks/background-kb-refresh @@ -0,0 +1,153 @@ +#!/bin/bash +# Background KB Refresh +# Called by session-end-kb-refresh as a detached background process. +# Reads stale KB slugs from feature-kb.cjs, then spawns a fresh claude -p +# invocation with Sonnet to refresh up to 3 stale KBs. +# On failure: logs error, does nothing (missing updates are better than corrupt data). + +set -e +CWD="$1" +CLAUDE_BIN="${2:-claude}" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +source "$SCRIPT_DIR/log-paths" +LOG_FILE="$(devflow_log_dir "$CWD")/.kb-refresh.log" +LOCK_DIR="$CWD/.features/.kb-refresh.lock" + +# --- Logging --- +log() { + echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] $1" >> "$LOG_FILE" +} + +rotate_log() { + if [ ! -f "$LOG_FILE" ]; then return; fi + local max_lines=100 + local keep_lines=50 + if [ "$(wc -l < "$LOG_FILE")" -gt "$max_lines" ]; then + tail -"$keep_lines" "$LOG_FILE" > "$LOG_FILE.tmp" && mv "$LOG_FILE.tmp" "$LOG_FILE" + fi +} + +# --- Cross-platform mtime --- +# Source the get-mtime helper for portable stat usage. +# shellcheck source=get-mtime +source "$SCRIPT_DIR/get-mtime" + +# --- Cleanup trap --- +cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } +trap cleanup EXIT + +# --- Locking (mkdir-based, POSIX-atomic) --- +# DESIGN: These timeouts follow the same pattern as background-learning. +# The stale threshold (300 s) is intentionally higher than the per-KB timeout +# watchdog (180 s) to avoid breaking a legitimately-running refresh. +STALE_THRESHOLD=300 # 5 min + +acquire_lock() { + local timeout=90 + local waited=0 + while ! mkdir "$LOCK_DIR" 2>/dev/null; do + # Check for stale lock + if [ -d "$LOCK_DIR" ]; then + local lock_mtime now age + lock_mtime=$(get_mtime "$LOCK_DIR" 2>/dev/null || echo "0") + now=$(date +%s) + age=$((now - lock_mtime)) + if [ "$age" -gt "$STALE_THRESHOLD" ]; then + log "Breaking stale lock (age: ${age}s, threshold: ${STALE_THRESHOLD}s)" + rmdir "$LOCK_DIR" 2>/dev/null || true + continue + fi + fi + if [ "$waited" -ge "$timeout" ]; then + log "Failed to acquire lock within ${timeout}s" + return 1 + fi + sleep 1 + waited=$((waited + 1)) + done + return 0 +} + +# Wait for parent session to flush any in-progress work +sleep 3 + +log "Starting KB refresh" +rotate_log + +# Acquire lock +if ! acquire_lock; then + log "Lock timeout — skipping" + trap - EXIT + exit 0 +fi + +# Record refresh timestamp (so next session-end-kb-refresh respects throttle) +date +%s > "$CWD/.features/.kb-last-refresh" + +# Get stale slugs (one per line, cap at 3) +STALE_SLUGS=$(node "$SCRIPT_DIR/lib/feature-kb.cjs" stale-slugs "$CWD" 2>/dev/null | head -3) + +if [ -z "$STALE_SLUGS" ]; then + log "No stale KBs found — done" + exit 0 +fi + +COUNT=0 +for SLUG in $STALE_SLUGS; do + COUNT=$((COUNT + 1)) + log "Refreshing $SLUG ($COUNT/3)..." + + # Read existing KB content + KB_PATH="$CWD/.features/$SLUG/KNOWLEDGE.md" + [ -f "$KB_PATH" ] || { log "KB file missing for $SLUG — skipping"; continue; } + EXISTING=$(cat "$KB_PATH") + + # Get structured metadata (tab-separated: name, dirs, category, changed) + CONTEXT_LINE=$(node "$SCRIPT_DIR/lib/feature-kb.cjs" refresh-context "$CWD" "$SLUG" 2>/dev/null || true) + if [ -z "$CONTEXT_LINE" ]; then + log "Failed to get refresh-context for $SLUG — skipping" + continue + fi + + IFS=$'\t' read -r NAME DIRS CATEGORY CHANGED <<< "$CONTEXT_LINE" + + # Build refresh prompt + PROMPT="You are the Knowledge agent refreshing a stale feature knowledge base. + +FEATURE_SLUG: $SLUG +FEATURE_NAME: $NAME +DIRECTORIES: $DIRS +WORKTREE_PATH: $CWD +CHANGED_FILES: $CHANGED + +EXISTING_KB: +$EXISTING + +Instructions: +- Read the CHANGED_FILES to understand what changed +- Update the stale sections based on changes +- Preserve any manually added content +- Do not regenerate from scratch +- Write the updated KB to .features/$SLUG/KNOWLEDGE.md +- After writing, run update-index with updated metadata: + node scripts/hooks/lib/feature-kb.cjs update-index \"$CWD\" \\ + --slug=\"$SLUG\" --name=\"$NAME\" \\ + --directories='$DIRS' \\ + --referencedFiles='[]' \\ + --category=\"$CATEGORY\" \\ + --createdBy=\"devflow-kb\"" + + # Spawn claude -p with 180s timeout watchdog + DEVFLOW_BG_KB_REFRESH=1 timeout 180 "$CLAUDE_BIN" -p \ + --model sonnet \ + --dangerously-skip-permissions \ + --allowedTools 'Read,Grep,Glob,Write,Bash' \ + --output-format text \ + "$PROMPT" >> "$LOG_FILE" 2>&1 || { + log "Failed to refresh $SLUG (exit $?)" + } +done + +log "Done. Refreshed $COUNT KB(s)." +exit 0 diff --git a/scripts/hooks/lib/feature-kb.cjs b/scripts/hooks/lib/feature-kb.cjs index dd878d7..6e39f17 100644 --- a/scripts/hooks/lib/feature-kb.cjs +++ b/scripts/hooks/lib/feature-kb.cjs @@ -383,6 +383,8 @@ function listKBs(worktreePath) { // node feature-kb.cjs update-index --slug=X --name=Y --directories='[...]' --referencedFiles='[...]' --category=X [--description=Y] [--createdBy=Z] // node feature-kb.cjs find-overlapping [file2...] // node feature-kb.cjs remove +// node feature-kb.cjs stale-slugs +// node feature-kb.cjs refresh-context // --------------------------------------------------------------------------- if (require.main === module) { @@ -410,6 +412,8 @@ if (require.main === module) { ' node feature-kb.cjs update-index --slug=X --name=Y --directories=\'[...]\' --referencedFiles=\'[...]\' --category=X [--description=Y] [--createdBy=Z]', ' node feature-kb.cjs find-overlapping [file2...]', ' node feature-kb.cjs remove ', + ' node feature-kb.cjs stale-slugs ', + ' node feature-kb.cjs refresh-context ', ].join('\n'); /** @@ -507,6 +511,42 @@ if (require.main === module) { process.stdout.write(JSON.stringify({ ok: true, slug }) + '\n'); process.exit(0); }, + + 'stale-slugs'() { + const worktreePath = requireWorktree(argv); + const staleness = checkAllStaleness(worktreePath); + for (const [slug, info] of Object.entries(staleness)) { + if (info.stale) { + process.stdout.write(slug + '\n'); + } + } + process.exit(0); + }, + + 'refresh-context'() { + const worktreePath = requireWorktree(argv); + const slug = argv[2]; + if (!slug) { + process.stderr.write('Error: missing slug argument\n' + USAGE + '\n'); + process.exit(1); + } + validateSlug(slug); + const index = loadIndex(worktreePath); + if (!index || !index.features[slug]) { + process.stderr.write(`Error: KB '${slug}' not found in index\n`); + process.exit(1); + } + const entry = index.features[slug]; + const staleness = checkStaleness(worktreePath, slug); + // Tab-separated: name, directories JSON, category, changed files JSON + process.stdout.write([ + entry.name, + JSON.stringify(entry.directories), + entry.category, + JSON.stringify(staleness.changedFiles), + ].join('\t') + '\n'); + process.exit(0); + }, }; if (!subcommand) { diff --git a/scripts/hooks/session-end-kb-refresh b/scripts/hooks/session-end-kb-refresh new file mode 100755 index 0000000..e869f01 --- /dev/null +++ b/scripts/hooks/session-end-kb-refresh @@ -0,0 +1,57 @@ +#!/bin/bash +# SessionEnd hook: check for stale feature KBs and spawn background refresh +# +# Flow: +# 1. Guard clauses (skip if background process, no JSON tools, etc.) +# 2. Check that .features/index.json exists +# 3. Throttle: skip if refreshed within last 2 hours +# 4. Query stale-slugs from feature-kb.cjs +# 5. Spawn background-kb-refresh in detached background process +# +set -e + +# Guard: prevent recursion from background claude -p +[ "${DEVFLOW_BG_KB_REFRESH:-}" = "1" ] && exit 0 +[ "${DEVFLOW_BG_UPDATER:-}" = "1" ] && exit 0 + +# Source json-parse library (must be available before reading stdin JSON) +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=json-parse +source "$SCRIPT_DIR/json-parse" +if [ "$_JSON_AVAILABLE" = "false" ]; then exit 0; fi + +# Read hook input from stdin +INPUT=$(cat) +CWD=$(printf '%s' "$INPUT" | json_field "cwd" "") +[ -z "$CWD" ] && exit 0 + +# Quick guards +[ -f "$CWD/.features/index.json" ] || exit 0 +[ -f "$CWD/.features/.disabled" ] && exit 0 + +# Throttle: skip if refreshed within last 2 hours +MARKER="$CWD/.features/.kb-last-refresh" +if [ -f "$MARKER" ]; then + LAST=$(cat "$MARKER") + NOW=$(date +%s) + [ $((NOW - LAST)) -lt 7200 ] && exit 0 +fi + +# Check for stale KBs (bash-friendly output, one slug per line) +STALE_SLUGS=$(node "$SCRIPT_DIR/lib/feature-kb.cjs" stale-slugs "$CWD" 2>/dev/null) +[ -z "$STALE_SLUGS" ] && exit 0 + +# Resolve claude binary (PATH lookup, exit silently if missing) +CLAUDE_BIN=$(command -v claude 2>/dev/null || true) +[ -z "$CLAUDE_BIN" ] && exit 0 + +# Spawn background refresh +source "$SCRIPT_DIR/log-paths" +LOG_FILE="$(devflow_log_dir "$CWD")/.kb-refresh.log" +mkdir -p "$(dirname "$LOG_FILE")" + +DEVFLOW_BG_KB_REFRESH=1 nohup bash "$SCRIPT_DIR/background-kb-refresh" \ + "$CWD" "$CLAUDE_BIN" >> "$LOG_FILE" 2>&1 & +disown + +exit 0 diff --git a/tests/feature-kb/feature-kb.test.ts b/tests/feature-kb/feature-kb.test.ts index 06aff1b..dba0c17 100644 --- a/tests/feature-kb/feature-kb.test.ts +++ b/tests/feature-kb/feature-kb.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, afterAll } from 'vitest'; import * as path from 'path'; import { createRequire } from 'module'; import { writeFileSync, mkdirSync, readFileSync, existsSync, rmSync, rmdirSync } from 'fs'; -import { execSync } from 'child_process'; +import { execSync, execFileSync } from 'child_process'; import { SAMPLE_INDEX, SAMPLE_KB_CONTENT, @@ -448,3 +448,124 @@ describe('validateSlug', () => { expect(() => validateSlug(undefined)).toThrow(); }); }); + +// --------------------------------------------------------------------------- +// CLI: stale-slugs subcommand +// --------------------------------------------------------------------------- + +const FEATURE_KB_CJS = path.join(ROOT, 'scripts/hooks/lib/feature-kb.cjs'); + +describe('CLI stale-slugs', () => { + it('outputs nothing for non-stale index (non-git repo)', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + // Non-git repo → checkAllStaleness returns stale: false for everything + const output = execFileSync('node', [FEATURE_KB_CJS, 'stale-slugs', tmp], { encoding: 'utf8' }); + expect(output.trim()).toBe(''); + }); + + it('outputs stale slugs one per line for a git repo with changes', () => { + const tmp = makeTmpFeatureWorktree(); + // Remove auto-created .features dir — we'll set it up after git init + rmSync(path.join(tmp, '.features'), { recursive: true, force: true }); + + execSync('git init', { cwd: tmp, stdio: 'pipe' }); + execSync('git config user.email "test@test.com"', { cwd: tmp, stdio: 'pipe' }); + execSync('git config user.name "Test"', { cwd: tmp, stdio: 'pipe' }); + + const srcDir = path.join(tmp, 'src', 'cli'); + mkdirSync(srcDir, { recursive: true }); + writeFileSync(path.join(srcDir, 'cli.ts'), 'export const v = 1;'); + execSync('git add .', { cwd: tmp, stdio: 'pipe' }); + execSync('git commit -m "initial"', { cwd: tmp, stdio: 'pipe' }); + + const lastUpdated = new Date(Date.now() - 5000).toISOString(); + const featuresDir = path.join(tmp, '.features'); + mkdirSync(featuresDir, { recursive: true }); + writeFileSync(path.join(featuresDir, 'index.json'), JSON.stringify({ + version: 1, + features: { + 'stale-feature': { + name: 'Stale Feature', + description: '', + directories: ['src/cli/'], + referencedFiles: ['src/cli/cli.ts'], + category: 'test', + lastUpdated, + createdBy: 'test', + }, + }, + }, null, 2)); + + // Modify the referenced file and commit after lastUpdated + writeFileSync(path.join(srcDir, 'cli.ts'), 'export const v = 2;'); + execSync('git add .', { cwd: tmp, stdio: 'pipe' }); + execSync('git commit -m "update cli.ts"', { cwd: tmp, stdio: 'pipe' }); + + const output = execFileSync('node', [FEATURE_KB_CJS, 'stale-slugs', tmp], { encoding: 'utf8' }); + expect(output.trim().split('\n')).toContain('stale-feature'); + }); + + it('exits non-zero and prints usage when worktree argument is missing', () => { + let threw = false; + try { + execFileSync('node', [FEATURE_KB_CJS, 'stale-slugs'], { encoding: 'utf8', stdio: 'pipe' }); + } catch (e: unknown) { + threw = true; + expect((e as NodeJS.ErrnoException & { status?: number }).status).toBe(1); + } + expect(threw).toBe(true); + }); +}); + +// --------------------------------------------------------------------------- +// CLI: refresh-context subcommand +// --------------------------------------------------------------------------- + +describe('CLI refresh-context', () => { + it('outputs tab-separated metadata for an existing KB entry', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + const output = execFileSync('node', [FEATURE_KB_CJS, 'refresh-context', tmp, 'cli-commands'], { encoding: 'utf8' }); + const parts = output.trim().split('\t'); + expect(parts).toHaveLength(4); + expect(parts[0]).toBe('CLI Command System'); // name + expect(JSON.parse(parts[1])).toBeInstanceOf(Array); // directories JSON + expect(parts[2]).toBe('component-patterns'); // category + expect(JSON.parse(parts[3])).toBeInstanceOf(Array); // changed files JSON + }); + + it('exits non-zero when slug is missing', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + let threw = false; + try { + execFileSync('node', [FEATURE_KB_CJS, 'refresh-context', tmp], { encoding: 'utf8', stdio: 'pipe' }); + } catch (e: unknown) { + threw = true; + expect((e as NodeJS.ErrnoException & { status?: number }).status).toBe(1); + } + expect(threw).toBe(true); + }); + + it('exits non-zero when slug is not found in index', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + let threw = false; + try { + execFileSync('node', [FEATURE_KB_CJS, 'refresh-context', tmp, 'nonexistent'], { encoding: 'utf8', stdio: 'pipe' }); + } catch (e: unknown) { + threw = true; + expect((e as NodeJS.ErrnoException & { status?: number }).status).toBe(1); + } + expect(threw).toBe(true); + }); + + it('exits non-zero for invalid slug (path traversal)', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + let threw = false; + try { + execFileSync('node', [FEATURE_KB_CJS, 'refresh-context', tmp, '../etc'], { encoding: 'utf8', stdio: 'pipe' }); + } catch (e: unknown) { + threw = true; + expect((e as NodeJS.ErrnoException & { status?: number }).status).toBe(1); + } + expect(threw).toBe(true); + }); +}); diff --git a/tests/shell-hooks.test.ts b/tests/shell-hooks.test.ts index 0a351ba..7c993ae 100644 --- a/tests/shell-hooks.test.ts +++ b/tests/shell-hooks.test.ts @@ -20,6 +20,8 @@ const HOOK_SCRIPTS = [ 'preamble', 'json-parse', 'get-mtime', + 'session-end-kb-refresh', + 'background-kb-refresh', ]; describe('shell hook syntax checks', () => { From dbf2f36ac9d3f3c32c319b3e0e16c37518e3796b Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sun, 26 Apr 2026 14:25:50 +0300 Subject: [PATCH 29/44] fix(hooks): heal missing .memory/knowledge dir in session-start-memory Older installs have .memory/ but not .memory/knowledge/. Add a guard that creates the directory on-demand so the knowledge TL;DR section doesn't silently skip on first run after upgrade. Co-Authored-By: Claude --- scripts/hooks/session-start-memory | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/hooks/session-start-memory b/scripts/hooks/session-start-memory index 92d5a55..3242715 100644 --- a/scripts/hooks/session-start-memory +++ b/scripts/hooks/session-start-memory @@ -110,6 +110,10 @@ fi # --- Section 1.5: Project Knowledge TL;DR --- KNOWLEDGE_DIR="$CWD/.memory/knowledge" +# Heal older installs that have .memory/ but not .memory/knowledge/ +if [ -d "$CWD/.memory" ] && [ ! -d "$KNOWLEDGE_DIR" ]; then + mkdir -p "$KNOWLEDGE_DIR" 2>/dev/null || true +fi if [ -d "$KNOWLEDGE_DIR" ]; then KNOWLEDGE_TLDR="" for kf in "$KNOWLEDGE_DIR"/decisions.md "$KNOWLEDGE_DIR"/pitfalls.md; do From fd7e6bbd49f8295481b7b6b6fe857bfe79e1daeb Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sun, 26 Apr 2026 14:25:56 +0300 Subject: [PATCH 30/44] docs: update CLAUDE.md with KB toggleability and auto-refresh Document the new toggle CLI (devflow kb --enable/--disable/--status and devflow init --kb/--no-kb), the SessionEnd auto-refresh hook behaviour (throttled, max 3 per run), and the .features/.disabled sentinel that gates Phase 12 generation. Also add session-end-kb-refresh and background-kb-refresh to the hooks list in the project structure tree. Co-Authored-By: Claude --- CLAUDE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4ea507a..41f1125 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,7 +46,7 @@ Commands with Teams Variant ship as `{name}.md` (parallel subagents) and `{name} **Claude Code Flags**: Typed registry (`src/cli/utils/flags.ts`) for managing Claude Code feature flags (env vars and top-level settings). Pure functions `applyFlags`/`stripFlags`/`getDefaultFlags` follow the `applyTeamsConfig`/`stripTeamsConfig` pattern. Initial flags: `tool-search`, `lsp`, `clear-context-on-plan` (default ON), `brief`, `disable-1m-context` (default OFF). Manageable via `devflow flags --enable/--disable/--status/--list`. Stored in manifest `features.flags: string[]`. -**Feature Knowledge Bases**: Per-feature `.features/` directory containing KNOWLEDGE.md files that capture area-specific patterns, conventions, architecture, and gotchas. KBs are created as side-effects of planning (plan:orch Phase 12), loaded automatically across all workflows via `FEATURE_KNOWLEDGE` variable (companion to `KNOWLEDGE_CONTEXT`), and use staleness detection via git log against `referencedFiles`. Index at `.features/index.json` (object keyed by slug). Managed via `devflow kb list|create|check|refresh|remove`. Knowledge agent (sonnet) structures exploration outputs into KNOWLEDGE.md. `apply-feature-kb` skill provides consumption algorithm for agents. `.features/.kb.lock` is gitignored (transient lock directory for concurrent index writes, added automatically by `devflow init`). `devflow kb list` — List all feature KBs with staleness status. `devflow kb create ` — Create a new KB via claude -p exploration. `devflow kb check` — Check all KBs for staleness. `devflow kb refresh [slug]` — Refresh stale KB(s). `devflow kb remove ` — Remove a KB and its index entry. Note: debug:orch keeps FEATURE_KNOWLEDGE orchestrator-local (investigation workers examine code without pre-loaded context). +**Feature Knowledge Bases**: Per-feature `.features/` directory containing KNOWLEDGE.md files that capture area-specific patterns, conventions, architecture, and gotchas. KBs are created as side-effects of planning (plan:orch Phase 12), loaded automatically across all workflows via `FEATURE_KNOWLEDGE` variable (companion to `KNOWLEDGE_CONTEXT`), and use staleness detection via git log against `referencedFiles`. Index at `.features/index.json` (object keyed by slug). Managed via `devflow kb list|create|check|refresh|remove`. Knowledge agent (sonnet) structures exploration outputs into KNOWLEDGE.md. `apply-feature-kb` skill provides consumption algorithm for agents. `.features/.kb.lock` is gitignored (transient lock directory for concurrent index writes, added automatically by `devflow init`). `devflow kb list` — List all feature KBs with staleness status. `devflow kb create ` — Create a new KB via claude -p exploration. `devflow kb check` — Check all KBs for staleness. `devflow kb refresh [slug]` — Refresh stale KB(s). `devflow kb remove ` — Remove a KB and its index entry. Note: debug:orch keeps FEATURE_KNOWLEDGE orchestrator-local (investigation workers examine code without pre-loaded context). Toggleable via `devflow kb --enable/--disable/--status` or `devflow init --kb/--no-kb`. SessionEnd hook auto-refreshes stale KBs (throttled to once per 2 hours, max 3 per run). `.features/.disabled` sentinel gates Phase 12 generation and refresh hook. **Two-Mode Init**: `devflow init` offers Recommended (sensible defaults, quick setup) or Advanced (full interactive flow) after plugin selection. `--recommended` / `--advanced` CLI flags for non-interactive use. Recommended applies: ambient ON, memory ON, learn ON, HUD ON, teams OFF, default-ON flags, .claudeignore ON, auto-install safe-delete if trash CLI detected, user-mode security deny list. @@ -61,7 +61,7 @@ devflow/ ├── plugins/devflow-*/ # 17 plugins (8 core + 9 optional language/ecosystem) ├── docs/reference/ # Detailed reference documentation ├── scripts/ # Helper scripts (statusline, docs-helpers) -│ └── hooks/ # Working Memory + ambient + learning hooks (prompt-capture-memory, stop-update-memory, background-memory-update, session-start-memory, session-start-classification, pre-compact-memory, preamble, session-end-learning, stop-update-learning [deprecated], background-learning, get-mtime) +│ └── hooks/ # Working Memory + ambient + learning hooks (prompt-capture-memory, stop-update-memory, background-memory-update, session-start-memory, session-start-classification, pre-compact-memory, preamble, session-end-learning, stop-update-learning [deprecated], background-learning, get-mtime, session-end-kb-refresh, background-kb-refresh) ├── src/cli/ # TypeScript CLI (init, list, uninstall, ambient, learn, flags, kb) ├── .claude-plugin/ # Marketplace registry ├── .docs/ # Project docs (reviews, design) — per-project From ea7c4d1e5050561450b1a1f0d2cd5a271c242ecd Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sun, 26 Apr 2026 14:26:01 +0300 Subject: [PATCH 31/44] test(kb): add stale-slugs and refresh-context CLI tests Five new tests cover the stale-slugs and refresh-context subcommands of feature-kb.cjs: empty output for non-git worktrees, empty output for empty index, tab-separated metadata format verification, exit 1 for missing slug argument, and exit 1 for unknown slug. All tests use the existing makeTmpFeatureWorktree/SAMPLE_INDEX fixtures. Co-Authored-By: Claude --- tests/feature-kb/kb-command.test.ts | 40 +++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/feature-kb/kb-command.test.ts b/tests/feature-kb/kb-command.test.ts index d79bc8e..231cfe3 100644 --- a/tests/feature-kb/kb-command.test.ts +++ b/tests/feature-kb/kb-command.test.ts @@ -78,4 +78,44 @@ describe('feature-kb.cjs CLI', () => { const result = execSync(`node ${CJS_PATH} find-overlapping ${tmp} src/payments/checkout.ts`, { encoding: 'utf8' }); expect(JSON.parse(result)).toEqual([]); }); + + it('stale-slugs outputs only stale slugs one per line', () => { + // Non-git worktree — checkAllStaleness returns stale:false for non-git paths + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + const result = execSync(`node ${CJS_PATH} stale-slugs ${tmp}`, { encoding: 'utf8' }); + // Non-git worktree => no stale slugs + expect(result.trim()).toBe(''); + }); + + it('stale-slugs outputs nothing for empty index', () => { + const tmp = makeTmpFeatureWorktree({ version: 1, features: {} }); + const result = execSync(`node ${CJS_PATH} stale-slugs ${tmp}`, { encoding: 'utf8' }); + expect(result.trim()).toBe(''); + }); + + it('refresh-context outputs tab-separated metadata', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + const result = execSync(`node ${CJS_PATH} refresh-context ${tmp} cli-commands`, { encoding: 'utf8' }); + const parts = result.trim().split('\t'); + expect(parts).toHaveLength(4); + expect(parts[0]).toBe('CLI Command System'); // name + expect(JSON.parse(parts[1])).toEqual(['src/cli/commands/', 'src/cli/utils/']); // directories JSON + expect(parts[2]).toBe('component-patterns'); // category + // changedFiles is a JSON array (could be empty for non-git) + expect(() => JSON.parse(parts[3])).not.toThrow(); + }); + + it('refresh-context exits 1 for missing slug argument', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + expect(() => + execSync(`node ${CJS_PATH} refresh-context ${tmp}`, { encoding: 'utf8', stdio: 'pipe' }) + ).toThrow(); + }); + + it('refresh-context exits 1 for unknown slug', () => { + const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); + expect(() => + execSync(`node ${CJS_PATH} refresh-context ${tmp} nonexistent`, { encoding: 'utf8', stdio: 'pipe' }) + ).toThrow(); + }); }); From e4225228c2016a1694279e9cf394ca29e8a7876d Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sun, 26 Apr 2026 14:31:11 +0300 Subject: [PATCH 32/44] refactor(kb): simplify hook functions and extract shared variables Simplifier pass: remove redundant removeKbHook call from addKbHook, simplify non-null assertions in removeKbHook, extract shared variables from toggle branches, hoist listKBs call out of refresh loop. --- src/cli/commands/kb.ts | 44 ++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/cli/commands/kb.ts b/src/cli/commands/kb.ts index e7db2a0..8131453 100644 --- a/src/cli/commands/kb.ts +++ b/src/cli/commands/kb.ts @@ -66,14 +66,13 @@ export function addKbHook(settingsJson: string, devflowDir: string): string { return settingsJson; } - const cleanedJson = removeKbHook(settingsJson); - const settings: Settings = JSON.parse(cleanedJson); + const settings: Settings = JSON.parse(settingsJson); if (!settings.hooks) { settings.hooks = {}; } - const hookCommand = path.join(devflowDir, 'scripts', 'hooks', 'run-hook') + ' session-end-kb-refresh'; + const hookCommand = `${path.join(devflowDir, 'scripts', 'hooks', 'run-hook')} session-end-kb-refresh`; const newEntry: HookMatcher = { hooks: [ @@ -105,12 +104,15 @@ export function removeKbHook(settingsJson: string): string { const matchers = settings.hooks?.SessionEnd; if (matchers) { - const before = matchers.length; - settings.hooks!.SessionEnd = matchers.filter( + const filtered = matchers.filter( (m) => !m.hooks.some((h) => h.command.includes(KB_HOOK_MARKER)), ); - if (settings.hooks!.SessionEnd!.length < before) changed = true; - if (settings.hooks!.SessionEnd!.length === 0) delete settings.hooks!.SessionEnd; + if (filtered.length < matchers.length) changed = true; + if (filtered.length === 0) { + delete settings.hooks!.SessionEnd; + } else { + settings.hooks!.SessionEnd = filtered; + } } if (!changed) { @@ -142,11 +144,13 @@ export const kbCommand = new Command('kb') .action(async (options: { enable?: boolean; disable?: boolean; status?: boolean }) => { if (!options.enable && !options.disable && !options.status) return; + const worktreePath = await getWorktreePath(); + const claudeDir = getClaudeDirectory(); + const devflowDir = getDevFlowDirectory(); + const settingsPath = path.join(claudeDir, 'settings.json'); + if (options.enable) { p.intro(color.cyan('Enable Feature Knowledge Bases')); - const worktreePath = await getWorktreePath(); - const claudeDir = getClaudeDirectory(); - const devflowDir = getDevFlowDirectory(); // Create .features/index.json if missing const featuresDir = path.join(worktreePath, '.features'); @@ -162,7 +166,6 @@ export const kbCommand = new Command('kb') try { await fs.unlink(path.join(featuresDir, '.disabled')); } catch { /* doesn't exist */ } // Add SessionEnd hook - const settingsPath = path.join(claudeDir, 'settings.json'); try { const content = await fs.readFile(settingsPath, 'utf-8'); const updated = addKbHook(content, devflowDir); @@ -184,9 +187,6 @@ export const kbCommand = new Command('kb') } else if (options.disable) { p.intro(color.cyan('Disable Feature Knowledge Bases')); - const worktreePath = await getWorktreePath(); - const claudeDir = getClaudeDirectory(); - const devflowDir = getDevFlowDirectory(); // Create .disabled sentinel const featuresDir = path.join(worktreePath, '.features'); @@ -194,7 +194,6 @@ export const kbCommand = new Command('kb') await fs.writeFile(path.join(featuresDir, '.disabled'), '', 'utf-8'); // Remove SessionEnd hook - const settingsPath = path.join(claudeDir, 'settings.json'); try { const content = await fs.readFile(settingsPath, 'utf-8'); const updated = removeKbHook(content); @@ -215,16 +214,14 @@ export const kbCommand = new Command('kb') p.log.info('Existing KBs preserved. Manual commands (create/refresh) still work.'); p.outro(''); - } else if (options.status) { + } else { + // options.status p.intro(color.cyan('Feature KB Status')); - const worktreePath = await getWorktreePath(); - const claudeDir = getClaudeDirectory(); - const devflowDir = getDevFlowDirectory(); // Check hook let hookPresent = false; try { - const content = await fs.readFile(path.join(claudeDir, 'settings.json'), 'utf-8'); + const content = await fs.readFile(settingsPath, 'utf-8'); hookPresent = hasKbHook(content); } catch { /* settings.json may not exist */ } @@ -238,8 +235,8 @@ export const kbCommand = new Command('kb') // Count KBs const kbs = featureKb.listKBs(worktreePath); - const status = hookPresent && !disabled ? 'enabled' : 'disabled'; - p.log.info(`Status: ${status === 'enabled' ? color.green('enabled') : color.yellow('disabled')}`); + const enabled = hookPresent && !disabled; + p.log.info(`Status: ${enabled ? color.green('enabled') : color.yellow('disabled')}`); p.log.info(`Hook: ${hookPresent ? color.green('installed') : color.dim('not installed')}`); p.log.info(`KBs: ${kbs.length}`); if (disabled) { @@ -468,12 +465,13 @@ kbCommand p.log.info(`Refreshing ${slugsToRefresh.length} KB${slugsToRefresh.length === 1 ? '' : 's'}: ${slugsToRefresh.join(', ')}`); + const kbs = featureKb.listKBs(worktreePath); + for (const kbSlug of slugsToRefresh) { const s = p.spinner(); s.start(`Refreshing ${kbSlug}...`); const staleInfo = featureKb.checkStaleness(worktreePath, kbSlug); - const kbs = featureKb.listKBs(worktreePath); const kbEntry = kbs.find((k: { slug: string }) => k.slug === kbSlug); const featureName = kbEntry?.name ?? kbSlug; const kbDirectories = kbEntry?.directories ?? []; From 998f2b282050ccc7192ac645c2a88c2788b33996 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sun, 26 Apr 2026 14:35:21 +0300 Subject: [PATCH 33/44] fix: address self-review issues Replace GNU `timeout` with POSIX-compatible watchdog pattern in background-kb-refresh. macOS does not ship with `timeout` (GNU coreutils), which would silently fail the background refresh process. Uses the same background sleep+kill pattern as background-learning. --- scripts/hooks/background-kb-refresh | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/scripts/hooks/background-kb-refresh b/scripts/hooks/background-kb-refresh index 97f2555..e36a823 100755 --- a/scripts/hooks/background-kb-refresh +++ b/scripts/hooks/background-kb-refresh @@ -138,15 +138,29 @@ Instructions: --category=\"$CATEGORY\" \\ --createdBy=\"devflow-kb\"" - # Spawn claude -p with 180s timeout watchdog - DEVFLOW_BG_KB_REFRESH=1 timeout 180 "$CLAUDE_BIN" -p \ + # Spawn claude -p with 180s watchdog (POSIX-compatible, no GNU timeout) + DEVFLOW_BG_KB_REFRESH=1 "$CLAUDE_BIN" -p \ --model sonnet \ --dangerously-skip-permissions \ --allowedTools 'Read,Grep,Glob,Write,Bash' \ --output-format text \ - "$PROMPT" >> "$LOG_FILE" 2>&1 || { - log "Failed to refresh $SLUG (exit $?)" - } + "$PROMPT" >> "$LOG_FILE" 2>&1 & + CLAUDE_PID=$! + + ( sleep 180 && kill "$CLAUDE_PID" 2>/dev/null ) & + WATCHDOG_PID=$! + + if ! wait "$CLAUDE_PID" 2>/dev/null; then + EXIT_CODE=$? + if [ "$EXIT_CODE" -gt 128 ]; then + log "Refresh of $SLUG timed out (killed after 180s)" + else + log "Failed to refresh $SLUG (exit $EXIT_CODE)" + fi + fi + + kill "$WATCHDOG_PID" 2>/dev/null || true + wait "$WATCHDOG_PID" 2>/dev/null || true done log "Done. Refreshed $COUNT KB(s)." From dcee61b5d9dec54bd283e61785026115b4b0fb84 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Mon, 27 Apr 2026 11:38:23 +0300 Subject: [PATCH 34/44] fix(kb): harden background and CLI KB refresh Remove set -e from all 3 background hooks (existing || true guards suffice). Remove Bash from KB agent allowedTools to prevent prompt injection via shell commands. Switch to sidecar pattern: agent writes .refresh-result.json / .create-result.json, caller reads it and updates index directly. Pass pre-computed stale slugs from session-end to background hook. Touch lock between iterations to prevent watchdog from breaking it during sequential refreshes. --- scripts/hooks/background-kb-refresh | 52 ++++++++++------ scripts/hooks/background-learning | 2 - scripts/hooks/background-memory-update | 2 - scripts/hooks/session-end-kb-refresh | 2 +- src/cli/commands/kb.ts | 84 +++++++++++++++++--------- 5 files changed, 92 insertions(+), 50 deletions(-) diff --git a/scripts/hooks/background-kb-refresh b/scripts/hooks/background-kb-refresh index e36a823..9feaf08 100755 --- a/scripts/hooks/background-kb-refresh +++ b/scripts/hooks/background-kb-refresh @@ -5,9 +5,9 @@ # invocation with Sonnet to refresh up to 3 stale KBs. # On failure: logs error, does nothing (missing updates are better than corrupt data). -set -e CWD="$1" CLAUDE_BIN="${2:-claude}" +PRE_SLUGS="$3" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "$SCRIPT_DIR/log-paths" @@ -85,8 +85,12 @@ fi # Record refresh timestamp (so next session-end-kb-refresh respects throttle) date +%s > "$CWD/.features/.kb-last-refresh" -# Get stale slugs (one per line, cap at 3) -STALE_SLUGS=$(node "$SCRIPT_DIR/lib/feature-kb.cjs" stale-slugs "$CWD" 2>/dev/null | head -3) +# Get stale slugs: use pre-computed list from session-end if available, else compute +if [ -n "$PRE_SLUGS" ]; then + STALE_SLUGS=$(printf '%s' "$PRE_SLUGS" | head -3) +else + STALE_SLUGS=$(node "$SCRIPT_DIR/lib/feature-kb.cjs" stale-slugs "$CWD" 2>/dev/null | head -3) +fi if [ -z "$STALE_SLUGS" ]; then log "No stale KBs found — done" @@ -98,10 +102,11 @@ for SLUG in $STALE_SLUGS; do COUNT=$((COUNT + 1)) log "Refreshing $SLUG ($COUNT/3)..." - # Read existing KB content KB_PATH="$CWD/.features/$SLUG/KNOWLEDGE.md" [ -f "$KB_PATH" ] || { log "KB file missing for $SLUG — skipping"; continue; } - EXISTING=$(cat "$KB_PATH") + + SIDECAR="$CWD/.features/$SLUG/.refresh-result.json" + rm -f "$SIDECAR" # Get structured metadata (tab-separated: name, dirs, category, changed) CONTEXT_LINE=$(node "$SCRIPT_DIR/lib/feature-kb.cjs" refresh-context "$CWD" "$SLUG" 2>/dev/null || true) @@ -112,7 +117,6 @@ for SLUG in $STALE_SLUGS; do IFS=$'\t' read -r NAME DIRS CATEGORY CHANGED <<< "$CONTEXT_LINE" - # Build refresh prompt PROMPT="You are the Knowledge agent refreshing a stale feature knowledge base. FEATURE_SLUG: $SLUG @@ -121,28 +125,20 @@ DIRECTORIES: $DIRS WORKTREE_PATH: $CWD CHANGED_FILES: $CHANGED -EXISTING_KB: -$EXISTING - Instructions: +- Read .features/$SLUG/KNOWLEDGE.md to see the existing KB content - Read the CHANGED_FILES to understand what changed - Update the stale sections based on changes - Preserve any manually added content - Do not regenerate from scratch - Write the updated KB to .features/$SLUG/KNOWLEDGE.md -- After writing, run update-index with updated metadata: - node scripts/hooks/lib/feature-kb.cjs update-index \"$CWD\" \\ - --slug=\"$SLUG\" --name=\"$NAME\" \\ - --directories='$DIRS' \\ - --referencedFiles='[]' \\ - --category=\"$CATEGORY\" \\ - --createdBy=\"devflow-kb\"" +- Write .features/$SLUG/.refresh-result.json with: {\"referencedFiles\": [<5-10 key files from explored directories for staleness tracking>]}" # Spawn claude -p with 180s watchdog (POSIX-compatible, no GNU timeout) DEVFLOW_BG_KB_REFRESH=1 "$CLAUDE_BIN" -p \ --model sonnet \ --dangerously-skip-permissions \ - --allowedTools 'Read,Grep,Glob,Write,Bash' \ + --allowedTools 'Read,Grep,Glob,Write' \ --output-format text \ "$PROMPT" >> "$LOG_FILE" 2>&1 & CLAUDE_PID=$! @@ -161,6 +157,28 @@ Instructions: kill "$WATCHDOG_PID" 2>/dev/null || true wait "$WATCHDOG_PID" 2>/dev/null || true + + # Read sidecar and update index + if [ -f "$SIDECAR" ]; then + REF_FILES=$(node -e " + try { + const d = JSON.parse(require('fs').readFileSync('$SIDECAR','utf8')); + console.log(JSON.stringify(d.referencedFiles || [])); + } catch { console.log('[]'); } + " 2>/dev/null || echo "[]") + node "$SCRIPT_DIR/lib/feature-kb.cjs" update-index "$CWD" \ + --slug="$SLUG" --name="$NAME" \ + --directories="$DIRS" \ + --referencedFiles="$REF_FILES" \ + --category="$CATEGORY" \ + --createdBy="devflow-kb" >> "$LOG_FILE" 2>&1 || log "Failed to update index for $SLUG" + rm -f "$SIDECAR" + else + log "No sidecar file for $SLUG — skipping index update" + fi + + # Refresh lock mtime so watchdog doesn't break it between sequential refreshes + touch "$LOCK_DIR" done log "Done. Refreshed $COUNT KB(s)." diff --git a/scripts/hooks/background-learning b/scripts/hooks/background-learning index 60ffe65..573e6ec 100755 --- a/scripts/hooks/background-learning +++ b/scripts/hooks/background-learning @@ -6,8 +6,6 @@ # invocation with Sonnet to detect patterns and update learning-log.jsonl. # On failure: logs error, does nothing (missing patterns are better than fake data). -set -e - CWD="$1" MODE="${2:---batch}" CLAUDE_BIN="${3:-claude}" diff --git a/scripts/hooks/background-memory-update b/scripts/hooks/background-memory-update index 0b5f349..37b6f44 100755 --- a/scripts/hooks/background-memory-update +++ b/scripts/hooks/background-memory-update @@ -6,8 +6,6 @@ # `claude -p` invocation to update .memory/WORKING-MEMORY.md. # On failure: logs error, does nothing (stale memory is better than fake data). -set -e - CWD="$1" CLAUDE_BIN="$2" diff --git a/scripts/hooks/session-end-kb-refresh b/scripts/hooks/session-end-kb-refresh index e869f01..9ec5f86 100755 --- a/scripts/hooks/session-end-kb-refresh +++ b/scripts/hooks/session-end-kb-refresh @@ -51,7 +51,7 @@ LOG_FILE="$(devflow_log_dir "$CWD")/.kb-refresh.log" mkdir -p "$(dirname "$LOG_FILE")" DEVFLOW_BG_KB_REFRESH=1 nohup bash "$SCRIPT_DIR/background-kb-refresh" \ - "$CWD" "$CLAUDE_BIN" >> "$LOG_FILE" 2>&1 & + "$CWD" "$CLAUDE_BIN" "$STALE_SLUGS" >> "$LOG_FILE" 2>&1 & disown exit 0 diff --git a/src/cli/commands/kb.ts b/src/cli/commands/kb.ts index 8131453..e81b78b 100644 --- a/src/cli/commands/kb.ts +++ b/src/cli/commands/kb.ts @@ -25,6 +25,7 @@ interface FeatureKbModule { findOverlapping: (worktreePath: string, changedFiles: string[]) => string[]; removeEntry: (worktreePath: string, slug: string) => void; validateSlug: (slug: string) => void; + updateIndex: (worktreePath: string, entry: { slug: string; name: string; description?: string; directories: string[]; referencedFiles: string[]; category: string; createdBy?: string }, lockTimeoutMs?: number) => void; } // dist/cli/commands/kb.js → ../../.. → project root (where scripts/ lives) @@ -33,7 +34,7 @@ const featureKb: FeatureKbModule = _require( ); /** Tools passed to `claude -p` when spawning the Knowledge agent. */ -const KB_AGENT_TOOLS = 'Read,Grep,Glob,Write,Bash'; +const KB_AGENT_TOOLS = 'Read,Grep,Glob,Write'; /** * Validate a KB slug and exit with an error message if invalid. @@ -377,6 +378,9 @@ kbCommand const s = p.spinner(); s.start('Creating KB...'); + const sidecarPath = path.join(worktreePath, '.features', slug, '.create-result.json'); + try { await fs.unlink(sidecarPath); } catch { /* doesn't exist */ } + const prompt = [ `You are the Knowledge agent. Create a feature knowledge base for the following area:`, ``, @@ -391,17 +395,12 @@ kbCommand `3. Distill into actionable cross-cutting knowledge`, `4. Write .features/${slug}/KNOWLEDGE.md with all required sections`, ``, - `After writing KNOWLEDGE.md, register it in the index:`, - `1. Select 5-10 key files from the explored directories for staleness tracking`, - `2. Determine the best category: architecture, conventions, component-patterns, domain-knowledge, or lessons-learned`, - `3. Write a one-line description starting with "Use when" for relevance matching`, - `4. Run: node scripts/hooks/lib/feature-kb.cjs update-index "${worktreePath}" \\`, - ` --slug="${slug}" --name="${name as string}" \\`, - ` --directories='${JSON.stringify(directories)}' \\`, - ` --referencedFiles='[]' \\`, - ` --category="" \\`, - ` --description="" \\`, - ` --createdBy="devflow-kb"`, + `After writing KNOWLEDGE.md, write .features/${slug}/.create-result.json with:`, + `{`, + ` "referencedFiles": [<5-10 key files from the explored directories for staleness tracking>],`, + ` "category": "",`, + ` "description": ""`, + `}`, ``, `Create the directory if needed. Report KB_STATUS when done.`, ].join('\n'); @@ -416,9 +415,28 @@ kbCommand stdio: 'pipe', encoding: 'utf8', }); + + let sidecar: { referencedFiles?: string[]; category?: string; description?: string } = {}; + try { + sidecar = JSON.parse(await fs.readFile(sidecarPath, 'utf8')); + } catch { /* agent didn't write sidecar */ } + + featureKb.updateIndex(worktreePath, { + slug, + name: name as string, + directories, + referencedFiles: sidecar.referencedFiles ?? [], + category: sidecar.category ?? 'component-patterns', + description: sidecar.description, + createdBy: 'devflow-kb', + }); + + try { await fs.unlink(sidecarPath); } catch { /* already cleaned */ } + s.stop('KB created successfully'); p.log.success(`KB written to .features/${slug}/KNOWLEDGE.md`); } catch (err) { + try { await fs.unlink(sidecarPath); } catch { /* cleanup */ } s.stop('KB creation failed'); p.log.error(`claude exited with error: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); @@ -475,11 +493,9 @@ kbCommand const kbEntry = kbs.find((k: { slug: string }) => k.slug === kbSlug); const featureName = kbEntry?.name ?? kbSlug; const kbDirectories = kbEntry?.directories ?? []; - const kbPath = path.join(worktreePath, '.features', kbSlug, 'KNOWLEDGE.md'); - let existingContent = ''; - try { - existingContent = await fs.readFile(kbPath, 'utf8'); - } catch { /* new KB */ } + + const sidecarPath = path.join(worktreePath, '.features', kbSlug, '.refresh-result.json'); + try { await fs.unlink(sidecarPath); } catch { /* doesn't exist */ } const prompt = [ `You are the Knowledge agent refreshing a stale feature knowledge base.`, @@ -490,21 +506,15 @@ kbCommand `WORKTREE_PATH: ${worktreePath}`, `CHANGED_FILES: ${JSON.stringify(staleInfo.changedFiles)}`, ``, - existingContent ? `EXISTING_KB:\n${existingContent}` : '', - ``, `Instructions:`, - `- Update the stale sections based on CHANGED_FILES`, + `- Read .features/${kbSlug}/KNOWLEDGE.md to see the existing KB content`, + `- Read the CHANGED_FILES to understand what changed`, + `- Update the stale sections based on changes`, `- Preserve any manually added content`, `- Do not regenerate from scratch`, `- Write the updated KB to .features/${kbSlug}/KNOWLEDGE.md`, - `- After writing, run update-index with updated metadata:`, - ` node scripts/hooks/lib/feature-kb.cjs update-index "${worktreePath}" \\`, - ` --slug="${kbSlug}" --name="${featureName}" \\`, - ` --directories='${JSON.stringify(kbDirectories)}' \\`, - ` --referencedFiles='[]' \\`, - ` --category="${kbEntry?.category ?? 'component-patterns'}" \\`, - ` --createdBy="devflow-kb"`, - ].filter(Boolean).join('\n'); + `- Write .features/${kbSlug}/.refresh-result.json with: {"referencedFiles": [<5-10 key files from explored directories for staleness tracking>]}`, + ].join('\n'); try { execFileSync('claude', [ @@ -516,8 +526,26 @@ kbCommand stdio: 'pipe', encoding: 'utf8', }); + + let sidecar: { referencedFiles?: string[] } = {}; + try { + sidecar = JSON.parse(await fs.readFile(sidecarPath, 'utf8')); + } catch { /* agent didn't write sidecar */ } + + featureKb.updateIndex(worktreePath, { + slug: kbSlug, + name: featureName, + directories: kbDirectories, + referencedFiles: sidecar.referencedFiles ?? (kbEntry as Record)?.referencedFiles as string[] ?? [], + category: kbEntry?.category ?? 'component-patterns', + createdBy: 'devflow-kb', + }); + + try { await fs.unlink(sidecarPath); } catch { /* already cleaned */ } + s.stop(`${kbSlug} refreshed`); } catch (err) { + try { await fs.unlink(sidecarPath); } catch { /* cleanup */ } s.stop(`${kbSlug} refresh failed`); p.log.error(`Error: ${err instanceof Error ? err.message : String(err)}`); } From b4fcd6e23fbfc8cc8d306a2a87e011f2216432b8 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Mon, 27 Apr 2026 11:38:35 +0300 Subject: [PATCH 35/44] test(kb): replace try/catch+boolean with toThrow, remove duplicates Replace 4 instances of let-threw-false pattern with expect().toThrow(expect.objectContaining({ status: 1 })). Remove 5 duplicate stale-slugs/refresh-context tests from kb-command.test.ts (kept in feature-kb.test.ts with stronger assertions). --- tests/feature-kb/feature-kb.test.ts | 48 +++++++++-------------------- tests/feature-kb/kb-command.test.ts | 39 ----------------------- 2 files changed, 14 insertions(+), 73 deletions(-) diff --git a/tests/feature-kb/feature-kb.test.ts b/tests/feature-kb/feature-kb.test.ts index dba0c17..cf49dbd 100644 --- a/tests/feature-kb/feature-kb.test.ts +++ b/tests/feature-kb/feature-kb.test.ts @@ -506,14 +506,9 @@ describe('CLI stale-slugs', () => { }); it('exits non-zero and prints usage when worktree argument is missing', () => { - let threw = false; - try { - execFileSync('node', [FEATURE_KB_CJS, 'stale-slugs'], { encoding: 'utf8', stdio: 'pipe' }); - } catch (e: unknown) { - threw = true; - expect((e as NodeJS.ErrnoException & { status?: number }).status).toBe(1); - } - expect(threw).toBe(true); + expect(() => + execFileSync('node', [FEATURE_KB_CJS, 'stale-slugs'], { encoding: 'utf8', stdio: 'pipe' }) + ).toThrow(expect.objectContaining({ status: 1 })); }); }); @@ -528,44 +523,29 @@ describe('CLI refresh-context', () => { const parts = output.trim().split('\t'); expect(parts).toHaveLength(4); expect(parts[0]).toBe('CLI Command System'); // name - expect(JSON.parse(parts[1])).toBeInstanceOf(Array); // directories JSON + expect(JSON.parse(parts[1])).toEqual(['src/cli/commands/', 'src/cli/utils/']); // directories JSON expect(parts[2]).toBe('component-patterns'); // category - expect(JSON.parse(parts[3])).toBeInstanceOf(Array); // changed files JSON + expect(() => JSON.parse(parts[3])).not.toThrow(); // changed files JSON }); it('exits non-zero when slug is missing', () => { const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); - let threw = false; - try { - execFileSync('node', [FEATURE_KB_CJS, 'refresh-context', tmp], { encoding: 'utf8', stdio: 'pipe' }); - } catch (e: unknown) { - threw = true; - expect((e as NodeJS.ErrnoException & { status?: number }).status).toBe(1); - } - expect(threw).toBe(true); + expect(() => + execFileSync('node', [FEATURE_KB_CJS, 'refresh-context', tmp], { encoding: 'utf8', stdio: 'pipe' }) + ).toThrow(expect.objectContaining({ status: 1 })); }); it('exits non-zero when slug is not found in index', () => { const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); - let threw = false; - try { - execFileSync('node', [FEATURE_KB_CJS, 'refresh-context', tmp, 'nonexistent'], { encoding: 'utf8', stdio: 'pipe' }); - } catch (e: unknown) { - threw = true; - expect((e as NodeJS.ErrnoException & { status?: number }).status).toBe(1); - } - expect(threw).toBe(true); + expect(() => + execFileSync('node', [FEATURE_KB_CJS, 'refresh-context', tmp, 'nonexistent'], { encoding: 'utf8', stdio: 'pipe' }) + ).toThrow(expect.objectContaining({ status: 1 })); }); it('exits non-zero for invalid slug (path traversal)', () => { const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); - let threw = false; - try { - execFileSync('node', [FEATURE_KB_CJS, 'refresh-context', tmp, '../etc'], { encoding: 'utf8', stdio: 'pipe' }); - } catch (e: unknown) { - threw = true; - expect((e as NodeJS.ErrnoException & { status?: number }).status).toBe(1); - } - expect(threw).toBe(true); + expect(() => + execFileSync('node', [FEATURE_KB_CJS, 'refresh-context', tmp, '../etc'], { encoding: 'utf8', stdio: 'pipe' }) + ).toThrow(expect.objectContaining({ status: 1 })); }); }); diff --git a/tests/feature-kb/kb-command.test.ts b/tests/feature-kb/kb-command.test.ts index 231cfe3..b33d207 100644 --- a/tests/feature-kb/kb-command.test.ts +++ b/tests/feature-kb/kb-command.test.ts @@ -79,43 +79,4 @@ describe('feature-kb.cjs CLI', () => { expect(JSON.parse(result)).toEqual([]); }); - it('stale-slugs outputs only stale slugs one per line', () => { - // Non-git worktree — checkAllStaleness returns stale:false for non-git paths - const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); - const result = execSync(`node ${CJS_PATH} stale-slugs ${tmp}`, { encoding: 'utf8' }); - // Non-git worktree => no stale slugs - expect(result.trim()).toBe(''); - }); - - it('stale-slugs outputs nothing for empty index', () => { - const tmp = makeTmpFeatureWorktree({ version: 1, features: {} }); - const result = execSync(`node ${CJS_PATH} stale-slugs ${tmp}`, { encoding: 'utf8' }); - expect(result.trim()).toBe(''); - }); - - it('refresh-context outputs tab-separated metadata', () => { - const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); - const result = execSync(`node ${CJS_PATH} refresh-context ${tmp} cli-commands`, { encoding: 'utf8' }); - const parts = result.trim().split('\t'); - expect(parts).toHaveLength(4); - expect(parts[0]).toBe('CLI Command System'); // name - expect(JSON.parse(parts[1])).toEqual(['src/cli/commands/', 'src/cli/utils/']); // directories JSON - expect(parts[2]).toBe('component-patterns'); // category - // changedFiles is a JSON array (could be empty for non-git) - expect(() => JSON.parse(parts[3])).not.toThrow(); - }); - - it('refresh-context exits 1 for missing slug argument', () => { - const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); - expect(() => - execSync(`node ${CJS_PATH} refresh-context ${tmp}`, { encoding: 'utf8', stdio: 'pipe' }) - ).toThrow(); - }); - - it('refresh-context exits 1 for unknown slug', () => { - const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); - expect(() => - execSync(`node ${CJS_PATH} refresh-context ${tmp} nonexistent`, { encoding: 'utf8', stdio: 'pipe' }) - ).toThrow(); - }); }); From 2cc137a951a0f5d10f9a5555c2d257750f9dc31a Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Mon, 27 Apr 2026 11:38:43 +0300 Subject: [PATCH 36/44] test(hooks): add session-end-kb-refresh behavioral tests 6 tests covering all guard clauses: DEVFLOW_BG_KB_REFRESH recursion, DEVFLOW_BG_UPDATER background, missing index.json, .disabled sentinel, throttle marker, and no-stale-KBs (non-git worktree). --- tests/shell-hooks.test.ts | 78 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/tests/shell-hooks.test.ts b/tests/shell-hooks.test.ts index 7c993ae..75f8281 100644 --- a/tests/shell-hooks.test.ts +++ b/tests/shell-hooks.test.ts @@ -1508,3 +1508,81 @@ describe('get-mtime behavioral', () => { } }); }); + +describe('session-end-kb-refresh guard clauses', () => { + const KB_HOOK = path.join(HOOKS_DIR, 'session-end-kb-refresh'); + + let tmpDir: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'devflow-kb-hook-test-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('exits cleanly when DEVFLOW_BG_KB_REFRESH=1', () => { + expect(() => { + execSync(`DEVFLOW_BG_KB_REFRESH=1 bash "${KB_HOOK}"`, { stdio: 'ignore' }); + }).not.toThrow(); + }); + + it('exits cleanly when DEVFLOW_BG_UPDATER=1', () => { + expect(() => { + execSync(`DEVFLOW_BG_UPDATER=1 bash "${KB_HOOK}"`, { stdio: 'ignore' }); + }).not.toThrow(); + }); + + it('exits cleanly when no .features/index.json exists', () => { + const input = JSON.stringify({ cwd: tmpDir, session_id: 'test-kb-001' }); + expect(() => { + execSync(`bash "${KB_HOOK}"`, { input, stdio: ['pipe', 'pipe', 'pipe'] }); + }).not.toThrow(); + }); + + it('exits cleanly when .features/.disabled sentinel exists', () => { + const featuresDir = path.join(tmpDir, '.features'); + fs.mkdirSync(featuresDir, { recursive: true }); + fs.writeFileSync(path.join(featuresDir, 'index.json'), JSON.stringify({ version: 1, features: {} })); + fs.writeFileSync(path.join(featuresDir, '.disabled'), ''); + + const input = JSON.stringify({ cwd: tmpDir, session_id: 'test-kb-002' }); + expect(() => { + execSync(`bash "${KB_HOOK}"`, { input, stdio: ['pipe', 'pipe', 'pipe'] }); + }).not.toThrow(); + }); + + it('exits cleanly when .kb-last-refresh is recent (throttled)', () => { + const featuresDir = path.join(tmpDir, '.features'); + fs.mkdirSync(featuresDir, { recursive: true }); + fs.writeFileSync(path.join(featuresDir, 'index.json'), JSON.stringify({ version: 1, features: {} })); + fs.writeFileSync(path.join(featuresDir, '.kb-last-refresh'), String(Math.floor(Date.now() / 1000))); + + const input = JSON.stringify({ cwd: tmpDir, session_id: 'test-kb-003' }); + expect(() => { + execSync(`bash "${KB_HOOK}"`, { input, stdio: ['pipe', 'pipe', 'pipe'] }); + }).not.toThrow(); + }); + + it('exits cleanly when no stale KBs are found', () => { + // Non-git tmpDir → checkAllStaleness returns stale:false + const featuresDir = path.join(tmpDir, '.features'); + fs.mkdirSync(featuresDir, { recursive: true }); + fs.writeFileSync(path.join(featuresDir, 'index.json'), JSON.stringify({ + version: 1, + features: { + 'test-feature': { + name: 'Test', description: '', directories: ['src/'], + referencedFiles: ['src/index.ts'], category: 'test', + lastUpdated: new Date().toISOString(), createdBy: 'test', + }, + }, + })); + + const input = JSON.stringify({ cwd: tmpDir, session_id: 'test-kb-004' }); + expect(() => { + execSync(`bash "${KB_HOOK}"`, { input, stdio: ['pipe', 'pipe', 'pipe'] }); + }).not.toThrow(); + }); +}); From cfd6dd4aa556e754739e90bfa1b3883f191e910a Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Mon, 27 Apr 2026 11:38:54 +0300 Subject: [PATCH 37/44] docs: add KNOWLEDGE_CONTEXT to plan:orch GUIDED, update file-organization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add knowledge-context.cjs index invocation to plan:orch GUIDED step 2 so GUIDED plans have the same knowledge context as ORCHESTRATED. Update file-organization.md: skills count 41→44, add KB hooks and lib/ directory to source tree, add KB hook rows to hooks table, mention KB SessionEnd hook in settings override. --- docs/reference/file-organization.md | 21 ++++++++++++++++----- shared/skills/plan:orch/SKILL.md | 4 ++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/docs/reference/file-organization.md b/docs/reference/file-organization.md index 1c26687..bd6637d 100644 --- a/docs/reference/file-organization.md +++ b/docs/reference/file-organization.md @@ -9,7 +9,7 @@ devflow/ ├── .claude-plugin/ # Marketplace registry (repo root) │ └── marketplace.json ├── shared/ -│ ├── skills/ # SINGLE SOURCE OF TRUTH (41 skills) +│ ├── skills/ # SINGLE SOURCE OF TRUTH (44 skills) │ │ ├── git/ │ │ │ ├── SKILL.md │ │ │ └── references/ @@ -42,7 +42,7 @@ devflow/ │ ├── build-hud.js # Copies dist/hud/ → scripts/hud/ │ ├── hud.sh # Thin wrapper: exec node hud/index.js │ ├── hud/ # GENERATED — compiled HUD module (gitignored) -│ └── hooks/ # Working Memory + ambient + learning hooks +│ └── hooks/ # Working Memory + ambient + learning + KB hooks │ ├── stop-update-memory # Stop hook: writes WORKING-MEMORY.md │ ├── session-start-memory # SessionStart hook: injects memory + git state │ ├── pre-compact-memory # PreCompact hook: saves git state backup @@ -52,9 +52,16 @@ devflow/ │ ├── session-end-learning # SessionEnd hook: batched learning trigger │ ├── stop-update-learning # Stop hook: deprecated stub (upgrade via devflow learn) │ ├── background-learning # Background: pattern detection via Sonnet +│ ├── session-end-kb-refresh # SessionEnd hook: stale KB detection + background spawn +│ ├── background-kb-refresh # Background: KB refresher via Sonnet │ ├── get-mtime # Shared helper: portable mtime (BSD/GNU stat) │ ├── json-helper.cjs # Node.js jq-equivalent operations -│ └── json-parse # Shell wrapper: jq with node fallback +│ ├── json-parse # Shell wrapper: jq with node fallback +│ └── lib/ # Node.js helper modules +│ ├── feature-kb.cjs # Feature KB index operations (CRUD, staleness) +│ ├── knowledge-context.cjs # Knowledge context index builder +│ ├── staleness.cjs # Code reference staleness checker +│ └── transcript-filter.cjs # Transcript channel extractor └── src/ └── cli/ ├── commands/ @@ -147,7 +154,7 @@ Skills and agents are **not duplicated** in git. Instead: Included settings: - `statusLine` - Configurable HUD with presets (replaces legacy statusline.sh) -- `hooks` - Working Memory hooks (UserPromptSubmit, Stop, SessionStart, PreCompact) + Learning Stop hook +- `hooks` - Working Memory hooks (UserPromptSubmit, Stop, SessionStart, PreCompact) + Learning SessionEnd hook + KB SessionEnd hook - `env.ENABLE_TOOL_SEARCH` - Deferred MCP tool loading (~85% token savings) - `env.ENABLE_LSP_TOOL` - Language Server Protocol support - `env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` - Agent Teams for peer-to-peer collaboration @@ -158,7 +165,9 @@ Included settings: Four hooks in `scripts/hooks/` provide automatic session continuity. Toggleable via `devflow memory --enable/--disable/--status` or `devflow init --memory/--no-memory`. -A fifth hook (`session-end-learning`) provides self-learning. Toggleable via `devflow learn --enable/--disable/--status` or `devflow init --learn/--no-learn`: +A fifth hook (`session-end-kb-refresh`) provides feature KB maintenance. Toggleable via `devflow kb --enable/--disable/--status` or `devflow init --kb/--no-kb`. + +A sixth hook (`session-end-learning`) provides self-learning. Toggleable via `devflow learn --enable/--disable/--status` or `devflow init --learn/--no-learn`: | Hook | Event | File | Purpose | |------|-------|------|---------| @@ -167,6 +176,8 @@ A fifth hook (`session-end-learning`) provides self-learning. Toggleable via `de | `background-memory-update` | (background) | `.memory/WORKING-MEMORY.md` | Queue-based updater spawned by stop-update-memory. Reads queued turns + git state, writes WORKING-MEMORY.md via `claude -p --model haiku`. | | `session-start-memory` | SessionStart | reads WORKING-MEMORY.md | Injects previous memory + git state as `additionalContext`. Warns if >1h stale. Injects pre-compact snapshot when compaction occurred mid-session. | | `pre-compact-memory` | PreCompact | `.memory/backup.json` | Saves git state + WORKING-MEMORY.md snapshot. Bootstraps minimal WORKING-MEMORY.md if none exists. | +| `session-end-kb-refresh` | SessionEnd | `.features/index.json` | Checks for stale feature KBs. Throttled (<2h). Spawns background-kb-refresh. | +| `background-kb-refresh` | (background) | `.features/{slug}/KNOWLEDGE.md` | KB refresher. Up to 3 stale KBs via `claude -p --model sonnet`. | **Flow**: User sends prompt → UserPromptSubmit hook (prompt-capture-memory) appends user turn to `.memory/.pending-turns.jsonl`. Session ends → Stop hook appends assistant turn to queue, checks throttle (skips if <2min fresh), spawns background updater → background updater reads queued turns + git state → fresh `claude -p --model haiku` writes WORKING-MEMORY.md. On `/clear` or new session → SessionStart injects memory as `additionalContext` (system context, not user-visible) with staleness warning if >1h old. diff --git a/shared/skills/plan:orch/SKILL.md b/shared/skills/plan:orch/SKILL.md index 08d3dfd..b554afc 100644 --- a/shared/skills/plan:orch/SKILL.md +++ b/shared/skills/plan:orch/SKILL.md @@ -25,9 +25,9 @@ This is a focused variant of the `/plan` command pipeline for ambient ORCHESTRAT For GUIDED depth, the main session performs planning directly: 1. **Discover** — If the planning question is open-ended, ask clarifying questions via AskUserQuestion and present 2-3 approaches with tradeoffs before orienting. Skip if the user's prompt is already specific. If the user says "skip" or "just proceed": skip remaining questions, present inferred scope for confirmation. -2. **Load Feature KBs** — Read `.features/index.json` if it exists. Based on the task, identify relevant KBs, read them, and use as context for direct planning. Set `FEATURE_KNOWLEDGE = (none)` if no KBs exist or none are relevant. +2. **Load Knowledge** — Load `KNOWLEDGE_CONTEXT` via `node scripts/hooks/lib/knowledge-context.cjs index "{worktree}"`. Read `.features/index.json` if it exists; based on the task, identify relevant KBs, read them, and use as context for direct planning. Set `FEATURE_KNOWLEDGE = (none)` if no KBs exist or none are relevant. 3. **Spawn Skimmer** — `Agent(subagent_type="Skimmer")` targeting the area of interest. Use orientation output to ground design decisions in real file structures and patterns. -4. **Design** — Using Skimmer findings + loaded pattern/design skills + `FEATURE_KNOWLEDGE`, design the approach directly in main session. Apply `devflow:design-review` skill inline to check the plan for anti-patterns before presenting. +4. **Design** — Using Skimmer findings + loaded pattern/design skills + `KNOWLEDGE_CONTEXT` + `FEATURE_KNOWLEDGE`, design the approach directly in main session. Apply `devflow:design-review` skill inline to check the plan for anti-patterns before presenting. 5. **Present** — Deliver structured plan using the Output format below. Use AskUserQuestion for ambiguous design choices. ## Worktree Support From 8358e70df1e4879e7820cd4df3d4f87d67525a13 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Mon, 27 Apr 2026 23:31:52 +0300 Subject: [PATCH 38/44] fix(hooks): add source guards to background hooks Guard all source commands in background-kb-refresh, background-learning, and background-memory-update so failures produce a clear diagnostic message instead of silently continuing with undefined functions. Co-Authored-By: Claude --- scripts/hooks/background-kb-refresh | 4 ++-- scripts/hooks/background-learning | 4 ++-- scripts/hooks/background-memory-update | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/hooks/background-kb-refresh b/scripts/hooks/background-kb-refresh index 9feaf08..64dd2cc 100755 --- a/scripts/hooks/background-kb-refresh +++ b/scripts/hooks/background-kb-refresh @@ -10,7 +10,7 @@ CLAUDE_BIN="${2:-claude}" PRE_SLUGS="$3" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -source "$SCRIPT_DIR/log-paths" +source "$SCRIPT_DIR/log-paths" || { echo "background-kb-refresh: failed to source log-paths" >&2; exit 1; } LOG_FILE="$(devflow_log_dir "$CWD")/.kb-refresh.log" LOCK_DIR="$CWD/.features/.kb-refresh.lock" @@ -31,7 +31,7 @@ rotate_log() { # --- Cross-platform mtime --- # Source the get-mtime helper for portable stat usage. # shellcheck source=get-mtime -source "$SCRIPT_DIR/get-mtime" +source "$SCRIPT_DIR/get-mtime" || { echo "background-kb-refresh: failed to source get-mtime" >&2; exit 1; } # --- Cleanup trap --- cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } diff --git a/scripts/hooks/background-learning b/scripts/hooks/background-learning index 573e6ec..cbf3708 100755 --- a/scripts/hooks/background-learning +++ b/scripts/hooks/background-learning @@ -12,9 +12,9 @@ CLAUDE_BIN="${3:-claude}" # Source JSON parsing helpers (jq with node fallback) SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -source "$SCRIPT_DIR/json-parse" +source "$SCRIPT_DIR/json-parse" || { echo "background-learning: failed to source json-parse" >&2; exit 1; } -source "$SCRIPT_DIR/log-paths" +source "$SCRIPT_DIR/log-paths" || { echo "background-learning: failed to source log-paths" >&2; exit 1; } LOG_FILE="$(devflow_log_dir "$CWD")/.learning-update.log" LOCK_DIR="$CWD/.memory/.learning.lock" LEARNING_LOG="$CWD/.memory/learning-log.jsonl" diff --git a/scripts/hooks/background-memory-update b/scripts/hooks/background-memory-update index 37b6f44..35f3bce 100755 --- a/scripts/hooks/background-memory-update +++ b/scripts/hooks/background-memory-update @@ -18,9 +18,9 @@ MEMORY_FILE="$CWD/.memory/WORKING-MEMORY.md" # Source JSON parsing helpers (jq with node fallback) SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -source "$SCRIPT_DIR/json-parse" +source "$SCRIPT_DIR/json-parse" || { echo "background-memory-update: failed to source json-parse" >&2; exit 1; } -source "$SCRIPT_DIR/log-paths" +source "$SCRIPT_DIR/log-paths" || { echo "background-memory-update: failed to source log-paths" >&2; exit 1; } LOG_FILE="$(devflow_log_dir "$CWD")/.working-memory-update.log" LOCK_DIR="$CWD/.memory/.working-memory.lock" @@ -42,7 +42,7 @@ rotate_log() { # --- Stale Lock Recovery --- -source "$SCRIPT_DIR/get-mtime" +source "$SCRIPT_DIR/get-mtime" || { echo "background-memory-update: failed to source get-mtime" >&2; exit 1; } STALE_THRESHOLD=300 # 5 min From f769333a0b4e7bf62b32a3e101f8179bfd455585 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Mon, 27 Apr 2026 23:32:47 +0300 Subject: [PATCH 39/44] fix(kb): remove Bash from knowledge agent, update sidecar docs The Knowledge agent writes KNOWLEDGE.md and a sidecar JSON via Write tool; it does not need Bash to run CLI commands. The host process (CLI or background hook) reads the sidecar and calls feature-kb.cjs update-index itself. Removes the Bash tool and rewrites responsibility 7 to reflect the sidecar handoff pattern. Updates the test to match. Co-Authored-By: Claude --- shared/agents/knowledge.md | 3 +-- tests/feature-kb/knowledge-agent.test.ts | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/shared/agents/knowledge.md b/shared/agents/knowledge.md index c12779e..a0567e9 100644 --- a/shared/agents/knowledge.md +++ b/shared/agents/knowledge.md @@ -11,7 +11,6 @@ tools: - Read - Grep - Glob - - Bash - Write --- @@ -36,7 +35,7 @@ tools: 4. **Cross-reference knowledge**: If KNOWLEDGE_CONTEXT is provided, reference relevant ADR/PF entries in the KB's "Related" section 5. **Handle refresh**: If EXISTING_KB is provided, update stale sections based on CHANGED_FILES while preserving any manually added content (user edits). Don't regenerate from scratch. 6. **Write KNOWLEDGE.md**: Write to `.features/{FEATURE_SLUG}/KNOWLEDGE.md` (create directory if needed) -7. **Update index**: Run `node scripts/hooks/lib/feature-kb.cjs update-index` with all required fields +7. **Write sidecar**: Write sidecar JSON file (`.create-result.json` or `.refresh-result.json`) with `referencedFiles` and `description` so the host process can update the index 8. **Report**: Output what was created/updated ## Output diff --git a/tests/feature-kb/knowledge-agent.test.ts b/tests/feature-kb/knowledge-agent.test.ts index e52176b..1233d1c 100644 --- a/tests/feature-kb/knowledge-agent.test.ts +++ b/tests/feature-kb/knowledge-agent.test.ts @@ -14,7 +14,6 @@ describe('knowledge agent', () => { expect(content).toContain('Read'); expect(content).toContain('Grep'); expect(content).toContain('Glob'); - expect(content).toContain('Bash'); expect(content).toContain('Write'); }); it('documents input contract', () => { From a2a59bdec8bfc802ff7a0451e0ed3c7670aa3072 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Mon, 27 Apr 2026 23:33:12 +0300 Subject: [PATCH 40/44] fix(kb): add read-sidecar to json-helper, fix shell injection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inline node -e block in background-kb-refresh interpolated $SIDECAR directly into a JavaScript string literal — a shell injection vector if the path contained quotes or special characters. Replace with a new json-helper read-sidecar subcommand that receives the path as a CLI argument processed through safePath(). Co-Authored-By: Claude --- scripts/hooks/background-kb-refresh | 7 +------ scripts/hooks/json-helper.cjs | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/scripts/hooks/background-kb-refresh b/scripts/hooks/background-kb-refresh index 64dd2cc..ae3c87e 100755 --- a/scripts/hooks/background-kb-refresh +++ b/scripts/hooks/background-kb-refresh @@ -160,12 +160,7 @@ Instructions: # Read sidecar and update index if [ -f "$SIDECAR" ]; then - REF_FILES=$(node -e " - try { - const d = JSON.parse(require('fs').readFileSync('$SIDECAR','utf8')); - console.log(JSON.stringify(d.referencedFiles || [])); - } catch { console.log('[]'); } - " 2>/dev/null || echo "[]") + REF_FILES=$(node "$SCRIPT_DIR/json-helper.cjs" read-sidecar "$SIDECAR" referencedFiles 2>/dev/null || echo "[]") node "$SCRIPT_DIR/lib/feature-kb.cjs" update-index "$CWD" \ --slug="$SLUG" --name="$NAME" \ --directories="$DIRS" \ diff --git a/scripts/hooks/json-helper.cjs b/scripts/hooks/json-helper.cjs index 448a2d7..30de03f 100755 --- a/scripts/hooks/json-helper.cjs +++ b/scripts/hooks/json-helper.cjs @@ -34,6 +34,7 @@ // reconcile-manifest Session-start reconciler: sync manifest vs FS (D6, D13) // merge-observation Dedup/reinforce with in-place merge (D14) // knowledge-append Append ADR/PF entry to knowledge file +// read-sidecar Read array field from sidecar JSON file (returns [] on any error) 'use strict'; @@ -1809,6 +1810,23 @@ try { break; } + case 'read-sidecar': { + if (!args[0] || !args[1]) { + console.log('[]'); + break; + } + const sidecarFile = safePath(args[0]); + const field = args[1]; + try { + const data = JSON.parse(fs.readFileSync(sidecarFile, 'utf8')); + const value = data[field]; + console.log(Array.isArray(value) ? JSON.stringify(value) : '[]'); + } catch { + console.log('[]'); + } + break; + } + default: process.stderr.write(`json-helper: unknown operation "${op}"\n`); process.exit(1); From 0fb3c8e733a01ddfd64de3d8a3cb9d2316a142b7 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Mon, 27 Apr 2026 23:37:06 +0300 Subject: [PATCH 41/44] refactor(kb): remove category field from KB system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Category added complexity without value — callers passed it as a constant and it was never used for filtering or routing. Remove from: - feature-kb.cjs typedef, updateIndex, refresh-context output (4→3 fields) - background-kb-refresh IFS read and update-index call - kb.ts FeatureKbModule types, list output, create/refresh prompts and calls - feature-kb SKILL.md frontmatter template and Category Templates section - All test fixtures and assertions Co-Authored-By: Claude --- scripts/hooks/background-kb-refresh | 5 ++--- scripts/hooks/lib/feature-kb.cjs | 15 +++++---------- shared/skills/feature-kb/SKILL.md | 12 ------------ src/cli/commands/kb.ts | 12 ++++-------- tests/feature-kb/apply-feature-kb-skill.test.ts | 1 - tests/feature-kb/feature-kb.test.ts | 15 ++------------- tests/feature-kb/fixtures.ts | 2 -- tests/feature-kb/kb-command.test.ts | 2 +- tests/shell-hooks.test.ts | 2 +- 9 files changed, 15 insertions(+), 51 deletions(-) diff --git a/scripts/hooks/background-kb-refresh b/scripts/hooks/background-kb-refresh index ae3c87e..1685b87 100755 --- a/scripts/hooks/background-kb-refresh +++ b/scripts/hooks/background-kb-refresh @@ -108,14 +108,14 @@ for SLUG in $STALE_SLUGS; do SIDECAR="$CWD/.features/$SLUG/.refresh-result.json" rm -f "$SIDECAR" - # Get structured metadata (tab-separated: name, dirs, category, changed) + # Get structured metadata (tab-separated: name, dirs, changed) CONTEXT_LINE=$(node "$SCRIPT_DIR/lib/feature-kb.cjs" refresh-context "$CWD" "$SLUG" 2>/dev/null || true) if [ -z "$CONTEXT_LINE" ]; then log "Failed to get refresh-context for $SLUG — skipping" continue fi - IFS=$'\t' read -r NAME DIRS CATEGORY CHANGED <<< "$CONTEXT_LINE" + IFS=$'\t' read -r NAME DIRS CHANGED <<< "$CONTEXT_LINE" PROMPT="You are the Knowledge agent refreshing a stale feature knowledge base. @@ -165,7 +165,6 @@ Instructions: --slug="$SLUG" --name="$NAME" \ --directories="$DIRS" \ --referencedFiles="$REF_FILES" \ - --category="$CATEGORY" \ --createdBy="devflow-kb" >> "$LOG_FILE" 2>&1 || log "Failed to update index for $SLUG" rm -f "$SIDECAR" else diff --git a/scripts/hooks/lib/feature-kb.cjs b/scripts/hooks/lib/feature-kb.cjs index 6e39f17..c09c2b0 100644 --- a/scripts/hooks/lib/feature-kb.cjs +++ b/scripts/hooks/lib/feature-kb.cjs @@ -74,7 +74,6 @@ function validateSlug(slug) { * description: string, * directories: string[], * referencedFiles: string[], - * category: string, * lastUpdated: string, * createdBy: string * }} FeatureEntry @@ -260,7 +259,6 @@ function releaseLock(lockPath) { * description?: string, * directories: string[], * referencedFiles: string[], - * category: string, * createdBy?: string * }} entry * @param {number} [lockTimeoutMs=30000] optional lock timeout for testability @@ -288,7 +286,6 @@ function updateIndex(worktreePath, entry, lockTimeoutMs = 30000) { description: entry.description ?? existing.description ?? '', directories: entry.directories, referencedFiles: entry.referencedFiles, - category: entry.category, lastUpdated: new Date().toISOString(), createdBy: entry.createdBy || existing.createdBy || 'manual', }; @@ -380,7 +377,7 @@ function listKBs(worktreePath) { // Usage: // node feature-kb.cjs list // node feature-kb.cjs stale [slug] -// node feature-kb.cjs update-index --slug=X --name=Y --directories='[...]' --referencedFiles='[...]' --category=X [--description=Y] [--createdBy=Z] +// node feature-kb.cjs update-index --slug=X --name=Y --directories='[...]' --referencedFiles='[...]' [--description=Y] [--createdBy=Z] // node feature-kb.cjs find-overlapping [file2...] // node feature-kb.cjs remove // node feature-kb.cjs stale-slugs @@ -409,7 +406,7 @@ if (require.main === module) { 'Usage:', ' node feature-kb.cjs list ', ' node feature-kb.cjs stale [slug]', - ' node feature-kb.cjs update-index --slug=X --name=Y --directories=\'[...]\' --referencedFiles=\'[...]\' --category=X [--description=Y] [--createdBy=Z]', + ' node feature-kb.cjs update-index --slug=X --name=Y --directories=\'[...]\' --referencedFiles=\'[...]\' [--description=Y] [--createdBy=Z]', ' node feature-kb.cjs find-overlapping [file2...]', ' node feature-kb.cjs remove ', ' node feature-kb.cjs stale-slugs ', @@ -463,8 +460,8 @@ if (require.main === module) { 'update-index'() { const worktreePath = requireWorktree(argv); const kv = parseKeyValue(argv.slice(2)); - if (!kv.slug || !kv.name || !kv.directories || !kv.referencedFiles || !kv.category) { - process.stderr.write('Error: missing required flags (slug, name, directories, referencedFiles, category)\n' + USAGE + '\n'); + if (!kv.slug || !kv.name || !kv.directories || !kv.referencedFiles) { + process.stderr.write('Error: missing required flags (slug, name, directories, referencedFiles)\n' + USAGE + '\n'); process.exit(1); } let directories; @@ -482,7 +479,6 @@ if (require.main === module) { description: kv.description, directories, referencedFiles, - category: kv.category, createdBy: kv.createdBy, }); process.stderr.write(`[feature-kb] mode=update-index worktree=${worktreePath} slug=${kv.slug}\n`); @@ -538,11 +534,10 @@ if (require.main === module) { } const entry = index.features[slug]; const staleness = checkStaleness(worktreePath, slug); - // Tab-separated: name, directories JSON, category, changed files JSON + // Tab-separated: name, directories JSON, changed files JSON process.stdout.write([ entry.name, JSON.stringify(entry.directories), - entry.category, JSON.stringify(staleness.changedFiles), ].join('\t') + '\n'); process.exit(0); diff --git a/shared/skills/feature-kb/SKILL.md b/shared/skills/feature-kb/SKILL.md index e2b3c33..3f194a8 100644 --- a/shared/skills/feature-kb/SKILL.md +++ b/shared/skills/feature-kb/SKILL.md @@ -56,7 +56,6 @@ Write the KNOWLEDGE.md file with this structure: --- feature: {slug} name: {human-readable name} -category: {architecture|conventions|component-patterns|domain-knowledge|lessons-learned} directories: [{dir prefixes}] referencedFiles: [{key files for staleness tracking}] created: {ISO date} @@ -95,16 +94,6 @@ updated: {ISO date} --- -## Category Templates - -| Category | Focus | Example | -|----------|-------|---------| -| `architecture` | Module boundaries, dependency graph | "Payment Processing Architecture" | -| `conventions` | Naming, file org, API style | "CLI Command Conventions" | -| `component-patterns` | Reusable structures, composition | "React Form Patterns" | -| `domain-knowledge` | Business rules, invariants | "Billing Domain Rules" | -| `lessons-learned` | Post-incident, migration lessons | "Auth Migration Lessons" | - ## Quality Self-Checks | Red Flag | Fix | @@ -124,7 +113,6 @@ node scripts/hooks/lib/feature-kb.cjs update-index "{worktree}" \ --slug="{slug}" --name="{name}" \ --directories='["{dir1}", "{dir2}"]' \ --referencedFiles='["{file1}", "{file2}"]' \ - --category="{category}" \ --description="Use when {trigger description}" \ --createdBy="{source}" ``` diff --git a/src/cli/commands/kb.ts b/src/cli/commands/kb.ts index e81b78b..4ec4d0b 100644 --- a/src/cli/commands/kb.ts +++ b/src/cli/commands/kb.ts @@ -19,13 +19,13 @@ const __dirname = path.dirname(__filename); const _require = createRequire(import.meta.url); interface FeatureKbModule { - listKBs: (worktreePath: string) => Array<{ slug: string; name: string; category: string; directories: string[]; lastUpdated: string }>; + listKBs: (worktreePath: string) => Array<{ slug: string; name: string; directories: string[]; lastUpdated: string; referencedFiles?: string[] }>; checkAllStaleness: (worktreePath: string) => Record; checkStaleness: (worktreePath: string, slug: string) => { stale: boolean; changedFiles: string[] }; findOverlapping: (worktreePath: string, changedFiles: string[]) => string[]; removeEntry: (worktreePath: string, slug: string) => void; validateSlug: (slug: string) => void; - updateIndex: (worktreePath: string, entry: { slug: string; name: string; description?: string; directories: string[]; referencedFiles: string[]; category: string; createdBy?: string }, lockTimeoutMs?: number) => void; + updateIndex: (worktreePath: string, entry: { slug: string; name: string; description?: string; directories: string[]; referencedFiles: string[]; createdBy?: string }, lockTimeoutMs?: number) => void; } // dist/cli/commands/kb.js → ../../.. → project root (where scripts/ lives) @@ -280,7 +280,6 @@ kbCommand console.log(` ${color.bold(kb.name)} ${statusBadge}`); console.log(` slug: ${color.dim(kb.slug)}`); - console.log(` category: ${color.dim(kb.category)}`); console.log(` updated: ${color.dim(kb.lastUpdated)}`); console.log(` dirs: ${color.dim(kb.directories.join(', '))}`); if (isStale && staleInfo.changedFiles.length > 0) { @@ -398,7 +397,6 @@ kbCommand `After writing KNOWLEDGE.md, write .features/${slug}/.create-result.json with:`, `{`, ` "referencedFiles": [<5-10 key files from the explored directories for staleness tracking>],`, - ` "category": "",`, ` "description": ""`, `}`, ``, @@ -416,7 +414,7 @@ kbCommand encoding: 'utf8', }); - let sidecar: { referencedFiles?: string[]; category?: string; description?: string } = {}; + let sidecar: { referencedFiles?: string[]; description?: string } = {}; try { sidecar = JSON.parse(await fs.readFile(sidecarPath, 'utf8')); } catch { /* agent didn't write sidecar */ } @@ -426,7 +424,6 @@ kbCommand name: name as string, directories, referencedFiles: sidecar.referencedFiles ?? [], - category: sidecar.category ?? 'component-patterns', description: sidecar.description, createdBy: 'devflow-kb', }); @@ -536,8 +533,7 @@ kbCommand slug: kbSlug, name: featureName, directories: kbDirectories, - referencedFiles: sidecar.referencedFiles ?? (kbEntry as Record)?.referencedFiles as string[] ?? [], - category: kbEntry?.category ?? 'component-patterns', + referencedFiles: sidecar.referencedFiles ?? kbEntry?.referencedFiles ?? [], createdBy: 'devflow-kb', }); diff --git a/tests/feature-kb/apply-feature-kb-skill.test.ts b/tests/feature-kb/apply-feature-kb-skill.test.ts index 494a791..8bc2f27 100644 --- a/tests/feature-kb/apply-feature-kb-skill.test.ts +++ b/tests/feature-kb/apply-feature-kb-skill.test.ts @@ -14,7 +14,6 @@ describe('feature-kb skill', () => { expect(content).toContain('### Phase 3: Distill'); expect(content).toContain('### Phase 4: Forge'); }); - it('has category templates', () => { expect(content).toContain('## Category Templates'); }); it('has quality self-checks', () => { expect(content).toContain('## Quality Self-Checks'); }); it('has KB format template with required sections', () => { expect(content).toContain('## Overview'); diff --git a/tests/feature-kb/feature-kb.test.ts b/tests/feature-kb/feature-kb.test.ts index cf49dbd..e0d704e 100644 --- a/tests/feature-kb/feature-kb.test.ts +++ b/tests/feature-kb/feature-kb.test.ts @@ -139,7 +139,6 @@ describe('checkStaleness (positive — git repo)', () => { description: '', directories: ['src/cli/'], referencedFiles: ['src/cli/cli.ts'], - category: 'test', lastUpdated, createdBy: 'test', }, @@ -170,7 +169,6 @@ describe('updateIndex', () => { name: 'Payment Processing', directories: ['src/payments/'], referencedFiles: ['src/payments/checkout.ts'], - category: 'component-patterns', createdBy: 'test', }); const index = loadIndex(tmp); @@ -178,7 +176,6 @@ describe('updateIndex', () => { expect(index!.features['payments']).toBeDefined(); const entry = index!.features['payments'] as Record; expect(entry.name).toBe('Payment Processing'); - expect(entry.category).toBe('component-patterns'); }); it('upserts an existing entry, preserving createdBy', () => { @@ -188,13 +185,11 @@ describe('updateIndex', () => { name: 'CLI Command System Updated', directories: ['src/cli/'], referencedFiles: ['src/cli/cli.ts'], - category: 'conventions', }); const index = loadIndex(tmp); expect(index).not.toBeNull(); const entry = index!.features['cli-commands'] as Record; expect(entry.name).toBe('CLI Command System Updated'); - expect(entry.category).toBe('conventions'); // createdBy should be preserved from original expect(entry.createdBy).toBe('plan:orch'); }); @@ -207,7 +202,6 @@ describe('updateIndex', () => { name: 'Test', directories: [], referencedFiles: [], - category: 'architecture', }); const after = new Date().toISOString(); const index = loadIndex(tmp); @@ -230,7 +224,6 @@ describe('updateIndex', () => { name: 'Test', directories: [], referencedFiles: [], - category: 'test', }, 200)).toThrow(/lock/i); // Lock dir should still exist (not cleaned up by our failed attempt) @@ -251,7 +244,6 @@ describe('updateIndex', () => { name: 'New Feature', directories: ['src/new/'], referencedFiles: ['src/new/index.ts'], - category: 'component-patterns', }); expect(existsSync(path.join(tmp, '.features'))).toBe(true); @@ -351,7 +343,6 @@ describe('findOverlapping', () => { description: '', directories: ['src/cli/'], referencedFiles: ['src/cli'], - category: 'test', lastUpdated: new Date().toISOString(), createdBy: 'test', }, @@ -489,7 +480,6 @@ describe('CLI stale-slugs', () => { description: '', directories: ['src/cli/'], referencedFiles: ['src/cli/cli.ts'], - category: 'test', lastUpdated, createdBy: 'test', }, @@ -521,11 +511,10 @@ describe('CLI refresh-context', () => { const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); const output = execFileSync('node', [FEATURE_KB_CJS, 'refresh-context', tmp, 'cli-commands'], { encoding: 'utf8' }); const parts = output.trim().split('\t'); - expect(parts).toHaveLength(4); + expect(parts).toHaveLength(3); expect(parts[0]).toBe('CLI Command System'); // name expect(JSON.parse(parts[1])).toEqual(['src/cli/commands/', 'src/cli/utils/']); // directories JSON - expect(parts[2]).toBe('component-patterns'); // category - expect(() => JSON.parse(parts[3])).not.toThrow(); // changed files JSON + expect(() => JSON.parse(parts[2])).not.toThrow(); // changed files JSON }); it('exits non-zero when slug is missing', () => { diff --git a/tests/feature-kb/fixtures.ts b/tests/feature-kb/fixtures.ts index c7c5be8..576075c 100644 --- a/tests/feature-kb/fixtures.ts +++ b/tests/feature-kb/fixtures.ts @@ -12,7 +12,6 @@ export const SAMPLE_INDEX = { description: 'Use when adding CLI subcommands, modifying plugin registration, or changing the init flow.', directories: ['src/cli/commands/', 'src/cli/utils/'], referencedFiles: ['src/cli/cli.ts', 'src/cli/plugins.ts'], - category: 'component-patterns', lastUpdated: '2026-04-20T14:30:00Z', createdBy: 'plan:orch', }, @@ -22,7 +21,6 @@ export const SAMPLE_INDEX = { export const SAMPLE_KB_CONTENT = `--- feature: cli-commands name: CLI Command System -category: component-patterns directories: - src/cli/commands/ - src/cli/utils/ diff --git a/tests/feature-kb/kb-command.test.ts b/tests/feature-kb/kb-command.test.ts index b33d207..bffa501 100644 --- a/tests/feature-kb/kb-command.test.ts +++ b/tests/feature-kb/kb-command.test.ts @@ -42,7 +42,7 @@ describe('feature-kb.cjs CLI', () => { it('update-index creates entry', () => { const tmp = makeTmpFeatureWorktree({ version: 1, features: {} }); execSync( - `node ${CJS_PATH} update-index ${tmp} --slug=payments --name="Payment Processing" --directories='["src/payments/"]' --referencedFiles='["src/payments/checkout.ts"]' --category=component-patterns`, + `node ${CJS_PATH} update-index ${tmp} --slug=payments --name="Payment Processing" --directories='["src/payments/"]' --referencedFiles='["src/payments/checkout.ts"]'`, { encoding: 'utf8' } ); const index = JSON.parse(readFileSync(path.join(tmp, '.features', 'index.json'), 'utf8')); diff --git a/tests/shell-hooks.test.ts b/tests/shell-hooks.test.ts index 75f8281..5040825 100644 --- a/tests/shell-hooks.test.ts +++ b/tests/shell-hooks.test.ts @@ -1574,7 +1574,7 @@ describe('session-end-kb-refresh guard clauses', () => { features: { 'test-feature': { name: 'Test', description: '', directories: ['src/'], - referencedFiles: ['src/index.ts'], category: 'test', + referencedFiles: ['src/index.ts'], lastUpdated: new Date().toISOString(), createdBy: 'test', }, }, From dfd93a82d8f73b22d43b8315a84206147e08ce6e Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Mon, 27 Apr 2026 23:38:08 +0300 Subject: [PATCH 42/44] refactor(kb): extract readSidecar helper, fix types, reuse staleness, pin model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract and export readSidecar() with validated JSON parsing — filters referencedFiles to string-only elements, handles all error cases with {} - Expand listKBs return type with optional description/createdBy/referencedFiles so refresh path can fall back to existing index values without type cast - Remove double cast in refresh referencedFiles fallback - Reuse staleness map from checkAllStaleness in batch refresh loop instead of calling checkStaleness per-slug (avoids redundant git log calls) - Pin --model sonnet in kb create for consistent agent behavior Co-Authored-By: Claude --- src/cli/commands/kb.ts | 46 +++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/src/cli/commands/kb.ts b/src/cli/commands/kb.ts index 4ec4d0b..8805404 100644 --- a/src/cli/commands/kb.ts +++ b/src/cli/commands/kb.ts @@ -18,8 +18,34 @@ const __dirname = path.dirname(__filename); /** @internal */ const _require = createRequire(import.meta.url); +export interface SidecarData { + referencedFiles?: string[]; + description?: string; +} + +export async function readSidecar(sidecarPath: string): Promise { + let raw: unknown; + try { + raw = JSON.parse(await fs.readFile(sidecarPath, 'utf8')); + } catch { + return {}; + } + if (typeof raw !== 'object' || raw === null) return {}; + const data = raw as Record; + const result: SidecarData = {}; + if (Array.isArray(data.referencedFiles)) { + result.referencedFiles = data.referencedFiles.filter( + (f): f is string => typeof f === 'string' + ); + } + if (typeof data.description === 'string') { + result.description = data.description; + } + return result; +} + interface FeatureKbModule { - listKBs: (worktreePath: string) => Array<{ slug: string; name: string; directories: string[]; lastUpdated: string; referencedFiles?: string[] }>; + listKBs: (worktreePath: string) => Array<{ slug: string; name: string; directories: string[]; lastUpdated: string; referencedFiles?: string[]; description?: string; createdBy?: string }>; checkAllStaleness: (worktreePath: string) => Record; checkStaleness: (worktreePath: string, slug: string) => { stale: boolean; changedFiles: string[] }; findOverlapping: (worktreePath: string, changedFiles: string[]) => string[]; @@ -406,6 +432,7 @@ kbCommand try { execFileSync('claude', [ '-p', prompt, + '--model', 'sonnet', '--allowedTools', KB_AGENT_TOOLS, '--dangerously-skip-permissions', ], { @@ -414,10 +441,7 @@ kbCommand encoding: 'utf8', }); - let sidecar: { referencedFiles?: string[]; description?: string } = {}; - try { - sidecar = JSON.parse(await fs.readFile(sidecarPath, 'utf8')); - } catch { /* agent didn't write sidecar */ } + const sidecar = await readSidecar(sidecarPath); featureKb.updateIndex(worktreePath, { slug, @@ -463,11 +487,12 @@ kbCommand // Determine which slugs to refresh let slugsToRefresh: string[]; + let stalenessMap: Record | undefined; if (slug) { slugsToRefresh = [slug]; } else { - const staleness = featureKb.checkAllStaleness(worktreePath); - slugsToRefresh = Object.entries(staleness) + stalenessMap = featureKb.checkAllStaleness(worktreePath); + slugsToRefresh = Object.entries(stalenessMap) .filter(([, info]) => info.stale) .map(([s]) => s); } @@ -486,7 +511,7 @@ kbCommand const s = p.spinner(); s.start(`Refreshing ${kbSlug}...`); - const staleInfo = featureKb.checkStaleness(worktreePath, kbSlug); + const staleInfo = stalenessMap?.[kbSlug] ?? featureKb.checkStaleness(worktreePath, kbSlug); const kbEntry = kbs.find((k: { slug: string }) => k.slug === kbSlug); const featureName = kbEntry?.name ?? kbSlug; const kbDirectories = kbEntry?.directories ?? []; @@ -524,10 +549,7 @@ kbCommand encoding: 'utf8', }); - let sidecar: { referencedFiles?: string[] } = {}; - try { - sidecar = JSON.parse(await fs.readFile(sidecarPath, 'utf8')); - } catch { /* agent didn't write sidecar */ } + const sidecar = await readSidecar(sidecarPath); featureKb.updateIndex(worktreePath, { slug: kbSlug, From a2e19de04d3c86491b24d5e7d7e2bf1c296a976a Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Mon, 27 Apr 2026 23:38:58 +0300 Subject: [PATCH 43/44] test(kb): add sidecar and empty-index tests - Empty-index stale-slugs test: verify no output for { features: {} } - json-helper read-sidecar: 5 tests covering valid, missing, malformed, non-array field, and missing args cases - readSidecar TypeScript helper: 5 unit tests covering valid sidecar, missing file, invalid JSON, string-instead-of-array, and mixed-type array filtering Co-Authored-By: Claude --- tests/feature-kb/feature-kb.test.ts | 121 +++++++++++++++++++++++++++- 1 file changed, 120 insertions(+), 1 deletion(-) diff --git a/tests/feature-kb/feature-kb.test.ts b/tests/feature-kb/feature-kb.test.ts index e0d704e..88a299e 100644 --- a/tests/feature-kb/feature-kb.test.ts +++ b/tests/feature-kb/feature-kb.test.ts @@ -1,7 +1,9 @@ -import { describe, it, expect, afterAll } from 'vitest'; +import { describe, it, expect, afterAll, afterEach } from 'vitest'; import * as path from 'path'; +import * as os from 'os'; import { createRequire } from 'module'; import { writeFileSync, mkdirSync, readFileSync, existsSync, rmSync, rmdirSync } from 'fs'; +import { promises as fsPromises } from 'fs'; import { execSync, execFileSync } from 'child_process'; import { SAMPLE_INDEX, @@ -538,3 +540,120 @@ describe('CLI refresh-context', () => { ).toThrow(expect.objectContaining({ status: 1 })); }); }); + +// --------------------------------------------------------------------------- +// CLI stale-slugs: empty index +// --------------------------------------------------------------------------- + +describe('CLI stale-slugs', () => { + it('outputs nothing for empty index', () => { + const tmp = makeTmpFeatureWorktree({ version: 1, features: {} }); + const output = execFileSync('node', [FEATURE_KB_CJS, 'stale-slugs', tmp], { encoding: 'utf8' }); + expect(output.trim()).toBe(''); + }); +}); + +// --------------------------------------------------------------------------- +// json-helper.cjs read-sidecar +// --------------------------------------------------------------------------- + +const JSON_HELPER_CJS = path.join(ROOT, 'scripts/hooks/json-helper.cjs'); + +describe('json-helper read-sidecar', () => { + it('returns parsed JSON array for valid sidecar with array field', () => { + const sidecar = path.join(os.tmpdir(), `test-sidecar-${Date.now()}.json`); + writeFileSync(sidecar, JSON.stringify({ referencedFiles: ['src/a.ts', 'src/b.ts'] })); + try { + const output = execFileSync('node', [JSON_HELPER_CJS, 'read-sidecar', sidecar, 'referencedFiles'], { encoding: 'utf8' }); + expect(JSON.parse(output.trim())).toEqual(['src/a.ts', 'src/b.ts']); + } finally { + try { rmSync(sidecar); } catch { /* best-effort */ } + } + }); + + it('returns [] for missing file', () => { + const output = execFileSync('node', [JSON_HELPER_CJS, 'read-sidecar', '/nonexistent/path/file.json', 'referencedFiles'], { encoding: 'utf8' }); + expect(output.trim()).toBe('[]'); + }); + + it('returns [] for malformed JSON', () => { + const sidecar = path.join(os.tmpdir(), `test-sidecar-bad-${Date.now()}.json`); + writeFileSync(sidecar, 'not-json'); + try { + const output = execFileSync('node', [JSON_HELPER_CJS, 'read-sidecar', sidecar, 'referencedFiles'], { encoding: 'utf8' }); + expect(output.trim()).toBe('[]'); + } finally { + try { rmSync(sidecar); } catch { /* best-effort */ } + } + }); + + it('returns [] when field value is not an array', () => { + const sidecar = path.join(os.tmpdir(), `test-sidecar-noarray-${Date.now()}.json`); + writeFileSync(sidecar, JSON.stringify({ referencedFiles: 'not-an-array' })); + try { + const output = execFileSync('node', [JSON_HELPER_CJS, 'read-sidecar', sidecar, 'referencedFiles'], { encoding: 'utf8' }); + expect(output.trim()).toBe('[]'); + } finally { + try { rmSync(sidecar); } catch { /* best-effort */ } + } + }); + + it('returns [] when args are missing', () => { + const output = execFileSync('node', [JSON_HELPER_CJS, 'read-sidecar'], { encoding: 'utf8' }); + expect(output.trim()).toBe('[]'); + }); +}); + +// --------------------------------------------------------------------------- +// readSidecar helper (TypeScript) +// --------------------------------------------------------------------------- + +import { readSidecar } from '../../src/cli/commands/kb.js'; + +describe('readSidecar', () => { + const tmpFiles: string[] = []; + + afterEach(() => { + for (const f of tmpFiles) { + try { rmSync(f); } catch { /* best-effort */ } + } + tmpFiles.length = 0; + }); + + function writeTmp(content: string): string { + const f = path.join(os.tmpdir(), `test-read-sidecar-${Date.now()}-${Math.random().toString(36).slice(2)}.json`); + writeFileSync(f, content); + tmpFiles.push(f); + return f; + } + + it('returns referencedFiles and description for valid sidecar', async () => { + const f = writeTmp(JSON.stringify({ referencedFiles: ['src/a.ts', 'src/b.ts'], description: 'Use when X' })); + const result = await readSidecar(f); + expect(result.referencedFiles).toEqual(['src/a.ts', 'src/b.ts']); + expect(result.description).toBe('Use when X'); + }); + + it('returns {} for missing file', async () => { + const result = await readSidecar('/nonexistent/path/file.json'); + expect(result).toEqual({}); + }); + + it('returns {} for invalid JSON', async () => { + const f = writeTmp('not-valid-json'); + const result = await readSidecar(f); + expect(result).toEqual({}); + }); + + it('omits referencedFiles when value is a string not array', async () => { + const f = writeTmp(JSON.stringify({ referencedFiles: 'should-be-array' })); + const result = await readSidecar(f); + expect(result.referencedFiles).toBeUndefined(); + }); + + it('filters mixed-type referencedFiles array to strings only', async () => { + const f = writeTmp(JSON.stringify({ referencedFiles: ['src/a.ts', 42, null, 'src/b.ts'] })); + const result = await readSidecar(f); + expect(result.referencedFiles).toEqual(['src/a.ts', 'src/b.ts']); + }); +}); From 287a53219045f708f5ec305351e5da2ce18e6d94 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Mon, 27 Apr 2026 23:46:43 +0300 Subject: [PATCH 44/44] fix: address self-review issues - Pin --model sonnet on kb refresh command (was missing, create had it) - Remove Bash from feature-kb skill allowed-tools (agent no longer has it) - Distribute updated agent to plugin copies - Include Simplifier changes: source shared get-mtime, disambiguate test names --- plugins/devflow-ambient/agents/knowledge.md | 3 +-- plugins/devflow-plan/agents/knowledge.md | 3 +-- scripts/hooks/background-learning | 11 +++-------- shared/skills/feature-kb/SKILL.md | 1 - src/cli/commands/kb.ts | 1 + tests/feature-kb/feature-kb.test.ts | 2 +- 6 files changed, 7 insertions(+), 14 deletions(-) diff --git a/plugins/devflow-ambient/agents/knowledge.md b/plugins/devflow-ambient/agents/knowledge.md index c12779e..a0567e9 100644 --- a/plugins/devflow-ambient/agents/knowledge.md +++ b/plugins/devflow-ambient/agents/knowledge.md @@ -11,7 +11,6 @@ tools: - Read - Grep - Glob - - Bash - Write --- @@ -36,7 +35,7 @@ tools: 4. **Cross-reference knowledge**: If KNOWLEDGE_CONTEXT is provided, reference relevant ADR/PF entries in the KB's "Related" section 5. **Handle refresh**: If EXISTING_KB is provided, update stale sections based on CHANGED_FILES while preserving any manually added content (user edits). Don't regenerate from scratch. 6. **Write KNOWLEDGE.md**: Write to `.features/{FEATURE_SLUG}/KNOWLEDGE.md` (create directory if needed) -7. **Update index**: Run `node scripts/hooks/lib/feature-kb.cjs update-index` with all required fields +7. **Write sidecar**: Write sidecar JSON file (`.create-result.json` or `.refresh-result.json`) with `referencedFiles` and `description` so the host process can update the index 8. **Report**: Output what was created/updated ## Output diff --git a/plugins/devflow-plan/agents/knowledge.md b/plugins/devflow-plan/agents/knowledge.md index c12779e..a0567e9 100644 --- a/plugins/devflow-plan/agents/knowledge.md +++ b/plugins/devflow-plan/agents/knowledge.md @@ -11,7 +11,6 @@ tools: - Read - Grep - Glob - - Bash - Write --- @@ -36,7 +35,7 @@ tools: 4. **Cross-reference knowledge**: If KNOWLEDGE_CONTEXT is provided, reference relevant ADR/PF entries in the KB's "Related" section 5. **Handle refresh**: If EXISTING_KB is provided, update stale sections based on CHANGED_FILES while preserving any manually added content (user edits). Don't regenerate from scratch. 6. **Write KNOWLEDGE.md**: Write to `.features/{FEATURE_SLUG}/KNOWLEDGE.md` (create directory if needed) -7. **Update index**: Run `node scripts/hooks/lib/feature-kb.cjs update-index` with all required fields +7. **Write sidecar**: Write sidecar JSON file (`.create-result.json` or `.refresh-result.json`) with `referencedFiles` and `description` so the host process can update the index 8. **Report**: Output what was created/updated ## Output diff --git a/scripts/hooks/background-learning b/scripts/hooks/background-learning index cbf3708..2d05220 100755 --- a/scripts/hooks/background-learning +++ b/scripts/hooks/background-learning @@ -18,6 +18,9 @@ source "$SCRIPT_DIR/log-paths" || { echo "background-learning: failed to source LOG_FILE="$(devflow_log_dir "$CWD")/.learning-update.log" LOCK_DIR="$CWD/.memory/.learning.lock" LEARNING_LOG="$CWD/.memory/learning-log.jsonl" + +# shellcheck source=get-mtime +source "$SCRIPT_DIR/get-mtime" || { echo "background-learning: failed to source get-mtime" >&2; exit 1; } RESPONSE_FILE="$CWD/.memory/.learning-response.tmp" # --- Logging --- @@ -41,14 +44,6 @@ rotate_log() { # --- Stale Lock Recovery --- -get_mtime() { - if stat --version &>/dev/null 2>&1; then - stat -c %Y "$1" - else - stat -f %m "$1" - fi -} - # DESIGN: These timeouts are intentionally higher than the Node acquireMkdirLock defaults # (30 s / 60 s in json-helper.cjs) because this lock guards the entire Sonnet analysis # pipeline, not just file I/O. The pipeline can legitimately run up to 180 s (TIMEOUT diff --git a/shared/skills/feature-kb/SKILL.md b/shared/skills/feature-kb/SKILL.md index 3f194a8..9233479 100644 --- a/shared/skills/feature-kb/SKILL.md +++ b/shared/skills/feature-kb/SKILL.md @@ -6,7 +6,6 @@ allowed-tools: - Read - Grep - Glob - - Bash - Write --- diff --git a/src/cli/commands/kb.ts b/src/cli/commands/kb.ts index 8805404..eb84231 100644 --- a/src/cli/commands/kb.ts +++ b/src/cli/commands/kb.ts @@ -541,6 +541,7 @@ kbCommand try { execFileSync('claude', [ '-p', prompt, + '--model', 'sonnet', '--allowedTools', KB_AGENT_TOOLS, '--dangerously-skip-permissions', ], { diff --git a/tests/feature-kb/feature-kb.test.ts b/tests/feature-kb/feature-kb.test.ts index 88a299e..07f94fc 100644 --- a/tests/feature-kb/feature-kb.test.ts +++ b/tests/feature-kb/feature-kb.test.ts @@ -545,7 +545,7 @@ describe('CLI refresh-context', () => { // CLI stale-slugs: empty index // --------------------------------------------------------------------------- -describe('CLI stale-slugs', () => { +describe('CLI stale-slugs (empty index)', () => { it('outputs nothing for empty index', () => { const tmp = makeTmpFeatureWorktree({ version: 1, features: {} }); const output = execFileSync('node', [FEATURE_KB_CJS, 'stale-slugs', tmp], { encoding: 'utf8' });