factory-ai 1.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.
Files changed (84) hide show
  1. package/AGENTS.md +3 -0
  2. package/ARCHITECTURE.md +9 -0
  3. package/CHANGELOG.md +21 -0
  4. package/CODE_OF_CONDUCT.md +7 -0
  5. package/CONTRIBUTING.md +17 -0
  6. package/Dockerfile.worker +19 -0
  7. package/GOVERNANCE.md +21 -0
  8. package/HANDOFF.md +23 -0
  9. package/LICENSE +21 -0
  10. package/README.md +244 -0
  11. package/ROADMAP.md +23 -0
  12. package/RUNBOOK.md +42 -0
  13. package/SECURITY.md +7 -0
  14. package/SUPPORT.md +7 -0
  15. package/bin/factory +271 -0
  16. package/bootstrap/agent-factory-control.service +32 -0
  17. package/bootstrap/agent-factory-release.service +25 -0
  18. package/bootstrap/agent-factory-reporter.service +17 -0
  19. package/bootstrap/agent-factory-reporter.timer +10 -0
  20. package/bootstrap/agent-factory-telegram.service +31 -0
  21. package/bootstrap/agent-factory-worker.service +39 -0
  22. package/bootstrap/cloud-init.yaml +78 -0
  23. package/bootstrap/deploy-runtime.sh +74 -0
  24. package/bootstrap/setup.sh +124 -0
  25. package/capabilities/autonomous-loop/SKILL.md +10 -0
  26. package/capabilities/code-search.md +3 -0
  27. package/capabilities/dependency-security.md +3 -0
  28. package/capabilities/frontend-verification.md +3 -0
  29. package/capabilities/goal-management/SKILL.md +10 -0
  30. package/capabilities/project-context/SKILL.md +10 -0
  31. package/capabilities/release-checklist.md +3 -0
  32. package/capabilities/secure-review.md +3 -0
  33. package/capabilities/systematic-debugging.md +3 -0
  34. package/capabilities/test-discipline.md +3 -0
  35. package/capabilities/test-driven-development.md +3 -0
  36. package/capabilities/token-efficiency.md +3 -0
  37. package/capabilities/verification-before-completion.md +3 -0
  38. package/config/agent-instructions.md +8 -0
  39. package/config/capabilities.json +107 -0
  40. package/infra/main.bicep +310 -0
  41. package/package.json +78 -0
  42. package/src/agent-executor.js +52 -0
  43. package/src/agent-runner.js +110 -0
  44. package/src/azure-harness.js +118 -0
  45. package/src/bedrock-harness.js +52 -0
  46. package/src/bus.js +20 -0
  47. package/src/capabilities.js +43 -0
  48. package/src/ceo.js +67 -0
  49. package/src/config.js +65 -0
  50. package/src/container-runner.js +77 -0
  51. package/src/control-plane.js +142 -0
  52. package/src/control-service.js +55 -0
  53. package/src/dashboard.js +180 -0
  54. package/src/log.js +19 -0
  55. package/src/mcp-tools.js +53 -0
  56. package/src/operator.js +57 -0
  57. package/src/process.js +48 -0
  58. package/src/project-memory.js +22 -0
  59. package/src/registry.js +6 -0
  60. package/src/release-bot.js +36 -0
  61. package/src/release-gate.js +11 -0
  62. package/src/release-service.js +41 -0
  63. package/src/release.js +102 -0
  64. package/src/reporter.js +49 -0
  65. package/src/routing.js +34 -0
  66. package/src/scanner-suite.js +58 -0
  67. package/src/secrets.js +22 -0
  68. package/src/setup-menu.js +26 -0
  69. package/src/state.js +49 -0
  70. package/src/task-entry.js +19 -0
  71. package/src/task-graph.js +67 -0
  72. package/src/telegram-service.js +99 -0
  73. package/src/telegram.js +31 -0
  74. package/src/tui.js +130 -0
  75. package/src/validation.js +74 -0
  76. package/src/worker.js +77 -0
  77. package/src/workspace-tools.js +121 -0
  78. package/src/workspace.js +78 -0
  79. package/templates/project/architecture.md +3 -0
  80. package/templates/project/commands.md +3 -0
  81. package/templates/project/decisions.md +3 -0
  82. package/templates/project/handoff.md +3 -0
  83. package/templates/project/project.md +3 -0
  84. package/templates/project/risks.md +3 -0
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "factory-ai",
3
+ "version": "1.0.0",
4
+ "description": "Deploy a private autonomous coding-agent factory on Azure: isolated builders, testers, security reviewers, durable orchestration, multi-model routing, memory, cost controls, and gated GitHub pull requests.",
5
+ "private": false,
6
+ "license": "MIT",
7
+ "author": "Vedant Kumar",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/itsvedantkumar/factory-ai.git"
11
+ },
12
+ "homepage": "https://github.com/itsvedantkumar/factory-ai#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/itsvedantkumar/factory-ai/issues"
15
+ },
16
+ "keywords": [
17
+ "ai-agents",
18
+ "coding-agent",
19
+ "multi-agent",
20
+ "azure",
21
+ "bedrock",
22
+ "mcp",
23
+ "automation",
24
+ "devtools"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "type": "module",
30
+ "engines": {
31
+ "node": ">=20 <21"
32
+ },
33
+ "bin": {
34
+ "factory": "bin/factory",
35
+ "factory-ui": "src/tui.js",
36
+ "agent-factory-ceo": "src/ceo.js",
37
+ "agent-factory-control": "src/control-service.js",
38
+ "agent-factory-dashboard": "src/dashboard.js",
39
+ "agent-factory-reporter": "src/reporter.js",
40
+ "agent-factory-release": "src/release-service.js",
41
+ "agent-factory-task": "src/task-entry.js",
42
+ "factory-ai-telegram": "src/telegram-service.js",
43
+ "agent-factory-worker": "src/worker.js"
44
+ },
45
+ "files": [
46
+ "bin/",
47
+ "bootstrap/",
48
+ "capabilities/",
49
+ "config/",
50
+ "infra/",
51
+ "src/",
52
+ "templates/",
53
+ "*.md",
54
+ "Dockerfile.worker"
55
+ ],
56
+ "scripts": {
57
+ "test": "node --test",
58
+ "lint": "eslint .",
59
+ "check": "node --check src/*.js"
60
+ },
61
+ "dependencies": {
62
+ "@aws-sdk/client-bedrock-runtime": "3.1085.0",
63
+ "@azure/identity": "4.13.1",
64
+ "@azure/keyvault-secrets": "4.10.0",
65
+ "@azure/service-bus": "7.9.5",
66
+ "@inquirer/prompts": "8.5.2",
67
+ "@modelcontextprotocol/sdk": "1.29.0",
68
+ "@modelcontextprotocol/server-memory": "2026.7.4",
69
+ "@playwright/mcp": "0.0.78",
70
+ "@upstash/context7-mcp": "3.2.3",
71
+ "neo-blessed": "0.2.0",
72
+ "zod": "3.25.76"
73
+ },
74
+ "devDependencies": {
75
+ "eslint": "9.32.0",
76
+ "globals": "16.3.0"
77
+ }
78
+ }
@@ -0,0 +1,52 @@
1
+ import { parseTaskResult } from "./validation.js";
2
+
3
+ export class AgentExecutor {
4
+ constructor({ workspaces, agentRunner, scannerSuite, sendControl }) {
5
+ this.workspaces = workspaces;
6
+ this.agentRunner = agentRunner;
7
+ this.scannerSuite = scannerSuite;
8
+ this.sendControl = sendControl;
9
+ }
10
+
11
+ async process(message) {
12
+ if (message?.type === "planning_task") return this.processPlanning(message);
13
+ if (message?.type === "agent_task") return this.processTask(message);
14
+ throw new Error(`Unsupported agent message type: ${message?.type}`);
15
+ }
16
+
17
+ async processPlanning(message) {
18
+ const directory = await this.workspaces.ensureObjective(message.objective);
19
+ const delivery = await this.agentRunner.plan(message.objective, directory, message.context ?? []);
20
+ await this.sendControl({ type: "planning_result", objectiveId: message.objectiveId, delivery });
21
+ }
22
+
23
+ async processTask(message) {
24
+ const directory = await this.workspaces.prepareTask(
25
+ message.objective,
26
+ message.task,
27
+ message.dependencyCommits ?? [],
28
+ );
29
+ const scannerEvidence = message.task.role === "security" && this.scannerSuite
30
+ ? await this.scannerSuite.scan(directory)
31
+ : [];
32
+ const prompt = [
33
+ "Execute only this assigned task, verify it, and report factual outcomes.",
34
+ ...(scannerEvidence.length ? [`TRUSTED SCANNER EVIDENCE (mechanical output; do not claim checks beyond this evidence):\n${JSON.stringify(scannerEvidence)}`] : []),
35
+ ].join("\n\n");
36
+ const result = parseTaskResult(await this.agentRunner.invoke({
37
+ objective: message.objective,
38
+ task: message.task,
39
+ directory,
40
+ prompt,
41
+ }));
42
+ const checkpoint = await this.workspaces.checkpoint(directory, message.objective, message.task);
43
+ await this.sendControl({
44
+ type: "result",
45
+ objectiveId: message.objectiveId,
46
+ taskId: message.task.id,
47
+ status: "succeeded",
48
+ ...result,
49
+ ...checkpoint,
50
+ });
51
+ }
52
+ }
@@ -0,0 +1,110 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { AzureResponsesHarness } from "./azure-harness.js";
3
+ import { selectCapabilities } from "./capabilities.js";
4
+ import { modelForTask } from "./routing.js";
5
+ import { createWorkspaceTools } from "./workspace-tools.js";
6
+ import { connectMcpTools } from "./mcp-tools.js";
7
+ import { BedrockHarness } from "./bedrock-harness.js";
8
+
9
+ function parseJson(text) {
10
+ const fenced = text.match(/```json\s*([\s\S]*?)```/i)?.[1];
11
+ if (fenced) return JSON.parse(fenced);
12
+ const start = text.indexOf("{");
13
+ const end = text.lastIndexOf("}");
14
+ if (start < 0 || end <= start) throw new Error("Agent response did not contain a JSON object");
15
+ return JSON.parse(text.slice(start, end + 1));
16
+ }
17
+
18
+ function endpointForRoute(route, role, environment) {
19
+ const [provider, model] = route.split("/");
20
+ const lightweight = provider === "azureai-responses";
21
+ const baseUrl = lightweight ? environment.AZURE_OPENAI_BASE_URL : environment.TEXTVED_AZURE_BASE_URL;
22
+ const apiKey = lightweight ? environment.AZURE_OPENAI_API_KEY : environment.TEXTVED_AZURE_API_KEY;
23
+ if (!baseUrl || !apiKey) throw new Error(`Azure credentials are unavailable for ${role}`);
24
+ return { baseUrl, apiKey, model };
25
+ }
26
+
27
+ export class AzureAgentRunner {
28
+ constructor(config, registry, {
29
+ environment = process.env,
30
+ createHarness = (options) => new AzureResponsesHarness(options),
31
+ createBedrockHarness = (options) => new BedrockHarness(options),
32
+ } = {}) {
33
+ this.config = config;
34
+ this.registry = registry;
35
+ this.environment = environment;
36
+ this.createHarness = createHarness;
37
+ this.createBedrockHarness = createBedrockHarness;
38
+ }
39
+
40
+ async promptForTask(objective, task, prompt, capabilities) {
41
+ const skills = await Promise.all(capabilities.filter((item) => item.type === "skill").map(async (item) => (
42
+ `ALLOWLISTED SKILL ${item.name}@${item.version}:\n${await readFile(item.path, "utf8")}`
43
+ )));
44
+ return [
45
+ `You are the isolated ${task.role} subagent for CEO objective: ${objective.objective}`,
46
+ task.instructions,
47
+ "Work only in the assigned repository. Never inspect credentials, push Git refs, deploy, or install global tools.",
48
+ "Use tools for evidence. Make the smallest correct change and verify every completion claim.",
49
+ prompt,
50
+ ...skills,
51
+ 'Return only JSON: {"summary":"concise outcome","checks":["command/result"],"risks":["remaining risk"],"approval":"approved|changes_requested|not_applicable"}.',
52
+ ].join("\n\n");
53
+ }
54
+
55
+ harness(task, directory, additionalTools = {}) {
56
+ const role = task.role;
57
+ const route = modelForTask(task, this.environment);
58
+ const tools = { ...createWorkspaceTools(directory), ...additionalTools };
59
+ const maxSteps = role === "scout" ? 20 : 40;
60
+ if (route.startsWith("bedrock/")) {
61
+ return this.createBedrockHarness({
62
+ region: this.environment.AWS_REGION ?? "us-east-1",
63
+ model: route.slice("bedrock/".length),
64
+ tools,
65
+ maxSteps,
66
+ });
67
+ }
68
+ return this.createHarness({ ...endpointForRoute(route, role, this.environment), tools, timeoutMs: this.config.timeoutMs, maxSteps });
69
+ }
70
+
71
+ async invoke({ objective, task, directory, prompt }) {
72
+ const capabilities = selectCapabilities(this.registry, task.role, task.capabilities);
73
+ const mcp = await connectMcpTools(capabilities);
74
+ try {
75
+ const response = await this.harness(task, directory, mcp.tools).run(
76
+ await this.promptForTask(objective, task, prompt, capabilities),
77
+ );
78
+ return parseJson(response.text);
79
+ } finally {
80
+ await mcp.close();
81
+ }
82
+ }
83
+
84
+ async plan(objective, directory, projectContext = []) {
85
+ const plannerCapabilities = selectCapabilities(this.registry, "planner", []);
86
+ const plannerSkills = await Promise.all(plannerCapabilities.filter((item) => item.type === "skill").map(async (item) => (
87
+ `PLANNER SKILL ${item.name}@${item.version}:\n${await readFile(item.path, "utf8")}`
88
+ )));
89
+ const registrySummary = Object.fromEntries([
90
+ ...Object.entries(this.registry.skills ?? {}),
91
+ ...Object.entries(this.registry.mcp ?? {}),
92
+ ].map(([name, item]) => [name, { version: item.version, roles: item.roles }]));
93
+ const prompt = `You are a planner subagent. Decompose the objective into a small executable DAG.
94
+ Objective: ${objective.objective}
95
+ Verified prior project context: ${JSON.stringify(projectContext)}
96
+ Allowed roles: scout, builder, tester, debugger, reviewer, security, release.
97
+ Allowed capabilities: ${JSON.stringify(registrySummary)}
98
+ Include tester, reviewer, and security ancestors of exactly one terminal release task.
99
+ Return only JSON: {"executiveIntent":"...","tasks":[{"id":"...","role":"...","title":"...","instructions":"...","dependsOn":[],"capabilities":[],"complexity":"simple|complex"}]}
100
+
101
+ ${plannerSkills.join("\n\n")}`;
102
+ const mcp = await connectMcpTools(plannerCapabilities);
103
+ try {
104
+ const response = await this.harness({ role: "planner", complexity: "complex" }, directory, mcp.tools).run(prompt);
105
+ return parseJson(response.text);
106
+ } finally {
107
+ await mcp.close();
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,118 @@
1
+ const TRANSIENT_STATUS = new Set([408, 409, 429, 500, 502, 503, 504]);
2
+
3
+ function outputText(response) {
4
+ if (typeof response.output_text === "string") return response.output_text;
5
+ return (response.output ?? [])
6
+ .filter((item) => item.type === "message")
7
+ .flatMap((item) => item.content ?? [])
8
+ .filter((item) => item.type === "output_text")
9
+ .map((item) => item.text)
10
+ .join("\n");
11
+ }
12
+
13
+ function toolDefinitions(tools) {
14
+ return Object.entries(tools).map(([name, tool]) => ({
15
+ type: "function",
16
+ name,
17
+ description: tool.description,
18
+ parameters: tool.parameters,
19
+ }));
20
+ }
21
+
22
+ export class AzureResponsesHarness {
23
+ constructor({
24
+ baseUrl,
25
+ apiKey,
26
+ model,
27
+ tools,
28
+ fetch = globalThis.fetch,
29
+ maxSteps = 40,
30
+ retries = 4,
31
+ timeoutMs = 180_000,
32
+ sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
33
+ }) {
34
+ this.endpoint = `${baseUrl.replace(/\/$/, "")}/responses`;
35
+ this.apiKey = apiKey;
36
+ this.model = model;
37
+ this.tools = tools;
38
+ this.fetch = fetch;
39
+ this.maxSteps = maxSteps;
40
+ this.retries = retries;
41
+ this.timeoutMs = timeoutMs;
42
+ this.sleep = sleep;
43
+ }
44
+
45
+ async request(body) {
46
+ for (let attempt = 0; attempt <= this.retries; attempt += 1) {
47
+ const controller = new AbortController();
48
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
49
+ timer.unref?.();
50
+ try {
51
+ const response = await this.fetch(this.endpoint, {
52
+ method: "POST",
53
+ headers: {
54
+ "content-type": "application/json",
55
+ "api-key": this.apiKey,
56
+ authorization: `Bearer ${this.apiKey}`,
57
+ },
58
+ body: JSON.stringify(body),
59
+ signal: controller.signal,
60
+ });
61
+ const payload = await response.json().catch(() => ({}));
62
+ if (response.ok) return payload;
63
+ if (!TRANSIENT_STATUS.has(response.status) || attempt === this.retries) {
64
+ throw new Error(`Azure Responses HTTP ${response.status}: ${payload.error?.code ?? "request_failed"}`);
65
+ }
66
+ } catch (error) {
67
+ const transientNetworkError = error.name === "AbortError" || error instanceof TypeError;
68
+ if (!transientNetworkError || attempt === this.retries) throw error;
69
+ } finally {
70
+ clearTimeout(timer);
71
+ }
72
+ const delay = Math.min(10_000, 500 * (2 ** attempt)) + Math.floor(Math.random() * 250);
73
+ await this.sleep(delay);
74
+ }
75
+ throw new Error("Azure Responses retry loop exhausted");
76
+ }
77
+
78
+ async run(prompt) {
79
+ let input = prompt;
80
+ let previousResponseId;
81
+ const definitions = toolDefinitions(this.tools);
82
+ for (let step = 0; step < this.maxSteps; step += 1) {
83
+ const response = await this.request({
84
+ model: this.model,
85
+ input,
86
+ ...(previousResponseId ? { previous_response_id: previousResponseId } : {}),
87
+ tools: definitions,
88
+ });
89
+ const calls = (response.output ?? []).filter((item) => item.type === "function_call");
90
+ if (calls.length === 0) return { text: outputText(response), responseId: response.id, steps: step + 1 };
91
+ const outputs = [];
92
+ for (const call of calls) {
93
+ const tool = this.tools[call.name];
94
+ if (!tool) throw new Error(`Tool not allowed: ${call.name}`);
95
+ let argumentsValue;
96
+ try {
97
+ argumentsValue = JSON.parse(call.arguments || "{}");
98
+ } catch {
99
+ throw new Error(`Invalid arguments for tool: ${call.name}`);
100
+ }
101
+ let value;
102
+ try {
103
+ value = await tool.execute(argumentsValue);
104
+ } catch (error) {
105
+ value = `ERROR: ${String(error.message ?? error).slice(0, 4000)}`;
106
+ }
107
+ outputs.push({
108
+ type: "function_call_output",
109
+ call_id: call.call_id,
110
+ output: typeof value === "string" ? value : JSON.stringify(value),
111
+ });
112
+ }
113
+ previousResponseId = response.id;
114
+ input = outputs;
115
+ }
116
+ throw new Error(`Agent exceeded step limit of ${this.maxSteps}`);
117
+ }
118
+ }
@@ -0,0 +1,52 @@
1
+ import { BedrockRuntimeClient, ConverseCommand } from "@aws-sdk/client-bedrock-runtime";
2
+
3
+ function definitions(tools) {
4
+ return Object.entries(tools).map(([name, tool]) => ({
5
+ toolSpec: {
6
+ name,
7
+ description: tool.description,
8
+ inputSchema: { json: tool.parameters },
9
+ },
10
+ }));
11
+ }
12
+
13
+ export class BedrockHarness {
14
+ constructor({ client, region = process.env.AWS_REGION ?? "us-east-1", model, tools, maxSteps = 40 }) {
15
+ this.client = client ?? new BedrockRuntimeClient({ region });
16
+ this.model = model;
17
+ this.tools = tools;
18
+ this.maxSteps = maxSteps;
19
+ }
20
+
21
+ async run(prompt) {
22
+ const messages = [{ role: "user", content: [{ text: prompt }] }];
23
+ const usage = { inputTokens: 0, outputTokens: 0 };
24
+ for (let step = 0; step < this.maxSteps; step += 1) {
25
+ const response = await this.client.send(new ConverseCommand({
26
+ modelId: this.model,
27
+ messages,
28
+ ...(Object.keys(this.tools).length ? { toolConfig: { tools: definitions(this.tools) } } : {}),
29
+ }));
30
+ usage.inputTokens += response.usage?.inputTokens ?? 0;
31
+ usage.outputTokens += response.usage?.outputTokens ?? 0;
32
+ const message = response.output?.message;
33
+ const calls = (message?.content ?? []).flatMap((item) => item.toolUse ? [item.toolUse] : []);
34
+ if (calls.length === 0) {
35
+ const text = (message?.content ?? []).flatMap((item) => typeof item.text === "string" ? [item.text] : []).join("\n");
36
+ return { text, steps: step + 1, usage };
37
+ }
38
+ messages.push(message);
39
+ const results = [];
40
+ for (const call of calls) {
41
+ const tool = this.tools[call.name];
42
+ if (!tool) throw new Error(`Tool not allowed: ${call.name}`);
43
+ let value;
44
+ let status = "success";
45
+ try { value = await tool.execute(call.input ?? {}); } catch (error) { value = `ERROR: ${String(error.message ?? error).slice(0, 4000)}`; status = "error"; }
46
+ results.push({ toolResult: { toolUseId: call.toolUseId, content: [{ text: typeof value === "string" ? value : JSON.stringify(value) }], status } });
47
+ }
48
+ messages.push({ role: "user", content: results });
49
+ }
50
+ throw new Error(`Agent exceeded step limit of ${this.maxSteps}`);
51
+ }
52
+ }
package/src/bus.js ADDED
@@ -0,0 +1,20 @@
1
+ import { DefaultAzureCredential } from "@azure/identity";
2
+ import { ServiceBusClient } from "@azure/service-bus";
3
+
4
+ export function createBus(config, receiveQueue, sendQueue) {
5
+ const client = new ServiceBusClient(config.serviceBusFqdn, new DefaultAzureCredential());
6
+ const sender = client.createSender(sendQueue);
7
+ const receiver = client.createReceiver(receiveQueue, { receiveMode: "peekLock" });
8
+ return { client, sender, receiver };
9
+ }
10
+
11
+ export async function sendMessage(sender, body, messageId, correlationId = messageId) {
12
+ await sender.sendMessages({
13
+ body,
14
+ messageId,
15
+ correlationId,
16
+ contentType: "application/json",
17
+ subject: body.type,
18
+ applicationProperties: { schemaVersion: 1 },
19
+ });
20
+ }
@@ -0,0 +1,43 @@
1
+ function entries(registry) {
2
+ return [
3
+ ...Object.entries(registry.skills ?? {}).map(([name, definition]) => ({ name, type: "skill", ...definition })),
4
+ ...Object.entries(registry.mcp ?? {}).map(([name, definition]) => ({ name, type: "mcp", ...definition })),
5
+ ];
6
+ }
7
+
8
+ export function validateRegistry(registry) {
9
+ for (const item of entries(registry)) {
10
+ if (!item.version || !/^\d+\.\d+\.\d+$/.test(item.version)) {
11
+ throw new Error(`Capability ${item.name} is not pinned to a semantic version`);
12
+ }
13
+ if (!Array.isArray(item.roles) || item.roles.length === 0) {
14
+ throw new Error(`Capability ${item.name} has no allowed roles`);
15
+ }
16
+ if (item.type === "skill" && !item.path?.startsWith("/")) {
17
+ throw new Error(`Skill ${item.name} must use an absolute path`);
18
+ }
19
+ if (item.type === "mcp" && (!Array.isArray(item.command) || !item.command[0]?.startsWith("/"))) {
20
+ throw new Error(`MCP ${item.name} must use an absolute executable`);
21
+ }
22
+ }
23
+ for (const [role, names] of Object.entries(registry.defaults ?? {})) {
24
+ if (!Array.isArray(names)) throw new Error(`Default capabilities for ${role} must be an array`);
25
+ const available = new Map(entries(registry).map((item) => [item.name, item]));
26
+ for (const name of names) {
27
+ const item = available.get(name);
28
+ if (!item || !item.roles.includes(role)) throw new Error(`Invalid default capability ${name} for ${role}`);
29
+ }
30
+ }
31
+ return registry;
32
+ }
33
+
34
+ export function selectCapabilities(registry, role, requested) {
35
+ validateRegistry(registry);
36
+ const available = new Map(entries(registry).map((item) => [item.name, item]));
37
+ return [...new Set([...(registry.defaults?.[role] ?? []), ...requested])].map((name) => {
38
+ const item = available.get(name);
39
+ if (!item) throw new Error(`Unknown capability: ${name}`);
40
+ if (!item.roles.includes(role)) throw new Error(`Capability ${name} is not allowed for role ${role}`);
41
+ return item;
42
+ });
43
+ }
package/src/ceo.js ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ import { randomUUID } from "node:crypto";
3
+ import { readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { loadConfig } from "./config.js";
6
+ import { createBus, sendMessage } from "./bus.js";
7
+ import { parseObjective } from "./validation.js";
8
+
9
+ function argumentsFrom(argv) {
10
+ const options = { baseBranch: "main", wait: false, timeoutMs: 3_600_000, words: [] };
11
+ for (let index = 0; index < argv.length; index += 1) {
12
+ const value = argv[index];
13
+ if (value === "--repo") options.repository = argv[++index];
14
+ else if (value === "--base") options.baseBranch = argv[++index];
15
+ else if (value === "--wait") options.wait = true;
16
+ else if (value === "--timeout-ms") options.timeoutMs = Number(argv[++index]);
17
+ else if (value.startsWith("--")) throw new Error(`Unknown option: ${value}`);
18
+ else options.words.push(value);
19
+ }
20
+ if (!options.repository || options.words.length === 0) {
21
+ throw new Error('Usage: agent-factory-ceo --repo <https-url> [--base main] [--wait] "objective"');
22
+ }
23
+ if (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1000 || options.timeoutMs > 86_400_000) {
24
+ throw new Error("--timeout-ms must be between 1000 and 86400000");
25
+ }
26
+ return options;
27
+ }
28
+
29
+ async function waitForResult(file, timeoutMs) {
30
+ const deadline = Date.now() + timeoutMs;
31
+ while (Date.now() < deadline) {
32
+ try {
33
+ return JSON.parse(await readFile(file, "utf8"));
34
+ } catch (error) {
35
+ if (error.code !== "ENOENT") throw error;
36
+ }
37
+ await new Promise((resolve) => setTimeout(resolve, 2000));
38
+ }
39
+ throw new Error(`Timed out waiting for result after ${timeoutMs}ms`);
40
+ }
41
+
42
+ try {
43
+ const options = argumentsFrom(process.argv.slice(2));
44
+ const config = loadConfig();
45
+ const objective = parseObjective({
46
+ id: randomUUID(),
47
+ type: "objective",
48
+ objective: options.words.join(" "),
49
+ repository: options.repository,
50
+ baseBranch: options.baseBranch,
51
+ createdAt: new Date().toISOString(),
52
+ });
53
+ const bus = createBus(config, config.controlQueue, config.controlQueue);
54
+ await sendMessage(bus.sender, objective, objective.id);
55
+ await bus.sender.close();
56
+ await bus.receiver.close();
57
+ await bus.client.close();
58
+ if (options.wait) {
59
+ const result = await waitForResult(path.join(config.stateDir, objective.id, "result.json"), options.timeoutMs);
60
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
61
+ } else {
62
+ process.stdout.write(`${JSON.stringify({ objectiveId: objective.id, status: "queued" })}\n`);
63
+ }
64
+ } catch (error) {
65
+ process.stderr.write(`${error.message}\n`);
66
+ process.exitCode = 1;
67
+ }
package/src/config.js ADDED
@@ -0,0 +1,65 @@
1
+ import path from "node:path";
2
+ import { z } from "zod";
3
+
4
+ const environmentSchema = z.object({
5
+ SERVICE_BUS_NAMESPACE: z.string().min(3),
6
+ CONTROL_QUEUE: z.string().min(1).default("control-events"),
7
+ AGENT_QUEUE: z.string().min(1).default("agent-tasks"),
8
+ RELEASE_QUEUE: z.string().min(1).default("release-tasks"),
9
+ KEY_VAULT_NAME: z.string().min(3),
10
+ AZURE_SUBSCRIPTION_ID: z.string().optional(),
11
+ FACTORY_RESOURCE_GROUP: z.string().default("factory-ai-rg"),
12
+ FACTORY_STATE_DIR: z.string().default("/opt/agent-factory/state"),
13
+ FACTORY_WORKSPACE_DIR: z.string().default("/opt/agent-factory/workspaces"),
14
+ FACTORY_REGISTRY: z.string().default("/opt/agent-factory/app/config/capabilities.json"),
15
+ FACTORY_WORKER_IMAGE: z.string().min(1).default("agent-factory-worker:development"),
16
+ MAX_CONCURRENCY: z.coerce.number().int().min(1).max(3).default(3),
17
+ TASK_TIMEOUT_MS: z.coerce.number().int().min(60_000).max(3_600_000).default(1_800_000),
18
+ MAX_DELIVERY_COUNT: z.coerce.number().int().min(2).max(20).default(8),
19
+ AZURE_PRIMARY_API_KEY_SECRET: z.string().min(1).default("azure-primary-api-key"),
20
+ AZURE_PRIMARY_BASE_URL_SECRET: z.string().min(1).default("azure-primary-base-url"),
21
+ AZURE_SMALL_API_KEY_SECRET: z.string().min(1).default("azure-small-api-key"),
22
+ AZURE_SMALL_BASE_URL_SECRET: z.string().min(1).default("azure-small-base-url"),
23
+ GITHUB_TOKEN_SECRET: z.string().min(1).default("github-token"),
24
+ AWS_ACCESS_KEY_ID_SECRET: z.string().min(1).default("aws-access-key-id"),
25
+ AWS_SECRET_ACCESS_KEY_SECRET: z.string().min(1).default("aws-secret-access-key"),
26
+ AWS_SESSION_TOKEN_SECRET: z.string().min(1).default("aws-session-token"),
27
+ TELEGRAM_BOT_TOKEN_SECRET: z.string().min(1).default("telegram-bot-token"),
28
+ TELEGRAM_ALLOWED_CHAT_IDS_SECRET: z.string().min(1).default("telegram-allowed-chat-ids"),
29
+ }).passthrough();
30
+
31
+ export function loadConfig(environment = process.env) {
32
+ const env = environmentSchema.parse(environment);
33
+ if (env.CONTROL_QUEUE === env.AGENT_QUEUE) throw new Error("Control and agent queues must be different");
34
+ return {
35
+ serviceBusFqdn: env.SERVICE_BUS_NAMESPACE.includes(".")
36
+ ? env.SERVICE_BUS_NAMESPACE
37
+ : `${env.SERVICE_BUS_NAMESPACE}.servicebus.windows.net`,
38
+ controlQueue: env.CONTROL_QUEUE,
39
+ agentQueue: env.AGENT_QUEUE,
40
+ releaseQueue: env.RELEASE_QUEUE,
41
+ keyVaultUrl: `https://${env.KEY_VAULT_NAME}.vault.azure.net`,
42
+ subscriptionId: env.AZURE_SUBSCRIPTION_ID,
43
+ resourceGroup: env.FACTORY_RESOURCE_GROUP,
44
+ secretNames: {
45
+ TEXTVED_AZURE_API_KEY: env.AZURE_PRIMARY_API_KEY_SECRET,
46
+ TEXTVED_AZURE_BASE_URL: env.AZURE_PRIMARY_BASE_URL_SECRET,
47
+ AZURE_OPENAI_API_KEY: env.AZURE_SMALL_API_KEY_SECRET,
48
+ AZURE_OPENAI_BASE_URL: env.AZURE_SMALL_BASE_URL_SECRET,
49
+ GH_TOKEN: env.GITHUB_TOKEN_SECRET,
50
+ AWS_ACCESS_KEY_ID: env.AWS_ACCESS_KEY_ID_SECRET,
51
+ AWS_SECRET_ACCESS_KEY: env.AWS_SECRET_ACCESS_KEY_SECRET,
52
+ AWS_SESSION_TOKEN: env.AWS_SESSION_TOKEN_SECRET,
53
+ TELEGRAM_BOT_TOKEN: env.TELEGRAM_BOT_TOKEN_SECRET,
54
+ TELEGRAM_ALLOWED_CHAT_IDS: env.TELEGRAM_ALLOWED_CHAT_IDS_SECRET,
55
+ },
56
+ stateDir: path.resolve(env.FACTORY_STATE_DIR),
57
+ memoryDir: path.resolve(env.FACTORY_STATE_DIR, "memory"),
58
+ workspaceDir: path.resolve(env.FACTORY_WORKSPACE_DIR),
59
+ registryPath: path.resolve(env.FACTORY_REGISTRY),
60
+ workerImage: env.FACTORY_WORKER_IMAGE,
61
+ concurrency: env.MAX_CONCURRENCY,
62
+ timeoutMs: env.TASK_TIMEOUT_MS,
63
+ maxDeliveryCount: env.MAX_DELIVERY_COUNT,
64
+ };
65
+ }