factory-ai 1.3.0 → 1.4.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/CHANGELOG.md CHANGED
@@ -11,6 +11,14 @@ All notable changes follow semantic versioning and the Keep a Changelog structur
11
11
  - Azure and Bedrock provider wizard.
12
12
  - Durable evaluation, analytics, scaling, policy, extension, and recovery roadmap.
13
13
 
14
+ ## [1.4.0] - 2026-07-13
15
+
16
+ ### Added
17
+
18
+ - Free local semantic code retrieval with pinned Ollama embeddings and retained Qdrant vectors.
19
+ - Commit-aware indexing, bounded chunking, top-k context injection, and failure fallback.
20
+ - Modern agent-harness capability matrix and explicit completion criteria.
21
+
14
22
  ## [1.3.0] - 2026-07-13
15
23
 
16
24
  ### Added
package/README.md CHANGED
@@ -131,6 +131,7 @@ Factory AI minimizes tokens before relying on cheaper models:
131
131
  - File reads are line-ranged and bounded; listings, commands, MCP output, memory, and scanner evidence are truncated with continuation hints.
132
132
  - Read-only roles do not receive write-tool schemas.
133
133
  - Planner memory is compact, repository-scoped, and limited to recent verified events.
134
+ - Ollama generates local embeddings and Qdrant retrieves only top-ranked code chunks, avoiding embedding API charges and whole-repository prompts.
134
135
  - Dashboard and TUI track input, cached-input, and output tokens by model.
135
136
  - The planner is instructed to produce the smallest valid DAG, avoiding duplicate agents.
136
137
 
@@ -255,6 +256,7 @@ npm pack --dry-run
255
256
  | [CONTRIBUTING.md](CONTRIBUTING.md) | Development and verification contract |
256
257
  | [HANDOFF.md](HANDOFF.md) | Team/friend transfer context |
257
258
  | [docs/COMPARISON.md](docs/COMPARISON.md) | Honest comparison with paid alternatives |
259
+ | [docs/HARNESS_PARITY.md](docs/HARNESS_PARITY.md) | Feature parity across modern agent harnesses |
258
260
  | [ROADMAP.md](ROADMAP.md) | Planned platform and ecosystem work |
259
261
  | [GOVERNANCE.md](GOVERNANCE.md) | Decision and release governance |
260
262
  | [SUPPORT.md](SUPPORT.md) | Community support process |
package/bin/factory CHANGED
@@ -10,6 +10,8 @@ VM=${FACTORY_VM:-agent-factory-vm}
10
10
  NAMESPACE=${FACTORY_SERVICE_BUS:-}
11
11
  VAULT=${FACTORY_KEY_VAULT:-}
12
12
  STORAGE=${FACTORY_STORAGE_ACCOUNT:-}
13
+ FACTORY_NAME=${FACTORY_NAME:-Factory AI}
14
+ FACTORY_PURPOSE=${FACTORY_PURPOSE:-Ship secure reviewed software continuously}
13
15
 
14
16
  remote() {
15
17
  az vm run-command invoke --resource-group "$RESOURCE_GROUP" --name "$VM" \
@@ -32,7 +34,7 @@ usage() {
32
34
  ' pause | resume'
33
35
  }
34
36
 
35
- command=${1:-help}
37
+ command=${1:-ui}
36
38
  shift || true
37
39
  case "$command" in
38
40
  telegram)
@@ -74,7 +76,11 @@ case "$command" in
74
76
  exec "$0" submit "$repo" "$objective"
75
77
  ;;
76
78
  ui)
77
- FACTORY_RESOURCE_GROUP="$RESOURCE_GROUP" FACTORY_VM="$VM" FACTORY_SERVICE_BUS="$NAMESPACE" FACTORY_KEY_VAULT="$VAULT" FACTORY_STORAGE_ACCOUNT="$STORAGE" node "$FACTORY_ROOT/src/tui.js"
79
+ if [[ ! -f $CONFIG_FILE && ! -f $LEGACY_CONFIG_FILE ]]; then
80
+ "$0" setup
81
+ exec "$0" ui
82
+ fi
83
+ FACTORY_RESOURCE_GROUP="$RESOURCE_GROUP" FACTORY_VM="$VM" FACTORY_SERVICE_BUS="$NAMESPACE" FACTORY_KEY_VAULT="$VAULT" FACTORY_STORAGE_ACCOUNT="$STORAGE" FACTORY_NAME="$FACTORY_NAME" FACTORY_PURPOSE="$FACTORY_PURPOSE" node "$FACTORY_ROOT/src/tui.js"
78
84
  ;;
79
85
  setup)
80
86
  for dependency in az gh jq curl ssh-keygen node; do command -v "$dependency" >/dev/null || { printf 'Missing dependency: %s\n' "$dependency" >&2; exit 1; }; done
@@ -82,6 +88,8 @@ case "$command" in
82
88
  trap 'rm -f "$choices"' EXIT
83
89
  node "$FACTORY_ROOT/src/setup-menu.js" "$choices"
84
90
  provider=$(jq -r .provider "$choices")
91
+ factory_name=$(jq -r .factoryName "$choices")
92
+ factory_purpose=$(jq -r .factoryPurpose "$choices")
85
93
  location=$(jq -r .location "$choices")
86
94
  enterprise_org=$(jq -r .githubOrg "$choices")
87
95
  aws_region=$(jq -r .awsRegion "$choices")
@@ -107,6 +115,8 @@ case "$command" in
107
115
  printf 'FACTORY_SERVICE_BUS=%q\n' "$namespace"
108
116
  printf 'FACTORY_KEY_VAULT=%q\n' "$vault"
109
117
  printf 'FACTORY_STORAGE_ACCOUNT=%q\n' "$storage"
118
+ printf 'FACTORY_NAME=%q\n' "$factory_name"
119
+ printf 'FACTORY_PURPOSE=%q\n' "$factory_purpose"
110
120
  } > "$CONFIG_FILE"
