Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/

Expand Down
24 changes: 11 additions & 13 deletions CLAUDE.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 0 additions & 1 deletion plugins/devflow-core-skills/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
"testing",
"dependency-research",
"dream-decisions",
"dream-knowledge",
"dream-curation"
],
"rules": [
Expand Down
1 change: 1 addition & 0 deletions plugins/devflow-debug/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"skills": [
"git",
"worktree-support",
"feature-knowledge",
"apply-feature-knowledge"
]
}
1 change: 1 addition & 0 deletions plugins/devflow-implement/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"qa",
"quality-gates",
"worktree-support",
"feature-knowledge",
"apply-feature-knowledge"
]
}
2 changes: 1 addition & 1 deletion plugins/devflow-release/commands/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions plugins/devflow-resolve/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"patterns",
"security",
"worktree-support",
"feature-knowledge",
"apply-feature-knowledge"
]
}
1 change: 1 addition & 0 deletions plugins/devflow-self-review/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"quality-gates",
"software-design",
"worktree-support",
"feature-knowledge",
"apply-feature-knowledge"
]
}
180 changes: 180 additions & 0 deletions scripts/build-knowledge.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
"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<CompileOutcome> {
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<void> {
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);
});
45 changes: 21 additions & 24 deletions scripts/hooks/dream-collect-tasks
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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=""

Expand Down Expand Up @@ -110,20 +110,21 @@ 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
dbg "dream_collect_tasks: deleted disabled-feature marker: $_base"
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.
Expand Down Expand Up @@ -230,27 +231,23 @@ _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).
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).\")
"
Expand Down
Loading
Loading