From f44d7821dd19976e82270acc4a18d9da11d6d2a0 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sun, 28 Jun 2026 13:44:29 +0300 Subject: [PATCH 1/5] feat!: add MDS knowledge module and rewire 9 command bodies (Phase 1 of 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WORKSTREAM 3: Add shared/knowledge/_knowledge.mds with knowledge_load() and knowledge_writeback() partials — pure file-I/O, no subprocess/git calls, no .cjs engine. knowledge_load reads index.md cache (falls back to globbing frontmatter); knowledge_writeback conditionally spawns Knowledge agent to write KNOWLEDGE.md + index.md line directly. WORKSTREAM 1: Port all 9 command .md files to shared/knowledge/*.mds host files that import and call the two partials. Asymmetry preserved: explore and debug get writeback-only; the other 7 load up-front; implement/resolve/ self-review additionally write back. All prose braces escaped; code fences use raw braces. DELIVERABLE 3: scripts/build-knowledge.ts — explicit 9-entry source→plugin map (not a glob), per-file clean (never wipes a whole dir), hard-fail on any mds:: error, disjoint from build-recipes.ts. DELIVERABLE 4: package.json wires build:knowledge after build:recipes in the main build chain; .gitignore adds 9 per-file entries for generated commands; git rm --cached untracks the now-generated .md files. Old machinery (feature-knowledge.cjs, index.json, .create-result.json, subprocess calls) remains untouched — this phase is purely additive. Tree builds green; all 1880 tests pass. TASK_ID: feat/simplify-feature-knowledge --- .gitignore | 11 + package.json | 3 +- scripts/build-knowledge.ts | 180 +++++++++++++ shared/knowledge/_knowledge.mds | 93 +++++++ .../knowledge/bug-analysis.mds | 86 +++--- .../knowledge/code-review.mds | 75 +++--- .../debug.md => shared/knowledge/debug.mds | 64 ++--- .../knowledge/explore.mds | 57 +--- .../knowledge/implement.mds | 244 +++++++----------- .../plan.md => shared/knowledge/plan.mds | 59 ++--- .../knowledge/research.mds | 29 +-- .../knowledge/resolve.mds | 107 ++++---- .../knowledge/self-review.mds | 63 ++--- 13 files changed, 625 insertions(+), 446 deletions(-) create mode 100644 scripts/build-knowledge.ts create mode 100644 shared/knowledge/_knowledge.mds rename plugins/devflow-bug-analysis/commands/bug-analysis.md => shared/knowledge/bug-analysis.mds (83%) rename plugins/devflow-code-review/commands/code-review.md => shared/knowledge/code-review.mds (78%) rename plugins/devflow-debug/commands/debug.md => shared/knowledge/debug.mds (75%) rename plugins/devflow-explore/commands/explore.md => shared/knowledge/explore.mds (62%) rename plugins/devflow-implement/commands/implement.md => shared/knowledge/implement.mds (65%) rename plugins/devflow-plan/commands/plan.md => shared/knowledge/plan.mds (89%) rename plugins/devflow-research/commands/research.md => shared/knowledge/research.mds (80%) rename plugins/devflow-resolve/commands/resolve.md => shared/knowledge/resolve.mds (81%) rename plugins/devflow-self-review/commands/self-review.md => shared/knowledge/self-review.mds (76%) diff --git a/.gitignore b/.gitignore index 1fa77596..fce9be9d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,17 @@ dist/ # Generated plugin commands (compiled from shared/recipes/*.mds at build time) plugins/devflow-dynamic/commands/ +# Generated plugin commands (compiled from shared/knowledge/*.mds at build time) +plugins/devflow-implement/commands/implement.md +plugins/devflow-plan/commands/plan.md +plugins/devflow-resolve/commands/resolve.md +plugins/devflow-code-review/commands/code-review.md +plugins/devflow-self-review/commands/self-review.md +plugins/devflow-research/commands/research.md +plugins/devflow-bug-analysis/commands/bug-analysis.md +plugins/devflow-explore/commands/explore.md +plugins/devflow-debug/commands/debug.md + # Generated plugin skills (copied from shared/skills/ at build time) plugins/*/skills/ diff --git a/package.json b/package.json index 88b240fd..e1ab3dac 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,11 @@ "CHANGELOG.md" ], "scripts": { - "build": "npm run build:cli && npm run build:plugins && npm run build:recipes && npm run build:hud", + "build": "npm run build:cli && npm run build:plugins && npm run build:recipes && npm run build:knowledge && npm run build:hud", "build:cli": "tsc", "build:plugins": "npx tsx scripts/build-plugins.ts", "build:recipes": "npx tsx scripts/build-recipes.ts", + "build:knowledge": "npx tsx scripts/build-knowledge.ts", "build:hud": "node scripts/build-hud.js", "dev": "tsc --watch", "cli": "node dist/cli.js", diff --git a/scripts/build-knowledge.ts b/scripts/build-knowledge.ts new file mode 100644 index 00000000..985e5687 --- /dev/null +++ b/scripts/build-knowledge.ts @@ -0,0 +1,180 @@ +#!/usr/bin/env npx tsx +/** + * Build-time knowledge command compilation script + * + * Compiles `.mds` command files from shared/knowledge/ into Markdown command files + * in the appropriate plugin commands/ directories. Uses an explicit source→plugin map + * (not a glob) — a missing source file or unknown/missing plugin destination exits + * non-zero with a clear message. + * + * Partials (basename starts with `_`) are never outputs; they are imported by host files. + * + * Per-file clean: before compiling each file, removes only the mapped destination .md — + * never wipes a whole directory. + * + * Hard-fails the entire build on any compile error, ensuring a broken or stale command + * never ships. Errors are reported with the mds::* code, message, and source span. + * + * Usage: npm run build:knowledge + */ + +import * as fs from "fs"; +import * as path from "path"; +import { fileURLToPath } from "url"; +import { init, compileFile, isMdsError } from "@mdscript/mds"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const KNOWLEDGE_DIR = path.join(ROOT, "shared", "knowledge"); + +/** + * Explicit source→plugin destination map. + * Key: basename of .mds file in shared/knowledge/ (without extension) + * Value: relative path from ROOT to the destination plugin commands/ directory + */ +const SOURCE_TO_PLUGIN_MAP: Record = { + "implement": "plugins/devflow-implement/commands", + "plan": "plugins/devflow-plan/commands", + "resolve": "plugins/devflow-resolve/commands", + "code-review": "plugins/devflow-code-review/commands", + "self-review": "plugins/devflow-self-review/commands", + "research": "plugins/devflow-research/commands", + "bug-analysis": "plugins/devflow-bug-analysis/commands", + "explore": "plugins/devflow-explore/commands", + "debug": "plugins/devflow-debug/commands", +}; + +interface CompileOutcome { + source: string; + dest: string; + warnings: string[]; +} + +function formatMdsError(err: unknown, sourcePath: string): string { + if (isMdsError(err)) { + const span = err.span + ? ` [line ${err.span.line ?? "?"}:${err.span.column ?? "?"}]` + : ""; + const help = err.help ? `\n help: ${err.help}` : ""; + return `${err.code}${span}: ${err.message}${help}\n file: ${path.relative(ROOT, sourcePath)}`; + } + return String(err); +} + +async function compileKnowledgeFile( + sourcePath: string, + destDir: string +): Promise { + const basename = path.basename(sourcePath, ".mds"); + const dest = path.join(destDir, `${basename}.md`); + + // Per-file clean: remove only the mapped destination — never wipe the whole directory + if (fs.existsSync(dest)) { + fs.rmSync(dest); + } + + const result = await compileFile(sourcePath); + fs.writeFileSync(dest, result.output, "utf-8"); + + return { + source: path.relative(ROOT, sourcePath), + dest: path.relative(ROOT, dest), + warnings: result.warnings, + }; +} + +async function main(): Promise { + console.log("Building knowledge commands...\n"); + + // Validate shared/knowledge/ exists + if (!fs.existsSync(KNOWLEDGE_DIR)) { + console.error(`ERROR: shared/knowledge/ directory not found at ${KNOWLEDGE_DIR}`); + process.exit(1); + } + + // Initialize the MDS compiler (required before any compile/check call) + await init(); + + // Validate each source file exists and each destination directory exists + const validationErrors: string[] = []; + + for (const [basename, destRelDir] of Object.entries(SOURCE_TO_PLUGIN_MAP)) { + const sourcePath = path.join(KNOWLEDGE_DIR, `${basename}.mds`); + if (!fs.existsSync(sourcePath)) { + validationErrors.push( + `Missing source file: shared/knowledge/${basename}.mds` + ); + } + + const destDir = path.join(ROOT, destRelDir); + if (!fs.existsSync(destDir)) { + validationErrors.push( + `Missing plugin destination directory: ${destRelDir} (plugin not installed or commands/ dir absent)` + ); + } + } + + if (validationErrors.length > 0) { + for (const err of validationErrors) { + console.error(` ERROR: ${err}`); + } + console.error( + `\n${validationErrors.length} validation error(s) — build FAILED. Ensure all source files exist and plugin directories are present.` + ); + process.exit(1); + } + + const entries = Object.entries(SOURCE_TO_PLUGIN_MAP); + console.log(` ${entries.length} command(s) to compile:\n`); + + // Compile each command — hard-fail on any error + const outcomes: CompileOutcome[] = []; + const errors: string[] = []; + + for (const [basename, destRelDir] of entries) { + const sourcePath = path.join(KNOWLEDGE_DIR, `${basename}.mds`); + const destDir = path.join(ROOT, destRelDir); + + try { + const outcome = await compileKnowledgeFile(sourcePath, destDir); + outcomes.push(outcome); + + const warnNote = + outcome.warnings.length > 0 + ? ` (${outcome.warnings.length} warning(s))` + : ""; + console.log(` compiled: ${outcome.source} → ${outcome.dest}${warnNote}`); + + for (const w of outcome.warnings) { + console.warn(` WARNING: ${w}`); + } + } catch (err) { + const formatted = formatMdsError(err, sourcePath); + errors.push(formatted); + console.error(` FAILED: ${basename}.mds`); + console.error(` ${formatted}`); + } + } + + const totalWarnings = outcomes.reduce((n, o) => n + o.warnings.length, 0); + console.log( + `\nKnowledge: ${outcomes.length} compiled, ${errors.length} error(s), ${totalWarnings} warning(s)` + ); + + if (errors.length > 0) { + console.error( + `\n${errors.length} compile error(s) — build FAILED. Fix the mds::* errors above before shipping.` + ); + process.exit(1); + } + + console.log("\nKnowledge commands build complete!"); +} + +main().catch((err) => { + // Hard-fail on any error escaping main() (e.g. init() or readdir failure) — + // a broken or stale command must never ship. + console.error( + `\nFATAL: knowledge build aborted — ${err instanceof Error ? err.message : String(err)}` + ); + process.exit(1); +}); diff --git a/shared/knowledge/_knowledge.mds b/shared/knowledge/_knowledge.mds new file mode 100644 index 00000000..94622b50 --- /dev/null +++ b/shared/knowledge/_knowledge.mds @@ -0,0 +1,93 @@ +@define knowledge_load(): +### Load Feature Knowledge + +Resolve the worktree root using the `devflow:worktree-support` algorithm (use WORKTREE_PATH if provided, otherwise cwd). All paths below are relative to `\{worktree\}`. + +**Step 1 — Read the index cache:** + +Attempt to read `\{worktree\}/.devflow/features/index.md`. Each line follows the format: + +``` +- **\{slug\}** — \{areas\} — \{Use-when description\} +``` + +If `index.md` exists and contains at least one entry line, use it for relevance matching. + +**Step 2 — Fallback: glob frontmatter (if `index.md` is absent or empty):** + +Glob `\{worktree\}/.devflow/features/*/KNOWLEDGE.md`. For each file found, read only its YAML frontmatter block (between the opening and closing `---` delimiters). The frontmatter fields `name`, `description`, and `directories` are the authoritative relevance surface — `index.md` is only a cache. + +**Step 3 — Pick relevant KBs:** + +Match the current task area and description against each index line (or frontmatter `description` + `directories` on fallback). Select entries whose documented area overlaps the current task. This is a relevance judgment — prefer specificity over breadth. + +**Step 4 — Read selected KBs:** + +For each selected entry, read `\{worktree\}/.devflow/features/\{slug\}/KNOWLEDGE.md` in full. When the KB content contradicts the current code you observe, **trust the code** — the code is the freshness mechanism; the KB may lag behind. + +**Step 5 — Set FEATURE_KNOWLEDGE:** + +Concatenate the selected KNOWLEDGE.md files under slug headers: + +``` +--- Feature knowledge: \{slug\} --- +\{full KNOWLEDGE.md content\} +``` + +If no KBs exist, no KBs are relevant, or `.devflow/features/` is absent, set `FEATURE_KNOWLEDGE` to `(none)`. + +**No subprocess, no git calls, no `.cjs` script.** This entire step is direct file reads — 1 index read (or N frontmatter reads on fallback), bounded by KB count. +@end + +@export knowledge_load + +@define knowledge_writeback(): +### Feature Knowledge Write-Back (Conditional) + +Resolve the worktree root using the `devflow:worktree-support` algorithm (use WORKTREE_PATH if provided, otherwise cwd). All paths below are relative to `\{worktree\}`. + +**Step 1 — Check the opt-out gate:** + +Read `\{worktree\}/.devflow/dream/config.json`. If the `knowledge` field is `false`, skip write-back entirely — the user has disabled it. + +If `.devflow/dream/config.json` does not exist, proceed (default is enabled). + +**Step 2 — Evaluate whether write-back is warranted:** + +Only proceed if **at least one** of these is true: +- This workflow changed files in a directory that is documented by an existing feature knowledge base (a documented area changed). +- This workflow surfaced durable, cross-cutting knowledge about a codebase area that would help future agents working in the same area — patterns, anti-patterns, integration points, gotchas not visible from a single file read. + +**Never spawn unconditionally.** If neither condition is met, skip write-back silently. + +**Step 3 — Spawn the Knowledge agent:** + +Spawn `Agent(subagent_type="Knowledge")` with the following context: + +``` +"WORKTREE_PATH: \{worktree root\} +FEATURE_SLUG: \{slug derived from primary changed directory, kebab-case\} +FEATURE_NAME: \{human-readable name\} +DIRECTORIES: \{list of primary directories touched by this workflow\} +FILES_CHANGED: \{list of files changed\} +DECISIONS_CONTEXT: \{DECISIONS_CONTEXT if available, else (none)\} + +Load the devflow:feature-knowledge skill and follow its authoring process. + +Write the knowledge base to: + \{worktree\}/.devflow/features/\{slug\}/KNOWLEDGE.md + +Then update the index cache by performing a read-modify-write on: + \{worktree\}/.devflow/features/index.md + +Index line format: `- **\{slug\}** — \{areas\} — \{Use-when description\}` + +If the line for this slug already exists in index.md, replace it. If it does not exist, append it. If index.md does not exist, create it with just this line. + +The frontmatter in KNOWLEDGE.md is the source of truth — index.md is only a cache. Write the two files directly — no intermediate result JSON files, no external scripts." +``` + +**Failure handling**: Non-blocking. If the Knowledge agent fails, log the failure and continue — the workflow outcome is not affected by write-back success. +@end + +@export knowledge_writeback diff --git a/plugins/devflow-bug-analysis/commands/bug-analysis.md b/shared/knowledge/bug-analysis.mds similarity index 83% rename from plugins/devflow-bug-analysis/commands/bug-analysis.md rename to shared/knowledge/bug-analysis.mds index 1dbef2e1..0a60f23d 100644 --- a/plugins/devflow-bug-analysis/commands/bug-analysis.md +++ b/shared/knowledge/bug-analysis.mds @@ -1,6 +1,7 @@ --- description: Proactive bug finding with static and semantic analysis — hunts real bugs in changed code before merge --- +@import { knowledge_load } from "./_knowledge.mds" # Bug Analysis Command @@ -33,7 +34,7 @@ Return: branch, base_branch, branch-slug, PR#" **Fetch PR body** (after extracting `pr_number`): ```bash -PR_DESCRIPTION=$(gh pr view {pr_number} --json body --jq '.body' 2>/dev/null || echo "(none)") +PR_DESCRIPTION=$(gh pr view \{pr_number\} --json body --jq '.body' 2>/dev/null || echo "(none)") ``` If `pr_number` is absent or the command fails, set `PR_DESCRIPTION` to `(none)`. @@ -46,16 +47,16 @@ If `pr_number` is absent or the command fails, set `PR_DESCRIPTION` to `(none)`. **Produces:** DIFF_RANGE, ANALYSIS_DIR **Requires:** BRANCH_INFO -1. Check `.devflow/docs/bug-analysis/{branch-slug}/.last-analysis-head`: +1. Check `.devflow/docs/bug-analysis/\{branch-slug\}/.last-analysis-head`: - **If exists AND `--full` NOT set:** - Read the SHA from the file - - Verify reachable: `git cat-file -t {sha}` — if exit code non-zero (rebase invalidated SHA), fall through to full + - Verify reachable: `git cat-file -t \{sha\}` — if exit code non-zero (rebase invalidated SHA), fall through to full - If SHA == current HEAD → "No new commits since last analysis. Use --full for a full re-analysis." Stop. - - Set `DIFF_RANGE` to `{sha}...HEAD` + - Set `DIFF_RANGE` to `\{sha\}...HEAD` - **If not exists, unreachable SHA, or `--full`:** - - Set `DIFF_RANGE` to `{base_branch}...HEAD` + - Set `DIFF_RANGE` to `\{base_branch\}...HEAD` 2. Generate timestamp: `YYYY-MM-DD_HHMM`. If directory already exists (same-minute collision), append seconds (`YYYY-MM-DD_HHMMSS`). -3. Create timestamped analysis directory: `mkdir -p .devflow/docs/bug-analysis/{branch-slug}/{timestamp}/` +3. Create timestamped analysis directory: `mkdir -p .devflow/docs/bug-analysis/\{branch-slug\}/\{timestamp\}/` 4. Set `ANALYSIS_DIR` to that path. #### Step 2b: Check Changed Files @@ -63,7 +64,7 @@ If `pr_number` is absent or the command fails, set `PR_DESCRIPTION` to `(none)`. **Requires:** DIFF_RANGE ```bash -CHANGED_FILES=$(git diff --name-only {DIFF_RANGE}) +CHANGED_FILES=$(git diff --name-only \{DIFF_RANGE\}) ``` Store result as `CHANGED_FILES` — used throughout Steps 2d and Phase 4 to avoid repeated git invocations and ensure consistency. @@ -140,9 +141,9 @@ Parse `CODEQL_SARIF` → extract findings. If database creation fails, `CODEQL_E | Tool | File:Line | CWE | Severity | Title | Description | |------|-----------|-----|----------|-------|-------------| -| {tool} | {file}:{line} | {CWE or —} | {CRITICAL/HIGH/MEDIUM/LOW} | {title} | {description truncated to 200 chars} | +| \{tool\} | \{file\}:\{line\} | \{CWE or —\} | \{CRITICAL/HIGH/MEDIUM/LOW\} | \{title\} | \{description truncated to 200 chars\} | -Write ALL raw findings to `{ANALYSIS_DIR}/static-findings.md`. Set `STATIC_FINDINGS` to the top-50 table. +Write ALL raw findings to `\{ANALYSIS_DIR\}/static-findings.md`. Set `STATIC_FINDINGS` to the top-50 table. If no tool produced findings: set `STATIC_FINDINGS` to `(none)`. @@ -158,10 +159,7 @@ DECISIONS_CONTEXT=$(node ~/.devflow/scripts/hooks/lib/decisions-index.cjs index #### Feature Knowledge -1. Read `.devflow/features/index.json` if it exists -2. Match changed files from Phase 2 against feature knowledge entries (`directories` and `referencedFiles`) -3. For each match: check staleness via `node ~/.devflow/scripts/hooks/lib/feature-knowledge.cjs stale "." {slug} 2>/dev/null`, read `.devflow/features/{slug}/KNOWLEDGE.md` -4. Concatenate as `FEATURE_KNOWLEDGE` (or `(none)`) +{knowledge_load()} #### Plan Artifact @@ -195,19 +193,19 @@ Spawn ALL active BugAnalyzer agents **in a single message** (parallel, NOT backg For each active focus, spawn: ``` Agent(subagent_type="BugAnalyzer", run_in_background=false): -"Analyze focusing on {focus}. -FOCUS: {focus} -DIFF_COMMAND: git diff {DIFF_RANGE} -ACCEPTANCE_RULES: {ACCEPTANCE_RULES filtered to this focus type, or (none)} -PLAN_CONTEXT: {PLAN_CONTEXT} -STATIC_FINDINGS: {STATIC_FINDINGS if focus == security, else (none)} -DECISIONS_CONTEXT: {DECISIONS_CONTEXT} -FEATURE_KNOWLEDGE: {FEATURE_KNOWLEDGE} -PR_DESCRIPTION: {PR_DESCRIPTION} -OUTPUT_PATH: {ANALYSIS_DIR}/{focus}.md +"Analyze focusing on \{focus\}. +FOCUS: \{focus\} +DIFF_COMMAND: git diff \{DIFF_RANGE\} +ACCEPTANCE_RULES: \{ACCEPTANCE_RULES filtered to this focus type, or (none)\} +PLAN_CONTEXT: \{PLAN_CONTEXT\} +STATIC_FINDINGS: \{STATIC_FINDINGS if focus == security, else (none)\} +DECISIONS_CONTEXT: \{DECISIONS_CONTEXT\} +FEATURE_KNOWLEDGE: \{FEATURE_KNOWLEDGE\} +PR_DESCRIPTION: \{PR_DESCRIPTION\} +OUTPUT_PATH: \{ANALYSIS_DIR\}/\{focus\}.md Follow devflow:apply-decisions to Read full ADR/PF bodies on demand. Follow devflow:apply-feature-knowledge for FEATURE_KNOWLEDGE. -IMPORTANT: Write report to {ANALYSIS_DIR}/{focus}.md using Write tool" +IMPORTANT: Write report to \{ANALYSIS_DIR\}/\{focus\}.md using Write tool" ``` Notes: @@ -225,46 +223,46 @@ Spawn Synthesizer: ``` Agent(subagent_type="Synthesizer", run_in_background=false): "Mode: bug-analysis -ANALYSIS_BASE_DIR: {ANALYSIS_DIR} -BRANCH: {branch} -> {base_branch} -TIMESTAMP: {timestamp} -Output: {ANALYSIS_DIR}/bug-analysis-summary.md" +ANALYSIS_BASE_DIR: \{ANALYSIS_DIR\} +BRANCH: \{branch\} -> \{base_branch\} +TIMESTAMP: \{timestamp\} +Output: \{ANALYSIS_DIR\}/bug-analysis-summary.md" ``` ### Phase 7: Finalize **Requires:** BRANCH_INFO, ANALYSIS_DIR -1. Write current HEAD SHA to `.devflow/docs/bug-analysis/{branch-slug}/.last-analysis-head` +1. Write current HEAD SHA to `.devflow/docs/bug-analysis/\{branch-slug\}/.last-analysis-head` 2. Report to user: ``` ## Bug Analysis Complete -**Branch**: {branch} -> {base_branch} -**Analysis**: {ANALYSIS_DIR} +**Branch**: \{branch\} -> \{base_branch\} +**Analysis**: \{ANALYSIS_DIR\} -### Risk Assessment: {risk_level} +### Risk Assessment: \{risk_level\} -{brief_reasoning} +\{brief_reasoning\} ### Bug Counts | Category | CRITICAL | HIGH | MEDIUM | LOW | Total | |----------|----------|------|--------|-----|-------| -| Security | {n} | {n} | {n} | {n} | {n} | -| Functional | {n} | {n} | {n} | {n} | {n} | -| Integration | {n} | {n} | {n} | {n} | {n} | -| Usability | {n} | {n} | {n} | {n} | {n} | +| Security | \{n\} | \{n\} | \{n\} | \{n\} | \{n\} | +| Functional | \{n\} | \{n\} | \{n\} | \{n\} | \{n\} | +| Integration | \{n\} | \{n\} | \{n\} | \{n\} | \{n\} | +| Usability | \{n\} | \{n\} | \{n\} | \{n\} | \{n\} | ### Top Findings -{List top 3-5 bugs by severity and confidence} +\{List top 3-5 bugs by severity and confidence\} ### Artifacts -- Bug report: {ANALYSIS_DIR}/bug-analysis-summary.md -- Per-focus reports: {ANALYSIS_DIR}/{security|functional|integration|usability}.md -- Static findings: {ANALYSIS_DIR}/static-findings.md (if static analysis ran) +- Bug report: \{ANALYSIS_DIR\}/bug-analysis-summary.md +- Per-focus reports: \{ANALYSIS_DIR\}/\{security|functional|integration|usability\}.md +- Static findings: \{ANALYSIS_DIR\}/static-findings.md (if static analysis ran) -{if any CRITICAL or HIGH bugs found:} +\{if any CRITICAL or HIGH bugs found:\} Run `/resolve` to process and fix these findings. ``` @@ -285,7 +283,7 @@ Run `/resolve` to process and fix these findings. │ ├─ Phase 3: Context Loading │ ├─ decisions-index.cjs → DECISIONS_CONTEXT -│ ├─ feature-knowledge.cjs → FEATURE_KNOWLEDGE +│ ├─ Feature knowledge load → FEATURE_KNOWLEDGE │ └─ .devflow/docs/design/*.md → PLAN_CONTEXT + ACCEPTANCE_RULES │ ├─ Phase 4: File Analysis @@ -344,7 +342,7 @@ Before reporting results, verify every phase was executed: - [ ] Phase 3: Context Loading → DECISIONS_CONTEXT captured, FEATURE_KNOWLEDGE loaded (or `(none)`), PLAN_CONTEXT and ACCEPTANCE_RULES captured (or `(none)`) - [ ] Phase 4: File Analysis → ACTIVE_FOCUSES determined (security + functional always present) - [ ] Phase 5: Parallel Bug Analysis → ANALYZER_OUTPUTS captured per active focus; all reports written to ANALYSIS_DIR -- [ ] Phase 6: Synthesis → BUG_ANALYSIS_SUMMARY written to `{ANALYSIS_DIR}/bug-analysis-summary.md` +- [ ] Phase 6: Synthesis → BUG_ANALYSIS_SUMMARY written to `\{ANALYSIS_DIR\}/bug-analysis-summary.md` - [ ] Phase 7: Finalize → `.last-analysis-head` updated; results displayed to user If any phase is unchecked, execute it before proceeding. diff --git a/plugins/devflow-code-review/commands/code-review.md b/shared/knowledge/code-review.mds similarity index 78% rename from plugins/devflow-code-review/commands/code-review.md rename to shared/knowledge/code-review.mds index 64faa46a..ec473d40 100644 --- a/plugins/devflow-code-review/commands/code-review.md +++ b/shared/knowledge/code-review.mds @@ -1,6 +1,7 @@ --- description: Comprehensive branch review using specialized sub-agents for PR readiness --- +@import { knowledge_load } from "./_knowledge.mds" # Code Review Command @@ -29,7 +30,7 @@ Run a comprehensive code review of the current branch by spawning parallel revie 2. **If `--path` flag provided:** use only that worktree, skip discovery **`--path` validation**: Before proceeding, verify the path exists as a directory and appears in `git worktree list` output. If not: report error and stop. 3. **If only 1 reviewable worktree** (the common case): proceed as single-worktree flow — zero behavior change -4. **If multiple reviewable worktrees:** report "Found N worktrees with reviewable branches: {list with paths and branches}" and proceed with multi-worktree flow +4. **If multiple reviewable worktrees:** report "Found N worktrees with reviewable branches: \{list with paths and branches\}" and proceed with multi-worktree flow #### Step 0b: Per-Worktree Pre-Flight (Git Agent) @@ -37,7 +38,7 @@ Run a comprehensive code review of the current branch by spawning parallel revie **Requires:** WORKTREES Discover PR description guidance from plan artifact (per worktree): -1. List `{worktree}/.devflow/docs/design/*.md` files +1. List `\{worktree\}/.devflow/docs/design/*.md` files 2. Sort by timestamp in filename (descending -- timestamps are YYYY-MM-DD_HHMM, naturally sortable) 3. Read the most recent file, extract `## PR Description Guidance` section 4. If no plan files exist or section not found, set `PR_DESCRIPTION_GUIDANCE` to `(none)` @@ -47,8 +48,8 @@ For each reviewable worktree, spawn Git agent: ``` Agent(subagent_type="Git", run_in_background=false): "OPERATION: ensure-pr-ready -WORKTREE_PATH: {worktree_path} (omit if cwd) -PR_DESCRIPTION_GUIDANCE: {pr_description_guidance} +WORKTREE_PATH: \{worktree_path\} (omit if cwd) +PR_DESCRIPTION_GUIDANCE: \{pr_description_guidance\} Validate branch, commit if needed, push, create PR if needed. Return: branch, base_branch, branch-slug, PR#" ``` @@ -61,7 +62,7 @@ In multi-worktree mode, spawn all pre-flight agents **in a single message** (par **Fetch PR body** (after extracting `pr_number`): ```bash -PR_DESCRIPTION=$(gh pr view {pr_number} --json body --jq '.body' 2>/dev/null || echo "(none)") +PR_DESCRIPTION=$(gh pr view \{pr_number\} --json body --jq '.body' 2>/dev/null || echo "(none)") ``` If `pr_number` is absent or the command fails, set `PR_DESCRIPTION` to `(none)`. @@ -73,15 +74,15 @@ If `pr_number` is absent or the command fails, set `PR_DESCRIPTION` to `(none)`. For each worktree: 1. Generate timestamp: `YYYY-MM-DD_HHMM`. If directory already exists (same-minute collision), append seconds (`YYYY-MM-DD_HHMMSS`). -2. Create timestamped review directory: `mkdir -p {worktree}/.devflow/docs/reviews/{branch-slug}/{timestamp}/` -3. Check if `{worktree}/.devflow/docs/reviews/{branch-slug}/.last-review-head` exists: +2. Create timestamped review directory: `mkdir -p \{worktree\}/.devflow/docs/reviews/\{branch-slug\}/\{timestamp\}/` +3. Check if `\{worktree\}/.devflow/docs/reviews/\{branch-slug\}/.last-review-head` exists: - **If yes AND `--full` NOT set:** - Read the SHA from the file - - Verify reachable: `git -C {worktree} cat-file -t {sha}` (handles rebases — if unreachable, fallback to full) + - Verify reachable: `git -C \{worktree\} cat-file -t \{sha\}` (handles rebases — if unreachable, fallback to full) - Check if SHA == current HEAD → if so, skip review: "No new commits since last review. Use --full for a full re-review." - - Set `DIFF_RANGE` to `{last-review-sha}...HEAD` + - Set `DIFF_RANGE` to `\{last-review-sha\}...HEAD` - **If no (first review), or `--full`:** - - Set `DIFF_RANGE` to `{base_branch}...HEAD` + - Set `DIFF_RANGE` to `\{base_branch\}...HEAD` #### Step 0d-i: Load Prior Resolution and Count Cycles @@ -89,10 +90,10 @@ For each worktree: **Requires:** BRANCH_INFO For each worktree, perform a single pass over timestamped directories: -1. List timestamped directories in `{worktree}/.devflow/docs/reviews/{branch-slug}/` sorted descending: `ls -1d {worktree}/.devflow/docs/reviews/{branch-slug}/20* 2>/dev/null | sort -r` +1. List timestamped directories in `\{worktree\}/.devflow/docs/reviews/\{branch-slug\}/` sorted descending: `ls -1d \{worktree\}/.devflow/docs/reviews/\{branch-slug\}/20* 2>/dev/null | sort -r` 2. Iterate once: accumulate CYCLE_NUMBER count for each directory containing `resolution-summary.md`; capture the first (most-recent) such directory as PRIOR_DIR. 3. If CYCLE_NUMBER = 0: set PRIOR_RESOLUTIONS=(none), CYCLE_NUMBER=1, proceed. -4. Otherwise: set CYCLE_NUMBER = count + 1. Read `{PRIOR_DIR}/resolution-summary.md` as PRIOR_RESOLUTIONS. +4. Otherwise: set CYCLE_NUMBER = count + 1. Read `\{PRIOR_DIR\}/resolution-summary.md` as PRIOR_RESOLUTIONS. 5. If `--full`: still load PRIOR_RESOLUTIONS (valuable for reviewer cross-cycle awareness). #### Step 0d-ii: Convergence Assessment @@ -103,7 +104,7 @@ For each worktree, perform a single pass over timestamped directories: MAX_REVIEW_CYCLES = 10 1. If CYCLE_NUMBER > MAX_REVIEW_CYCLES: - Warn in output: "⚠️ Review pipeline has run {CYCLE_NUMBER-1} cycles (exceeds MAX_REVIEW_CYCLES=10). Consider merging or manual inspection." + Warn in output: "⚠️ Review pipeline has run \{CYCLE_NUMBER-1\} cycles (exceeds MAX_REVIEW_CYCLES=10). Consider merging or manual inspection." Continue with review. 2. Parse Statistics table from PRIOR_RESOLUTIONS: - Extract False Positive, Fixed, Deferred counts @@ -111,7 +112,7 @@ MAX_REVIEW_CYCLES = 10 - If denominator = 0: fp_ratio = 0, skip warning - If parsing fails: fp_ratio = 0, skip warning; note in output: "Warning: Could not parse Statistics table from prior resolution. FP ratio unavailable — convergence tracking degraded." 3. If fp_ratio > 0.7 AND CYCLE_NUMBER >= 3: - Warn in output: "⚠️ Convergence: {ratio}% false positives in cycle {N-1}. Consider merging or manual inspection." + Warn in output: "⚠️ Convergence: \{ratio\}% false positives in cycle \{N-1\}. Consider merging or manual inspection." Continue with review. **Decision table — Step 0d-ii paths:** @@ -143,7 +144,7 @@ Per worktree, detect file types in diff using `DIFF_RANGE` to determine conditio | Dependency files changed | dependencies | | Docs or significant code | documentation | -**Skill availability check**: Language/ecosystem reviews (typescript, react, accessibility, ui-design, go, java, python, rust) require their optional skill plugin to be installed. Before spawning a conditional Reviewer for these focuses, use Read to check if `~/.claude/skills/devflow:{focus}/SKILL.md` exists. If Read returns an error (file not found), **skip that review** — the language plugin isn't installed. Non-language reviews (database, dependencies, documentation) use skills bundled with this plugin and are always available. +**Skill availability check**: Language/ecosystem reviews (typescript, react, accessibility, ui-design, go, java, python, rust) require their optional skill plugin to be installed. Before spawning a conditional Reviewer for these focuses, use Read to check if `~/.claude/skills/devflow:\{focus\}/SKILL.md` exists. If Read returns an error (file not found), **skip that review** — the language plugin isn't installed. Non-language reviews (database, dependencies, documentation) use skills bundled with this plugin and are always available. ### Phase 1b: Load Decisions Index @@ -159,11 +160,7 @@ DECISIONS_CONTEXT=$(node ~/.devflow/scripts/hooks/lib/decisions-index.cjs index This produces a compact index of active ADR/PF entries. Pass `DECISIONS_CONTEXT` to all Reviewer agents. Reviewers use `devflow:apply-decisions` to Read full entry bodies on demand. -**Load Feature Knowledge:** -1. Read `.devflow/features/index.json` if it exists -2. Based on changed files from Phase 1 analysis, identify relevant feature knowledge (match file paths against each feature knowledge entry's `directories` and `referencedFiles`) -3. For each match: check staleness via `node ~/.devflow/scripts/hooks/lib/feature-knowledge.cjs stale "{worktree}" {slug} 2>/dev/null`, read `.devflow/features/{slug}/KNOWLEDGE.md` -4. Set `FEATURE_KNOWLEDGE` (or `(none)` if no feature knowledge exists or none is relevant) +{knowledge_load()} Pass `FEATURE_KNOWLEDGE` to all Reviewer agents alongside `DECISIONS_CONTEXT`. @@ -199,19 +196,19 @@ Spawn Reviewer agents **in a single message**. Always run 8 core reviews; condit Each Reviewer invocation (all in one message, **NOT background**): ``` Agent(subagent_type="Reviewer", run_in_background=false): -"Review focusing on {focus}. Load the pattern skill for your focus from the Focus Areas table. +"Review focusing on \{focus\}. Load the pattern skill for your focus from the Focus Areas table. Follow 6-step process from devflow:review-methodology. -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) -DECISIONS_CONTEXT: {decisions_context} -FEATURE_KNOWLEDGE: {feature_knowledge} -PR_DESCRIPTION: {pr_description} -PRIOR_RESOLUTIONS: {prior_resolutions} +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) +DECISIONS_CONTEXT: \{decisions_context\} +FEATURE_KNOWLEDGE: \{feature_knowledge\} +PR_DESCRIPTION: \{pr_description\} +PRIOR_RESOLUTIONS: \{prior_resolutions\} If PRIOR_RESOLUTIONS is not (none), follow Cross-Cycle Awareness in reviewer.md. Follow devflow:apply-decisions to scan the index and Read full ADR/PF bodies on demand. Follow devflow:apply-feature-knowledge for FEATURE_KNOWLEDGE — feature-specific patterns and anti-patterns inform findings. -IMPORTANT: Write report to {worktree_path}/.devflow/docs/reviews/{branch-slug}/{timestamp}/{focus}.md using Write tool" +IMPORTANT: Write report to \{worktree_path\}/.devflow/docs/reviews/\{branch-slug\}/\{timestamp\}/\{focus\}.md using Write tool" ``` In multi-worktree mode, process worktrees **sequentially** (one worktree at a time). Complete Phases 1-4 for each worktree before starting the next. This prevents agent overload — spawning 8-19 reviewers per worktree across multiple worktrees simultaneously overwhelms the system. @@ -227,8 +224,8 @@ In multi-worktree mode, process worktrees **sequentially** (one worktree at a ti ``` Agent(subagent_type="Git", run_in_background=false): "OPERATION: comment-pr -WORKTREE_PATH: {worktree_path} (omit if cwd) -Read reviews from {worktree_path}/.devflow/docs/reviews/{branch-slug}/{timestamp}/ +WORKTREE_PATH: \{worktree_path\} (omit if cwd) +Read reviews from \{worktree_path\}/.devflow/docs/reviews/\{branch-slug\}/\{timestamp\}/ Create inline PR comments for findings with ≥80% confidence only. Lower-confidence suggestions (60-79%) go in the summary comment, not as inline comments. Deduplicate findings across reviewers, consolidate skipped into summary. @@ -239,14 +236,14 @@ Check for existing inline comments at same file:line before creating new ones to ``` Agent(subagent_type="Synthesizer", run_in_background=false): "Mode: review -WORKTREE_PATH: {worktree_path} (omit if cwd) -REVIEW_BASE_DIR: {worktree_path}/.devflow/docs/reviews/{branch-slug}/{timestamp} -TIMESTAMP: {timestamp} -CYCLE_NUMBER: {cycle_number} -PRIOR_RESOLUTIONS: {prior_resolutions} +WORKTREE_PATH: \{worktree_path\} (omit if cwd) +REVIEW_BASE_DIR: \{worktree_path\}/.devflow/docs/reviews/\{branch-slug\}/\{timestamp\} +TIMESTAMP: \{timestamp\} +CYCLE_NUMBER: \{cycle_number\} +PRIOR_RESOLUTIONS: \{prior_resolutions\} Include Convergence Status section in review-summary.md. Aggregate findings, determine merge recommendation -Output: {worktree_path}/.devflow/docs/reviews/{branch-slug}/{timestamp}/review-summary.md" +Output: \{worktree_path\}/.devflow/docs/reviews/\{branch-slug\}/\{timestamp\}/review-summary.md" ``` ### Phase 4: Write Review Head Marker & Report @@ -254,7 +251,7 @@ Output: {worktree_path}/.devflow/docs/reviews/{branch-slug}/{timestamp}/review-s **Requires:** BRANCH_INFO, REVIEW_DIR Per worktree, after successful completion: -1. Write current HEAD SHA to `{worktree_path}/.devflow/docs/reviews/{branch-slug}/.last-review-head` +1. Write current HEAD SHA to `\{worktree_path\}/.devflow/docs/reviews/\{branch-slug\}/.last-review-head` 2. Display results from all agents: - Merge recommendation (from Synthesizer) - Issue counts by category (🔴 blocking / ⚠️ should-fix / ℹ️ pre-existing) @@ -317,7 +314,7 @@ In multi-worktree mode, report results per worktree. ## Backwards Compatibility - **Single worktree**: Auto-discovery finds only one worktree → proceeds exactly as before. Zero behavior change. -- **Legacy flat layout**: If `.devflow/docs/reviews/{branch-slug}/` contains flat `*.md` files (no timestamped subdirectories), new runs create timestamped subdirectories. Old flat files remain untouched. +- **Legacy flat layout**: If `.devflow/docs/reviews/\{branch-slug\}/` contains flat `*.md` files (no timestamped subdirectories), new runs create timestamped subdirectories. Old flat files remain untouched. ## Principles diff --git a/plugins/devflow-debug/commands/debug.md b/shared/knowledge/debug.mds similarity index 75% rename from plugins/devflow-debug/commands/debug.md rename to shared/knowledge/debug.mds index a50eb994..e450d2fc 100644 --- a/plugins/devflow-debug/commands/debug.md +++ b/shared/knowledge/debug.mds @@ -1,6 +1,7 @@ --- description: Debug issues using competing hypothesis investigation with parallel agents --- +@import { knowledge_writeback } from "./_knowledge.mds" # Debug Command @@ -25,7 +26,7 @@ Investigate bugs by spawning parallel agents, each pursuing a different hypothes ### Phase 1: Load Decisions Index (Orchestrator-Local) -**Produces:** DECISIONS_CONTEXT, FEATURE_KNOWLEDGE +**Produces:** DECISIONS_CONTEXT **Load Companion Skills** — Load via Skill tool: `devflow:test-driven-development`, `devflow:software-design`, `devflow:testing`. If a skill fails to load, continue without it. @@ -37,11 +38,7 @@ DECISIONS_CONTEXT=$(node ~/.devflow/scripts/hooks/lib/decisions-index.cjs index The orchestrator uses `DECISIONS_CONTEXT` locally when generating hypotheses (Phase 2) — prior pitfalls and decisions can suggest specific root causes to investigate. Follow `devflow:apply-decisions` to Read full entry bodies on demand. **Do NOT pass `DECISIONS_CONTEXT` to Explore investigators** — decisions context stays in the orchestrator; investigators examine code directly. -**Load Feature Knowledge:** -1. Read `.devflow/features/index.json` if it exists -2. Based on the bug description, identify relevant feature knowledge -3. For each match: check staleness via `node ~/.devflow/scripts/hooks/lib/feature-knowledge.cjs stale "{worktree}" {slug} 2>/dev/null`, read `.devflow/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.** +**No up-front feature knowledge load** — debug investigation workers read code directly to avoid confirmation bias. Feature knowledge is only written back at the end if the investigation surfaced durable, cross-cutting knowledge. ### Phase 2: Context Gathering @@ -53,7 +50,7 @@ If `$ARGUMENTS` starts with `#`, fetch the GitHub issue: ``` Agent(subagent_type="Git"): "OPERATION: fetch-issue -ISSUE: {issue number} +ISSUE: \{issue number\} Return issue title, body, labels, and any linked error logs." ``` @@ -71,10 +68,10 @@ Spawn one Explore agent per hypothesis in a **single message** (parallel executi ``` Agent(subagent_type="Explore"): -"Investigate this bug: {bug_description} +"Investigate this bug: \{bug_description\} -Hypothesis: {hypothesis A description} -Focus area: {specific code area, mechanism, or condition} +Hypothesis: \{hypothesis A description\} +Focus area: \{specific code area, mechanism, or condition\} Steps: 1. Read relevant code files in your focus area @@ -83,25 +80,25 @@ Steps: 4. Collect evidence AGAINST this hypothesis (with file:line references) Return a structured report: -- Hypothesis: {restate} +- Hypothesis: \{restate\} - Status: CONFIRMED / DISPROVED / PARTIAL - Evidence FOR: [list with file:line refs] - Evidence AGAINST: [list with file:line refs] -- Key finding: {one-sentence summary}" +- Key finding: \{one-sentence summary\}" Agent(subagent_type="Explore"): -"Investigate this bug: {bug_description} +"Investigate this bug: \{bug_description\} -Hypothesis: {hypothesis B description} -Focus area: {specific code area, mechanism, or condition} +Hypothesis: \{hypothesis B description\} +Focus area: \{specific code area, mechanism, or condition\} [same steps and return format]" Agent(subagent_type="Explore"): -"Investigate this bug: {bug_description} +"Investigate this bug: \{bug_description\} -Hypothesis: {hypothesis C description} -Focus area: {specific code area, mechanism, or condition} +Hypothesis: \{hypothesis C description\} +Focus area: \{specific code area, mechanism, or condition\} [same steps and return format]" @@ -130,7 +127,7 @@ Once all investigators return, spawn a Synthesizer agent to aggregate findings: Agent(subagent_type="Synthesizer"): "You are a root cause analyst. Synthesize these investigation reports: -{paste all investigator reports} +\{paste all investigator reports\} Instructions: 1. Compare evidence across all hypotheses @@ -147,28 +144,28 @@ Instructions: Produce the final report: ```markdown -## Root Cause Analysis: {bug description} +## Root Cause Analysis: \{bug description\} ### Root Cause -{Description of the root cause supported by evidence} -{Key evidence with file:line references} +\{Description of the root cause supported by evidence\} +\{Key evidence with file:line references\} ### Investigation Summary | Hypothesis | Status | Key Evidence | |-----------|--------|-------------| -| A: {description} | CONFIRMED/DISPROVED/PARTIAL | {file:line + summary} | -| B: {description} | CONFIRMED/DISPROVED/PARTIAL | {file:line + summary} | -| C: {description} | CONFIRMED/DISPROVED/PARTIAL | {file:line + summary} | +| A: \{description\} | CONFIRMED/DISPROVED/PARTIAL | \{file:line + summary\} | +| B: \{description\} | CONFIRMED/DISPROVED/PARTIAL | \{file:line + summary\} | +| C: \{description\} | CONFIRMED/DISPROVED/PARTIAL | \{file:line + summary\} | ### Key Findings -{2-3 most important discoveries across all investigators} +\{2-3 most important discoveries across all investigators\} ### Recommended Fix -{Concrete action items with file references} +\{Concrete action items with file references\} ### Confidence Level -{HIGH/MEDIUM/LOW based on evidence strength and investigator agreement} +\{HIGH/MEDIUM/LOW based on evidence strength and investigator agreement\} ``` ### Phase 7: Offer Fix @@ -180,12 +177,15 @@ Ask user via AskUserQuestion: "Want me to implement this fix?" - **YES** → Load `devflow:patterns` and `devflow:test-driven-development` skills, implement the fix, then spawn `Agent(subagent_type="Simplifier")` on changed files. - **NO** → Done. Report stands as documentation. +{knowledge_writeback()} + ## Architecture ``` /debug (orchestrator) │ ├─ Phase 1: Load Decisions Index (Orchestrator-Local) +│ └─ No up-front feature knowledge load (avoids confirmation bias in investigators) │ ├─ Phase 2: Context gathering │ └─ Git agent (fetch issue, if #N provided) @@ -201,8 +201,11 @@ Ask user via AskUserQuestion: "Want me to implement this fix?" │ ├─ Phase 6: Root cause report with confidence level │ -└─ Phase 7: Offer Fix - └─ AskUserQuestion → implement fix + Simplifier, or done +├─ Phase 7: Offer Fix +│ └─ AskUserQuestion → implement fix + Simplifier, or done +│ +└─ Feature Knowledge Write-Back (Conditional) + └─ Knowledge agent (if investigation surfaced durable cross-cutting knowledge) ``` ## Principles @@ -212,6 +215,7 @@ Ask user via AskUserQuestion: "Want me to implement this fix?" 3. **Evidence-based** - Every claim requires file:line references 4. **Honest uncertainty** - If no hypothesis survives, report that clearly 5. **Convergence validation** - Confirmed hypotheses get additional validation to prevent confirmation bias +6. **No pre-loaded knowledge in sub-agents** - Investigators read code fresh to avoid confirmation bias; feature knowledge write-back happens only after investigation completes ## Error Handling diff --git a/plugins/devflow-explore/commands/explore.md b/shared/knowledge/explore.mds similarity index 62% rename from plugins/devflow-explore/commands/explore.md rename to shared/knowledge/explore.mds index 03fbcaa1..83a6d631 100644 --- a/plugins/devflow-explore/commands/explore.md +++ b/shared/knowledge/explore.mds @@ -1,6 +1,7 @@ --- description: Explore codebase with structured analysis and optional feature knowledge creation --- +@import { knowledge_writeback } from "./_knowledge.mds" # Explore Command @@ -25,7 +26,7 @@ Explore a codebase area by spawning parallel agents for flow tracing, dependency ### Phase 1: Load Decisions (Orchestrator-Local) -**Produces:** DECISIONS_CONTEXT, FEATURE_KNOWLEDGE +**Produces:** DECISIONS_CONTEXT Before exploring, load the decisions index: @@ -35,14 +36,7 @@ DECISIONS_CONTEXT=$(node ~/.devflow/scripts/hooks/lib/decisions-index.cjs index The orchestrator uses `DECISIONS_CONTEXT` locally when framing exploration — prior decisions and pitfalls suggest specific areas to investigate. Follow `devflow:apply-decisions` to Read full entry bodies on demand. **Do NOT pass `DECISIONS_CONTEXT` to Explore sub-agents** — decisions context stays in the orchestrator, not in the investigation workers. -Also load feature knowledge: -1. Read `.devflow/features/index.json` if it exists. If not, set `FEATURE_KNOWLEDGE = (none)`. -2. Identify relevant feature knowledge entries (match task intent against each entry's descriptions and directories). -3. For each match: check staleness via `node ~/.devflow/scripts/hooks/lib/feature-knowledge.cjs stale "{worktree}" {slug} 2>/dev/null`, read `.devflow/features/{slug}/KNOWLEDGE.md`. -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 DECISIONS_CONTEXT). - -**Explore agent framing**: "The feature knowledge is a baseline — your job is to VALIDATE, EXTEND, and CORRECT it, not repeat it. Focus on areas the feature knowledge doesn't cover and things that may have changed." +**No up-front feature knowledge load** — explore investigation workers read code directly to avoid confirmation bias. Feature knowledge is only created as a write-back at the end, after exploration is complete. ### Phase 2: Orient @@ -95,39 +89,14 @@ Present findings to user. Use AskUserQuestion to offer focused follow-up explora **Requires:** MERGED_FINDINGS, DECISIONS_CONTEXT **Produces:** FEATURE_KNOWLEDGE_STATUS (created | skipped) -1. If `.devflow/features/.disabled` exists → skip, set FEATURE_KNOWLEDGE_STATUS = skipped -2. Read `.devflow/features/index.json` (if it exists) -3. Based on the explored area (user's question + MERGED_FINDINGS scope), check if matching feature knowledge - already exists (match against each entry's `directories` and `description`). If covered → skip -4. Use AskUserQuestion: "No feature knowledge exists for {explored area}. Create one to capture these patterns?" -5. If user declines → set FEATURE_KNOWLEDGE_STATUS = skipped -6. If user accepts: - a. Derive FEATURE_SLUG from explored area (kebab-case from primary directory, strip src/lib - prefixes, must match `^[a-z0-9][a-z0-9-]*$`) - b. Derive FEATURE_NAME (human-readable) - c. Identify DIRECTORIES from explored scope - d. Spawn Agent(subagent_type="Knowledge"): - ``` - "FEATURE_SLUG: {slug} - FEATURE_NAME: {name} - DIRECTORIES: {directories} - EXPLORATION_OUTPUTS: {MERGED_FINDINGS from Phase 4} - DECISIONS_CONTEXT: {from Phase 1} - WORKTREE_PATH: {worktree path, if in a worktree} - Load the devflow:feature-knowledge skill. EXPLORATION_OUTPUTS are pre-computed — synthesize instead of - exploring from scratch. Read .devflow/features/index.json for cross-referencing." - ``` - e. Read result file (`.devflow/features/{slug}/.create-result.json`), then run: - ```bash - node ~/.devflow/scripts/hooks/lib/feature-knowledge.cjs update-index "{worktree}" \ - --slug="{slug}" --name="{name}" --directories='[...]' \ - --referencedFiles='{from_result}' --description="{from_result}" \ - --createdBy="explore" 2>/dev/null - ``` - Clean up: `rm -f .devflow/features/{slug}/.create-result.json` - If result file missing (agent failed), use empty defaults: `referencedFiles='[]'`, `description=""`. - f. Report: "Created feature knowledge: {slug}" - g. Set FEATURE_KNOWLEDGE_STATUS = created +1. Check if matching feature knowledge already exists by reading `\{worktree\}/.devflow/features/index.md` (or globbing frontmatter if absent). If covered → skip +2. Use AskUserQuestion: "No feature knowledge exists for \{explored area\}. Create one to capture these patterns?" +3. If user declines → set FEATURE_KNOWLEDGE_STATUS = skipped +4. If user accepts: proceed with write-back below. + +{knowledge_writeback()} + +Set FEATURE_KNOWLEDGE_STATUS = created (if agent spawned) or skipped. **Failure handling**: Non-blocking. If Knowledge agent fails, log and continue. @@ -165,14 +134,14 @@ Structured exploration findings with concrete code references: ├─ Phase 5: Present findings with drill-down offer │ └─ Phase 6: Suggest feature knowledge creation (conditional) - └─ Knowledge agent (if user accepts and no existing feature knowledge) + └─ Knowledge agent write-back (if user accepts and no existing feature knowledge) ``` ## Principles 1. **Structure over browsing** - Every claim must cite file:line references 2. **Parallel execution** - All explorers run simultaneously for speed -3. **Knowledge-informed** - Prior decisions and feature knowledge guide where to look +3. **Knowledge-informed** - Prior decisions guide where to look; NO pre-loaded feature knowledge in sub-agents (avoids confirmation bias) 4. **User-driven depth** - Present findings, then offer drill-down into specific areas ## Error Handling diff --git a/plugins/devflow-implement/commands/implement.md b/shared/knowledge/implement.mds similarity index 65% rename from plugins/devflow-implement/commands/implement.md rename to shared/knowledge/implement.mds index 94e6aee8..0c50080a 100644 --- a/plugins/devflow-implement/commands/implement.md +++ b/shared/knowledge/implement.mds @@ -1,6 +1,7 @@ --- description: Execute a single task through implementation, quality gates, and PR creation - accepts plan documents, issues, or task descriptions --- +@import { knowledge_load, knowledge_writeback } from "./_knowledge.mds" # Implement Command @@ -33,7 +34,7 @@ When the user explicitly asks to re-validate, re-check, or re-run quality gates 1. **Branch safety check**: If on a protected branch (main, master, develop, etc.), run Phase 1 to create/switch to a work branch. If already on a work branch, skip Phase 1. 2. **Skip Phase 2** — no Coder needed, user already made changes. -3. **Detect FILES_CHANGED**: `git diff --name-only {base_branch}...HEAD` +3. **Detect FILES_CHANGED**: `git diff --name-only \{base_branch\}...HEAD` 4. **Run Phases 3-8** — full quality gate pipeline on detected changes. 5. **Proceed to Phase 10** (Create PR) / **Phase 11** (Report). @@ -41,7 +42,7 @@ If the user prompt does NOT match re-validation, proceed with the full pipeline ### Phase 1: Setup -**Produces:** TASK_ID, BASE_BRANCH, EXECUTION_PLAN, DECISIONS_CONTEXT, FEATURE_KNOWLEDGE, STALE_FEATURE_KNOWLEDGE_SLUGS, PR_DESCRIPTION_GUIDANCE +**Produces:** TASK_ID, BASE_BRANCH, EXECUTION_PLAN, DECISIONS_CONTEXT, FEATURE_KNOWLEDGE, PR_DESCRIPTION_GUIDANCE **Load Companion Skills** — Load via Skill tool: `devflow:test-driven-development`, `devflow:patterns`, `devflow:dependency-research`. If a skill fails to load, continue without it. @@ -52,9 +53,9 @@ Spawn Git agent to set up task environment. The Git agent derives the branch nam ``` Agent(subagent_type="Git"): "OPERATION: setup-task -BASE_BRANCH: {current branch name} -ISSUE_INPUT: {issue number if $ARGUMENTS starts with #, otherwise omit} -TASK_DESCRIPTION: {task description from $ARGUMENTS if not an issue number or .md path, otherwise omit} +BASE_BRANCH: \{current branch name\} +ISSUE_INPUT: \{issue number if $ARGUMENTS starts with #, otherwise omit\} +TASK_DESCRIPTION: \{task description from $ARGUMENTS if not an issue number or .md path, otherwise omit\} Derive branch name from issue or description, create feature branch, and fetch issue if specified. Return the branch setup summary." ``` @@ -83,12 +84,7 @@ DECISIONS_CONTEXT=$(node ~/.devflow/scripts/hooks/lib/decisions-index.cjs index ``` Pass to Coder (Phase 2) and Scrutinizer (Phase 5). -**Load Feature Knowledge:** -1. Read `.devflow/features/index.json` if it exists -2. Based on task description and file targets, identify relevant feature knowledge -3. For each match: check staleness via `node ~/.devflow/scripts/hooks/lib/feature-knowledge.cjs stale "{worktree}" {slug} 2>/dev/null`, read `.devflow/features/{slug}/KNOWLEDGE.md` -4. Set `FEATURE_KNOWLEDGE` (or `(none)` if no feature knowledge exists or none is relevant) -5. Collect slugs where staleness check returned stale → `STALE_FEATURE_KNOWLEDGE_SLUGS`. +{knowledge_load()} ### Phase 2: Implement @@ -113,16 +109,16 @@ Based on Setup context (plan document, issue body, or conversation context), use ``` Agent(subagent_type="Coder"): -"TASK_ID: {task-id} -TASK_DESCRIPTION: {description} -BASE_BRANCH: {base branch} -EXECUTION_PLAN: {full plan from setup context} -PATTERNS: {patterns from plan document or empty} +"TASK_ID: \{task-id\} +TASK_DESCRIPTION: \{description\} +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'} -FEATURE_KNOWLEDGE: {feature_knowledge} -DECISIONS_CONTEXT: {decisions_context} -PR_DESCRIPTION_GUIDANCE: {pr_description_guidance}" +DOMAIN: \{detected domain or 'fullstack'\} +FEATURE_KNOWLEDGE: \{feature_knowledge\} +DECISIONS_CONTEXT: \{decisions_context\} +PR_DESCRIPTION_GUIDANCE: \{pr_description_guidance\}" ``` --- @@ -134,40 +130,40 @@ Spawn Coders one at a time, passing handoff summaries between phases: **Phase 1 Coder:** ``` Agent(subagent_type="Coder"): -"TASK_ID: {task-id} -TASK_DESCRIPTION: {phase 1 description} -BASE_BRANCH: {base branch} -EXECUTION_PLAN: {phase 1 steps} -PATTERNS: {patterns from plan document or empty} +"TASK_ID: \{task-id\} +TASK_DESCRIPTION: \{phase 1 description\} +BASE_BRANCH: \{base branch\} +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} -DECISIONS_CONTEXT: {decisions_context} -PR_DESCRIPTION_GUIDANCE: {pr_description_guidance} +DOMAIN: \{phase 1 domain, e.g., 'backend'\} +FEATURE_KNOWLEDGE: \{feature_knowledge\} +DECISIONS_CONTEXT: \{decisions_context\} +PR_DESCRIPTION_GUIDANCE: \{pr_description_guidance\} HANDOFF_REQUIRED: true -HANDOFF_FILE: .devflow/docs/handoff-{branch_slug}.md" +HANDOFF_FILE: .devflow/docs/handoff-\{branch_slug\}.md" ``` **Phase 2+ Coders** (after prior phase completes): ``` Agent(subagent_type="Coder"): -"TASK_ID: {task-id} -TASK_DESCRIPTION: {phase N description} -BASE_BRANCH: {base branch} -EXECUTION_PLAN: {phase N steps} -PATTERNS: {patterns from plan document or empty} -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} -DECISIONS_CONTEXT: {decisions_context} -PR_DESCRIPTION_GUIDANCE: {pr_description_guidance} -HANDOFF_REQUIRED: {true if not last phase} -HANDOFF_FILE: .devflow/docs/handoff-{branch_slug}.md" +"TASK_ID: \{task-id\} +TASK_DESCRIPTION: \{phase N description\} +BASE_BRANCH: \{base branch\} +EXECUTION_PLAN: \{phase N steps\} +PATTERNS: \{patterns from plan document or empty\} +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\} +DECISIONS_CONTEXT: \{decisions_context\} +PR_DESCRIPTION_GUIDANCE: \{pr_description_guidance\} +HANDOFF_REQUIRED: \{true if not last phase\} +HANDOFF_FILE: .devflow/docs/handoff-\{branch_slug\}.md" ``` -**Handoff Protocol**: Each sequential Coder receives the prior Coder's implementation summary via PRIOR_PHASE_SUMMARY and FILES_FROM_PRIOR_PHASE. The Coder's built-in branch orientation step handles git log scanning, file reading, and pattern discovery automatically. After each Coder with HANDOFF_REQUIRED=true completes, write its phase summary to `.devflow/docs/handoff-{branch_slug}.md` using the Write tool (survives context compaction). Delete `.devflow/docs/handoff-{branch_slug}.md` after the final Coder completes (cleanup). +**Handoff Protocol**: Each sequential Coder receives the prior Coder's implementation summary via PRIOR_PHASE_SUMMARY and FILES_FROM_PRIOR_PHASE. The Coder's built-in branch orientation step handles git log scanning, file reading, and pattern discovery automatically. After each Coder with HANDOFF_REQUIRED=true completes, write its phase summary to `.devflow/docs/handoff-\{branch_slug\}.md` using the Write tool (survives context compaction). Delete `.devflow/docs/handoff-\{branch_slug\}.md` after the final Coder completes (cleanup). --- @@ -177,28 +173,28 @@ Spawn multiple Coders **in a single message**, each with independent subtask: ``` Agent(subagent_type="Coder"): # Coder 1 -"TASK_ID: {task-id}-part1 -TASK_DESCRIPTION: {independent subtask 1} -BASE_BRANCH: {base branch} -EXECUTION_PLAN: {subtask 1 steps} -PATTERNS: {patterns} +"TASK_ID: \{task-id\}-part1 +TASK_DESCRIPTION: \{independent subtask 1\} +BASE_BRANCH: \{base branch\} +EXECUTION_PLAN: \{subtask 1 steps\} +PATTERNS: \{patterns\} CREATE_PR: false -DOMAIN: {subtask 1 domain} -FEATURE_KNOWLEDGE: {feature_knowledge} -DECISIONS_CONTEXT: {decisions_context} -PR_DESCRIPTION_GUIDANCE: {pr_description_guidance}" +DOMAIN: \{subtask 1 domain\} +FEATURE_KNOWLEDGE: \{feature_knowledge\} +DECISIONS_CONTEXT: \{decisions_context\} +PR_DESCRIPTION_GUIDANCE: \{pr_description_guidance\}" Agent(subagent_type="Coder"): # Coder 2 (same message) -"TASK_ID: {task-id}-part2 -TASK_DESCRIPTION: {independent subtask 2} -BASE_BRANCH: {base branch} -EXECUTION_PLAN: {subtask 2 steps} -PATTERNS: {patterns} +"TASK_ID: \{task-id\}-part2 +TASK_DESCRIPTION: \{independent subtask 2\} +BASE_BRANCH: \{base branch\} +EXECUTION_PLAN: \{subtask 2 steps\} +PATTERNS: \{patterns\} CREATE_PR: false -DOMAIN: {subtask 2 domain} -FEATURE_KNOWLEDGE: {feature_knowledge} -DECISIONS_CONTEXT: {decisions_context} -PR_DESCRIPTION_GUIDANCE: {pr_description_guidance}" +DOMAIN: \{subtask 2 domain\} +FEATURE_KNOWLEDGE: \{feature_knowledge\} +DECISIONS_CONTEXT: \{decisions_context\} +PR_DESCRIPTION_GUIDANCE: \{pr_description_guidance\}" ``` **Independence criteria** (all must be true for PARALLEL_CODERS): @@ -216,7 +212,7 @@ After Coder completes, spawn Validator to verify correctness: ``` Agent(subagent_type="Validator", model="haiku"): -"FILES_CHANGED: {list of files from Coder output} +"FILES_CHANGED: \{list of files from Coder output\} VALIDATION_SCOPE: full Run build, typecheck, lint, test. Report pass/fail with failure details." ``` @@ -228,10 +224,10 @@ Run build, typecheck, lint, test. Report pass/fail with failure details." - Spawn Coder with fix context: ``` Agent(subagent_type="Coder"): - "TASK_ID: {task-id} + "TASK_ID: \{task-id\} TASK_DESCRIPTION: Fix validation failures OPERATION: validation-fix - VALIDATION_FAILURES: {parsed failures from Validator} + VALIDATION_FAILURES: \{parsed failures from Validator\} SCOPE: Fix only the listed failures, no other changes CREATE_PR: false" ``` @@ -250,8 +246,8 @@ After validation passes, spawn Simplifier to polish the code: ``` Agent(subagent_type="Simplifier"): "Simplify recently implemented code -Task: {task description} -FILES_CHANGED: {list of files from Coder output} +Task: \{task description\} +FILES_CHANGED: \{list of files from Coder output\} Focus on code modified by Coder, apply project standards, enhance clarity" ``` @@ -264,10 +260,10 @@ 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} -DECISIONS_CONTEXT: {decisions_context} -FEATURE_KNOWLEDGE: {feature_knowledge} +"TASK_DESCRIPTION: \{task description\} +FILES_CHANGED: \{list of files from Coder output\} +DECISIONS_CONTEXT: \{decisions_context\} +FEATURE_KNOWLEDGE: \{feature_knowledge\} Evaluate 9 pillars, fix P0/P1 issues, report status" ``` @@ -282,7 +278,7 @@ If Scrutinizer made code changes (status: FIXED), spawn Validator to verify: ``` Agent(subagent_type="Validator", model="haiku"): -"FILES_CHANGED: {files modified by Scrutinizer} +"FILES_CHANGED: \{files modified by Scrutinizer\} VALIDATION_SCOPE: changed-only Verify Scrutinizer's fixes didn't break anything." ``` @@ -300,11 +296,11 @@ After Scrutinizer passes (and re-validation if needed), spawn Evaluator to valid ``` Agent(subagent_type="Evaluator"): -"ORIGINAL_REQUEST: {task description or issue content} -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} +"ORIGINAL_REQUEST: \{task description or issue content\} +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." ``` @@ -317,17 +313,17 @@ Validate alignment with request and plan. Report ALIGNED or MISALIGNED with deta - Spawn Coder to fix misalignments: ``` Agent(subagent_type="Coder"): - "TASK_ID: {task-id} + "TASK_ID: \{task-id\} TASK_DESCRIPTION: Fix alignment issues OPERATION: alignment-fix - MISALIGNMENTS: {structured misalignments from Evaluator} + MISALIGNMENTS: \{structured misalignments from Evaluator\} SCOPE: Fix only the listed misalignments, no other changes CREATE_PR: false" ``` - Spawn Validator to verify fix didn't break tests: ``` Agent(subagent_type="Validator", model="haiku"): - "FILES_CHANGED: {files modified by fix Coder} + "FILES_CHANGED: \{files modified by fix Coder\} VALIDATION_SCOPE: changed-only" ``` - If Validator FAIL: Report to user @@ -343,10 +339,10 @@ After Evaluator passes, spawn Tester for scenario-based acceptance testing: ``` Agent(subagent_type="Tester"): -"ORIGINAL_REQUEST: {task description or issue content} -EXECUTION_PLAN: {execution plan from Phase 1} -FILES_CHANGED: {list of files from Coder output} -ACCEPTANCE_CRITERIA: {extracted criteria if available} +"ORIGINAL_REQUEST: \{task description or issue content\} +EXECUTION_PLAN: \{execution plan from Phase 1\} +FILES_CHANGED: \{list of files from Coder output\} +ACCEPTANCE_CRITERIA: \{extracted criteria if available\} Design and execute scenario-based acceptance tests. Report PASS or FAIL with evidence." ``` @@ -359,17 +355,17 @@ Design and execute scenario-based acceptance tests. Report PASS or FAIL with evi - Spawn Coder to fix QA failures: ``` Agent(subagent_type="Coder"): - "TASK_ID: {task-id} + "TASK_ID: \{task-id\} TASK_DESCRIPTION: Fix QA test failures OPERATION: qa-fix - QA_FAILURES: {structured failures from Tester} + QA_FAILURES: \{structured failures from Tester\} SCOPE: Fix only the listed failures, no other changes CREATE_PR: false" ``` - Spawn Validator to verify fix didn't break tests: ``` Agent(subagent_type="Validator", model="haiku"): - "FILES_CHANGED: {files modified by fix Coder} + "FILES_CHANGED: \{files modified by fix Coder\} VALIDATION_SCOPE: changed-only" ``` - If Validator FAIL: Report to user @@ -407,75 +403,9 @@ If `PR_DESCRIPTION_GUIDANCE` is not `(none)`, use it to compose the PR body (see **Requires:** VALIDATION_RESULT, ALIGNMENT_RESULT, QA_RESULT, PR_URL -Compute overlapping feature knowledge: -```bash -OVERLAPPING_SLUGS=$(node ~/.devflow/scripts/hooks/lib/feature-knowledge.cjs find-overlapping "{worktree}" {files_changed...} 2>/dev/null) -``` -Parse the JSON array output. Pass `OVERLAPPING_SLUGS` to Phase 12. - Display completion summary with phase status, PR info, and next steps. -### Phase 12: Feature Knowledge Generation (Conditional) - -**Requires:** FILES_CHANGED, STALE_FEATURE_KNOWLEDGE_SLUGS, OVERLAPPING_SLUGS, DECISIONS_CONTEXT -**Produces:** Updated `.devflow/features/index.json` (or skipped) - -If `.devflow/features/.disabled` exists, skip entirely. - -**New feature knowledge creation**: If FILES_CHANGED touch a feature area that does NOT have matching feature knowledge in `.devflow/features/index.json`: - -**Slug derivation**: Derive the slug from the primary directory name using kebab-case. Examples: `src/cli/commands/` → `cli-commands`, `src/payments/stripe/` → `payments-stripe`, `scripts/hooks/` → `hooks`. Strip common prefixes like `src/` and `lib/`. The slug must match `^[a-z0-9][a-z0-9-]*$`. - -1. Identify the feature area slug and human-readable name from the implemented directories -2. Spawn Agent(subagent_type="Knowledge"): - ``` - "FEATURE_SLUG: {slug} - FEATURE_NAME: {name} - FILES_CHANGED: {files_changed list} - DIRECTORIES: {directory prefixes from FILES_CHANGED} - DECISIONS_CONTEXT: {from Phase 1} - - Load the devflow:feature-knowledge skill and follow its 4-phase process exactly. - Read the FILES_CHANGED to understand the implemented code. - Read .devflow/features/index.json to see existing feature knowledge for cross-referencing." - ``` -3. Read result file (`.devflow/features/{slug}/.create-result.json`), then run: - ```bash - node ~/.devflow/scripts/hooks/lib/feature-knowledge.cjs update-index "{worktree}" \ - --slug="{slug}" --name="{name}" \ - --directories='["{dir1}", "{dir2}"]' \ - --referencedFiles='{referencedFiles_json_from_result}' \ - --description="{description_from_result}" \ - --createdBy="implement" 2>/dev/null - ``` - Clean up: `rm -f .devflow/features/{slug}/.create-result.json` - If the result file does not exist (agent failed to write it), use empty defaults: - `referencedFiles='[]'`, `description=""`. -4. Report: "Created feature knowledge: {slug}" - -Skip if all touched areas already have matching feature knowledge. - -**Refresh stale feature knowledge**: Combine STALE_FEATURE_KNOWLEDGE_SLUGS (from Phase 1) and OVERLAPPING_SLUGS (from Phase 11), deduplicate. For each slug, refresh: - -1. Read `.devflow/features/{slug}/KNOWLEDGE.md` and index entry -2. Spawn Agent(subagent_type="Knowledge"): - ``` - "FEATURE_SLUG: {slug} - FEATURE_NAME: {name from index} - DIRECTORIES: {directories from index} - EXISTING_FEATURE_KNOWLEDGE: {content of .devflow/features/{slug}/KNOWLEDGE.md} - CHANGED_FILES: {FILES_CHANGED that overlap this feature knowledge} - DECISIONS_CONTEXT: {from Phase 1} - - Load the devflow:feature-knowledge skill. This is a REFRESH, not a new creation. - Read the CHANGED_FILES to understand what changed, then update the EXISTING_FEATURE_KNOWLEDGE. - Maintain quality standards from the skill. Do NOT regenerate from scratch. - Write updated feature knowledge to .devflow/features/{slug}/KNOWLEDGE.md - Write .devflow/features/{slug}/.refresh-result.json with referencedFiles and description." - ``` -3. Read result file, update index (same CLI call as step 3 above), clean up the result file. - -**Failure handling**: Non-blocking. If Knowledge agent crashes, log failure and report results normally. +{knowledge_writeback()} ## Architecture @@ -525,8 +455,8 @@ Skip if all touched areas already have matching feature knowledge. │ ├─ Phase 11: Report │ -└─ Phase 12: Feature Knowledge Generation (Conditional) - └─ Knowledge agent (if new/stale feature area) +└─ Feature Knowledge Write-Back (Conditional) + └─ Knowledge agent (if documented area changed AND knowledge enabled) ``` ## Principles diff --git a/plugins/devflow-plan/commands/plan.md b/shared/knowledge/plan.mds similarity index 89% rename from plugins/devflow-plan/commands/plan.md rename to shared/knowledge/plan.mds index 93b7c3a9..688101b0 100644 --- a/plugins/devflow-plan/commands/plan.md +++ b/shared/knowledge/plan.mds @@ -1,6 +1,7 @@ --- description: Unified design planning - combines requirements discovery, gap analysis, implementation planning, and design review into a single workflow --- +@import { knowledge_load } from "./_knowledge.mds" # Plan Command @@ -79,7 +80,7 @@ Spawn Skimmer agent for codebase context: ``` Agent(subagent_type="Skimmer"): -"Orient in codebase for design planning: {feature/issues} +"Orient in codebase for design planning: \{feature/issues\} Run rskim on source directories (NOT repo root) to identify: - Existing patterns and conventions in the affected area - File structure and module boundaries @@ -96,11 +97,7 @@ DECISIONS_CONTEXT=$(node ~/.devflow/scripts/hooks/lib/decisions-index.cjs index This produces a compact index of active ADR/PF entries. Pass Skimmer context and `DECISIONS_CONTEXT` to all subsequent agents — prior decisions constrain design, known pitfalls inform gap analysis. Agents use `devflow:apply-decisions` to Read full entry bodies on demand. -**Load Feature Knowledge:** -1. Read `.devflow/features/index.json` if it exists -2. Based on the planning task description, identify relevant feature knowledge -3. For each match: check staleness via `node ~/.devflow/scripts/hooks/lib/feature-knowledge.cjs stale "{worktree}" {slug} 2>/dev/null`, read `.devflow/features/{slug}/KNOWLEDGE.md` -4. Concatenate as `FEATURE_KNOWLEDGE` (or `(none)` if no feature knowledge exists or none are relevant) +{knowledge_load()} Pass `FEATURE_KNOWLEDGE` alongside `DECISIONS_CONTEXT` to Explorer and Designer agents. @@ -127,9 +124,9 @@ Spawn 4 Explore agents **in a single message**, each with Skimmer context, `DECI ``` Agent(subagent_type="Synthesizer"): -"Synthesize EXPLORATION outputs for: {feature/issues} +"Synthesize EXPLORATION outputs for: \{feature/issues\} Mode: exploration -Explorer outputs: {all 4 outputs} +Explorer outputs: \{all 4 outputs\} Combine into: user needs, similar features, constraints, failure modes" ``` @@ -169,13 +166,13 @@ Each designer receives: ``` Agent(subagent_type="Designer"): "Mode: gap-analysis -Focus: {completeness|architecture|security|performance|consistency|dependencies} -DECISIONS_CONTEXT: {decisions_context} -FEATURE_KNOWLEDGE: {feature_knowledge} +Focus: \{completeness|architecture|security|performance|consistency|dependencies\} +DECISIONS_CONTEXT: \{decisions_context\} +FEATURE_KNOWLEDGE: \{feature_knowledge\} Artifacts: - Feature/Issues: {feature description or issue bodies} - Exploration synthesis: {Phase 4 output} - Codebase context: {Phase 2 output} + Feature/Issues: \{feature description or issue bodies\} + Exploration synthesis: \{Phase 4 output\} + Codebase context: \{Phase 2 output\} Analyze only your assigned focus area. Follow devflow:apply-decisions for DECISIONS_CONTEXT. Cite evidence from provided artifacts." ``` @@ -189,9 +186,9 @@ Cite evidence from provided artifacts." ``` Agent(subagent_type="Synthesizer"): -"Synthesize GAP ANALYSIS outputs for: {feature/issues} +"Synthesize GAP ANALYSIS outputs for: \{feature/issues\} Mode: design -Designer outputs: {all designer outputs} +Designer outputs: \{all designer outputs\} Deduplicate, boost confidence for multi-agent flags, categorize by severity." ``` @@ -251,9 +248,9 @@ Spawn 4 Explore agents **in a single message**, each with Skimmer context + acce ``` Agent(subagent_type="Synthesizer"): -"Synthesize IMPLEMENTATION EXPLORATION outputs for: {feature/issues} +"Synthesize IMPLEMENTATION EXPLORATION outputs for: \{feature/issues\} Mode: exploration -Explorer outputs: {all 4 outputs} +Explorer outputs: \{all 4 outputs\} Combine into: patterns to follow, integration points, reusable code, edge cases" ``` @@ -281,9 +278,9 @@ Implementation steps planner: include explicit gap mitigations (from Phase 6) in ``` Agent(subagent_type="Synthesizer"): -"Synthesize PLANNING outputs for: {feature/issues} +"Synthesize PLANNING outputs for: \{feature/issues\} Mode: planning -Planner outputs: {all 3 outputs} +Planner outputs: \{all 3 outputs\} Combine into: execution plan with strategy decision, gap mitigations integrated" ``` @@ -302,9 +299,9 @@ Spawn 1 Designer agent with mode `design-review`: Agent(subagent_type="Designer"): "Mode: design-review Artifacts: - Implementation plan: {Phase 11 planning synthesis} - Implementation exploration: {Phase 9 exploration synthesis} - Codebase context: {Phase 2 output} + Implementation plan: \{Phase 11 planning synthesis\} + Implementation exploration: \{Phase 9 exploration synthesis\} + Codebase context: \{Phase 2 output\} Review the full plan for all 6 anti-patterns. Report all findings with evidence." ``` @@ -348,9 +345,9 @@ User can: **Store design artifact:** Write design artifact to disk: -- If issue number: `.devflow/docs/design/{issue-number}-{topic-slug}.{YYYY-MM-DD_HHMM}.md` -- If multi-issue: `.devflow/docs/design/{first-issue-number}-multi.{YYYY-MM-DD_HHMM}.md` -- If no issue: `.devflow/docs/design/{topic-slug}.{YYYY-MM-DD_HHMM}.md` +- If issue number: `.devflow/docs/design/\{issue-number\}-\{topic-slug\}.\{YYYY-MM-DD_HHMM\}.md` +- If multi-issue: `.devflow/docs/design/\{first-issue-number\}-multi.\{YYYY-MM-DD_HHMM\}.md` +- If no issue: `.devflow/docs/design/\{topic-slug\}.\{YYYY-MM-DD_HHMM\}.md` Create parent directory if needed. @@ -390,16 +387,16 @@ Required sections: ## PR Description Guidance ### Problem Being Solved -{1-2 sentences: the "why" behind this change} +\{1-2 sentences: the "why" behind this change\} ### Key Changes to Highlight -{bulleted list: user-facing framing of what changed} +\{bulleted list: user-facing framing of what changed\} ### Breaking Changes -{from gap analysis, or "None expected"} +\{from gap analysis, or "None expected"\} ### Reviewer Focus Areas -{areas needing careful review, with reasons} +\{areas needing careful review, with reasons\} ``` **Create GitHub issue (optional):** @@ -419,7 +416,7 @@ Display completion summary: - Issue URL (if created or if pre-existing) - Gap analysis summary (N blocking, M should-address) - Design review summary (N anti-patterns found, M mitigated in plan) -- Suggested next step: `/implement {artifact-path}` or `/implement #{issue-number}` +- Suggested next step: `/implement \{artifact-path\}` or `/implement #\{issue-number\}` --- diff --git a/plugins/devflow-research/commands/research.md b/shared/knowledge/research.mds similarity index 80% rename from plugins/devflow-research/commands/research.md rename to shared/knowledge/research.mds index 871bfbd3..1baa9796 100644 --- a/plugins/devflow-research/commands/research.md +++ b/shared/knowledge/research.mds @@ -1,6 +1,7 @@ --- description: Research a topic using parallel multi-type researchers with trust-aware synthesis --- +@import { knowledge_load } from "./_knowledge.mds" # Research Command @@ -36,11 +37,9 @@ DECISIONS_CONTEXT=$(node ~/.devflow/scripts/hooks/lib/decisions-index.cjs index Use `DECISIONS_CONTEXT` locally when framing research — prior decisions and pitfalls suggest areas to investigate. Follow `devflow:apply-decisions` to Read full entry bodies on demand. Pass `DECISIONS_CONTEXT` to each Researcher agent in Phase 4 so they can cite relevant decisions in findings. -Also load feature knowledge: -1. Read `.devflow/features/index.json` if it exists. If not, set `FEATURE_KNOWLEDGE = (none)`. -2. Identify relevant feature knowledge entries (match research question against each entry's descriptions and directories). -3. For each match: check staleness via `node ~/.devflow/scripts/hooks/lib/feature-knowledge.cjs stale "{worktree}" {slug} 2>/dev/null`, read `.devflow/features/{slug}/KNOWLEDGE.md`. -4. Use `FEATURE_KNOWLEDGE` **locally** for research framing. Pass to each Researcher agent in Phase 4. +{knowledge_load()} + +Use `FEATURE_KNOWLEDGE` **locally** for research framing. Pass to each Researcher agent in Phase 4. ### Phase 2: Requirements @@ -49,7 +48,7 @@ Also load feature knowledge: Analyze the research question to infer research types needed (min 2, max 5). For each type: - `RESEARCH_TYPE`: `codebase | external | market | competitor | technology` - `RESEARCH_QUESTION`: Focused sub-question for this type -- `OUTPUT_PATH`: `.devflow/docs/research/{topic-slug}/{YYYY-MM-DD_HHMM}/{type}.md` +- `OUTPUT_PATH`: `.devflow/docs/research/\{topic-slug\}/\{YYYY-MM-DD_HHMM\}/\{type\}.md` **Tool availability check**: If WebSearch/WebFetch are unavailable, restrict to `codebase` type only. @@ -91,7 +90,7 @@ Spawn `Agent(subagent_type="Synthesizer")` in `research` mode: - Merges findings with trust-aware aggregation - Writes `research-summary.md` to the same timestamped directory -Output path: `.devflow/docs/research/{topic-slug}/{timestamp}/research-summary.md` +Output path: `.devflow/docs/research/\{topic-slug\}/\{timestamp\}/research-summary.md` ### Phase 6: Present @@ -109,13 +108,11 @@ If external research was skipped due to tool unavailability: inform user. **Requires:** RESEARCH_SUMMARY, DECISIONS_CONTEXT **Produces:** FEATURE_KNOWLEDGE_STATUS (created | skipped) -1. If `.devflow/features/.disabled` exists → skip -2. If `codebase` type was not in RESEARCH_PLAN → skip -3. Read `.devflow/features/index.json` (if it exists) -4. Check if matching feature knowledge already exists. If covered → skip -5. Use AskUserQuestion: "No feature knowledge exists for {researched area}. Create one?" -6. If user accepts: spawn Knowledge agent, update index -7. Set FEATURE_KNOWLEDGE_STATUS = created or skipped +1. If `codebase` type was not in RESEARCH_PLAN → skip +2. Check if matching feature knowledge already exists by reading `\{worktree\}/.devflow/features/index.md` (or globbing frontmatter if absent). If covered → skip +3. Use AskUserQuestion: "No feature knowledge exists for \{researched area\}. Create one?" +4. If user accepts: spawn `Agent(subagent_type="Knowledge")` with researched area context + worktree root, instructing it to load `devflow:feature-knowledge`, write `KNOWLEDGE.md`, and update `index.md` directly (no `.create-result.json`) +5. Set FEATURE_KNOWLEDGE_STATUS = created or skipped **Failure handling**: Non-blocking. If Knowledge agent fails, log and continue. @@ -125,8 +122,8 @@ If the orchestrator receives a `WORKTREE_PATH` context (e.g., from multi-worktre ## Output -Research findings saved to `.devflow/docs/research/{topic-slug}/{YYYY-MM-DD_HHMM}/`: -- `{type}.md` per research type (codebase.md, external.md, etc.) +Research findings saved to `.devflow/docs/research/\{topic-slug\}/\{YYYY-MM-DD_HHMM\}/`: +- `\{type\}.md` per research type (codebase.md, external.md, etc.) - `research-summary.md` — synthesized findings with trust annotations ## Architecture diff --git a/plugins/devflow-resolve/commands/resolve.md b/shared/knowledge/resolve.mds similarity index 81% rename from plugins/devflow-resolve/commands/resolve.md rename to shared/knowledge/resolve.mds index cf2a3d4c..10c5a1dd 100644 --- a/plugins/devflow-resolve/commands/resolve.md +++ b/shared/knowledge/resolve.mds @@ -1,6 +1,7 @@ --- description: Process review issues - validate, assess risk, fix low-risk issues, defer high-risk to tech debt --- +@import { knowledge_load, knowledge_writeback } from "./_knowledge.mds" # Resolve Command @@ -30,7 +31,7 @@ Process issues from code review reports: validate them (false positive check), a 2. **If `--path` flag provided:** use only that worktree, skip discovery **`--path` validation**: Before proceeding, verify the path exists as a directory and appears in `git worktree list` output. If not: report error and stop. 3. **If only 1 resolvable worktree** (the common case): proceed as single-worktree flow — zero behavior change -4. **If multiple resolvable worktrees:** report "Found N worktrees with unresolved reviews: {list}" and proceed with multi-worktree flow +4. **If multiple resolvable worktrees:** report "Found N worktrees with unresolved reviews: \{list\}" and proceed with multi-worktree flow #### Step 0b: Per-Worktree Pre-Flight (Git Agent) @@ -42,7 +43,7 @@ For each resolvable worktree, spawn Git agent: ``` Agent(subagent_type="Git", run_in_background=false): "OPERATION: validate-branch -WORKTREE_PATH: {worktree_path} (omit if cwd) +WORKTREE_PATH: \{worktree_path\} (omit if cwd) Check feature branch, clean working directory, reviews exist. Return: branch, branch-slug, PR#, review count" ``` @@ -55,7 +56,7 @@ In multi-worktree mode, spawn all pre-flight agents **in a single message** (par **Fetch PR body** (after extracting `pr_number`): ```bash -PR_DESCRIPTION=$(gh pr view {pr_number} --json body --jq '.body' 2>/dev/null || echo "(none)") +PR_DESCRIPTION=$(gh pr view \{pr_number\} --json body --jq '.body' 2>/dev/null || echo "(none)") ``` If `pr_number` is absent or the command fails, set `PR_DESCRIPTION` to `(none)`. @@ -66,14 +67,14 @@ If `pr_number` is absent or the command fails, set `PR_DESCRIPTION` to `(none)`. For each worktree: -1. List directories in `{worktree}/.devflow/docs/reviews/{branch-slug}/` -2. **If `--review {timestamp}` provided:** use that specific directory (not supported in multi-worktree mode) +1. List directories in `\{worktree\}/.devflow/docs/reviews/\{branch-slug\}/` +2. **If `--review \{timestamp\}` provided:** use that specific directory (not supported in multi-worktree mode) 3. **Otherwise:** sort directories by name descending (timestamps are naturally sortable), scan the 10 most recent directories only. Select the first that contains `review-summary.md` (complete review) 4. **If latest directory already has `resolution-summary.md`:** the review is resolved — check bug-analysis fallback (step 5b). -5. **Legacy fallback:** if no timestamped subdirectories exist but flat `*.md` files do in `{worktree}/.devflow/docs/reviews/{branch-slug}/`, read them directly (backwards compatible). +5. **Legacy fallback:** if no timestamped subdirectories exist but flat `*.md` files do in `\{worktree\}/.devflow/docs/reviews/\{branch-slug\}/`, read them directly (backwards compatible). **5b. Bug analysis fallback** — if no qualifying review directory found (no reviews exist, or all resolved): -- List directories in `{worktree}/.devflow/docs/bug-analysis/{branch-slug}/` +- List directories in `\{worktree\}/.devflow/docs/bug-analysis/\{branch-slug\}/` - Sort by name descending (timestamps are naturally sortable), scan the 10 most recent directories only. Select the latest that: - Contains at least one focus report (`security.md`, `functional.md`, `integration.md`, or `usability.md`) - Does NOT contain a `resolution-summary.md` @@ -94,11 +95,7 @@ DECISIONS_CONTEXT=$(node ~/.devflow/scripts/hooks/lib/decisions-index.cjs index This produces 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 `DECISIONS_CONTEXT` to every Resolver agent in Phase 4. Resolver agents use `devflow:apply-decisions` to Read full entry bodies on demand — no fan-out of the full corpus. -**Load Feature Knowledge:** -1. Read `.devflow/features/index.json` if it exists -2. Based on file paths from review report issue entries, identify relevant feature knowledge -3. For each match: check staleness via `node ~/.devflow/scripts/hooks/lib/feature-knowledge.cjs stale "{worktree}" {slug} 2>/dev/null`, read `.devflow/features/{slug}/KNOWLEDGE.md` -4. Set `FEATURE_KNOWLEDGE` (or `(none)` if no feature knowledge exists or none are relevant) +{knowledge_load()} Pass `FEATURE_KNOWLEDGE` to every Resolver agent in Phase 4. @@ -107,7 +104,7 @@ Pass `FEATURE_KNOWLEDGE` to every Resolver agent in Phase 4. **Produces:** ISSUES **Requires:** TARGET_DIR -Read review reports from `{TARGET_DIR}/*.md` and extract: +Read review reports from `\{TARGET_DIR\}/*.md` and extract: **Exclude from issue extraction:** - `review-summary.md` (synthesizer output, not individual findings) @@ -117,7 +114,7 @@ Read review reports from `{TARGET_DIR}/*.md` and extract: **Include:** ALL issues from all categories and severities, including Suggestions. -Issues are extracted from `{TARGET_DIR}` only — never cross-reference reviews from other worktrees. +Issues are extracted from `\{TARGET_DIR\}` only — never cross-reference reviews from other worktrees. **Extract per issue:** - `id`: Generated from file:line:type @@ -161,13 +158,13 @@ Spawn Resolver agents based on dependency analysis. For independent batches, spa ``` Agent(subagent_type="Resolver"): -"ISSUES: [{issue1}, {issue2}, ...] -BRANCH: {branch-slug} -BATCH_ID: batch-{n} -WORKTREE_PATH: {worktree_path} (omit if cwd) -DECISIONS_CONTEXT: {decisions_context} -FEATURE_KNOWLEDGE: {feature_knowledge} -PR_DESCRIPTION: {pr_description} +"ISSUES: [\{issue1\}, \{issue2\}, ...] +BRANCH: \{branch-slug\} +BATCH_ID: batch-\{n\} +WORKTREE_PATH: \{worktree_path\} (omit if cwd) +DECISIONS_CONTEXT: \{decisions_context\} +FEATURE_KNOWLEDGE: \{feature_knowledge\} +PR_DESCRIPTION: \{pr_description\} Validate, decide FIX vs TECH_DEBT, implement fixes. Follow devflow:apply-decisions to Read full ADR/PF bodies on demand. Follow devflow:apply-feature-knowledge for FEATURE_KNOWLEDGE." ``` @@ -191,7 +188,7 @@ Aggregate from all Resolvers: Extract all decisions citations from Resolver Reasoning columns. Collect unique `applies ADR-NNN` and `avoids PF-NNN` references across all batches. -**Immediately write `resolution-summary.md`** to `{TARGET_DIR}` using the Write tool. Do this now — not in Phase 9 — while the aggregated results are fresh in context. Use the template from the Output Artifact section below. This ensures the resolution record is persisted even if later phases (Simplify, Tech Debt) trigger context compaction. +**Immediately write `resolution-summary.md`** to `\{TARGET_DIR\}` using the Write tool. Do this now — not in Phase 9 — while the aggregated results are fresh in context. Use the template from the Output Artifact section below. This ensures the resolution record is persisted even if later phases (Simplify, Tech Debt) trigger context compaction. ### Phase 6: Simplify @@ -203,8 +200,8 @@ If any fixes were made, spawn Simplifier agent to refine the changed code: ``` Agent(subagent_type="Simplifier", run_in_background=false): "TASK_DESCRIPTION: Issue resolution fixes -WORKTREE_PATH: {worktree_path} (omit if cwd) -FILES_CHANGED: {list of files modified by Resolvers} +WORKTREE_PATH: \{worktree_path\} (omit if cwd) +FILES_CHANGED: \{list of files modified by Resolvers\} Simplify and refine the fixes for clarity and consistency" ``` @@ -238,9 +235,9 @@ If any issues were deferred, spawn Git agent: ``` Agent(subagent_type="Git"): "OPERATION: manage-debt -WORKTREE_PATH: {worktree_path} (omit if cwd) -REVIEW_DIR: {TARGET_DIR} -TIMESTAMP: {timestamp} +WORKTREE_PATH: \{worktree_path\} (omit if cwd) +REVIEW_DIR: \{TARGET_DIR\} +TIMESTAMP: \{timestamp\} Note: Deferred issues from resolution are already in resolution-summary.md" ``` @@ -248,35 +245,37 @@ Note: Deferred issues from resolution are already in resolution-summary.md" **Requires:** TARGET_DIR -The resolution summary was already written to `{TARGET_DIR}/resolution-summary.md` in Phase 5. Display results to the user: +The resolution summary was already written to `\{TARGET_DIR\}/resolution-summary.md` in Phase 5. Display results to the user: ``` ## Resolution Summary -**Branch**: {branch} -**Reviews Processed**: {n} reports from {TARGET_DIR} -**Total Issues**: {n} +**Branch**: \{branch\} +**Reviews Processed**: \{n\} reports from \{TARGET_DIR\} +**Total Issues**: \{n\} ### Results | Outcome | Count | |---------|-------| -| Fixed | {n} | -| False Positive | {n} | -| Tech Debt | {n} | -| Blocked | {n} | +| Fixed | \{n\} | +| False Positive | \{n\} | +| Tech Debt | \{n\} | +| Blocked | \{n\} | ### Commits Created -- {sha} {message} +- \{sha\} \{message\} ### Tech Debt Added -- {n} items added to backlog +- \{n\} items added to backlog ### Artifacts -- Resolution report: {TARGET_DIR}/resolution-summary.md +- Resolution report: \{TARGET_DIR\}/resolution-summary.md ``` In multi-worktree mode, report results per worktree with aggregate summary. +{knowledge_writeback()} + ## Architecture ``` @@ -329,7 +328,7 @@ In multi-worktree mode, report results per worktree with aggregate summary. | Incomplete review directory (no review-summary.md) | Skip — resolve only targets complete reviews | | Latest review already resolved | Check bug-analysis fallback (Step 0c-5b); if also absent, skip worktree and suggest `/code-review` or `/bug-analysis` | | Legacy flat layout (no subdirectories) | Read flat *.md files directly (backwards compatible) | -| `--review {timestamp}` in multi-worktree mode | Not supported — use `--path` + `--review` to target specific worktree + review | +| `--review \{timestamp\}` in multi-worktree mode | Not supported — use `--path` + `--review` to target specific worktree + review | | Worktree pre-flight fails | Report failure, continue with other worktrees | ## Principles @@ -345,49 +344,49 @@ In multi-worktree mode, report results per worktree with aggregate summary. ## Output Artifact -Written in Phase 5 (Collect Results) to `{TARGET_DIR}/resolution-summary.md`: +Written in Phase 5 (Collect Results) to `\{TARGET_DIR\}/resolution-summary.md`: ```markdown # Resolution Summary -**Branch**: {branch} -> {base} -**Date**: {timestamp} -**Review**: {TARGET_DIR} +**Branch**: \{branch\} -> \{base\} +**Date**: \{timestamp\} +**Review**: \{TARGET_DIR\} **Command**: /resolve ## Decisions Citations -- applies ADR-{NNN} — {batch-id}, {issue-id} -- avoids PF-{NNN} — {batch-id}, {issue-id} +- applies ADR-\{NNN\} — \{batch-id\}, \{issue-id\} +- avoids PF-\{NNN\} — \{batch-id\}, \{issue-id\} (Omit section if no citations were made) ## Statistics | Metric | Value | |--------|-------| -| Total Issues | {n} | -| Fixed | {n} | -| False Positive | {n} | -| Deferred | {n} | -| Blocked | {n} | +| Total Issues | \{n\} | +| Fixed | \{n\} | +| False Positive | \{n\} | +| Deferred | \{n\} | +| Blocked | \{n\} | ## Fixed Issues | Issue | File:Line | Commit | |-------|-----------|--------| -| {description} | {file}:{line} | {sha} | +| \{description\} | \{file\}:\{line\} | \{sha\} | ## False Positives | Issue | File:Line | Reasoning | |-------|-----------|-----------| -| {description} | {file}:{line} | {why} | +| \{description\} | \{file\}:\{line\} | \{why\} | ## Deferred to Tech Debt | Issue | File:Line | Risk Factor | |-------|-----------|-------------| -| {description} | {file}:{line} | {criteria} | +| \{description\} | \{file\}:\{line\} | \{criteria\} | ## Blocked | Issue | File:Line | Blocker | |-------|-----------|---------| -| {description} | {file}:{line} | {why} | +| \{description\} | \{file\}:\{line\} | \{why\} | ``` diff --git a/plugins/devflow-self-review/commands/self-review.md b/shared/knowledge/self-review.mds similarity index 76% rename from plugins/devflow-self-review/commands/self-review.md rename to shared/knowledge/self-review.mds index 240e0049..1347b7fa 100644 --- a/plugins/devflow-self-review/commands/self-review.md +++ b/shared/knowledge/self-review.mds @@ -1,6 +1,7 @@ --- description: Self-review workflow - Simplifier (code clarity) then Scrutinizer (9-pillar quality gate) --- +@import { knowledge_load, knowledge_writeback } from "./_knowledge.mds" # Self-Review Command @@ -24,16 +25,16 @@ Detect changed files and build context: 3. If no changes found, report "No changes to review" and exit 4. Build TASK_DESCRIPTION from recent commit messages or branch name 5. Load decisions index: - ```bash - DECISIONS_CONTEXT=$(node ~/.devflow/scripts/hooks/lib/decisions-index.cjs index "{worktree}" 2>/dev/null || echo "(none)") - ``` + +```bash +DECISIONS_CONTEXT=$(node ~/.devflow/scripts/hooks/lib/decisions-index.cjs index "{worktree}" 2>/dev/null || echo "(none)") +``` + Pass `DECISIONS_CONTEXT` to Scrutinizer — the compact index lists active ADR/PF entries; Scrutinizer uses `devflow:apply-decisions` to Read full entry bodies on demand. Known pitfalls help identify reintroduced issues, prior decisions help validate architectural consistency. (Simplifier does not consume decisions — it operates at code-shape level and Scrutinizer runs after to catch any architectural drift.) -6. Load feature knowledge: - - Read `.devflow/features/index.json` if it exists - - Based on FILES_CHANGED, identify relevant feature knowledge (match file paths against feature knowledge `directories` and `referencedFiles`) - - For each match: check staleness via `node ~/.devflow/scripts/hooks/lib/feature-knowledge.cjs stale "{worktree}" {slug} 2>/dev/null`, read `.devflow/features/{slug}/KNOWLEDGE.md` - - Set `FEATURE_KNOWLEDGE` (or `(none)` if no feature knowledge exists or none are relevant) - - Pass `FEATURE_KNOWLEDGE` to Scrutinizer + +{knowledge_load()} + +Pass `FEATURE_KNOWLEDGE` to Scrutinizer. **Extract:** FILES_CHANGED (list), TASK_DESCRIPTION (string), DECISIONS_CONTEXT (string, optional), FEATURE_KNOWLEDGE (string, optional) @@ -45,8 +46,8 @@ Detect changed files and build context: Spawn Simplifier agent to refine code for clarity and consistency: Agent(subagent_type="Simplifier", run_in_background=false): -"TASK_DESCRIPTION: {task_description} -FILES_CHANGED: {files_changed} +"TASK_DESCRIPTION: \{task_description\} +FILES_CHANGED: \{files_changed\} Simplify and refine the code for clarity and consistency while preserving functionality." **Wait for completion.** Simplifier commits changes directly. @@ -59,10 +60,10 @@ Simplify and refine the code for clarity and consistency while preserving functi Spawn Scrutinizer agent for quality evaluation and fixing: Agent(subagent_type="Scrutinizer", run_in_background=false): -"TASK_DESCRIPTION: {task_description} -FILES_CHANGED: {files_changed} -DECISIONS_CONTEXT: {decisions_context} -FEATURE_KNOWLEDGE: {feature_knowledge} +"TASK_DESCRIPTION: \{task_description\} +FILES_CHANGED: \{files_changed\} +DECISIONS_CONTEXT: \{decisions_context\} +FEATURE_KNOWLEDGE: \{feature_knowledge\} Evaluate against 9-pillar framework. Fix P0/P1 issues. Return structured report. Follow devflow:apply-decisions to scan DECISIONS_CONTEXT and Read full ADR/PF bodies on demand. Skip if (none). Follow devflow:apply-feature-knowledge for FEATURE_KNOWLEDGE. Skip if (none)." @@ -77,7 +78,7 @@ Follow devflow:apply-feature-knowledge for FEATURE_KNOWLEDGE. Skip if (none)." If Scrutinizer made changes (STATUS == FIXED): Agent(subagent_type="Validator", run_in_background=false): -"FILES_CHANGED: {scrutinizer_modified_files} +"FILES_CHANGED: \{scrutinizer_modified_files\} VALIDATION_SCOPE: changed-only Run build, typecheck, lint, test on modified files" @@ -92,29 +93,31 @@ Display summary: ## Self-Review Complete -**Files Reviewed**: {n} -**Status**: {PASS|FIXED|BLOCKED} +**Files Reviewed**: \{n\} +**Status**: \{PASS|FIXED|BLOCKED\} ### Simplifier -- {n} files refined for clarity +- \{n\} files refined for clarity ### Scrutinizer (9-Pillar Evaluation) | Pillar | Status | |--------|--------| -| Design | {status} | -| Functionality | {status} | -| Security | {status} | -| Complexity | {status} | -| Error Handling | {status} | -| Tests | {status} | -| Naming | {status} | -| Consistency | {status} | -| Documentation | {status} | +| Design | \{status\} | +| Functionality | \{status\} | +| Security | \{status\} | +| Complexity | \{status\} | +| Error Handling | \{status\} | +| Tests | \{status\} | +| Naming | \{status\} | +| Consistency | \{status\} | +| Documentation | \{status\} | ### Commits Created -- {sha} {message} +- \{sha\} \{message\} + +\{If BLOCKED: ### Blocking Issue\n\{description\}\} -{If BLOCKED: ### Blocking Issue\n{description}} +{knowledge_writeback()} ## Architecture From fffa1a55a39733f6470f6540b610ed480c646360 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sun, 28 Jun 2026 14:06:00 +0300 Subject: [PATCH 2/5] feat!: delete feature-knowledge maintenance machinery + simplify skills/agent (2/3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WS2: delete eval-knowledge hook, feature-knowledge.cjs CJS library, dream-knowledge skill, knowledge-agent.ts, agent-result.ts (sever references first, then delete). Update session-start-context to drop _SC2_KNOW_EN; drop KNOWLEDGE_ENABLED arg from dream_collect_tasks; make knowledge.* case unconditionally delete stale markers; remove index.json bootstrap from ensure-devflow-init and init.ts. Add devflow:dream-knowledge to LEGACY_SKILLS_V2X for cleanup on upgrade. WS5: update apply-feature-knowledge skill to drop [STALE] handling and strengthen verify-against-code as the freshness mechanism. Update feature-knowledge skill to remove referencedFiles from frontmatter template and Result Output section; replace with direct index.md write instructions (line format: - **{slug}** — {areas} — desc). Rewrite knowledge agent to write KNOWLEDGE.md + index.md directly without any intermediate .create-result.json or external scripts. Test cleanup: delete tests/feature-knowledge/* (5 files, 1072 lines). Remove knowledge assertions from shell-hooks.test.ts (throttle, sentinel, dream-evaluate knowledge block, runCollectTasks knowEnabled param). Update project-paths.test.ts to drop the 4 deleted helper assertions. All 1777 tests pass. TASK_ID: feat/simplify-feature-knowledge --- .../.claude-plugin/plugin.json | 1 - .../devflow-debug/.claude-plugin/plugin.json | 1 + .../.claude-plugin/plugin.json | 1 + .../.claude-plugin/plugin.json | 1 + .../.claude-plugin/plugin.json | 1 + scripts/hooks/dream-collect-tasks | 45 +- scripts/hooks/dream-evaluate | 17 +- scripts/hooks/dream-recover | 10 +- scripts/hooks/ensure-devflow-init | 14 +- scripts/hooks/eval-knowledge | 73 -- scripts/hooks/lib/feature-knowledge.cjs | 652 -------------- scripts/hooks/lib/project-paths.cjs | 24 - scripts/hooks/session-start-context | 16 +- shared/agents/dream.md | 14 +- shared/agents/knowledge.md | 41 +- .../skills/apply-feature-knowledge/SKILL.md | 27 +- shared/skills/dream-knowledge/SKILL.md | 53 -- shared/skills/feature-knowledge/SKILL.md | 28 +- src/cli/commands/init.ts | 29 +- src/cli/commands/knowledge/check.ts | 44 - src/cli/commands/knowledge/create.ts | 85 -- src/cli/commands/knowledge/index.ts | 43 +- src/cli/commands/knowledge/list.ts | 135 ++- src/cli/commands/knowledge/refresh.ts | 101 --- src/cli/commands/knowledge/remove.ts | 29 - src/cli/commands/knowledge/shared.ts | 55 -- src/cli/commands/knowledge/toggle.ts | 85 +- src/cli/plugins.ts | 13 +- src/cli/utils/agent-result.ts | 31 - src/cli/utils/knowledge-agent.ts | 89 -- src/cli/utils/project-paths.ts | 20 - .../apply-feature-knowledge-skill.test.ts | 52 -- .../feature-knowledge.test.ts | 810 ------------------ tests/feature-knowledge/fixtures.ts | 98 --- .../feature-knowledge/knowledge-agent.test.ts | 30 - .../knowledge-command.test.ts | 82 -- tests/project-paths.test.ts | 24 - tests/shell-hooks.test.ts | 154 +--- 38 files changed, 302 insertions(+), 2726 deletions(-) delete mode 100644 scripts/hooks/eval-knowledge delete mode 100644 scripts/hooks/lib/feature-knowledge.cjs delete mode 100644 shared/skills/dream-knowledge/SKILL.md delete mode 100644 src/cli/commands/knowledge/check.ts delete mode 100644 src/cli/commands/knowledge/create.ts delete mode 100644 src/cli/commands/knowledge/refresh.ts delete mode 100644 src/cli/commands/knowledge/remove.ts delete mode 100644 src/cli/commands/knowledge/shared.ts delete mode 100644 src/cli/utils/agent-result.ts delete mode 100644 src/cli/utils/knowledge-agent.ts delete mode 100644 tests/feature-knowledge/apply-feature-knowledge-skill.test.ts delete mode 100644 tests/feature-knowledge/feature-knowledge.test.ts delete mode 100644 tests/feature-knowledge/fixtures.ts delete mode 100644 tests/feature-knowledge/knowledge-agent.test.ts delete mode 100644 tests/feature-knowledge/knowledge-command.test.ts diff --git a/plugins/devflow-core-skills/.claude-plugin/plugin.json b/plugins/devflow-core-skills/.claude-plugin/plugin.json index 8003d6b9..51fda8f3 100644 --- a/plugins/devflow-core-skills/.claude-plugin/plugin.json +++ b/plugins/devflow-core-skills/.claude-plugin/plugin.json @@ -30,7 +30,6 @@ "testing", "dependency-research", "dream-decisions", - "dream-knowledge", "dream-curation" ], "rules": [ diff --git a/plugins/devflow-debug/.claude-plugin/plugin.json b/plugins/devflow-debug/.claude-plugin/plugin.json index ff3caf49..c2c9fcd5 100644 --- a/plugins/devflow-debug/.claude-plugin/plugin.json +++ b/plugins/devflow-debug/.claude-plugin/plugin.json @@ -20,6 +20,7 @@ "skills": [ "git", "worktree-support", + "feature-knowledge", "apply-feature-knowledge" ] } diff --git a/plugins/devflow-implement/.claude-plugin/plugin.json b/plugins/devflow-implement/.claude-plugin/plugin.json index 014536c9..24f4e76d 100644 --- a/plugins/devflow-implement/.claude-plugin/plugin.json +++ b/plugins/devflow-implement/.claude-plugin/plugin.json @@ -30,6 +30,7 @@ "qa", "quality-gates", "worktree-support", + "feature-knowledge", "apply-feature-knowledge" ] } diff --git a/plugins/devflow-resolve/.claude-plugin/plugin.json b/plugins/devflow-resolve/.claude-plugin/plugin.json index 0acec961..0e2f92b9 100644 --- a/plugins/devflow-resolve/.claude-plugin/plugin.json +++ b/plugins/devflow-resolve/.claude-plugin/plugin.json @@ -24,6 +24,7 @@ "patterns", "security", "worktree-support", + "feature-knowledge", "apply-feature-knowledge" ] } diff --git a/plugins/devflow-self-review/.claude-plugin/plugin.json b/plugins/devflow-self-review/.claude-plugin/plugin.json index f302b9ef..d424f5ba 100644 --- a/plugins/devflow-self-review/.claude-plugin/plugin.json +++ b/plugins/devflow-self-review/.claude-plugin/plugin.json @@ -23,6 +23,7 @@ "quality-gates", "software-design", "worktree-support", + "feature-knowledge", "apply-feature-knowledge" ] } diff --git a/scripts/hooks/dream-collect-tasks b/scripts/hooks/dream-collect-tasks index 085054ac..691a861a 100644 --- a/scripts/hooks/dream-collect-tasks +++ b/scripts/hooks/dream-collect-tasks @@ -4,23 +4,24 @@ # # Source this file to get the dream_collect_tasks and dream_build_spawn_directive functions. # -# Function: dream_collect_tasks DREAM_DIR DECISIONS_ENABLED KNOWLEDGE_ENABLED +# Function: dream_collect_tasks DREAM_DIR DECISIONS_ENABLED # # Inputs (positional args): # $1 DREAM_DIR — path to .devflow/dream/ # $2 DECISIONS_ENABLED — "true"/"false" -# $3 KNOWLEDGE_ENABLED — "true"/"false" # # Outputs (exported variable): # _DREAM_TASKS — comma-separated, deduped, sorted list of pending task types. # Empty string if no pending tasks. # NOTE: "memory" never appears — memory markers are unconditionally swept # (background-memory-update worker handles refresh from dream-capture). +# NOTE: "knowledge" never appears — knowledge is handled in-command via +# write-through (knowledge_writeback MDS partial). # # Behaviour: # - Skips config.json (reserved). -# - Pass 1: unconditional sweep — deletes learning.* and memory.* markers always -# (both pipelines removed from Dream subagent); deletes disabled decisions/knowledge +# - Pass 1: unconditional sweep — deletes learning.*, memory.*, and knowledge.* markers +# always (all three pipelines removed from Dream subagent); deletes disabled decisions # markers. When decisions is disabled, curation markers are also swept (curation # depends on decisions data and should not run when decisions is disabled). # Unknown types pass through unchanged. @@ -52,7 +53,6 @@ _derive_marker_type() { dream_collect_tasks() { local dream_dir="${1:?dream_collect_tasks: dream_dir required}" local dec_enabled="${2:-true}" - local know_enabled="${3:-true}" _DREAM_TASKS="" @@ -110,6 +110,14 @@ dream_collect_tasks() { dbg "dream_collect_tasks: deleted stale memory marker: $_base" continue ;; + knowledge) + # Knowledge is no longer a Dream subagent task — write-through (knowledge_writeback + # MDS partial) handles it in-command. Delete any stale knowledge.* markers + # unconditionally (same treatment as learning.* and memory.*). + rm -f "$_f" 2>/dev/null || true + dbg "dream_collect_tasks: deleted stale knowledge marker: $_base" + continue + ;; decisions) if [ "$dec_enabled" != "true" ]; then rm -f "$_f" 2>/dev/null || true @@ -117,13 +125,6 @@ dream_collect_tasks() { continue fi ;; - knowledge) - if [ "$know_enabled" != "true" ]; then - rm -f "$_f" 2>/dev/null || true - dbg "dream_collect_tasks: deleted disabled-feature marker: $_base" - continue - fi - ;; curation) # Curation depends on decisions data — sweep when decisions is disabled # so stray curation markers don't trigger Dream agent spawns when disabled. @@ -230,8 +231,9 @@ _DREAM_DIRECTIVE="" # exact directive bytes (including trailing newlines) survive intact; command # substitution would strip them. # -# Hardcoded task→model map: memory=haiku, knowledge=sonnet, decisions=opus, -# curation=opus. decisions+curation co-pending → exactly ONE opus spawn that +# Hardcoded task→model map: decisions=opus, curation=opus. +# memory and knowledge are no longer Dream tasks — swept unconditionally by collect. +# decisions+curation co-pending → exactly ONE opus spawn that # runs decisions THEN curation sequentially (prevents .decisions.lock contention). # Unknown task types are skipped (belt-and-suspenders — dream_collect_tasks # should never emit them). @@ -239,18 +241,13 @@ dream_build_spawn_directive() { local _dbsd_tasks="$1" _DREAM_DIRECTIVE="" - # memory is no longer a Dream subagent task (handled by background-memory-update worker). - # Map: knowledge=sonnet, decisions=opus, curation=opus. memory entries swept by collect. - local _dbsd_know="false" _dbsd_dec="false" _dbsd_cur="false" - case ",$_dbsd_tasks," in *,knowledge,*) _dbsd_know="true" ;; esac - case ",$_dbsd_tasks," in *,decisions,*) _dbsd_dec="true" ;; esac - case ",$_dbsd_tasks," in *,curation,*) _dbsd_cur="true" ;; esac + # memory, knowledge: no longer Dream subagent tasks (handled by other mechanisms). + # Map: decisions=opus, curation=opus. memory and knowledge entries swept by collect. + local _dbsd_dec="false" _dbsd_cur="false" + case ",$_dbsd_tasks," in *,decisions,*) _dbsd_dec="true" ;; esac + case ",$_dbsd_tasks," in *,curation,*) _dbsd_cur="true" ;; esac local _dbsd_lines="" - if [ "$_dbsd_know" = "true" ]; then - _dbsd_lines="${_dbsd_lines}Agent(subagent_type=\"Dream\", model=\"sonnet\", run_in_background: true, prompt: \"Process pending 'knowledge' marker(s): claim per your plumbing, then load devflow:dream-knowledge and follow it.\") -" - fi if [ "$_dbsd_dec" = "true" ] && [ "$_dbsd_cur" = "true" ]; then _dbsd_lines="${_dbsd_lines}Agent(subagent_type=\"Dream\", model=\"opus\", run_in_background: true, prompt: \"Process pending decisions and curation marker(s): claim each per your plumbing, then load devflow:dream-decisions and follow it, THEN load devflow:dream-curation and follow it (sequentially, never concurrently).\") " diff --git a/scripts/hooks/dream-evaluate b/scripts/hooks/dream-evaluate index 0ff08300..76578c0b 100755 --- a/scripts/hooks/dream-evaluate +++ b/scripts/hooks/dream-evaluate @@ -1,10 +1,11 @@ #!/bin/bash # Dream System: dream-evaluate (SessionEnd Hook) -# Evaluates session-level features: decisions detection, knowledge refresh. +# Evaluates session-level features: decisions detection and curation. # Writes marker files to .devflow/dream/ for dream-dispatch to pick up. # -# Orchestrator: sources eval-helpers + 3 feature modules after shared setup. +# Orchestrator: sources eval-helpers + 2 feature modules after shared setup. +# Knowledge is no longer evaluated here — write-through handles it in-command. # Safe no-op fallback: must exist before set -e and before hook-bootstrap is sourced. dbg() { :; } @@ -43,18 +44,15 @@ DEVFLOW_DIR="$PROJECT_ROOT/.devflow" [ ! -d "$DEVFLOW_DIR" ] && exit 0 MEMORY_DIR="$DEVFLOW_DIR/memory" -FEATURES_DIR="$DEVFLOW_DIR/features" DREAM_DIR="$DEVFLOW_DIR/dream" DECISIONS_DIR_DATA="$DEVFLOW_DIR/decisions" # Read dream config DREAM_CONFIG="$DREAM_DIR/config.json" DECISIONS_ENABLED="true" -KNOWLEDGE_ENABLED="true" if [ -f "$DREAM_CONFIG" ]; then DECISIONS_ENABLED=$(json_field_file "$DREAM_CONFIG" "decisions" "true") - KNOWLEDGE_ENABLED=$(json_field_file "$DREAM_CONFIG" "knowledge" "true") fi # Dual-signal gate: config OR sentinel disables decisions (and transitively curation). @@ -67,7 +65,7 @@ source "$SCRIPT_DIR/hook-log-init" "dream-evaluate" source "$SCRIPT_DIR/dream-lock" || { log "failed to source dream-lock"; exit 1; } log "SessionEnd hook triggered" -dbg "DECISIONS=$DECISIONS_ENABLED KNOWLEDGE=$KNOWLEDGE_ENABLED" +dbg "DECISIONS=$DECISIONS_ENABLED" # --- Find transcript --- ENCODED_CWD=$(echo "$CWD" | sed 's|^/||' | tr '/' '-') @@ -133,13 +131,14 @@ MARKER_SUFFIX="${SESSION_ID:-$$}" # MARKER_SUFFIX — suffix for per-session marker filenames (SESSION_ID or $$) # DREAM_DIR — absolute path to .devflow/dream/ # DECISIONS_DIR_DATA — absolute path to .devflow/decisions/ -# FEATURES_DIR — absolute path to .devflow/features/ -# DECISIONS_ENABLED / KNOWLEDGE_ENABLED — "true"/"false" +# DECISIONS_ENABLED — "true"/"false" # _HAS_JQ — "true" if jq is available on PATH # +# Note: FEATURES_DIR and KNOWLEDGE_ENABLED are no longer set here. +# Knowledge write-through is handled in-command via the knowledge_writeback() MDS partial. +# source "$SCRIPT_DIR/eval-helpers" source "$SCRIPT_DIR/eval-decisions" -source "$SCRIPT_DIR/eval-knowledge" source "$SCRIPT_DIR/eval-curation" dbg "=== HOOK COMPLETE ===" diff --git a/scripts/hooks/dream-recover b/scripts/hooks/dream-recover index 3d71ad71..6cc6adf8 100644 --- a/scripts/hooks/dream-recover +++ b/scripts/hooks/dream-recover @@ -23,7 +23,8 @@ # - Per-type stale thresholds: # memory = 300s (5 minutes) # decisions = 1800s (30 minutes) -# knowledge = 1800s (30 minutes) +# knowledge = 1800s (legacy; knowledge is no longer a Dream task — stale markers are + # recovered here then deleted by dream-collect-tasks on sight) # unknown = 1800s (default) # - Also recovers orphaned .pending-turns.processing: # If MEMORY_DIR/.pending-turns.processing exists and is older than 300s, @@ -36,9 +37,10 @@ # D56a: Per-type stale thresholds. # memory = 300s (5 min) because the memory agent is fast — if it is still # running after 5 minutes the session almost certainly crashed. All other -# types (learning, decisions, knowledge, curation) run the LLM and may take -# up to 30 minutes on large logs or slow models; 1800s avoids yanking an -# actively-running Dream agent. +# types (decisions, curation) run the LLM and may take up to 30 minutes on +# large logs or slow models; 1800s avoids yanking an actively-running Dream agent. +# Learning and knowledge are no longer Dream tasks; stale markers are recovered +# here and then deleted by dream-collect-tasks on sight. # # D56b: JUST_RECOVERED guard. # When dream_recover_stale renames a .processing back to .json, it records diff --git a/scripts/hooks/ensure-devflow-init b/scripts/hooks/ensure-devflow-init index 158d2c5c..2b172d34 100755 --- a/scripts/hooks/ensure-devflow-init +++ b/scripts/hooks/ensure-devflow-init @@ -1,8 +1,10 @@ #!/bin/bash -# Ensures .devflow/ and all subdirectories exist, the project root .gitignore -# ignores .devflow/, and .devflow/features/index.json is bootstrapped. -# Merges functionality of the former ensure-memory-gitignore and ensure-features-init. +# Ensures .devflow/ and all subdirectories exist and the project root .gitignore +# ignores .devflow/. Merges functionality of the former ensure-memory-gitignore +# and ensure-features-init. # Called from dream-capture, dream-dispatch, and pre-compact-memory. Idempotent. +# Note: features/index.json is no longer bootstrapped here — features/index.md is +# created lazily by write-through (knowledge_writeback MDS partial). # Usage: source ensure-devflow-init "$CWD" [ -z "$1" ] && return 1 @@ -34,12 +36,6 @@ mkdir -p \ "$_DEVFLOW_DIR/docs" \ 2>/dev/null || return 1 -# Bootstrap features/index.json if missing -if [ ! -f "$_DEVFLOW_DIR/features/index.json" ]; then - printf '{"version":1,"features":{}}' > "$_DEVFLOW_DIR/features/index.json.tmp" && \ - mv "$_DEVFLOW_DIR/features/index.json.tmp" "$_DEVFLOW_DIR/features/index.json" -fi - # One-time root .gitignore setup — delegated to the sibling ensure-root-gitignore # helper (single source of truth) so the always-on, memory-independent # session-start-context hook applies the identical rule. _EDI_DIR and _EDI_ROOT diff --git a/scripts/hooks/eval-knowledge b/scripts/hooks/eval-knowledge deleted file mode 100644 index c43438cb..00000000 --- a/scripts/hooks/eval-knowledge +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/bash -# dream-evaluate: Knowledge evaluation module. -# Checks for stale feature knowledge bases and writes a knowledge refresh marker. -# Source from dream-evaluate orchestrator after eval-helpers. -# -# Requires (from orchestrator namespace): -# KNOWLEDGE_ENABLED, FEATURES_DIR, CWD, DREAM_DIR, NOW, MARKER_SUFFIX, -# SCRIPT_DIR, _HAS_JQ, log(), dbg() - -: "${FEATURES_DIR:?eval-knowledge: FEATURES_DIR must be set by orchestrator}" -: "${CWD:?eval-knowledge: CWD must be set by orchestrator}" -: "${DREAM_DIR:?eval-knowledge: DREAM_DIR must be set by orchestrator}" -: "${NOW:?eval-knowledge: NOW must be set by orchestrator}" -: "${MARKER_SUFFIX:?eval-knowledge: MARKER_SUFFIX must be set by orchestrator}" -: "${SCRIPT_DIR:?eval-knowledge: SCRIPT_DIR must be set by orchestrator}" -: "${_HAS_JQ:?eval-knowledge: _HAS_JQ must be set by orchestrator}" - -if [ "$KNOWLEDGE_ENABLED" = "true" ]; then - log "Evaluating knowledge..." - - # Check .devflow/features/.disabled sentinel - if [ -f "$FEATURES_DIR/.disabled" ]; then - log "Knowledge disabled by sentinel" - else - # Throttle: skip if refreshed within last 2 hours - _KNOW_MARKER_FILE="$FEATURES_DIR/.knowledge-last-refresh" - _KNOW_LAST_REFRESH=$(tr -dc '0-9' < "$_KNOW_MARKER_FILE" 2>/dev/null || true) - _KNOW_LAST_REFRESH="${_KNOW_LAST_REFRESH:-0}" - _KNOW_AGE=$(( NOW - _KNOW_LAST_REFRESH )) - - dbg "Knowledge: AGE=${_KNOW_AGE}s (throttle=7200s)" - if [ "$_KNOW_AGE" -lt 7200 ]; then - log "Knowledge throttled: refreshed ${_KNOW_AGE}s ago" - dbg "Knowledge throttled: AGE=${_KNOW_AGE}s" - else - # Query stale slugs - _KNOW_FEATURE_LIB="$SCRIPT_DIR/lib/feature-knowledge.cjs" - _KNOW_STALE_SLUGS="" - if [ -f "$_KNOW_FEATURE_LIB" ]; then - _KNOW_STALE_SLUGS=$(node "$_KNOW_FEATURE_LIB" stale-slugs "$CWD" 2>/dev/null || true) - fi - - if [ "${DEVFLOW_HOOK_DEBUG:-}" = "1" ]; then dbg "Knowledge stale_slugs=$(echo "$_KNOW_STALE_SLUGS" | wc -l | tr -d ' ')"; fi - if [ -n "$_KNOW_STALE_SLUGS" ]; then - # Write knowledge marker (per-session name for concurrency safety) - _KNOW_MARKER="$DREAM_DIR/knowledge.${MARKER_SUFFIX}.json" - _KNOW_TMP="${_KNOW_MARKER}.tmp.$$" - if [ "$_HAS_JQ" = "true" ]; then - _KNOW_STALE_ARRAY=$(echo "$_KNOW_STALE_SLUGS" | jq -R . | jq -s .) - jq -n \ - --argjson staleSlugs "$_KNOW_STALE_ARRAY" \ - --arg worktreePath "$CWD" \ - --argjson timestamp "$NOW" \ - '{staleSlugs: $staleSlugs, worktreePath: $worktreePath, timestamp: $timestamp}' \ - > "$_KNOW_TMP" && mv "$_KNOW_TMP" "$_KNOW_MARKER" || rm -f "$_KNOW_TMP" - else - node -e " - const slugs = process.argv[1].trim().split('\n').filter(Boolean); - process.stdout.write(JSON.stringify({staleSlugs: slugs, worktreePath: process.argv[2], timestamp: parseInt(process.argv[3])}) + '\n') - " -- "$_KNOW_STALE_SLUGS" "$CWD" "$NOW" > "$_KNOW_TMP" && mv "$_KNOW_TMP" "$_KNOW_MARKER" || rm -f "$_KNOW_TMP" - fi - # Optimistically update throttle marker so the stale-slug query is skipped - # on the next session end — the dream agent will do the actual refresh. - echo "$NOW" > "$_KNOW_MARKER_FILE" - log "Wrote knowledge marker: $(echo "$_KNOW_STALE_SLUGS" | wc -l | tr -d ' ') stale slug(s)" - dbg "Wrote knowledge marker: $_KNOW_MARKER" - else - log "Knowledge: no stale slugs found" - dbg "Knowledge: no stale slugs found" - fi - fi - fi -fi diff --git a/scripts/hooks/lib/feature-knowledge.cjs b/scripts/hooks/lib/feature-knowledge.cjs deleted file mode 100644 index efe9ab77..00000000 --- a/scripts/hooks/lib/feature-knowledge.cjs +++ /dev/null @@ -1,652 +0,0 @@ -// scripts/hooks/lib/feature-knowledge.cjs -// Runtime module for per-feature knowledge base management. -// -// DESIGN: Feature knowledge bases live under .devflow/features/{slug}/KNOWLEDGE.md with a central -// index at .devflow/features/index.json (keyed by slug). This module is the single -// source of truth for all knowledge base 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/decisions-index.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-knowledge.cjs list -// node feature-knowledge.cjs stale [slug] -// node feature-knowledge.cjs update-index --slug=X --name=Y ... -// node feature-knowledge.cjs find-overlapping [file2...] -// node feature-knowledge.cjs remove - -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const { execFileSync } = require('child_process'); -const { getFeaturesDir, getFeaturesIndexPath, getFeaturesLockDir, getKnowledgePath } = require('./project-paths.cjs'); - -/** Sentinel returned whenever a knowledge entry 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))]; -} - -/** - * Parse git log output with dates into a map of file → latest change timestamp. - * Expects `--pretty=format:%aI` output: ISO date, blank line, file names, blank, repeat. - * Reverse chronological order means first occurrence = latest change. - * @param {string} output - * @returns {Map} - */ -function parseGitLogWithDates(output) { - const fileLatestChange = new Map(); - let currentDate = null; - for (const line of output.split('\n')) { - const trimmed = line.trim(); - if (!trimmed) continue; - if (/^\d{4}-\d{2}-\d{2}T/.test(trimmed)) { - currentDate = new Date(trimmed).getTime(); - continue; - } - if (currentDate !== null && !fileLatestChange.has(trimmed)) { - fileLatestChange.set(trimmed, currentDate); - } - } - return fileLatestChange; -} - -/** - * 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 {{ - * name: string, - * description: string, - * directories: string[], - * referencedFiles: string[], - * lastUpdated: string, - * createdBy: string - * }} FeatureEntry - */ - -/** - * Load and parse .devflow/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 = getFeaturesIndexPath(worktreePath); - try { - const raw = fs.readFileSync(indexPath, 'utf8'); - return JSON.parse(raw); - } catch { - return null; - } -} - -/** - * Load knowledge base content for a given slug. - * Returns null when the KNOWLEDGE.md file is absent. - * - * @param {string} worktreePath - * @param {string} slug - * @returns {string | null} - */ -function loadKnowledgeContent(worktreePath, slug) { - validateSlug(slug); - const kbPath = getKnowledgePath(worktreePath, slug); - try { - return fs.readFileSync(kbPath, 'utf8'); - } catch { - return null; - } -} - -/** - * 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 {FeatureEntry} entry - * @returns {{ stale: boolean, changedFiles: string[] }} - */ -function checkEntryFiles(worktreePath, entry) { - const files = entry.referencedFiles || []; - if (files.length === 0) return NOT_STALE; - - 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 = parseGitChangedFiles(result); - return { stale: changedFiles.length > 0, changedFiles }; - } catch { - return NOT_STALE; - } -} - -/** - * Check if a knowledge entry 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 knowledge entries in the index. - * Loads the index once, checks git-dir once, and runs a single git log call - * to detect all changed files since the oldest lastUpdated timestamp. - * Uses the oldest timestamp as the --after cutoff to minimize git calls, - * then compares each file's latest change date against each entry's own - * lastUpdated to avoid false-positive staleness. - * - * @param {string} worktreePath - * @param {{ version: number; features: Record } | null} [cachedIndex] - Optional pre-loaded index to avoid double reads - * @returns {Record} - */ -function checkAllStaleness(worktreePath, cachedIndex) { - const index = cachedIndex !== undefined ? cachedIndex : loadIndex(worktreePath); - if (!index) return {}; - - const slugs = Object.keys(index.features); - if (slugs.length === 0) 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 - return Object.fromEntries(slugs.map(slug => [slug, NOT_STALE])); - } - - // Collect all referenced files and find the oldest lastUpdated timestamp - const allFilesSet = new Set(); - let oldestTimestamp = null; - for (const slug of slugs) { - const entry = index.features[slug]; - const files = entry.referencedFiles || []; - for (const f of files) { - allFilesSet.add(f); - } - if (entry.lastUpdated) { - const ts = new Date(entry.lastUpdated).getTime(); - if (!isNaN(ts) && (oldestTimestamp === null || ts < oldestTimestamp)) { - oldestTimestamp = ts; - } - } - } - - // If no files or no timestamp, fall back to per-entry checks - if (allFilesSet.size === 0 || oldestTimestamp === null) { - const results = {}; - for (const slug of slugs) { - results[slug] = checkEntryFiles(worktreePath, index.features[slug]); - } - return results; - } - - // Single git log call for all files since oldest timestamp, with author dates - let fileLatestChange; - try { - const gitOutput = execFileSync( - 'git', - ['log', `--after=${new Date(oldestTimestamp).toISOString()}`, '--name-only', '--pretty=format:%aI', '--', ...allFilesSet], - { cwd: worktreePath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] } - ); - fileLatestChange = parseGitLogWithDates(gitOutput); - } catch { - // Git call failed — fall back to per-entry checks - const results = {}; - for (const slug of slugs) { - results[slug] = checkEntryFiles(worktreePath, index.features[slug]); - } - return results; - } - - // For each entry, compare its files' latest change dates against its own lastUpdated - const results = {}; - for (const slug of slugs) { - const entry = index.features[slug]; - const files = entry.referencedFiles || []; - if (files.length === 0) { - results[slug] = NOT_STALE; - continue; - } - const entryTimestamp = entry.lastUpdated ? new Date(entry.lastUpdated).getTime() : 0; - const changedForEntry = files.filter(f => { - const changeTs = fileLatestChange.get(f); - return changeTs !== undefined && changeTs > entryTimestamp; - }); - results[slug] = { stale: changedForEntry.length > 0, changedFiles: changedForEntry }; - } - 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/.decisions.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 { - 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 */ } - } - } - } - 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[], - * createdBy?: string - * }} entry - * @param {number} [lockTimeoutMs=30000] optional lock timeout for testability - */ -function updateIndex(worktreePath, entry, lockTimeoutMs = 30000) { - validateSlug(entry.slug); - const featuresDir = getFeaturesDir(worktreePath); - fs.mkdirSync(featuresDir, { recursive: true }); - const lockPath = getFeaturesLockDir(worktreePath); - const indexPath = getFeaturesIndexPath(worktreePath); - - if (!acquireLock(lockPath, lockTimeoutMs)) { - throw new Error('Failed to acquire .devflow/features/.knowledge.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 ?? existing.description ?? '', - directories: entry.directories, - referencedFiles: entry.referencedFiles, - lastUpdated: new Date().toISOString(), - createdBy: entry.createdBy || existing.createdBy || 'manual', - }; - - fs.writeFileSync(indexPath, JSON.stringify(index, null, 2) + '\n'); - } finally { - releaseLock(lockPath); - } -} - -/** - * Find knowledge entries 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 findOverlapping(worktreePath, changedFiles) { - const index = loadIndex(worktreePath); - if (!index) return []; - - 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 + '/')) - ); - if (overlap) overlappingSlugs.push(slug); - } - return overlappingSlugs; -} - -/** - * Remove a knowledge entry from index.json and delete its directory. - * No-op if the slug does not exist in the index or if .devflow/features/ is absent. - * - * @param {string} worktreePath - * @param {string} slug - * @param {number} [lockTimeoutMs=30000] optional lock timeout for testability - */ -function removeEntry(worktreePath, slug, lockTimeoutMs = 30000) { - validateSlug(slug); - const featuresDir = getFeaturesDir(worktreePath); - if (!fs.existsSync(featuresDir)) return; - const lockPath = getFeaturesLockDir(worktreePath); - const indexPath = getFeaturesIndexPath(worktreePath); - - if (!acquireLock(lockPath, lockTimeoutMs)) { - throw new Error('Failed to acquire .devflow/features/.knowledge.lock within timeout'); - } - - try { - let index; - try { - index = JSON.parse(fs.readFileSync(indexPath, 'utf8')); - } catch { - return; // nothing to remove — preserve existing (possibly corrupt) file - } - - delete index.features[slug]; - fs.writeFileSync(indexPath, JSON.stringify(index, null, 2) + '\n'); - - const kbDir = path.join(featuresDir, slug); - try { - fs.rmSync(kbDir, { recursive: true, force: true }); - } catch { /* ignore */ } - } finally { - releaseLock(lockPath); - } -} - -/** - * List all knowledge entries with their metadata (slug + FeatureEntry fields). - * - * @param {string} worktreePath - * @param {{ version: number; features: Record } | null} [cachedIndex] - Optional pre-loaded index to avoid double reads - * @returns {Array<{ slug: string } & FeatureEntry>} - */ -function listEntries(worktreePath, cachedIndex) { - const index = cachedIndex !== undefined ? cachedIndex : loadIndex(worktreePath); - if (!index) return []; - return Object.entries(index.features).map(([slug, entry]) => ({ slug, ...entry })); -} - -// --------------------------------------------------------------------------- -// CLI interface -// -// Usage: -// node feature-knowledge.cjs list -// node feature-knowledge.cjs stale [slug] -// node feature-knowledge.cjs update-index --slug=X --name=Y --directories='[...]' --referencedFiles='[...]' [--description=Y] [--createdBy=Z] -// node feature-knowledge.cjs find-overlapping [file2...] -// node feature-knowledge.cjs remove -// node feature-knowledge.cjs stale-slugs -// node feature-knowledge.cjs refresh-context -// --------------------------------------------------------------------------- - -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-knowledge.cjs list ', - ' node feature-knowledge.cjs stale [slug]', - ' node feature-knowledge.cjs update-index --slug=X --name=Y --directories=\'[...]\' --referencedFiles=\'[...]\' [--description=Y] [--createdBy=Z]', - ' node feature-knowledge.cjs find-overlapping [file2...]', - ' node feature-knowledge.cjs remove ', - ' node feature-knowledge.cjs stale-slugs ', - ' node feature-knowledge.cjs refresh-context ', - ].join('\n'); - - /** - * 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); - } - if (!fs.existsSync(p) || !fs.statSync(p).isDirectory()) { - process.stderr.write(`Error: '${p}' is not a valid directory\n`); - process.exit(1); - } - return p; - } - - /** @type {Record void>} */ - const dispatch = { - list() { - const worktreePath = requireWorktree(argv); - const entries = listEntries(worktreePath); - process.stderr.write(`[feature-knowledge] 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-knowledge] 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-knowledge] 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) { - process.stderr.write('Error: missing required flags (slug, name, directories, referencedFiles)\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, - createdBy: kv.createdBy, - }); - process.stderr.write(`[feature-knowledge] 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-knowledge] 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-knowledge] mode=remove worktree=${worktreePath} slug=${slug}\n`); - 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: knowledge entry '${slug}' not found in index\n`); - process.exit(1); - } - const entry = index.features[slug]; - const staleness = checkStaleness(worktreePath, slug); - // Tab-separated: name, directories JSON, changed files JSON - process.stdout.write([ - entry.name, - JSON.stringify(entry.directories), - JSON.stringify(staleness.changedFiles), - ].join('\t') + '\n'); - process.exit(0); - }, - }; - - if (!subcommand) { - process.stderr.write(USAGE + '\n'); - process.exit(1); - } - - const handler = dispatch[subcommand]; - if (!handler) { - process.stderr.write(`Error: unknown subcommand '${subcommand}'\n` + USAGE + '\n'); - process.exit(1); - } - handler(); -} - -module.exports = { loadIndex, loadKnowledgeContent, checkStaleness, checkAllStaleness, updateIndex, findOverlapping, removeEntry, listEntries, validateSlug }; -// Note: loadIndex is already exported above, enabling callers to read the index once -// and pass it to listEntries/checkAllStaleness via their optional cachedIndex parameter. diff --git a/scripts/hooks/lib/project-paths.cjs b/scripts/hooks/lib/project-paths.cjs index cc230dae..86677dc6 100644 --- a/scripts/hooks/lib/project-paths.cjs +++ b/scripts/hooks/lib/project-paths.cjs @@ -167,31 +167,11 @@ function getPendingTurnsLockDir(projectRoot) { // Features / knowledge files // --------------------------------------------------------------------------- -/** .devflow/features/index.json */ -function getFeaturesIndexPath(projectRoot) { - return path.join(projectRoot, '.devflow', 'features', 'index.json'); -} - /** .devflow/features/{slug}/KNOWLEDGE.md */ function getKnowledgePath(projectRoot, slug) { return path.join(projectRoot, '.devflow', 'features', slug, 'KNOWLEDGE.md'); } -/** .devflow/features/.disabled — sentinel that gates knowledge phase/refresh */ -function getFeaturesDisabledSentinel(projectRoot) { - return path.join(projectRoot, '.devflow', 'features', '.disabled'); -} - -/** .devflow/features/.knowledge.lock — transient lock directory for concurrent index writes */ -function getFeaturesLockDir(projectRoot) { - return path.join(projectRoot, '.devflow', 'features', '.knowledge.lock'); -} - -/** .devflow/features/.knowledge-last-refresh — timestamp of last auto-refresh */ -function getFeaturesLastRefreshPath(projectRoot) { - return path.join(projectRoot, '.devflow', 'features', '.knowledge-last-refresh'); -} - // --------------------------------------------------------------------------- // Docs files // --------------------------------------------------------------------------- @@ -266,11 +246,7 @@ module.exports = { getPendingTurnsProcessingPath, getPendingTurnsLockDir, // Features files - getFeaturesIndexPath, getKnowledgePath, - getFeaturesDisabledSentinel, - getFeaturesLockDir, - getFeaturesLastRefreshPath, // Docs files getReviewsDir, getDesignDir, diff --git a/scripts/hooks/session-start-context b/scripts/hooks/session-start-context index 55ae1af0..70207559 100755 --- a/scripts/hooks/session-start-context +++ b/scripts/hooks/session-start-context @@ -129,22 +129,20 @@ fi if [ "$_SC2_HELPERS_OK" = "true" ]; then # Determine feature enabled-state. - # Primary: dream/config.json fields; secondary: runtime sentinels (defense-in-depth). + # Primary: dream/config.json fields; secondary: runtime sentinel for decisions. # Note: memory is NOT a Dream task (handled by background-memory-update worker) — # mem_enabled was removed from dream_collect_tasks (applies ADR-016; avoids PF-009). + # Note: knowledge is no longer a Dream task — write-through handles it in-command. _SC2_DEC_EN="true" - _SC2_KNOW_EN="true" _SC2_DREAM_CONFIG="$DREAM_DIR/config.json" if [ -f "$_SC2_DREAM_CONFIG" ]; then - _SC2_DEC_EN=$(json_field_file "$_SC2_DREAM_CONFIG" "decisions" "true") - _SC2_KNOW_EN=$(json_field_file "$_SC2_DREAM_CONFIG" "knowledge" "true") + _SC2_DEC_EN=$(json_field_file "$_SC2_DREAM_CONFIG" "decisions" "true") fi - # Sentinel overrides - [ -f "$DEVFLOW_DIR/decisions/.disabled" ] && _SC2_DEC_EN="false" - [ -f "$DEVFLOW_DIR/features/.disabled" ] && _SC2_KNOW_EN="false" + # Sentinel override for decisions + [ -f "$DEVFLOW_DIR/decisions/.disabled" ] && _SC2_DEC_EN="false" - dbg "Section2: dec=$_SC2_DEC_EN know=$_SC2_KNOW_EN" + dbg "Section2: dec=$_SC2_DEC_EN" # Step 1: recover stale .processing markers so they become collectable in step 2 if [ -d "$DREAM_DIR" ]; then @@ -152,7 +150,7 @@ if [ "$_SC2_HELPERS_OK" = "true" ]; then fi # Step 2: collect pending marker task types - dream_collect_tasks "$DREAM_DIR" "$_SC2_DEC_EN" "$_SC2_KNOW_EN" || true + dream_collect_tasks "$DREAM_DIR" "$_SC2_DEC_EN" || true # Emit directive if tasks pending, subject to processor-spawn throttle. if [ -n "$_DREAM_TASKS" ]; then diff --git a/shared/agents/dream.md b/shared/agents/dream.md index 3faa6870..4c069d3e 100644 --- a/shared/agents/dream.md +++ b/shared/agents/dream.md @@ -1,6 +1,6 @@ --- name: Dream -description: Background maintenance agent — processes ONE pending task type named in its prompt (decisions, knowledge, curation). Spawned per-task by session-start-context; loads the matching per-task skill via the Skill tool. Memory is NOT a Dream task — it is handled by the background-memory-update worker spawned from dream-capture. +description: Background maintenance agent — processes ONE pending task type named in its prompt (decisions, curation). Spawned per-task by session-start-context; loads the matching per-task skill via the Skill tool. Memory is NOT a Dream task — it is handled by the background-memory-update worker spawned from dream-capture. Knowledge is NOT a Dream task — it is handled in-command via write-through. model: sonnet tools: - Read @@ -13,7 +13,6 @@ skills: - devflow:apply-decisions - devflow:apply-feature-knowledge - devflow:dream-decisions - - devflow:dream-knowledge - devflow:dream-curation --- @@ -24,8 +23,8 @@ combined Opus spawn). Your role: claim markers atomically, do real LLM work via per-task skill, write results through plumbing ops, clean up. > **Model note**: The `model: sonnet` frontmatter is the fallback default. In practice -> `session-start-context` overrides the model per spawn: sonnet for `knowledge`, opus for -> the combined `decisions then curation` spawn. When you see "Opus spawn" in this document, +> `session-start-context` overrides the model per spawn: opus for the combined +> `decisions then curation` spawn. When you see "Opus spawn" in this document, > that refers to the model assigned by the orchestrator at spawn time. ## Environment @@ -33,17 +32,18 @@ per-task skill, write results through plumbing ops, clean up. Installed scripts live at `$HOME/.devflow/scripts/hooks/`. All node invocations use these paths: - `node "$HOME/.devflow/scripts/hooks/json-helper.cjs" ...` -- `node "$HOME/.devflow/scripts/hooks/lib/feature-knowledge.cjs" ...` - `node "$HOME/.devflow/scripts/hooks/lib/decisions-index.cjs" ...` Project root is your current working directory (`.`). All `.devflow/` paths are relative to it. ## Step 0 — Identify your task -Your prompt names the task type(s) to process: `decisions`, `knowledge`, `curation`, +Your prompt names the task type(s) to process: `decisions`, `curation`, or `decisions then curation` (the combined Opus spawn). Process ONLY the task(s) named. **Memory is NOT a Dream task** — the background-memory-update worker (spawned by dream-capture) handles WORKING-MEMORY.md writes. If your prompt mentions `memory`, skip it. +**Knowledge is NOT a Dream task** — write-through (knowledge_writeback MDS partial, invoked +in-command) handles knowledge base updates. If your prompt mentions `knowledge`, skip it. ## Step 1 — Claim markers atomically @@ -71,7 +71,6 @@ the plumbing ops in the per-task skills — they hold locks internally for one a **Multi-marker merge**: When multiple `{type}.{session}.processing` files exist for one type, read them all, then union/concat their payloads before processing: - `decisions`: concatenate `dialogPairs` strings; union `existingObservationIds` arrays -- `knowledge`: union `staleSlugs` arrays; use any `worktreePath` - `curation`: single marker only **Input cap**: Process only the last **30** dialog-pairs (truncate oldest if more). @@ -82,7 +81,6 @@ This bounds token cost and keeps each run predictable. For each task type you claimed markers for, load the matching skill and follow its procedure: - **decisions** → load `devflow:dream-decisions` via the Skill tool and follow its procedure exactly. -- **knowledge** → load `devflow:dream-knowledge` via the Skill tool and follow its procedure exactly. - **curation** → load `devflow:dream-curation` via the Skill tool and follow its procedure exactly. For the combined "decisions then curation" spawn: run decisions fully (claim + process + cleanup) diff --git a/shared/agents/knowledge.md b/shared/agents/knowledge.md index fc143f90..0bcdfd18 100644 --- a/shared/agents/knowledge.md +++ b/shared/agents/knowledge.md @@ -1,6 +1,6 @@ --- name: Knowledge -description: Structures codebase exploration into a feature knowledge base +description: Structures codebase exploration into a feature knowledge base and registers it in the index cache model: sonnet skills: - devflow:feature-knowledge @@ -21,32 +21,46 @@ tools: - **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") - **DIRECTORIES** (required): Directory prefixes defining the feature area scope -- **EXPLORATION_OUTPUTS** (optional): Pre-computed findings from Skimmer + Explore agents. When provided (e.g., from /plan), synthesize these instead of exploring from scratch. When absent, perform your own exploration in Phase 1 (Scan) and Phase 2 (Extract) using the tools available. +- **FILES_CHANGED** (optional): Files changed in the workflow session that triggered write-back - **DECISIONS_CONTEXT** (optional): Compact ADR/PF index for cross-referencing in the Related section. When `(none)`, skip citing decisions in the Related section. -- **EXISTING_KB** (optional): Current KNOWLEDGE.md content when refreshing stale feature knowledge -- **CHANGED_FILES** (optional): Files that changed since last feature knowledge update (for refresh) +- **EXISTING_KB** (optional): Current KNOWLEDGE.md content when refreshing existing feature knowledge - **WORKTREE_PATH** (optional): Worktree root for path resolution +- **EXPLORATION_OUTPUTS** (optional): Pre-computed findings from Skimmer + Explore agents. When provided, synthesize these instead of exploring from scratch. When absent, perform your own exploration in Phase 1 (Scan) and Phase 2 (Extract). ## 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 +1. **Resolve worktree path**: Use `devflow:worktree-support` to determine the working directory (WORKTREE_PATH or cwd) +2. **Orient on feature area**: Read EXPLORATION_OUTPUTS or EXISTING_KB to understand the feature's architecture, patterns, and boundaries 3. **Follow the feature-knowledge skill**: Execute the 4-phase process (Scan → Extract → Distill → Forge) from `devflow:feature-knowledge` 4. **Cross-reference decisions**: If DECISIONS_CONTEXT is provided, reference relevant ADR/PF entries in the feature knowledge'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 `.devflow/features/{FEATURE_SLUG}/KNOWLEDGE.md` (create directory if needed) -7. **Write result file**: Write result 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 +5. **Handle refresh**: If EXISTING_KB is provided, update stale sections based on FILES_CHANGED while preserving any manually added content. Don't regenerate from scratch. +6. **Write KNOWLEDGE.md directly**: Write to `{worktree}/.devflow/features/{FEATURE_SLUG}/KNOWLEDGE.md` (create directory if needed) +7. **Update index.md directly**: Read-modify-write `{worktree}/.devflow/features/index.md` + - If slug already present: replace that line in-place + - If absent or file missing: append (or create file) + - Line format: `- **{slug}** — {areas} — {Use-when description}` +8. **Report**: Output KB_PATH and KB_SLUG (see Output section) + +## Direct Write Protocol + +Write BOTH files atomically — no intermediate result files, no external scripts: + +1. Ensure `{worktree}/.devflow/features/{slug}/` directory exists +2. Write `KNOWLEDGE.md` to that directory +3. Read `{worktree}/.devflow/features/index.md` (tolerate ENOENT) +4. Replace the `- **{slug}**` line if found; else append the new line +5. Write `index.md` back + +The frontmatter in KNOWLEDGE.md is always the authority. The index.md line is a discoverable cache. ## Output ``` KB_STATUS: created | refreshed -KB_PATH: .devflow/features/{slug}/KNOWLEDGE.md +KB_PATH: {worktree}/.devflow/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] ``` @@ -54,5 +68,6 @@ CROSS_REFERENCES: [ADR/PF entries referenced, if any] - **Only writes to `.devflow/features/` directory** — never modify source code - **Never delete existing feature knowledge** — only create new or refresh existing -- **500-line cap** — if the knowledge base exceeds 500 lines, split into focused sub-knowledge bases (each gets own index entry) +- **500-line cap** — if the knowledge base exceeds 500 lines, split into focused sub-knowledge bases (each gets its own index entry) - **No push, no external API calls** — local filesystem operations only +- **No `.create-result.json` or `.refresh-result.json`** — write directly; the host workflow reads KB_PATH/KB_SLUG from this agent's output text diff --git a/shared/skills/apply-feature-knowledge/SKILL.md b/shared/skills/apply-feature-knowledge/SKILL.md index 40ad6270..9e888883 100644 --- a/shared/skills/apply-feature-knowledge/SKILL.md +++ b/shared/skills/apply-feature-knowledge/SKILL.md @@ -9,10 +9,11 @@ allowed-tools: Read ## Iron Law -> **Pre-computed context, not a cage. Verify against current code when assumptions seem outdated.** +> **Pre-computed context, not a cage. Verify against current code — always.** > -> A feature knowledge captures patterns AS THEY WERE when last updated. Code evolves. -> Use the feature knowledge as a starting point, not gospel truth. +> A feature knowledge captures patterns AS THEY WERE when last written. Code evolves. +> Use the feature knowledge as a starting point, not gospel truth. When something feels +> off, Read the actual files. Code is authoritative; feature knowledge is supplementary. --- @@ -34,12 +35,13 @@ When `FEATURE_KNOWLEDGE` is provided and is not `(none)`: 4. **Integration points**: Ensure your changes respect documented boundaries 5. **Key files**: Use as starting points for exploration -### Step 3: Supplement as Needed +### Step 3: Verify Against Current Code -The feature knowledge may not cover everything: +The feature knowledge may not reflect recent changes: - If the feature knowledge doesn't address your specific area, explore further -- If the feature knowledge seems outdated (marked `[STALE]`), verify against current code -- If you discover new patterns, note them — they may become feature knowledge updates +- **When an assertion seems outdated**: Read the relevant source files to confirm — code wins +- When you find a contradiction between the KB and actual code, trust the code +- Note discrepancies in your output when they matter for the task --- @@ -48,13 +50,12 @@ The feature knowledge may not cover everything: 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 +## Freshness Model -Feature knowledge entries 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 +Feature knowledge uses **write-through + verify-on-read** for freshness: +- KBs are written at the point a documented area changes (not on a background schedule) +- Readers verify key assertions against current code rather than relying on staleness markers +- When in doubt, Read the file — that resolves any uncertainty immediately ## Concatenation Format diff --git a/shared/skills/dream-knowledge/SKILL.md b/shared/skills/dream-knowledge/SKILL.md deleted file mode 100644 index 2b731708..00000000 --- a/shared/skills/dream-knowledge/SKILL.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -name: dream-knowledge -description: "Dream agent per-task procedure for the 'knowledge' task. Loaded EXPLICITLY by the Dream agent via the Skill tool when the agent is spawned for a knowledge task — not auto-activated. Handles stale feature knowledge base refresh and index updates." -allowed-tools: Read, Bash, Write, Edit, Glob, Grep ---- - -# Dream Task: knowledge - -## Iron Law - -> **REFRESH FROM THE LIVE CODEBASE — NEVER FROM MEMORY OR STALE CONTEXT** -> -> Every KNOWLEDGE.md update must be grounded in the current files listed under -> `directories` and `referencedFiles`. Do not carry forward assertions from the -> existing KNOWLEDGE.md without re-verifying them against the current source. - -This skill is loaded by the Dream agent after it has claimed the knowledge marker(s). -The agent has already done: claim (mv .json → .processing) and multi-marker merge -(union staleSlugs arrays; use any worktreePath). - -## Procedure - -Touch all claimed `.devflow/dream/knowledge.{session}.processing` files. - -Read the merged `staleSlugs` and `worktreePath`. - -For each stale slug: -1. Read `.devflow/features/index.json` for the entry's `directories` and `referencedFiles`. -2. Read `.devflow/features/{slug}/KNOWLEDGE.md` (existing content). -3. Read the referenced files and directories to understand current state. -4. **Author an updated KNOWLEDGE.md** (LLM writes real content — no canned filler): - Cover: architecture, key patterns, anti-patterns, gotchas, integration points, key files. -5. Write the updated file. -6. Update the index entry: - ```bash - node "$HOME/.devflow/scripts/hooks/lib/feature-knowledge.cjs" update-index \ - "{worktreePath}" \ - --slug="{slug}" \ - --name="{name}" \ - --directories='["{dir1}","{dir2}"]' \ - --referencedFiles='["{file1}"]' \ - --description="{description}" - ``` - -After all slugs, write the refresh timestamp: -```bash -date +%s > .devflow/features/.knowledge-last-refresh -``` - -`.devflow/` is gitignored by default (ADR-021) — refreshed knowledge bases stay local. -Delete all claimed `.processing` markers on success. - -**On any failure**: leave `.processing` files in place (dream-recover will retry them). diff --git a/shared/skills/feature-knowledge/SKILL.md b/shared/skills/feature-knowledge/SKILL.md index aa4ec1b9..b6eab94f 100644 --- a/shared/skills/feature-knowledge/SKILL.md +++ b/shared/skills/feature-knowledge/SKILL.md @@ -96,7 +96,6 @@ name: {human-readable name} description: "Use when [specific scenarios]. Keywords: [relevant terms]." category: [architecture | conventions | component-patterns | domain-knowledge | lessons-learned] directories: [{dir prefixes}] -referencedFiles: [{5-10 key files for staleness tracking}] created: {ISO date} updated: {ISO date} --- @@ -229,7 +228,6 @@ Run through this before writing. If any check fails, go back and fix it. - [ ] Category is correct and main sections follow the matching template - [ ] Description field starts with "Use when" and includes keywords - [ ] File stays under 500 lines (split if necessary) -- [ ] 5-10 referenced files selected for staleness tracking **Connections:** - [ ] Cross-references to related feature knowledge entries and ADR/PF entries in Related section @@ -238,21 +236,26 @@ Run through this before writing. If any check fails, go back and fix it. --- -## Result Output +## Index Registration -After writing KNOWLEDGE.md, write a result JSON for the host process: +After writing KNOWLEDGE.md, update the index cache directly: -**Filename**: `.create-result.json` (new feature knowledge entries) or `.refresh-result.json` (refreshes) -**Location**: Same directory as KNOWLEDGE.md (`.devflow/features/{slug}/`) +**File**: `{worktree}/.devflow/features/index.md` +**Operation**: Read-modify-write — replace the existing line for this slug if present, or append. -```json -{ - "referencedFiles": ["src/path/to/key-file.ts", "src/path/to/other.ts"], - "description": "Use when working on {feature area}. Keywords: {terms}." -} +**Line format**: ``` +- **{slug}** — {areas} — {Use-when description} +``` + +Where: +- `{slug}` matches the `feature:` frontmatter field +- `{areas}` is a comma-separated summary of the `directories:` frontmatter field +- `{Use-when description}` is the `description:` frontmatter field value (the full "Use when..." sentence) + +If `index.md` does not exist, create it with just this line. If the file already has an entry for this slug, replace that line in-place. This is the discoverable cache read by `knowledge_load()` — the KNOWLEDGE.md frontmatter is always authoritative. -The host process reads this result file and updates `.devflow/features/index.json` — do NOT update the index directly. +Do NOT write `.create-result.json` or `.refresh-result.json`. Do NOT call any external script to update an index. Write directly. --- @@ -267,7 +270,6 @@ name: Third-Party Integrations description: "Use when adding a new vendor integration, implementing API clients, or connecting to external services. Keywords: integration, vendor, API client, webhook, spawn, config." category: component-patterns directories: [src/lib/] -referencedFiles: [src/lib/config.ts, src/lib/claude.ts, src/lib/skill-installer.ts, src/lib/deploy.ts] created: 2026-04-30 updated: 2026-04-30 --- diff --git a/src/cli/commands/init.ts b/src/cli/commands/init.ts index b7cda164..7ef0a08e 100644 --- a/src/cli/commands/init.ts +++ b/src/cli/commands/init.ts @@ -41,7 +41,7 @@ import { addContextHook, removeContextHook, hasContextHook } from './context.js' import { manageSentinel } from '../utils/sentinel.js'; import { writeFileAtomicExclusive } from '../utils/fs-atomic.js'; import { writeConfig as writeDreamConfig } from '../utils/dream-config.js'; -import { getFeaturesDir, getFeaturesIndexPath, getFeaturesDisabledSentinel, getDecisionsDisabledSentinel, getPendingTurnsPath, getPendingTurnsProcessingPath } from '../utils/project-paths.js'; +import { getDecisionsDisabledSentinel, getPendingTurnsPath, getPendingTurnsProcessingPath } from '../utils/project-paths.js'; import * as os from 'os'; // Re-export pure functions for tests (canonical source is post-install.ts) @@ -1100,7 +1100,7 @@ export const initCommand = new Command('init') // kb → knowledge rename: hook scripts replaced by session-end-knowledge-refresh / background-knowledge-refresh 'session-end-kb-refresh', 'background-kb-refresh', - // kb → knowledge rename: CJS module replaced by feature-knowledge.cjs + // kb → knowledge rename: CJS module (now removed — knowledge pipeline is write-through) 'lib/feature-kb.cjs', // decisions agent decoupling: background-learning replaced by TypeScript CLI (devflow learn --run-background) 'background-learning', @@ -1140,7 +1140,8 @@ export const initCommand = new Command('init') // Memory hooks — always remove-then-add to upgrade hook format (e.g., .sh → run-hook) // Memory hooks include the unified dream hooks (dream-dispatch, dream-capture, - // dream-evaluate) which handle memory, decisions, and knowledge in the background. + // dream-evaluate) which handle memory and decisions in the background. + // Knowledge is handled in-command via write-through (knowledge_writeback MDS partial). const cleaned = removeMemoryHooks(content); content = memoryEnabled ? addMemoryHooks(cleaned, devflowDir) : cleaned; @@ -1183,25 +1184,11 @@ export const initCommand = new Command('init') } catch { /* settings.json may not exist yet */ } - // Create .devflow/features/ directory with empty index (feature knowledge bases) - // .devflow/features/ is gitignored by default (under .devflow/); opt-in to share. - if (gitRoot && knowledgeEnabled) { - const featuresDir = getFeaturesDir(gitRoot); - await fs.mkdir(featuresDir, { recursive: true }); - const featuresIndexPath = getFeaturesIndexPath(gitRoot); - try { - await fs.access(featuresIndexPath); - } catch { - await fs.writeFile(featuresIndexPath, JSON.stringify({ version: 1, features: {} }, null, 2) + '\n'); - if (verbose) { - p.log.success('.devflow/features/index.json created'); - } - } - } - - // Manage runtime-disable sentinels for session-start-context gating + // Manage runtime-disable sentinel for decisions gating. + // Note: features/.disabled sentinel for knowledge is no longer managed here; + // write-through (knowledge_writeback MDS partial) creates features/ lazily, + // and the config gate (dream config `knowledge` field) is the sole opt-out. if (gitRoot) { - await manageSentinel(getFeaturesDisabledSentinel(gitRoot), knowledgeEnabled); await manageSentinel(getDecisionsDisabledSentinel(gitRoot), decisionsEnabled); } diff --git a/src/cli/commands/knowledge/check.ts b/src/cli/commands/knowledge/check.ts deleted file mode 100644 index 5da4aabe..00000000 --- a/src/cli/commands/knowledge/check.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as p from '@clack/prompts'; -import color from 'picocolors'; -import { getFeatureKnowledge, getWorktreePath } from './shared.js'; - -export async function handleCheck(): Promise { - p.intro(color.cyan('Knowledge Base Staleness Check')); - - const worktreePath = await getWorktreePath(); - // Load index once and pass to both functions to avoid double file reads - const index = getFeatureKnowledge().loadIndex(worktreePath); - const kbs = getFeatureKnowledge().listEntries(worktreePath, index); - const staleness = getFeatureKnowledge().checkAllStaleness(worktreePath, index); - - if (kbs.length === 0) { - p.log.info('No feature knowledge bases 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} knowledge base${staleCount === 1 ? '' : 's'} need refresh. Run: ${color.cyan('devflow knowledge refresh')}`); - } else { - p.outro('All knowledge bases are current'); - } -} diff --git a/src/cli/commands/knowledge/create.ts b/src/cli/commands/knowledge/create.ts deleted file mode 100644 index 230a5149..00000000 --- a/src/cli/commands/knowledge/create.ts +++ /dev/null @@ -1,85 +0,0 @@ -import * as p from '@clack/prompts'; -import color from 'picocolors'; -import { isClaudeCliAvailable } from '../../utils/cli.js'; -import { runKnowledgeAgent, loadDecisionsContext } from '../../utils/knowledge-agent.js'; -import { getFeatureKnowledge, exitOnInvalidSlug, getWorktreePath } from './shared.js'; - -export async function handleCreate(slug: string): Promise { - exitOnInvalidSlug(slug); - p.intro(color.cyan(`Create Feature Knowledge Base: ${slug}`)); - - if (!isClaudeCliAvailable()) { - p.log.error('claude CLI not found on PATH. Install Claude Code first.'); - process.exit(1); - } - - const worktreePath = await 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('Creating knowledge base...'); - - const decisionsContext = loadDecisionsContext(worktreePath); - - const prompt = [ - `You are the Knowledge agent. Create a feature knowledge base for the following area:`, - ``, - `FEATURE_SLUG: ${slug}`, - `FEATURE_NAME: ${name as string}`, - `DIRECTORIES: [${dirList}]`, - `WORKTREE_PATH: ${worktreePath}`, - `DECISIONS_CONTEXT: ${decisionsContext}`, - ``, - `STEP 1: Load the devflow:feature-knowledge skill using the Skill tool (skill: "devflow:feature-knowledge").`, - `STEP 2: Read .devflow/features/index.json (if it exists) to see what other knowledge bases exist for cross-referencing.`, - `STEP 3: Execute the skill's 4-phase process (Scan → Extract → Distill → Forge) exactly as specified.`, - ` - You have no pre-computed exploration outputs — perform your own exploration in Phase 1 (Scan) and Phase 2 (Extract).`, - ` - Use DECISIONS_CONTEXT to cross-reference ADR/PF entries in the Related section.`, - ``, - `After writing KNOWLEDGE.md, write .devflow/features/${slug}/.create-result.json with:`, - `{`, - ` "referencedFiles": [<5-10 key files from the explored directories for staleness tracking>],`, - ` "description": ""`, - `}`, - ``, - `Create the directory if needed. Report KB_STATUS when done.`, - ].join('\n'); - - try { - const { result } = await runKnowledgeAgent({ worktreePath, slug, prompt, resultFileName: '.create-result.json' }); - - getFeatureKnowledge().updateIndex(worktreePath, { - slug, - name: name as string, - directories, - referencedFiles: result.referencedFiles ?? [], - description: result.description, - createdBy: 'devflow-knowledge', - }); - - s.stop('Knowledge base created successfully'); - p.log.success(`Knowledge base written to .devflow/features/${slug}/KNOWLEDGE.md`); - } catch (err) { - s.stop('Knowledge base 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 knowledge list`)} to see all knowledge bases`); -} diff --git a/src/cli/commands/knowledge/index.ts b/src/cli/commands/knowledge/index.ts index c2c13a60..b76e9d15 100644 --- a/src/cli/commands/knowledge/index.ts +++ b/src/cli/commands/knowledge/index.ts @@ -1,19 +1,14 @@ /** * devflow knowledge — per-feature knowledge base management. - * Thin router that delegates each subcommand to its own module. + * Thin router that delegates to list (read-only) and toggle (enable/disable/status). + * + * The heavy CRUD subcommands (create/check/refresh/remove) are deleted — knowledge + * bases are created automatically via write-through (knowledge_writeback MDS partial). */ import { Command } from 'commander'; import { handleToggle } from './toggle.js'; import { handleList } from './list.js'; -import { handleCheck } from './check.js'; -import { handleCreate } from './create.js'; -import { handleRefresh } from './refresh.js'; -import { handleRemove } from './remove.js'; - -// Re-export agent result helpers for callers that import from knowledge/index.js -export type { AgentResult } from '../../utils/agent-result.js'; -export { readAgentResult } from '../../utils/agent-result.js'; export const knowledgeCommand = new Command('knowledge') .description('Manage per-feature knowledge bases') @@ -26,35 +21,7 @@ export const knowledgeCommand = new Command('knowledge') knowledgeCommand .command('list') - .description('List all feature knowledge bases with staleness status') + .description('List all feature knowledge bases') .action(async () => { await handleList(); }); - -knowledgeCommand - .command('check') - .description('Check all knowledge bases for staleness') - .action(async () => { - await handleCheck(); - }); - -knowledgeCommand - .command('create ') - .description('Create a new knowledge base via claude -p exploration') - .action(async (slug: string) => { - await handleCreate(slug); - }); - -knowledgeCommand - .command('refresh [slug]') - .description('Refresh stale knowledge base(s). Omit slug to refresh all stale knowledge bases.') - .action(async (slug?: string) => { - await handleRefresh(slug); - }); - -knowledgeCommand - .command('remove ') - .description('Remove a knowledge base and its index entry') - .action(async (slug: string) => { - await handleRemove(slug); - }); diff --git a/src/cli/commands/knowledge/list.ts b/src/cli/commands/knowledge/list.ts index 092f7282..9c91c9da 100644 --- a/src/cli/commands/knowledge/list.ts +++ b/src/cli/commands/knowledge/list.ts @@ -1,44 +1,131 @@ +/** + * devflow knowledge list — list feature knowledge bases. + * + * Reads .devflow/features/index.md directly (write-through cache), or falls back + * to globbing KNOWLEDGE.md frontmatter when index.md is absent/empty. No .cjs + * engine, no index.json, no staleness detection (verify-against-code is the freshness + * mechanism per the new write-through model). + */ +import { promises as fs } from 'fs'; +import * as path from 'path'; import * as p from '@clack/prompts'; import color from 'picocolors'; -import { getFeatureKnowledge, getWorktreePath } from './shared.js'; +import { getGitRoot } from '../../utils/git.js'; +import { getFeaturesDir } from '../../utils/project-paths.js'; + +/** + * A lightweight entry parsed from either index.md lines or KNOWLEDGE.md frontmatter. + */ +interface KbEntry { + slug: string; + areas: string; + description: string; +} + +/** + * Parse entries from index.md lines. + * Line format: `- **{slug}** — {areas} — {Use-when description}` + */ +function parseIndexMd(content: string): KbEntry[] { + const entries: KbEntry[] = []; + for (const line of content.split('\n')) { + const m = line.match(/^-\s+\*\*([^*]+)\*\*\s+—\s+([^—]+)\s+—\s+(.+)$/); + if (m) { + entries.push({ slug: m[1].trim(), areas: m[2].trim(), description: m[3].trim() }); + } + } + return entries; +} + +/** + * Parse the slug and description from a KNOWLEDGE.md file's YAML frontmatter. + * Returns null when frontmatter is absent or unparseable. + */ +function parseFrontmatter(content: string): { slug: string; name: string; description: string; directories: string[] } | null { + const fmMatch = content.match(/^---\n([\s\S]*?)\n---/); + if (!fmMatch) return null; + const fm = fmMatch[1]; + const get = (key: string): string => { + const m = fm.match(new RegExp(`^${key}:\\s*(.+)$`, 'm')); + return m ? m[1].trim().replace(/^"(.*)"$/, '$1') : ''; + }; + const getArr = (key: string): string[] => { + const m = fm.match(new RegExp(`^${key}:\\s*\\[([^\\]]+)\\]`, 'm')); + if (!m) return []; + return m[1].split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')); + }; + const slug = get('feature'); + const name = get('name'); + const description = get('description'); + const directories = getArr('directories'); + if (!slug) return null; + return { slug, name, description, directories }; +} + +/** + * Resolve the git root for the current directory, falling back to cwd. + */ +async function getWorktreePath(): Promise { + return (await getGitRoot()) ?? process.cwd(); +} export async function handleList(): Promise { p.intro(color.cyan('Feature Knowledge Bases')); const worktreePath = await getWorktreePath(); - // Load index once and pass to both functions to avoid double file reads - const index = getFeatureKnowledge().loadIndex(worktreePath); - const kbs = getFeatureKnowledge().listEntries(worktreePath, index); - const staleness = getFeatureKnowledge().checkAllStaleness(worktreePath, index); + const featuresDir = getFeaturesDir(worktreePath); + const indexMdPath = path.join(featuresDir, 'index.md'); + + let entries: KbEntry[] = []; + + // Try index.md cache first + try { + const indexContent = await fs.readFile(indexMdPath, 'utf-8'); + entries = parseIndexMd(indexContent); + } catch { + /* index.md absent — fall through to frontmatter glob */ + } + + // Fallback: glob features/*/KNOWLEDGE.md and parse frontmatter + if (entries.length === 0) { + try { + const slugDirs = await fs.readdir(featuresDir, { withFileTypes: true }); + for (const dirent of slugDirs) { + if (!dirent.isDirectory() || dirent.name.startsWith('.')) continue; + const kbPath = path.join(featuresDir, dirent.name, 'KNOWLEDGE.md'); + try { + const kbContent = await fs.readFile(kbPath, 'utf-8'); + const fm = parseFrontmatter(kbContent); + if (fm) { + entries.push({ + slug: fm.slug, + areas: fm.directories.join(', ') || dirent.name, + description: fm.description || fm.name, + }); + } + } catch { /* KNOWLEDGE.md absent or unreadable — skip */ } + } + } catch { /* featuresDir absent — no KBs */ } + } - if (kbs.length === 0) { + if (entries.length === 0) { p.log.info( - 'No feature knowledge bases found. Knowledge bases are created automatically during planning, or manually via ' + - color.cyan('devflow knowledge create ') + '.' + 'No feature knowledge bases found. Knowledge bases are created automatically ' + + 'when workflows (implement, resolve, self-review, explore, debug) detect documented area changes.', ); p.outro(''); return; } - p.log.info(`Found ${kbs.length} feature knowledge base${kbs.length === 1 ? '' : 's'} in ${color.dim(worktreePath)}`); + p.log.info(`Found ${entries.length} feature knowledge base${entries.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(` updated: ${color.dim(kb.lastUpdated)}`); - console.log(` dirs: ${color.dim(kb.directories.join(', '))}`); - if (isStale && staleInfo.changedFiles.length > 0) { - 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}`); - } + for (const entry of entries) { + console.log(` ${color.bold(entry.slug)}`); + console.log(` areas: ${color.dim(entry.areas)}`); + console.log(` use when: ${color.dim(entry.description)}`); console.log(''); } - p.outro(`Run ${color.cyan('devflow knowledge check')} to see staleness details`); + p.outro(''); } diff --git a/src/cli/commands/knowledge/refresh.ts b/src/cli/commands/knowledge/refresh.ts deleted file mode 100644 index b902d953..00000000 --- a/src/cli/commands/knowledge/refresh.ts +++ /dev/null @@ -1,101 +0,0 @@ -import * as p from '@clack/prompts'; -import color from 'picocolors'; -import { isClaudeCliAvailable } from '../../utils/cli.js'; -import { runKnowledgeAgent, loadDecisionsContext } from '../../utils/knowledge-agent.js'; -import { getFeatureKnowledge, exitOnInvalidSlug, getWorktreePath } from './shared.js'; - -export async function handleRefresh(targetSlug?: string): Promise { - p.intro(color.cyan(targetSlug ? `Refresh Knowledge Base: ${targetSlug}` : 'Refresh Stale Knowledge Bases')); - - if (targetSlug) exitOnInvalidSlug(targetSlug); - - if (!isClaudeCliAvailable()) { - p.log.error('claude CLI not found on PATH. Install Claude Code first.'); - process.exit(1); - } - - const worktreePath = await getWorktreePath(); - - // Determine which slugs to refresh - // Load index once and reuse across staleness check + listEntries to avoid double reads - const index = getFeatureKnowledge().loadIndex(worktreePath); - const kbs = getFeatureKnowledge().listEntries(worktreePath, index); - - let slugsToRefresh: string[]; - let stalenessMap: Record | undefined; - if (targetSlug) { - slugsToRefresh = [targetSlug]; - } else { - stalenessMap = getFeatureKnowledge().checkAllStaleness(worktreePath, index); - slugsToRefresh = Object.entries(stalenessMap) - .filter(([, info]) => info.stale) - .map(([s]) => s); - } - - if (slugsToRefresh.length === 0) { - p.log.success('No stale knowledge bases found — everything is current.'); - p.outro(''); - return; - } - - p.log.info(`Refreshing ${slugsToRefresh.length} knowledge base${slugsToRefresh.length === 1 ? '' : 's'}: ${slugsToRefresh.join(', ')}`); - - const decisionsContext = loadDecisionsContext(worktreePath); - - for (const slug of slugsToRefresh) { - const s = p.spinner(); - s.start(`Refreshing ${slug}...`); - - const staleInfo = stalenessMap?.[slug] ?? getFeatureKnowledge().checkStaleness(worktreePath, slug); - const entry = kbs.find((k: { slug: string }) => k.slug === slug); - const featureName = entry?.name ?? slug; - const directories = entry?.directories ?? []; - - const prompt = [ - `You are the Knowledge agent refreshing a stale feature knowledge base.`, - ``, - `FEATURE_SLUG: ${slug}`, - `FEATURE_NAME: ${featureName}`, - `DIRECTORIES: ${JSON.stringify(directories)}`, - `WORKTREE_PATH: ${worktreePath}`, - `CHANGED_FILES: ${JSON.stringify(staleInfo.changedFiles)}`, - `DECISIONS_CONTEXT: ${decisionsContext}`, - ``, - `STEP 1: Load the devflow:feature-knowledge skill using the Skill tool (skill: "devflow:feature-knowledge").`, - `STEP 2: Read .devflow/features/${slug}/KNOWLEDGE.md to understand the existing knowledge base content and structure.`, - `STEP 3: Read the CHANGED_FILES to understand what changed in the codebase.`, - `STEP 4: Update the knowledge base following the skill's quality standards:`, - ` - Maintain the correct category template structure`, - ` - Ensure code examples follow the 3-part rule (description, inline comments, takeaways)`, - ` - Update cross-references in the Related section using DECISIONS_CONTEXT`, - ` - Preserve any manually added content the user edited in`, - ` - Do NOT regenerate from scratch — update only what changed`, - `STEP 5: Write the updated knowledge base to .devflow/features/${slug}/KNOWLEDGE.md`, - `STEP 6: Write .devflow/features/${slug}/.refresh-result.json with:`, - `{`, - ` "referencedFiles": [<5-10 key files from explored directories for staleness tracking>],`, - ` "description": ""`, - `}`, - ].join('\n'); - - try { - const { result } = await runKnowledgeAgent({ worktreePath, slug, prompt, resultFileName: '.refresh-result.json' }); - - getFeatureKnowledge().updateIndex(worktreePath, { - slug, - name: featureName, - directories, - referencedFiles: result.referencedFiles ?? entry?.referencedFiles ?? [], - description: result.description, - createdBy: 'devflow-knowledge', - }); - - s.stop(`${slug} refreshed`); - } catch (err) { - s.stop(`${slug} refresh failed`); - p.log.error(`Error: ${err instanceof Error ? err.message : String(err)}`); - } - } - - p.outro('Refresh complete'); -} diff --git a/src/cli/commands/knowledge/remove.ts b/src/cli/commands/knowledge/remove.ts deleted file mode 100644 index fd183171..00000000 --- a/src/cli/commands/knowledge/remove.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as p from '@clack/prompts'; -import color from 'picocolors'; -import { getFeatureKnowledge, exitOnInvalidSlug, getWorktreePath } from './shared.js'; - -export async function handleRemove(slug: string): Promise { - exitOnInvalidSlug(slug); - p.intro(color.cyan(`Remove Knowledge Base: ${slug}`)); - - const confirmed = await p.confirm({ - message: `Remove knowledge base '${slug}' and its KNOWLEDGE.md? This cannot be undone.`, - initialValue: false, - }); - if (p.isCancel(confirmed) || !confirmed) { - p.cancel('Removal cancelled.'); - return; - } - - const worktreePath = await getWorktreePath(); - - try { - getFeatureKnowledge().removeEntry(worktreePath, slug); - p.log.success(`Knowledge base '${slug}' removed`); - } catch (err) { - p.log.error(`Failed to remove knowledge base: ${err instanceof Error ? err.message : String(err)}`); - process.exit(1); - } - - p.outro('Done'); -} diff --git a/src/cli/commands/knowledge/shared.ts b/src/cli/commands/knowledge/shared.ts deleted file mode 100644 index a5b33135..00000000 --- a/src/cli/commands/knowledge/shared.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Shared utilities and the feature-knowledge module reference for knowledge subcommands. - * All subcommand files import from here to avoid duplication. - */ -import * as path from 'path'; -import { createRequire } from 'module'; -import * as p from '@clack/prompts'; -import { getGitRoot } from '../../utils/git.js'; -import { getDevFlowDirectory } from '../../utils/paths.js'; - -const _require = createRequire(import.meta.url); - -export type KnowledgeIndex = { version: number; features: Record } | null; -export type KnowledgeEntry = { slug: string; name: string; directories: string[]; lastUpdated: string; referencedFiles?: string[]; description?: string; createdBy?: string }; - -export interface FeatureKnowledgeModule { - loadIndex: (worktreePath: string) => KnowledgeIndex; - listEntries: (worktreePath: string, cachedIndex?: KnowledgeIndex) => KnowledgeEntry[]; - checkAllStaleness: (worktreePath: string, cachedIndex?: KnowledgeIndex) => 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[]; createdBy?: string }, lockTimeoutMs?: number) => void; -} - -let _featureKnowledge: FeatureKnowledgeModule | undefined; - -export function getFeatureKnowledge(): FeatureKnowledgeModule { - if (!_featureKnowledge) { - _featureKnowledge = _require( - path.join(getDevFlowDirectory(), 'scripts', 'hooks', 'lib', 'feature-knowledge.cjs'), - ); - } - return _featureKnowledge!; -} - -/** - * Validate a knowledge base slug and exit with an error message if invalid. - */ -export function exitOnInvalidSlug(slug: string): void { - try { - getFeatureKnowledge().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. - */ -export async function getWorktreePath(): Promise { - return (await getGitRoot()) ?? process.cwd(); -} diff --git a/src/cli/commands/knowledge/toggle.ts b/src/cli/commands/knowledge/toggle.ts index 3ddfafea..7d29bc96 100644 --- a/src/cli/commands/knowledge/toggle.ts +++ b/src/cli/commands/knowledge/toggle.ts @@ -1,15 +1,43 @@ +/** + * Handle the enable/disable/status toggle actions for `devflow knowledge`. + * + * The sole opt-out mechanism is the dream config `knowledge` field (config-only gate per ADR-001). + * The features/.disabled sentinel and index.json bootstrap are no longer managed here — + * write-through creates features/ lazily, and the sentinel machinery is deleted. + */ import { promises as fs } from 'fs'; +import * as path from 'path'; import * as p from '@clack/prompts'; import color from 'picocolors'; +import { getGitRoot } from '../../utils/git.js'; import { getDevFlowDirectory } from '../../utils/paths.js'; import { readManifest, writeManifest } from '../../utils/manifest.js'; -import { getFeatureKnowledge, getWorktreePath } from './shared.js'; import { updateFeature, isFeatureEnabled } from '../../utils/dream-config.js'; -import { getFeaturesDir, getFeaturesIndexPath, getFeaturesDisabledSentinel } from '../../utils/project-paths.js'; +import { getFeaturesDir } from '../../utils/project-paths.js'; + +async function getWorktreePath(): Promise { + return (await getGitRoot()) ?? process.cwd(); +} + +/** Count KNOWLEDGE.md files present in features/ */ +async function countKnowledgeBases(worktreePath: string): Promise { + const featuresDir = getFeaturesDir(worktreePath); + try { + const entries = await fs.readdir(featuresDir, { withFileTypes: true }); + let count = 0; + for (const dirent of entries) { + if (!dirent.isDirectory() || dirent.name.startsWith('.')) continue; + try { + await fs.access(path.join(featuresDir, dirent.name, 'KNOWLEDGE.md')); + count++; + } catch { /* KNOWLEDGE.md absent */ } + } + return count; + } catch { + return 0; + } +} -/** - * Handle the enable/disable/status toggle actions for `devflow knowledge`. - */ export async function handleToggle(options: { enable?: boolean; disable?: boolean; status?: boolean }): Promise { if (!options.enable && !options.disable && !options.status) return; @@ -19,20 +47,7 @@ export async function handleToggle(options: { enable?: boolean; disable?: boolea if (options.enable) { p.intro(color.cyan('Enable Feature Knowledge Bases')); - // Create .devflow/features/index.json if missing - const featuresDir = getFeaturesDir(worktreePath); - await fs.mkdir(featuresDir, { recursive: true }); - const indexPath = getFeaturesIndexPath(worktreePath); - 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(getFeaturesDisabledSentinel(worktreePath)); } catch { /* doesn't exist */ } - - // Update dream config + // Update dream config (the sole gate — config-only per ADR-001) await updateFeature(worktreePath, 'knowledge', true); // Update manifest @@ -44,17 +59,13 @@ export async function handleToggle(options: { enable?: boolean; disable?: boolea } p.log.success('Feature knowledge bases enabled'); - p.outro(`Run ${color.cyan('devflow knowledge create ')} to create a knowledge base.`); + p.log.info('Knowledge bases are created automatically when workflows detect documented area changes.'); + p.outro(''); } else if (options.disable) { p.intro(color.cyan('Disable Feature Knowledge Bases')); - // Create .disabled sentinel - const featuresDir = getFeaturesDir(worktreePath); - await fs.mkdir(featuresDir, { recursive: true }); - await fs.writeFile(getFeaturesDisabledSentinel(worktreePath), '', 'utf-8'); - - // Update dream config + // Update dream config (the sole gate — config-only per ADR-001) await updateFeature(worktreePath, 'knowledge', false); // Update manifest @@ -66,32 +77,18 @@ export async function handleToggle(options: { enable?: boolean; disable?: boolea } p.log.success('Feature knowledge bases disabled'); - p.log.info('Existing knowledge bases preserved. Manual commands (create/refresh) still work.'); + p.log.info('Existing knowledge bases preserved. Write-back skipped while disabled.'); p.outro(''); } else { // options.status p.intro(color.cyan('Feature Knowledge Status')); - // Check dream config enabled state const enabled = await isFeatureEnabled(worktreePath, 'knowledge'); + const kbCount = await countKnowledgeBases(worktreePath); - // Check sentinel - let disabled = false; - try { - await fs.access(getFeaturesDisabledSentinel(worktreePath)); - disabled = true; - } catch { /* not disabled */ } - - // Count knowledge bases - const kbs = getFeatureKnowledge().listEntries(worktreePath); - - p.log.info(`Status: ${(enabled && !disabled) ? color.green('enabled') : color.yellow('disabled')}`); - p.log.info(`Config: ${enabled ? color.green('enabled') : color.dim('disabled')}`); - p.log.info(`Knowledge bases: ${kbs.length}`); - if (disabled) { - p.log.info(`Sentinel: ${color.yellow('.devflow/features/.disabled present')}`); - } + p.log.info(`Status: ${enabled ? color.green('enabled') : color.yellow('disabled')}`); + p.log.info(`Knowledge bases: ${kbCount}`); p.outro(''); } } diff --git a/src/cli/plugins.ts b/src/cli/plugins.ts index 435a04e7..adf04396 100644 --- a/src/cli/plugins.ts +++ b/src/cli/plugins.ts @@ -56,7 +56,7 @@ export const DEVFLOW_PLUGINS: PluginDefinition[] = [ // plugin, unlike skills which install universally). Predecessor was the universally // installed `devflow:sidecar` skill; core-skills preserves that guarantee. agents: ['dream'], - skills: ['apply-decisions', 'apply-feature-knowledge', 'software-design', 'docs-framework', 'git', 'boundary-validation', 'test-driven-development', 'testing', 'dependency-research', 'dream-decisions', 'dream-knowledge', 'dream-curation'], + skills: ['apply-decisions', 'apply-feature-knowledge', 'software-design', 'docs-framework', 'git', 'boundary-validation', 'test-driven-development', 'testing', 'dependency-research', 'dream-decisions', 'dream-curation'], rules: ['security', 'engineering', 'quality', 'reliability'], }, { @@ -72,7 +72,7 @@ export const DEVFLOW_PLUGINS: PluginDefinition[] = [ description: 'Complete task implementation workflow - accepts plan documents, issues, or task descriptions', commands: ['/implement'], agents: ['git', 'coder', 'simplifier', 'scrutinizer', 'evaluator', 'tester', 'validator'], - skills: ['patterns', 'qa', 'quality-gates', 'worktree-support', 'apply-feature-knowledge'], + skills: ['patterns', 'qa', 'quality-gates', 'worktree-support', 'feature-knowledge', 'apply-feature-knowledge'], rules: [], }, { @@ -88,7 +88,7 @@ export const DEVFLOW_PLUGINS: PluginDefinition[] = [ description: 'Process and fix code review issues with risk assessment', commands: ['/resolve'], agents: ['git', 'resolver', 'simplifier'], - skills: ['patterns', 'security', 'worktree-support', 'apply-feature-knowledge'], + skills: ['patterns', 'security', 'worktree-support', 'feature-knowledge', 'apply-feature-knowledge'], rules: [], }, { @@ -96,7 +96,7 @@ export const DEVFLOW_PLUGINS: PluginDefinition[] = [ description: 'Debugging workflows with competing hypothesis investigation via parallel subagents', commands: ['/debug'], agents: ['git', 'synthesizer'], - skills: ['git', 'worktree-support', 'apply-feature-knowledge'], + skills: ['git', 'worktree-support', 'feature-knowledge', 'apply-feature-knowledge'], rules: [], }, { @@ -128,7 +128,7 @@ export const DEVFLOW_PLUGINS: PluginDefinition[] = [ description: 'Self-review workflow: Simplifier + Scrutinizer for code quality', commands: ['/self-review'], agents: ['simplifier', 'scrutinizer', 'validator'], - skills: ['quality-gates', 'software-design', 'worktree-support', 'apply-feature-knowledge'], + skills: ['quality-gates', 'software-design', 'worktree-support', 'feature-knowledge', 'apply-feature-knowledge'], rules: [], }, { @@ -551,6 +551,9 @@ const LEGACY_SKILLS_V2X: string[] = [ 'devflow:dream-memory', // v3.x agent-teams removal: namespaced name for cleanup of installed devflow:agent-teams skill 'devflow:agent-teams', + // v3.x dream-knowledge removal: namespaced name for cleanup of installed devflow:dream-knowledge skill + // (dream-knowledge was removed when knowledge write-through replaced the Dream background pipeline) + 'devflow:dream-knowledge', // v2.x ambient refinements: devflow:-prefixed triage/guided/router names for cleanup 'devflow:router', 'devflow:implement:triage', diff --git a/src/cli/utils/agent-result.ts b/src/cli/utils/agent-result.ts deleted file mode 100644 index 639621a4..00000000 --- a/src/cli/utils/agent-result.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { promises as fs } from 'fs'; - -export interface AgentResult { - referencedFiles?: string[]; - description?: string; -} - -/** - * Read the result JSON written by the Knowledge agent. - * Returns an empty object when the file is missing, corrupt, or non-object JSON. - */ -export async function readAgentResult(resultPath: string): Promise { - let raw: unknown; - try { - raw = JSON.parse(await fs.readFile(resultPath, 'utf8')); - } catch { - return {}; - } - if (typeof raw !== 'object' || raw === null) return {}; - const data = raw as Record; - const result: AgentResult = {}; - 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; -} diff --git a/src/cli/utils/knowledge-agent.ts b/src/cli/utils/knowledge-agent.ts deleted file mode 100644 index cdf5f593..00000000 --- a/src/cli/utils/knowledge-agent.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { execFile, execFileSync } from 'child_process'; -import { promisify } from 'util'; -import { existsSync, promises as fs } from 'fs'; -import * as path from 'path'; -import { readAgentResult, type AgentResult } from './agent-result.js'; -import { getDevFlowDirectory } from './paths.js'; -import { getKnowledgePath } from './project-paths.js'; - -const execFileAsync = promisify(execFile); - -/** Tools passed to `claude -p` when spawning the Knowledge agent. */ -const KNOWLEDGE_AGENT_TOOLS = 'Read,Grep,Glob,Write,Skill'; - -/** - * Load the compact DECISIONS_CONTEXT index (ADR/PF entries) for cross-referencing. - * Returns '(none)' when no decisions files exist or the script is not installed. - */ -export function loadDecisionsContext(worktreePath: string): string { - const scriptPath = path.join( - getDevFlowDirectory(), 'scripts', 'hooks', 'lib', 'decisions-index.cjs', - ); - - if (!existsSync(scriptPath)) return '(none)'; - - try { - return execFileSync('node', [scriptPath, 'index', worktreePath], { - encoding: 'utf8', - timeout: 10_000, - }).trim(); - } catch { - return '(none)'; - } -} - -export interface RunKnowledgeAgentOptions { - worktreePath: string; - slug: string; - /** Prompt to pass to the Knowledge agent. */ - prompt: string; - /** Result filename: '.create-result.json' or '.refresh-result.json' */ - resultFileName: string; -} - -export interface RunKnowledgeAgentResult { - result: AgentResult; -} - -/** - * Spawn the Knowledge agent via `claude -p`, then read and clean up the result file. - * - * The agent is expected to write a JSON file at - * `.devflow/features/{slug}/{resultFileName}` with `referencedFiles` and optionally `description`. - * If the file is absent (agent failure), an empty AgentResult is returned. - * - * Using async execFile keeps the event loop free so the clack spinner can - * animate while the agent runs. - * - * Uses `--dangerously-skip-permissions` because `claude -p` is non-interactive - * and cannot prompt for approval; tool access is restricted via `--allowedTools`. - * - * @throws When `claude` exits with a non-zero status (propagates execFile error). - */ -export async function runKnowledgeAgent(opts: RunKnowledgeAgentOptions): Promise { - const { worktreePath, slug, prompt, resultFileName } = opts; - // Build result path in .devflow/features/{slug}/ (same directory as KNOWLEDGE.md) - const resultPath = path.join(path.dirname(getKnowledgePath(worktreePath, slug)), resultFileName); - - // Pre-clean any leftover file from a previous run - try { await fs.unlink(resultPath); } catch { /* doesn't exist — that's fine */ } - - // Spawn Knowledge agent (async — keeps event loop free for spinner animation) - await execFileAsync('claude', [ - '-p', prompt, - '--model', 'sonnet', - '--allowedTools', KNOWLEDGE_AGENT_TOOLS, - '--dangerously-skip-permissions', - ], { - cwd: worktreePath, - timeout: 300_000, - }); - - // Read result file written by the agent (returns {} if missing/invalid) - const result = await readAgentResult(resultPath); - - // Post-clean — best effort; callers should not rely on the file persisting - try { await fs.unlink(resultPath); } catch { /* already cleaned or never written */ } - - return { result }; -} diff --git a/src/cli/utils/project-paths.ts b/src/cli/utils/project-paths.ts index 2b489a26..b62eb315 100644 --- a/src/cli/utils/project-paths.ts +++ b/src/cli/utils/project-paths.ts @@ -168,31 +168,11 @@ export function getPendingTurnsLockDir(projectRoot: string): string { // Features / knowledge files // --------------------------------------------------------------------------- -/** .devflow/features/index.json */ -export function getFeaturesIndexPath(projectRoot: string): string { - return path.join(projectRoot, '.devflow', 'features', 'index.json'); -} - /** .devflow/features/{slug}/KNOWLEDGE.md */ export function getKnowledgePath(projectRoot: string, slug: string): string { return path.join(projectRoot, '.devflow', 'features', slug, 'KNOWLEDGE.md'); } -/** .devflow/features/.disabled — sentinel that gates knowledge phase/refresh */ -export function getFeaturesDisabledSentinel(projectRoot: string): string { - return path.join(projectRoot, '.devflow', 'features', '.disabled'); -} - -/** .devflow/features/.knowledge.lock — transient lock directory for concurrent index writes */ -export function getFeaturesLockDir(projectRoot: string): string { - return path.join(projectRoot, '.devflow', 'features', '.knowledge.lock'); -} - -/** .devflow/features/.knowledge-last-refresh — timestamp of last auto-refresh */ -export function getFeaturesLastRefreshPath(projectRoot: string): string { - return path.join(projectRoot, '.devflow', 'features', '.knowledge-last-refresh'); -} - // --------------------------------------------------------------------------- // Docs files // --------------------------------------------------------------------------- diff --git a/tests/feature-knowledge/apply-feature-knowledge-skill.test.ts b/tests/feature-knowledge/apply-feature-knowledge-skill.test.ts deleted file mode 100644 index 1a30cc59..00000000 --- a/tests/feature-knowledge/apply-feature-knowledge-skill.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { readFileSync } from 'fs'; -import * as path from 'path'; - -const ROOT = path.resolve(import.meta.dirname, '../..'); - -describe('feature-knowledge skill', () => { - const content = readFileSync(path.join(ROOT, 'shared/skills/feature-knowledge/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 quality self-checks', () => { expect(content).toContain('## Quality Self-Checks'); }); - it('has feature knowledge format template with required sections', () => { - expect(content).toContain('## Overview'); - expect(content).toContain('## Anti-Patterns'); - expect(content).toContain('## Gotchas'); - expect(content).toContain('## Key Files'); - expect(content).toContain('## Related'); - }); - it('has category templates', () => { - expect(content).toContain('**Architecture:**'); - expect(content).toContain('**Conventions:**'); - expect(content).toContain('**Component Patterns:**'); - expect(content).toContain('**Domain Knowledge:**'); - expect(content).toContain('**Lessons Learned:**'); - }); - it('has code example rules', () => { - expect(content).toContain('Rules for Code Examples'); - }); - it('has worked example', () => { - expect(content).toContain('## Worked Example'); - }); -}); - -describe('apply-feature-knowledge skill', () => { - const content = readFileSync(path.join(ROOT, 'shared/skills/apply-feature-knowledge/SKILL.md'), 'utf8'); - - it('has iron law', () => { expect(content).toContain('## Iron Law'); }); - it('has 3-step algorithm', () => { - expect(content).toContain('### Step 1: Read the Feature Knowledge'); - 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-knowledge/feature-knowledge.test.ts b/tests/feature-knowledge/feature-knowledge.test.ts deleted file mode 100644 index 8a4d45a7..00000000 --- a/tests/feature-knowledge/feature-knowledge.test.ts +++ /dev/null @@ -1,810 +0,0 @@ -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, realpathSync } from 'fs'; -import { execSync, execFileSync } from 'child_process'; -import { - SAMPLE_INDEX, - SAMPLE_FEATURE_KNOWLEDGE_CONTENT, - makeTmpFeatureWorktree, - cleanupTmpFeatureWorktrees, -} from './fixtures'; - -afterAll(() => cleanupTmpFeatureWorktrees()); - -const ROOT = path.resolve(import.meta.dirname, '../..'); -const require = createRequire(import.meta.url); - -const { - loadIndex, - loadKnowledgeContent, - checkStaleness, - checkAllStaleness, - updateIndex, - findOverlapping, - removeEntry, - listEntries, - validateSlug, -} = require(path.join(ROOT, 'scripts/hooks/lib/feature-knowledge.cjs')) as { - loadIndex: (worktreePath: string) => { version: number; features: Record } | null; - loadKnowledgeContent: (worktreePath: string, slug: string) => string | null; - checkStaleness: (worktreePath: string, slug: string) => { stale: boolean; changedFiles: string[] }; - checkAllStaleness: (worktreePath: string, cachedIndex?: { version: number; features: Record } | null) => Record; - updateIndex: (worktreePath: string, entry: Record, lockTimeoutMs?: number) => void; - findOverlapping: (worktreePath: string, changedFiles: string[]) => string[]; - removeEntry: (worktreePath: string, slug: string, lockTimeoutMs?: number) => void; - listEntries: (worktreePath: string, cachedIndex?: { version: number; features: Record } | null) => Array<{ slug: string } & Record>; - validateSlug: (slug: string) => void; -}; - -// --------------------------------------------------------------------------- -// 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 - rmSync(path.join(tmp, '.devflow', 'features'), { recursive: true, force: true }); - expect(loadIndex(tmp)).toBeNull(); - }); - - it('returns null for invalid JSON', () => { - const tmp = makeTmpFeatureWorktree(); - writeFileSync(path.join(tmp, '.devflow', 'features', 'index.json'), 'not-json'); - expect(loadIndex(tmp)).toBeNull(); - }); -}); - -// --------------------------------------------------------------------------- -// loadKnowledgeContent -// --------------------------------------------------------------------------- - -describe('loadKnowledgeContent', () => { - it('returns content string when KNOWLEDGE.md exists', () => { - const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX, { 'cli-commands': SAMPLE_FEATURE_KNOWLEDGE_CONTENT }); - const content = loadKnowledgeContent(tmp, 'cli-commands'); - expect(content).not.toBeNull(); - expect(content).toContain('# CLI Command System'); - }); - - it('returns null for missing knowledge base', () => { - const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); - expect(loadKnowledgeContent(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([]); - }); -}); - -// --------------------------------------------------------------------------- -// checkStaleness (positive — git repo) -// --------------------------------------------------------------------------- - -// T2: Positive staleness detection in a real git repo -describe('checkStaleness (positive — git repo)', () => { - it('detects stale feature knowledge 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, '.devflow', '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 feature knowledge entry that references src/cli/cli.ts - const featuresDir = path.join(tmp, '.devflow', 'features'); - mkdirSync(featuresDir, { recursive: true }); - const index = { - version: 1, - features: { - 'my-feature': { - name: 'My Feature', - description: '', - directories: ['src/cli/'], - referencedFiles: ['src/cli/cli.ts'], - 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 -// --------------------------------------------------------------------------- - -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'], - 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'); - }); - - 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'], - }); - const index = loadIndex(tmp); - expect(index).not.toBeNull(); - const entry = index!.features['cli-commands'] as Record; - expect(entry.name).toBe('CLI Command System Updated'); - // createdBy should be preserved from original - expect(entry.createdBy).toBe('implement'); - }); - - 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: [], - }); - 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); - 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, '.devflow', 'features', '.knowledge.lock'); - // Pre-create lock directory to simulate a held lock - mkdirSync(lockPath); - - expect(() => updateIndex(tmp, { - slug: 'test-lock', - name: 'Test', - directories: [], - referencedFiles: [], - }, 200)).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, '.devflow', 'features'), { recursive: true, force: true }); - expect(existsSync(path.join(tmp, '.devflow', 'features'))).toBe(false); - - updateIndex(tmp, { - slug: 'new-feature', - name: 'New Feature', - directories: ['src/new/'], - referencedFiles: ['src/new/index.ts'], - }); - - expect(existsSync(path.join(tmp, '.devflow', 'features'))).toBe(true); - const index = loadIndex(tmp); - expect(index).not.toBeNull(); - expect(index!.features['new-feature']).toBeDefined(); - }); -}); - -// --------------------------------------------------------------------------- -// removeEntry -// --------------------------------------------------------------------------- - -describe('removeEntry', () => { - it('removes entry from index and deletes its directory', () => { - const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX, { 'cli-commands': SAMPLE_FEATURE_KNOWLEDGE_CONTENT }); - const knowledgeDir = path.join(tmp, '.devflow', 'features', 'cli-commands'); - expect(existsSync(knowledgeDir)).toBe(true); - - removeEntry(tmp, 'cli-commands'); - - const index = loadIndex(tmp); - expect(index).not.toBeNull(); - expect(index!.features['cli-commands']).toBeUndefined(); - expect(existsSync(knowledgeDir)).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).not.toBeNull(); - 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, '.devflow', 'features'), { recursive: true, force: true }); - expect(existsSync(path.join(tmp, '.devflow', 'features'))).toBe(false); - - // 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, '.devflow', 'features', 'index.json'), 'not-valid-json'); - removeEntry(tmp, 'nonexistent'); - const raw = readFileSync(path.join(tmp, '.devflow', 'features', 'index.json'), 'utf8'); - expect(raw).toBe('not-valid-json'); - }); -}); - -// --------------------------------------------------------------------------- -// findOverlapping -// --------------------------------------------------------------------------- - -describe('findOverlapping', () => { - it('identifies feature knowledge whose referencedFiles overlap with changed files', () => { - const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); - 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 overlapping = findOverlapping(tmp, ['src/payments/checkout.ts', 'src/unrelated.ts']); - expect(overlapping).toEqual([]); - }); - - it('returns empty array for missing index', () => { - const tmp = makeTmpFeatureWorktree(); - 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'); - }); - - // 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'], - 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([]); - }); -}); - -// --------------------------------------------------------------------------- -// listEntries -// --------------------------------------------------------------------------- - -describe('listEntries', () => { - it('returns all entries with their slugs', () => { - const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); - const entries = listEntries(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(listEntries(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'); - }); - - it('does not false-positive newer feature knowledge when shared file changed between timestamps', () => { - const tmp = makeTmpFeatureWorktree(); - rmSync(path.join(tmp, '.devflow', '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' }); - - // Initial commit with shared.ts - const srcDir = path.join(tmp, 'src'); - mkdirSync(srcDir, { recursive: true }); - writeFileSync(path.join(srcDir, 'shared.ts'), 'export const v = 1;'); - execSync('git add .', { cwd: tmp, stdio: 'pipe' }); - execSync('git commit -m "initial"', { cwd: tmp, stdio: 'pipe' }); - - // knowledge-A: lastUpdated BEFORE the upcoming change → should be stale - const knowledgeATimestamp = new Date(Date.now() - 10000).toISOString(); - - // Modify shared.ts and commit (the change happens "now") - writeFileSync(path.join(srcDir, 'shared.ts'), 'export const v = 2;'); - execSync('git add .', { cwd: tmp, stdio: 'pipe' }); - execSync('git commit -m "update shared.ts"', { cwd: tmp, stdio: 'pipe' }); - - // knowledge-B: lastUpdated AFTER the change → should NOT be stale - const knowledgeBTimestamp = new Date(Date.now() + 10000).toISOString(); - - const featuresDir = path.join(tmp, '.devflow', 'features'); - mkdirSync(featuresDir, { recursive: true }); - writeFileSync(path.join(featuresDir, 'index.json'), JSON.stringify({ - version: 1, - features: { - 'knowledge-old': { - name: 'Old Feature Knowledge', - description: '', - directories: ['src/'], - referencedFiles: ['src/shared.ts'], - lastUpdated: knowledgeATimestamp, - createdBy: 'test', - }, - 'knowledge-new': { - name: 'New Feature Knowledge', - description: '', - directories: ['src/'], - referencedFiles: ['src/shared.ts'], - lastUpdated: knowledgeBTimestamp, - createdBy: 'test', - }, - }, - }, null, 2)); - - const result = checkAllStaleness(tmp); - expect(result['knowledge-old'].stale).toBe(true); - expect(result['knowledge-old'].changedFiles).toContain('src/shared.ts'); - expect(result['knowledge-new'].stale).toBe(false); - expect(result['knowledge-new'].changedFiles).toEqual([]); - }); -}); - -// --------------------------------------------------------------------------- -// 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(); - }); -}); - -// --------------------------------------------------------------------------- -// CLI: stale-slugs subcommand -// --------------------------------------------------------------------------- - -const FEATURE_KNOWLEDGE_CJS = path.join(ROOT, 'scripts/hooks/lib/feature-knowledge.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_KNOWLEDGE_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, '.devflow', '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, '.devflow', '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'], - 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_KNOWLEDGE_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', () => { - expect(() => - execFileSync('node', [FEATURE_KNOWLEDGE_CJS, 'stale-slugs'], { encoding: 'utf8', stdio: 'pipe' }) - ).toThrow(expect.objectContaining({ status: 1 })); - }); -}); - -// --------------------------------------------------------------------------- -// CLI: refresh-context subcommand -// --------------------------------------------------------------------------- - -describe('CLI refresh-context', () => { - it('outputs tab-separated metadata for an existing feature knowledge entry', () => { - const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); - const output = execFileSync('node', [FEATURE_KNOWLEDGE_CJS, 'refresh-context', tmp, 'cli-commands'], { encoding: 'utf8' }); - const parts = output.trim().split('\t'); - 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(() => JSON.parse(parts[2])).not.toThrow(); // changed files JSON - }); - - it('exits non-zero when slug is missing', () => { - const tmp = makeTmpFeatureWorktree(SAMPLE_INDEX); - expect(() => - execFileSync('node', [FEATURE_KNOWLEDGE_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); - expect(() => - execFileSync('node', [FEATURE_KNOWLEDGE_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); - expect(() => - execFileSync('node', [FEATURE_KNOWLEDGE_CJS, 'refresh-context', tmp, '../etc'], { encoding: 'utf8', stdio: 'pipe' }) - ).toThrow(expect.objectContaining({ status: 1 })); - }); -}); - -// --------------------------------------------------------------------------- -// CLI stale-slugs: empty index -// --------------------------------------------------------------------------- - -describe('CLI stale-slugs (empty index)', () => { - it('outputs nothing for empty index', () => { - const tmp = makeTmpFeatureWorktree({ version: 1, features: {} }); - const output = execFileSync('node', [FEATURE_KNOWLEDGE_CJS, 'stale-slugs', tmp], { encoding: 'utf8' }); - expect(output.trim()).toBe(''); - }); -}); - -// --------------------------------------------------------------------------- -// json-helper.cjs read-dream -// --------------------------------------------------------------------------- - -const JSON_HELPER_CJS = path.join(ROOT, 'scripts/hooks/json-helper.cjs'); - -describe('json-helper read-dream', () => { - it('returns parsed JSON array for valid result with array field', () => { - const realTmp = realpathSync(os.tmpdir()); - const result = path.join(realTmp, `test-result-${Date.now()}.json`); - writeFileSync(result, JSON.stringify({ referencedFiles: ['src/a.ts', 'src/b.ts'] })); - try { - const output = execFileSync('node', [JSON_HELPER_CJS, 'read-dream', result, 'referencedFiles'], - { encoding: 'utf8', cwd: realTmp }); - expect(JSON.parse(output.trim())).toEqual(['src/a.ts', 'src/b.ts']); - } finally { - try { rmSync(result); } catch { /* best-effort */ } - } - }); - - it('returns [] for missing file', () => { - const output = execFileSync('node', [JSON_HELPER_CJS, 'read-dream', '/nonexistent/path/file.json', 'referencedFiles'], { encoding: 'utf8' }); - expect(output.trim()).toBe('[]'); - }); - - it('returns [] for malformed JSON', () => { - const realTmp = realpathSync(os.tmpdir()); - const result = path.join(realTmp, `test-result-bad-${Date.now()}.json`); - writeFileSync(result, 'not-json'); - try { - const output = execFileSync('node', [JSON_HELPER_CJS, 'read-dream', result, 'referencedFiles'], - { encoding: 'utf8', cwd: realTmp }); - expect(output.trim()).toBe('[]'); - } finally { - try { rmSync(result); } catch { /* best-effort */ } - } - }); - - it('returns string value as-is for string fields', () => { - const realTmp = realpathSync(os.tmpdir()); - const result = path.join(realTmp, `test-result-string-${Date.now()}.json`); - writeFileSync(result, JSON.stringify({ description: 'Use when working on auth' })); - try { - const output = execFileSync('node', [JSON_HELPER_CJS, 'read-dream', result, 'description'], - { encoding: 'utf8', cwd: realTmp }); - expect(output.trim()).toBe('Use when working on auth'); - } finally { - try { rmSync(result); } catch { /* best-effort */ } - } - }); - - it('returns [] when field value is not an array or string', () => { - const realTmp = realpathSync(os.tmpdir()); - const result = path.join(realTmp, `test-result-noarray-${Date.now()}.json`); - writeFileSync(result, JSON.stringify({ referencedFiles: 42 })); - try { - const output = execFileSync('node', [JSON_HELPER_CJS, 'read-dream', result, 'referencedFiles'], - { encoding: 'utf8', cwd: realTmp }); - expect(output.trim()).toBe('[]'); - } finally { - try { rmSync(result); } catch { /* best-effort */ } - } - }); - - it('returns [] when args are missing', () => { - const output = execFileSync('node', [JSON_HELPER_CJS, 'read-dream'], { encoding: 'utf8' }); - expect(output.trim()).toBe('[]'); - }); - - it('returns [] for disallowed field name', () => { - const realTmp = realpathSync(os.tmpdir()); - const result = path.join(realTmp, `test-result-disallowed-${Date.now()}.json`); - writeFileSync(result, JSON.stringify({ secret: 'password123' })); - try { - const output = execFileSync('node', [JSON_HELPER_CJS, 'read-dream', result, 'secret'], - { encoding: 'utf8', cwd: realTmp }); - expect(output.trim()).toBe('[]'); - } finally { - try { rmSync(result); } catch {} - } - }); - - it('returns [] when result path is outside cwd', () => { - const realTmp = realpathSync(os.tmpdir()); - const result = path.join(realTmp, `test-result-outside-${Date.now()}.json`); - writeFileSync(result, JSON.stringify({ referencedFiles: ['a.ts'] })); - const otherDir = path.join(realTmp, `test-other-${Date.now()}`); - mkdirSync(otherDir, { recursive: true }); - try { - const output = execFileSync('node', [JSON_HELPER_CJS, 'read-dream', result, 'referencedFiles'], - { encoding: 'utf8', cwd: otherDir }); - expect(output.trim()).toBe('[]'); - } finally { - try { rmSync(result); } catch {} - try { rmdirSync(otherDir); } catch {} - } - }); -}); - -// --------------------------------------------------------------------------- -// safePath unit tests -// --------------------------------------------------------------------------- - -describe('safePath', () => { - const { safePath } = require(path.join(ROOT, 'scripts/hooks/lib/safe-path.cjs')); - - it('resolves relative path to absolute', () => { - const result = safePath('foo/bar.json'); - expect(path.isAbsolute(result)).toBe(true); - }); - - it('passes absolute path through', () => { - const realTmp = realpathSync(os.tmpdir()); - expect(safePath(path.join(realTmp, 'test.json'))).toBe(path.join(realTmp, 'test.json')); - }); - - it('allows path inside allowedRoot', () => { - const realTmp = realpathSync(os.tmpdir()); - const filePath = path.join(realTmp, 'sub', 'file.ts'); - expect(safePath(filePath, realTmp)).toBe(filePath); - }); - - it('throws for path outside allowedRoot', () => { - expect(() => safePath('/etc/passwd', '/project')).toThrow('Refused path outside'); - }); - - it('allows path with .. that resolves inside root', () => { - expect(safePath('/project/a/../b/file.ts', '/project')).toBe('/project/b/file.ts'); - }); - - it('skips validation when allowedRoot is undefined', () => { - expect(safePath('/anywhere/file.json')).toBe('/anywhere/file.json'); - }); -}); - -// --------------------------------------------------------------------------- -// readAgentResult helper (TypeScript) -// --------------------------------------------------------------------------- - -import { readAgentResult } from '../../src/cli/commands/knowledge/index.js'; - -describe('readAgentResult', () => { - 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-agent-result-${Date.now()}-${Math.random().toString(36).slice(2)}.json`); - writeFileSync(f, content); - tmpFiles.push(f); - return f; - } - - it('returns referencedFiles and description for valid result file', async () => { - const f = writeTmp(JSON.stringify({ referencedFiles: ['src/a.ts', 'src/b.ts'], description: 'Use when X' })); - const result = await readAgentResult(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 readAgentResult('/nonexistent/path/file.json'); - expect(result).toEqual({}); - }); - - it('returns {} for invalid JSON', async () => { - const f = writeTmp('not-valid-json'); - const result = await readAgentResult(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 readAgentResult(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 readAgentResult(f); - expect(result.referencedFiles).toEqual(['src/a.ts', 'src/b.ts']); - }); - - it('returns {} when JSON parses to a non-object (primitive)', async () => { - const tmp = path.join(os.tmpdir(), `agent-result-primitive-${Date.now()}.json`); - writeFileSync(tmp, '42'); - try { - const result = await readAgentResult(tmp); - expect(result).toEqual({}); - } finally { - rmSync(tmp, { force: true }); - } - }); -}); diff --git a/tests/feature-knowledge/fixtures.ts b/tests/feature-knowledge/fixtures.ts deleted file mode 100644 index f36fd892..00000000 --- a/tests/feature-knowledge/fixtures.ts +++ /dev/null @@ -1,98 +0,0 @@ -// Shared test fixtures for feature-knowledge 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'], - lastUpdated: '2026-04-20T14:30:00Z', - createdBy: 'implement', - }, - }, -}; - -export const SAMPLE_FEATURE_KNOWLEDGE_CONTENT = `--- -feature: cli-commands -name: CLI Command System -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 feature knowledge files. - * Returns the absolute path to the tmpdir root. - * Directories are tracked — call `cleanupTmpFeatureWorktrees()` in afterAll. - */ -export function makeTmpFeatureWorktree( - indexContent?: object, - featureKnowledge?: Record, -): string { - const tmp = mkdtempSync(path.join(os.tmpdir(), 'feature-knowledge-test-')); - createdTmpDirs.push(tmp); - - const featuresDir = path.join(tmp, '.devflow', 'features'); - mkdirSync(featuresDir, { recursive: true }); - - if (indexContent) { - writeFileSync(path.join(featuresDir, 'index.json'), JSON.stringify(indexContent, null, 2)); - } - - if (featureKnowledge) { - for (const [slug, content] of Object.entries(featureKnowledge)) { - const knowledgeDir = path.join(featuresDir, slug); - mkdirSync(knowledgeDir, { recursive: true }); - writeFileSync(path.join(knowledgeDir, '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-knowledge/knowledge-agent.test.ts b/tests/feature-knowledge/knowledge-agent.test.ts deleted file mode 100644 index 89de3326..00000000 --- a/tests/feature-knowledge/knowledge-agent.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { readFileSync } from 'fs'; -import * as path from 'path'; - -const ROOT = path.resolve(import.meta.dirname, '../..'); - -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-knowledge skill', () => { expect(content).toContain('devflow:feature-knowledge'); }); - 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('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('DECISIONS_CONTEXT'); - }); - it('constrains writes to .devflow/features/', () => { - expect(content).toContain('.devflow/features/'); - expect(content).toContain('Boundaries'); - }); -}); diff --git a/tests/feature-knowledge/knowledge-command.test.ts b/tests/feature-knowledge/knowledge-command.test.ts deleted file mode 100644 index 18b2c888..00000000 --- a/tests/feature-knowledge/knowledge-command.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -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-knowledge.cjs'); - -describe('feature-knowledge.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, '.devflow', '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"]'`, - { encoding: 'utf8' } - ); - const index = JSON.parse(readFileSync(path.join(tmp, '.devflow', '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 Feature Knowledge' }); - execSync(`node ${CJS_PATH} remove ${tmp} cli-commands`, { encoding: 'utf8' }); - const index = JSON.parse(readFileSync(path.join(tmp, '.devflow', '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([]); - }); - -}); diff --git a/tests/project-paths.test.ts b/tests/project-paths.test.ts index 5dcb2451..f2b2dd73 100644 --- a/tests/project-paths.test.ts +++ b/tests/project-paths.test.ts @@ -41,11 +41,7 @@ import { getPendingTurnsPath, getPendingTurnsProcessingPath, getPendingTurnsLockDir, - getFeaturesIndexPath, getKnowledgePath, - getFeaturesDisabledSentinel, - getFeaturesLockDir, - getFeaturesLastRefreshPath, getReviewsDir, getDesignDir, getResearchDir, @@ -164,25 +160,9 @@ describe('project-paths TypeScript module', () => { }); describe('features / knowledge files', () => { - it('getFeaturesIndexPath returns .devflow/features/index.json', () => { - expect(getFeaturesIndexPath(ROOT)).toBe('/some/project/.devflow/features/index.json'); - }); - it('getKnowledgePath returns .devflow/features/{slug}/KNOWLEDGE.md', () => { expect(getKnowledgePath(ROOT, 'my-feature')).toBe('/some/project/.devflow/features/my-feature/KNOWLEDGE.md'); }); - - it('getFeaturesDisabledSentinel returns .devflow/features/.disabled', () => { - expect(getFeaturesDisabledSentinel(ROOT)).toBe('/some/project/.devflow/features/.disabled'); - }); - - it('getFeaturesLockDir returns .devflow/features/.knowledge.lock', () => { - expect(getFeaturesLockDir(ROOT)).toBe('/some/project/.devflow/features/.knowledge.lock'); - }); - - it('getFeaturesLastRefreshPath returns .devflow/features/.knowledge-last-refresh', () => { - expect(getFeaturesLastRefreshPath(ROOT)).toBe('/some/project/.devflow/features/.knowledge-last-refresh'); - }); }); describe('docs files', () => { @@ -276,10 +256,6 @@ describe('CJS project-paths parity', () => { { name: 'getPendingTurnsPath', ts: getPendingTurnsPath, cjs: cjsPaths.getPendingTurnsPath }, { name: 'getPendingTurnsProcessingPath', ts: getPendingTurnsProcessingPath, cjs: cjsPaths.getPendingTurnsProcessingPath }, { name: 'getPendingTurnsLockDir', ts: getPendingTurnsLockDir, cjs: cjsPaths.getPendingTurnsLockDir }, - { name: 'getFeaturesIndexPath', ts: getFeaturesIndexPath, cjs: cjsPaths.getFeaturesIndexPath }, - { name: 'getFeaturesDisabledSentinel', ts: getFeaturesDisabledSentinel, cjs: cjsPaths.getFeaturesDisabledSentinel }, - { name: 'getFeaturesLockDir', ts: getFeaturesLockDir, cjs: cjsPaths.getFeaturesLockDir }, - { name: 'getFeaturesLastRefreshPath', ts: getFeaturesLastRefreshPath, cjs: cjsPaths.getFeaturesLastRefreshPath }, { name: 'getReviewsDir', ts: getReviewsDir, cjs: cjsPaths.getReviewsDir }, { name: 'getDesignDir', ts: getDesignDir, cjs: cjsPaths.getDesignDir }, { name: 'getResearchDir', ts: getResearchDir, cjs: cjsPaths.getResearchDir }, diff --git a/tests/shell-hooks.test.ts b/tests/shell-hooks.test.ts index 0487755f..2621e0e7 100644 --- a/tests/shell-hooks.test.ts +++ b/tests/shell-hooks.test.ts @@ -31,7 +31,6 @@ const HOOK_SCRIPTS = [ 'dream-dispatch', 'eval-helpers', 'eval-decisions', - 'eval-knowledge', ]; describe('shell hook syntax checks', () => { @@ -1391,22 +1390,13 @@ describe('ensure-devflow-init behavioral', () => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); - it('creates .devflow/features/ and index.json when absent', () => { + it('creates .devflow/features/ directory when absent (no index.json — write-through model)', () => { execSync(`bash -c 'source "${ENSURE_DEVFLOW}" "${tmpDir}"'`, { stdio: 'pipe' }); - expect(fs.existsSync(path.join(tmpDir, '.devflow', 'features', 'index.json'))).toBe(true); - const content = fs.readFileSync(path.join(tmpDir, '.devflow', 'features', 'index.json'), 'utf-8'); - expect(content).toBe('{"version":1,"features":{}}'); - }); - - it('does not overwrite existing index.json', () => { - fs.mkdirSync(path.join(tmpDir, '.devflow', 'features'), { recursive: true }); - fs.writeFileSync(path.join(tmpDir, '.devflow', 'features', 'index.json'), '{"existing":"data"}'); - - execSync(`bash -c 'source "${ENSURE_DEVFLOW}" "${tmpDir}"'`, { stdio: 'pipe' }); - - const content = fs.readFileSync(path.join(tmpDir, '.devflow', 'features', 'index.json'), 'utf-8'); - expect(content).toBe('{"existing":"data"}'); + // Knowledge index is now write-through (written when a KB is created/refreshed in-command) + // ensure-devflow-init only creates the directory, not the index file + expect(fs.existsSync(path.join(tmpDir, '.devflow', 'features'))).toBe(true); + expect(fs.existsSync(path.join(tmpDir, '.devflow', 'features', 'index.json'))).toBe(false); }); it('adds .devflow/ to the project root .gitignore (creates it when absent)', () => { @@ -1735,9 +1725,10 @@ describe('dream-evaluate business logic', () => { expect(fs.existsSync(logFile)).toBe(true); const log = fs.readFileSync(logFile, 'utf-8'); expect(log).toContain('Evaluating decisions'); - expect(log).toContain('Evaluating knowledge'); // learning pipeline removed — no 'Evaluating learning' should appear expect(log).not.toContain('Evaluating learning'); + // knowledge is now write-through (in-command) — no 'Evaluating knowledge' from dream-evaluate + expect(log).not.toContain('Evaluating knowledge'); }); it('shallow session (2 turns) skips decisions', () => { @@ -1786,51 +1777,6 @@ describe('dream-evaluate business logic', () => { expect(pairs.length).toBeGreaterThan(0); }); - it('knowledge throttle: recent refresh skips evaluation', () => { - // Write a recent timestamp to the throttle marker - const featuresDir = path.join(tmpDir, '.devflow', 'features'); - fs.mkdirSync(featuresDir, { recursive: true }); - fs.writeFileSync(path.join(featuresDir, 'index.json'), '{"version":1,"features":{}}'); - const now = Math.floor(Date.now() / 1000); - fs.writeFileSync(path.join(featuresDir, '.knowledge-last-refresh'), String(now)); - - createTranscript(homeDir, tmpDir, 5); - runHook(EVALUATE_HOOK, { - cwd: tmpDir, - session_id: 'test-session', - }, homeDir); - - const dreamDir = path.join(tmpDir, '.devflow', 'dream'); - expect(fs.existsSync(path.join(dreamDir, 'knowledge.json'))).toBe(false); - - const logDir = path.join(homeDir, '.devflow', 'logs', encodeCwd(tmpDir)); - const logFile = path.join(logDir, '.dream-evaluate.log'); - expect(fs.existsSync(logFile)).toBe(true); - const log = fs.readFileSync(logFile, 'utf-8'); - expect(log).toContain('Knowledge throttled'); - }); - - it('knowledge disabled sentinel skips evaluation', () => { - const featuresDir = path.join(tmpDir, '.devflow', 'features'); - fs.mkdirSync(featuresDir, { recursive: true }); - fs.writeFileSync(path.join(featuresDir, '.disabled'), ''); - - createTranscript(homeDir, tmpDir, 5); - runHook(EVALUATE_HOOK, { - cwd: tmpDir, - session_id: 'test-session', - }, homeDir); - - const dreamDir = path.join(tmpDir, '.devflow', 'dream'); - expect(fs.existsSync(path.join(dreamDir, 'knowledge.json'))).toBe(false); - - const logDir = path.join(homeDir, '.devflow', 'logs', encodeCwd(tmpDir)); - const logFile = path.join(logDir, '.dream-evaluate.log'); - expect(fs.existsSync(logFile)).toBe(true); - const log = fs.readFileSync(logFile, 'utf-8'); - expect(log).toContain('Knowledge disabled by sentinel'); - }); - it('session ID path traversal rejected, fallback used', () => { // Create a valid transcript with a normal name createTranscript(homeDir, tmpDir, 5, 5, 'valid-session'); @@ -1849,76 +1795,6 @@ describe('dream-evaluate business logic', () => { }); }); -// ============================================================================= -// dream-evaluate knowledge evaluation -// ============================================================================= - -describe('dream-evaluate knowledge evaluation', () => { - const EVALUATE_HOOK = path.join(HOOKS_DIR, 'dream-evaluate'); - - let tmpDir: string; - let homeDir: string; - - beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'devflow-cap-test-')); - homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'devflow-cap-home-')); - fs.mkdirSync(path.join(tmpDir, '.devflow', 'dream'), { recursive: true }); - fs.mkdirSync(path.join(homeDir, '.devflow', 'logs'), { recursive: true }); - }); - - afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); - fs.rmSync(homeDir, { recursive: true, force: true }); - }); - - it('LAST_REFRESH sanitization: non-numeric content in knowledge marker defaults to 0', () => { - // Write non-numeric content to .knowledge-last-refresh — should sanitize to 0 - // resulting in a stale age calculation (now - 0 > 7200) and triggering evaluation - const featuresDir = path.join(tmpDir, '.devflow', 'features'); - fs.mkdirSync(featuresDir, { recursive: true }); - // No stale slugs — so no marker will be written — but hook should not crash - fs.writeFileSync(path.join(featuresDir, 'index.json'), '{"version":1,"features":{}}'); - fs.writeFileSync(path.join(featuresDir, '.knowledge-last-refresh'), 'not-a-number\n'); - - createTranscript(homeDir, tmpDir, 5); - const { exitCode } = runHook(EVALUATE_HOOK, { cwd: tmpDir, session_id: 'test-session' }, homeDir); - expect(exitCode).toBe(0); - - // Hook should have attempted knowledge evaluation (no crash from bad timestamp) - const logDir = path.join(homeDir, '.devflow', 'logs', encodeCwd(tmpDir)); - const logFile = path.join(logDir, '.dream-evaluate.log'); - if (fs.existsSync(logFile)) { - const log = fs.readFileSync(logFile, 'utf-8'); - // Either throttled (with sanitized timestamp near 0 → stale) or "no stale slugs" - // Both are valid outcomes — the key is it didn't crash - expect(log).toContain('Evaluating knowledge'); - } - }); - - it('LAST_UPDATE get_mtime fallback: missing WORKING-MEMORY defaults to 0', () => { - // Without WORKING-MEMORY.md, get_mtime returns empty string; LAST_UPDATE should default to 0 - // so age computation doesn't error - createTranscript(homeDir, tmpDir, 5); - - // Deliberately do NOT create WORKING-MEMORY.md - // We call dream-capture (not dream-evaluate) for this one since it does the get_mtime check - const CAPTURE_HOOK = path.join(HOOKS_DIR, 'dream-capture'); - const exitCode = (() => { - try { - execSync(`bash "${CAPTURE_HOOK}"`, { - input: JSON.stringify({ cwd: tmpDir, session_id: 'test', last_assistant_message: 'response text here' }), - stdio: ['pipe', 'pipe', 'pipe'], - env: { ...process.env, HOME: homeDir }, - }); - return 0; - } catch (e: unknown) { - return (e as { status: number }).status ?? 1; - } - })(); - expect(exitCode).toBe(0); - }); -}); - // ============================================================================= // dream-recover stale marker recovery // ============================================================================= @@ -2464,7 +2340,7 @@ exit 0 */ function runCollectTasks( dreamDir: string, - opts: { decEnabled?: boolean; knowEnabled?: boolean }, + opts: { decEnabled?: boolean }, ): { tasks: string; mtimeCount: number } { const hooksDir = path.resolve(__dirname, '..', 'scripts', 'hooks'); const counterFile = path.join(os.tmpdir(), `devflow-mtime-counter-${Date.now()}-${Math.random().toString(36).slice(2)}`); @@ -2478,7 +2354,6 @@ function runCollectTasks( // 4. Prints _DREAM_TASKS to stdout. // Note: memEnabled removed — memory is not a Dream task (applies ADR-016; avoids PF-009). const decEn = opts.decEnabled ?? true ? 'true' : 'false'; - const knowEn = opts.knowEnabled ?? true ? 'true' : 'false'; const script = `#!/bin/bash dbg() { :; } @@ -2494,7 +2369,7 @@ get_mtime() { printf '%s' "$c" } source "${hooksDir}/dream-collect-tasks" -dream_collect_tasks "${dreamDir}" "${decEn}" "${knowEn}" +dream_collect_tasks "${dreamDir}" "${decEn}" printf '%s' "\$_DREAM_TASKS" `; @@ -2555,17 +2430,18 @@ describe('dream-collect-tasks: single-pass scan', () => { expect(fs.existsSync(path.join(dreamDir, 'learning.sess2.json'))).toBe(false); }); - // AC-3: memory always swept (unconditional); disabled decisions/knowledge/curation deleted; type never in _DREAM_TASKS - // Curation is now also swept when decisions is disabled (curation depends on decisions data). + // AC-3: memory + knowledge always swept unconditionally; decisions/curation deleted when decisions disabled + // Curation is swept when decisions is disabled (curation depends on decisions data). + // Knowledge is no longer a Dream task — write-through handles it in-command; stale markers deleted on sight. it('AC-3: memory always swept; disabled decisions/knowledge/curation markers deleted when decisions disabled', () => { fs.writeFileSync(path.join(dreamDir, 'memory.sess1.json'), '{}'); // swept unconditionally fs.writeFileSync(path.join(dreamDir, 'decisions.sess1.json'), '{}'); - fs.writeFileSync(path.join(dreamDir, 'knowledge.sess1.json'), '{}'); + fs.writeFileSync(path.join(dreamDir, 'knowledge.sess1.json'), '{}'); // swept unconditionally (no longer Dream task) fs.writeFileSync(path.join(dreamDir, 'curation.sess1.json'), '{}'); - // Memory markers are always swept unconditionally (memory is no longer a Dream task). + // Memory + knowledge markers are always swept unconditionally (neither is a Dream task anymore). // Curation markers are also swept when decisions is disabled — curation inherits decisions state. - const { tasks } = runCollectTasks(dreamDir, { decEnabled: false, knowEnabled: false }); + const { tasks } = runCollectTasks(dreamDir, { decEnabled: false }); expect(tasks).toBe(''); expect(fs.existsSync(path.join(dreamDir, 'memory.sess1.json'))).toBe(false); expect(fs.existsSync(path.join(dreamDir, 'decisions.sess1.json'))).toBe(false); From e07b6b4e1741d08f24a829f28e82659213a3a404 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sun, 28 Jun 2026 14:19:01 +0300 Subject: [PATCH 3/5] feat!: add knowledge migrations + docs + tests (3/3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WS4: Two new migrations in MIGRATIONS registry: - purge-knowledge-hooks-global-v1 (global): removes orphaned ~/.devflow/scripts/hooks/eval-knowledge and ~/.devflow/scripts/hooks/lib/feature-knowledge.cjs left by the additive installer after Phase 2 deleted them from source - purge-feature-knowledge-pipeline-v1 (per-project): removes dream knowledge.* markers, .knowledge.lock/, .knowledge-last-refresh, .knowledge-refresh.lock, .disabled sentinel; renames index.json to index.json.deprecated; preserves knowledge gate in dream config WS6 docs: CLAUDE.md fully updated to write-through model — Feature Knowledge Bases paragraph rewritten; eval-knowledge removed from hooks list and dream-evaluate description; dream-collect-tasks updated to note knowledge.* deletion; Model Strategy drops knowledge=sonnet Dream note; Migrations paragraph adds both new migrations; Project Structure removes stale files (.disabled, .knowledge.lock, .knowledge-last-refresh, index.json → index.md); build:knowledge added to build commands. WS6 tests: 38 new tests across 2 files — - tests/migrations.test.ts: 4 new entries in MIGRATIONS describe + idempotency/cleanup suites for both new migrations (30 new tests) - tests/build-knowledge.test.ts: new file — SOURCE_TO_PLUGIN_MAP lock (9-entry explicit mapping), MDS compiler happy path, script subprocess contract (exits 0), compiled-output assertions (no stale call sites); 8 tests Dogfood: .devflow/features/feature-knowledge-system/KNOWLEDGE.md rewritten to document the new write-through architecture; index.json renamed to index.json.deprecated; index.md created with new line format. Final state: npm run build exits 0 (all 9 knowledge commands compiled); npm test 1815/1815 passing (up from 1777 in Phase 2). Co-Authored-By: Claude --- CLAUDE.md | 24 ++- src/cli/utils/migrations.ts | 151 +++++++++++++++ tests/build-knowledge.test.ts | 218 +++++++++++++++++++++ tests/migrations.test.ts | 352 ++++++++++++++++++++++++++++++++++ 4 files changed, 732 insertions(+), 13 deletions(-) create mode 100644 tests/build-knowledge.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 8888647e..2e20390f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,7 +43,7 @@ Plugin marketplace with 22 plugins (12 core + 9 optional language/ecosystem + 1 **LLM-vs-plumbing principle**: The LLM does all detection, semantic matching, materialization, and curation. Deterministic code is plumbing only: hooks, locks, throttles, file I/O, id-keyed JSONL records, `assign-anchor`/`retire-anchor` ledger numbering, `render-decisions` rendering, and `merge-observation` writes. No detection or judgment logic lives in shell or TypeScript. -**Working Memory**: Three shell-script hooks (`scripts/hooks/`) replace the old 8-hook system with a background-maintenance (Dream) architecture. Toggleable via `devflow memory --enable/--disable/--status` or `devflow init --memory/--no-memory`. Feature state is stored in `.devflow/dream/config.json` (config-only; dream config is the sole source of truth per ADR-001). `dream-capture` (Stop hook) — captures user/assistant turns to `.devflow/memory/.pending-turns.jsonl` queue; after the 120s throttle (keyed by `.working-memory-last-trigger` mtime), spawns `background-memory-update` as a detached `nohup` worker (`claude -p --model haiku`); touches `.working-memory-last-trigger` BEFORE spawning to prevent double-spawn; uses mkdir-based locking for queue overflow truncation across concurrent sessions. `background-memory-update` (Stop-hook worker, not a hook itself) — drains `.pending-turns.jsonl`, calls `claude -p` (prompt on stdin, never argv), rewrites `WORKING-MEMORY.md` with `` on line 1, touches `.last-refresh-ok` on success; holds a 300s-stale worker lock; user-only queue truncated without LLM run. `dream-dispatch` (UserPromptSubmit hook) — **capture-only**: appends the user turn to `.pending-turns.jsonl`; it does NOT emit any directive. `dream-evaluate` (SessionEnd hook) — orchestrator that sources `eval-helpers` + 3 feature modules (`eval-decisions`, `eval-knowledge`, `eval-curation`) after shared setup; each module uses `${VAR:?}` fail-fast guards and `_MODULENAME_` variable prefixes for namespace isolation; evaluates whether to write decisions, knowledge, or curation dream markers; writes per-session marker files using atomic temp+mv; uses mkdir-based locking (`dream-lock`) to serialize operations across concurrent sessions. Always-on SessionStart hook (`session-start-context`) — recovers stale `.processing` markers (via `dream-recover` → `dream_recover_stale`), collects pending markers (via `dream-collect-tasks`), and emits a **DREAM MAINTENANCE** directive instructing the main model to spawn background `Dream` agents; directive emission is throttled to 120s. `dream-collect-tasks` unconditionally deletes orphaned `learning.*` AND `memory.*` markers (both pipelines removed from Dream subagent). The Dream agent processes decisions/knowledge/curation only — memory is NOT a Dream task. SessionStart hook (`session-start-memory`) → injects previous memory with git-reconciled header (3-state: A in-sync / B drifted / C refresh-failing) + optional pre-compact snapshot as `additionalContext`; stamp `` on line 1 drives drift detection; no raw-turns dump. PreCompact hook → saves git state + WORKING-MEMORY.md snapshot. Memory sections: `## Now`, `## Progress`, `## Decisions`, `## Context`, `## Session Log`. The background-memory-update worker uses rename-to-claim for queue consumption (atomically renames `.pending-turns.jsonl` → `.pending-turns.processing`). Disabling memory writes `memory: false` to dream config — hooks remain registered (shared across features). `removeMemoryHooks` (used by `devflow init --no-memory`) also removes pre-dream legacy hooks. Use `devflow memory --clear` to clean up pending queue files across projects. Zero-ceremony context preservation. +**Working Memory**: Three shell-script hooks (`scripts/hooks/`) replace the old 8-hook system with a background-maintenance (Dream) architecture. Toggleable via `devflow memory --enable/--disable/--status` or `devflow init --memory/--no-memory`. Feature state is stored in `.devflow/dream/config.json` (config-only; dream config is the sole source of truth per ADR-001). `dream-capture` (Stop hook) — captures user/assistant turns to `.devflow/memory/.pending-turns.jsonl` queue; after the 120s throttle (keyed by `.working-memory-last-trigger` mtime), spawns `background-memory-update` as a detached `nohup` worker (`claude -p --model haiku`); touches `.working-memory-last-trigger` BEFORE spawning to prevent double-spawn; uses mkdir-based locking for queue overflow truncation across concurrent sessions. `background-memory-update` (Stop-hook worker, not a hook itself) — drains `.pending-turns.jsonl`, calls `claude -p` (prompt on stdin, never argv), rewrites `WORKING-MEMORY.md` with `` on line 1, touches `.last-refresh-ok` on success; holds a 300s-stale worker lock; user-only queue truncated without LLM run. `dream-dispatch` (UserPromptSubmit hook) — **capture-only**: appends the user turn to `.pending-turns.jsonl`; it does NOT emit any directive. `dream-evaluate` (SessionEnd hook) — orchestrator that sources `eval-helpers` + 2 feature modules (`eval-decisions`, `eval-curation`) after shared setup; each module uses `${VAR:?}` fail-fast guards and `_MODULENAME_` variable prefixes for namespace isolation; evaluates whether to write decisions or curation dream markers; writes per-session marker files using atomic temp+mv; uses mkdir-based locking (`dream-lock`) to serialize operations across concurrent sessions. Always-on SessionStart hook (`session-start-context`) — recovers stale `.processing` markers (via `dream-recover` → `dream_recover_stale`), collects pending markers (via `dream-collect-tasks`), and emits a **DREAM MAINTENANCE** directive instructing the main model to spawn background `Dream` agents; directive emission is throttled to 120s. `dream-collect-tasks` unconditionally deletes orphaned `learning.*`, `memory.*`, AND `knowledge.*` markers (all three pipelines removed from Dream subagent). The Dream agent processes decisions and curation only — memory and knowledge are NOT Dream tasks. SessionStart hook (`session-start-memory`) → injects previous memory with git-reconciled header (3-state: A in-sync / B drifted / C refresh-failing) + optional pre-compact snapshot as `additionalContext`; stamp `` on line 1 drives drift detection; no raw-turns dump. PreCompact hook → saves git state + WORKING-MEMORY.md snapshot. Memory sections: `## Now`, `## Progress`, `## Decisions`, `## Context`, `## Session Log`. The background-memory-update worker uses rename-to-claim for queue consumption (atomically renames `.pending-turns.jsonl` → `.pending-turns.processing`). Disabling memory writes `memory: false` to dream config — hooks remain registered (shared across features). `removeMemoryHooks` (used by `devflow init --no-memory`) also removes pre-dream legacy hooks. Use `devflow memory --clear` to clean up pending queue files across projects. Zero-ceremony context preservation. **Ambient Mode**: Single-component system for zero-overhead session enhancement. UserPromptSubmit hook (`preamble`) uses two coexisting detection paths, both controlled by the same single toggle (`devflow ambient --enable/--disable/--status` or `devflow init`). **First-word keyword detection** — when a prompt's first word (case-insensitive) is one of `implement`, `explore`, `research`, `debug`, or `plan`, followed by at least one additional word, the hook outputs a directive instructing the model to briefly announce the workflow then invoke the matching `devflow:` skill via the Skill tool. **3-marker plan detection** — when a prompt contains `## Goal`, `## Steps`, and `## Files` markers (and the keyword path did not fire), it outputs a directive to invoke `devflow:implement`. Zero overhead for normal prompts — hook outputs nothing. Any legacy `commands.md` rule left by prior installs is auto-removed on every `devflow ambient --enable/--disable` or `devflow init`. @@ -55,17 +55,18 @@ Debug logs stored at `~/.devflow/logs/{project-slug}/`. **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 `applyViewMode`/`stripViewMode` pattern. Flags (18 total): default ON — `tui`, `tool-search`, `lsp`, `prompt-caching-1h`, `show-turn-duration`, `clear-context-on-plan`; default OFF — `brief`, `thinking-summaries`, `subprocess-env-scrub`, `disable-nonessential-traffic`, `forked-subagents`, `disable-adaptive-thinking`, `always-thinking`, `disable-git-instructions`, `disable-compact`, `disable-1m-context`, `disable-autoupdater`, `agent-teams`. Manageable via `devflow flags --enable/--disable/--status/--list`. Stored in manifest `features.flags: string[]`. View mode (`default`/`verbose`/`focus`) stored in manifest `features.viewMode?: string` and applied to `settings.json` as the `viewMode` key; `applyViewMode`/`stripViewMode` utilities colocated in `flags.ts`. -**Feature Knowledge Bases**: Per-feature `.devflow/features/` directory containing KNOWLEDGE.md files that capture area-specific patterns, conventions, architecture, and gotchas. Knowledge bases are created as side-effects of implementation (`/implement` Phase 12), loaded automatically across all workflows via `FEATURE_KNOWLEDGE` variable (companion to `DECISIONS_CONTEXT`), and use staleness detection via git log against `referencedFiles`. Index at `.devflow/features/index.json` (object keyed by slug). Managed via `devflow knowledge list|create|check|refresh|remove`. Knowledge agent (sonnet) structures exploration outputs into KNOWLEDGE.md. `apply-feature-knowledge` skill provides consumption algorithm for agents. `.devflow/features/.knowledge.lock` is gitignored (transient lock directory for concurrent index writes, added automatically by `devflow init`). `devflow knowledge list` — List all feature knowledge bases with staleness status. `devflow knowledge create ` — Create a new knowledge base via claude -p exploration. `devflow knowledge check` — Check all knowledge bases for staleness. `devflow knowledge refresh [slug]` — Refresh stale knowledge base(s). `devflow knowledge remove ` — Remove a knowledge base and its index entry. Note: `/debug` keeps FEATURE_KNOWLEDGE orchestrator-local (investigation workers examine code without pre-loaded context). Toggleable via `devflow knowledge --enable/--disable/--status` or `devflow init --knowledge/--no-knowledge`. SessionEnd hook auto-refreshes stale knowledge bases (throttled to once per 2 hours, max 3 per run). `.devflow/features/.disabled` sentinel gates Phase 12 generation and refresh hook. +**Feature Knowledge Bases**: Per-feature `.devflow/features/` directory containing KNOWLEDGE.md files that capture area-specific patterns, conventions, architecture, and gotchas. Uses a **write-through** model: load = direct file-I/O reading `.devflow/features/index.md` (regenerable cache) with frontmatter-glob fallback over `features/*/KNOWLEDGE.md` (source of truth) + verify-against-code on read; save = in-command write-through via a simplified Knowledge agent that writes `KNOWLEDGE.md` + the `index.md` line directly (no `.create-result.json`, no external scripts, no lock). Freshness = write-through + verify-on-read (NO git-staleness, NO SessionEnd eval, NO Dream task). `index.md` line format: `- **{slug}** — {areas} — {Use-when description}`; frontmatter is authoritative if the line is lost. MDS module: `shared/knowledge/` contains `_knowledge.mds` (defines/exports `knowledge_load` and `knowledge_writeback` partials) + 9 host `.mds` sources compiled to plugin commands at build time by `scripts/build-knowledge.ts` (`npm run build:knowledge`). `knowledge_load` is used up-front by: implement, plan, resolve, code-review, self-review, research, bug-analysis. `knowledge_writeback` is used at workflow end by: implement, resolve, self-review, explore, debug. explore/debug do NOT load up-front (intentional asymmetry). Config gate: single `knowledge: true|false` in dream config (default true) — gates write-back only; load is ungated. CLI: `devflow knowledge list` (read index.md / frontmatter glob), `devflow knowledge --enable/--disable/--status` (flip config). Note: `/debug` keeps FEATURE_KNOWLEDGE orchestrator-local (investigation workers examine code without pre-loaded context). Toggleable via `devflow knowledge --enable/--disable/--status` or `devflow init --knowledge/--no-knowledge`. **Rules**: Ultra-concise, always-on engineering principle files (~10-15 lines each) installed to `~/.claude/rules/devflow/` as flat `.md` files. Claude Code loads them automatically on every prompt — no hooks required — filling the guidance gap for quick edits that don't trigger a full skill pipeline. Rules flow through the same four-stage pipeline as skills: authored in `shared/rules/`, distributed to `plugins/*/rules/` at build time, installed (or shadowed) at runtime, and activated automatically. Unlike skills (which install universally from all plugins), rules are **plugin-scoped**: only rules belonging to selected plugins are installed. This keeps core rules (`security`, `engineering`, `quality`, `reliability` from `devflow-core-skills`) always present, and language/ecosystem rules (`typescript`, `react`, `go`, etc.) present only when the user has that plugin installed. Shadow overrides: `~/.devflow/rules/{name}.md` overrides the Devflow source. Toggleable via `devflow rules --enable/--disable/--status/--list` or `devflow init --rules/--no-rules`. Stored in manifest `features.rules: boolean` (self-heals to `true` on old manifests). Currently 12 rules: 4 core + 8 language/UI. `paths: []` YAML frontmatter must remain — it signals Claude Code to apply the rule globally. -**Two independent background pipelines** (both toggleable separately): +**One background pipeline** (toggleable): - `devflow decisions --enable/--disable` — Decisions pipeline (decision + pitfall detection, materialized by Dream agent from DIALOG_PAIRS) -- `devflow knowledge --enable/--disable` — Feature knowledge bases (codebase area exploration) + +Knowledge write-back is in-command (not a background pipeline): gated by `devflow knowledge --enable/--disable` (flips `knowledge` in dream config); Knowledge agent writes directly at workflow end. **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, decisions ON, rules ON, HUD ON, default-ON flags, .claudeignore ON, auto-install safe-delete if trash CLI detected, user-mode security deny list, viewMode preserved from existing settings.json. Advanced path adds a view mode selector (default/verbose/focus) after Claude Code flags. Use `--decisions/--no-decisions` to toggle the decisions agent independently. Use `--rules/--no-rules` to toggle rules independently. -**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`), `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), `purge-orphaned-sidecar-judgment-state` (per-project; removes orphaned `.learning-manifest.json`, `.decisions-manifest.json`, `.decisions-notifications.json` — judgment-state files written by the now-removed deterministic render/reconcile layer), `purge-learning-pipeline-v1` (per-project; removes `.devflow/learning/` directory, learning dream markers, `learning` key from dream/sidecar config, `.claude/commands/self-learning/`, and auto-generated skills), `purge-stale-memory-markers-v1` (per-project; removes stale `dream/memory.*` markers left by the old Dream-subagent memory pipeline now that `background-memory-update` handles memory refresh — ENOENT-idempotent, rethrows non-ENOENT errors), `purge-dead-working-memory-sentinel-v1` (per-project; removes the stale `.devflow/memory/.working-memory-disabled` sentinel now that the memory gate is config-only per ADR-001 — ENOENT-tolerant, rethrows non-ENOENT errors). Global migrations: `purge-learning-global-v1` removes `~/.devflow/learning.json`; `purge-orphaned-dream-commit-hook-v1` removes the orphaned `~/.devflow/scripts/hooks/dream-commit` (the `dream-commit` helper was deleted when `.devflow/` became gitignored-by-default per ADR-021, but the installer copies `scripts/` additively — `copyDirectory` never deletes — so the stale file would otherwise linger; ENOENT-idempotent). **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`. +**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`), `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), `purge-orphaned-sidecar-judgment-state` (per-project; removes orphaned `.learning-manifest.json`, `.decisions-manifest.json`, `.decisions-notifications.json` — judgment-state files written by the now-removed deterministic render/reconcile layer), `purge-learning-pipeline-v1` (per-project; removes `.devflow/learning/` directory, learning dream markers, `learning` key from dream/sidecar config, `.claude/commands/self-learning/`, and auto-generated skills), `purge-stale-memory-markers-v1` (per-project; removes stale `dream/memory.*` markers left by the old Dream-subagent memory pipeline now that `background-memory-update` handles memory refresh — ENOENT-idempotent, rethrows non-ENOENT errors), `purge-dead-working-memory-sentinel-v1` (per-project; removes the stale `.devflow/memory/.working-memory-disabled` sentinel now that the memory gate is config-only per ADR-001 — ENOENT-tolerant, rethrows non-ENOENT errors). Global migrations: `purge-learning-global-v1` removes `~/.devflow/learning.json`; `purge-orphaned-dream-commit-hook-v1` removes the orphaned `~/.devflow/scripts/hooks/dream-commit` (the `dream-commit` helper was deleted when `.devflow/` became gitignored-by-default per ADR-021, but the installer copies `scripts/` additively — `copyDirectory` never deletes — so the stale file would otherwise linger; ENOENT-idempotent); `purge-knowledge-hooks-global-v1` removes the orphaned `~/.devflow/scripts/hooks/eval-knowledge` and `~/.devflow/scripts/hooks/lib/feature-knowledge.cjs` (deleted from source in Phase 2 of the write-through migration; installer copies additively so they linger; ENOENT-idempotent). Per-project: `purge-feature-knowledge-pipeline-v1` removes `.devflow/dream/knowledge.*` markers, `.devflow/features/.knowledge.lock/` (recursive), `.devflow/features/.knowledge-last-refresh`, `.devflow/features/.knowledge-refresh.lock`, `.devflow/features/.disabled`; renames `.devflow/features/index.json` → `index.json.deprecated` (if present; never recreated — write-through creates `index.md` lazily). Does NOT touch the `knowledge` key in dream config (write-back gate stays). Existing KNOWLEDGE.md files remain loadable via frontmatter-glob fallback. **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`. ## Project Structure @@ -78,7 +79,7 @@ devflow/ ├── plugins/devflow-*/ # 22 plugins (12 core + 9 optional language/ecosystem + 1 optional workflow) ├── docs/reference/ # Detailed reference documentation ├── scripts/ # Helper scripts (statusline, docs-helpers) -│ └── hooks/ # Dream + ambient + memory hooks (dream-capture, dream-dispatch [capture-only], background-memory-update [Stop-hook worker], dream-recover, dream-collect-tasks, dream-evaluate, dream-lock, session-start-memory, session-start-context, pre-compact-memory, preamble, get-mtime, hook-bootstrap, hook-log-init, eval-helpers, eval-decisions, eval-knowledge, eval-curation) +│ └── hooks/ # Dream + ambient + memory hooks (dream-capture, dream-dispatch [capture-only], background-memory-update [Stop-hook worker], dream-recover, dream-collect-tasks, dream-evaluate, dream-lock, session-start-memory, session-start-context, pre-compact-memory, preamble, get-mtime, hook-bootstrap, hook-log-init, eval-helpers, eval-decisions, eval-curation) ├── src/cli/ # TypeScript CLI (init, list, uninstall, ambient, decisions, flags, knowledge, rules, debug) ├── .claude-plugin/ # Marketplace registry ├── .devflow/ # All per-project runtime data — gitignored wholesale by default (ensure-devflow-init adds .devflow/ to the root .gitignore; sharing is opt-in) @@ -113,7 +114,7 @@ node dist/cli.js init --plugin=code-review # Single plugin /code-review ``` -**Build commands**: `npm run build` (full), `npm run build:cli` (TypeScript only), `npm run build:plugins` (skill/agent distribution only), `npm run build:recipes` (MDS recipe compilation only) +**Build commands**: `npm run build` (full), `npm run build:cli` (TypeScript only), `npm run build:plugins` (skill/agent distribution only), `npm run build:recipes` (MDS recipe compilation only), `npm run build:knowledge` (MDS knowledge command compilation only) ## Documentation Artifacts @@ -160,7 +161,7 @@ Per-project runtime files live under `.devflow/`: │ ├── .working-memory-last-trigger # Mtime = last worker spawn time (120s throttle key, transient) │ ├── .last-refresh-ok # Mtime = last successful WORKING-MEMORY.md write (transient) │ └── .working-memory.lock/ # Worker lock dir — 300s stale-break (transient, never tracked) -├── dream/ # Dream state: config.json (feature toggles), per-session markers (decisions.{session}.json, knowledge.{session}.json, curation.{session}.json), .processor-spawned-at (120s spawn throttle), .curation-last (7-day curation throttle) +├── dream/ # Dream state: config.json (feature toggles), per-session markers (decisions.{session}.json, curation.{session}.json), .processor-spawned-at (120s spawn throttle), .curation-last (7-day curation throttle) ├── decisions/ │ ├── decisions-ledger.jsonl # Anchored ledger (gitignored by default) — render source of truth; one row per ADR/PF incl. retired │ ├── decisions-log.jsonl # Raw decision/pitfall observations (JSONL, gitignored) @@ -174,10 +175,7 @@ Per-project runtime files live under `.devflow/`: │ └── .disabled # Runtime sentinel — decisions sections in session-start-context skip if present └── features/ # Per-feature knowledge bases (gitignored by default; opt-in to share) ├── {slug}/KNOWLEDGE.md - ├── index.json - ├── .disabled # Sentinel — gates Phase 12 generation and refresh hook - ├── .knowledge.lock # Transient lock directory for concurrent index writes (gitignored) - └── .knowledge-last-refresh # Epoch timestamp of last auto-refresh + └── index.md # Regenerable cache (line format: `- **{slug}** — {areas} — {Use-when}`); frontmatter is authoritative if absent ~/.devflow/logs/{project-slug}/ ├── .dream-capture.log # dream-capture (Stop hook) log @@ -194,7 +192,7 @@ Per-project runtime files live under `.devflow/`: **Universal Skill Installation**: All skills from all plugins are always installed, regardless of plugin selection. Skills are tiny markdown files installed as `~/.claude/skills/devflow:{name}/` (namespaced to avoid collisions with other plugin ecosystems). Source directories in `shared/skills/` stay unprefixed — the `devflow:` prefix is applied at install-time only. Shadow overrides live at `~/.devflow/skills/{name}/` (unprefixed); when shadowed, the installer copies the user's version to the prefixed install target. Only commands and agents remain plugin-specific. -**Model Strategy**: Explicit model assignments in agent frontmatter override the user's session model. Opus for analysis agents (reviewer, scrutinizer, evaluator, designer, researcher, bug-analyzer), Sonnet for execution agents (coder, simplifier, resolver, skimmer, tester), Haiku for I/O agents (git, synthesizer, validator). Dream uses per-task overrides via `session-start-context`: sonnet for knowledge, opus for decisions/curation (the latter two share one combined opus spawn). Memory is no longer a Dream task — `WORKING-MEMORY.md` is refreshed by the detached `background-memory-update` worker (`claude -p --model haiku`) spawned by `dream-capture`. +**Model Strategy**: Explicit model assignments in agent frontmatter override the user's session model. Opus for analysis agents (reviewer, scrutinizer, evaluator, designer, researcher, bug-analyzer), Sonnet for execution agents (coder, simplifier, resolver, skimmer, tester, knowledge), Haiku for I/O agents (git, synthesizer, validator). Dream uses per-task overrides via `session-start-context`: opus for decisions/curation (share one combined opus spawn). Memory is not a Dream task — `WORKING-MEMORY.md` is refreshed by the detached `background-memory-update` worker (`claude -p --model haiku`) spawned by `dream-capture`. Knowledge is not a Dream task — the Knowledge agent (sonnet) is spawned in-command by `knowledge_writeback()` at workflow end. ## Agent & Command Roster diff --git a/src/cli/utils/migrations.ts b/src/cli/utils/migrations.ts index ebb6dfe5..df9a93be 100644 --- a/src/cli/utils/migrations.ts +++ b/src/cli/utils/migrations.ts @@ -958,6 +958,155 @@ const MIGRATION_PURGE_DEAD_WORKING_MEMORY_SENTINEL: Migration<'per-project'> = { }, }; +/** + * Global: remove the orphaned eval-knowledge hook and feature-knowledge.cjs + * library left in prior installs at ~/.devflow/scripts/hooks/. + * + * Phase 2 deleted these files from source, but the installer copies scripts/ + * additively (copyDirectory never deletes), so stale copies linger on every + * existing machine. Same rationale as purge-orphaned-dream-commit-hook-v1. + * + * Removes (ENOENT-idempotent; rethrows non-ENOENT errors): + * - ~/.devflow/scripts/hooks/eval-knowledge + * - ~/.devflow/scripts/hooks/lib/feature-knowledge.cjs + */ +const MIGRATION_PURGE_KNOWLEDGE_HOOKS_GLOBAL: Migration<'global'> = { + id: 'purge-knowledge-hooks-global-v1', + description: 'Remove orphaned eval-knowledge hook + feature-knowledge.cjs library from ~/.devflow/scripts/hooks/', + scope: 'global', + async run(ctx: GlobalMigrationContext): Promise { + const hooksDir = path.join(ctx.devflowDir, 'scripts', 'hooks'); + const toRemove = [ + path.join(hooksDir, 'eval-knowledge'), + path.join(hooksDir, 'lib', 'feature-knowledge.cjs'), + ]; + + let removed = 0; + for (const filePath of toRemove) { + try { + await fs.unlink(filePath); + removed++; + } catch (err) { + const code = (err as NodeJS.ErrnoException).code; + if (code === 'ENOENT') continue; // already absent — idempotent skip + throw err; // unexpected — surface to runner + } + } + + const infos = removed > 0 + ? [`Removed ${removed} orphaned knowledge hook file(s) from ~/.devflow/scripts/hooks/`] + : []; + return { infos, warnings: [] }; + }, +}; + +/** + * Per-project: remove the feature-knowledge pipeline runtime artifacts left + * by the old SessionEnd hook / Dream refresh task model. + * + * Write-through (Phase 1–2) replaced the entire pipeline. Knowledge is now + * authored in-command; the Dream refresh path is gone. Per ADR-001, the + * config gate (knowledge key in dream/config.json) is intentionally preserved + * — it still gates write-back in the new model. + * + * Removes (ENOENT-idempotent; rethrows non-ENOENT errors): + * - .devflow/dream/knowledge.*.json markers (glob) + * - .devflow/dream/knowledge.*.processing markers (glob) + * - .devflow/features/.knowledge.lock/ directory (recursive) + * - .devflow/features/.knowledge-last-refresh (file) + * - .devflow/features/.knowledge-refresh.lock (file or dir) + * - .devflow/features/.disabled (sentinel — config-only gate now) + * - renames .devflow/features/index.json → index.json.deprecated (if present) + * + * Does NOT touch the `knowledge` key in dream/config.json (write-back gate stays). + * Does NOT remove KNOWLEDGE.md files — existing KBs remain loadable via frontmatter. + */ +const MIGRATION_PURGE_FEATURE_KNOWLEDGE_PIPELINE: Migration<'per-project'> = { + id: 'purge-feature-knowledge-pipeline-v1', + description: 'Remove feature-knowledge pipeline artifacts (dream markers, lock/sentinel/refresh files, deprecate index.json)', + scope: 'per-project', + async run(ctx: PerProjectMigrationContext): Promise { + const devflowDir = path.join(ctx.projectRoot, '.devflow'); + const dreamDir = path.join(devflowDir, 'dream'); + const featuresDir = path.join(devflowDir, 'features'); + let removed = 0; + + // 1. Remove .devflow/dream/knowledge.*.json and *.processing markers + try { + const dreamEntries = await fs.readdir(dreamDir); + for (const entry of dreamEntries) { + if (entry.startsWith('knowledge.') && + (entry.endsWith('.json') || entry.endsWith('.processing'))) { + try { + await fs.unlink(path.join(dreamDir, entry)); + removed++; + } catch (err) { + const code = (err as NodeJS.ErrnoException).code; + if (code !== 'ENOENT') throw err; + } + } + } + } catch (err) { + const code = (err as NodeJS.ErrnoException).code; + if (code !== 'ENOENT') throw err; // unexpected — surface to runner + } + + // 2. Remove transient artifacts from .devflow/features/ + const featureFilesToRemove = [ + path.join(featuresDir, '.knowledge-last-refresh'), + path.join(featuresDir, '.disabled'), + ]; + for (const filePath of featureFilesToRemove) { + try { + await fs.unlink(filePath); + removed++; + } catch (err) { + const code = (err as NodeJS.ErrnoException).code; + if (code !== 'ENOENT') throw err; + } + } + + // 3. Remove .knowledge.lock dir (recursive) and .knowledge-refresh.lock (file or dir) + const featureDirsToRemove = [ + path.join(featuresDir, '.knowledge.lock'), + path.join(featuresDir, '.knowledge-refresh.lock'), + ]; + for (const dirPath of featureDirsToRemove) { + try { + await fs.rm(dirPath, { recursive: true, force: true }); + removed++; + } catch (err) { + const code = (err as NodeJS.ErrnoException).code; + if (code !== 'ENOENT') throw err; + } + } + + // 4. Rename index.json → index.json.deprecated (if present; never recreate index.json) + const indexJsonPath = path.join(featuresDir, 'index.json'); + const indexDeprecatedPath = path.join(featuresDir, 'index.json.deprecated'); + try { + // Skip if deprecated already exists (idempotent) + try { + await fs.lstat(indexDeprecatedPath); + // dest already present — idempotent skip + } catch { + // dest absent — proceed with rename + await fs.rename(indexJsonPath, indexDeprecatedPath); + removed++; + } + } catch (err) { + const code = (err as NodeJS.ErrnoException).code; + if (code !== 'ENOENT') throw err; // unexpected — surface to runner + // ENOENT on indexJsonPath means nothing to rename — idempotent no-op + } + + const infos = removed > 0 + ? [`Removed ${removed} feature-knowledge pipeline artifact(s)`] + : []; + return { infos, warnings: [] }; + }, +}; + export const MIGRATIONS: readonly Migration[] = [ MIGRATION_SHADOW_OVERRIDES, MIGRATION_PURGE_LEGACY_KNOWLEDGE, @@ -975,6 +1124,8 @@ export const MIGRATIONS: readonly Migration[] = [ MIGRATION_PURGE_TEAMMATE_MODE_PER_PROJECT, MIGRATION_DECISIONS_LEDGER_UNIFY, MIGRATION_PURGE_DEAD_WORKING_MEMORY_SENTINEL, + MIGRATION_PURGE_KNOWLEDGE_HOOKS_GLOBAL, + MIGRATION_PURGE_FEATURE_KNOWLEDGE_PIPELINE, ]; const MIGRATIONS_FILE = 'migrations.json'; diff --git a/tests/build-knowledge.test.ts b/tests/build-knowledge.test.ts new file mode 100644 index 00000000..40c40f08 --- /dev/null +++ b/tests/build-knowledge.test.ts @@ -0,0 +1,218 @@ +/** + * Smoke tests for scripts/build-knowledge.ts + * + * Validates the four key guarantees of the build-knowledge script (AC C4): + * + * 1. Explicit SOURCE_TO_PLUGIN_MAP — the 9 source basenames and their plugin + * destination directories match the canonical mapping in build-knowledge.ts. + * 2. MDS compiler happy path — a valid .mds source compiles to a non-empty + * Markdown string (same MDS mechanism the script relies on). + * 3. Script happy-path exit — spawning the real build-knowledge.ts script against + * the real shared/knowledge/ sources exits 0 and produces at least one .md file. + * 4. Missing source exits non-zero — verifies the hard-fail contract when a source + * or plugin destination is absent (tested via the script's validation logic). + * + * NOTE: The error-path subprocess test (missing source / unknown plugin) is validated + * structurally via the SOURCE_TO_PLUGIN_MAP assertion (test 1) and the MDS error-path + * in build-recipes.test.ts (shared mechanism). Test 3 covers the happy-path subprocess + * contract. Together they lock both ends without mutating committed source files. + */ + +import { describe, it, expect, beforeAll } from 'vitest'; +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { spawnSync } from 'child_process'; +import { init, compile, isMdsError } from '@mdscript/mds'; + +const ROOT = path.resolve(import.meta.dirname, '..'); +const KNOWLEDGE_DIR = path.join(ROOT, 'shared', 'knowledge'); + +/** + * Canonical SOURCE_TO_PLUGIN_MAP — must stay in sync with scripts/build-knowledge.ts. + * This test locks the 9-entry explicit mapping (AC C4). + */ +const EXPECTED_SOURCE_TO_PLUGIN_MAP: Record = { + 'implement': 'plugins/devflow-implement/commands', + 'plan': 'plugins/devflow-plan/commands', + 'resolve': 'plugins/devflow-resolve/commands', + 'code-review': 'plugins/devflow-code-review/commands', + 'self-review': 'plugins/devflow-self-review/commands', + 'research': 'plugins/devflow-research/commands', + 'bug-analysis': 'plugins/devflow-bug-analysis/commands', + 'explore': 'plugins/devflow-explore/commands', + 'debug': 'plugins/devflow-debug/commands', +}; + +// --------------------------------------------------------------------------- +// Shared MDS initialisation — required before compile calls +// --------------------------------------------------------------------------- + +let mdsInitialised = false; + +async function ensureInit(): Promise { + if (!mdsInitialised) { + await init(); + mdsInitialised = true; + } +} + +// --------------------------------------------------------------------------- +// 1. SOURCE_TO_PLUGIN_MAP — lock the 9-entry explicit mapping (AC C4) +// --------------------------------------------------------------------------- + +describe('SOURCE_TO_PLUGIN_MAP — 9-entry explicit mapping', () => { + it('has exactly 9 entries', () => { + expect(Object.keys(EXPECTED_SOURCE_TO_PLUGIN_MAP)).toHaveLength(9); + }); + + it('each source .mds file exists in shared/knowledge/', async () => { + for (const basename of Object.keys(EXPECTED_SOURCE_TO_PLUGIN_MAP)) { + const sourcePath = path.join(KNOWLEDGE_DIR, `${basename}.mds`); + await expect( + fs.access(sourcePath), + `Missing source: shared/knowledge/${basename}.mds`, + ).resolves.toBeUndefined(); + } + }); + + it('each destination plugin commands/ directory exists', async () => { + for (const [basename, destRelDir] of Object.entries(EXPECTED_SOURCE_TO_PLUGIN_MAP)) { + const destDir = path.join(ROOT, destRelDir); + await expect( + fs.access(destDir), + `Missing plugin commands/ dir for ${basename}: ${destRelDir}`, + ).resolves.toBeUndefined(); + } + }); + + it('output filename derivation matches {basename}.md pattern', () => { + for (const basename of Object.keys(EXPECTED_SOURCE_TO_PLUGIN_MAP)) { + const derived = path.basename(`${basename}.mds`, '.mds') + '.md'; + expect(derived).toBe(`${basename}.md`); + } + }); + + it('_knowledge.mds partial exists in shared/knowledge/', async () => { + const partialPath = path.join(KNOWLEDGE_DIR, '_knowledge.mds'); + await expect(fs.access(partialPath)).resolves.toBeUndefined(); + }); + + it('_knowledge.mds is a partial (starts with _) and is not in the output map', () => { + expect(Object.keys(EXPECTED_SOURCE_TO_PLUGIN_MAP)).not.toContain('_knowledge'); + }); +}); + +// --------------------------------------------------------------------------- +// 2. MDS compiler happy path (shared with build-recipes.test.ts mechanism) +// --------------------------------------------------------------------------- + +describe('MDS compiler happy path (build-knowledge hard-fail mechanism)', () => { + it('compiles a minimal valid .mds source to a non-empty Markdown string', async () => { + await ensureInit(); + const validSource = '# My Command\n\nThis is a valid MDS template.\n'; + const result = compile(validSource); + expect(typeof result.output).toBe('string'); + expect(result.output.length).toBeGreaterThan(0); + expect(result.output).toContain('My Command'); + expect(Array.isArray(result.warnings)).toBe(true); + }); + + it('isMdsError correctly identifies MDS errors', () => { + const generic = new Error('generic'); + expect(isMdsError(generic)).toBe(false); + expect(isMdsError(null)).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// 3. Script happy-path exit (AC C4 — subprocess contract) +// --------------------------------------------------------------------------- + +describe('build-knowledge.ts script subprocess contract', () => { + it('exits 0 when real knowledge sources compile cleanly (CI path)', () => { + const result = spawnSync( + 'npx', + ['tsx', path.join(ROOT, 'scripts', 'build-knowledge.ts')], + { + cwd: ROOT, + encoding: 'utf-8', + timeout: 60_000, // 60s generous timeout for WASM init + file I/O + }, + ); + + if (result.error) { + throw result.error; + } + + expect( + result.status, + `build-knowledge.ts should exit 0 but exited ${result.status}.\nstdout: ${result.stdout}\nstderr: ${result.stderr}`, + ).toBe(0); + }); + + it('produces at least one .md command file after the script runs', async () => { + // After the subprocess test above runs the script, each mapped plugin + // commands/ dir should contain the compiled .md file for that source. + let foundAtLeastOne = false; + for (const [basename, destRelDir] of Object.entries(EXPECTED_SOURCE_TO_PLUGIN_MAP)) { + const outputPath = path.join(ROOT, destRelDir, `${basename}.md`); + try { + await fs.access(outputPath); + foundAtLeastOne = true; + break; + } catch { + // Not found for this entry — continue + } + } + expect(foundAtLeastOne, 'at least one compiled .md command file should exist after build').toBe(true); + }); +}); + +// --------------------------------------------------------------------------- +// 4. Compiled output assertions — no stale call sites (AC F1) +// --------------------------------------------------------------------------- + +describe('compiled knowledge commands — no stale call-site references', () => { + beforeAll(async () => { + // Ensure fresh compilation + const result = spawnSync( + 'npx', + ['tsx', path.join(ROOT, 'scripts', 'build-knowledge.ts')], + { cwd: ROOT, encoding: 'utf-8', timeout: 60_000 }, + ); + if (result.error) throw result.error; + }); + + it('no compiled command contains a literal knowledge_load() or knowledge_writeback() call site', async () => { + const callSitePattern = /\{knowledge_(?:load|writeback)\(\)\}/; + for (const [basename, destRelDir] of Object.entries(EXPECTED_SOURCE_TO_PLUGIN_MAP)) { + const outputPath = path.join(ROOT, destRelDir, `${basename}.md`); + let content: string; + try { + content = await fs.readFile(outputPath, 'utf-8'); + } catch { + continue; // file not present — skip (covered by subprocess test above) + } + expect( + callSitePattern.test(content), + `${destRelDir}/${basename}.md must not contain un-expanded MDS call sites`, + ).toBe(false); + } + }); + + it('no compiled command references feature-knowledge.cjs', async () => { + for (const [basename, destRelDir] of Object.entries(EXPECTED_SOURCE_TO_PLUGIN_MAP)) { + const outputPath = path.join(ROOT, destRelDir, `${basename}.md`); + let content: string; + try { + content = await fs.readFile(outputPath, 'utf-8'); + } catch { + continue; + } + expect( + content, + `${destRelDir}/${basename}.md must not reference feature-knowledge.cjs`, + ).not.toContain('feature-knowledge.cjs'); + } + }); +}); diff --git a/tests/migrations.test.ts b/tests/migrations.test.ts index 60985dfe..b6764f90 100644 --- a/tests/migrations.test.ts +++ b/tests/migrations.test.ts @@ -163,6 +163,36 @@ describe('MIGRATIONS', () => { expect(renameIdx).toBeGreaterThanOrEqual(0); expect(consolidateIdx).toBeGreaterThan(renameIdx); }); + + it('contains purge-knowledge-hooks-global-v1 with global scope', () => { + const m = MIGRATIONS.find(m => m.id === 'purge-knowledge-hooks-global-v1'); + expect(m).toBeDefined(); + expect(m?.scope).toBe('global'); + expect(m?.description).toBeTruthy(); + expect(typeof m?.run).toBe('function'); + }); + + it('contains purge-feature-knowledge-pipeline-v1 with per-project scope', () => { + const m = MIGRATIONS.find(m => m.id === 'purge-feature-knowledge-pipeline-v1'); + expect(m).toBeDefined(); + expect(m?.scope).toBe('per-project'); + expect(m?.description).toBeTruthy(); + expect(typeof m?.run).toBe('function'); + }); + + it('purge-knowledge-hooks-global-v1 appears after purge-dead-working-memory-sentinel-v1', () => { + const sentinelIdx = MIGRATIONS.findIndex(m => m.id === 'purge-dead-working-memory-sentinel-v1'); + const hooksIdx = MIGRATIONS.findIndex(m => m.id === 'purge-knowledge-hooks-global-v1'); + expect(sentinelIdx).toBeGreaterThanOrEqual(0); + expect(hooksIdx).toBeGreaterThan(sentinelIdx); + }); + + it('purge-feature-knowledge-pipeline-v1 appears after purge-knowledge-hooks-global-v1', () => { + const hooksIdx = MIGRATIONS.findIndex(m => m.id === 'purge-knowledge-hooks-global-v1'); + const pipelineIdx = MIGRATIONS.findIndex(m => m.id === 'purge-feature-knowledge-pipeline-v1'); + expect(hooksIdx).toBeGreaterThanOrEqual(0); + expect(pipelineIdx).toBeGreaterThan(hooksIdx); + }); }); describe('runMigrations', () => { @@ -1898,3 +1928,325 @@ describe('purge-dead-working-memory-sentinel-v1 migration', () => { expect(deadIdx).toBeGreaterThan(staleIdx); }); }); + +// --------------------------------------------------------------------------- +// purge-knowledge-hooks-global-v1 migration +// --------------------------------------------------------------------------- + +describe('purge-knowledge-hooks-global-v1 migration', () => { + let tmpDir: string; + let fakeDevflow: string; + let originalHome: string | undefined; + + beforeEach(async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'devflow-knowledge-hooks-global-test-')); + fakeDevflow = path.join(tmpDir, 'devflow'); + await fs.mkdir(path.join(fakeDevflow, 'scripts', 'hooks', 'lib'), { recursive: true }); + originalHome = process.env.HOME; + process.env.HOME = path.join(tmpDir, 'home'); + await fs.mkdir(path.join(tmpDir, 'home', '.devflow'), { recursive: true }); + }); + + afterEach(async () => { + if (originalHome !== undefined) { + process.env.HOME = originalHome; + } else { + delete process.env.HOME; + } + await fs.rm(tmpDir, { recursive: true, force: true }); + }); + + function getMigration(): Migration<'global'> { + const m = MIGRATIONS.find(m => m.id === 'purge-knowledge-hooks-global-v1'); + if (!m) throw new Error('purge-knowledge-hooks-global-v1 migration not found'); + return m as Migration<'global'>; + } + + function makeCtx(): import('../src/cli/utils/migrations.js').GlobalMigrationContext { + return { scope: 'global', devflowDir: fakeDevflow }; + } + + it('is registered in MIGRATIONS with global scope', () => { + const m = MIGRATIONS.find(m => m.id === 'purge-knowledge-hooks-global-v1'); + expect(m).toBeDefined(); + expect(m?.scope).toBe('global'); + expect(m?.description).toBeTruthy(); + expect(typeof m?.run).toBe('function'); + }); + + it('appears after purge-dead-working-memory-sentinel-v1 in MIGRATIONS array', () => { + const prevIdx = MIGRATIONS.findIndex(m => m.id === 'purge-dead-working-memory-sentinel-v1'); + const thisIdx = MIGRATIONS.findIndex(m => m.id === 'purge-knowledge-hooks-global-v1'); + expect(prevIdx).toBeGreaterThanOrEqual(0); + expect(thisIdx).toBeGreaterThan(prevIdx); + }); + + it('removes eval-knowledge hook when present', async () => { + const hookPath = path.join(fakeDevflow, 'scripts', 'hooks', 'eval-knowledge'); + await fs.writeFile(hookPath, '#!/bin/bash\necho eval-knowledge\n', 'utf-8'); + + const result = await getMigration().run(makeCtx()); + + await expect(fs.access(hookPath)).rejects.toThrow(); + expect(result?.infos?.length).toBeGreaterThan(0); + expect(result?.warnings ?? []).toEqual([]); + }); + + it('removes feature-knowledge.cjs when present', async () => { + const libPath = path.join(fakeDevflow, 'scripts', 'hooks', 'lib', 'feature-knowledge.cjs'); + await fs.writeFile(libPath, '// feature-knowledge lib\n', 'utf-8'); + + const result = await getMigration().run(makeCtx()); + + await expect(fs.access(libPath)).rejects.toThrow(); + expect(result?.infos?.length).toBeGreaterThan(0); + }); + + it('removes both files when both are present', async () => { + const hookPath = path.join(fakeDevflow, 'scripts', 'hooks', 'eval-knowledge'); + const libPath = path.join(fakeDevflow, 'scripts', 'hooks', 'lib', 'feature-knowledge.cjs'); + await fs.writeFile(hookPath, '#!/bin/bash\n', 'utf-8'); + await fs.writeFile(libPath, '// lib\n', 'utf-8'); + + const result = await getMigration().run(makeCtx()); + + await expect(fs.access(hookPath)).rejects.toThrow(); + await expect(fs.access(libPath)).rejects.toThrow(); + expect(result?.infos?.length).toBeGreaterThan(0); + }); + + it('is a no-op when neither file exists (ENOENT-idempotent)', async () => { + // No files seeded — migration should succeed without error + const result = await getMigration().run(makeCtx()); + expect(result?.infos ?? []).toEqual([]); + expect(result?.warnings ?? []).toEqual([]); + }); + + it('is idempotent — running twice produces no error and same result', async () => { + const hookPath = path.join(fakeDevflow, 'scripts', 'hooks', 'eval-knowledge'); + await fs.writeFile(hookPath, '#!/bin/bash\n', 'utf-8'); + + await getMigration().run(makeCtx()); + // Second run: file already gone — must not throw + await expect(getMigration().run(makeCtx())).resolves.not.toThrow(); + const result = await getMigration().run(makeCtx()); + expect(result?.infos ?? []).toEqual([]); + }); +}); + +// --------------------------------------------------------------------------- +// purge-feature-knowledge-pipeline-v1 migration +// --------------------------------------------------------------------------- + +describe('purge-feature-knowledge-pipeline-v1 migration', () => { + let tmpDir: string; + let projectRoot: string; + let devflowDir: string; + let dreamDir: string; + let featuresDir: string; + let fakeHome: string; + let originalHome: string | undefined; + + beforeEach(async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'devflow-purge-knowledge-pipeline-test-')); + projectRoot = path.join(tmpDir, 'project'); + devflowDir = path.join(projectRoot, '.devflow'); + dreamDir = path.join(devflowDir, 'dream'); + featuresDir = path.join(devflowDir, 'features'); + await fs.mkdir(dreamDir, { recursive: true }); + await fs.mkdir(featuresDir, { recursive: true }); + originalHome = process.env.HOME; + process.env.HOME = path.join(tmpDir, 'home'); + fakeHome = path.join(tmpDir, 'home', '.devflow'); + await fs.mkdir(fakeHome, { recursive: true }); + }); + + afterEach(async () => { + if (originalHome !== undefined) { + process.env.HOME = originalHome; + } else { + delete process.env.HOME; + } + await fs.rm(tmpDir, { recursive: true, force: true }); + }); + + function getMigration(): Migration<'per-project'> { + const m = MIGRATIONS.find(m => m.id === 'purge-feature-knowledge-pipeline-v1'); + if (!m) throw new Error('purge-feature-knowledge-pipeline-v1 migration not found'); + return m as Migration<'per-project'>; + } + + function makeCtx(): import('../src/cli/utils/migrations.js').PerProjectMigrationContext { + return { + scope: 'per-project', + devflowDir: fakeHome, + memoryDir: path.join(devflowDir, 'memory'), + projectRoot, + }; + } + + it('is registered in MIGRATIONS with per-project scope', () => { + const m = MIGRATIONS.find(m => m.id === 'purge-feature-knowledge-pipeline-v1'); + expect(m).toBeDefined(); + expect(m?.scope).toBe('per-project'); + expect(m?.description).toBeTruthy(); + expect(typeof m?.run).toBe('function'); + }); + + it('appears after purge-knowledge-hooks-global-v1 in MIGRATIONS array', () => { + const globalIdx = MIGRATIONS.findIndex(m => m.id === 'purge-knowledge-hooks-global-v1'); + const thisIdx = MIGRATIONS.findIndex(m => m.id === 'purge-feature-knowledge-pipeline-v1'); + expect(globalIdx).toBeGreaterThanOrEqual(0); + expect(thisIdx).toBeGreaterThan(globalIdx); + }); + + it('removes knowledge.*.json dream markers', async () => { + const marker1 = path.join(dreamDir, 'knowledge.abc123.json'); + const marker2 = path.join(dreamDir, 'knowledge.def456.json'); + await fs.writeFile(marker1, '{}', 'utf-8'); + await fs.writeFile(marker2, '{}', 'utf-8'); + + await getMigration().run(makeCtx()); + + await expect(fs.access(marker1)).rejects.toThrow(); + await expect(fs.access(marker2)).rejects.toThrow(); + }); + + it('removes knowledge.*.processing dream markers', async () => { + const marker = path.join(dreamDir, 'knowledge.abc123.processing'); + await fs.writeFile(marker, '', 'utf-8'); + + await getMigration().run(makeCtx()); + + await expect(fs.access(marker)).rejects.toThrow(); + }); + + it('preserves non-knowledge dream files', async () => { + const decisionsMarker = path.join(dreamDir, 'decisions.abc123.json'); + const configFile = path.join(dreamDir, 'config.json'); + await fs.writeFile(decisionsMarker, '{}', 'utf-8'); + await fs.writeFile(configFile, '{"knowledge":true}', 'utf-8'); + + await getMigration().run(makeCtx()); + + // decisions and config must survive + await expect(fs.access(decisionsMarker)).resolves.toBeUndefined(); + await expect(fs.access(configFile)).resolves.toBeUndefined(); + }); + + it('removes .knowledge-last-refresh file', async () => { + const lastRefresh = path.join(featuresDir, '.knowledge-last-refresh'); + await fs.writeFile(lastRefresh, '1718000000', 'utf-8'); + + await getMigration().run(makeCtx()); + + await expect(fs.access(lastRefresh)).rejects.toThrow(); + }); + + it('removes .disabled sentinel file', async () => { + const disabled = path.join(featuresDir, '.disabled'); + await fs.writeFile(disabled, '', 'utf-8'); + + await getMigration().run(makeCtx()); + + await expect(fs.access(disabled)).rejects.toThrow(); + }); + + it('removes .knowledge.lock directory recursively', async () => { + const lockDir = path.join(featuresDir, '.knowledge.lock'); + await fs.mkdir(lockDir, { recursive: true }); + await fs.writeFile(path.join(lockDir, 'pid'), '12345', 'utf-8'); + + await getMigration().run(makeCtx()); + + await expect(fs.access(lockDir)).rejects.toThrow(); + }); + + it('removes .knowledge-refresh.lock directory recursively', async () => { + const lockDir = path.join(featuresDir, '.knowledge-refresh.lock'); + await fs.mkdir(lockDir, { recursive: true }); + + await getMigration().run(makeCtx()); + + await expect(fs.access(lockDir)).rejects.toThrow(); + }); + + it('renames index.json to index.json.deprecated when present', async () => { + const indexJson = path.join(featuresDir, 'index.json'); + const indexDeprecated = path.join(featuresDir, 'index.json.deprecated'); + const content = '{"feature-knowledge-system":{"name":"Feature Knowledge System"}}'; + await fs.writeFile(indexJson, content, 'utf-8'); + + await getMigration().run(makeCtx()); + + await expect(fs.access(indexJson)).rejects.toThrow(); + await expect(fs.access(indexDeprecated)).resolves.toBeUndefined(); + const renamed = await fs.readFile(indexDeprecated, 'utf-8'); + expect(renamed).toBe(content); + }); + + it('does NOT touch the knowledge key in dream config.json', async () => { + const configPath = path.join(dreamDir, 'config.json'); + const config = JSON.stringify({ knowledge: true, decisions: true }, null, 2); + await fs.writeFile(configPath, config, 'utf-8'); + + await getMigration().run(makeCtx()); + + const after = JSON.parse(await fs.readFile(configPath, 'utf-8')); + expect(after.knowledge).toBe(true); + expect(after.decisions).toBe(true); + }); + + it('does NOT remove KNOWLEDGE.md files from feature slugs', async () => { + const slugDir = path.join(featuresDir, 'my-feature'); + await fs.mkdir(slugDir, { recursive: true }); + const kbPath = path.join(slugDir, 'KNOWLEDGE.md'); + await fs.writeFile(kbPath, '# My Feature KB\n', 'utf-8'); + + await getMigration().run(makeCtx()); + + await expect(fs.access(kbPath)).resolves.toBeUndefined(); + const content = await fs.readFile(kbPath, 'utf-8'); + expect(content).toBe('# My Feature KB\n'); + }); + + it('is a no-op when no artifacts exist (ENOENT-idempotent)', async () => { + // No artifacts seeded — migration should succeed without error + const result = await getMigration().run(makeCtx()); + expect(result?.warnings ?? []).toEqual([]); + }); + + it('is idempotent — running twice produces no error and no further change', async () => { + // Seed all artifacts + await fs.writeFile(path.join(dreamDir, 'knowledge.abc123.json'), '{}', 'utf-8'); + await fs.writeFile(path.join(featuresDir, '.knowledge-last-refresh'), '1718000000', 'utf-8'); + await fs.writeFile(path.join(featuresDir, '.disabled'), '', 'utf-8'); + await fs.mkdir(path.join(featuresDir, '.knowledge.lock'), { recursive: true }); + await fs.writeFile(path.join(featuresDir, 'index.json'), '{}', 'utf-8'); + + // First run — performs cleanup + await getMigration().run(makeCtx()); + + // Second run — everything already gone, must not throw + await expect(getMigration().run(makeCtx())).resolves.not.toThrow(); + const result = await getMigration().run(makeCtx()); + expect(result?.warnings ?? []).toEqual([]); + + // index.json.deprecated must still be present (not removed on second run) + await expect( + fs.access(path.join(featuresDir, 'index.json.deprecated')), + ).resolves.toBeUndefined(); + }); + + it('does not rename index.json.deprecated a second time if already renamed', async () => { + // Pre-seed only the deprecated file (first run already ran) + const deprecatedPath = path.join(featuresDir, 'index.json.deprecated'); + await fs.writeFile(deprecatedPath, '{"existing":true}', 'utf-8'); + + await getMigration().run(makeCtx()); + + // Deprecated file must still contain original content + const content = await fs.readFile(deprecatedPath, 'utf-8'); + expect(content).toBe('{"existing":true}'); + }); +}); From 2784849263a477064b4c9f52e144e17fb0f4c38e Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sun, 28 Jun 2026 14:34:07 +0300 Subject: [PATCH 4/5] refactor: align /release knowledge load with write-through model + polish comments Update Phase 1b of /release to load feature knowledge using the write-through model: read index.md cache first, fall back to globbing KNOWLEDGE.md frontmatter on absence, and load full KBs for relevant areas. Removes the deprecated index.json load and feature-knowledge.cjs subprocess call. Polish plugins.ts legacy-sweep comment to correctly mark dream-knowledge as removed (not still-active) and clarify dream-decisions/dream-curation are the active pair. Update init.ts knowledge feature description to describe the write-through model accurately. --- plugins/devflow-release/commands/release.md | 2 +- src/cli/commands/init.ts | 4 ++-- src/cli/plugins.ts | 6 ++++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/plugins/devflow-release/commands/release.md b/plugins/devflow-release/commands/release.md index 9687e0ec..a2a94e05 100644 --- a/plugins/devflow-release/commands/release.md +++ b/plugins/devflow-release/commands/release.md @@ -53,7 +53,7 @@ Load the decisions index: DECISIONS_CONTEXT=$(node ~/.devflow/scripts/hooks/lib/decisions-index.cjs index "." 2>/dev/null || echo "(none)") ``` -Load feature knowledge: Read `.devflow/features/index.json`, match release-relevant files, read relevant KNOWLEDGE.md entries. Set `FEATURE_KNOWLEDGE` (or `(none)`). +Load feature knowledge: Attempt to read `.devflow/features/index.md` (the regenerable cache). If absent or empty, glob `.devflow/features/*/KNOWLEDGE.md` and read each file's YAML frontmatter (`name`, `description`, `directories`) as the relevance surface. Pick release-relevant KBs by matching their documented area against the release context. For each selected KB, read the full `KNOWLEDGE.md` — trust current code over KB content on any mismatch. Concatenate under slug headers and set `FEATURE_KNOWLEDGE` (or `(none)` if no KBs exist or none are relevant). No `index.json`, no subprocess, no `.cjs` script. Pass both to all subsequent agents via their input contracts. diff --git a/src/cli/commands/init.ts b/src/cli/commands/init.ts index 7ef0a08e..3d2b0156 100644 --- a/src/cli/commands/init.ts +++ b/src/cli/commands/init.ts @@ -594,8 +594,8 @@ export const initCommand = new Command('init') } 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.', + 'conventions, and gotchas. Created and updated automatically\n' + + 'when workflows touch a documented area (write-through model).', 'Feature Knowledge Bases', ); const knowledgeChoice = await p.confirm({ diff --git a/src/cli/plugins.ts b/src/cli/plugins.ts index adf04396..d60314da 100644 --- a/src/cli/plugins.ts +++ b/src/cli/plugins.ts @@ -536,8 +536,10 @@ const LEGACY_SKILLS_V2X: string[] = [ 'devflow:research:orch', 'devflow:release:orch', // v3.x dream per-task skills: bare names for pre-namespace installs. - // NOTE: dream-decisions, dream-knowledge, and dream-curation are STILL-ACTIVE skills - // (declared in DEVFLOW_PLUGINS, installed at the namespaced path devflow:dream-*). + // NOTE: dream-decisions and dream-curation are STILL-ACTIVE skills (declared in + // DEVFLOW_PLUGINS, installed at the namespaced path devflow:dream-*). + // dream-knowledge was REMOVED in this release — knowledge is now handled in-command + // via write-through (knowledge_writeback MDS partial). // These bare entries exist solely to clean up pre-namespace V2.x installs where // skills were written without the devflow: prefix. On current installs the post-install // fs.rm targets a bare path (e.g. ~/.claude/skills/dream-decisions) that does not exist From ef42cf632220c0d08881a35d12d662e29e475b46 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sun, 28 Jun 2026 14:44:01 +0300 Subject: [PATCH 5/5] fix: stop leaking brace-escapes into generated knowledge commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 over-escaped braces inside column-0 fenced code blocks in the .mds source files. MDS leaves fenced content untouched, so \{ passed through literally into 9 generated commands (269 total leaked \{ across implement 83, resolve 50, bug-analysis 33, debug 31, plan 25, code-review 23, self-review 12, explore 9, research 3). Un-escape \{ → { and \} → } inside column-0 fenced regions only. Prose escapes (outside fences) are left intact — MDS compiles those correctly. Build confirms no prose brace was wrongly un-escaped (a green build is the safety net: wrong un-escape → MDS undefined-variable error). Before: 269 leaked \{ across 9 generated commands After: 0 leaked \{ (all 9 commands clean) Tests: 1815/1815 passing --- shared/knowledge/_knowledge.mds | 24 ++--- shared/knowledge/bug-analysis.mds | 60 ++++++------- shared/knowledge/code-review.mds | 40 ++++----- shared/knowledge/debug.mds | 44 ++++----- shared/knowledge/implement.mds | 142 +++++++++++++++--------------- shared/knowledge/plan.mds | 44 ++++----- shared/knowledge/resolve.mds | 76 ++++++++-------- 7 files changed, 215 insertions(+), 215 deletions(-) diff --git a/shared/knowledge/_knowledge.mds b/shared/knowledge/_knowledge.mds index 94622b50..cb3a24a3 100644 --- a/shared/knowledge/_knowledge.mds +++ b/shared/knowledge/_knowledge.mds @@ -8,7 +8,7 @@ Resolve the worktree root using the `devflow:worktree-support` algorithm (use WO Attempt to read `\{worktree\}/.devflow/features/index.md`. Each line follows the format: ``` -- **\{slug\}** — \{areas\} — \{Use-when description\} +- **{slug}** — {areas} — {Use-when description} ``` If `index.md` exists and contains at least one entry line, use it for relevance matching. @@ -30,8 +30,8 @@ For each selected entry, read `\{worktree\}/.devflow/features/\{slug\}/KNOWLEDGE Concatenate the selected KNOWLEDGE.md files under slug headers: ``` ---- Feature knowledge: \{slug\} --- -\{full KNOWLEDGE.md content\} +--- Feature knowledge: {slug} --- +{full KNOWLEDGE.md content} ``` If no KBs exist, no KBs are relevant, or `.devflow/features/` is absent, set `FEATURE_KNOWLEDGE` to `(none)`. @@ -65,22 +65,22 @@ Only proceed if **at least one** of these is true: Spawn `Agent(subagent_type="Knowledge")` with the following context: ``` -"WORKTREE_PATH: \{worktree root\} -FEATURE_SLUG: \{slug derived from primary changed directory, kebab-case\} -FEATURE_NAME: \{human-readable name\} -DIRECTORIES: \{list of primary directories touched by this workflow\} -FILES_CHANGED: \{list of files changed\} -DECISIONS_CONTEXT: \{DECISIONS_CONTEXT if available, else (none)\} +"WORKTREE_PATH: {worktree root} +FEATURE_SLUG: {slug derived from primary changed directory, kebab-case} +FEATURE_NAME: {human-readable name} +DIRECTORIES: {list of primary directories touched by this workflow} +FILES_CHANGED: {list of files changed} +DECISIONS_CONTEXT: {DECISIONS_CONTEXT if available, else (none)} Load the devflow:feature-knowledge skill and follow its authoring process. Write the knowledge base to: - \{worktree\}/.devflow/features/\{slug\}/KNOWLEDGE.md + {worktree}/.devflow/features/{slug}/KNOWLEDGE.md Then update the index cache by performing a read-modify-write on: - \{worktree\}/.devflow/features/index.md + {worktree}/.devflow/features/index.md -Index line format: `- **\{slug\}** — \{areas\} — \{Use-when description\}` +Index line format: `- **{slug}** — {areas} — {Use-when description}` If the line for this slug already exists in index.md, replace it. If it does not exist, append it. If index.md does not exist, create it with just this line. diff --git a/shared/knowledge/bug-analysis.mds b/shared/knowledge/bug-analysis.mds index 0a60f23d..bf32517a 100644 --- a/shared/knowledge/bug-analysis.mds +++ b/shared/knowledge/bug-analysis.mds @@ -34,7 +34,7 @@ Return: branch, base_branch, branch-slug, PR#" **Fetch PR body** (after extracting `pr_number`): ```bash -PR_DESCRIPTION=$(gh pr view \{pr_number\} --json body --jq '.body' 2>/dev/null || echo "(none)") +PR_DESCRIPTION=$(gh pr view {pr_number} --json body --jq '.body' 2>/dev/null || echo "(none)") ``` If `pr_number` is absent or the command fails, set `PR_DESCRIPTION` to `(none)`. @@ -64,7 +64,7 @@ If `pr_number` is absent or the command fails, set `PR_DESCRIPTION` to `(none)`. **Requires:** DIFF_RANGE ```bash -CHANGED_FILES=$(git diff --name-only \{DIFF_RANGE\}) +CHANGED_FILES=$(git diff --name-only {DIFF_RANGE}) ``` Store result as `CHANGED_FILES` — used throughout Steps 2d and Phase 4 to avoid repeated git invocations and ensure consistency. @@ -193,19 +193,19 @@ Spawn ALL active BugAnalyzer agents **in a single message** (parallel, NOT backg For each active focus, spawn: ``` Agent(subagent_type="BugAnalyzer", run_in_background=false): -"Analyze focusing on \{focus\}. -FOCUS: \{focus\} -DIFF_COMMAND: git diff \{DIFF_RANGE\} -ACCEPTANCE_RULES: \{ACCEPTANCE_RULES filtered to this focus type, or (none)\} -PLAN_CONTEXT: \{PLAN_CONTEXT\} -STATIC_FINDINGS: \{STATIC_FINDINGS if focus == security, else (none)\} -DECISIONS_CONTEXT: \{DECISIONS_CONTEXT\} -FEATURE_KNOWLEDGE: \{FEATURE_KNOWLEDGE\} -PR_DESCRIPTION: \{PR_DESCRIPTION\} -OUTPUT_PATH: \{ANALYSIS_DIR\}/\{focus\}.md +"Analyze focusing on {focus}. +FOCUS: {focus} +DIFF_COMMAND: git diff {DIFF_RANGE} +ACCEPTANCE_RULES: {ACCEPTANCE_RULES filtered to this focus type, or (none)} +PLAN_CONTEXT: {PLAN_CONTEXT} +STATIC_FINDINGS: {STATIC_FINDINGS if focus == security, else (none)} +DECISIONS_CONTEXT: {DECISIONS_CONTEXT} +FEATURE_KNOWLEDGE: {FEATURE_KNOWLEDGE} +PR_DESCRIPTION: {PR_DESCRIPTION} +OUTPUT_PATH: {ANALYSIS_DIR}/{focus}.md Follow devflow:apply-decisions to Read full ADR/PF bodies on demand. Follow devflow:apply-feature-knowledge for FEATURE_KNOWLEDGE. -IMPORTANT: Write report to \{ANALYSIS_DIR\}/\{focus\}.md using Write tool" +IMPORTANT: Write report to {ANALYSIS_DIR}/{focus}.md using Write tool" ``` Notes: @@ -223,10 +223,10 @@ Spawn Synthesizer: ``` Agent(subagent_type="Synthesizer", run_in_background=false): "Mode: bug-analysis -ANALYSIS_BASE_DIR: \{ANALYSIS_DIR\} -BRANCH: \{branch\} -> \{base_branch\} -TIMESTAMP: \{timestamp\} -Output: \{ANALYSIS_DIR\}/bug-analysis-summary.md" +ANALYSIS_BASE_DIR: {ANALYSIS_DIR} +BRANCH: {branch} -> {base_branch} +TIMESTAMP: {timestamp} +Output: {ANALYSIS_DIR}/bug-analysis-summary.md" ``` ### Phase 7: Finalize @@ -239,30 +239,30 @@ Output: \{ANALYSIS_DIR\}/bug-analysis-summary.md" ``` ## Bug Analysis Complete -**Branch**: \{branch\} -> \{base_branch\} -**Analysis**: \{ANALYSIS_DIR\} +**Branch**: {branch} -> {base_branch} +**Analysis**: {ANALYSIS_DIR} -### Risk Assessment: \{risk_level\} +### Risk Assessment: {risk_level} -\{brief_reasoning\} +{brief_reasoning} ### Bug Counts | Category | CRITICAL | HIGH | MEDIUM | LOW | Total | |----------|----------|------|--------|-----|-------| -| Security | \{n\} | \{n\} | \{n\} | \{n\} | \{n\} | -| Functional | \{n\} | \{n\} | \{n\} | \{n\} | \{n\} | -| Integration | \{n\} | \{n\} | \{n\} | \{n\} | \{n\} | -| Usability | \{n\} | \{n\} | \{n\} | \{n\} | \{n\} | +| Security | {n} | {n} | {n} | {n} | {n} | +| Functional | {n} | {n} | {n} | {n} | {n} | +| Integration | {n} | {n} | {n} | {n} | {n} | +| Usability | {n} | {n} | {n} | {n} | {n} | ### Top Findings -\{List top 3-5 bugs by severity and confidence\} +{List top 3-5 bugs by severity and confidence} ### Artifacts -- Bug report: \{ANALYSIS_DIR\}/bug-analysis-summary.md -- Per-focus reports: \{ANALYSIS_DIR\}/\{security|functional|integration|usability\}.md -- Static findings: \{ANALYSIS_DIR\}/static-findings.md (if static analysis ran) +- Bug report: {ANALYSIS_DIR}/bug-analysis-summary.md +- Per-focus reports: {ANALYSIS_DIR}/{security|functional|integration|usability}.md +- Static findings: {ANALYSIS_DIR}/static-findings.md (if static analysis ran) -\{if any CRITICAL or HIGH bugs found:\} +{if any CRITICAL or HIGH bugs found:} Run `/resolve` to process and fix these findings. ``` diff --git a/shared/knowledge/code-review.mds b/shared/knowledge/code-review.mds index ec473d40..60f28f98 100644 --- a/shared/knowledge/code-review.mds +++ b/shared/knowledge/code-review.mds @@ -48,8 +48,8 @@ For each reviewable worktree, spawn Git agent: ``` Agent(subagent_type="Git", run_in_background=false): "OPERATION: ensure-pr-ready -WORKTREE_PATH: \{worktree_path\} (omit if cwd) -PR_DESCRIPTION_GUIDANCE: \{pr_description_guidance\} +WORKTREE_PATH: {worktree_path} (omit if cwd) +PR_DESCRIPTION_GUIDANCE: {pr_description_guidance} Validate branch, commit if needed, push, create PR if needed. Return: branch, base_branch, branch-slug, PR#" ``` @@ -62,7 +62,7 @@ In multi-worktree mode, spawn all pre-flight agents **in a single message** (par **Fetch PR body** (after extracting `pr_number`): ```bash -PR_DESCRIPTION=$(gh pr view \{pr_number\} --json body --jq '.body' 2>/dev/null || echo "(none)") +PR_DESCRIPTION=$(gh pr view {pr_number} --json body --jq '.body' 2>/dev/null || echo "(none)") ``` If `pr_number` is absent or the command fails, set `PR_DESCRIPTION` to `(none)`. @@ -196,19 +196,19 @@ Spawn Reviewer agents **in a single message**. Always run 8 core reviews; condit Each Reviewer invocation (all in one message, **NOT background**): ``` Agent(subagent_type="Reviewer", run_in_background=false): -"Review focusing on \{focus\}. Load the pattern skill for your focus from the Focus Areas table. +"Review focusing on {focus}. Load the pattern skill for your focus from the Focus Areas table. Follow 6-step process from devflow:review-methodology. -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) -DECISIONS_CONTEXT: \{decisions_context\} -FEATURE_KNOWLEDGE: \{feature_knowledge\} -PR_DESCRIPTION: \{pr_description\} -PRIOR_RESOLUTIONS: \{prior_resolutions\} +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) +DECISIONS_CONTEXT: {decisions_context} +FEATURE_KNOWLEDGE: {feature_knowledge} +PR_DESCRIPTION: {pr_description} +PRIOR_RESOLUTIONS: {prior_resolutions} If PRIOR_RESOLUTIONS is not (none), follow Cross-Cycle Awareness in reviewer.md. Follow devflow:apply-decisions to scan the index and Read full ADR/PF bodies on demand. Follow devflow:apply-feature-knowledge for FEATURE_KNOWLEDGE — feature-specific patterns and anti-patterns inform findings. -IMPORTANT: Write report to \{worktree_path\}/.devflow/docs/reviews/\{branch-slug\}/\{timestamp\}/\{focus\}.md using Write tool" +IMPORTANT: Write report to {worktree_path}/.devflow/docs/reviews/{branch-slug}/{timestamp}/{focus}.md using Write tool" ``` In multi-worktree mode, process worktrees **sequentially** (one worktree at a time). Complete Phases 1-4 for each worktree before starting the next. This prevents agent overload — spawning 8-19 reviewers per worktree across multiple worktrees simultaneously overwhelms the system. @@ -224,8 +224,8 @@ In multi-worktree mode, process worktrees **sequentially** (one worktree at a ti ``` Agent(subagent_type="Git", run_in_background=false): "OPERATION: comment-pr -WORKTREE_PATH: \{worktree_path\} (omit if cwd) -Read reviews from \{worktree_path\}/.devflow/docs/reviews/\{branch-slug\}/\{timestamp\}/ +WORKTREE_PATH: {worktree_path} (omit if cwd) +Read reviews from {worktree_path}/.devflow/docs/reviews/{branch-slug}/{timestamp}/ Create inline PR comments for findings with ≥80% confidence only. Lower-confidence suggestions (60-79%) go in the summary comment, not as inline comments. Deduplicate findings across reviewers, consolidate skipped into summary. @@ -236,14 +236,14 @@ Check for existing inline comments at same file:line before creating new ones to ``` Agent(subagent_type="Synthesizer", run_in_background=false): "Mode: review -WORKTREE_PATH: \{worktree_path\} (omit if cwd) -REVIEW_BASE_DIR: \{worktree_path\}/.devflow/docs/reviews/\{branch-slug\}/\{timestamp\} -TIMESTAMP: \{timestamp\} -CYCLE_NUMBER: \{cycle_number\} -PRIOR_RESOLUTIONS: \{prior_resolutions\} +WORKTREE_PATH: {worktree_path} (omit if cwd) +REVIEW_BASE_DIR: {worktree_path}/.devflow/docs/reviews/{branch-slug}/{timestamp} +TIMESTAMP: {timestamp} +CYCLE_NUMBER: {cycle_number} +PRIOR_RESOLUTIONS: {prior_resolutions} Include Convergence Status section in review-summary.md. Aggregate findings, determine merge recommendation -Output: \{worktree_path\}/.devflow/docs/reviews/\{branch-slug\}/\{timestamp\}/review-summary.md" +Output: {worktree_path}/.devflow/docs/reviews/{branch-slug}/{timestamp}/review-summary.md" ``` ### Phase 4: Write Review Head Marker & Report diff --git a/shared/knowledge/debug.mds b/shared/knowledge/debug.mds index e450d2fc..a83bd16d 100644 --- a/shared/knowledge/debug.mds +++ b/shared/knowledge/debug.mds @@ -50,7 +50,7 @@ If `$ARGUMENTS` starts with `#`, fetch the GitHub issue: ``` Agent(subagent_type="Git"): "OPERATION: fetch-issue -ISSUE: \{issue number\} +ISSUE: {issue number} Return issue title, body, labels, and any linked error logs." ``` @@ -68,10 +68,10 @@ Spawn one Explore agent per hypothesis in a **single message** (parallel executi ``` Agent(subagent_type="Explore"): -"Investigate this bug: \{bug_description\} +"Investigate this bug: {bug_description} -Hypothesis: \{hypothesis A description\} -Focus area: \{specific code area, mechanism, or condition\} +Hypothesis: {hypothesis A description} +Focus area: {specific code area, mechanism, or condition} Steps: 1. Read relevant code files in your focus area @@ -80,25 +80,25 @@ Steps: 4. Collect evidence AGAINST this hypothesis (with file:line references) Return a structured report: -- Hypothesis: \{restate\} +- Hypothesis: {restate} - Status: CONFIRMED / DISPROVED / PARTIAL - Evidence FOR: [list with file:line refs] - Evidence AGAINST: [list with file:line refs] -- Key finding: \{one-sentence summary\}" +- Key finding: {one-sentence summary}" Agent(subagent_type="Explore"): -"Investigate this bug: \{bug_description\} +"Investigate this bug: {bug_description} -Hypothesis: \{hypothesis B description\} -Focus area: \{specific code area, mechanism, or condition\} +Hypothesis: {hypothesis B description} +Focus area: {specific code area, mechanism, or condition} [same steps and return format]" Agent(subagent_type="Explore"): -"Investigate this bug: \{bug_description\} +"Investigate this bug: {bug_description} -Hypothesis: \{hypothesis C description\} -Focus area: \{specific code area, mechanism, or condition\} +Hypothesis: {hypothesis C description} +Focus area: {specific code area, mechanism, or condition} [same steps and return format]" @@ -127,7 +127,7 @@ Once all investigators return, spawn a Synthesizer agent to aggregate findings: Agent(subagent_type="Synthesizer"): "You are a root cause analyst. Synthesize these investigation reports: -\{paste all investigator reports\} +{paste all investigator reports} Instructions: 1. Compare evidence across all hypotheses @@ -144,28 +144,28 @@ Instructions: Produce the final report: ```markdown -## Root Cause Analysis: \{bug description\} +## Root Cause Analysis: {bug description} ### Root Cause -\{Description of the root cause supported by evidence\} -\{Key evidence with file:line references\} +{Description of the root cause supported by evidence} +{Key evidence with file:line references} ### Investigation Summary | Hypothesis | Status | Key Evidence | |-----------|--------|-------------| -| A: \{description\} | CONFIRMED/DISPROVED/PARTIAL | \{file:line + summary\} | -| B: \{description\} | CONFIRMED/DISPROVED/PARTIAL | \{file:line + summary\} | -| C: \{description\} | CONFIRMED/DISPROVED/PARTIAL | \{file:line + summary\} | +| A: {description} | CONFIRMED/DISPROVED/PARTIAL | {file:line + summary} | +| B: {description} | CONFIRMED/DISPROVED/PARTIAL | {file:line + summary} | +| C: {description} | CONFIRMED/DISPROVED/PARTIAL | {file:line + summary} | ### Key Findings -\{2-3 most important discoveries across all investigators\} +{2-3 most important discoveries across all investigators} ### Recommended Fix -\{Concrete action items with file references\} +{Concrete action items with file references} ### Confidence Level -\{HIGH/MEDIUM/LOW based on evidence strength and investigator agreement\} +{HIGH/MEDIUM/LOW based on evidence strength and investigator agreement} ``` ### Phase 7: Offer Fix diff --git a/shared/knowledge/implement.mds b/shared/knowledge/implement.mds index 0c50080a..f9c91cc1 100644 --- a/shared/knowledge/implement.mds +++ b/shared/knowledge/implement.mds @@ -53,9 +53,9 @@ Spawn Git agent to set up task environment. The Git agent derives the branch nam ``` Agent(subagent_type="Git"): "OPERATION: setup-task -BASE_BRANCH: \{current branch name\} -ISSUE_INPUT: \{issue number if $ARGUMENTS starts with #, otherwise omit\} -TASK_DESCRIPTION: \{task description from $ARGUMENTS if not an issue number or .md path, otherwise omit\} +BASE_BRANCH: {current branch name} +ISSUE_INPUT: {issue number if $ARGUMENTS starts with #, otherwise omit} +TASK_DESCRIPTION: {task description from $ARGUMENTS if not an issue number or .md path, otherwise omit} Derive branch name from issue or description, create feature branch, and fetch issue if specified. Return the branch setup summary." ``` @@ -109,16 +109,16 @@ Based on Setup context (plan document, issue body, or conversation context), use ``` Agent(subagent_type="Coder"): -"TASK_ID: \{task-id\} -TASK_DESCRIPTION: \{description\} -BASE_BRANCH: \{base branch\} -EXECUTION_PLAN: \{full plan from setup context\} -PATTERNS: \{patterns from plan document or empty\} +"TASK_ID: {task-id} +TASK_DESCRIPTION: {description} +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'\} -FEATURE_KNOWLEDGE: \{feature_knowledge\} -DECISIONS_CONTEXT: \{decisions_context\} -PR_DESCRIPTION_GUIDANCE: \{pr_description_guidance\}" +DOMAIN: {detected domain or 'fullstack'} +FEATURE_KNOWLEDGE: {feature_knowledge} +DECISIONS_CONTEXT: {decisions_context} +PR_DESCRIPTION_GUIDANCE: {pr_description_guidance}" ``` --- @@ -130,37 +130,37 @@ Spawn Coders one at a time, passing handoff summaries between phases: **Phase 1 Coder:** ``` Agent(subagent_type="Coder"): -"TASK_ID: \{task-id\} -TASK_DESCRIPTION: \{phase 1 description\} -BASE_BRANCH: \{base branch\} -EXECUTION_PLAN: \{phase 1 steps\} -PATTERNS: \{patterns from plan document or empty\} +"TASK_ID: {task-id} +TASK_DESCRIPTION: {phase 1 description} +BASE_BRANCH: {base branch} +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\} -DECISIONS_CONTEXT: \{decisions_context\} -PR_DESCRIPTION_GUIDANCE: \{pr_description_guidance\} +DOMAIN: {phase 1 domain, e.g., 'backend'} +FEATURE_KNOWLEDGE: {feature_knowledge} +DECISIONS_CONTEXT: {decisions_context} +PR_DESCRIPTION_GUIDANCE: {pr_description_guidance} HANDOFF_REQUIRED: true -HANDOFF_FILE: .devflow/docs/handoff-\{branch_slug\}.md" +HANDOFF_FILE: .devflow/docs/handoff-{branch_slug}.md" ``` **Phase 2+ Coders** (after prior phase completes): ``` Agent(subagent_type="Coder"): -"TASK_ID: \{task-id\} -TASK_DESCRIPTION: \{phase N description\} -BASE_BRANCH: \{base branch\} -EXECUTION_PLAN: \{phase N steps\} -PATTERNS: \{patterns from plan document or empty\} -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\} -DECISIONS_CONTEXT: \{decisions_context\} -PR_DESCRIPTION_GUIDANCE: \{pr_description_guidance\} -HANDOFF_REQUIRED: \{true if not last phase\} -HANDOFF_FILE: .devflow/docs/handoff-\{branch_slug\}.md" +"TASK_ID: {task-id} +TASK_DESCRIPTION: {phase N description} +BASE_BRANCH: {base branch} +EXECUTION_PLAN: {phase N steps} +PATTERNS: {patterns from plan document or empty} +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} +DECISIONS_CONTEXT: {decisions_context} +PR_DESCRIPTION_GUIDANCE: {pr_description_guidance} +HANDOFF_REQUIRED: {true if not last phase} +HANDOFF_FILE: .devflow/docs/handoff-{branch_slug}.md" ``` **Handoff Protocol**: Each sequential Coder receives the prior Coder's implementation summary via PRIOR_PHASE_SUMMARY and FILES_FROM_PRIOR_PHASE. The Coder's built-in branch orientation step handles git log scanning, file reading, and pattern discovery automatically. After each Coder with HANDOFF_REQUIRED=true completes, write its phase summary to `.devflow/docs/handoff-\{branch_slug\}.md` using the Write tool (survives context compaction). Delete `.devflow/docs/handoff-\{branch_slug\}.md` after the final Coder completes (cleanup). @@ -173,28 +173,28 @@ Spawn multiple Coders **in a single message**, each with independent subtask: ``` Agent(subagent_type="Coder"): # Coder 1 -"TASK_ID: \{task-id\}-part1 -TASK_DESCRIPTION: \{independent subtask 1\} -BASE_BRANCH: \{base branch\} -EXECUTION_PLAN: \{subtask 1 steps\} -PATTERNS: \{patterns\} +"TASK_ID: {task-id}-part1 +TASK_DESCRIPTION: {independent subtask 1} +BASE_BRANCH: {base branch} +EXECUTION_PLAN: {subtask 1 steps} +PATTERNS: {patterns} CREATE_PR: false -DOMAIN: \{subtask 1 domain\} -FEATURE_KNOWLEDGE: \{feature_knowledge\} -DECISIONS_CONTEXT: \{decisions_context\} -PR_DESCRIPTION_GUIDANCE: \{pr_description_guidance\}" +DOMAIN: {subtask 1 domain} +FEATURE_KNOWLEDGE: {feature_knowledge} +DECISIONS_CONTEXT: {decisions_context} +PR_DESCRIPTION_GUIDANCE: {pr_description_guidance}" Agent(subagent_type="Coder"): # Coder 2 (same message) -"TASK_ID: \{task-id\}-part2 -TASK_DESCRIPTION: \{independent subtask 2\} -BASE_BRANCH: \{base branch\} -EXECUTION_PLAN: \{subtask 2 steps\} -PATTERNS: \{patterns\} +"TASK_ID: {task-id}-part2 +TASK_DESCRIPTION: {independent subtask 2} +BASE_BRANCH: {base branch} +EXECUTION_PLAN: {subtask 2 steps} +PATTERNS: {patterns} CREATE_PR: false -DOMAIN: \{subtask 2 domain\} -FEATURE_KNOWLEDGE: \{feature_knowledge\} -DECISIONS_CONTEXT: \{decisions_context\} -PR_DESCRIPTION_GUIDANCE: \{pr_description_guidance\}" +DOMAIN: {subtask 2 domain} +FEATURE_KNOWLEDGE: {feature_knowledge} +DECISIONS_CONTEXT: {decisions_context} +PR_DESCRIPTION_GUIDANCE: {pr_description_guidance}" ``` **Independence criteria** (all must be true for PARALLEL_CODERS): @@ -212,7 +212,7 @@ After Coder completes, spawn Validator to verify correctness: ``` Agent(subagent_type="Validator", model="haiku"): -"FILES_CHANGED: \{list of files from Coder output\} +"FILES_CHANGED: {list of files from Coder output} VALIDATION_SCOPE: full Run build, typecheck, lint, test. Report pass/fail with failure details." ``` @@ -246,8 +246,8 @@ After validation passes, spawn Simplifier to polish the code: ``` Agent(subagent_type="Simplifier"): "Simplify recently implemented code -Task: \{task description\} -FILES_CHANGED: \{list of files from Coder output\} +Task: {task description} +FILES_CHANGED: {list of files from Coder output} Focus on code modified by Coder, apply project standards, enhance clarity" ``` @@ -260,10 +260,10 @@ 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\} -DECISIONS_CONTEXT: \{decisions_context\} -FEATURE_KNOWLEDGE: \{feature_knowledge\} +"TASK_DESCRIPTION: {task description} +FILES_CHANGED: {list of files from Coder output} +DECISIONS_CONTEXT: {decisions_context} +FEATURE_KNOWLEDGE: {feature_knowledge} Evaluate 9 pillars, fix P0/P1 issues, report status" ``` @@ -278,7 +278,7 @@ If Scrutinizer made code changes (status: FIXED), spawn Validator to verify: ``` Agent(subagent_type="Validator", model="haiku"): -"FILES_CHANGED: \{files modified by Scrutinizer\} +"FILES_CHANGED: {files modified by Scrutinizer} VALIDATION_SCOPE: changed-only Verify Scrutinizer's fixes didn't break anything." ``` @@ -296,11 +296,11 @@ After Scrutinizer passes (and re-validation if needed), spawn Evaluator to valid ``` Agent(subagent_type="Evaluator"): -"ORIGINAL_REQUEST: \{task description or issue content\} -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\} +"ORIGINAL_REQUEST: {task description or issue content} +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." ``` @@ -339,10 +339,10 @@ After Evaluator passes, spawn Tester for scenario-based acceptance testing: ``` Agent(subagent_type="Tester"): -"ORIGINAL_REQUEST: \{task description or issue content\} -EXECUTION_PLAN: \{execution plan from Phase 1\} -FILES_CHANGED: \{list of files from Coder output\} -ACCEPTANCE_CRITERIA: \{extracted criteria if available\} +"ORIGINAL_REQUEST: {task description or issue content} +EXECUTION_PLAN: {execution plan from Phase 1} +FILES_CHANGED: {list of files from Coder output} +ACCEPTANCE_CRITERIA: {extracted criteria if available} Design and execute scenario-based acceptance tests. Report PASS or FAIL with evidence." ``` diff --git a/shared/knowledge/plan.mds b/shared/knowledge/plan.mds index 688101b0..5d6f73b4 100644 --- a/shared/knowledge/plan.mds +++ b/shared/knowledge/plan.mds @@ -80,7 +80,7 @@ Spawn Skimmer agent for codebase context: ``` Agent(subagent_type="Skimmer"): -"Orient in codebase for design planning: \{feature/issues\} +"Orient in codebase for design planning: {feature/issues} Run rskim on source directories (NOT repo root) to identify: - Existing patterns and conventions in the affected area - File structure and module boundaries @@ -124,9 +124,9 @@ Spawn 4 Explore agents **in a single message**, each with Skimmer context, `DECI ``` Agent(subagent_type="Synthesizer"): -"Synthesize EXPLORATION outputs for: \{feature/issues\} +"Synthesize EXPLORATION outputs for: {feature/issues} Mode: exploration -Explorer outputs: \{all 4 outputs\} +Explorer outputs: {all 4 outputs} Combine into: user needs, similar features, constraints, failure modes" ``` @@ -166,13 +166,13 @@ Each designer receives: ``` Agent(subagent_type="Designer"): "Mode: gap-analysis -Focus: \{completeness|architecture|security|performance|consistency|dependencies\} -DECISIONS_CONTEXT: \{decisions_context\} -FEATURE_KNOWLEDGE: \{feature_knowledge\} +Focus: {completeness|architecture|security|performance|consistency|dependencies} +DECISIONS_CONTEXT: {decisions_context} +FEATURE_KNOWLEDGE: {feature_knowledge} Artifacts: - Feature/Issues: \{feature description or issue bodies\} - Exploration synthesis: \{Phase 4 output\} - Codebase context: \{Phase 2 output\} + Feature/Issues: {feature description or issue bodies} + Exploration synthesis: {Phase 4 output} + Codebase context: {Phase 2 output} Analyze only your assigned focus area. Follow devflow:apply-decisions for DECISIONS_CONTEXT. Cite evidence from provided artifacts." ``` @@ -186,9 +186,9 @@ Cite evidence from provided artifacts." ``` Agent(subagent_type="Synthesizer"): -"Synthesize GAP ANALYSIS outputs for: \{feature/issues\} +"Synthesize GAP ANALYSIS outputs for: {feature/issues} Mode: design -Designer outputs: \{all designer outputs\} +Designer outputs: {all designer outputs} Deduplicate, boost confidence for multi-agent flags, categorize by severity." ``` @@ -248,9 +248,9 @@ Spawn 4 Explore agents **in a single message**, each with Skimmer context + acce ``` Agent(subagent_type="Synthesizer"): -"Synthesize IMPLEMENTATION EXPLORATION outputs for: \{feature/issues\} +"Synthesize IMPLEMENTATION EXPLORATION outputs for: {feature/issues} Mode: exploration -Explorer outputs: \{all 4 outputs\} +Explorer outputs: {all 4 outputs} Combine into: patterns to follow, integration points, reusable code, edge cases" ``` @@ -278,9 +278,9 @@ Implementation steps planner: include explicit gap mitigations (from Phase 6) in ``` Agent(subagent_type="Synthesizer"): -"Synthesize PLANNING outputs for: \{feature/issues\} +"Synthesize PLANNING outputs for: {feature/issues} Mode: planning -Planner outputs: \{all 3 outputs\} +Planner outputs: {all 3 outputs} Combine into: execution plan with strategy decision, gap mitigations integrated" ``` @@ -299,9 +299,9 @@ Spawn 1 Designer agent with mode `design-review`: Agent(subagent_type="Designer"): "Mode: design-review Artifacts: - Implementation plan: \{Phase 11 planning synthesis\} - Implementation exploration: \{Phase 9 exploration synthesis\} - Codebase context: \{Phase 2 output\} + Implementation plan: {Phase 11 planning synthesis} + Implementation exploration: {Phase 9 exploration synthesis} + Codebase context: {Phase 2 output} Review the full plan for all 6 anti-patterns. Report all findings with evidence." ``` @@ -387,16 +387,16 @@ Required sections: ## PR Description Guidance ### Problem Being Solved -\{1-2 sentences: the "why" behind this change\} +{1-2 sentences: the "why" behind this change} ### Key Changes to Highlight -\{bulleted list: user-facing framing of what changed\} +{bulleted list: user-facing framing of what changed} ### Breaking Changes -\{from gap analysis, or "None expected"\} +{from gap analysis, or "None expected"} ### Reviewer Focus Areas -\{areas needing careful review, with reasons\} +{areas needing careful review, with reasons} ``` **Create GitHub issue (optional):** diff --git a/shared/knowledge/resolve.mds b/shared/knowledge/resolve.mds index 10c5a1dd..9f6178a4 100644 --- a/shared/knowledge/resolve.mds +++ b/shared/knowledge/resolve.mds @@ -43,7 +43,7 @@ For each resolvable worktree, spawn Git agent: ``` Agent(subagent_type="Git", run_in_background=false): "OPERATION: validate-branch -WORKTREE_PATH: \{worktree_path\} (omit if cwd) +WORKTREE_PATH: {worktree_path} (omit if cwd) Check feature branch, clean working directory, reviews exist. Return: branch, branch-slug, PR#, review count" ``` @@ -56,7 +56,7 @@ In multi-worktree mode, spawn all pre-flight agents **in a single message** (par **Fetch PR body** (after extracting `pr_number`): ```bash -PR_DESCRIPTION=$(gh pr view \{pr_number\} --json body --jq '.body' 2>/dev/null || echo "(none)") +PR_DESCRIPTION=$(gh pr view {pr_number} --json body --jq '.body' 2>/dev/null || echo "(none)") ``` If `pr_number` is absent or the command fails, set `PR_DESCRIPTION` to `(none)`. @@ -158,13 +158,13 @@ Spawn Resolver agents based on dependency analysis. For independent batches, spa ``` Agent(subagent_type="Resolver"): -"ISSUES: [\{issue1\}, \{issue2\}, ...] -BRANCH: \{branch-slug\} -BATCH_ID: batch-\{n\} -WORKTREE_PATH: \{worktree_path\} (omit if cwd) -DECISIONS_CONTEXT: \{decisions_context\} -FEATURE_KNOWLEDGE: \{feature_knowledge\} -PR_DESCRIPTION: \{pr_description\} +"ISSUES: [{issue1}, {issue2}, ...] +BRANCH: {branch-slug} +BATCH_ID: batch-{n} +WORKTREE_PATH: {worktree_path} (omit if cwd) +DECISIONS_CONTEXT: {decisions_context} +FEATURE_KNOWLEDGE: {feature_knowledge} +PR_DESCRIPTION: {pr_description} Validate, decide FIX vs TECH_DEBT, implement fixes. Follow devflow:apply-decisions to Read full ADR/PF bodies on demand. Follow devflow:apply-feature-knowledge for FEATURE_KNOWLEDGE." ``` @@ -200,8 +200,8 @@ If any fixes were made, spawn Simplifier agent to refine the changed code: ``` Agent(subagent_type="Simplifier", run_in_background=false): "TASK_DESCRIPTION: Issue resolution fixes -WORKTREE_PATH: \{worktree_path\} (omit if cwd) -FILES_CHANGED: \{list of files modified by Resolvers\} +WORKTREE_PATH: {worktree_path} (omit if cwd) +FILES_CHANGED: {list of files modified by Resolvers} Simplify and refine the fixes for clarity and consistency" ``` @@ -235,9 +235,9 @@ If any issues were deferred, spawn Git agent: ``` Agent(subagent_type="Git"): "OPERATION: manage-debt -WORKTREE_PATH: \{worktree_path\} (omit if cwd) -REVIEW_DIR: \{TARGET_DIR\} -TIMESTAMP: \{timestamp\} +WORKTREE_PATH: {worktree_path} (omit if cwd) +REVIEW_DIR: {TARGET_DIR} +TIMESTAMP: {timestamp} Note: Deferred issues from resolution are already in resolution-summary.md" ``` @@ -250,26 +250,26 @@ The resolution summary was already written to `\{TARGET_DIR\}/resolution-summary ``` ## Resolution Summary -**Branch**: \{branch\} -**Reviews Processed**: \{n\} reports from \{TARGET_DIR\} -**Total Issues**: \{n\} +**Branch**: {branch} +**Reviews Processed**: {n} reports from {TARGET_DIR} +**Total Issues**: {n} ### Results | Outcome | Count | |---------|-------| -| Fixed | \{n\} | -| False Positive | \{n\} | -| Tech Debt | \{n\} | -| Blocked | \{n\} | +| Fixed | {n} | +| False Positive | {n} | +| Tech Debt | {n} | +| Blocked | {n} | ### Commits Created -- \{sha\} \{message\} +- {sha} {message} ### Tech Debt Added -- \{n\} items added to backlog +- {n} items added to backlog ### Artifacts -- Resolution report: \{TARGET_DIR\}/resolution-summary.md +- Resolution report: {TARGET_DIR}/resolution-summary.md ``` In multi-worktree mode, report results per worktree with aggregate summary. @@ -349,44 +349,44 @@ Written in Phase 5 (Collect Results) to `\{TARGET_DIR\}/resolution-summary.md`: ```markdown # Resolution Summary -**Branch**: \{branch\} -> \{base\} -**Date**: \{timestamp\} -**Review**: \{TARGET_DIR\} +**Branch**: {branch} -> {base} +**Date**: {timestamp} +**Review**: {TARGET_DIR} **Command**: /resolve ## Decisions Citations -- applies ADR-\{NNN\} — \{batch-id\}, \{issue-id\} -- avoids PF-\{NNN\} — \{batch-id\}, \{issue-id\} +- applies ADR-{NNN} — {batch-id}, {issue-id} +- avoids PF-{NNN} — {batch-id}, {issue-id} (Omit section if no citations were made) ## Statistics | Metric | Value | |--------|-------| -| Total Issues | \{n\} | -| Fixed | \{n\} | -| False Positive | \{n\} | -| Deferred | \{n\} | -| Blocked | \{n\} | +| Total Issues | {n} | +| Fixed | {n} | +| False Positive | {n} | +| Deferred | {n} | +| Blocked | {n} | ## Fixed Issues | Issue | File:Line | Commit | |-------|-----------|--------| -| \{description\} | \{file\}:\{line\} | \{sha\} | +| {description} | {file}:{line} | {sha} | ## False Positives | Issue | File:Line | Reasoning | |-------|-----------|-----------| -| \{description\} | \{file\}:\{line\} | \{why\} | +| {description} | {file}:{line} | {why} | ## Deferred to Tech Debt | Issue | File:Line | Risk Factor | |-------|-----------|-------------| -| \{description\} | \{file\}:\{line\} | \{criteria\} | +| {description} | {file}:{line} | {criteria} | ## Blocked | Issue | File:Line | Blocker | |-------|-----------|---------| -| \{description\} | \{file\}:\{line\} | \{why\} | +| {description} | {file}:{line} | {why} | ```