111
121
  chmod 0600 "$CONFIG_FILE"
112
122
  ip=$(curl -fsS https://api.ipify.org)
@@ -154,7 +164,7 @@ case "$command" in
154
164
  printf 'Azure foundation and credential vault ready. Config: %s\n' "$CONFIG_FILE"
155
165
  if [[ $deploy_now == true ]]; then
156
166
  source_ref=$(gh api repos/itsvedantkumar/factory-ai/commits/main --jq .sha)
157
- parameters=(KEY_VAULT_NAME "$vault" SERVICE_BUS_NAMESPACE "$namespace" SERVICE_BUS_QUEUE code-tasks FACTORY_STORAGE_ACCOUNT "$storage" SOURCE_REPOSITORY itsvedantkumar/factory-ai SOURCE_REF "$source_ref")
167
+ parameters=(KEY_VAULT_NAME "$vault" SERVICE_BUS_NAMESPACE "$namespace" SERVICE_BUS_QUEUE code-tasks FACTORY_STORAGE_ACCOUNT "$storage" FACTORY_NAME "$factory_name" FACTORY_PURPOSE "$factory_purpose" SOURCE_REPOSITORY itsvedantkumar/factory-ai SOURCE_REF "$source_ref")
158
168
  if [[ $provider == bedrock ]]; then
159
169
  parameters+=(AWS_REGION "$aws_region")
160
170
  for role in SCOUT PLANNER BUILDER TESTER DEBUGGER REVIEWER SECURITY RELEASE; do parameters+=("FACTORY_MODEL_$role" "bedrock/$bedrock_builder"); done
@@ -1,7 +1,7 @@
1
1
  [Unit]
2
2
  Description=Factory AI durable worker
3
- After=network-online.target docker.service
4
- Wants=network-online.target
3
+ After=network-online.target docker.service factory-ai-qdrant.service factory-ai-ollama.service
4
+ Wants=network-online.target factory-ai-qdrant.service factory-ai-ollama.service
5
5
  Requires=docker.service
6
6
  StartLimitIntervalSec=300
7
7
  StartLimitBurst=10
@@ -20,7 +20,7 @@ while (($# > 0)); do
20
20
  shift
21
21
  fi
22
22
  case "$name" in
23
- KEY_VAULT_NAME|SERVICE_BUS_NAMESPACE|SERVICE_BUS_QUEUE|SOURCE_REPOSITORY|SOURCE_REF|FACTORY_STORAGE_ACCOUNT|AWS_REGION|FACTORY_MODEL_SCOUT|FACTORY_MODEL_PLANNER|FACTORY_MODEL_BUILDER|FACTORY_MODEL_TESTER|FACTORY_MODEL_DEBUGGER|FACTORY_MODEL_REVIEWER|FACTORY_MODEL_SECURITY|FACTORY_MODEL_RELEASE) printf -v "$name" '%s' "$value" ;;
23
+ KEY_VAULT_NAME|SERVICE_BUS_NAMESPACE|SERVICE_BUS_QUEUE|SOURCE_REPOSITORY|SOURCE_REF|FACTORY_STORAGE_ACCOUNT|FACTORY_NAME|FACTORY_PURPOSE|AWS_REGION|FACTORY_MODEL_SCOUT|FACTORY_MODEL_PLANNER|FACTORY_MODEL_BUILDER|FACTORY_MODEL_TESTER|FACTORY_MODEL_DEBUGGER|FACTORY_MODEL_REVIEWER|FACTORY_MODEL_SECURITY|FACTORY_MODEL_RELEASE) printf -v "$name" '%s' "$value" ;;
24
24
  *) echo "Unknown deployment parameter: $name" >&2; exit 1 ;;
25
25
  esac
26
26
  done
@@ -63,6 +63,8 @@ SERVICE_BUS_NAMESPACE="$SERVICE_BUS_NAMESPACE" \
63
63
  SERVICE_BUS_QUEUE="$SERVICE_BUS_QUEUE" \
64
64
  FACTORY_WORKER_IMAGE="agent-factory-worker:$SOURCE_REF" \
65
65
  FACTORY_STORAGE_ACCOUNT="${FACTORY_STORAGE_ACCOUNT:-}" \
66
+ FACTORY_NAME="${FACTORY_NAME:-Factory AI}" \
67
+ FACTORY_PURPOSE="${FACTORY_PURPOSE:-Ship secure reviewed software continuously}" \
66
68
  AWS_REGION="${AWS_REGION:-}" \
67
69
  FACTORY_MODEL_SCOUT="${FACTORY_MODEL_SCOUT:-}" \
68
70
  FACTORY_MODEL_PLANNER="${FACTORY_MODEL_PLANNER:-}" \
