factory-ai 1.3.0 → 1.5.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 (65) hide show
  1. package/CHANGELOG.md +24 -4
  2. package/Dockerfile.worker +1 -1
  3. package/README.md +10 -2
  4. package/ROADMAP.md +0 -1
  5. package/bin/factory +130 -3
  6. package/bootstrap/agent-factory-worker.service +3 -3
  7. package/bootstrap/auto-update.sh +20 -12
  8. package/bootstrap/deploy-runtime.sh +7 -3
  9. package/bootstrap/factory-ai-container-firewall.service +14 -0
  10. package/bootstrap/factory-ai-ollama.service +16 -0
  11. package/bootstrap/factory-ai-qdrant.service +16 -0
  12. package/bootstrap/factory-ai-snapshot.service +1 -0
  13. package/bootstrap/factory-ai-watchdog.service +21 -0
  14. package/bootstrap/factory-ai-watchdog.timer +11 -0
  15. package/bootstrap/setup.sh +33 -8
  16. package/cmd/factory-ui/main.go +455 -0
  17. package/go.mod +39 -0
  18. package/go.sum +82 -0
  19. package/package.json +7 -3
  20. package/scripts/test.js +7 -0
  21. package/src/acp-adapter.js +26 -0
  22. package/src/acp-cli.js +16 -0
  23. package/src/activity.js +96 -0
  24. package/src/agent-executor.js +47 -3
  25. package/src/agent-runner.js +34 -12
  26. package/src/approval-policy.js +12 -0
  27. package/src/azure-harness.js +47 -14
  28. package/src/bedrock-harness.js +45 -14
  29. package/src/config.js +13 -0
  30. package/src/container-runner.js +65 -12
  31. package/src/context-compaction.js +16 -0
  32. package/src/control-plane.js +77 -16
  33. package/src/control-service.js +6 -1
  34. package/src/dashboard.js +25 -6
  35. package/src/extension-cli.js +11 -0
  36. package/src/extension-manifest.js +76 -0
  37. package/src/hooks.js +62 -0
  38. package/src/instructions.js +45 -0
  39. package/src/mcp-tools.js +19 -3
  40. package/src/objective-status.js +16 -0
  41. package/src/operator-logs.js +24 -0
  42. package/src/operator.js +34 -10
  43. package/src/process.js +3 -3
  44. package/src/release-gate.js +1 -0
  45. package/src/release-service.js +4 -1
  46. package/src/release.js +1 -1
  47. package/src/repo-map.js +128 -0
  48. package/src/reporter.js +12 -1
  49. package/src/retriever.js +154 -0
  50. package/src/routing.js +11 -1
  51. package/src/scanner-suite.js +4 -3
  52. package/src/setup-menu.js +3 -1
  53. package/src/snapshot.js +16 -0
  54. package/src/task-entry.js +28 -6
  55. package/src/task-graph.js +0 -5
  56. package/src/telegram-service.js +10 -4
  57. package/src/telegram.js +7 -3
  58. package/src/telemetry.js +173 -0
  59. package/src/tui.js +31 -10
  60. package/src/validation.js +41 -9
  61. package/src/watchdog.js +62 -0
  62. package/src/worker.js +34 -3
  63. package/src/workspace-tools.js +13 -3
  64. package/src/workspace.js +22 -2
  65. package/templates/AGENTS.md +15 -0
@@ -5,6 +5,7 @@ import { run } from "./process.js";
5
5
 
6
6
  const ALLOWED_COMMANDS = new Set(["git", "ls", "node", "npm", "npx", "pwd", "rg"]);
7
7
  const DENIED_GIT_OPERATIONS = new Set(["credential", "push", "remote"]);
8
+ const READ_ONLY_GIT_OPERATIONS = new Set(["diff", "grep", "log", "ls-files", "rev-parse", "show", "status"]);
8
9
 
9
10
  function lexicalPath(root, requested) {
10
11
  const target = path.resolve(root, requested);
@@ -43,10 +44,11 @@ async function walk(directory, root, output, limit) {
43
44
  }
44
45
  }
45
46
 
