cf-claw 3.0.6 → 3.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/agent/compaction.d.ts +4 -0
  2. package/dist/agent/compaction.d.ts.map +1 -0
  3. package/dist/agent/compaction.js +45 -0
  4. package/dist/agent/compaction.js.map +1 -0
  5. package/dist/agent/pruning.d.ts +3 -0
  6. package/dist/agent/pruning.d.ts.map +1 -0
  7. package/dist/agent/pruning.js +19 -0
  8. package/dist/agent/pruning.js.map +1 -0
  9. package/dist/agent/retry.d.ts +27 -0
  10. package/dist/agent/retry.d.ts.map +1 -0
  11. package/dist/agent/retry.js +107 -0
  12. package/dist/agent/retry.js.map +1 -0
  13. package/dist/agent.d.ts.map +1 -1
  14. package/dist/agent.js +27 -2
  15. package/dist/agent.js.map +1 -1
  16. package/dist/api/routes.d.ts.map +1 -1
  17. package/dist/api/routes.js +27 -1
  18. package/dist/api/routes.js.map +1 -1
  19. package/dist/bot.d.ts +4 -1
  20. package/dist/bot.d.ts.map +1 -1
  21. package/dist/bot.js +199 -186
  22. package/dist/bot.js.map +1 -1
  23. package/dist/cli.js +19 -8
  24. package/dist/cli.js.map +1 -1
  25. package/dist/commands.d.ts.map +1 -1
  26. package/dist/commands.js +48 -3
  27. package/dist/commands.js.map +1 -1
  28. package/dist/config/json-config.js +1 -1
  29. package/dist/config/json-config.js.map +1 -1
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +2 -4
  32. package/dist/config.js.map +1 -1
  33. package/dist/dashboard/404/index.html +1 -1
  34. package/dist/dashboard/404.html +1 -1
  35. package/dist/dashboard/index.html +1 -1
  36. package/dist/dashboard/index.txt +1 -1
  37. package/dist/dashboard/manual/index.html +1 -1
  38. package/dist/dashboard/manual/index.txt +1 -1
  39. package/dist/factory.d.ts +12 -1
  40. package/dist/factory.d.ts.map +1 -1
  41. package/dist/factory.js +417 -28
  42. package/dist/factory.js.map +1 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +24 -13
  45. package/dist/index.js.map +1 -1
  46. package/dist/llm/registry.d.ts.map +1 -1
  47. package/dist/llm/registry.js +3 -1
  48. package/dist/llm/registry.js.map +1 -1
  49. package/dist/llm.d.ts +1 -0
  50. package/dist/llm.d.ts.map +1 -1
  51. package/dist/llm.js +24 -107
  52. package/dist/llm.js.map +1 -1
  53. package/dist/paths.d.ts +1 -0
  54. package/dist/paths.d.ts.map +1 -1
  55. package/dist/paths.js +3 -0
  56. package/dist/paths.js.map +1 -1
  57. package/dist/proactive/heartbeat.d.ts.map +1 -1
  58. package/dist/proactive/heartbeat.js +60 -14
  59. package/dist/proactive/heartbeat.js.map +1 -1
  60. package/dist/proactive/recap.d.ts.map +1 -1
  61. package/dist/proactive/recap.js +21 -7
  62. package/dist/proactive/recap.js.map +1 -1
  63. package/dist/proactive/recommendations.d.ts.map +1 -1
  64. package/dist/proactive/recommendations.js +12 -4
  65. package/dist/proactive/recommendations.js.map +1 -1
  66. package/dist/tools/index.d.ts.map +1 -1
  67. package/dist/tools/index.js +96 -0
  68. package/dist/tools/index.js.map +1 -1
  69. package/dist/workspace.d.ts +20 -0
  70. package/dist/workspace.d.ts.map +1 -0
  71. package/dist/workspace.js +279 -0
  72. package/dist/workspace.js.map +1 -0
  73. package/package.json +2 -1
  74. /package/dist/dashboard/_next/static/{uAvc69-aHCeo27Mru1M6B → MLRjFvDoyTXDkmynTEE7v}/_buildManifest.js +0 -0
  75. /package/dist/dashboard/_next/static/{uAvc69-aHCeo27Mru1M6B → MLRjFvDoyTXDkmynTEE7v}/_ssgManifest.js +0 -0
package/dist/factory.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import Database from "better-sqlite3";
2
2
  import path from "node:path";
