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.
- package/dist/agent/compaction.d.ts +4 -0
- package/dist/agent/compaction.d.ts.map +1 -0
- package/dist/agent/compaction.js +45 -0
- package/dist/agent/compaction.js.map +1 -0
- package/dist/agent/pruning.d.ts +3 -0
- package/dist/agent/pruning.d.ts.map +1 -0
- package/dist/agent/pruning.js +19 -0
- package/dist/agent/pruning.js.map +1 -0
- package/dist/agent/retry.d.ts +27 -0
- package/dist/agent/retry.d.ts.map +1 -0
- package/dist/agent/retry.js +107 -0
- package/dist/agent/retry.js.map +1 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +27 -2
- package/dist/agent.js.map +1 -1
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +27 -1
- package/dist/api/routes.js.map +1 -1
- package/dist/bot.d.ts +4 -1
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +199 -186
- package/dist/bot.js.map +1 -1
- package/dist/cli.js +19 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +48 -3
- package/dist/commands.js.map +1 -1
- package/dist/config/json-config.js +1 -1
- package/dist/config/json-config.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -4
- package/dist/config.js.map +1 -1
- package/dist/dashboard/404/index.html +1 -1
- package/dist/dashboard/404.html +1 -1
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.txt +1 -1
- package/dist/dashboard/manual/index.html +1 -1
- package/dist/dashboard/manual/index.txt +1 -1
- package/dist/factory.d.ts +12 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +417 -28
- package/dist/factory.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -13
- package/dist/index.js.map +1 -1
- package/dist/llm/registry.d.ts.map +1 -1
- package/dist/llm/registry.js +3 -1
- package/dist/llm/registry.js.map +1 -1
- package/dist/llm.d.ts +1 -0
- package/dist/llm.d.ts.map +1 -1
- package/dist/llm.js +24 -107
- package/dist/llm.js.map +1 -1
- package/dist/paths.d.ts +1 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +3 -0
- package/dist/paths.js.map +1 -1
- package/dist/proactive/heartbeat.d.ts.map +1 -1
- package/dist/proactive/heartbeat.js +60 -14
- package/dist/proactive/heartbeat.js.map +1 -1
- package/dist/proactive/recap.d.ts.map +1 -1
- package/dist/proactive/recap.js +21 -7
- package/dist/proactive/recap.js.map +1 -1
- package/dist/proactive/recommendations.d.ts.map +1 -1
- package/dist/proactive/recommendations.js +12 -4
- package/dist/proactive/recommendations.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +96 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/workspace.d.ts +20 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +279 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +2 -1
- /package/dist/dashboard/_next/static/{uAvc69-aHCeo27Mru1M6B → MLRjFvDoyTXDkmynTEE7v}/_buildManifest.js +0 -0
- /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
|
-
|
|
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
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
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`);
|