cf-claw 3.0.0
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.d.ts +15 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +262 -0
- package/dist/agent.js.map +1 -0
- package/dist/agents.d.ts +51 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +478 -0
- package/dist/agents.js.map +1 -0
- package/dist/api/routes.d.ts +3 -0
- package/dist/api/routes.d.ts.map +1 -0
- package/dist/api/routes.js +491 -0
- package/dist/api/routes.js.map +1 -0
- package/dist/bot.d.ts +4 -0
- package/dist/bot.d.ts.map +1 -0
- package/dist/bot.js +295 -0
- package/dist/bot.js.map +1 -0
- package/dist/canvas.d.ts +37 -0
- package/dist/canvas.d.ts.map +1 -0
- package/dist/canvas.js +47 -0
- package/dist/canvas.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +202 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +6 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +384 -0
- package/dist/commands.js.map +1 -0
- package/dist/components/TaskList.d.ts +7 -0
- package/dist/components/TaskList.d.ts.map +1 -0
- package/dist/components/TaskList.js +37 -0
- package/dist/components/TaskList.js.map +1 -0
- package/dist/config/encryption.d.ts +10 -0
- package/dist/config/encryption.d.ts.map +1 -0
- package/dist/config/encryption.js +111 -0
- package/dist/config/encryption.js.map +1 -0
- package/dist/config/json-config.d.ts +114 -0
- package/dist/config/json-config.d.ts.map +1 -0
- package/dist/config/json-config.js +388 -0
- package/dist/config/json-config.js.map +1 -0
- package/dist/config.d.ts +51 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +137 -0
- package/dist/config.js.map +1 -0
- package/dist/context/pruning.d.ts +30 -0
- package/dist/context/pruning.d.ts.map +1 -0
- package/dist/context/pruning.js +132 -0
- package/dist/context/pruning.js.map +1 -0
- package/dist/dashboard/404/index.html +1 -0
- package/dist/dashboard/404.html +1 -0
- package/dist/dashboard/_next/static/chunks/117-c657912d4a6fa056.js +2 -0
- package/dist/dashboard/_next/static/chunks/191-a6922264096cb3ad.js +11 -0
- package/dist/dashboard/_next/static/chunks/343-71498a8257bc1e81.js +1 -0
- package/dist/dashboard/_next/static/chunks/app/_not-found/page-15cbe4395e02a084.js +1 -0
- package/dist/dashboard/_next/static/chunks/app/layout-191efbc962809bb4.js +1 -0
- package/dist/dashboard/_next/static/chunks/app/manual/page-3c401ecf89979cd7.js +1 -0
- package/dist/dashboard/_next/static/chunks/app/page-dff55e58941a3c4d.js +1 -0
- package/dist/dashboard/_next/static/chunks/fd9d1056-9583fa19bc194043.js +1 -0
- package/dist/dashboard/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
- package/dist/dashboard/_next/static/chunks/main-2461f93106bcf687.js +1 -0
- package/dist/dashboard/_next/static/chunks/main-app-89f5ec28b3bb0e7f.js +1 -0
- package/dist/dashboard/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
- package/dist/dashboard/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
- package/dist/dashboard/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/dist/dashboard/_next/static/chunks/webpack-616e068a201ad621.js +1 -0
- package/dist/dashboard/_next/static/css/baff0f221c10680b.css +3 -0
- package/dist/dashboard/_next/static/pyqPyo6dkz4uTWdfdFAOJ/_buildManifest.js +1 -0
- package/dist/dashboard/_next/static/pyqPyo6dkz4uTWdfdFAOJ/_ssgManifest.js +1 -0
- package/dist/dashboard/index.html +1 -0
- package/dist/dashboard/index.txt +7 -0
- package/dist/dashboard/manual/index.html +1 -0
- package/dist/dashboard/manual/index.txt +7 -0
- package/dist/documents.d.ts +20 -0
- package/dist/documents.d.ts.map +1 -0
- package/dist/documents.js +227 -0
- package/dist/documents.js.map +1 -0
- package/dist/embeddings.d.ts +15 -0
- package/dist/embeddings.d.ts.map +1 -0
- package/dist/embeddings.js +40 -0
- package/dist/embeddings.js.map +1 -0
- package/dist/factory.d.ts +72 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +2010 -0
- package/dist/factory.js.map +1 -0
- package/dist/groups.d.ts +13 -0
- package/dist/groups.d.ts.map +1 -0
- package/dist/groups.js +42 -0
- package/dist/groups.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +223 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/taskStorage.d.ts +4 -0
- package/dist/lib/taskStorage.d.ts.map +1 -0
- package/dist/lib/taskStorage.js +28 -0
- package/dist/lib/taskStorage.js.map +1 -0
- package/dist/llm/anthropic.d.ts +13 -0
- package/dist/llm/anthropic.d.ts.map +1 -0
- package/dist/llm/anthropic.js +96 -0
- package/dist/llm/anthropic.js.map +1 -0
- package/dist/llm/failover.d.ts +13 -0
- package/dist/llm/failover.d.ts.map +1 -0
- package/dist/llm/failover.js +42 -0
- package/dist/llm/failover.js.map +1 -0
- package/dist/llm/google.d.ts +13 -0
- package/dist/llm/google.d.ts.map +1 -0
- package/dist/llm/google.js +112 -0
- package/dist/llm/google.js.map +1 -0
- package/dist/llm/groq.d.ts +8 -0
- package/dist/llm/groq.d.ts.map +1 -0
- package/dist/llm/groq.js +13 -0
- package/dist/llm/groq.js.map +1 -0
- package/dist/llm/index.d.ts +11 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +10 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/llm/ollama.d.ts +9 -0
- package/dist/llm/ollama.d.ts.map +1 -0
- package/dist/llm/ollama.js +27 -0
- package/dist/llm/ollama.js.map +1 -0
- package/dist/llm/openai-compat.d.ts +17 -0
- package/dist/llm/openai-compat.d.ts.map +1 -0
- package/dist/llm/openai-compat.js +69 -0
- package/dist/llm/openai-compat.js.map +1 -0
- package/dist/llm/openrouter.d.ts +8 -0
- package/dist/llm/openrouter.d.ts.map +1 -0
- package/dist/llm/openrouter.js +20 -0
- package/dist/llm/openrouter.js.map +1 -0
- package/dist/llm/provider.d.ts +41 -0
- package/dist/llm/provider.d.ts.map +1 -0
- package/dist/llm/provider.js +2 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/llm/registry.d.ts +10 -0
- package/dist/llm/registry.d.ts.map +1 -0
- package/dist/llm/registry.js +90 -0
- package/dist/llm/registry.js.map +1 -0
- package/dist/llm/thinking.d.ts +7 -0
- package/dist/llm/thinking.d.ts.map +1 -0
- package/dist/llm/thinking.js +34 -0
- package/dist/llm/thinking.js.map +1 -0
- package/dist/llm.d.ts +17 -0
- package/dist/llm.d.ts.map +1 -0
- package/dist/llm.js +184 -0
- package/dist/llm.js.map +1 -0
- package/dist/logs.d.ts +11 -0
- package/dist/logs.d.ts.map +1 -0
- package/dist/logs.js +54 -0
- package/dist/logs.js.map +1 -0
- package/dist/memory/knowledge-graph.d.ts +34 -0
- package/dist/memory/knowledge-graph.d.ts.map +1 -0
- package/dist/memory/knowledge-graph.js +137 -0
- package/dist/memory/knowledge-graph.js.map +1 -0
- package/dist/memory.d.ts +73 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +320 -0
- package/dist/memory.js.map +1 -0
- package/dist/mesh.d.ts +18 -0
- package/dist/mesh.d.ts.map +1 -0
- package/dist/mesh.js +120 -0
- package/dist/mesh.js.map +1 -0
- package/dist/paths.d.ts +11 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +51 -0
- package/dist/paths.js.map +1 -0
- package/dist/proactive/heartbeat.d.ts +4 -0
- package/dist/proactive/heartbeat.d.ts.map +1 -0
- package/dist/proactive/heartbeat.js +56 -0
- package/dist/proactive/heartbeat.js.map +1 -0
- package/dist/proactive/recap.d.ts +12 -0
- package/dist/proactive/recap.d.ts.map +1 -0
- package/dist/proactive/recap.js +90 -0
- package/dist/proactive/recap.js.map +1 -0
- package/dist/proactive/recommendations.d.ts +11 -0
- package/dist/proactive/recommendations.d.ts.map +1 -0
- package/dist/proactive/recommendations.js +92 -0
- package/dist/proactive/recommendations.js.map +1 -0
- package/dist/projects.d.ts +44 -0
- package/dist/projects.d.ts.map +1 -0
- package/dist/projects.js +101 -0
- package/dist/projects.js.map +1 -0
- package/dist/scheduler/index.d.ts +17 -0
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/scheduler/index.js +116 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/sessions.d.ts +19 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +176 -0
- package/dist/sessions.js.map +1 -0
- package/dist/skills/index.d.ts +14 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +126 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/swarm.d.ts +18 -0
- package/dist/swarm.d.ts.map +1 -0
- package/dist/swarm.js +146 -0
- package/dist/swarm.js.map +1 -0
- package/dist/taskListWidget.d.ts +76 -0
- package/dist/taskListWidget.d.ts.map +1 -0
- package/dist/taskListWidget.js +312 -0
- package/dist/taskListWidget.js.map +1 -0
- package/dist/tools/browser.d.ts +5 -0
- package/dist/tools/browser.d.ts.map +1 -0
- package/dist/tools/browser.js +104 -0
- package/dist/tools/browser.js.map +1 -0
- package/dist/tools/config-tools.d.ts +4 -0
- package/dist/tools/config-tools.d.ts.map +1 -0
- package/dist/tools/config-tools.js +154 -0
- package/dist/tools/config-tools.js.map +1 -0
- package/dist/tools/files.d.ts +3 -0
- package/dist/tools/files.d.ts.map +1 -0
- package/dist/tools/files.js +208 -0
- package/dist/tools/files.js.map +1 -0
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +1109 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/mcp-bridge.d.ts +5 -0
- package/dist/tools/mcp-bridge.d.ts.map +1 -0
- package/dist/tools/mcp-bridge.js +200 -0
- package/dist/tools/mcp-bridge.js.map +1 -0
- package/dist/tools/shell.d.ts +8 -0
- package/dist/tools/shell.d.ts.map +1 -0
- package/dist/tools/shell.js +78 -0
- package/dist/tools/shell.js.map +1 -0
- package/dist/tools/skills-manage.d.ts +25 -0
- package/dist/tools/skills-manage.d.ts.map +1 -0
- package/dist/tools/skills-manage.js +155 -0
- package/dist/tools/skills-manage.js.map +1 -0
- package/dist/tools/web-search.d.ts +4 -0
- package/dist/tools/web-search.d.ts.map +1 -0
- package/dist/tools/web-search.js +60 -0
- package/dist/tools/web-search.js.map +1 -0
- package/dist/tts.d.ts +6 -0
- package/dist/tts.d.ts.map +1 -0
- package/dist/tts.js +42 -0
- package/dist/tts.js.map +1 -0
- package/dist/types/task.d.ts +7 -0
- package/dist/types/task.d.ts.map +1 -0
- package/dist/types/task.js +2 -0
- package/dist/types/task.js.map +1 -0
- package/dist/typing.d.ts +18 -0
- package/dist/typing.d.ts.map +1 -0
- package/dist/typing.js +63 -0
- package/dist/typing.js.map +1 -0
- package/dist/usage.d.ts +28 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +69 -0
- package/dist/usage.js.map +1 -0
- package/dist/voice.d.ts +6 -0
- package/dist/voice.d.ts.map +1 -0
- package/dist/voice.js +47 -0
- package/dist/voice.js.map +1 -0
- package/dist/webchat/index.d.ts +3 -0
- package/dist/webchat/index.d.ts.map +1 -0
- package/dist/webchat/index.js +3 -0
- package/dist/webchat/index.js.map +1 -0
- package/dist/webchat/public/index.html +344 -0
- package/dist/webchat/public/public/index.html +344 -0
- package/dist/webchat/public/public/task-list-widget.js +410 -0
- package/dist/webchat/public/task-list-widget.js +410 -0
- package/dist/webchat/server.d.ts +3 -0
- package/dist/webchat/server.d.ts.map +1 -0
- package/dist/webchat/server.js +80 -0
- package/dist/webchat/server.js.map +1 -0
- package/dist/webchat/ws.d.ts +4 -0
- package/dist/webchat/ws.d.ts.map +1 -0
- package/dist/webchat/ws.js +232 -0
- package/dist/webchat/ws.js.map +1 -0
- package/dist/webhooks/index.d.ts +14 -0
- package/dist/webhooks/index.d.ts.map +1 -0
- package/dist/webhooks/index.js +86 -0
- package/dist/webhooks/index.js.map +1 -0
- package/package.json +53 -0
package/dist/factory.js
ADDED
|
@@ -0,0 +1,2010 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { chatCompletionWithFallback } from "./llm/index.js";
|
|
5
|
+
import { getTool, tools } from "./tools/index.js";
|
|
6
|
+
import { logUsage, estimateCost } from "./usage.js";
|
|
7
|
+
import { getActiveModel } from "./llm/index.js";
|
|
8
|
+
import { getAgent, listAgents } from "./agents.js";
|
|
9
|
+
import { sendToTelegram } from "./bot.js";
|
|
10
|
+
import { config } from "./config.js";
|
|
11
|
+
import { getDataDir, getProjectsDir } from "./paths.js";
|
|
12
|
+
const DATA_DIR = getDataDir();
|
|
13
|
+
const PROJECTS_DIR = getProjectsDir();
|
|
14
|
+
const DB_PATH = path.join(DATA_DIR, "cf-claw.db");
|
|
15
|
+
const db = new Database(DB_PATH);
|
|
16
|
+
db.pragma("journal_mode = WAL");
|
|
17
|
+
db.exec(`
|
|
18
|
+
CREATE TABLE IF NOT EXISTS factory_projects (
|
|
19
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
20
|
+
title TEXT NOT NULL,
|
|
21
|
+
description TEXT DEFAULT '',
|
|
22
|
+
status TEXT DEFAULT 'planning' CHECK(status IN ('planning','running','paused','completed','failed')),
|
|
23
|
+
workspace_path TEXT,
|
|
24
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
25
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE TABLE IF NOT EXISTS factory_tasks (
|
|
29
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
30
|
+
project_id INTEGER NOT NULL,
|
|
31
|
+
title TEXT NOT NULL,
|
|
32
|
+
description TEXT DEFAULT '',
|
|
33
|
+
assigned_agent TEXT NOT NULL,
|
|
34
|
+
reviewer_agent TEXT,
|
|
35
|
+
status TEXT DEFAULT 'backlog' CHECK(status IN ('backlog','in_progress','review','done','failed','human_intervention')),
|
|
36
|
+
dependencies TEXT DEFAULT '[]',
|
|
37
|
+
retry_count INTEGER DEFAULT 0,
|
|
38
|
+
max_retries INTEGER DEFAULT 10,
|
|
39
|
+
context_payload TEXT DEFAULT '{}',
|
|
40
|
+
result TEXT,
|
|
41
|
+
started_at TEXT,
|
|
42
|
+
completed_at TEXT,
|
|
43
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
44
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
45
|
+
FOREIGN KEY (project_id) REFERENCES factory_projects(id) ON DELETE CASCADE
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_factory_tasks_project ON factory_tasks(project_id);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_factory_tasks_status ON factory_tasks(status);
|
|
50
|
+
`);
|
|
51
|
+
const projectTableInfo = db.prepare("PRAGMA table_info(factory_projects)").all();
|
|
52
|
+
const hasWorkspaceCol = projectTableInfo.some((c) => c.name === "workspace_path");
|
|
53
|
+
if (!hasWorkspaceCol) {
|
|
54
|
+
db.exec("ALTER TABLE factory_projects ADD COLUMN workspace_path TEXT");
|
|
55
|
+
}
|
|
56
|
+
const taskTableInfo = db.prepare("PRAGMA table_info(factory_tasks)").all();
|
|
57
|
+
const hasPhaseCol = taskTableInfo.some((c) => c.name === "phase");
|
|
58
|
+
if (!hasPhaseCol) {
|
|
59
|
+
db.exec("ALTER TABLE factory_tasks ADD COLUMN phase TEXT DEFAULT NULL");
|
|
60
|
+
}
|
|
61
|
+
const insertProject = db.prepare("INSERT INTO factory_projects (title, description, status) VALUES (?, ?, ?)");
|
|
62
|
+
const getProjectById = db.prepare("SELECT * FROM factory_projects WHERE id = ?");
|
|
63
|
+
const getAllProjects = db.prepare("SELECT * FROM factory_projects ORDER BY updated_at DESC");
|
|
64
|
+
const updateProjectStatus = db.prepare("UPDATE factory_projects SET status = ?, updated_at = datetime('now') WHERE id = ?");
|
|
65
|
+
const updateProjectWorkspacePath = db.prepare("UPDATE factory_projects SET workspace_path = ?, updated_at = datetime('now') WHERE id = ?");
|
|
66
|
+
const insertTask = db.prepare("INSERT INTO factory_tasks (project_id, title, description, assigned_agent, reviewer_agent, dependencies, max_retries, context_payload, phase) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
67
|
+
const getTasksByProject = db.prepare("SELECT * FROM factory_tasks WHERE project_id = ? ORDER BY id");
|
|
68
|
+
const getTaskById = db.prepare("SELECT * FROM factory_tasks WHERE id = ?");
|
|
69
|
+
const updateTaskStatus = db.prepare("UPDATE factory_tasks SET status = ?, updated_at = datetime('now') WHERE id = ?");
|
|
70
|
+
const updateTaskResult = db.prepare("UPDATE factory_tasks SET status = ?, result = ?, context_payload = ?, completed_at = datetime('now'), updated_at = datetime('now') WHERE id = ?");
|
|
71
|
+
const updateTaskRetry = db.prepare("UPDATE factory_tasks SET status = ?, retry_count = ?, context_payload = ?, result = NULL, started_at = NULL, completed_at = NULL, updated_at = datetime('now') WHERE id = ?");
|
|
72
|
+
const updateTaskDependencies = db.prepare("UPDATE factory_tasks SET dependencies = ?, updated_at = datetime('now') WHERE id = ?");
|
|
73
|
+
const updateProjectUpdatedAt = db.prepare("UPDATE factory_projects SET updated_at = datetime('now') WHERE id = ?");
|
|
74
|
+
const updateProjectTaskMaxRetries = db.prepare("UPDATE factory_tasks SET max_retries = ?, updated_at = datetime('now') WHERE project_id = ? AND (max_retries IS NULL OR max_retries <= 0)");
|
|
75
|
+
const resetProjectTasksPartial = db.prepare("UPDATE factory_tasks SET status = 'backlog', retry_count = 0, context_payload = '{}', result = NULL, started_at = NULL, completed_at = NULL, updated_at = datetime('now') WHERE project_id = ? AND status != 'done'");
|
|
76
|
+
const resetProjectTasksFull = db.prepare("UPDATE factory_tasks SET status = 'backlog', retry_count = 0, context_payload = '{}', result = NULL, started_at = NULL, completed_at = NULL, updated_at = datetime('now') WHERE project_id = ?");
|
|
77
|
+
const setTaskStarted = db.prepare("UPDATE factory_tasks SET status = 'in_progress', started_at = datetime('now'), updated_at = datetime('now') WHERE id = ?");
|
|
78
|
+
const getBacklogTasks = db.prepare("SELECT * FROM factory_tasks WHERE status = 'backlog'");
|
|
79
|
+
const getReviewTasks = db.prepare("SELECT * FROM factory_tasks WHERE status = 'review'");
|
|
80
|
+
const getStaleInProgressTasks = db.prepare("SELECT * FROM factory_tasks WHERE status = 'in_progress' AND started_at IS NOT NULL AND started_at <= datetime('now', ?)");
|
|
81
|
+
const getTaskCounts = db.prepare("SELECT COUNT(*) as total, SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) as completed FROM factory_tasks WHERE project_id = ?");
|
|
82
|
+
const DEFAULT_MAX_RETRIES = 10;
|
|
83
|
+
const AGENT_PHASES = {
|
|
84
|
+
Fredrix: "ideation",
|
|
85
|
+
Stoffe: "blueprint",
|
|
86
|
+
ulla: "design",
|
|
87
|
+
fia: "implementation",
|
|
88
|
+
benny: "implementation",
|
|
89
|
+
ivan: "integration",
|
|
90
|
+
sara: "security",
|
|
91
|
+
tess: "qa",
|
|
92
|
+
chris: "code-review",
|
|
93
|
+
dan: "deployment",
|
|
94
|
+
stig: "release",
|
|
95
|
+
sune: "feedback",
|
|
96
|
+
};
|
|
97
|
+
const PHASE_ORDER = [
|
|
98
|
+
"ideation", "blueprint", "design", "implementation",
|
|
99
|
+
"integration", "security", "qa", "code-review",
|
|
100
|
+
"deployment", "release", "feedback",
|
|
101
|
+
];
|
|
102
|
+
export { AGENT_PHASES, PHASE_ORDER };
|
|
103
|
+
function asStringArray(input) {
|
|
104
|
+
if (!Array.isArray(input))
|
|
105
|
+
return [];
|
|
106
|
+
return input
|
|
107
|
+
.map((v) => (typeof v === "string" ? v.trim() : ""))
|
|
108
|
+
.filter((v) => v.length > 0);
|
|
109
|
+
}
|
|
110
|
+
function normalizeArchitectureContract(raw) {
|
|
111
|
+
if (!raw || typeof raw !== "object")
|
|
112
|
+
return null;
|
|
113
|
+
const obj = raw;
|
|
114
|
+
const stackType = typeof obj.stack_type === "string"
|
|
115
|
+
? obj.stack_type.trim()
|
|
116
|
+
: typeof obj.stackType === "string"
|
|
117
|
+
? obj.stackType.trim()
|
|
118
|
+
: typeof obj.framework === "string"
|
|
119
|
+
? obj.framework.trim()
|
|
120
|
+
: "";
|
|
121
|
+
const language = typeof obj.language === "string" ? obj.language.trim() : "";
|
|
122
|
+
const notes = typeof obj.notes === "string" ? obj.notes.trim() : "";
|
|
123
|
+
const requiredArtifacts = asStringArray(obj.required_artifacts ?? obj.requiredArtifacts ?? obj.required_files ?? obj.requiredFiles);
|
|
124
|
+
const forbiddenArtifacts = asStringArray(obj.forbidden_artifacts ?? obj.forbiddenArtifacts ?? obj.forbidden_files ?? obj.forbiddenFiles);
|
|
125
|
+
const reviewerFocus = asStringArray(obj.reviewer_focus ?? obj.reviewerFocus ?? obj.review_checks ?? obj.reviewChecks);
|
|
126
|
+
if (!stackType && !language && !notes && requiredArtifacts.length === 0 && forbiddenArtifacts.length === 0 && reviewerFocus.length === 0) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
stackType: stackType || null,
|
|
131
|
+
language: language || null,
|
|
132
|
+
requiredArtifacts,
|
|
133
|
+
forbiddenArtifacts,
|
|
134
|
+
reviewerFocus,
|
|
135
|
+
notes: notes || null,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function inferArchitectureContract(task) {
|
|
139
|
+
const blob = `${task.title} ${task.description}`.toLowerCase();
|
|
140
|
+
if (/\b(no framework|without framework|vanilla|static html|plain html|plain javascript)\b/.test(blob)) {
|
|
141
|
+
return {
|
|
142
|
+
stackType: "vanilla-web",
|
|
143
|
+
language: "javascript",
|
|
144
|
+
requiredArtifacts: ["index.html"],
|
|
145
|
+
forbiddenArtifacts: [
|
|
146
|
+
"src/main.tsx",
|
|
147
|
+
"src/main.jsx",
|
|
148
|
+
"vite.config.ts",
|
|
149
|
+
"vite.config.js",
|
|
150
|
+
"tailwind.config.ts",
|
|
151
|
+
"tailwind.config.js",
|
|
152
|
+
],
|
|
153
|
+
reviewerFocus: ["No framework artifacts", "Static shell requirement honored"],
|
|
154
|
+
notes: "Inferred from task wording",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (/\b(next\.js|nextjs|react|vite|tsx|spa)\b/.test(blob) || task.assignedAgent === "fia") {
|
|
158
|
+
return {
|
|
159
|
+
stackType: "react-vite",
|
|
160
|
+
language: "typescript",
|
|
161
|
+
requiredArtifacts: ["package.json", "src/**"],
|
|
162
|
+
forbiddenArtifacts: [],
|
|
163
|
+
reviewerFocus: ["Component architecture", "Client behavior aligns with task"],
|
|
164
|
+
notes: "Inferred from task wording/agent specialization",
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (/\b(c\+\+|cpp|game engine|render loop|unreal|opengl|vulkan)\b/.test(blob)) {
|
|
168
|
+
return {
|
|
169
|
+
stackType: "cpp-engine",
|
|
170
|
+
language: "c++",
|
|
171
|
+
requiredArtifacts: ["**/*.cpp", "**/*.h"],
|
|
172
|
+
forbiddenArtifacts: [],
|
|
173
|
+
reviewerFocus: ["Buildability", "Engine loop correctness"],
|
|
174
|
+
notes: "Inferred from task wording",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
function resolveTaskArchitectureContract(task, contextPayload) {
|
|
180
|
+
const fromContext = normalizeArchitectureContract(contextPayload.architecture_contract);
|
|
181
|
+
if (fromContext)
|
|
182
|
+
return fromContext;
|
|
183
|
+
return inferArchitectureContract(task);
|
|
184
|
+
}
|
|
185
|
+
function toPathRegex(pattern) {
|
|
186
|
+
const normalized = pattern.replace(/\\/g, "/");
|
|
187
|
+
let escaped = normalized.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
188
|
+
escaped = escaped.replace(/\*\*/g, "__DOUBLE_STAR__");
|
|
189
|
+
escaped = escaped.replace(/\*/g, "[^/]*");
|
|
190
|
+
escaped = escaped.replace(/__DOUBLE_STAR__/g, ".*");
|
|
191
|
+
return new RegExp(`^${escaped}$`);
|
|
192
|
+
}
|
|
193
|
+
function matchesArtifact(pattern, relPath) {
|
|
194
|
+
const normalizedPattern = pattern.replace(/\\/g, "/").trim();
|
|
195
|
+
const normalizedPath = relPath.replace(/\\/g, "/");
|
|
196
|
+
if (!normalizedPattern)
|
|
197
|
+
return false;
|
|
198
|
+
if (!normalizedPattern.includes("/") && !normalizedPattern.includes("*")) {
|
|
199
|
+
return path.basename(normalizedPath) === normalizedPattern;
|
|
200
|
+
}
|
|
201
|
+
return toPathRegex(normalizedPattern).test(normalizedPath);
|
|
202
|
+
}
|
|
203
|
+
function validateWorkspaceAgainstContract(workspacePath, contract) {
|
|
204
|
+
if (!workspacePath || !contract) {
|
|
205
|
+
return { ok: true, violations: [] };
|
|
206
|
+
}
|
|
207
|
+
const workspaceAbs = absoluteWorkspacePath(workspacePath);
|
|
208
|
+
if (!fs.existsSync(workspaceAbs)) {
|
|
209
|
+
return { ok: false, violations: [`Workspace does not exist: ${workspacePath}`] };
|
|
210
|
+
}
|
|
211
|
+
const files = collectFiles(workspaceAbs, 1200).map((f) => path.relative(workspaceAbs, f).replace(/\\/g, "/"));
|
|
212
|
+
const violations = [];
|
|
213
|
+
for (const required of contract.requiredArtifacts) {
|
|
214
|
+
const found = files.some((f) => matchesArtifact(required, f));
|
|
215
|
+
if (!found) {
|
|
216
|
+
violations.push(`Missing required artifact pattern: ${required}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
for (const forbidden of contract.forbiddenArtifacts) {
|
|
220
|
+
const matches = files.filter((f) => matchesArtifact(forbidden, f));
|
|
221
|
+
if (matches.length > 0) {
|
|
222
|
+
const sample = matches.slice(0, 3).join(", ");
|
|
223
|
+
violations.push(`Forbidden artifact present (${forbidden}): ${sample}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return { ok: violations.length === 0, violations };
|
|
227
|
+
}
|
|
228
|
+
function formatArchitectureContract(contract) {
|
|
229
|
+
const lines = [];
|
|
230
|
+
if (contract.stackType)
|
|
231
|
+
lines.push(`- Stack: ${contract.stackType}`);
|
|
232
|
+
if (contract.language)
|
|
233
|
+
lines.push(`- Language: ${contract.language}`);
|
|
234
|
+
if (contract.requiredArtifacts.length > 0) {
|
|
235
|
+
lines.push(`- Required artifacts: ${contract.requiredArtifacts.join(", ")}`);
|
|
236
|
+
}
|
|
237
|
+
if (contract.forbiddenArtifacts.length > 0) {
|
|
238
|
+
lines.push(`- Forbidden artifacts: ${contract.forbiddenArtifacts.join(", ")}`);
|
|
239
|
+
}
|
|
240
|
+
if (contract.reviewerFocus.length > 0) {
|
|
241
|
+
lines.push(`- Reviewer focus: ${contract.reviewerFocus.join(" | ")}`);
|
|
242
|
+
}
|
|
243
|
+
if (contract.notes)
|
|
244
|
+
lines.push(`- Notes: ${contract.notes}`);
|
|
245
|
+
return lines.join("\n");
|
|
246
|
+
}
|
|
247
|
+
function getAgentByNameFromList(agentName, allAgents) {
|
|
248
|
+
const normalized = agentName.toLowerCase();
|
|
249
|
+
return allAgents.find((a) => a.name.toLowerCase() === normalized) || null;
|
|
250
|
+
}
|
|
251
|
+
function hasAnyTag(agent, tags) {
|
|
252
|
+
const tagSet = new Set((agent.tags || []).map((t) => t.toLowerCase()));
|
|
253
|
+
return tags.some((tag) => tagSet.has(tag));
|
|
254
|
+
}
|
|
255
|
+
function supportsCodeTools(agent) {
|
|
256
|
+
const toolSet = new Set((agent.tools || []).map((t) => t.toLowerCase()));
|
|
257
|
+
const hasFileTool = toolSet.has("file_write") || toolSet.has("file_create");
|
|
258
|
+
return hasFileTool && toolSet.has("shell");
|
|
259
|
+
}
|
|
260
|
+
function isQaAgent(agent) {
|
|
261
|
+
const phase = AGENT_PHASES[agent.name.toLowerCase()];
|
|
262
|
+
if (phase === "qa")
|
|
263
|
+
return true;
|
|
264
|
+
if (agent.team === "quality")
|
|
265
|
+
return true;
|
|
266
|
+
if (hasAnyTag(agent, ["qa", "testing", "test", "quality"]))
|
|
267
|
+
return true;
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
function isReviewAgent(agent) {
|
|
271
|
+
const phase = AGENT_PHASES[agent.name.toLowerCase()];
|
|
272
|
+
if (phase === "code-review")
|
|
273
|
+
return true;
|
|
274
|
+
if (hasAnyTag(agent, ["code-review", "review", "reviewer"]))
|
|
275
|
+
return true;
|
|
276
|
+
if (agent.role.toLowerCase().includes("review"))
|
|
277
|
+
return true;
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
function isSecurityAgent(agent) {
|
|
281
|
+
const phase = AGENT_PHASES[agent.name.toLowerCase()];
|
|
282
|
+
if (phase === "security")
|
|
283
|
+
return true;
|
|
284
|
+
if (hasAnyTag(agent, ["security", "audit", "secops"]))
|
|
285
|
+
return true;
|
|
286
|
+
return agent.role.toLowerCase().includes("security");
|
|
287
|
+
}
|
|
288
|
+
function isCodeAgent(agent) {
|
|
289
|
+
const phase = AGENT_PHASES[agent.name.toLowerCase()];
|
|
290
|
+
if (phase === "implementation" || phase === "integration")
|
|
291
|
+
return true;
|
|
292
|
+
if (agent.team === "development")
|
|
293
|
+
return true;
|
|
294
|
+
if (hasAnyTag(agent, ["frontend", "backend", "fullstack", "api", "integration", "mobile", "cpp", "engine", "developer"])) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
return supportsCodeTools(agent);
|
|
298
|
+
}
|
|
299
|
+
export function getAgentPipelineRoles(agentName) {
|
|
300
|
+
const agent = getAgent(agentName.toLowerCase());
|
|
301
|
+
if (!agent)
|
|
302
|
+
return [];
|
|
303
|
+
const roles = [];
|
|
304
|
+
if (isCodeAgent(agent))
|
|
305
|
+
roles.push("code");
|
|
306
|
+
if (isQaAgent(agent))
|
|
307
|
+
roles.push("qa");
|
|
308
|
+
if (isReviewAgent(agent))
|
|
309
|
+
roles.push("review");
|
|
310
|
+
if (isSecurityAgent(agent))
|
|
311
|
+
roles.push("security");
|
|
312
|
+
const phase = AGENT_PHASES[agent.name.toLowerCase()];
|
|
313
|
+
if (phase && !roles.includes(phase)) {
|
|
314
|
+
roles.push(phase);
|
|
315
|
+
}
|
|
316
|
+
return roles;
|
|
317
|
+
}
|
|
318
|
+
function preferredAgentsForContract(contract, allAgents) {
|
|
319
|
+
const stack = contract?.stackType?.toLowerCase() || "";
|
|
320
|
+
const language = contract?.language?.toLowerCase() || "";
|
|
321
|
+
const candidates = allAgents.filter((agent) => {
|
|
322
|
+
if (!isCodeAgent(agent))
|
|
323
|
+
return false;
|
|
324
|
+
const blob = `${agent.role} ${agent.description} ${(agent.tags || []).join(" ")}`.toLowerCase();
|
|
325
|
+
if (!stack && !language)
|
|
326
|
+
return true;
|
|
327
|
+
if (stack && (blob.includes(stack) || stack.split(/[^a-z0-9+.-]+/).some((token) => token.length > 2 && blob.includes(token)))) {
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
if (language && blob.includes(language))
|
|
331
|
+
return true;
|
|
332
|
+
return false;
|
|
333
|
+
});
|
|
334
|
+
if (candidates.length > 0) {
|
|
335
|
+
return candidates.map((a) => a.name.toLowerCase());
|
|
336
|
+
}
|
|
337
|
+
const genericCodeAgents = allAgents.filter((a) => isCodeAgent(a)).map((a) => a.name.toLowerCase());
|
|
338
|
+
if (genericCodeAgents.length > 0)
|
|
339
|
+
return genericCodeAgents;
|
|
340
|
+
return ["chris"];
|
|
341
|
+
}
|
|
342
|
+
function rowToTask(row) {
|
|
343
|
+
return {
|
|
344
|
+
id: row.id,
|
|
345
|
+
projectId: row.project_id,
|
|
346
|
+
title: row.title,
|
|
347
|
+
description: row.description || "",
|
|
348
|
+
assignedAgent: row.assigned_agent,
|
|
349
|
+
reviewerAgent: row.reviewer_agent || null,
|
|
350
|
+
status: row.status,
|
|
351
|
+
phase: row.phase || AGENT_PHASES[row.assigned_agent?.toLowerCase()] || null,
|
|
352
|
+
dependencies: JSON.parse(row.dependencies || "[]"),
|
|
353
|
+
retryCount: row.retry_count || 0,
|
|
354
|
+
maxRetries: row.max_retries || DEFAULT_MAX_RETRIES,
|
|
355
|
+
contextPayload: JSON.parse(row.context_payload || "{}"),
|
|
356
|
+
result: row.result || null,
|
|
357
|
+
startedAt: row.started_at || null,
|
|
358
|
+
completedAt: row.completed_at || null,
|
|
359
|
+
createdAt: row.created_at,
|
|
360
|
+
updatedAt: row.updated_at,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function slugifyProjectTitle(input) {
|
|
364
|
+
const slug = input
|
|
365
|
+
.toLowerCase()
|
|
366
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
367
|
+
.replace(/^-+|-+$/g, "")
|
|
368
|
+
.slice(0, 48);
|
|
369
|
+
return slug || "project";
|
|
370
|
+
}
|
|
371
|
+
function buildDefaultWorkspacePath(projectId, title) {
|
|
372
|
+
return path.join("projects", `${slugifyProjectTitle(title)}-${projectId}`);
|
|
373
|
+
}
|
|
374
|
+
function absoluteWorkspacePath(workspacePath) {
|
|
375
|
+
return path.resolve(workspacePath);
|
|
376
|
+
}
|
|
377
|
+
function ensureWorkspaceDir(workspacePath) {
|
|
378
|
+
const abs = absoluteWorkspacePath(workspacePath);
|
|
379
|
+
fs.mkdirSync(abs, { recursive: true });
|
|
380
|
+
}
|
|
381
|
+
function ensureProjectWorkspace(row) {
|
|
382
|
+
const workspacePath = row.workspace_path || buildDefaultWorkspacePath(row.id, row.title || "project");
|
|
383
|
+
if (!row.workspace_path) {
|
|
384
|
+
updateProjectWorkspacePath.run(workspacePath, row.id);
|
|
385
|
+
}
|
|
386
|
+
ensureWorkspaceDir(workspacePath);
|
|
387
|
+
return workspacePath;
|
|
388
|
+
}
|
|
389
|
+
function rowToProject(row) {
|
|
390
|
+
const counts = getTaskCounts.get(row.id);
|
|
391
|
+
const workspacePath = ensureProjectWorkspace(row);
|
|
392
|
+
return {
|
|
393
|
+
id: row.id,
|
|
394
|
+
title: row.title,
|
|
395
|
+
description: row.description || "",
|
|
396
|
+
status: row.status,
|
|
397
|
+
workspacePath,
|
|
398
|
+
taskCount: counts?.total || 0,
|
|
399
|
+
completedTasks: counts?.completed || 0,
|
|
400
|
+
createdAt: row.created_at,
|
|
401
|
+
updatedAt: row.updated_at,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
export function createFactoryProject(title, description) {
|
|
405
|
+
const result = insertProject.run(title, description, "planning");
|
|
406
|
+
const projectId = Number(result.lastInsertRowid);
|
|
407
|
+
const workspacePath = buildDefaultWorkspacePath(projectId, title);
|
|
408
|
+
ensureWorkspaceDir(workspacePath);
|
|
409
|
+
updateProjectWorkspacePath.run(workspacePath, projectId);
|
|
410
|
+
const row = getProjectById.get(projectId);
|
|
411
|
+
return rowToProject(row);
|
|
412
|
+
}
|
|
413
|
+
export function listFactoryProjects() {
|
|
414
|
+
return getAllProjects.all().map(rowToProject);
|
|
415
|
+
}
|
|
416
|
+
export function getFactoryProject(id) {
|
|
417
|
+
const row = getProjectById.get(id);
|
|
418
|
+
if (!row)
|
|
419
|
+
return null;
|
|
420
|
+
return rowToProject(row);
|
|
421
|
+
}
|
|
422
|
+
export function setProjectStatus(id, status) {
|
|
423
|
+
return updateProjectStatus.run(status, id).changes > 0;
|
|
424
|
+
}
|
|
425
|
+
export function getFactoryTasks(projectId) {
|
|
426
|
+
return getTasksByProject.all(projectId).map(rowToTask);
|
|
427
|
+
}
|
|
428
|
+
export function getFactoryTask(taskId) {
|
|
429
|
+
const row = getTaskById.get(taskId);
|
|
430
|
+
if (!row)
|
|
431
|
+
return null;
|
|
432
|
+
return rowToTask(row);
|
|
433
|
+
}
|
|
434
|
+
export function forceRetryTask(taskId) {
|
|
435
|
+
const task = getFactoryTask(taskId);
|
|
436
|
+
if (!task || task.status !== "human_intervention")
|
|
437
|
+
return false;
|
|
438
|
+
updateTaskRetry.run("backlog", 0, JSON.stringify({}), taskId);
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
export function switchProjectProfile(projectId, newProfile) {
|
|
442
|
+
const project = getFactoryProject(projectId);
|
|
443
|
+
if (!project)
|
|
444
|
+
return { ok: false, message: `Project ${projectId} not found.`, injectedTasks: 0 };
|
|
445
|
+
const profiles = config.factoryOrchestration.profiles;
|
|
446
|
+
const profile = profiles[newProfile];
|
|
447
|
+
if (!profile) {
|
|
448
|
+
const available = Object.keys(profiles).join(", ");
|
|
449
|
+
return { ok: false, message: `Unknown profile "${newProfile}". Available: ${available}`, injectedTasks: 0 };
|
|
450
|
+
}
|
|
451
|
+
const allAgents = listAgents();
|
|
452
|
+
const tasks = getFactoryTasks(projectId);
|
|
453
|
+
const existingStages = new Set(tasks.map((t) => {
|
|
454
|
+
const stageId = normalizeStageId(typeof t.contextPayload?.stage_id === "string" ? t.contextPayload.stage_id : null);
|
|
455
|
+
return stageId;
|
|
456
|
+
}));
|
|
457
|
+
const mandatoryStages = (profile.mandatoryStages || []).map((s) => normalizeStageId(s)).filter((s) => !!s);
|
|
458
|
+
const stageOrder = Object.fromEntries(Object.entries(config.factoryOrchestration.stageCatalog).map(([id, def]) => [id, def.order || 999]));
|
|
459
|
+
mandatoryStages.sort((a, b) => (stageOrder[a] || 999) - (stageOrder[b] || 999));
|
|
460
|
+
let injectedCount = 0;
|
|
461
|
+
for (const stageId of mandatoryStages) {
|
|
462
|
+
if (existingStages.has(stageId))
|
|
463
|
+
continue;
|
|
464
|
+
const stagePolicy = config.factoryOrchestration.stageCatalog[stageId];
|
|
465
|
+
const stageMaxRetries = stagePolicy?.retryPolicy?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
466
|
+
const fallbackAgent = stageId === "qa-testing" ? "tess" :
|
|
467
|
+
stageId === "code-review" ? "chris" :
|
|
468
|
+
stageId === "security-audit" ? "sara" :
|
|
469
|
+
stageId === "ideation" || stageId === "final-review" ? "Fredrix" :
|
|
470
|
+
stageId === "blueprint" ? "Stoffe" :
|
|
471
|
+
stageId === "ui-ux-design" ? "ulla" :
|
|
472
|
+
stageId === "implementation" ? "chris" :
|
|
473
|
+
stageId === "integration" ? "ivan" :
|
|
474
|
+
stageId === "deployment" ? "dan" :
|
|
475
|
+
stageId === "release" ? "stig" :
|
|
476
|
+
stageId === "feedback" ? "sune" : "chris";
|
|
477
|
+
const selectedAgent = findAgentForStage(stageId, allAgents, fallbackAgent);
|
|
478
|
+
const deps = tasks
|
|
479
|
+
.filter((t) => {
|
|
480
|
+
const tid = normalizeStageId(typeof t.contextPayload?.stage_id === "string" ? t.contextPayload.stage_id : null);
|
|
481
|
+
const tidOrder = stageOrder[tid || ""] || 999;
|
|
482
|
+
return tidOrder < (stageOrder[stageId] || 999);
|
|
483
|
+
})
|
|
484
|
+
.map((t) => t.id);
|
|
485
|
+
const contextPayload = {
|
|
486
|
+
stage_id: stageId,
|
|
487
|
+
pipeline_profile: newProfile,
|
|
488
|
+
};
|
|
489
|
+
insertTask.run(projectId, `${stageId} (injected)`, `Stage task for "${stageId}" injected when switching profile to "${newProfile}".`, selectedAgent, null, JSON.stringify(deps), Math.max(1, stageMaxRetries), JSON.stringify(contextPayload), AGENT_PHASES[selectedAgent] || null);
|
|
490
|
+
injectedCount++;
|
|
491
|
+
console.log(`🏭 Profile switch: injected stage ${stageId} → ${selectedAgent} for project ${projectId}`);
|
|
492
|
+
}
|
|
493
|
+
updateProjectUpdatedAt.run(projectId);
|
|
494
|
+
return { ok: true, message: `Switched to profile "${newProfile}". Injected ${injectedCount} missing stage tasks.`, injectedTasks: injectedCount };
|
|
495
|
+
}
|
|
496
|
+
export function listFactoryProfiles() {
|
|
497
|
+
return Object.entries(config.factoryOrchestration.profiles).map(([id, def]) => ({
|
|
498
|
+
id,
|
|
499
|
+
mandatoryStages: def.mandatoryStages,
|
|
500
|
+
optionalStages: def.optionalStages,
|
|
501
|
+
}));
|
|
502
|
+
}
|
|
503
|
+
export function restartFactoryProject(projectId, mode = "partial") {
|
|
504
|
+
const row = getProjectById.get(projectId);
|
|
505
|
+
if (!row) {
|
|
506
|
+
return {
|
|
507
|
+
ok: false,
|
|
508
|
+
projectId,
|
|
509
|
+
mode,
|
|
510
|
+
resetTasks: 0,
|
|
511
|
+
workspacePath: "",
|
|
512
|
+
message: `Project ${projectId} not found.`,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
const project = rowToProject(row);
|
|
516
|
+
const workspaceAbs = absoluteWorkspacePath(project.workspacePath);
|
|
517
|
+
const projectsRootAbs = path.resolve(PROJECTS_DIR);
|
|
518
|
+
if (mode === "full") {
|
|
519
|
+
if (workspaceAbs === projectsRootAbs || !workspaceAbs.startsWith(projectsRootAbs + path.sep)) {
|
|
520
|
+
return {
|
|
521
|
+
ok: false,
|
|
522
|
+
projectId,
|
|
523
|
+
mode,
|
|
524
|
+
resetTasks: 0,
|
|
525
|
+
workspacePath: project.workspacePath,
|
|
526
|
+
message: `Refusing to delete unsafe workspace path: ${project.workspacePath}`,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
if (fs.existsSync(workspaceAbs)) {
|
|
530
|
+
fs.rmSync(workspaceAbs, { recursive: true, force: true });
|
|
531
|
+
}
|
|
532
|
+
fs.mkdirSync(workspaceAbs, { recursive: true });
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
ensureWorkspaceDir(project.workspacePath);
|
|
536
|
+
}
|
|
537
|
+
setProjectStatus(projectId, "paused");
|
|
538
|
+
const resetResult = mode === "full"
|
|
539
|
+
? resetProjectTasksFull.run(projectId)
|
|
540
|
+
: resetProjectTasksPartial.run(projectId);
|
|
541
|
+
setProjectStatus(projectId, "running");
|
|
542
|
+
return {
|
|
543
|
+
ok: true,
|
|
544
|
+
projectId,
|
|
545
|
+
mode,
|
|
546
|
+
resetTasks: resetResult.changes,
|
|
547
|
+
workspacePath: project.workspacePath,
|
|
548
|
+
message: mode === "full"
|
|
549
|
+
? `Project ${projectId} fully restarted. Workspace reset at ${project.workspacePath}.`
|
|
550
|
+
: `Project ${projectId} restarted. Non-done tasks moved back to backlog.`,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
function toolsToDefinitions(toolNames) {
|
|
554
|
+
return toolNames
|
|
555
|
+
.map((name) => {
|
|
556
|
+
const t = tools.find((tool) => tool.name === name);
|
|
557
|
+
if (!t)
|
|
558
|
+
return null;
|
|
559
|
+
return { name: t.name, description: t.description, parameters: t.input_schema };
|
|
560
|
+
})
|
|
561
|
+
.filter((t) => t !== null);
|
|
562
|
+
}
|
|
563
|
+
function isWithinDir(targetPath, rootPath) {
|
|
564
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
565
|
+
const resolvedRoot = path.resolve(rootPath);
|
|
566
|
+
return resolvedTarget === resolvedRoot || resolvedTarget.startsWith(resolvedRoot + path.sep);
|
|
567
|
+
}
|
|
568
|
+
function resolveWorkspacePath(inputPath, workspaceAbs) {
|
|
569
|
+
const maybeAbsolute = path.isAbsolute(inputPath)
|
|
570
|
+
? path.resolve(inputPath)
|
|
571
|
+
: path.resolve(workspaceAbs, inputPath);
|
|
572
|
+
if (!isWithinDir(maybeAbsolute, workspaceAbs)) {
|
|
573
|
+
throw new Error(`Path escapes project workspace: ${inputPath}`);
|
|
574
|
+
}
|
|
575
|
+
return maybeAbsolute;
|
|
576
|
+
}
|
|
577
|
+
function quoteForShell(value) {
|
|
578
|
+
return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
|
|
579
|
+
}
|
|
580
|
+
function normalizeAgentRelativePath(inputPath, workspacePath) {
|
|
581
|
+
if (path.isAbsolute(inputPath))
|
|
582
|
+
return inputPath;
|
|
583
|
+
const normalizedInput = inputPath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
584
|
+
const normalizedWorkspace = workspacePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
585
|
+
if (normalizedInput === normalizedWorkspace)
|
|
586
|
+
return ".";
|
|
587
|
+
if (normalizedInput.startsWith(normalizedWorkspace + "/")) {
|
|
588
|
+
return normalizedInput.slice(normalizedWorkspace.length + 1);
|
|
589
|
+
}
|
|
590
|
+
if (normalizedInput.startsWith("projects/")) {
|
|
591
|
+
throw new Error("Use workspace-relative paths only (do not prefix with projects/...).");
|
|
592
|
+
}
|
|
593
|
+
return inputPath;
|
|
594
|
+
}
|
|
595
|
+
function scopeToolInput(toolName, rawArgs, workspacePath) {
|
|
596
|
+
if (!workspacePath)
|
|
597
|
+
return rawArgs;
|
|
598
|
+
const workspaceAbs = absoluteWorkspacePath(workspacePath);
|
|
599
|
+
const scoped = { ...rawArgs };
|
|
600
|
+
const pathTools = new Set(["file_read", "file_write", "file_create", "file_delete", "file_list", "file_search"]);
|
|
601
|
+
if (pathTools.has(toolName)) {
|
|
602
|
+
const rawPath = typeof scoped.path === "string" ? scoped.path : ".";
|
|
603
|
+
const normalizedInput = normalizeAgentRelativePath(rawPath, workspacePath);
|
|
604
|
+
scoped.path = resolveWorkspacePath(normalizedInput, workspaceAbs);
|
|
605
|
+
return scoped;
|
|
606
|
+
}
|
|
607
|
+
if (toolName === "shell_exec" && typeof scoped.command === "string") {
|
|
608
|
+
const workspaceQuoted = quoteForShell(workspaceAbs);
|
|
609
|
+
scoped.command = `cd ${workspaceQuoted} && ${scoped.command}`;
|
|
610
|
+
return scoped;
|
|
611
|
+
}
|
|
612
|
+
return scoped;
|
|
613
|
+
}
|
|
614
|
+
async function runFactoryAgent(agentDef, task, contextPayload, maxIterations = 12, workspacePath = null) {
|
|
615
|
+
const systemPrompt = agentDef.systemPrompt +
|
|
616
|
+
(agentDef.rules.length > 0
|
|
617
|
+
? `\n\n## Special Rules\n${agentDef.rules.map((r) => `- ${r}`).join("\n")}`
|
|
618
|
+
: "");
|
|
619
|
+
const toolDefs = toolsToDefinitions(agentDef.tools);
|
|
620
|
+
const contextStr = Object.keys(contextPayload).length > 0
|
|
621
|
+
? `\n\n## Context from previous tasks\n${JSON.stringify(contextPayload, null, 2)}`
|
|
622
|
+
: "";
|
|
623
|
+
const workspaceStr = workspacePath
|
|
624
|
+
? `\n\n## Workspace\nYou must read/write files only inside this project workspace: ${workspacePath}\nUse this path as your base directory for code changes.`
|
|
625
|
+
: "";
|
|
626
|
+
const messages = [
|
|
627
|
+
{ role: "system", content: systemPrompt + contextStr + workspaceStr },
|
|
628
|
+
{ role: "user", content: task },
|
|
629
|
+
];
|
|
630
|
+
const agentModel = agentDef.model || undefined;
|
|
631
|
+
const agentFallback = agentDef.fallbackModel || undefined;
|
|
632
|
+
const agentChat = (opts) => chatCompletionWithFallback(agentModel, agentFallback, opts);
|
|
633
|
+
const modelLabel = agentModel || getActiveModel();
|
|
634
|
+
let totalToolCalls = 0;
|
|
635
|
+
let totalFileWriteCalls = 0;
|
|
636
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
637
|
+
const iterStart = Date.now();
|
|
638
|
+
const response = await agentChat({
|
|
639
|
+
messages,
|
|
640
|
+
tools: toolDefs.length > 0 ? toolDefs : undefined,
|
|
641
|
+
});
|
|
642
|
+
const iterLatency = Date.now() - iterStart;
|
|
643
|
+
const usage = response.usage;
|
|
644
|
+
if (usage) {
|
|
645
|
+
logUsage({
|
|
646
|
+
model: modelLabel,
|
|
647
|
+
promptTokens: usage.promptTokens,
|
|
648
|
+
completionTokens: usage.completionTokens,
|
|
649
|
+
totalTokens: usage.promptTokens + usage.completionTokens,
|
|
650
|
+
costUsd: estimateCost(modelLabel, usage.promptTokens, usage.completionTokens),
|
|
651
|
+
latencyMs: iterLatency,
|
|
652
|
+
toolCalls: 0,
|
|
653
|
+
source: `factory:${agentDef.name}`,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
657
|
+
return {
|
|
658
|
+
content: response.content || "(no response)",
|
|
659
|
+
toolCalls: totalToolCalls,
|
|
660
|
+
fileWriteCalls: totalFileWriteCalls,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
totalToolCalls += response.toolCalls.length;
|
|
664
|
+
messages.push({
|
|
665
|
+
role: "assistant",
|
|
666
|
+
content: response.content || null,
|
|
667
|
+
tool_calls: response.toolCalls.map((tc) => ({
|
|
668
|
+
id: tc.id,
|
|
669
|
+
function: { name: tc.name, arguments: tc.arguments },
|
|
670
|
+
})),
|
|
671
|
+
});
|
|
672
|
+
for (const tc of response.toolCalls) {
|
|
673
|
+
if (tc.name === "file_write" || tc.name === "file_create") {
|
|
674
|
+
totalFileWriteCalls++;
|
|
675
|
+
}
|
|
676
|
+
const tool = getTool(tc.name);
|
|
677
|
+
if (!tool) {
|
|
678
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: `Unknown tool: ${tc.name}` });
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
const args = JSON.parse(tc.arguments);
|
|
683
|
+
const scopedArgs = scopeToolInput(tc.name, args, workspacePath);
|
|
684
|
+
const result = await tool.execute(scopedArgs);
|
|
685
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: result });
|
|
686
|
+
}
|
|
687
|
+
catch (err) {
|
|
688
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
689
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: `Error: ${msg}` });
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
try {
|
|
694
|
+
const forcedFinalize = await agentChat({
|
|
695
|
+
messages: [
|
|
696
|
+
...messages,
|
|
697
|
+
{
|
|
698
|
+
role: "user",
|
|
699
|
+
content: "Stop using tools. Return your final deliverable now using only the information already gathered in this conversation.",
|
|
700
|
+
},
|
|
701
|
+
],
|
|
702
|
+
});
|
|
703
|
+
if (forcedFinalize.content && forcedFinalize.content.trim().length > 0) {
|
|
704
|
+
return {
|
|
705
|
+
content: forcedFinalize.content,
|
|
706
|
+
toolCalls: totalToolCalls,
|
|
707
|
+
fileWriteCalls: totalFileWriteCalls,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
// Ignore finalize errors and fall back to sentinel below.
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
content: "Max iterations reached.",
|
|
716
|
+
toolCalls: totalToolCalls,
|
|
717
|
+
fileWriteCalls: totalFileWriteCalls,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
const Fredrix_DECOMPOSE_PROMPT = `You are Fredrix, Product Manager. Given a project description, create a product specification.
|
|
721
|
+
|
|
722
|
+
Define clear User Stories, MoSCoW prioritization (Must/Should/Could/Won't), and Acceptance Criteria.
|
|
723
|
+
|
|
724
|
+
Available teams and agents:
|
|
725
|
+
{agents}
|
|
726
|
+
|
|
727
|
+
Project: {description}
|
|
728
|
+
|
|
729
|
+
Return ONLY a JSON object with:
|
|
730
|
+
{
|
|
731
|
+
"title": "Project title",
|
|
732
|
+
"user_stories": [
|
|
733
|
+
{
|
|
734
|
+
"id": "US-1",
|
|
735
|
+
"title": "Short title",
|
|
736
|
+
"description": "As a user I want X so that Y",
|
|
737
|
+
"priority": "must|should|could|wont",
|
|
738
|
+
"acceptance_criteria": ["AC1", "AC2"]
|
|
739
|
+
}
|
|
740
|
+
]
|
|
741
|
+
}`;
|
|
742
|
+
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
|
+
|
|
744
|
+
Available agents:
|
|
745
|
+
{agents}
|
|
746
|
+
|
|
747
|
+
Fredrix's Product Spec:
|
|
748
|
+
{Fredrix_spec}
|
|
749
|
+
|
|
750
|
+
Available orchestration profiles:
|
|
751
|
+
{profiles}
|
|
752
|
+
|
|
753
|
+
Recommended project profile for this project: {selected_profile}
|
|
754
|
+
|
|
755
|
+
## CODE AGENTS (for implementation/integration tasks):
|
|
756
|
+
{code_agents}
|
|
757
|
+
|
|
758
|
+
## QA AGENTS (for testing tasks):
|
|
759
|
+
{qa_agents}
|
|
760
|
+
|
|
761
|
+
## REVIEW AGENTS (for code review tasks):
|
|
762
|
+
{review_agents}
|
|
763
|
+
|
|
764
|
+
## SECURITY AGENTS (for security audit tasks):
|
|
765
|
+
{security_agents}
|
|
766
|
+
|
|
767
|
+
MANDATORY RULES FOR CODE TASKS:
|
|
768
|
+
- Split implementation across MULTIPLE code agents listed above when the project has distinct areas (frontend, backend, integrations, mobile, etc.).
|
|
769
|
+
- Each coding task should be independently deployable (no circular dependencies between code tasks).
|
|
770
|
+
- All code tasks that can run in parallel MUST share the same dependency set (e.g. all depend on the architecture task).
|
|
771
|
+
- Assign reviewer_agent to every code task using a QA agent from the list above. NEVER leave reviewer_agent null for code tasks.
|
|
772
|
+
|
|
773
|
+
MANDATORY PIPELINE STAGES:
|
|
774
|
+
- Every project with code tasks MUST include a QA Testing task assigned to a QA agent that depends on ALL code tasks.
|
|
775
|
+
- Every project with code tasks MUST include a Code Review task assigned to a review agent that depends on the QA task.
|
|
776
|
+
- Security-sensitive tasks (auth, payments, user data, API keys) MUST have reviewer_agent set to a security agent.
|
|
777
|
+
|
|
778
|
+
Define dependencies between tasks (0-indexed task IDs). Tasks with no dependencies run first. Tasks that depend on others wait.
|
|
779
|
+
|
|
780
|
+
Provide a Mermaid flow diagram showing the task dependencies.
|
|
781
|
+
|
|
782
|
+
Return ONLY JSON in one of these formats:
|
|
783
|
+
|
|
784
|
+
Option A (preferred):
|
|
785
|
+
{
|
|
786
|
+
"pipeline_profile": "one of the available profiles",
|
|
787
|
+
"tasks": [
|
|
788
|
+
{
|
|
789
|
+
"title": "Short task title",
|
|
790
|
+
"description": "Detailed technical description including what the agent should produce",
|
|
791
|
+
"assigned_agent": "agent_name_lowercase",
|
|
792
|
+
"reviewer_agent": "agent_name_lowercase" | null,
|
|
793
|
+
"dependencies": [0, 1],
|
|
794
|
+
"max_retries": 10,
|
|
795
|
+
"stage_id": "optional stage id from stage catalog",
|
|
796
|
+
"architecture_contract": {
|
|
797
|
+
"stack_type": "optional stack",
|
|
798
|
+
"language": "optional language",
|
|
799
|
+
"required_artifacts": ["optional patterns"],
|
|
800
|
+
"forbidden_artifacts": ["optional patterns"],
|
|
801
|
+
"reviewer_focus": ["optional checks"],
|
|
802
|
+
"notes": "optional"
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
]
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
Option B (legacy fallback): JSON array of tasks only.`;
|
|
809
|
+
function inferProjectProfile(description) {
|
|
810
|
+
const text = description.toLowerCase();
|
|
811
|
+
if (/\b(game|engine|unreal|unity|opengl|vulkan|render)\b/.test(text))
|
|
812
|
+
return "game-engine-cpp";
|
|
813
|
+
if (/\b(ios|android|mobile|react native|flutter|swift|kotlin)\b/.test(text))
|
|
814
|
+
return "mobile-app";
|
|
815
|
+
if (/\b(marketing|campaign|seo|social media|ads|funnel|growth)\b/.test(text))
|
|
816
|
+
return "marketing-campaign";
|
|
817
|
+
if (/\b(code review|review only|audit existing code|refactor audit)\b/.test(text))
|
|
818
|
+
return "code-review-only";
|
|
819
|
+
if (/\b(publish|publishing|newsletter|editorial|content pipeline|release notes)\b/.test(text))
|
|
820
|
+
return "publishing-workflow";
|
|
821
|
+
return config.factoryOrchestration.selection.defaultProfile;
|
|
822
|
+
}
|
|
823
|
+
function resolveProjectProfile(description, requestedProfile) {
|
|
824
|
+
const profiles = config.factoryOrchestration.profiles || {};
|
|
825
|
+
if (requestedProfile && profiles[requestedProfile])
|
|
826
|
+
return requestedProfile;
|
|
827
|
+
const inferred = inferProjectProfile(description);
|
|
828
|
+
if (profiles[inferred])
|
|
829
|
+
return inferred;
|
|
830
|
+
return config.factoryOrchestration.selection.defaultProfile;
|
|
831
|
+
}
|
|
832
|
+
function formatProfilesForPrompt() {
|
|
833
|
+
const profiles = config.factoryOrchestration.profiles || {};
|
|
834
|
+
return Object.entries(profiles)
|
|
835
|
+
.map(([key, value]) => `- ${key}: mandatory=[${value.mandatoryStages.join(", ")}], optional=[${value.optionalStages.join(", ")}]`)
|
|
836
|
+
.join("\n");
|
|
837
|
+
}
|
|
838
|
+
function normalizeStageId(input) {
|
|
839
|
+
if (!input)
|
|
840
|
+
return null;
|
|
841
|
+
const normalized = input.toLowerCase().trim().replace(/_/g, "-");
|
|
842
|
+
const aliasMap = {
|
|
843
|
+
qa: "qa-testing",
|
|
844
|
+
security: "security-audit",
|
|
845
|
+
"final review": "final-review",
|
|
846
|
+
review: "code-review",
|
|
847
|
+
};
|
|
848
|
+
return aliasMap[normalized] || normalized;
|
|
849
|
+
}
|
|
850
|
+
function agentMatchesSelectors(agent, selectors) {
|
|
851
|
+
if (!selectors)
|
|
852
|
+
return false;
|
|
853
|
+
const phase = AGENT_PHASES[agent.name.toLowerCase()] || "";
|
|
854
|
+
const team = (agent.team || "").toLowerCase();
|
|
855
|
+
const tags = new Set((agent.tags || []).map((t) => t.toLowerCase()));
|
|
856
|
+
const tools = new Set((agent.tools || []).map((t) => t.toLowerCase()));
|
|
857
|
+
const phaseMatch = Array.isArray(selectors.phase)
|
|
858
|
+
? selectors.phase.some((p) => String(p).toLowerCase() === phase)
|
|
859
|
+
: false;
|
|
860
|
+
const teamMatch = Array.isArray(selectors.team)
|
|
861
|
+
? selectors.team.some((t) => String(t).toLowerCase() === team)
|
|
862
|
+
: false;
|
|
863
|
+
const tagMatch = Array.isArray(selectors.tags)
|
|
864
|
+
? selectors.tags.some((t) => tags.has(String(t).toLowerCase()))
|
|
865
|
+
: false;
|
|
866
|
+
const toolsMatch = Array.isArray(selectors.toolsAny)
|
|
867
|
+
? selectors.toolsAny.some((t) => tools.has(String(t).toLowerCase()))
|
|
868
|
+
: false;
|
|
869
|
+
return phaseMatch || teamMatch || tagMatch || toolsMatch;
|
|
870
|
+
}
|
|
871
|
+
function findAgentForStage(stageId, allAgents, fallback) {
|
|
872
|
+
const stageDef = config.factoryOrchestration.stageCatalog?.[stageId];
|
|
873
|
+
const fromSelector = allAgents.find((a) => agentMatchesSelectors(a, stageDef?.selectors));
|
|
874
|
+
if (fromSelector)
|
|
875
|
+
return fromSelector.name;
|
|
876
|
+
const byMappedPhase = allAgents.find((a) => {
|
|
877
|
+
const phase = AGENT_PHASES[a.name.toLowerCase()] || "";
|
|
878
|
+
if (stageId === "qa-testing")
|
|
879
|
+
return phase === "qa";
|
|
880
|
+
if (stageId === "security-audit")
|
|
881
|
+
return phase === "security";
|
|
882
|
+
if (stageId === "code-review")
|
|
883
|
+
return phase === "code-review";
|
|
884
|
+
if (stageId === "ideation")
|
|
885
|
+
return phase === "ideation";
|
|
886
|
+
if (stageId === "blueprint")
|
|
887
|
+
return phase === "blueprint";
|
|
888
|
+
if (stageId === "ui-ux-design")
|
|
889
|
+
return phase === "design";
|
|
890
|
+
if (stageId === "implementation")
|
|
891
|
+
return phase === "implementation";
|
|
892
|
+
if (stageId === "integration")
|
|
893
|
+
return phase === "integration";
|
|
894
|
+
if (stageId === "feedback")
|
|
895
|
+
return phase === "feedback";
|
|
896
|
+
return false;
|
|
897
|
+
});
|
|
898
|
+
if (byMappedPhase)
|
|
899
|
+
return byMappedPhase.name;
|
|
900
|
+
const fallbackByName = getAgentByNameFromList(fallback, allAgents);
|
|
901
|
+
return fallbackByName?.name || fallback;
|
|
902
|
+
}
|
|
903
|
+
function detectTaskStageId(task, allAgents) {
|
|
904
|
+
const direct = normalizeStageId(typeof task.stage_id === "string" ? task.stage_id : null);
|
|
905
|
+
if (direct && config.factoryOrchestration.stageCatalog[direct])
|
|
906
|
+
return direct;
|
|
907
|
+
const assigned = typeof task.assigned_agent === "string" ? task.assigned_agent : "";
|
|
908
|
+
const agent = getAgentByNameFromList(assigned, allAgents);
|
|
909
|
+
if (agent) {
|
|
910
|
+
const entries = Object.entries(config.factoryOrchestration.stageCatalog)
|
|
911
|
+
.sort((a, b) => (a[1].order || 999) - (b[1].order || 999));
|
|
912
|
+
for (const [stageId, stageDef] of entries) {
|
|
913
|
+
if (agentMatchesSelectors(agent, stageDef.selectors)) {
|
|
914
|
+
return stageId;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
const blob = `${task.title || ""} ${task.description || ""}`.toLowerCase();
|
|
919
|
+
if (/\b(qa|test|smoke|edge case)\b/.test(blob))
|
|
920
|
+
return "qa-testing";
|
|
921
|
+
if (/\b(security|vulnerab|auth|gdpr|xss|sqli)\b/.test(blob))
|
|
922
|
+
return "security-audit";
|
|
923
|
+
if (/\b(code review|review code|refactor review)\b/.test(blob))
|
|
924
|
+
return "code-review";
|
|
925
|
+
if (/\b(deploy|docker|infra|kubernetes|vercel|aws)\b/.test(blob))
|
|
926
|
+
return "deployment";
|
|
927
|
+
if (/\b(release|semver|changelog|release notes)\b/.test(blob))
|
|
928
|
+
return "release";
|
|
929
|
+
if (/\b(feedback|support|user report)\b/.test(blob))
|
|
930
|
+
return "feedback";
|
|
931
|
+
if (/\b(design|ui|ux|wireframe)\b/.test(blob))
|
|
932
|
+
return "ui-ux-design";
|
|
933
|
+
if (/\b(architecture|blueprint|schema|system design)\b/.test(blob))
|
|
934
|
+
return "blueprint";
|
|
935
|
+
if (/\b(integration|webhook|external api)\b/.test(blob))
|
|
936
|
+
return "integration";
|
|
937
|
+
if (/\b(implement|build|develop|feature|frontend|backend|api)\b/.test(blob))
|
|
938
|
+
return "implementation";
|
|
939
|
+
if (/\b(product spec|user stor|acceptance criteria)\b/.test(blob))
|
|
940
|
+
return "ideation";
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
function buildInjectedStageTask(stageId, assignedAgent, dependencies, maxRetries) {
|
|
944
|
+
const templates = {
|
|
945
|
+
"ideation": {
|
|
946
|
+
title: "Ideation & Product Specification",
|
|
947
|
+
description: "Refine product scope, user stories, and acceptance criteria based on current project context.",
|
|
948
|
+
},
|
|
949
|
+
"blueprint": {
|
|
950
|
+
title: "Technical Blueprint",
|
|
951
|
+
description: "Create or refine architecture decisions, system boundaries, and technical constraints.",
|
|
952
|
+
},
|
|
953
|
+
"ui-ux-design": {
|
|
954
|
+
title: "UI/UX Design",
|
|
955
|
+
description: "Deliver interaction flow, visual language, and implementation-ready design guidance.",
|
|
956
|
+
},
|
|
957
|
+
"implementation": {
|
|
958
|
+
title: "Implementation",
|
|
959
|
+
description: "Implement the required product functionality in code according to accepted architecture and quality standards.",
|
|
960
|
+
},
|
|
961
|
+
"integration": {
|
|
962
|
+
title: "Integration",
|
|
963
|
+
description: "Integrate modules and external services; ensure interfaces and contracts are correctly connected.",
|
|
964
|
+
},
|
|
965
|
+
"security-audit": {
|
|
966
|
+
title: "Security Audit",
|
|
967
|
+
description: "Audit for vulnerabilities, auth weaknesses, secrets handling, and compliance/security posture.",
|
|
968
|
+
},
|
|
969
|
+
"qa-testing": {
|
|
970
|
+
title: "QA Testing",
|
|
971
|
+
description: "Run smoke, regression, and edge-case validation against acceptance criteria.",
|
|
972
|
+
},
|
|
973
|
+
"code-review": {
|
|
974
|
+
title: "Code Review",
|
|
975
|
+
description: "Review maintainability, readability, correctness, and architecture adherence.",
|
|
976
|
+
},
|
|
977
|
+
"final-review": {
|
|
978
|
+
title: "Final Product Review",
|
|
979
|
+
description: "Verify final output against original user stories and acceptance criteria.",
|
|
980
|
+
},
|
|
981
|
+
"deployment": {
|
|
982
|
+
title: "Deployment",
|
|
983
|
+
description: "Prepare and execute deployment workflow with environment and release safety checks.",
|
|
984
|
+
},
|
|
985
|
+
"release": {
|
|
986
|
+
title: "Release",
|
|
987
|
+
description: "Prepare release notes, versioning, and rollout communication.",
|
|
988
|
+
},
|
|
989
|
+
"feedback": {
|
|
990
|
+
title: "Feedback Collection",
|
|
991
|
+
description: "Collect post-release feedback and convert findings into actionable follow-up tasks.",
|
|
992
|
+
},
|
|
993
|
+
};
|
|
994
|
+
const template = templates[stageId] || {
|
|
995
|
+
title: `Pipeline Stage: ${stageId}`,
|
|
996
|
+
description: `Execute required activities for stage ${stageId}.`,
|
|
997
|
+
};
|
|
998
|
+
return {
|
|
999
|
+
title: template.title,
|
|
1000
|
+
description: template.description,
|
|
1001
|
+
assigned_agent: assignedAgent,
|
|
1002
|
+
reviewer_agent: null,
|
|
1003
|
+
dependencies,
|
|
1004
|
+
max_retries: maxRetries,
|
|
1005
|
+
stage_id: stageId,
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
async function decomposeProject(description) {
|
|
1009
|
+
const agents = listAgents();
|
|
1010
|
+
const agentList = agents
|
|
1011
|
+
.map((a) => `- ${a.name} (${a.role}, team: ${a.team})`)
|
|
1012
|
+
.join("\n");
|
|
1013
|
+
const codeAgents = agents.filter((a) => isCodeAgent(a));
|
|
1014
|
+
const qaAgents = agents.filter((a) => isQaAgent(a));
|
|
1015
|
+
const reviewAgents = agents.filter((a) => isReviewAgent(a));
|
|
1016
|
+
const securityAgents = agents.filter((a) => isSecurityAgent(a));
|
|
1017
|
+
const suggestedProfile = resolveProjectProfile(description, null);
|
|
1018
|
+
const formatAgentList = (list) => list.length > 0
|
|
1019
|
+
? list.map((a) => `- ${a.name} (${a.role})`).join("\n")
|
|
1020
|
+
: "- (none available)";
|
|
1021
|
+
const FredrixPrompt = Fredrix_DECOMPOSE_PROMPT
|
|
1022
|
+
.replace("{agents}", agentList)
|
|
1023
|
+
.replace("{description}", description);
|
|
1024
|
+
console.log("🏭 Fredrix decomposing requirements...");
|
|
1025
|
+
const FredrixAgent = getAgent("Fredrix");
|
|
1026
|
+
const FredrixResponse = await chatCompletionWithFallback(FredrixAgent?.model || undefined, FredrixAgent?.fallbackModel || undefined, { messages: [{ role: "user", content: FredrixPrompt }] });
|
|
1027
|
+
const FredrixText = FredrixResponse.content?.trim() || "{}";
|
|
1028
|
+
const FredrixJsonMatch = FredrixText.match(/\{[\s\S]*\}/);
|
|
1029
|
+
if (!FredrixJsonMatch)
|
|
1030
|
+
throw new Error("Fredrix failed to produce valid JSON");
|
|
1031
|
+
const FredrixSpec = JSON.parse(FredrixJsonMatch[0]);
|
|
1032
|
+
console.log("🏭 Stoffe building architecture...");
|
|
1033
|
+
const StoffeAgent = getAgent("Stoffe");
|
|
1034
|
+
const StoffePrompt = Stoffe_DECOMPOSE_PROMPT
|
|
1035
|
+
.replace("{agents}", agentList)
|
|
1036
|
+
.replace("{Fredrix_spec}", JSON.stringify(FredrixSpec, null, 2))
|
|
1037
|
+
.replace("{profiles}", formatProfilesForPrompt())
|
|
1038
|
+
.replace("{selected_profile}", suggestedProfile)
|
|
1039
|
+
.replace("{code_agents}", formatAgentList(codeAgents))
|
|
1040
|
+
.replace("{qa_agents}", formatAgentList(qaAgents))
|
|
1041
|
+
.replace("{review_agents}", formatAgentList(reviewAgents))
|
|
1042
|
+
.replace("{security_agents}", formatAgentList(securityAgents));
|
|
1043
|
+
const StoffeResponse = await chatCompletionWithFallback(StoffeAgent?.model || undefined, StoffeAgent?.fallbackModel || undefined, { messages: [{ role: "user", content: StoffePrompt }] });
|
|
1044
|
+
const StoffeText = StoffeResponse.content?.trim() || "[]";
|
|
1045
|
+
let rawTasks = [];
|
|
1046
|
+
let StoffePipelineProfile = null;
|
|
1047
|
+
const objectMatch = StoffeText.match(/\{[\s\S]*\}/);
|
|
1048
|
+
if (objectMatch) {
|
|
1049
|
+
const parsed = JSON.parse(objectMatch[0]);
|
|
1050
|
+
if (typeof parsed.pipeline_profile === "string") {
|
|
1051
|
+
StoffePipelineProfile = parsed.pipeline_profile;
|
|
1052
|
+
}
|
|
1053
|
+
if (Array.isArray(parsed.tasks)) {
|
|
1054
|
+
rawTasks = parsed.tasks;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (rawTasks.length === 0) {
|
|
1058
|
+
const StoffeJsonMatch = StoffeText.match(/\[[\s\S]*\]/);
|
|
1059
|
+
if (!StoffeJsonMatch)
|
|
1060
|
+
throw new Error("Stoffe failed to produce valid JSON");
|
|
1061
|
+
rawTasks = JSON.parse(StoffeJsonMatch[0]);
|
|
1062
|
+
}
|
|
1063
|
+
const pipelineProfile = resolveProjectProfile(description, StoffePipelineProfile);
|
|
1064
|
+
const validAgents = new Set(agents.map((a) => a.name.toLowerCase()));
|
|
1065
|
+
const defaultSecurityReviewer = findAgentForStage("security-audit", agents, "sara");
|
|
1066
|
+
const tasks = rawTasks.map((t) => {
|
|
1067
|
+
const assigned = validAgents.has((t.assigned_agent || "").toLowerCase())
|
|
1068
|
+
? t.assigned_agent.toLowerCase()
|
|
1069
|
+
: "chris";
|
|
1070
|
+
let reviewer = t.reviewer_agent && validAgents.has(t.reviewer_agent.toLowerCase())
|
|
1071
|
+
? t.reviewer_agent.toLowerCase()
|
|
1072
|
+
: null;
|
|
1073
|
+
if (isSecuritySensitiveTask(t) && (!reviewer || reviewer !== defaultSecurityReviewer)) {
|
|
1074
|
+
reviewer = defaultSecurityReviewer;
|
|
1075
|
+
}
|
|
1076
|
+
return {
|
|
1077
|
+
title: t.title || "Untitled task",
|
|
1078
|
+
description: t.description || "",
|
|
1079
|
+
assigned_agent: assigned,
|
|
1080
|
+
reviewer_agent: reviewer,
|
|
1081
|
+
dependencies: Array.isArray(t.dependencies) ? t.dependencies : [],
|
|
1082
|
+
max_retries: Number.isInteger(t.max_retries)
|
|
1083
|
+
? Math.max(1, Number(t.max_retries))
|
|
1084
|
+
: DEFAULT_MAX_RETRIES,
|
|
1085
|
+
stage_id: normalizeStageId(typeof t.stage_id === "string" ? t.stage_id : detectTaskStageId(t, agents)),
|
|
1086
|
+
architecture_contract: normalizeArchitectureContract(t.architecture_contract),
|
|
1087
|
+
};
|
|
1088
|
+
});
|
|
1089
|
+
return {
|
|
1090
|
+
title: FredrixSpec.title || description.substring(0, 60),
|
|
1091
|
+
tasks,
|
|
1092
|
+
pipelineProfile,
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
function isCodeTask(agentName, allAgents) {
|
|
1096
|
+
const agent = getAgentByNameFromList(agentName, allAgents);
|
|
1097
|
+
if (!agent)
|
|
1098
|
+
return false;
|
|
1099
|
+
return isCodeAgent(agent);
|
|
1100
|
+
}
|
|
1101
|
+
function isSecuritySensitiveTask(task) {
|
|
1102
|
+
const blob = `${task.title || ""} ${task.description || ""}`.toLowerCase();
|
|
1103
|
+
if (/\b(auth|authentication|authorization|oauth|jwt|token|payment|billing|invoice|card|pci|gdpr|pii|personal data|secret|api key|encryption|password|session|security)\b/.test(blob)) {
|
|
1104
|
+
return true;
|
|
1105
|
+
}
|
|
1106
|
+
const contract = normalizeArchitectureContract(task.architecture_contract);
|
|
1107
|
+
if (!contract)
|
|
1108
|
+
return false;
|
|
1109
|
+
const contractBlob = `${contract.stackType || ""} ${contract.notes || ""} ${contract.reviewerFocus.join(" ")}`.toLowerCase();
|
|
1110
|
+
return /\b(security|auth|gdpr|privacy|compliance)\b/.test(contractBlob);
|
|
1111
|
+
}
|
|
1112
|
+
function injectMissingStages(tasks, profileId, allAgents) {
|
|
1113
|
+
const patched = tasks.map((t) => ({ ...t }));
|
|
1114
|
+
const profile = config.factoryOrchestration.profiles[profileId]
|
|
1115
|
+
|| config.factoryOrchestration.profiles[config.factoryOrchestration.selection.defaultProfile]
|
|
1116
|
+
|| { mandatoryStages: ["qa-testing"], optionalStages: [] };
|
|
1117
|
+
const mandatoryStages = (profile.mandatoryStages || []).map((s) => normalizeStageId(s)).filter((s) => !!s);
|
|
1118
|
+
const stageOrder = Object.fromEntries(Object.entries(config.factoryOrchestration.stageCatalog).map(([id, def]) => [id, def.order || 999]));
|
|
1119
|
+
mandatoryStages.sort((a, b) => (stageOrder[a] || 999) - (stageOrder[b] || 999));
|
|
1120
|
+
for (const task of patched) {
|
|
1121
|
+
const detected = normalizeStageId(task.stage_id) || detectTaskStageId(task, allAgents);
|
|
1122
|
+
task.stage_id = detected;
|
|
1123
|
+
}
|
|
1124
|
+
const codeTaskIndices = [];
|
|
1125
|
+
patched.forEach((t, i) => {
|
|
1126
|
+
if (isCodeTask(t.assigned_agent, allAgents) || t.stage_id === "implementation" || t.stage_id === "integration") {
|
|
1127
|
+
codeTaskIndices.push(i);
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
const stageIndices = (stageId) => patched
|
|
1131
|
+
.map((t, idx) => ({ stageId: t.stage_id, idx }))
|
|
1132
|
+
.filter((x) => x.stageId === stageId)
|
|
1133
|
+
.map((x) => x.idx);
|
|
1134
|
+
for (let i = 0; i < mandatoryStages.length; i++) {
|
|
1135
|
+
const stageId = mandatoryStages[i];
|
|
1136
|
+
const exists = stageIndices(stageId).length > 0;
|
|
1137
|
+
if (exists)
|
|
1138
|
+
continue;
|
|
1139
|
+
const prevStages = mandatoryStages.slice(0, i).reverse();
|
|
1140
|
+
let deps = [];
|
|
1141
|
+
for (const prev of prevStages) {
|
|
1142
|
+
const found = stageIndices(prev);
|
|
1143
|
+
if (found.length > 0) {
|
|
1144
|
+
deps = found;
|
|
1145
|
+
break;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
if (stageId === "qa-testing" || stageId === "security-audit") {
|
|
1149
|
+
deps = codeTaskIndices.length > 0 ? [...codeTaskIndices] : deps;
|
|
1150
|
+
}
|
|
1151
|
+
else if (stageId === "code-review") {
|
|
1152
|
+
const qaDeps = stageIndices("qa-testing");
|
|
1153
|
+
deps = qaDeps.length > 0 ? qaDeps : (codeTaskIndices.length > 0 ? [...codeTaskIndices] : deps);
|
|
1154
|
+
}
|
|
1155
|
+
else if (stageId === "final-review") {
|
|
1156
|
+
const reviewDeps = stageIndices("code-review");
|
|
1157
|
+
const qaDeps = stageIndices("qa-testing");
|
|
1158
|
+
deps = reviewDeps.length > 0 ? reviewDeps : (qaDeps.length > 0 ? qaDeps : deps);
|
|
1159
|
+
}
|
|
1160
|
+
else if (stageId === "deployment") {
|
|
1161
|
+
const finalDeps = stageIndices("final-review");
|
|
1162
|
+
const reviewDeps = stageIndices("code-review");
|
|
1163
|
+
deps = finalDeps.length > 0 ? finalDeps : (reviewDeps.length > 0 ? reviewDeps : deps);
|
|
1164
|
+
}
|
|
1165
|
+
else if (stageId === "release") {
|
|
1166
|
+
const deployDeps = stageIndices("deployment");
|
|
1167
|
+
deps = deployDeps.length > 0 ? deployDeps : deps;
|
|
1168
|
+
}
|
|
1169
|
+
else if (stageId === "feedback") {
|
|
1170
|
+
const releaseDeps = stageIndices("release");
|
|
1171
|
+
const finalDeps = stageIndices("final-review");
|
|
1172
|
+
deps = releaseDeps.length > 0 ? releaseDeps : (finalDeps.length > 0 ? finalDeps : deps);
|
|
1173
|
+
}
|
|
1174
|
+
const stagePolicy = config.factoryOrchestration.stageCatalog[stageId];
|
|
1175
|
+
const stageMaxRetries = stagePolicy?.retryPolicy?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
1176
|
+
const fallbackAgent = stageId === "qa-testing"
|
|
1177
|
+
? "tess"
|
|
1178
|
+
: stageId === "code-review"
|
|
1179
|
+
? "chris"
|
|
1180
|
+
: stageId === "security-audit"
|
|
1181
|
+
? "sara"
|
|
1182
|
+
: stageId === "ideation" || stageId === "final-review"
|
|
1183
|
+
? "Fredrix"
|
|
1184
|
+
: stageId === "blueprint"
|
|
1185
|
+
? "Stoffe"
|
|
1186
|
+
: stageId === "ui-ux-design"
|
|
1187
|
+
? "ulla"
|
|
1188
|
+
: stageId === "implementation"
|
|
1189
|
+
? "chris"
|
|
1190
|
+
: stageId === "integration"
|
|
1191
|
+
? "ivan"
|
|
1192
|
+
: stageId === "deployment"
|
|
1193
|
+
? "dan"
|
|
1194
|
+
: stageId === "release"
|
|
1195
|
+
? "stig"
|
|
1196
|
+
: stageId === "feedback"
|
|
1197
|
+
? "sune"
|
|
1198
|
+
: "chris";
|
|
1199
|
+
const selectedAgent = findAgentForStage(stageId, allAgents, fallbackAgent);
|
|
1200
|
+
const injectedTask = buildInjectedStageTask(stageId, selectedAgent, deps, Math.max(1, stageMaxRetries));
|
|
1201
|
+
patched.push(injectedTask);
|
|
1202
|
+
console.log(`🏭 Injected missing stage ${stageId} → ${selectedAgent}, deps: [${deps.join(",")}]`);
|
|
1203
|
+
}
|
|
1204
|
+
const qaAgent = findAgentForStage("qa-testing", allAgents, "tess");
|
|
1205
|
+
const securityAgent = findAgentForStage("security-audit", allAgents, "sara");
|
|
1206
|
+
for (const idx of codeTaskIndices) {
|
|
1207
|
+
if (!patched[idx].reviewer_agent) {
|
|
1208
|
+
patched[idx].reviewer_agent = qaAgent;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
for (let i = 0; i < patched.length; i++) {
|
|
1212
|
+
const task = patched[i];
|
|
1213
|
+
if (isSecuritySensitiveTask(task)) {
|
|
1214
|
+
task.reviewer_agent = securityAgent;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
for (const task of patched) {
|
|
1218
|
+
const stageId = normalizeStageId(task.stage_id);
|
|
1219
|
+
if (!stageId)
|
|
1220
|
+
continue;
|
|
1221
|
+
const stagePolicy = config.factoryOrchestration.stageCatalog[stageId];
|
|
1222
|
+
if (stagePolicy?.retryPolicy?.maxRetries) {
|
|
1223
|
+
task.max_retries = Math.max(1, Number(stagePolicy.retryPolicy.maxRetries));
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
return patched;
|
|
1227
|
+
}
|
|
1228
|
+
export async function factoryCreate(description) {
|
|
1229
|
+
const { title, tasks, pipelineProfile } = await decomposeProject(description);
|
|
1230
|
+
const allAgents = listAgents();
|
|
1231
|
+
const profile = resolveProjectProfile(description, pipelineProfile);
|
|
1232
|
+
const patchedTasks = injectMissingStages(tasks, profile, allAgents);
|
|
1233
|
+
const project = createFactoryProject(title, description);
|
|
1234
|
+
const projectId = project.id;
|
|
1235
|
+
const taskMap = new Map();
|
|
1236
|
+
for (let i = 0; i < patchedTasks.length; i++) {
|
|
1237
|
+
const t = patchedTasks[i];
|
|
1238
|
+
const phase = AGENT_PHASES[t.assigned_agent?.toLowerCase()] || null;
|
|
1239
|
+
const contextPayload = {
|
|
1240
|
+
...(t.architecture_contract ? { architecture_contract: t.architecture_contract } : {}),
|
|
1241
|
+
...(t.stage_id ? { stage_id: t.stage_id } : {}),
|
|
1242
|
+
pipeline_profile: profile,
|
|
1243
|
+
};
|
|
1244
|
+
const result = insertTask.run(projectId, t.title, t.description, t.assigned_agent, t.reviewer_agent || null, "[]", t.max_retries, JSON.stringify(contextPayload), phase);
|
|
1245
|
+
taskMap.set(i, Number(result.lastInsertRowid));
|
|
1246
|
+
}
|
|
1247
|
+
for (let i = 0; i < patchedTasks.length; i++) {
|
|
1248
|
+
const taskId = taskMap.get(i);
|
|
1249
|
+
if (!taskId)
|
|
1250
|
+
continue;
|
|
1251
|
+
const mappedDependencies = (Array.isArray(patchedTasks[i].dependencies) ? patchedTasks[i].dependencies : [])
|
|
1252
|
+
.map((depIdx) => {
|
|
1253
|
+
if (typeof depIdx !== "number" || !Number.isInteger(depIdx))
|
|
1254
|
+
return null;
|
|
1255
|
+
return taskMap.get(depIdx) ?? null;
|
|
1256
|
+
})
|
|
1257
|
+
.filter((depId) => depId !== null);
|
|
1258
|
+
updateTaskDependencies.run(JSON.stringify(mappedDependencies), taskId);
|
|
1259
|
+
}
|
|
1260
|
+
const created = getFactoryTasks(projectId);
|
|
1261
|
+
updateProjectStatus.run("running", projectId);
|
|
1262
|
+
const updatedRow = getProjectById.get(projectId);
|
|
1263
|
+
return {
|
|
1264
|
+
project: rowToProject(updatedRow),
|
|
1265
|
+
tasks: created,
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
function areDependenciesMet(task, allTasks) {
|
|
1269
|
+
if (task.dependencies.length === 0)
|
|
1270
|
+
return true;
|
|
1271
|
+
return task.dependencies.every((depId) => {
|
|
1272
|
+
const dep = allTasks.find((t) => t.id === depId);
|
|
1273
|
+
return dep && dep.status === "done";
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
function buildContextFromDeps(task, allTasks) {
|
|
1277
|
+
const context = {};
|
|
1278
|
+
for (const depId of task.dependencies) {
|
|
1279
|
+
const dep = allTasks.find((t) => t.id === depId);
|
|
1280
|
+
if (dep && dep.result) {
|
|
1281
|
+
context[`${dep.title} (${dep.assignedAgent})`] = dep.result;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return context;
|
|
1285
|
+
}
|
|
1286
|
+
function normalizeLegacyProjectDependencies(projectId) {
|
|
1287
|
+
const projectTasks = getTasksByProject.all(projectId).map(rowToTask);
|
|
1288
|
+
if (projectTasks.length === 0)
|
|
1289
|
+
return;
|
|
1290
|
+
const hasLegacyDependencies = projectTasks.some((task) => task.dependencies.includes(0));
|
|
1291
|
+
if (!hasLegacyDependencies)
|
|
1292
|
+
return;
|
|
1293
|
+
const indexToTaskId = projectTasks.map((task) => task.id);
|
|
1294
|
+
let changed = 0;
|
|
1295
|
+
for (const task of projectTasks) {
|
|
1296
|
+
const normalizedDeps = task.dependencies
|
|
1297
|
+
.map((depIdx) => {
|
|
1298
|
+
if (!Number.isInteger(depIdx) || depIdx < 0)
|
|
1299
|
+
return null;
|
|
1300
|
+
return indexToTaskId[depIdx] ?? null;
|
|
1301
|
+
})
|
|
1302
|
+
.filter((depId) => depId !== null);
|
|
1303
|
+
if (JSON.stringify(normalizedDeps) !== JSON.stringify(task.dependencies)) {
|
|
1304
|
+
updateTaskDependencies.run(JSON.stringify(normalizedDeps), task.id);
|
|
1305
|
+
changed++;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
if (changed > 0) {
|
|
1309
|
+
autoNormalizedProjects.add(projectId);
|
|
1310
|
+
updateProjectUpdatedAt.run(projectId);
|
|
1311
|
+
console.log(`🏭 Normalized legacy dependencies for project ${projectId} (${changed} tasks)`);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
async function notifyHumanIntervention(task, project) {
|
|
1315
|
+
const userId = [...config.allowedUserIds][0];
|
|
1316
|
+
if (!userId)
|
|
1317
|
+
return;
|
|
1318
|
+
const msg = `⚠️ **Factory Alert**\n\nProject: ${project.title}\nTask: ${task.title}\nAgent: ${task.assignedAgent}\n\n${task.assignedAgent} and ${task.reviewerAgent || "QA"} are stuck in a loop (${task.retryCount} retries).\n\nUse \`/factory retry ${task.id}\` to force retry.`;
|
|
1319
|
+
try {
|
|
1320
|
+
await sendToTelegram(userId, msg);
|
|
1321
|
+
}
|
|
1322
|
+
catch (err) {
|
|
1323
|
+
console.error("Factory: failed to send Telegram notification:", err);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
const MAX_CONCURRENT = 3;
|
|
1327
|
+
const TASK_AGENT_MAX_ITERATIONS = 14;
|
|
1328
|
+
const REVIEW_AGENT_MAX_ITERATIONS = 10;
|
|
1329
|
+
const STALE_IN_PROGRESS_MINUTES = 8;
|
|
1330
|
+
let activeRuns = 0;
|
|
1331
|
+
const autoNormalizedProjects = new Set();
|
|
1332
|
+
const finalizedProjects = new Set();
|
|
1333
|
+
export function wasProjectAutoNormalized(projectId) {
|
|
1334
|
+
return autoNormalizedProjects.has(projectId);
|
|
1335
|
+
}
|
|
1336
|
+
function extractLatestReviewFeedback(contextPayload) {
|
|
1337
|
+
const raw = contextPayload.latest_review_feedback;
|
|
1338
|
+
if (!raw)
|
|
1339
|
+
return null;
|
|
1340
|
+
if (typeof raw === "string") {
|
|
1341
|
+
return {
|
|
1342
|
+
status: "rejected",
|
|
1343
|
+
from: "review",
|
|
1344
|
+
to: "agent",
|
|
1345
|
+
reason: raw,
|
|
1346
|
+
retry_count: 0,
|
|
1347
|
+
problem: raw,
|
|
1348
|
+
fix: "Address each issue in this review and verify against the task requirements.",
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
if (typeof raw !== "object")
|
|
1352
|
+
return null;
|
|
1353
|
+
const obj = raw;
|
|
1354
|
+
const reason = typeof obj.reason === "string" ? obj.reason : "Review identified issues";
|
|
1355
|
+
const retryCount = typeof obj.retry_count === "number" && Number.isInteger(obj.retry_count)
|
|
1356
|
+
? obj.retry_count
|
|
1357
|
+
: 0;
|
|
1358
|
+
return {
|
|
1359
|
+
status: "rejected",
|
|
1360
|
+
from: typeof obj.from === "string" ? obj.from : "review",
|
|
1361
|
+
to: typeof obj.to === "string" ? obj.to : "agent",
|
|
1362
|
+
reason,
|
|
1363
|
+
retry_count: retryCount,
|
|
1364
|
+
problem: typeof obj.problem === "string" ? obj.problem : reason,
|
|
1365
|
+
fix: typeof obj.fix === "string" ? obj.fix : "Address all review issues before submitting.",
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
function getAttemptsByAgent(contextPayload) {
|
|
1369
|
+
const raw = contextPayload.attempts_by_agent;
|
|
1370
|
+
if (!raw || typeof raw !== "object")
|
|
1371
|
+
return {};
|
|
1372
|
+
const attempts = {};
|
|
1373
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
1374
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
1375
|
+
attempts[name.toLowerCase()] = Math.floor(value);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
return attempts;
|
|
1379
|
+
}
|
|
1380
|
+
function getExecutionAgent(task, contextPayload, contract) {
|
|
1381
|
+
const attemptsByAgent = getAttemptsByAgent(contextPayload);
|
|
1382
|
+
const allAgents = listAgents();
|
|
1383
|
+
const stageId = normalizeStageId(typeof contextPayload.stage_id === "string" ? contextPayload.stage_id : null);
|
|
1384
|
+
const stagePreferred = stageId
|
|
1385
|
+
? allAgents
|
|
1386
|
+
.filter((a) => {
|
|
1387
|
+
const stageDef = config.factoryOrchestration.stageCatalog[stageId];
|
|
1388
|
+
return stageDef ? agentMatchesSelectors(a, stageDef.selectors) : false;
|
|
1389
|
+
})
|
|
1390
|
+
.map((a) => a.name.toLowerCase())
|
|
1391
|
+
: [];
|
|
1392
|
+
const preferred = preferredAgentsForContract(contract, allAgents);
|
|
1393
|
+
const candidates = Array.from(new Set([task.assignedAgent.toLowerCase(), ...stagePreferred, ...preferred].filter(Boolean)));
|
|
1394
|
+
let selected = task.assignedAgent.toLowerCase();
|
|
1395
|
+
let reason = "default assigned agent";
|
|
1396
|
+
if (task.retryCount >= 7) {
|
|
1397
|
+
const unseenCandidate = candidates.find((candidate) => (attemptsByAgent[candidate] || 0) === 0 && getAgent(candidate));
|
|
1398
|
+
if (unseenCandidate) {
|
|
1399
|
+
selected = unseenCandidate;
|
|
1400
|
+
reason = `rerouted after repeated retries (${task.retryCount})`;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
if (task.retryCount >= 9) {
|
|
1404
|
+
const sortedCandidates = candidates
|
|
1405
|
+
.filter((candidate) => getAgent(candidate))
|
|
1406
|
+
.sort((a, b) => (attemptsByAgent[a] || 0) - (attemptsByAgent[b] || 0));
|
|
1407
|
+
if (sortedCandidates.length > 0) {
|
|
1408
|
+
selected = sortedCandidates[0];
|
|
1409
|
+
reason = `lowest-attempt candidate at high retry (${task.retryCount})`;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
return {
|
|
1413
|
+
agentName: selected,
|
|
1414
|
+
forceFallbackModel: task.retryCount >= 9,
|
|
1415
|
+
reason,
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
async function executeTask(task, allTasks) {
|
|
1419
|
+
const project = getFactoryProject(task.projectId);
|
|
1420
|
+
const workspacePath = project?.workspacePath || null;
|
|
1421
|
+
setTaskStarted.run(task.id);
|
|
1422
|
+
const context = buildContextFromDeps(task, allTasks);
|
|
1423
|
+
const mergedContext = { ...task.contextPayload, ...context };
|
|
1424
|
+
const contract = resolveTaskArchitectureContract(task, mergedContext);
|
|
1425
|
+
if (contract) {
|
|
1426
|
+
mergedContext.architecture_contract = contract;
|
|
1427
|
+
}
|
|
1428
|
+
const latestReviewFeedback = extractLatestReviewFeedback(mergedContext);
|
|
1429
|
+
const agentChoice = getExecutionAgent(task, mergedContext, contract);
|
|
1430
|
+
const chosenAgent = getAgent(agentChoice.agentName) || getAgent(task.assignedAgent);
|
|
1431
|
+
if (!chosenAgent) {
|
|
1432
|
+
console.error(`🏭 Agent "${task.assignedAgent}" not found for task ${task.id}`);
|
|
1433
|
+
updateTaskResult.run("failed", `Agent "${task.assignedAgent}" not found`, JSON.stringify(mergedContext), task.id);
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
const attemptsByAgent = getAttemptsByAgent(mergedContext);
|
|
1437
|
+
attemptsByAgent[chosenAgent.name] = (attemptsByAgent[chosenAgent.name] || 0) + 1;
|
|
1438
|
+
mergedContext.attempts_by_agent = attemptsByAgent;
|
|
1439
|
+
mergedContext.last_execution_agent = chosenAgent.name;
|
|
1440
|
+
mergedContext.last_execution_reason = agentChoice.reason;
|
|
1441
|
+
const agentDef = agentChoice.forceFallbackModel && chosenAgent.fallbackModel
|
|
1442
|
+
? { ...chosenAgent, model: chosenAgent.fallbackModel, fallbackModel: undefined }
|
|
1443
|
+
: chosenAgent;
|
|
1444
|
+
const needsCodeOutput = /\b(implement|build|scaffold|frontend|backend|api|ui|dom|component|code|create)\b/i.test(`${task.title} ${task.description}`) && !/\b(specification|diagram|flow|review|audit|document)\b/i.test(`${task.title} ${task.description}`);
|
|
1445
|
+
const codeDeliveryGuard = needsCodeOutput
|
|
1446
|
+
? "\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
|
+
: "";
|
|
1448
|
+
const nudgeHint = buildNudgeHint(task);
|
|
1449
|
+
const reviewRequirements = latestReviewFeedback
|
|
1450
|
+
? `\n\n## Reviewer findings to resolve\n- Reason: ${latestReviewFeedback.reason}\n- Problem: ${latestReviewFeedback.problem || latestReviewFeedback.reason}\n- Fix required: ${latestReviewFeedback.fix || "Resolve all identified issues."}\n\n## Retry acceptance checklist\n- Address every review item above explicitly\n- State which files were changed and why\n- Confirm the output now satisfies task requirements`
|
|
1451
|
+
: null;
|
|
1452
|
+
const contractGuard = contract
|
|
1453
|
+
? `\n\n## Architecture contract (must be honored)\n${formatArchitectureContract(contract)}`
|
|
1454
|
+
: "";
|
|
1455
|
+
const taskPrompt = `${task.description}${codeDeliveryGuard}${contractGuard}${reviewRequirements || ""}${nudgeHint}`;
|
|
1456
|
+
console.log(`🏭 Running task ${task.id}: "${task.title}" → ${chosenAgent.name}`);
|
|
1457
|
+
try {
|
|
1458
|
+
const run = await runFactoryAgent(agentDef, taskPrompt, mergedContext, TASK_AGENT_MAX_ITERATIONS, workspacePath);
|
|
1459
|
+
if (needsCodeOutput && run.fileWriteCalls === 0) {
|
|
1460
|
+
const newRetryCount = task.retryCount + 1;
|
|
1461
|
+
const bounce = buildBounceFeedback("system", chosenAgent.name, "Agent provided no code changes", newRetryCount, "This task requires implementation, but no file_write/file_create operations were executed.", "Create/modify the required project files in the workspace and include concrete code output, not only descriptive text.");
|
|
1462
|
+
const newContext = {
|
|
1463
|
+
...mergedContext,
|
|
1464
|
+
latest_review_feedback: bounce,
|
|
1465
|
+
[`review_feedback_attempt_${newRetryCount}`]: bounce,
|
|
1466
|
+
};
|
|
1467
|
+
if (newRetryCount >= task.maxRetries) {
|
|
1468
|
+
updateTaskResult.run("human_intervention", run.content, JSON.stringify(newContext), task.id);
|
|
1469
|
+
console.log(`🏭 Task ${task.id} produced no code and hit max retries (${newRetryCount}) → human_intervention`);
|
|
1470
|
+
if (project) {
|
|
1471
|
+
await notifyHumanIntervention(task, project);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
else {
|
|
1475
|
+
updateTaskRetry.run("backlog", newRetryCount, JSON.stringify(newContext), task.id);
|
|
1476
|
+
console.log(`🏭 Task ${task.id} produced no code → retry ${newRetryCount}/${task.maxRetries}`);
|
|
1477
|
+
}
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
const contractValidation = validateWorkspaceAgainstContract(workspacePath, contract);
|
|
1481
|
+
if (!contractValidation.ok) {
|
|
1482
|
+
const newRetryCount = task.retryCount + 1;
|
|
1483
|
+
const problem = contractValidation.violations.join("\n");
|
|
1484
|
+
const bounce = buildBounceFeedback("system", chosenAgent.name, "Output violates architecture contract", newRetryCount, problem, "Bring workspace artifacts in line with the architecture contract before resubmitting.");
|
|
1485
|
+
const newContext = {
|
|
1486
|
+
...mergedContext,
|
|
1487
|
+
latest_review_feedback: bounce,
|
|
1488
|
+
[`review_feedback_attempt_${newRetryCount}`]: bounce,
|
|
1489
|
+
};
|
|
1490
|
+
if (newRetryCount >= task.maxRetries) {
|
|
1491
|
+
updateTaskResult.run("human_intervention", run.content, JSON.stringify(newContext), task.id);
|
|
1492
|
+
console.log(`🏭 Task ${task.id} hit max retries (${newRetryCount}) after contract validation failure → human_intervention`);
|
|
1493
|
+
if (project) {
|
|
1494
|
+
await notifyHumanIntervention(task, project);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
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
|
+
}
|
|
1503
|
+
updateTaskResult.run("review", run.content, JSON.stringify(mergedContext), task.id);
|
|
1504
|
+
console.log(`🏭 Task ${task.id} completed → review`);
|
|
1505
|
+
}
|
|
1506
|
+
catch (err) {
|
|
1507
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1508
|
+
console.error(`🏭 Task ${task.id} failed: ${errMsg}`);
|
|
1509
|
+
updateTaskResult.run("failed", errMsg, JSON.stringify(mergedContext), task.id);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
function buildBounceFeedback(from, to, reason, retryCount, problem, fix) {
|
|
1513
|
+
return {
|
|
1514
|
+
status: "rejected",
|
|
1515
|
+
from,
|
|
1516
|
+
to,
|
|
1517
|
+
reason,
|
|
1518
|
+
retry_count: retryCount,
|
|
1519
|
+
problem,
|
|
1520
|
+
fix,
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
function buildNudgeHint(task) {
|
|
1524
|
+
if (task.retryCount < 2)
|
|
1525
|
+
return "";
|
|
1526
|
+
const previousFeedback = [];
|
|
1527
|
+
for (const [key, val] of Object.entries(task.contextPayload)) {
|
|
1528
|
+
if (key.startsWith("review_feedback_attempt_") && typeof val === "object" && val !== null) {
|
|
1529
|
+
const bounce = val;
|
|
1530
|
+
if (bounce.reason)
|
|
1531
|
+
previousFeedback.push(`- Attempt ${bounce.retry_count}: ${bounce.reason}`);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
const hint = previousFeedback.length > 0
|
|
1535
|
+
? `\n\nPrevious review feedback:\n${previousFeedback.join("\n")}`
|
|
1536
|
+
: "";
|
|
1537
|
+
return `\n\n## Nudge\nYou have tried to complete this task ${task.retryCount} times now.${hint}\nConsider a different approach. Check skills and documentation for inspiration.`;
|
|
1538
|
+
}
|
|
1539
|
+
async function reviewTask(task) {
|
|
1540
|
+
if (!task.reviewerAgent) {
|
|
1541
|
+
updateTaskStatus.run("done", task.id);
|
|
1542
|
+
console.log(`🏭 Task ${task.id} auto-approved (no reviewer) → done`);
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
const reviewerDef = getAgent(task.reviewerAgent);
|
|
1546
|
+
if (!reviewerDef) {
|
|
1547
|
+
updateTaskStatus.run("done", task.id);
|
|
1548
|
+
console.log(`🏭 Task ${task.id} auto-approved (reviewer not found) → done`);
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
console.log(`🏭 Reviewing task ${task.id}: "${task.title}" → ${task.reviewerAgent}`);
|
|
1552
|
+
const project = getFactoryProject(task.projectId);
|
|
1553
|
+
const workspacePath = project?.workspacePath || null;
|
|
1554
|
+
const contract = resolveTaskArchitectureContract(task, task.contextPayload);
|
|
1555
|
+
const contractReviewBlock = contract
|
|
1556
|
+
? `\n\nArchitecture contract (must be enforced):\n${formatArchitectureContract(contract)}\n\nIf the output violates this contract, you MUST return FAIL.`
|
|
1557
|
+
: "";
|
|
1558
|
+
const reviewPrompt = `You are reviewing the output of agent "${task.assignedAgent}" for task "${task.title}".
|
|
1559
|
+
|
|
1560
|
+
Original task description: ${task.description}
|
|
1561
|
+
|
|
1562
|
+
Agent output:
|
|
1563
|
+
${task.result || "(no output)"}
|
|
1564
|
+
|
|
1565
|
+
Evaluate the output:
|
|
1566
|
+
- Does it fulfill the task description?
|
|
1567
|
+
- Are there any errors or issues?
|
|
1568
|
+
- Is the quality acceptable?
|
|
1569
|
+
${contract ? "- Does it comply with the architecture contract?" : ""}
|
|
1570
|
+
|
|
1571
|
+
Reply with EXACTLY one of these formats:
|
|
1572
|
+
|
|
1573
|
+
PASS: <reasoning>
|
|
1574
|
+
or
|
|
1575
|
+
FAIL: <short summary>
|
|
1576
|
+
PROBLEM: <what is wrong, concrete and specific>
|
|
1577
|
+
FIX: <clear actions the next agent attempt must take>
|
|
1578
|
+
|
|
1579
|
+
The first non-whitespace text in your response must be PASS: or FAIL:.
|
|
1580
|
+
|
|
1581
|
+
If FAIL, make PROBLEM and FIX specific enough that another agent can implement the fix without guessing.${contractReviewBlock}`;
|
|
1582
|
+
const parseReviewVerdict = (raw) => {
|
|
1583
|
+
const trimmed = raw.trim();
|
|
1584
|
+
if (!trimmed)
|
|
1585
|
+
return "unclear";
|
|
1586
|
+
const upper = trimmed.toUpperCase();
|
|
1587
|
+
if (upper.startsWith("PASS:"))
|
|
1588
|
+
return "pass";
|
|
1589
|
+
if (upper.startsWith("FAIL:"))
|
|
1590
|
+
return "fail";
|
|
1591
|
+
if (/^\s*PASS\s*:/im.test(raw))
|
|
1592
|
+
return "pass";
|
|
1593
|
+
if (/^\s*FAIL\s*:/im.test(raw))
|
|
1594
|
+
return "fail";
|
|
1595
|
+
if (/^\s*PROBLEM\s*:/im.test(raw) || /^\s*FIX\s*:/im.test(raw))
|
|
1596
|
+
return "fail";
|
|
1597
|
+
const passMarks = (raw.match(/✅/g) || []).length;
|
|
1598
|
+
const failMarks = (raw.match(/❌|FAIL\b/gi) || []).length;
|
|
1599
|
+
if (passMarks >= 2 && failMarks === 0)
|
|
1600
|
+
return "pass";
|
|
1601
|
+
return "unclear";
|
|
1602
|
+
};
|
|
1603
|
+
try {
|
|
1604
|
+
const reviewRun = await runFactoryAgent(reviewerDef, reviewPrompt, {}, REVIEW_AGENT_MAX_ITERATIONS, workspacePath);
|
|
1605
|
+
const reviewResult = reviewRun.content;
|
|
1606
|
+
const verdict = parseReviewVerdict(reviewResult);
|
|
1607
|
+
if (verdict === "pass") {
|
|
1608
|
+
updateTaskStatus.run("done", task.id);
|
|
1609
|
+
console.log(`🏭 Task ${task.id} PASSED review → done`);
|
|
1610
|
+
}
|
|
1611
|
+
else if (verdict === "fail") {
|
|
1612
|
+
const newRetryCount = task.retryCount + 1;
|
|
1613
|
+
const normalizedFeedback = /^\s*FAIL\s*:/im.test(reviewResult)
|
|
1614
|
+
? reviewResult
|
|
1615
|
+
: `FAIL: Review identified issues.\n\n${reviewResult}`;
|
|
1616
|
+
const bounce = buildBounceFeedback(task.reviewerAgent, task.assignedAgent, normalizedFeedback.split("\n").find((l) => l.startsWith("FAIL:"))?.replace(/^FAIL:\s*/, "") || "Review identified issues", newRetryCount, normalizedFeedback, normalizedFeedback);
|
|
1617
|
+
const newContext = {
|
|
1618
|
+
...task.contextPayload,
|
|
1619
|
+
latest_review_feedback: bounce,
|
|
1620
|
+
[`review_feedback_attempt_${newRetryCount}`]: bounce,
|
|
1621
|
+
};
|
|
1622
|
+
if (newRetryCount >= task.maxRetries) {
|
|
1623
|
+
updateTaskResult.run("human_intervention", task.result, JSON.stringify(newContext), task.id);
|
|
1624
|
+
console.log(`🏭 Task ${task.id} hit max retries (${newRetryCount}) → human_intervention`);
|
|
1625
|
+
const project = getFactoryProject(task.projectId);
|
|
1626
|
+
if (project) {
|
|
1627
|
+
await notifyHumanIntervention(task, project);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
else {
|
|
1631
|
+
updateTaskRetry.run("backlog", newRetryCount, JSON.stringify(newContext), task.id);
|
|
1632
|
+
console.log(`🏭 Task ${task.id} FAILED review → retry ${newRetryCount}/${task.maxRetries}`);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
else {
|
|
1636
|
+
const newRetryCount = task.retryCount + 1;
|
|
1637
|
+
const bounce = buildBounceFeedback(task.reviewerAgent, task.assignedAgent, "Review response format was unclear", newRetryCount, "Reviewer did not provide a valid PASS/FAIL response with actionable fixes.", "Rework the task output against the original requirements and provide explicit rationale for each requirement fulfilled.");
|
|
1638
|
+
const unclearFeedback = {
|
|
1639
|
+
...bounce,
|
|
1640
|
+
raw_review: reviewResult,
|
|
1641
|
+
};
|
|
1642
|
+
const newContext = {
|
|
1643
|
+
...task.contextPayload,
|
|
1644
|
+
latest_review_feedback: unclearFeedback,
|
|
1645
|
+
[`review_feedback_attempt_${newRetryCount}`]: unclearFeedback,
|
|
1646
|
+
};
|
|
1647
|
+
if (newRetryCount >= task.maxRetries) {
|
|
1648
|
+
updateTaskResult.run("human_intervention", task.result, JSON.stringify(newContext), task.id);
|
|
1649
|
+
console.log(`🏭 Task ${task.id} hit max retries (${newRetryCount}) after unclear review → human_intervention`);
|
|
1650
|
+
const project = getFactoryProject(task.projectId);
|
|
1651
|
+
if (project) {
|
|
1652
|
+
await notifyHumanIntervention(task, project);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
else {
|
|
1656
|
+
updateTaskRetry.run("backlog", newRetryCount, JSON.stringify(newContext), task.id);
|
|
1657
|
+
console.log(`🏭 Task ${task.id} review unclear → retry ${newRetryCount}/${task.maxRetries}`);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
catch (err) {
|
|
1662
|
+
console.error(`🏭 Review failed for task ${task.id}:`, err);
|
|
1663
|
+
const newRetryCount = task.retryCount + 1;
|
|
1664
|
+
const bounce = buildBounceFeedback(task.reviewerAgent, task.assignedAgent, "Reviewer execution failed", newRetryCount, "Review step crashed before validating the output.", "Re-validate task output against requirements and ensure all acceptance points are covered.");
|
|
1665
|
+
const newContext = {
|
|
1666
|
+
...task.contextPayload,
|
|
1667
|
+
latest_review_feedback: bounce,
|
|
1668
|
+
[`review_feedback_attempt_${newRetryCount}`]: bounce,
|
|
1669
|
+
};
|
|
1670
|
+
if (newRetryCount >= task.maxRetries) {
|
|
1671
|
+
updateTaskResult.run("human_intervention", task.result, JSON.stringify(newContext), task.id);
|
|
1672
|
+
const project = getFactoryProject(task.projectId);
|
|
1673
|
+
if (project) {
|
|
1674
|
+
await notifyHumanIntervention(task, project);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
else {
|
|
1678
|
+
updateTaskRetry.run("backlog", newRetryCount, JSON.stringify(newContext), task.id);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
function normalizeProjectMaxRetries(projectId) {
|
|
1683
|
+
const result = updateProjectTaskMaxRetries.run(DEFAULT_MAX_RETRIES, projectId);
|
|
1684
|
+
if (result.changes > 0) {
|
|
1685
|
+
autoNormalizedProjects.add(projectId);
|
|
1686
|
+
updateProjectUpdatedAt.run(projectId);
|
|
1687
|
+
console.log(`🏭 Normalized invalid max retries to ${DEFAULT_MAX_RETRIES} for project ${projectId} (${result.changes} tasks)`);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
function normalizeProjectWorkspace(row) {
|
|
1691
|
+
if (row.workspace_path) {
|
|
1692
|
+
ensureWorkspaceDir(row.workspace_path);
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
const workspacePath = buildDefaultWorkspacePath(row.id, row.title || "project");
|
|
1696
|
+
ensureWorkspaceDir(workspacePath);
|
|
1697
|
+
updateProjectWorkspacePath.run(workspacePath, row.id);
|
|
1698
|
+
autoNormalizedProjects.add(row.id);
|
|
1699
|
+
console.log(`🏭 Assigned workspace for project ${row.id}: ${workspacePath}`);
|
|
1700
|
+
}
|
|
1701
|
+
function collectFiles(rootDir, maxFiles = 400) {
|
|
1702
|
+
const files = [];
|
|
1703
|
+
function walk(current) {
|
|
1704
|
+
if (files.length >= maxFiles)
|
|
1705
|
+
return;
|
|
1706
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
1707
|
+
for (const entry of entries) {
|
|
1708
|
+
if (files.length >= maxFiles)
|
|
1709
|
+
return;
|
|
1710
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") {
|
|
1711
|
+
continue;
|
|
1712
|
+
}
|
|
1713
|
+
const abs = path.join(current, entry.name);
|
|
1714
|
+
if (entry.isDirectory()) {
|
|
1715
|
+
walk(abs);
|
|
1716
|
+
}
|
|
1717
|
+
else {
|
|
1718
|
+
files.push(abs);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
walk(rootDir);
|
|
1723
|
+
return files;
|
|
1724
|
+
}
|
|
1725
|
+
function findLikelyAppRoot(workspaceAbs) {
|
|
1726
|
+
const rootPkg = path.join(workspaceAbs, "package.json");
|
|
1727
|
+
if (fs.existsSync(rootPkg))
|
|
1728
|
+
return workspaceAbs;
|
|
1729
|
+
const allFiles = collectFiles(workspaceAbs, 500);
|
|
1730
|
+
const pkgFiles = allFiles.filter((f) => path.basename(f) === "package.json");
|
|
1731
|
+
if (pkgFiles.length === 0)
|
|
1732
|
+
return workspaceAbs;
|
|
1733
|
+
pkgFiles.sort((a, b) => a.length - b.length);
|
|
1734
|
+
return path.dirname(pkgFiles[0]);
|
|
1735
|
+
}
|
|
1736
|
+
function detectTechStack(appRootAbs) {
|
|
1737
|
+
const stack = new Set();
|
|
1738
|
+
const scripts = {};
|
|
1739
|
+
const packageJsonPath = path.join(appRootAbs, "package.json");
|
|
1740
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
1741
|
+
try {
|
|
1742
|
+
const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
1743
|
+
const deps = {
|
|
1744
|
+
...(parsed.dependencies || {}),
|
|
1745
|
+
...(parsed.devDependencies || {}),
|
|
1746
|
+
};
|
|
1747
|
+
Object.assign(scripts, parsed.scripts || {});
|
|
1748
|
+
if (deps.react)
|
|
1749
|
+
stack.add("React");
|
|
1750
|
+
if (deps.vue)
|
|
1751
|
+
stack.add("Vue");
|
|
1752
|
+
if (deps.svelte)
|
|
1753
|
+
stack.add("Svelte");
|
|
1754
|
+
if (deps.vite || fs.existsSync(path.join(appRootAbs, "vite.config.ts")) || fs.existsSync(path.join(appRootAbs, "vite.config.js"))) {
|
|
1755
|
+
stack.add("Vite");
|
|
1756
|
+
}
|
|
1757
|
+
if (deps.tailwindcss || fs.existsSync(path.join(appRootAbs, "tailwind.config.js")) || fs.existsSync(path.join(appRootAbs, "tailwind.config.ts"))) {
|
|
1758
|
+
stack.add("Tailwind CSS");
|
|
1759
|
+
}
|
|
1760
|
+
if (deps.typescript || fs.existsSync(path.join(appRootAbs, "tsconfig.json"))) {
|
|
1761
|
+
stack.add("TypeScript");
|
|
1762
|
+
}
|
|
1763
|
+
else {
|
|
1764
|
+
stack.add("JavaScript");
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
catch {
|
|
1768
|
+
stack.add("JavaScript");
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
else {
|
|
1772
|
+
stack.add("Static HTML/CSS/JS");
|
|
1773
|
+
}
|
|
1774
|
+
return {
|
|
1775
|
+
stack: [...stack],
|
|
1776
|
+
scripts,
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
function buildGeneratedReadme(project, tasks, appDirRel, stack, scripts) {
|
|
1780
|
+
const doneTasks = tasks.filter((t) => t.status === "done");
|
|
1781
|
+
const stackLines = stack.length > 0 ? stack.map((s) => `- ${s}`).join("\n") : "- Not automatically detected";
|
|
1782
|
+
const installCmd = scripts.install ? "npm run install" : "npm install";
|
|
1783
|
+
const devCmd = scripts.dev ? "npm run dev" : "npm run start";
|
|
1784
|
+
const buildCmd = scripts.build ? "npm run build" : "npm run build # if available";
|
|
1785
|
+
const testCmd = scripts.test ? "npm run test" : "npm test # if available";
|
|
1786
|
+
const appDirNote = appDirRel === "."
|
|
1787
|
+
? "Run commands from the project workspace root."
|
|
1788
|
+
: `Run commands from: \`${appDirRel}\``;
|
|
1789
|
+
return `# ${project.title}
|
|
1790
|
+
|
|
1791
|
+
<!-- generated-by-cf-factory -->
|
|
1792
|
+
|
|
1793
|
+
## Overview
|
|
1794
|
+
|
|
1795
|
+
This project was generated by CF Claw Factory and is currently marked as completed.
|
|
1796
|
+
|
|
1797
|
+
## Tech Stack
|
|
1798
|
+
|
|
1799
|
+
${stackLines}
|
|
1800
|
+
|
|
1801
|
+
## Completed Scope
|
|
1802
|
+
|
|
1803
|
+
${doneTasks.map((t) => `- ${t.title}`).join("\n") || "- No completed tasks recorded"}
|
|
1804
|
+
|
|
1805
|
+
## Getting Started
|
|
1806
|
+
|
|
1807
|
+
${appDirNote}
|
|
1808
|
+
|
|
1809
|
+
1. Install dependencies:
|
|
1810
|
+
|
|
1811
|
+
\`\`\`bash
|
|
1812
|
+
${installCmd}
|
|
1813
|
+
\`\`\`
|
|
1814
|
+
|
|
1815
|
+
2. Start development server:
|
|
1816
|
+
|
|
1817
|
+
\`\`\`bash
|
|
1818
|
+
${devCmd}
|
|
1819
|
+
\`\`\`
|
|
1820
|
+
|
|
1821
|
+
3. Build for production:
|
|
1822
|
+
|
|
1823
|
+
\`\`\`bash
|
|
1824
|
+
${buildCmd}
|
|
1825
|
+
\`\`\`
|
|
1826
|
+
|
|
1827
|
+
4. Run tests:
|
|
1828
|
+
|
|
1829
|
+
\`\`\`bash
|
|
1830
|
+
${testCmd}
|
|
1831
|
+
\`\`\`
|
|
1832
|
+
|
|
1833
|
+
## Notes
|
|
1834
|
+
|
|
1835
|
+
- This README was generated automatically when the Factory project reached completion.
|
|
1836
|
+
- Review task outputs in Factory history if you want a detailed implementation log.
|
|
1837
|
+
`;
|
|
1838
|
+
}
|
|
1839
|
+
function finalizeCompletedProject(projectId) {
|
|
1840
|
+
const project = getFactoryProject(projectId);
|
|
1841
|
+
if (!project)
|
|
1842
|
+
return;
|
|
1843
|
+
const tasks = getFactoryTasks(projectId);
|
|
1844
|
+
if (tasks.length === 0)
|
|
1845
|
+
return;
|
|
1846
|
+
const workspaceAbs = absoluteWorkspacePath(project.workspacePath);
|
|
1847
|
+
ensureWorkspaceDir(project.workspacePath);
|
|
1848
|
+
const readmePath = path.join(workspaceAbs, "README.md");
|
|
1849
|
+
const existingReadme = fs.existsSync(readmePath)
|
|
1850
|
+
? fs.readFileSync(readmePath, "utf-8")
|
|
1851
|
+
: "";
|
|
1852
|
+
if (existingReadme && !existingReadme.includes("generated-by-cf-factory")) {
|
|
1853
|
+
finalizedProjects.add(projectId);
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
const appRootAbs = findLikelyAppRoot(workspaceAbs);
|
|
1857
|
+
const appDirRel = path.relative(workspaceAbs, appRootAbs) || ".";
|
|
1858
|
+
const { stack, scripts } = detectTechStack(appRootAbs);
|
|
1859
|
+
const readme = buildGeneratedReadme(project, tasks, appDirRel, stack, scripts);
|
|
1860
|
+
fs.writeFileSync(readmePath, readme, "utf-8");
|
|
1861
|
+
finalizedProjects.add(projectId);
|
|
1862
|
+
updateProjectUpdatedAt.run(projectId);
|
|
1863
|
+
console.log(`🏭 Project ${projectId} finalized → README generated at ${project.workspacePath}/README.md`);
|
|
1864
|
+
}
|
|
1865
|
+
async function recoverStaleInProgressTasks() {
|
|
1866
|
+
const threshold = `-${STALE_IN_PROGRESS_MINUTES} minutes`;
|
|
1867
|
+
const staleRows = getStaleInProgressTasks.all(threshold);
|
|
1868
|
+
if (staleRows.length === 0)
|
|
1869
|
+
return;
|
|
1870
|
+
for (const row of staleRows) {
|
|
1871
|
+
const task = rowToTask(row);
|
|
1872
|
+
const project = getFactoryProject(task.projectId);
|
|
1873
|
+
if (!project || project.status !== "running")
|
|
1874
|
+
continue;
|
|
1875
|
+
const newRetryCount = task.retryCount + 1;
|
|
1876
|
+
const recoveryNote = `Factory recovered stale in_progress task after ${STALE_IN_PROGRESS_MINUTES}+ minutes without completion.` +
|
|
1877
|
+
(task.startedAt ? ` Last started at ${task.startedAt}.` : "");
|
|
1878
|
+
const newContext = {
|
|
1879
|
+
...task.contextPayload,
|
|
1880
|
+
[`system_recovery_attempt_${newRetryCount}`]: recoveryNote,
|
|
1881
|
+
};
|
|
1882
|
+
if (newRetryCount >= task.maxRetries) {
|
|
1883
|
+
updateTaskResult.run("human_intervention", task.result, JSON.stringify(newContext), task.id);
|
|
1884
|
+
console.log(`🏭 Task ${task.id} recovered from stale in_progress but hit max retries (${newRetryCount}) → human_intervention`);
|
|
1885
|
+
await notifyHumanIntervention(task, project);
|
|
1886
|
+
continue;
|
|
1887
|
+
}
|
|
1888
|
+
updateTaskRetry.run("backlog", newRetryCount, JSON.stringify(newContext), task.id);
|
|
1889
|
+
console.log(`🏭 Recovered stale in_progress task ${task.id} → backlog (${newRetryCount}/${task.maxRetries})`);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
export async function tickFactory() {
|
|
1893
|
+
const projectRows = getAllProjects.all();
|
|
1894
|
+
const runningProjectIds = [];
|
|
1895
|
+
for (const row of projectRows) {
|
|
1896
|
+
if (row.status === "running") {
|
|
1897
|
+
runningProjectIds.push(row.id);
|
|
1898
|
+
normalizeProjectWorkspace(row);
|
|
1899
|
+
normalizeLegacyProjectDependencies(row.id);
|
|
1900
|
+
normalizeProjectMaxRetries(row.id);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
await recoverStaleInProgressTasks();
|
|
1904
|
+
const reviewRows = getReviewTasks.all();
|
|
1905
|
+
for (const row of reviewRows) {
|
|
1906
|
+
try {
|
|
1907
|
+
await reviewTask(rowToTask(row));
|
|
1908
|
+
}
|
|
1909
|
+
catch (err) {
|
|
1910
|
+
console.error("Factory review error:", err);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
for (const projectId of runningProjectIds) {
|
|
1914
|
+
checkProjectCompletion(projectId);
|
|
1915
|
+
}
|
|
1916
|
+
if (activeRuns >= MAX_CONCURRENT)
|
|
1917
|
+
return;
|
|
1918
|
+
const backlogRows = getBacklogTasks.all();
|
|
1919
|
+
const allBacklogTasks = backlogRows.map(rowToTask);
|
|
1920
|
+
const readyTasks = [];
|
|
1921
|
+
for (const task of allBacklogTasks) {
|
|
1922
|
+
const projectTasks = getTasksByProject.all(task.projectId).map(rowToTask);
|
|
1923
|
+
if (areDependenciesMet(task, projectTasks)) {
|
|
1924
|
+
const project = getFactoryProject(task.projectId);
|
|
1925
|
+
if (project && project.status === "running") {
|
|
1926
|
+
readyTasks.push(task);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
const toRun = readyTasks.slice(0, MAX_CONCURRENT - activeRuns);
|
|
1931
|
+
if (toRun.length === 0)
|
|
1932
|
+
return;
|
|
1933
|
+
const promises = toRun.map(async (task) => {
|
|
1934
|
+
activeRuns++;
|
|
1935
|
+
try {
|
|
1936
|
+
const projectTasks = getTasksByProject.all(task.projectId).map(rowToTask);
|
|
1937
|
+
await executeTask(task, projectTasks);
|
|
1938
|
+
}
|
|
1939
|
+
catch (err) {
|
|
1940
|
+
console.error(`Factory task ${task.id} execution error:`, err);
|
|
1941
|
+
}
|
|
1942
|
+
finally {
|
|
1943
|
+
activeRuns--;
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
await Promise.all(promises);
|
|
1947
|
+
const projectIds = new Set(toRun.map((t) => t.projectId));
|
|
1948
|
+
for (const projectId of projectIds) {
|
|
1949
|
+
checkProjectCompletion(projectId);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
function checkProjectCompletion(projectId) {
|
|
1953
|
+
if (finalizedProjects.has(projectId)) {
|
|
1954
|
+
const project = getFactoryProject(projectId);
|
|
1955
|
+
if (project?.status === "running") {
|
|
1956
|
+
finalizedProjects.delete(projectId);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
const counts = getTaskCounts.get(projectId);
|
|
1960
|
+
if (!counts || counts.total === 0)
|
|
1961
|
+
return;
|
|
1962
|
+
const taskRows = getTasksByProject.all(projectId);
|
|
1963
|
+
const hasBlockingTasks = taskRows.some((r) => r.status === "human_intervention" || r.status === "failed");
|
|
1964
|
+
if (hasBlockingTasks)
|
|
1965
|
+
return;
|
|
1966
|
+
if (counts.completed >= counts.total) {
|
|
1967
|
+
const currentRow = getProjectById.get(projectId);
|
|
1968
|
+
if (!currentRow || currentRow.status !== "completed") {
|
|
1969
|
+
updateProjectStatus.run("completed", projectId);
|
|
1970
|
+
console.log(`🏭 Project ${projectId} completed!`);
|
|
1971
|
+
}
|
|
1972
|
+
if (!finalizedProjects.has(projectId)) {
|
|
1973
|
+
finalizeCompletedProject(projectId);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
else {
|
|
1977
|
+
const progress = Math.round((counts.completed / counts.total) * 100);
|
|
1978
|
+
const currentRow = getProjectById.get(projectId);
|
|
1979
|
+
if (currentRow && currentRow.status === "running") {
|
|
1980
|
+
console.log(`🏭 Project ${projectId} progress: ${progress}% (${counts.completed}/${counts.total})`);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
let factoryInterval = null;
|
|
1985
|
+
const TICK_INTERVAL_MS = 30_000;
|
|
1986
|
+
export function startFactory() {
|
|
1987
|
+
if (factoryInterval)
|
|
1988
|
+
return;
|
|
1989
|
+
console.log("🏭 Factory orchestrator started (tick every 30s)");
|
|
1990
|
+
tickFactory().catch((err) => {
|
|
1991
|
+
console.error("Factory initial tick error:", err);
|
|
1992
|
+
});
|
|
1993
|
+
factoryInterval = setInterval(async () => {
|
|
1994
|
+
try {
|
|
1995
|
+
await tickFactory();
|
|
1996
|
+
}
|
|
1997
|
+
catch (err) {
|
|
1998
|
+
console.error("Factory tick error:", err);
|
|
1999
|
+
}
|
|
2000
|
+
}, TICK_INTERVAL_MS);
|
|
2001
|
+
}
|
|
2002
|
+
export function stopFactory() {
|
|
2003
|
+
if (factoryInterval) {
|
|
2004
|
+
clearInterval(factoryInterval);
|
|
2005
|
+
factoryInterval = null;
|
|
2006
|
+
console.log("🏭 Factory orchestrator stopped");
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
process.on("exit", () => db.close());
|
|
2010
|
+
//# sourceMappingURL=factory.js.map
|