3
3
  import fs from "node:fs";
4
+ import { execSync } from "node:child_process";
4
5
  import { chatCompletionWithFallback } from "./llm/index.js";
5
6
  import { getTool, tools } from "./tools/index.js";
6
7
  import { logUsage, estimateCost } from "./usage.js";
@@ -9,7 +10,7 @@ import { getAgent, listAgents } from "./agents.js";
9
10
  import { sendToTelegram } from "./bot.js";
10
11
  import { config } from "./config.js";
11
12
  import { getDataDir, getProjectsDir } from "./paths.js";
12
- import { docList } from "./documents.js";
13
+ import { docList, docRead } from "./documents.js";
13
14
  const DATA_DIR = getDataDir();
14
15
  const PROJECTS_DIR = getProjectsDir();
15
16
  const DB_PATH = path.join(DATA_DIR, "cf-claw.db");
@@ -59,7 +60,17 @@ const hasPhaseCol = taskTableInfo.some((c) => c.name === "phase");
59
60
  if (!hasPhaseCol) {
60
61
  db.exec("ALTER TABLE factory_tasks ADD COLUMN phase TEXT DEFAULT NULL");
61
62
  }
62
- const insertProject = db.prepare("INSERT INTO factory_projects (title, description, status) VALUES (?, ?, ?)");
63
+ if (!projectTableInfo.some((c) => c.name === "source_type")) {
64
+ db.exec("ALTER TABLE factory_projects ADD COLUMN source_type TEXT DEFAULT 'scratch'");
65
+ }
66
+ if (!projectTableInfo.some((c) => c.name === "source_git_url")) {
67
+ db.exec("ALTER TABLE factory_projects ADD COLUMN source_git_url TEXT");
68
+ }
69
+ if (!projectTableInfo.some((c) => c.name === "source_branch")) {
70
+ db.exec("ALTER TABLE factory_projects ADD COLUMN source_branch TEXT");
71
+ }
72
+ const updateProjectSource = db.prepare("UPDATE factory_projects SET source_type = ?, source_git_url = ?, source_branch = ?, updated_at = datetime('now') WHERE id = ?");
73
+ const insertProject = db.prepare("INSERT INTO factory_projects (title, description, status, source_type, source_git_url, source_branch) VALUES (?, ?, ?, ?, ?, ?)");
63
74
  const getProjectById = db.prepare("SELECT * FROM factory_projects WHERE id = ?");
64
75
  const getAllProjects = db.prepare("SELECT * FROM factory_projects ORDER BY updated_at DESC");
65
76
  const updateProjectStatus = db.prepare("UPDATE factory_projects SET status = ?, updated_at = datetime('now') WHERE id = ?");
@@ -416,14 +427,17 @@ function rowToProject(row) {
416
427
  description: row.description || "",
417
428
  status: row.status,
418
429
  workspacePath,
430
+ sourceType: row.source_type || "scratch",
431
+ sourceGitUrl: row.source_git_url || null,
432
+ sourceBranch: row.source_branch || null,
419
433
  taskCount: counts?.total || 0,
420
434
  completedTasks: counts?.completed || 0,
421
435
  createdAt: row.created_at,
422
436
  updatedAt: row.updated_at,
423
437
  };
424
438
  }
425
- export function createFactoryProject(title, description) {
426
- const result = insertProject.run(title, description, "planning");
439
+ export function createFactoryProject(title, description, sourceType = "scratch", sourceGitUrl = null, sourceBranch = null) {
440
+ const result = insertProject.run(title, description, "planning", sourceType, sourceGitUrl, sourceBranch);
427
441
  const projectId = Number(result.lastInsertRowid);
428
442
  const workspacePath = buildDefaultWorkspacePath(projectId, title);
429
443
  ensureWorkspaceDir(workspacePath);
@@ -454,7 +468,7 @@ export function getFactoryTask(taskId) {
454
468
  }
455
469
  export function forceRetryTask(taskId) {
456
470
  const task = getFactoryTask(taskId);
457
- if (!task || task.status !== "human_intervention")
471
+ if (!task || (task.status !== "human_intervention" && task.status !== "failed"))
458
472
  return false;
459
473
  updateTaskRetry.run("backlog", 0, JSON.stringify({}), taskId);
460
474
  return true;
@@ -691,7 +705,7 @@ async function runFactoryAgent(agentDef, task, contextPayload, maxIterations = 1
691
705
  })),
692
706
  });