46
- export function createWorkspaceTools(rootInput, { execute = run } = {}) {
47
+ export function createWorkspaceTools(rootInput, { execute = run, mutable = true, allowTests = false } = {}) {
47
48
  const root = realpathSync(path.resolve(rootInput));
48
- return {
49
+ const tools = {
49
50
  read_file: {
51
+ parallelSafe: true,
50
52
  description: "Read a bounded UTF-8 line range inside the assigned workspace. Use offsetLine/limitLines instead of rereading large files.",
51
53
  parameters: {
52
54
  type: "object",
@@ -69,6 +71,7 @@ export function createWorkspaceTools(rootInput, { execute = run } = {}) {
69
71
  },
70
72
  },
71
73
  write_file: {
74
+ parallelSafe: false,
72
75
  description: "Atomically replace a UTF-8 file inside the assigned workspace.",
73
76
  parameters: {
74
77
  type: "object",
@@ -86,6 +89,7 @@ export function createWorkspaceTools(rootInput, { execute = run } = {}) {
86
89
  },
87
90
  },
88
91
  list_files: {
92
+ parallelSafe: true,
89
93
  description: "List files recursively inside the assigned workspace.",
90
94
  parameters: {
91
95
  type: "object",
@@ -101,6 +105,7 @@ export function createWorkspaceTools(rootInput, { execute = run } = {}) {
101
105
  },
102
106
  },
103
107
  run_command: {
108
+ parallelSafe: false,
104
109
  description: "Run an allowlisted noninteractive development command in the assigned workspace.",
105
110
  parameters: {
106
111
  type: "object",
@@ -113,7 +118,10 @@ export function createWorkspaceTools(rootInput, { execute = run } = {}) {
113
118
  },
114
119
  execute: async ({ command, args }) => {
115
120
  if (!ALLOWED_COMMANDS.has(command)) throw new Error(`Command not allowed: ${command}`);
116
- if (command === "git" && DENIED_GIT_OPERATIONS.has(args[0])) throw new Error(`Git operation not allowed: ${args[0]}`);
121
+ const testCommand = allowTests && ((command === "npm" && ["test", "run"].includes(args[0])) || (command === "node" && args[0] === "--test") || (command === "npx" && args[0] === "--no-install"));
122
+ if (!mutable && !testCommand && !["git", "ls", "pwd", "rg"].includes(command)) throw new Error(`Command not allowed for read-only role: ${command}`);
123
+ if (command === "git" && (args.some((item) => DENIED_GIT_OPERATIONS.has(item)) || args.some((item) => ["-c", "--config-env", "--exec-path"].includes(item)))) throw new Error("Git operation not allowed");
124
+ if (!mutable && command === "git" && (args[0]?.startsWith("-") || !READ_ONLY_GIT_OPERATIONS.has(args[0]))) throw new Error(`Git operation not allowed for read-only role: ${args[0]}`);
117
125
  if (command === "npx" && args[0] !== "--no-install") throw new Error("npx requires --no-install");
118
126
  const result = await execute(command, args, {
119
127
  cwd: root,
@@ -127,4 +135,6 @@ export function createWorkspaceTools(rootInput, { execute = run } = {}) {
127
135
  },
128
136
  },
129
137
  };
138
+ if (!mutable) delete tools.write_file;
139
+ return tools;
130
140
  }
package/src/workspace.js CHANGED
@@ -45,10 +45,18 @@ export class WorkspaceManager {
45
45
 
46
46
  async prepareTask(objective, task, dependencyCommits = []) {
47
47
  const directory = this.taskDirectory(objective.id, task.id);
48
- if (await exists(directory)) return directory;
48
+ const branch = `factory-ai/${objective.id}/${task.id}`;
49
+ if (await exists(directory)) {
50
+ const current = (await run("git", ["-C", directory, "branch", "--show-current"])).stdout.trim();
51
+ if (current !== branch) throw new Error(`Existing task workspace is on unexpected branch: ${current}`);
52
+ for (const commit of dependencyCommits) {
53
+ const ancestor = await run("git", ["-C", directory, "merge-base", "--is-ancestor", commit, "HEAD"], { allowExitCodes: [0, 1] });
54
+ if (ancestor.code !== 0) throw new Error(`Existing task workspace is missing dependency commit: ${commit}`);
55
+ }
56
+ return directory;
57
+ }
49
58
  await mkdir(path.dirname(directory), { recursive: true, mode: 0o750 });
50
59
  const base = dependencyCommits[0] ?? `origin/${objective.baseBranch}`;
51
- const branch = `factory-ai/${objective.id}/${task.id}`;
52
60
  await run("git", ["clone", objective.repository, directory], { timeoutMs: this.timeoutMs });
53
61
  await run("git", ["-C", directory, "config", "user.name", "Factory AI"]);
54
62
  await run("git", ["-C", directory, "config", "user.email", "factory-ai@localhost"]);
@@ -60,6 +68,13 @@ export class WorkspaceManager {
60
68
  return directory;
61
69
  }
62
70
 
71
+ async recoveryContext(directory) {
72
+ const status = (await run("git", ["-C", directory, "status", "--short"], { maxOutputBytes: 50_000 })).stdout.trim();
73
+ if (!status) return "";
74
+ const summary = (await run("git", ["-C", directory, "diff", "--stat"], { maxOutputBytes: 50_000 })).stdout.trim();
75
+ return `RECOVERED DURABLE WORKTREE CHECKPOINT\nA previous attempt changed these files. Inspect and continue rather than repeating completed work.\n${status}${summary ? `\n\n${summary}` : ""}`;
76
+ }
77
+
63
78
  async checkpoint(directory, objective, task) {
64
79
  await run("git", ["-C", directory, "add", "-A"]);
65
80
  const diff = await run("git", ["-C", directory, "diff", "--cached", "--quiet"], { allowExitCodes: [0, 1] });
@@ -75,4 +90,9 @@ export class WorkspaceManager {
75
90
  return { commit, branch };
76
91
  }
77
92
 
93
+ async reference(directory, objective, task) {
94
+ const { stdout } = await run("git", ["-C", directory, "rev-parse", "HEAD"]);
95
+ return { commit: stdout.trim(), branch: `factory-ai/${objective.id}/${task.id}` };
96
+ }
97
+
78
98
  }
@@ -0,0 +1,15 @@
1
+ # Project Agent Instructions
2
+
3
+ ## Working Agreement
4
+
5
+ - Make the smallest correct change that satisfies the objective.
6
+ - Read existing code and tests before editing. Preserve established architecture and style.
7
+ - Never read, print, persist, or commit credentials or local environment files.
8
+ - Keep changes inside this repository. Do not push, deploy, or mutate external systems.
9
+ - Add or update focused tests for behavior changes.
10
+ - Run the project's documented format, typecheck, lint, test, and build gates before claiming completion.
11
+ - Report commands run, results, remaining risks, and any assumptions.
12
+
13
+ ## Project Commands
14
+
15
+ Record project-specific commands and constraints in `.agent-factory/commands.md` and `.agent-factory/project.md`.