@@ -0,0 +1,16 @@
1
+ [Unit]
2
+ Description=Factory AI local embedding inference
3
+ After=docker.service
4
+ Requires=docker.service
5
+
6
+ [Service]
7
+ Type=simple
8
+ ExecStartPre=-/usr/bin/docker rm -f factory-ai-ollama
9
+ ExecStart=/usr/bin/docker run --rm --name factory-ai-ollama --userns host --cap-drop ALL --security-opt no-new-privileges --pids-limit 1024 --memory 6g --cpus 4 --publish 127.0.0.1:11434:11434 --env OLLAMA_MODELS=/models --volume /opt/agent-factory/state/ollama:/models ollama/ollama@sha256:3d8a05e3432d50ea57594fabe971e46cc8fe963a0f9f8c40400bd56cd5388e47
10
+ ExecStop=/usr/bin/docker stop -t 20 factory-ai-ollama
11
+ Restart=always
12
+ RestartSec=5
13
+ SyslogIdentifier=factory-ai-ollama
14
+
15
+ [Install]
16
+ WantedBy=multi-user.target
@@ -0,0 +1,16 @@
1
+ [Unit]
2
+ Description=Factory AI local Qdrant retrieval store
3
+ After=docker.service
4
+ Requires=docker.service
5
+
6
+ [Service]
7
+ Type=simple
8
+ ExecStartPre=-/usr/bin/docker rm -f factory-ai-qdrant
9
+ ExecStart=/usr/bin/docker run --rm --name factory-ai-qdrant --userns host --read-only --cap-drop ALL --security-opt no-new-privileges --pids-limit 1024 --memory 2g --cpus 2 --publish 127.0.0.1:6333:6333 --volume /opt/agent-factory/state/qdrant:/qdrant/storage --volume /opt/agent-factory/state/qdrant-snapshots:/qdrant/snapshots qdrant/qdrant@sha256:31407c0e8e32eb771b71718f1a4772e2ad47a07557917b21ac96792f40eb8007
10
+ ExecStop=/usr/bin/docker stop -t 20 factory-ai-qdrant
11
+ Restart=always
12
+ RestartSec=5
13
+ SyslogIdentifier=factory-ai-qdrant
14
+
15
+ [Install]
16
+ WantedBy=multi-user.target
@@ -44,6 +44,8 @@ for scanner_image in \
44
44
  semgrep/semgrep@sha256:183a149fb3e9700ab5294a7b4ab0241a826fd046bc8b721062fbea80fdfa438f; do
45
45
  docker pull "$scanner_image"
46
46
  done
47
+ docker pull ollama/ollama@sha256:3d8a05e3432d50ea57594fabe971e46cc8fe963a0f9f8c40400bd56cd5388e47
48
+ docker pull qdrant/qdrant@sha256:31407c0e8e32eb771b71718f1a4772e2ad47a07557917b21ac96792f40eb8007
47
49
 
48
50
  STATE_DEVICE=/dev/disk/azure/scsi1/lun0
49
51
  if [[ -b $STATE_DEVICE ]]; then
@@ -65,7 +67,8 @@ for secret_name in "${GITHUB_TOKEN_SECRET:-github-token}"; do
65
67
  az keyvault secret show --vault-name "$KEY_VAULT_NAME" --name "$secret_name" --query id --output none
66
68
  done
67
69
 
68
- install -d -o "$FACTORY_USER" -g "$FACTORY_USER" -m 0750 /opt/agent-factory/state /opt/agent-factory/state/home /opt/agent-factory/state/memory /opt/agent-factory/state/telegram /opt/agent-factory/workspaces /opt/agent-factory/logs
70
+ install -d -o "$FACTORY_USER" -g "$FACTORY_USER" -m 0750 /opt/agent-factory/state /opt/agent-factory/state/home /opt/agent-factory/state/memory /opt/agent-factory/state/telegram /opt/agent-factory/state/retrieval /opt/agent-factory/workspaces /opt/agent-factory/logs
71
+ install -d -o root -g root -m 0750 /opt/agent-factory/state/qdrant /opt/agent-factory/state/qdrant-snapshots /opt/agent-factory/state/ollama
69
72
  install -m 0600 -o root -g root /dev/null /etc/agent-factory.env
70
73
  {
71
74
  printf 'SERVICE_BUS_NAMESPACE=%s\n' "$SERVICE_BUS_NAMESPACE"
@@ -81,10 +84,14 @@ install -m 0600 -o root -g root /dev/null /etc/agent-factory.env
81
84
  printf 'FACTORY_REGISTRY=%s/config/capabilities.json\n' "$APP_DIR"
82
85
  printf 'FACTORY_WORKER_IMAGE=%s\n' "$FACTORY_WORKER_IMAGE"
83
86
  printf 'FACTORY_VERSION=%s\n' "$factory_version"
87
+ printf 'FACTORY_NAME=%s\n' "${FACTORY_NAME:-Factory AI}"
88
+ printf 'FACTORY_PURPOSE=%s\n' "${FACTORY_PURPOSE:-Ship secure reviewed software continuously}"
84
89
  printf 'MAX_CONCURRENCY=3\n'
85
90
  printf 'TASK_TIMEOUT_MS=1800000\n'
86
91
  printf 'MAX_DELIVERY_COUNT=8\n'
87
92
  printf 'FACTORY_VERSION=%s\n' "$factory_version"
93
+ printf 'FACTORY_NAME=%s\n' "${FACTORY_NAME:-Factory AI}"
94
+ printf 'FACTORY_PURPOSE=%s\n' "${FACTORY_PURPOSE:-Ship secure reviewed software continuously}"
88
95
  [[ -n ${AWS_REGION:-} ]] && printf 'AWS_REGION=%s\nAWS_DEFAULT_REGION=%s\n' "$AWS_REGION" "$AWS_REGION"
89
96
  for variable in FACTORY_MODEL_SCOUT FACTORY_MODEL_PLANNER FACTORY_MODEL_BUILDER FACTORY_MODEL_TESTER FACTORY_MODEL_DEBUGGER FACTORY_MODEL_REVIEWER FACTORY_MODEL_SECURITY FACTORY_MODEL_RELEASE; do
90
97
  [[ -n ${!variable:-} ]] && printf '%s=%s\n' "$variable" "${!variable}"
@@ -111,6 +118,8 @@ install -m 0600 -o root -g root /dev/null /etc/agent-factory-control.env
111
118
  } > /etc/agent-factory-control.env
112
119
 
113
120
  chown -R "$FACTORY_USER:$FACTORY_USER" /opt/agent-factory/state /opt/agent-factory/workspaces /opt/agent-factory/logs
121
+ chown root:root /opt/agent-factory/state/qdrant /opt/agent-factory/state/qdrant-snapshots /opt/agent-factory/state/ollama
122
+ chmod 0750 /opt/agent-factory/state/qdrant /opt/agent-factory/state/qdrant-snapshots /opt/agent-factory/state/ollama
114
123
  chown -R root:root "$APP_DIR"
