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 +8 -0
- package/README.md +2 -0
- package/bin/factory +13 -3
- package/bootstrap/agent-factory-worker.service +2 -2
- package/bootstrap/deploy-runtime.sh +3 -1
- package/bootstrap/factory-ai-ollama.service +16 -0
- package/bootstrap/factory-ai-qdrant.service +16 -0
- package/bootstrap/setup.sh +15 -1
- package/package.json +1 -1
- package/src/agent-executor.js +12 -2
- package/src/agent-runner.js +4 -2
- package/src/config.js +4 -0
- package/src/operator.js +3 -1
- package/src/release.js +1 -1
- package/src/reporter.js +1 -1
- package/src/retriever.js +154 -0
- package/src/setup-menu.js +3 -1
- package/src/telegram-service.js +3 -3
- package/src/telegram.js +2 -2
- package/src/tui.js +4 -2
- package/src/worker.js +2 -0
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:-
|
|
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
|
-
|
|
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
|
package/bootstrap/setup.sh
CHANGED
|
@@ -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
|
+
"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",
|
package/src/agent-executor.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/src/agent-runner.js
CHANGED
|
@@ -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
|
|
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
|
|
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) {
|
package/src/retriever.js
ADDED
|
@@ -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 });
|
package/src/telegram-service.js
CHANGED
|
@@ -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 [
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|