693
707
  for (const tc of response.toolCalls) {
694
- if (tc.name === "file_write" || tc.name === "file_create") {
708
+ if (tc.name === "file_write" || tc.name === "file_create" || tc.name === "doc_save") {
695
709
  totalFileWriteCalls++;
696
710
  }
697
711
  const tool = getTool(tc.name);
@@ -1289,6 +1303,351 @@ export async function factoryCreate(description) {
1289
1303
  tasks: created,
1290
1304
  };
1291
1305
  }
1306
+ function enrichDescriptionFromWorkspace(workspacePath) {
1307
+ const workspaceAbs = absoluteWorkspacePath(workspacePath);
1308
+ if (!fs.existsSync(workspaceAbs))
1309
+ return "";
1310
+ const parts = [];
1311
+ const appRoot = findLikelyAppRoot(workspaceAbs);
1312
+ const { stack, scripts } = detectTechStack(appRoot);
1313
+ if (stack.length > 0) {
1314
+ parts.push(`Detected tech stack: ${stack.join(", ")}`);
1315
+ }
1316
+ const readmePath = path.join(workspaceAbs, "README.md");
1317
+ if (fs.existsSync(readmePath)) {
1318
+ try {
1319
+ const readme = fs.readFileSync(readmePath, "utf-8").slice(0, 2000);
1320
+ parts.push(`README excerpt:\n${readme}`);
1321
+ }
1322
+ catch { }
1323
+ }
1324
+ const pkgPath = path.join(appRoot, "package.json");
1325
+ if (fs.existsSync(pkgPath)) {
1326
+ try {
1327
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
1328
+ if (pkg.name)
1329
+ parts.push(`Package name: ${pkg.name}`);
1330
+ if (pkg.description)
1331
+ parts.push(`Package description: ${pkg.description}`);
1332
+ if (pkg.scripts && Object.keys(pkg.scripts).length > 0) {
1333
+ parts.push(`Available scripts: ${Object.keys(pkg.scripts).join(", ")}`);
1334
+ }
1335
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
1336
+ const depNames = Object.keys(allDeps);
1337
+ if (depNames.length > 0 && depNames.length <= 40) {
1338
+ parts.push(`Dependencies: ${depNames.join(", ")}`);
1339
+ }
1340
+ else if (depNames.length > 40) {
1341
+ parts.push(`Dependencies: ${depNames.length} packages`);
1342
+ }
1343
+ }
1344
+ catch { }
1345
+ }
1346
+ const allFiles = collectFiles(workspaceAbs, 200);
1347
+ const topLevel = new Set();
1348
+ for (const f of allFiles) {
1349
+ const rel = path.relative(workspaceAbs, f);
1350
+ const topDir = rel.split(path.sep)[0];
1351
+ if (topDir)
1352
+ topLevel.add(topDir);
1353
+ }
1354
+ if (topLevel.size > 0) {
1355
+ parts.push(`Top-level directories/files: ${[...topLevel].sort().join(", ")}`);
1356
+ }
1357
+ parts.push(`Total files scanned: ${allFiles.length}`);
1358
+ return parts.join("\n\n");
1359
+ }
1360
+ const EXISTING_PROJECT_DECOMPOSE_PROMPT = `You are Stoffe, Systems Architect. You are analyzing an EXISTING codebase that has already been built. The user wants to make changes/additions to it.
1361
+
1362
+ IMPORTANT: You MUST save the analysis to a file called \`ARCHITECTURE.md\` in the project workspace using the file_create tool.
1363
+
1364
+ Available agents:
1365
+ {agents}
1366
+
1367
+ User's request:
1368
+ {description}
1369
+
1370
+ ## EXISTING CODEBASE CONTEXT:
1371
+ {workspace_context}
1372
+
1373
+ Available orchestration profiles:
1374
+ {profiles}
1375
+
1376
+ ## CODE AGENTS (for implementation/integration tasks):
1377
+ {code_agents}
1378
+
1379
+ ## QA AGENTS (for testing tasks):
1380
+ {qa_agents}
1381
+
1382
+ ## REVIEW AGENTS (for code review tasks):
1383
+ {review_agents}
1384
+
1385
+ ## SECURITY AGENTS (for security audit tasks):
1386
+ {security_agents}
1387
+
1388
+ MANDATORY RULES:
1389
+ - This is an EXISTING project. Tasks must MODIFY or EXTEND existing files, not create from scratch.
1390
+ - Analyze the existing codebase structure before creating tasks.
1391
+ - Each implementation task must specify which existing files need to be modified.
1392
+ - Split implementation across MULTIPLE code agents when the work has distinct areas.
1393
+ - Assign reviewer_agent to every code task using a review/QA agent.
1394
+ - Include a QA Testing task that depends on ALL code tasks.
1395
+ - Include a Code Review task that depends on the QA task.
1396
+
1397
+ Define dependencies between tasks (0-indexed task IDs).
1398
+
1399
+ Provide a Mermaid flow diagram showing the task dependencies.
1400
+
1401
+ Return ONLY JSON:
1402
+ {
1403
+ "pipeline_profile": "one of the available profiles",
1404
+ "tasks": [
1405
+ {
1406
+ "title": "Short task title",
1407
+ "description": "Detailed description including which existing files to modify and what changes to make",
1408
+ "assigned_agent": "agent_name_lowercase",
1409
+ "reviewer_agent": "agent_name_lowercase" | null,
1410
+ "dependencies": [0, 1],
1411
+ "max_retries": 10,
1412
+ "stage_id": "optional stage id",
1413
+ "architecture_contract": {
1414
+ "stack_type": "optional",
1415
+ "language": "optional",
1416
+ "required_artifacts": [],
1417
+ "forbidden_artifacts": [],
1418
+ "reviewer_focus": [],
1419
+ "notes": "optional"
1420
+ }
1421
+ }
1422
+ ]
1423
+ }`;
1424
+ async function decomposeExistingProject(description, workspaceContext) {
1425
+ const agents = listAgents();
1426
+ const agentList = agents
1427
+ .map((a) => `- ${a.name} (${a.role}, team: ${a.team})`)
1428
+ .join("\n");
1429
+ const codeAgents = agents.filter((a) => isCodeAgent(a));
1430
+ const qaAgents = agents.filter((a) => isQaAgent(a));
1431
+ const reviewAgents = agents.filter((a) => isReviewAgent(a));
1432
+ const securityAgents = agents.filter((a) => isSecurityAgent(a));
1433
+ const suggestedProfile = resolveProjectProfile(description + " " + workspaceContext, null);
1434
+ const formatAgentList = (list) => list.length > 0
1435
+ ? list.map((a) => `- ${a.name} (${a.role})`).join("\n")
1436
+ : "- (none available)";
1437
+ const prompt = EXISTING_PROJECT_DECOMPOSE_PROMPT
1438
+ .replace("{agents}", agentList)
1439
+ .replace("{description}", description)
1440
+ .replace("{workspace_context}", workspaceContext)
1441
+ .replace("{profiles}", formatProfilesForPrompt())
1442
+ .replace("{code_agents}", formatAgentList(codeAgents))
1443
+ .replace("{qa_agents}", formatAgentList(qaAgents))
1444
+ .replace("{review_agents}", formatAgentList(reviewAgents))
1445
+ .replace("{security_agents}", formatAgentList(securityAgents));
1446
+ console.log("🏭 Stoffe analyzing existing codebase...");
1447
+ const stoffeAgent = getAgent("Stoffe");
1448
+ const stoffeResponse = await chatCompletionWithFallback(stoffeAgent?.model || undefined, stoffeAgent?.fallbackModel || undefined, { messages: [{ role: "user", content: prompt }] });
1449
+ const stoffeText = stoffeResponse.content?.trim() || "[]";
1450
+ let rawTasks = [];
1451
+ let stoffePipelineProfile = null;
1452
+ const objectMatch = stoffeText.match(/\{[\s\S]*\}/);
1453
+ if (objectMatch) {
1454
+ const parsed = JSON.parse(objectMatch[0]);
1455
+ if (typeof parsed.pipeline_profile === "string") {
1456
+ stoffePipelineProfile = parsed.pipeline_profile;
1457
+ }
1458
+ if (Array.isArray(parsed.tasks)) {
1459
+ rawTasks = parsed.tasks;
1460
+ }
1461
+ }
1462
+ if (rawTasks.length === 0) {
1463
+ const arrMatch = stoffeText.match(/\[[\s\S]*\]/);
1464
+ if (!arrMatch)
1465
+ throw new Error("Stoffe failed to produce valid JSON for existing project");
1466
+ rawTasks = JSON.parse(arrMatch[0]);
1467
+ }
1468
+ const pipelineProfile = resolveProjectProfile(description, stoffePipelineProfile);
1469
+ const validAgents = new Set(agents.map((a) => a.name.toLowerCase()));
1470
+ const defaultSecurityReviewer = findAgentForStage("security-audit", agents, "sara");
1471
+ const tasks = rawTasks.map((t) => {
1472
+ const assigned = validAgents.has((t.assigned_agent || "").toLowerCase())
1473
+ ? t.assigned_agent.toLowerCase()
1474
+ : "chris";
1475
+ let reviewer = t.reviewer_agent && validAgents.has(t.reviewer_agent.toLowerCase())
1476
+ ? t.reviewer_agent.toLowerCase()
1477
+ : null;
1478
+ if (isSecuritySensitiveTask(t) && (!reviewer || reviewer !== defaultSecurityReviewer)) {
1479
+ reviewer = defaultSecurityReviewer;
1480
+ }
1481
+ return {
1482
+ title: t.title || "Untitled task",
1483
+ description: t.description || "",
1484
+ assigned_agent: assigned,
1485
+ reviewer_agent: reviewer,
1486
+ dependencies: Array.isArray(t.dependencies) ? t.dependencies : [],
1487
+ max_retries: Number.isInteger(t.max_retries)
1488
+ ? Math.max(1, Number(t.max_retries))
1489
+ : DEFAULT_MAX_RETRIES,
1490
+ stage_id: normalizeStageId(typeof t.stage_id === "string" ? t.stage_id : detectTaskStageId(t, agents)),
1491
+ architecture_contract: normalizeArchitectureContract(t.architecture_contract),
1492
+ };
1493
+ });
1494
+ return {
1495
+ title: description.substring(0, 80).replace(/\n.*/g, "").trim() || "Existing Project",
1496
+ tasks,
1497
+ pipelineProfile,
1498
+ };
1499
+ }
1500
+ export async function factoryCreateFromGit(gitUrl, branch, description) {
1501
+ const repoName = gitUrl
1502
+ .replace(/\.git$/, "")
1503
+ .split("/")
1504
+ .pop() || "git-project";
1505
+ const title = description
1506
+ ? description.substring(0, 80).replace(/\n.*/g, "").trim()
1507
+ : repoName;
1508
+ const project = createFactoryProject(title, description || `Cloned from ${gitUrl}`, "git", gitUrl, branch);
1509
+ const workspacePath = project.workspacePath;
1510
+ const workspaceAbs = absoluteWorkspacePath(workspacePath);
1511
+ if (fs.existsSync(workspaceAbs)) {
1512
+ fs.rmSync(workspaceAbs, { recursive: true, force: true });
1513
+ }
1514
+ const branchArg = branch ? ` --branch ${quoteForShell(branch)}` : "";
1515
+ const cloneCmd = `git clone --depth 1${branchArg} ${quoteForShell(gitUrl)} ${quoteForShell(workspaceAbs)}`;
1516
+ console.log(`🏭 Cloning ${gitUrl} into ${workspacePath}...`);
1517
+ try {
1518
+ execSync(cloneCmd, { stdio: "pipe", timeout: 120_000 });
1519
+ }
1520
+ catch (err) {
1521
+ const msg = err instanceof Error ? err.message : String(err);
1522
+ updateProjectStatus.run("failed", project.id);
1523
+ throw new Error(`Git clone failed: ${msg}`);
1524
+ }
1525
+ const workspaceContext = enrichDescriptionFromWorkspace(workspacePath);
1526
+ const enrichedDescription = description
1527
+ ? `${description}\n\n--- Existing Codebase ---\n${workspaceContext}`
1528
+ : `Modify/improve the existing codebase.\n\n--- Existing Codebase ---\n${workspaceContext}`;
1529
+ const { title: decomposedTitle, tasks, pipelineProfile } = await decomposeExistingProject(enrichedDescription, workspaceContext);
1530
+ if (decomposedTitle && decomposedTitle !== "Existing Project") {
1531
+ db.prepare("UPDATE factory_projects SET title = ?, updated_at = datetime('now') WHERE id = ?").run(decomposedTitle, project.id);
1532
+ }
1533
+ const allAgents = listAgents();
1534
+ const profile = resolveProjectProfile(enrichedDescription, pipelineProfile);
1535
+ const patchedTasks = injectMissingStages(tasks, profile, allAgents);
1536
+ const projectId = project.id;
1537
+ const taskMap = new Map();
1538
+ for (let i = 0; i < patchedTasks.length; i++) {
1539
+ const t = patchedTasks[i];
1540
+ const phase = AGENT_PHASES[t.assigned_agent?.toLowerCase()] || null;
1541
+ const contextPayload = {
1542
+ ...(t.architecture_contract ? { architecture_contract: t.architecture_contract } : {}),
1543
+ ...(t.stage_id ? { stage_id: t.stage_id } : {}),
1544
+ pipeline_profile: profile,
1545
+ is_existing_project: true,
1546
+ };
1547
+ const result = insertTask.run(projectId, t.title, t.description, t.assigned_agent, t.reviewer_agent || null, "[]", t.max_retries, JSON.stringify(contextPayload), phase);
1548
+ taskMap.set(i, Number(result.lastInsertRowid));
1549
+ }
1550
+ for (let i = 0; i < patchedTasks.length; i++) {
1551
+ const taskId = taskMap.get(i);
1552
+ if (!taskId)
1553
+ continue;
1554
+ const mappedDependencies = (Array.isArray(patchedTasks[i].dependencies) ? patchedTasks[i].dependencies : [])
1555
+ .map((depIdx) => {
1556
+ if (typeof depIdx !== "number" || !Number.isInteger(depIdx))
1557
+ return null;
1558
+ return taskMap.get(depIdx) ?? null;
1559
+ })
1560
+ .filter((depId) => depId !== null);
1561
+ updateTaskDependencies.run(JSON.stringify(mappedDependencies), taskId);
1562
+ }
1563
+ const created = getFactoryTasks(projectId);
1564
+ updateProjectStatus.run("running", projectId);
1565
+ const updatedRow = getProjectById.get(projectId);
1566
+ return {
1567
+ project: rowToProject(updatedRow),
1568
+ tasks: created,
1569
+ };
1570
+ }
1571
+ export async function factoryCreateFromLocal(localPath, description) {
1572
+ const absLocalPath = path.resolve(localPath);
1573
+ if (!fs.existsSync(absLocalPath)) {
1574
+ throw new Error(`Local path does not exist: ${localPath}`);
1575
+ }
1576
+ const dirName = path.basename(absLocalPath);
1577
+ const title = description
1578
+ ? description.substring(0, 80).replace(/\n.*/g, "").trim()
1579
+ : dirName;
1580
+ const project = createFactoryProject(title, description || `Imported from ${localPath}`, "local", null, null);
1581
+ const workspacePath = project.workspacePath;
1582
+ const workspaceAbs = absoluteWorkspacePath(workspacePath);
1583
+ if (fs.existsSync(workspaceAbs)) {
1584
+ fs.rmSync(workspaceAbs, { recursive: true, force: true });
1585
+ }
1586
+ console.log(`🏭 Copying ${localPath} into ${workspacePath}...`);
1587
+ copyDirFiltered(absLocalPath, workspaceAbs);
1588
+ const workspaceContext = enrichDescriptionFromWorkspace(workspacePath);
1589
+ const enrichedDescription = description
1590
+ ? `${description}\n\n--- Existing Codebase ---\n${workspaceContext}`
1591
+ : `Modify/improve the existing codebase.\n\n--- Existing Codebase ---\n${workspaceContext}`;
1592
+ const { title: decomposedTitle, tasks, pipelineProfile } = await decomposeExistingProject(enrichedDescription, workspaceContext);
1593
+ if (decomposedTitle && decomposedTitle !== "Existing Project") {
1594
+ db.prepare("UPDATE factory_projects SET title = ?, updated_at = datetime('now') WHERE id = ?").run(decomposedTitle, project.id);
1595
+ }
1596
+ const allAgents = listAgents();
1597
+ const profile = resolveProjectProfile(enrichedDescription, pipelineProfile);
1598
+ const patchedTasks = injectMissingStages(tasks, profile, allAgents);
1599
+ const projectId = project.id;
1600
+ const taskMap = new Map();
1601
+ for (let i = 0; i < patchedTasks.length; i++) {
1602
+ const t = patchedTasks[i];
1603
+ const phase = AGENT_PHASES[t.assigned_agent?.toLowerCase()] || null;
1604
+ const contextPayload = {
1605
+ ...(t.architecture_contract ? { architecture_contract: t.architecture_contract } : {}),
1606
+ ...(t.stage_id ? { stage_id: t.stage_id } : {}),
1607
+ pipeline_profile: profile,
1608
+ is_existing_project: true,
1609
+ };
1610
+ const result = insertTask.run(projectId, t.title, t.description, t.assigned_agent, t.reviewer_agent || null, "[]", t.max_retries, JSON.stringify(contextPayload), phase);
1611
+ taskMap.set(i, Number(result.lastInsertRowid));
1612
+ }
1613
+ for (let i = 0; i < patchedTasks.length; i++) {
1614
+ const taskId = taskMap.get(i);
1615
+ if (!taskId)
1616
+ continue;
1617
+ const mappedDependencies = (Array.isArray(patchedTasks[i].dependencies) ? patchedTasks[i].dependencies : [])
1618
+ .map((depIdx) => {
1619
+ if (typeof depIdx !== "number" || !Number.isInteger(depIdx))
1620
+ return null;
1621
+ return taskMap.get(depIdx) ?? null;
1622
+ })
1623
+ .filter((depId) => depId !== null);
1624
+ updateTaskDependencies.run(JSON.stringify(mappedDependencies), taskId);
1625
+ }
1626
+ const created = getFactoryTasks(projectId);
1627
+ updateProjectStatus.run("running", projectId);
1628
+ const updatedRow = getProjectById.get(projectId);
1629
+ return {
1630
+ project: rowToProject(updatedRow),
1631
+ tasks: created,
1632
+ };
1633
+ }
1634
+ function copyDirFiltered(src, dest) {
1635
+ fs.mkdirSync(dest, { recursive: true });
1636
+ const entries = fs.readdirSync(src, { withFileTypes: true });
1637
+ const skip = new Set(["node_modules", ".git", "dist", ".next", "__pycache__", ".cache", "coverage"]);
1638
+ for (const entry of entries) {
1639
+ if (skip.has(entry.name))
1640
+ continue;
1641
+ const srcPath = path.join(src, entry.name);
1642
+ const destPath = path.join(dest, entry.name);
1643
+ if (entry.isDirectory()) {
1644
+ copyDirFiltered(srcPath, destPath);
1645
+ }
1646
+ else {
1647
+ fs.copyFileSync(srcPath, destPath);
1648
+ }
1649
+ }
1650
+ }
1292
1651
  function areDependenciesMet(task, allTasks) {
1293
1652
  if (task.dependencies.length === 0)
1294
1653
  return true;
@@ -1297,13 +1656,41 @@ function areDependenciesMet(task, allTasks) {
1297
1656
  return dep && dep.status === "done";
1298
1657
  });
1299
1658
  }