115
124
  chmod -R go-w "$APP_DIR"
116
125
  install -m 0644 "$APP_DIR/bootstrap/agent-factory-worker.service" /etc/systemd/system/agent-factory-worker.service
@@ -124,6 +133,8 @@ install -m 0644 "$APP_DIR/bootstrap/factory-ai-update.service" /etc/systemd/syst
124
133
  install -m 0644 "$APP_DIR/bootstrap/factory-ai-update.timer" /etc/systemd/system/factory-ai-update.timer
125
134
  install -m 0644 "$APP_DIR/bootstrap/factory-ai-snapshot.service" /etc/systemd/system/factory-ai-snapshot.service
126
135
  install -m 0644 "$APP_DIR/bootstrap/factory-ai-snapshot.timer" /etc/systemd/system/factory-ai-snapshot.timer
136
+ install -m 0644 "$APP_DIR/bootstrap/factory-ai-qdrant.service" /etc/systemd/system/factory-ai-qdrant.service
137
+ install -m 0644 "$APP_DIR/bootstrap/factory-ai-ollama.service" /etc/systemd/system/factory-ai-ollama.service
127
138
  systemctl daemon-reload
128
139
  systemctl enable --now agent-factory-worker.service
129
140
  systemctl enable --now agent-factory-control.service
@@ -132,5 +143,8 @@ systemctl enable --now agent-factory-reporter.timer
132
143
  systemctl enable --now agent-factory-telegram.service
133
144
  systemctl enable --now factory-ai-update.timer
134
145
  systemctl enable --now factory-ai-snapshot.timer
146
+ systemctl enable --now factory-ai-qdrant.service factory-ai-ollama.service
147
+ for _ in $(seq 1 60); do curl -fsS http://127.0.0.1:11434/api/tags >/dev/null && break; sleep 2; done
148
+ docker exec factory-ai-ollama ollama show embeddinggemma >/dev/null 2>&1 || docker exec factory-ai-ollama ollama pull embeddinggemma
135
149
  systemctl restart agent-factory-control.service agent-factory-worker.service agent-factory-release.service
136
150
  echo "Agent factory worker installed"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "factory-ai",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
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
5
  "private": false,
6
6
  "license": "MIT",
@@ -1,10 +1,11 @@
1
1
  import { parseTaskResult } from "./validation.js";
2
2
 
