cf-claw 3.0.5 → 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/agents.d.ts.map +1 -1
- package/dist/agents.js +18 -12
- package/dist/agents.js.map +1 -1
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +65 -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/_next/static/chunks/app/{page-dff55e58941a3c4d.js → page-f7171d9bc6d7a088.js} +1 -1
- package/dist/dashboard/_next/static/css/{baff0f221c10680b.css → 31ad9bb81d82a2d6.css} +1 -1
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.txt +2 -2
- 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 +450 -33
- 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/webchat/server.d.ts.map +1 -1
- package/dist/webchat/server.js +8 -3
- package/dist/webchat/server.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/{BW_n-kPyIVZ9Egb_9vzH3 → MLRjFvDoyTXDkmynTEE7v}/_buildManifest.js +0 -0
- /package/dist/dashboard/_next/static/{BW_n-kPyIVZ9Egb_9vzH3 → 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,6 +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";
|
|
13
|
+
import { docList, docRead } from "./documents.js";
|
|
12
14
|
const DATA_DIR = getDataDir();
|
|
13
15
|
const PROJECTS_DIR = getProjectsDir();
|
|
14
16
|
const DB_PATH = path.join(DATA_DIR, "cf-claw.db");
|
|
@@ -58,7 +60,17 @@ const hasPhaseCol = taskTableInfo.some((c) => c.name === "phase");
|
|
|
58
60
|
if (!hasPhaseCol) {
|
|
59
61
|
db.exec("ALTER TABLE factory_tasks ADD COLUMN phase TEXT DEFAULT NULL");
|
|
60
62
|
}
|
|
61
|
-
|
|
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 (?, ?, ?, ?, ?, ?)");
|
|
62
74
|
const getProjectById = db.prepare("SELECT * FROM factory_projects WHERE id = ?");
|
|
63
75
|
const getAllProjects = db.prepare("SELECT * FROM factory_projects ORDER BY updated_at DESC");
|
|
64
76
|
const updateProjectStatus = db.prepare("UPDATE factory_projects SET status = ?, updated_at = datetime('now') WHERE id = ?");
|
|
@@ -200,7 +212,23 @@ function matchesArtifact(pattern, relPath) {
|
|
|
200
212
|
}
|
|
201
213
|
return toPathRegex(normalizedPattern).test(normalizedPath);
|
|
202
214
|
}
|
|
203
|
-
function
|
|
215
|
+
function checkDocumentStoreForArtifact(pattern) {
|
|
216
|
+
const docs = docList();
|
|
217
|
+
const normalizedPattern = pattern.replace(/\\/g, "/").trim().toLowerCase();
|
|
218
|
+
for (const doc of docs) {
|
|
219
|
+
const filenameLower = doc.filename.toLowerCase();
|
|
220
|
+
const titleLower = doc.title.toLowerCase();
|
|
221
|
+
const matchesFilename = matchesArtifact(normalizedPattern, filenameLower) ||
|
|
222
|
+
filenameLower.includes(normalizedPattern.replace(/\*/g, ""));
|
|
223
|
+
const matchesTitle = matchesArtifact(normalizedPattern, titleLower) ||
|
|
224
|
+
titleLower.includes(normalizedPattern.replace(/\*/g, ""));
|
|
225
|
+
if (matchesFilename || matchesTitle) {
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
function validateWorkspaceAgainstContract(workspacePath, contract, projectTitle) {
|
|
204
232
|
if (!workspacePath || !contract) {
|
|
205
233
|
return { ok: true, violations: [] };
|
|
206
234
|
}
|
|
@@ -211,10 +239,14 @@ function validateWorkspaceAgainstContract(workspacePath, contract) {
|
|
|
211
239
|
const files = collectFiles(workspaceAbs, 1200).map((f) => path.relative(workspaceAbs, f).replace(/\\/g, "/"));
|
|
212
240
|
const violations = [];
|
|
213
241
|
for (const required of contract.requiredArtifacts) {
|
|
214
|
-
const
|
|
215
|
-
if (
|
|
216
|
-
|
|
242
|
+
const foundInFs = files.some((f) => matchesArtifact(required, f));
|
|
243
|
+
if (foundInFs)
|
|
244
|
+
continue;
|
|
245
|
+
if (checkDocumentStoreForArtifact(required)) {
|
|
246
|
+
console.log(`🏭 Artifact ${required} found in document store (not filesystem, but acceptable)`);
|
|
247
|
+
continue;
|
|
217
248
|
}
|
|
249
|
+
violations.push(`Missing required artifact pattern: ${required}`);
|
|
218
250
|
}
|
|
219
251
|
for (const forbidden of contract.forbiddenArtifacts) {
|
|
220
252
|
const matches = files.filter((f) => matchesArtifact(forbidden, f));
|
|
@@ -395,14 +427,17 @@ function rowToProject(row) {
|
|
|
395
427
|
description: row.description || "",
|
|
396
428
|
status: row.status,
|
|
397
429
|
workspacePath,
|
|
430
|
+
sourceType: row.source_type || "scratch",
|
|
431
|
+
sourceGitUrl: row.source_git_url || null,
|
|
432
|
+
sourceBranch: row.source_branch || null,
|
|
398
433
|
taskCount: counts?.total || 0,
|
|
399
434
|
completedTasks: counts?.completed || 0,
|
|
400
435
|
createdAt: row.created_at,
|
|
401
436
|
updatedAt: row.updated_at,
|
|
402
437
|
};
|
|
403
438
|
}
|
|
404
|
-
export function createFactoryProject(title, description) {
|
|
405
|
-
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);
|
|
406
441
|
const projectId = Number(result.lastInsertRowid);
|
|
407
442
|
const workspacePath = buildDefaultWorkspacePath(projectId, title);
|
|
408
443
|
ensureWorkspaceDir(workspacePath);
|
|
@@ -433,7 +468,7 @@ export function getFactoryTask(taskId) {
|
|
|
433
468
|
}
|
|
434
469
|
export function forceRetryTask(taskId) {
|
|
435
470
|
const task = getFactoryTask(taskId);
|
|
436
|
-
if (!task || task.status !== "human_intervention")
|
|
471
|
+
if (!task || (task.status !== "human_intervention" && task.status !== "failed"))
|
|
437
472
|
return false;
|
|
438
473
|
updateTaskRetry.run("backlog", 0, JSON.stringify({}), taskId);
|
|
439
474
|
return true;
|
|
@@ -597,7 +632,7 @@ function scopeToolInput(toolName, rawArgs, workspacePath) {
|
|
|
597
632
|
return rawArgs;
|
|
598
633
|
const workspaceAbs = absoluteWorkspacePath(workspacePath);
|
|
599
634
|
const scoped = { ...rawArgs };
|
|
600
|
-
const pathTools = new Set(["file_read", "file_write", "file_create", "file_delete", "file_list", "file_search"]);
|
|
635
|
+
const pathTools = new Set(["file_read", "file_write", "file_create", "file_delete", "file_list", "file_search", "doc_save"]);
|
|
601
636
|
if (pathTools.has(toolName)) {
|
|
602
637
|
const rawPath = typeof scoped.path === "string" ? scoped.path : ".";
|
|
603
638
|
const normalizedInput = normalizeAgentRelativePath(rawPath, workspacePath);
|
|
@@ -670,7 +705,7 @@ async function runFactoryAgent(agentDef, task, contextPayload, maxIterations = 1
|
|
|
670
705
|
})),
|
|
671
706
|
});
|
|
672
707
|
for (const tc of response.toolCalls) {
|
|
673
|
-
if (tc.name === "file_write" || tc.name === "file_create") {
|
|
708
|
+
if (tc.name === "file_write" || tc.name === "file_create" || tc.name === "doc_save") {
|
|
674
709
|
totalFileWriteCalls++;
|
|
675
710
|
}
|
|
676
711
|
const tool = getTool(tc.name);
|
|
@@ -741,6 +776,8 @@ Return ONLY a JSON object with:
|
|
|
741
776
|
}`;
|
|
742
777
|
const Stoffe_DECOMPOSE_PROMPT = `You are Stoffe, Systems Architect. Given Fredrix's product spec, break it into concrete technical tasks for the development team.
|
|
743
778
|
|
|
779
|
+
IMPORTANT: You MUST save the architecture blueprint to a file called \`ARCHITECTURE.md\` in the project workspace using the file_create tool. The architecture must be written to disk, not just returned as text.
|
|
780
|
+
|
|
744
781
|
Available agents:
|
|
745
782
|
{agents}
|
|
746
783
|
|
|
@@ -844,6 +881,7 @@ function normalizeStageId(input) {
|
|
|
844
881
|
security: "security-audit",
|
|
845
882
|
"final review": "final-review",
|
|
846
883
|
review: "code-review",
|
|
884
|
+
architecture: "blueprint",
|
|
847
885
|
};
|
|
848
886
|
return aliasMap[normalized] || normalized;
|
|
849
887
|
}
|
|
@@ -1265,6 +1303,351 @@ export async function factoryCreate(description) {
|
|
|
1265
1303
|
tasks: created,
|
|
1266
1304
|
};
|
|
1267
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
|
+
}
|
|
1268
1651
|
function areDependenciesMet(task, allTasks) {
|
|
1269
1652
|
if (task.dependencies.length === 0)
|
|
1270
1653
|
return true;
|
|
@@ -1273,13 +1656,41 @@ function areDependenciesMet(task, allTasks) {
|
|
|
1273
1656
|
return dep && dep.status === "done";
|
|
1274
1657
|
});
|
|
1275
1658
|
}
|
|
1276
|
-
function buildContextFromDeps(task, allTasks) {
|
|
1659
|
+
function buildContextFromDeps(task, allTasks, projectTitle) {
|
|
1277
1660
|
const context = {};
|
|
1661
|
+
const projectLower = (projectTitle || "").toLowerCase();
|
|
1278
1662
|
for (const depId of task.dependencies) {
|
|
1279
1663
|
const dep = allTasks.find((t) => t.id === depId);
|
|
1280
1664
|
if (dep && dep.result) {
|
|
1281
1665
|
context[`${dep.title} (${dep.assignedAgent})`] = dep.result;
|
|
1282
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
|
+
}
|
|
1283
1694
|
}
|
|
1284
1695
|
return context;
|
|
1285
1696
|
}
|
|
@@ -1419,7 +1830,7 @@ async function executeTask(task, allTasks) {
|
|
|
1419
1830
|
const project = getFactoryProject(task.projectId);
|
|
1420
1831
|
const workspacePath = project?.workspacePath || null;
|
|
1421
1832
|
setTaskStarted.run(task.id);
|
|
1422
|
-
const context = buildContextFromDeps(task, allTasks);
|
|
1833
|
+
const context = buildContextFromDeps(task, allTasks, project?.title);
|
|
1423
1834
|
const mergedContext = { ...task.contextPayload, ...context };
|
|
1424
1835
|
const contract = resolveTaskArchitectureContract(task, mergedContext);
|
|
1425
1836
|
if (contract) {
|
|
@@ -1441,7 +1852,11 @@ async function executeTask(task, allTasks) {
|
|
|
1441
1852
|
const agentDef = agentChoice.forceFallbackModel && chosenAgent.fallbackModel
|
|
1442
1853
|
? { ...chosenAgent, model: chosenAgent.fallbackModel, fallbackModel: undefined }
|
|
1443
1854
|
: chosenAgent;
|
|
1444
|
-
const
|
|
1855
|
+
const stageId = normalizeStageId(typeof task.contextPayload?.stage_id === "string" ? task.contextPayload.stage_id : null);
|
|
1856
|
+
const stageRequiresCodeOutput = stageId === "blueprint" || stageId === "ui-ux-design" || stageId === "implementation" || stageId === "integration";
|
|
1857
|
+
const keywordRequiresCode = /\b(implement|build|scaffold|frontend|backend|api|ui|dom|component|code|create)\b/i.test(`${task.title} ${task.description}`);
|
|
1858
|
+
const keywordExcludesCode = /\b(specification|diagram|flow|review|audit|document)\b/i.test(`${task.title} ${task.description}`);
|
|
1859
|
+
const needsCodeOutput = (stageRequiresCodeOutput || keywordRequiresCode) && !keywordExcludesCode;
|
|
1445
1860
|
const codeDeliveryGuard = needsCodeOutput
|
|
1446
1861
|
? "\n\n## Delivery requirement\nYou must produce real code changes in the project workspace using file_write and/or file_create. Descriptions without actual file operations are invalid."
|
|
1447
1862
|
: "";
|
|
@@ -1477,28 +1892,30 @@ async function executeTask(task, allTasks) {
|
|
|
1477
1892
|
}
|
|
1478
1893
|
return;
|
|
1479
1894
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
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
|
+
}
|
|
1495
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;
|
|
1496
1918
|
}
|
|
1497
|
-
else {
|
|
1498
|
-
updateTaskRetry.run("backlog", newRetryCount, JSON.stringify(newContext), task.id);
|
|
1499
|
-
console.log(`🏭 Task ${task.id} failed contract validation → retry ${newRetryCount}/${task.maxRetries}`);
|
|
1500
|
-
}
|
|
1501
|
-
return;
|
|
1502
1919
|
}
|
|
1503
1920
|
updateTaskResult.run("review", run.content, JSON.stringify(mergedContext), task.id);
|
|
1504
1921
|
console.log(`🏭 Task ${task.id} completed → review`);
|