1300
- function buildContextFromDeps(task, allTasks) {
1659
+ function buildContextFromDeps(task, allTasks, projectTitle) {
1301
1660
  const context = {};
1661
+ const projectLower = (projectTitle || "").toLowerCase();
1302
1662
  for (const depId of task.dependencies) {
1303
1663
  const dep = allTasks.find((t) => t.id === depId);
1304
1664
  if (dep && dep.result) {
1305
1665
  context[`${dep.title} (${dep.assignedAgent})`] = dep.result;
1306
1666
  }
1667
+ if (dep) {
1668
+ const docs = docList();
1669
+ const depTitleLower = (dep.title || "").toLowerCase();
1670
+ const relatedDocs = [];
1671
+ for (const doc of docs) {
1672
+ const docTitleLower = doc.title.toLowerCase();
1673
+ const docFilenameLower = doc.filename.toLowerCase();
1674
+ const docCategoryLower = (doc.category || "").toLowerCase();
1675
+ const docTagsLower = (doc.tags || []).join(" ").toLowerCase();
1676
+ const matchesDep = docTitleLower.includes(depTitleLower) ||
1677
+ docFilenameLower.includes(depTitleLower.split(" ").slice(0, 2).join("_")) ||
1678
+ docFilenameLower.includes(depTitleLower.split(" ").join("-"));
1679
+ const matchesProject = projectLower &&
1680
+ (docCategoryLower.includes(projectLower) ||
1681
+ docTagsLower.includes(projectLower) ||
1682
+ docFilenameLower.includes(projectLower.replace(/\s+/g, "-")));
1683
+ if (matchesDep && (matchesProject || !projectLower)) {
1684
+ const content = docRead(doc.id);
1685
+ if (content) {
1686
+ relatedDocs.push({ filename: doc.filename, content });
1687
+ }
1688
+ }
1689
+ }
1690
+ if (relatedDocs.length > 0) {
1691
+ context[`${dep.title} (${dep.assignedAgent}) - Documents`] = relatedDocs;
1692
+ }
1693
+ }
1307
1694
  }
1308
1695
  return context;
1309
1696
  }
@@ -1443,7 +1830,7 @@ async function executeTask(task, allTasks) {
1443
1830
  const project = getFactoryProject(task.projectId);
1444
1831
  const workspacePath = project?.workspacePath || null;
1445
1832
  setTaskStarted.run(task.id);
1446
- const context = buildContextFromDeps(task, allTasks);
1833
+ const context = buildContextFromDeps(task, allTasks, project?.title);
1447
1834
  const mergedContext = { ...task.contextPayload, ...context };
1448
1835
  const contract = resolveTaskArchitectureContract(task, mergedContext);
1449
1836
  if (contract) {
@@ -1505,28 +1892,30 @@ async function executeTask(task, allTasks) {
1505
1892
  }
1506
1893
  return;
1507
1894
  }
1508
- const contractValidation = validateWorkspaceAgainstContract(workspacePath, contract, project?.title);
1509
- if (!contractValidation.ok) {
1510
- const newRetryCount = task.retryCount + 1;
1511
- const problem = contractValidation.violations.join("\n");
1512
- const bounce = buildBounceFeedback("system", chosenAgent.name, "Output violates architecture contract", newRetryCount, problem, "Bring workspace artifacts in line with the architecture contract before resubmitting.");
1513
- const newContext = {
1514
- ...mergedContext,
1515
- latest_review_feedback: bounce,
1516
- [`review_feedback_attempt_${newRetryCount}`]: bounce,
1517
- };
1518
- if (newRetryCount >= task.maxRetries) {
1519
- updateTaskResult.run("human_intervention", run.content, JSON.stringify(newContext), task.id);
1520
- console.log(`🏭 Task ${task.id} hit max retries (${newRetryCount}) after contract validation failure → human_intervention`);
1521
- if (project) {
1522
- await notifyHumanIntervention(task, project);
1895
+ if (needsCodeOutput) {
1896
+ const contractValidation = validateWorkspaceAgainstContract(workspacePath, contract, project?.title);
1897
+ if (!contractValidation.ok) {
1898
+ const newRetryCount = task.retryCount + 1;
1899
+ const problem = contractValidation.violations.join("\n");
1900
+ const bounce = buildBounceFeedback("system", chosenAgent.name, "Output violates architecture contract", newRetryCount, problem, "Bring workspace artifacts in line with the architecture contract before resubmitting.");
1901
+ const newContext = {
1902
+ ...mergedContext,
1903
+ latest_review_feedback: bounce,
1904
+ [`review_feedback_attempt_${newRetryCount}`]: bounce,
1905
+ };
1906
+ if (newRetryCount >= task.maxRetries) {
1907
+ updateTaskResult.run("human_intervention", run.content, JSON.stringify(newContext), task.id);
1908
+ console.log(`🏭 Task ${task.id} hit max retries (${newRetryCount}) after contract validation failure → human_intervention`);
1909
+ if (project) {
1910
+ await notifyHumanIntervention(task, project);
1911
+ }
1523
1912
  }
1913
+ else {
1914
+ updateTaskRetry.run("backlog", newRetryCount, JSON.stringify(newContext), task.id);
1915
+ console.log(`🏭 Task ${task.id} failed contract validation → retry ${newRetryCount}/${task.maxRetries}`);
1916
+ }
1917
+ return;
1524
1918
  }
1525
- else {
1526
- updateTaskRetry.run("backlog", newRetryCount, JSON.stringify(newContext), task.id);
1527
- console.log(`🏭 Task ${task.id} failed contract validation → retry ${newRetryCount}/${task.maxRetries}`);
1528
- }
1529
- return;
1530
1919
  }
1531
1920
  updateTaskResult.run("review", run.content, JSON.stringify(mergedContext), task.id);
1532
1921
  console.log(`🏭 Task ${task.id} completed → review`);