3
3
  export class AgentExecutor {
4
- constructor({ workspaces, agentRunner, scannerSuite, sendControl }) {
4
+ constructor({ workspaces, agentRunner, scannerSuite, retriever, sendControl }) {
5
5
  this.workspaces = workspaces;
6
6
  this.agentRunner = agentRunner;
7
7
  this.scannerSuite = scannerSuite;
8
+ this.retriever = retriever;
8
9
  this.sendControl = sendControl;
9
10
  }
10
11
 
@@ -16,7 +17,10 @@ export class AgentExecutor {
16
17
 
17
18
  async processPlanning(message) {
18
19
  const directory = await this.workspaces.ensureObjective(message.objective);
19
- const delivery = await this.agentRunner.plan(message.objective, directory, message.context ?? []);
20
+ let semanticContext = "";
21
+ try { semanticContext = this.retriever ? await this.retriever.context(directory, message.objective.repository, message.objective.objective) : ""; } catch {}
22
+ const context = [...(message.context ?? []), ...(semanticContext ? [{ type: "local-semantic-retrieval", content: semanticContext }] : [])];
23
+ const delivery = await this.agentRunner.plan(message.objective, directory, context);
20
24
  await this.sendControl({ type: "planning_result", objectiveId: message.objectiveId, delivery });
21
25
  }
22
26
 
@@ -29,9 +33,15 @@ export class AgentExecutor {
29
33
  const scannerEvidence = message.task.role === "security" && this.scannerSuite
30
34
  ? await this.scannerSuite.scan(directory)
31
35
  : [];
36
+ let semanticContext = "";
37
+ if (message.task.role !== "release" && this.retriever) {
38
+ const query = `${message.objective.objective}\n${message.task.title}\n${message.task.instructions}`;
39
+ try { semanticContext = await this.retriever.context(directory, message.objective.repository, query); } catch {}
40
+ }
32
41
  const prompt = [
33
42
  "Execute only this assigned task, verify it, and report factual outcomes.",
34
43
  ...(scannerEvidence.length ? [`TRUSTED SCANNER EVIDENCE (mechanical output; do not claim checks beyond this evidence):\n${JSON.stringify(scannerEvidence)}`] : []),
44
+ ...(semanticContext ? [`LOCAL SEMANTIC CONTEXT (retrieved from this exact repository revision):\n${semanticContext}`] : []),
35
45
  ].join("\n\n");
36
46
  const result = parseTaskResult(await this.agentRunner.invoke({
37
47
  objective: message.objective,
@@ -47,11 +47,13 @@ export class AzureAgentRunner {
47
47
  }
48
48
 
49
49
  async promptForTask(objective, task, prompt, capabilities) {
50
+ const factoryName = this.environment.FACTORY_NAME ?? "Factory AI";
51
+ const factoryPurpose = this.environment.FACTORY_PURPOSE ?? "Ship secure reviewed software continuously";
50
52
  const skills = await Promise.all(capabilities.filter((item) => item.type === "skill").map(async (item) => (
51
53
  `ALLOWLISTED SKILL ${item.name}@${item.version}:\n${await readFile(item.path, "utf8")}`
52
54
  )));
53
55
  return [
54
- `You are a Factory AI isolated ${task.role} subagent.`,
56
+ `You are a ${factoryName} isolated ${task.role} subagent. Factory purpose: ${factoryPurpose}.`,
55
57
  "Work only in the assigned repository. Never inspect credentials, push Git refs, deploy, or install global tools.",
56
58
  "Use tools for evidence. Make the smallest correct change and verify every completion claim.",
57
59
  'Return only JSON: {"summary":"concise outcome","checks":["command/result"],"risks":["remaining risk"],"approval":"approved|changes_requested|not_applicable"}.',
@@ -115,7 +117,7 @@ export class AzureAgentRunner {
115
117
  ...Object.entries(this.registry.skills ?? {}),
116
118
  ...Object.entries(this.registry.mcp ?? {}),
117
119
  ].map(([name, item]) => [name, { version: item.version, roles: item.roles }]));
118
- const prompt = `You are a Factory AI planner subagent. Decompose objectives into the smallest executable DAG.
120
+ const prompt = `You are the ${this.environment.FACTORY_NAME ?? "Factory AI"} planner subagent. Factory purpose: ${this.environment.FACTORY_PURPOSE ?? "Ship secure reviewed software continuously"}. Decompose objectives into the smallest executable DAG.
119
121
  Allowed roles: scout, builder, tester, debugger, reviewer, security, release.
120
122
  Allowed capabilities: ${JSON.stringify(registrySummary)}
121
123
  Include tester, reviewer, and security ancestors of exactly one terminal release task.
package/src/config.js CHANGED
@@ -10,6 +10,8 @@ const environmentSchema = z.object({
10
10
  AZURE_SUBSCRIPTION_ID: z.string().optional(),
11
11
  FACTORY_RESOURCE_GROUP: z.string().default("factory-ai-rg"),
12
12
  FACTORY_STORAGE_ACCOUNT: z.string().optional(),
13
+ FACTORY_NAME: z.string().min(1).max(80).default("Factory AI"),
14
+ FACTORY_PURPOSE: z.string().min(1).max(500).default("Ship secure reviewed software continuously"),
13
15
  FACTORY_STATE_DIR: z.string().default("/opt/agent-factory/state"),
14
16
  FACTORY_WORKSPACE_DIR: z.string().default("/opt/agent-factory/workspaces"),
15
17
  FACTORY_REGISTRY: z.string().default("/opt/agent-factory/app/config/capabilities.json"),
@@ -43,6 +45,8 @@ export function loadConfig(environment = process.env) {
43
45
  subscriptionId: env.AZURE_SUBSCRIPTION_ID,
44
46
  resourceGroup: env.FACTORY_RESOURCE_GROUP,
45
47
  storageAccount: env.FACTORY_STORAGE_ACCOUNT,
48
+ factoryName: env.FACTORY_NAME,
49
+ factoryPurpose: env.FACTORY_PURPOSE,
46
50
  secretNames: {
47
51
  TEXTVED_AZURE_API_KEY: env.AZURE_PRIMARY_API_KEY_SECRET,
48
52
  TEXTVED_AZURE_BASE_URL: env.AZURE_PRIMARY_BASE_URL_SECRET,
package/src/operator.js CHANGED
@@ -24,6 +24,8 @@ export function createOperator(environment = process.env) {
24
24
  const namespace = environment.FACTORY_SERVICE_BUS ?? "";
25
25
  const vault = environment.FACTORY_KEY_VAULT ?? "";
26
26
  const storageAccount = environment.FACTORY_STORAGE_ACCOUNT ?? "";
27
+ const factoryName = environment.FACTORY_NAME ?? "Factory AI";
28
+ const factoryPurpose = environment.FACTORY_PURPOSE ?? "Ship secure reviewed software continuously";
27
29
  const remote = async (script) => {
28
30
  for (let attempt = 0; attempt < 6; attempt += 1) {
29
31
  try { return extractRunCommand(await command("az", ["vm", "run-command", "invoke", "--resource-group", resourceGroup, "--name", vm, "--command-id", "RunShellScript", "--scripts", script, "--query", "value[0].message", "--output", "tsv"])); }
@@ -68,7 +70,7 @@ export function createOperator(environment = process.env) {
68
70
  return command(path.join(root, "bin/factory"), [action]);
69
71
  },
70
72
  capabilities: async () => JSON.parse(await readFile(path.join(root, "config/capabilities.json"), "utf8")),
71
- config: () => ({ resourceGroup, vm, namespace, vault, storageAccount, models: { scout: "GPT-5.4 nano", simpleBuilder: "Kimi K2.7-Code", builder: "GPT-5.5", tester: "GPT-5.4", critical: "GPT-5.6" } }),
73
+ config: () => ({ factoryName, factoryPurpose, resourceGroup, vm, namespace, vault, storageAccount, models: { scout: "GPT-5.4 nano", simpleBuilder: "Kimi K2.7-Code", builder: "GPT-5.5", tester: "GPT-5.4", critical: "GPT-5.6" } }),
72
74
  listSecrets: async () => withVault(async () => JSON.parse(await command("az", ["keyvault", "secret", "list", "--vault-name", vault, "--query", "[].{name:name,updated:attributes.updated}", "--output", "json"]))),
73
75
  setSecret: async (name, value) => withVault(async () => {
74
76
  if (!/^[A-Za-z0-9-]{1,127}$/.test(name) || !value) throw new Error("Valid secret name and value are required");
package/src/release.js CHANGED
@@ -49,7 +49,7 @@ export class GitHubRelease {
49
49
  "",
50
50
  "Human review and repository branch protections remain authoritative.",
51
51
  ].join("\n");
52
- const title = `[Factory AI] ${task.title}`;
52
+ const title = `[${process.env.FACTORY_NAME ?? "Factory AI"}] ${task.title}`;
53
53
  const create = await this.execute("gh", [
54
54
  "pr", "create", "--base", objective.baseBranch, "--head", branch,
55
55
  "--title", title, "--body", body,
package/src/reporter.js CHANGED
@@ -12,7 +12,7 @@ process.title = "factory-ai-reporter";
12
12
  function markdown(dashboard) {
13
13
  const objectives = Object.entries(dashboard.summary.objectives).map(([state, count]) => `${state}=${count}`).join(", ") || "none";
14
14
  const cost = dashboard.cost ? `\nAzure month-to-date: ${dashboard.cost.currency} ${dashboard.cost.monthToDate.toFixed(2)} (billing data may be delayed)\n` : "";
15
- return `# Factory AI Hourly Report\n\nGenerated: ${dashboard.generatedAt}\n\nQueue: ${dashboard.queue.active} active, ${dashboard.queue.deadLetter} dead-letter\n${cost}\nObjectives: ${objectives}\n`;
15
+ return `# ${process.env.FACTORY_NAME ?? "Factory AI"} Hourly Report\n\nGenerated: ${dashboard.generatedAt}\n\nQueue: ${dashboard.queue.active} active, ${dashboard.queue.deadLetter} dead-letter\n${cost}\nObjectives: ${objectives}\n`;
16
16
  }
17
17
 
18
18
  async function atomicWrite(file, content) {
@@ -0,0 +1,154 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { run } from "./process.js";
5
+
6
+ const textExtensions = new Set([".c", ".cc", ".cpp", ".cs", ".css", ".go", ".h", ".hpp", ".html", ".java", ".js", ".jsx", ".json", ".md", ".mdx", ".php", ".py", ".rb", ".rs", ".scss", ".sh", ".sql", ".svelte", ".toml", ".ts", ".tsx", ".vue", ".yaml", ".yml"]);
7
+ const ignoredDirectories = new Set([".git", ".next", ".turbo", "build", "coverage", "dist", "node_modules", "vendor"]);
8
+
9
+ export function chunkText(file, value, { linesPerChunk = 120, overlapLines = 20 } = {}) {
10
+ const lines = value.split("\n");
11
+ const chunks = [];
12
+ const step = Math.max(1, linesPerChunk - overlapLines);
13
+ for (let start = 0; start < lines.length; start += step) {
14
+ const content = lines.slice(start, start + linesPerChunk).join("\n").trim();
15
+ if (content) chunks.push({ path: file, startLine: start + 1, endLine: Math.min(lines.length, start + linesPerChunk), content });
16
+ if (start + linesPerChunk >= lines.length) break;
17
+ }
18
+ return chunks;
19
+ }
20
+
21
+ export function formatRetrievedContext(points, maxCharacters = 12_000) {
22
+ let output = "";
23
+ for (const point of points) {
24
+ const payload = point.payload ?? {};
25
+ const section = `\n--- ${payload.path}:${payload.startLine} score=${Number(point.score ?? 0).toFixed(3)} ---\n${payload.content ?? ""}\n`;
26
+ if (output.length + section.length > maxCharacters) break;
27
+ output += section;
28
+ }
29
+ return output.trim();
30
+ }
31
+
32
+ async function collectFiles(directory, root, output, limits) {
33
+ if (output.length >= limits.maxFiles) return;
34
+ for (const entry of (await readdir(directory, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
35
+ if (entry.isSymbolicLink?.() || ignoredDirectories.has(entry.name) || output.length >= limits.maxFiles) continue;
36
+ const absolute = path.join(directory, entry.name);
37
+ if (entry.isDirectory()) await collectFiles(absolute, root, output, limits);
38
+ else if (entry.isFile() && textExtensions.has(path.extname(entry.name).toLowerCase())) {
39
+ const metadata = await lstat(absolute);
40
+ if (metadata.size <= limits.maxFileBytes) output.push({ absolute, relative: path.relative(root, absolute) });
41
+ }
42
+ }
43
+ }
44
+
45
+ function pointId(value) {
46
+ const hash = createHash("sha256").update(value).digest("hex");
47
+ return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-4${hash.slice(13, 16)}-a${hash.slice(17, 20)}-${hash.slice(20, 32)}`;
48
+ }
49
+
50
+ export class LocalRetriever {
51
+ #locks = new Map();
52
+
53
+ constructor({
54
+ stateDir,
55
+ ollamaUrl = "http://127.0.0.1:11434",
56
+ qdrantUrl = "http://127.0.0.1:6333",
57
+ model = "embeddinggemma",
58
+ collection = "factory_ai_code",
59
+ fetch = globalThis.fetch,
60
+ }) {
61
+ this.stateDir = stateDir;
62
+ this.manifestFile = path.join(stateDir, "retrieval", "manifest.json");
63
+ this.ollamaUrl = ollamaUrl;
64
+ this.qdrantUrl = qdrantUrl;
65
+ this.model = model;
66
+ this.collection = collection;
67
+ this.fetch = fetch;
68
+ }
69
+
70
+ async request(url, options = {}) {
71
+ const response = await this.fetch(url, { ...options, signal: AbortSignal.timeout(120_000) });
72
+ if (!response.ok) throw new Error(`Local retrieval HTTP ${response.status}: ${url}`);
73
+ return response.status === 204 ? {} : response.json();
74
+ }
75
+
76
+ async embed(input) {
77
+ const result = await this.request(`${this.ollamaUrl}/api/embed`, {
78
+ method: "POST",
79
+ headers: { "content-type": "application/json" },
80
+ body: JSON.stringify({ model: this.model, input }),
81
+ });
82
+ return result.embeddings;
83
+ }
84
+
85
+ async loadManifest() {
86
+ try { return JSON.parse(await readFile(this.manifestFile, "utf8")); } catch (error) { if (error.code === "ENOENT") return {}; throw error; }
87
+ }
88
+
89
+ async saveManifest(value) {
90
+ await mkdir(path.dirname(this.manifestFile), { recursive: true, mode: 0o750 });
91
+ const temporary = `${this.manifestFile}.${process.pid}.tmp`;
92
+ await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o640 });
93
+ await rename(temporary, this.manifestFile);
94
+ }
95
+
96
+ async ensureIndexed(directory, repository) {
97
+ const previous = this.#locks.get(repository) ?? Promise.resolve();
98
+ const current = previous.catch(() => {}).then(() => this.#index(directory, repository));
99
+ this.#locks.set(repository, current);
100
+ try { return await current; } finally { if (this.#locks.get(repository) === current) this.#locks.delete(repository); }
101
+ }
102
+
103
+ async #index(directory, repository) {
104
+ const commit = (await run("git", ["-C", directory, "rev-parse", "HEAD"])).stdout.trim();
105
+ const manifest = await this.loadManifest();
106
+ if (manifest[repository]?.commit === commit && manifest[repository]?.model === this.model) return false;
107
+ const files = [];
108
+ await collectFiles(directory, directory, files, { maxFiles: 2000, maxFileBytes: 256_000 });
109
+ const chunks = [];
110
+ for (const file of files) {
111
+ const value = await readFile(file.absolute, "utf8");
112
+ chunks.push(...chunkText(file.relative, value));
113
+ if (chunks.length >= 5000) break;
114
+ }
115
+ const selected = chunks.slice(0, 5000);
116
+ if (selected.length === 0) return false;
117
+ const sample = await this.embed([`${selected[0].path}\n${selected[0].content}`]);
118
+ const vectorSize = sample[0].length;
119
+ const collectionResponse = await this.fetch(`${this.qdrantUrl}/collections/${this.collection}`, { signal: AbortSignal.timeout(30_000) });
120
+ if (collectionResponse.status === 404) {
121
+ await this.request(`${this.qdrantUrl}/collections/${this.collection}`, {
122
+ method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ vectors: { size: vectorSize, distance: "Cosine" } }),
123
+ });
124
+ } else if (!collectionResponse.ok) throw new Error(`Qdrant collection HTTP ${collectionResponse.status}`);
125
+ await this.request(`${this.qdrantUrl}/collections/${this.collection}/points/delete?wait=true`, {
126
+ method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ filter: { must: [{ key: "repository", match: { value: repository } }] } }),
127
+ });
128
+ for (let start = 0; start < selected.length; start += 32) {
129
+ const batch = selected.slice(start, start + 32);
130
+ const remaining = batch.slice(1);
131
+ const embeddings = start === 0
132
+ ? [sample[0], ...(remaining.length ? await this.embed(remaining.map((item) => `${item.path}\n${item.content}`)) : [])]
133
+ : await this.embed(batch.map((item) => `${item.path}\n${item.content}`));
134
+ const points = batch.map((item, index) => ({ id: pointId(`${repository}:${commit}:${item.path}:${item.startLine}`), vector: embeddings[index], payload: { repository, commit, ...item } }));
135
+ await this.request(`${this.qdrantUrl}/collections/${this.collection}/points?wait=true`, {
136
+ method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ points }),
137
+ });
138
+ }
139
+ manifest[repository] = { commit, model: this.model, chunks: selected.length, indexedAt: new Date().toISOString() };
140
+ await this.saveManifest(manifest);
141
+ return true;
142
+ }
143
+
144
+ async context(directory, repository, query, { limit = 8, maxCharacters = 12_000 } = {}) {
145
+ await this.ensureIndexed(directory, repository);
146
+ const [vector] = await this.embed([query]);
147
+ const result = await this.request(`${this.qdrantUrl}/collections/${this.collection}/points/query`, {
148
+ method: "POST",
149
+ headers: { "content-type": "application/json" },
150
+ body: JSON.stringify({ query: vector, filter: { must: [{ key: "repository", match: { value: repository } }] }, limit, with_payload: true }),
151
+ });
152
+ return formatRetrievedContext(result.result?.points ?? [], maxCharacters);
153
+ }
154
+ }
package/src/setup-menu.js CHANGED
@@ -5,6 +5,8 @@ import { writeFile } from "node:fs/promises";
5
5
  const output = process.argv[2];
6
6
  if (!output) throw new Error("Setup result path is required");
7
7
 
8
+ const factoryName = await input({ message: "What should your factory be called?", default: "Factory AI" });
9
+ const factoryPurpose = await input({ message: "What should this factory build or optimize for?", default: "Ship secure, reviewed software continuously" });
8
10
  const provider = await select({
9
11
  message: "Which model provider should the factory configure?",
10
12
  choices: [
@@ -23,4 +25,4 @@ if (provider !== "azure") {
23
25
  }
24
26
  const deployNow = await confirm({ message: "Deploy and start the runtime after storing credentials?", default: true });
25
27
  const telegram = await confirm({ message: "Enable Telegram remote objective intake?", default: false });
26
- await writeFile(output, `${JSON.stringify({ provider, location, githubOrg, awsRegion, bedrockBuilderModel, deployNow, telegram })}\n`, { mode: 0o600 });
28
+ await writeFile(output, `${JSON.stringify({ factoryName, factoryPurpose, provider, location, githubOrg, awsRegion, bedrockBuilderModel, deployNow, telegram })}\n`, { mode: 0o600 });
@@ -70,7 +70,7 @@ async function statusText() {
70
70
  const [{ states }, queue] = await Promise.all([loadLocalState(config.stateDir), loadQueueMetrics(config)]);
71
71
  const counts = {};
72
72
  for (const state of states) counts[state.status ?? "unknown"] = (counts[state.status ?? "unknown"] ?? 0) + 1;
73
- return [`Factory AI`, `Queue: ${queue.active} active, ${queue.deadLetter} dead-letter`, `Objectives: ${Object.entries(counts).map(([name, count]) => `${name} ${count}`).join(", ") || "none"}`].join("\n");
73
+ return [config.factoryName, `Queue: ${queue.active} active, ${queue.deadLetter} dead-letter`, `Objectives: ${Object.entries(counts).map(([name, count]) => `${name} ${count}`).join(", ") || "none"}`].join("\n");
74
74
  }
75
75
 
76
76
  async function processUpdate(update) {
@@ -93,7 +93,7 @@ async function processUpdate(update) {
93
93
  }
94
94
  if (command.type === "objective") {
95
95
  const state = JSON.parse(await readFile(path.join(config.stateDir, command.objectiveId, "state.json"), "utf8"));
96
- return reply(message.chat.id, formatObjectiveProgress(state));
96
+ return reply(message.chat.id, formatObjectiveProgress(state, config.factoryName));
97
97
  }
98
98
  const objective = objectiveFromTelegram(update.update_id, command);
99
99
  await sendMessage(sender, objective, objective.id);
@@ -111,7 +111,7 @@ async function notifyProgress() {
111
111
  let state;
112
112
  try { state = JSON.parse(await readFile(path.join(config.stateDir, objectiveId, "state.json"), "utf8")); }
113
113
  catch (error) { if (error.code === "ENOENT") continue; throw error; }
114
- const text = formatObjectiveProgress(state);
114
+ const text = formatObjectiveProgress(state, config.factoryName);
115
115
  const digest = createHash("sha256").update(text).digest("hex");
116
116
  if (digest === subscription.lastDigest) continue;
117
117
  await reply(subscription.chatId, text);
package/src/telegram.js CHANGED
@@ -38,12 +38,12 @@ export function objectiveFromTelegram(updateId, command, now = new Date()) {
38
38
  };
39
39
  }
40
40
 
41
- export function formatObjectiveProgress(state) {
41
+ export function formatObjectiveProgress(state, factoryName = "Factory AI") {
42
42
  const tasks = state.tasks ?? [];
43
43
  const results = state.results ?? {};
44
44
  const complete = tasks.filter((task) => results[task.id]?.status === "succeeded").length;
45
45
  const lines = [
46
- `Factory AI objective ${state.objective?.id ?? "unknown"}`,
46
+ `${factoryName} objective ${state.objective?.id ?? "unknown"}`,
47
47
  state.objective?.objective ?? "",
48
48
  `Status: ${state.status ?? "unknown"}`,
49
49
  `${complete}/${tasks.length} tasks complete`,
package/src/tui.js CHANGED
@@ -5,10 +5,12 @@ import { createOperator } from "./operator.js";
5
5
  if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("Factory AI UI requires an interactive terminal");
6
6
 
7
7
  const operator = createOperator();
8
- const screen = blessed.screen({ smartCSR: true, title: "Factory AI", fullUnicode: true, dockBorders: true });
8
+ const factoryName = process.env.FACTORY_NAME ?? "Factory AI";
9
+ const factoryPurpose = process.env.FACTORY_PURPOSE ?? "Ship secure reviewed software continuously";
10
+ const screen = blessed.screen({ smartCSR: true, title: factoryName, fullUnicode: true, dockBorders: true });
9
11
  const colors = { bg: "#0d0f12", panel: "#15191f", border: "#303743", text: "#d9e0e8", muted: "#7f8b99", accent: "#78dba9", warn: "#efc46b", danger: "#ef7d7d", blue: "#77a8ff" };
10
12
 
11
- blessed.box({ parent: screen, top: 0, left: 0, width: "100%", height: 3, tags: true, style: { bg: colors.panel, fg: colors.text }, content: " {bold}{#78dba9-fg}FACTORY AI{/} {/bold} private coding organization" });
13
+ blessed.box({ parent: screen, top: 0, left: 0, width: "100%", height: 3, tags: true, style: { bg: colors.panel, fg: colors.text }, content: ` {bold}{#78dba9-fg}${factoryName.toUpperCase()}{/} {/bold} ${factoryPurpose}` });
12
14
  const menu = blessed.list({ parent: screen, top: 3, left: 0, width: 23, bottom: 2, border: { type: "line" }, label: " Navigate ", keys: true, mouse: true, vi: true, items: ["Overview", "Objectives", "Agents", "Secrets", "Capabilities", "Logs", "Settings"], style: { bg: colors.panel, fg: colors.text, border: { fg: colors.border }, selected: { bg: colors.accent, fg: "#07130d", bold: true }, item: { fg: colors.text } } });
13
15
  const main = blessed.box({ parent: screen, top: 3, left: 23, right: 0, bottom: 2, border: { type: "line" }, label: " Overview ", tags: true, scrollable: true, alwaysScroll: true, keys: true, mouse: true, vi: true, scrollbar: { ch: "▐", style: { fg: colors.accent } }, padding: { left: 2, right: 2 }, style: { bg: colors.bg, fg: colors.text, border: { fg: colors.border } } });
14
16
  const footer = blessed.box({ parent: screen, bottom: 0, left: 0, width: "100%", height: 2, tags: true, style: { bg: colors.panel, fg: colors.muted }, content: " {bold}n{/} new objective {bold}r{/} refresh {bold}a{/} add secret {bold}p/u{/} pause/resume {bold}q{/} quit" });
package/src/worker.js CHANGED
@@ -9,6 +9,7 @@ import { AgentExecutor } from "./agent-executor.js";
9
9
  import { sendMessage } from "./bus.js";
10
10
  import { ContainerAgentRunner } from "./container-runner.js";
11
11
  import { ScannerSuite } from "./scanner-suite.js";
12
+ import { LocalRetriever } from "./retriever.js";
12
13
 
13
14
  process.title = "factory-ai-worker";
14
15
  const config = loadConfig();
@@ -20,6 +21,7 @@ const executor = new AgentExecutor({
20
21
  workspaces: new WorkspaceManager(config.workspaceDir, config.timeoutMs),
21
22
  agentRunner: new ContainerAgentRunner({ image: config.workerImage, memoryDir: config.memoryDir, timeoutMs: config.timeoutMs }),
22
23
  scannerSuite: new ScannerSuite(),
24
+ retriever: new LocalRetriever({ stateDir: config.stateDir }),
23
25
  sendControl,
24
26
  });
25
27