factory-ai 1.2.1 → 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 +16 -0
- package/README.md +2 -0
- package/bin/factory +16 -3
- package/bootstrap/agent-factory-control.service +1 -0
- package/bootstrap/agent-factory-release.service +1 -0
- package/bootstrap/agent-factory-reporter.service +1 -0
- package/bootstrap/agent-factory-telegram.service +1 -0
- package/bootstrap/agent-factory-worker.service +3 -2
- package/bootstrap/deploy-runtime.sh +4 -1
- package/bootstrap/factory-ai-ollama.service +16 -0
- package/bootstrap/factory-ai-qdrant.service +16 -0
- package/bootstrap/factory-ai-snapshot.service +18 -0
- package/bootstrap/factory-ai-snapshot.timer +11 -0
- package/bootstrap/factory-ai-update.service +1 -0
- package/bootstrap/setup.sh +23 -1
- package/infra/main.bicep +59 -0
- package/package.json +3 -1
- package/src/agent-executor.js +12 -2
- package/src/agent-runner.js +4 -2
- package/src/azure-harness.js +4 -0
- package/src/bedrock-harness.js +1 -1
- package/src/ceo.js +2 -0
- package/src/config.js +6 -0
- package/src/container-runner.js +2 -1
- package/src/control-service.js +1 -0
- package/src/operator.js +15 -1
- package/src/release-service.js +1 -0
- package/src/release.js +1 -1
- package/src/reporter.js +22 -3
- package/src/retriever.js +154 -0
- package/src/setup-menu.js +3 -1
- package/src/snapshot.js +15 -0
- package/src/task-entry.js +1 -0
- package/src/telegram-service.js +4 -3
- package/src/telegram.js +2 -2
- package/src/tui.js +5 -3
- package/src/updater.js +2 -0
- package/src/worker.js +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,22 @@ 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
|
+
|
|
22
|
+
## [1.3.0] - 2026-07-13
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- RBAC-protected Azure Blob dashboard snapshots for lock-free multi-operator TUI access.
|
|
27
|
+
- One-minute snapshot timer and direct local Azure-identity reads.
|
|
28
|
+
- Factory AI process, container, journal, Azure, Bedrock, MCP, Git, and PR identity metadata.
|
|
29
|
+
|
|
14
30
|
## [1.2.1] - 2026-07-13
|
|
15
31
|
|
|
16
32
|
### Fixed
|
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
|
@@ -9,6 +9,9 @@ RESOURCE_GROUP=${FACTORY_RESOURCE_GROUP:-factory-ai-rg}
|
|
|
9
9
|
VM=${FACTORY_VM:-agent-factory-vm}
|
|
10
10
|
NAMESPACE=${FACTORY_SERVICE_BUS:-}
|
|
11
11
|
VAULT=${FACTORY_KEY_VAULT:-}
|
|
12
|
+
STORAGE=${FACTORY_STORAGE_ACCOUNT:-}
|
|
13
|
+
FACTORY_NAME=${FACTORY_NAME:-Factory AI}
|
|
14
|
+
FACTORY_PURPOSE=${FACTORY_PURPOSE:-Ship secure reviewed software continuously}
|
|
12
15
|
|
|
13
16
|
remote() {
|
|
14
17
|
az vm run-command invoke --resource-group "$RESOURCE_GROUP" --name "$VM" \
|
|
@@ -31,7 +34,7 @@ usage() {
|
|
|
31
34
|
' pause | resume'
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
command=${1:-
|
|
37
|
+
command=${1:-ui}
|
|
35
38
|
shift || true
|
|
36
39
|
case "$command" in
|
|
37
40
|
telegram)
|
|
@@ -73,7 +76,11 @@ case "$command" in
|
|
|
73
76
|
exec "$0" submit "$repo" "$objective"
|
|
74
77
|
;;
|
|
75
78
|
ui)
|
|
76
|
-
|
|
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"
|
|
77
84
|
;;
|
|
78
85
|
setup)
|
|
79
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
|
|
@@ -81,6 +88,8 @@ case "$command" in
|
|
|
81
88
|
trap 'rm -f "$choices"' EXIT
|
|
82
89
|
node "$FACTORY_ROOT/src/setup-menu.js" "$choices"
|
|
83
90
|
provider=$(jq -r .provider "$choices")
|
|
91
|
+
factory_name=$(jq -r .factoryName "$choices")
|
|
92
|
+
factory_purpose=$(jq -r .factoryPurpose "$choices")
|
|
84
93
|
location=$(jq -r .location "$choices")
|
|
85
94
|
enterprise_org=$(jq -r .githubOrg "$choices")
|
|
86
95
|
aws_region=$(jq -r .awsRegion "$choices")
|
|
@@ -96,6 +105,7 @@ case "$command" in
|
|
|
96
105
|
deployment=$(az deployment group create --name agent-factory-setup --resource-group "$RESOURCE_GROUP" --template-file "$FACTORY_ROOT/infra/main.bicep" --parameters adminSshKey="$(< "$key.pub")" operatorObjectId="$operator" --output json)
|
|
97
106
|
vault=$(jq -r '.properties.outputs.keyVaultName.value' <<<"$deployment")
|
|
98
107
|
namespace=$(jq -r '.properties.outputs.serviceBusNamespace.value' <<<"$deployment")
|
|
108
|
+
storage=$(jq -r '.properties.outputs.storageAccount.value' <<<"$deployment")
|
|
99
109
|
VAULT=$vault
|
|
100
110
|
NAMESPACE=$namespace
|
|
101
111
|
mkdir -p "$(dirname "$CONFIG_FILE")"
|
|
@@ -104,6 +114,9 @@ case "$command" in
|
|
|
104
114
|
printf 'FACTORY_VM=%q\n' agent-factory-vm
|
|
105
115
|
printf 'FACTORY_SERVICE_BUS=%q\n' "$namespace"
|
|
106
116
|
printf 'FACTORY_KEY_VAULT=%q\n' "$vault"
|
|
117
|
+
printf 'FACTORY_STORAGE_ACCOUNT=%q\n' "$storage"
|
|
118
|
+
printf 'FACTORY_NAME=%q\n' "$factory_name"
|
|
119
|
+
printf 'FACTORY_PURPOSE=%q\n' "$factory_purpose"
|
|
107
120
|
} > "$CONFIG_FILE"
|
|
108
121
|
chmod 0600 "$CONFIG_FILE"
|
|
109
122
|
ip=$(curl -fsS https://api.ipify.org)
|
|
@@ -151,7 +164,7 @@ case "$command" in
|
|
|
151
164
|
printf 'Azure foundation and credential vault ready. Config: %s\n' "$CONFIG_FILE"
|
|
152
165
|
if [[ $deploy_now == true ]]; then
|
|
153
166
|
source_ref=$(gh api repos/itsvedantkumar/factory-ai/commits/main --jq .sha)
|
|
154
|
-
parameters=(KEY_VAULT_NAME "$vault" SERVICE_BUS_NAMESPACE "$namespace" SERVICE_BUS_QUEUE code-tasks 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")
|
|
155
168
|
if [[ $provider == bedrock ]]; then
|
|
156
169
|
parameters+=(AWS_REGION "$aws_region")
|
|
157
170
|
for role in SCOUT PLANNER BUILDER TESTER DEBUGGER REVIEWER SECURITY RELEASE; do parameters+=("FACTORY_MODEL_$role" "bedrock/$bedrock_builder"); done
|
|
@@ -1,13 +1,14 @@
|
|
|
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
|
|
8
8
|
|
|
9
9
|
[Service]
|
|
10
10
|
Type=simple
|
|
11
|
+
SyslogIdentifier=factory-ai-worker
|
|
11
12
|
User=factory
|
|
12
13
|
Group=factory
|
|
13
14
|
WorkingDirectory=/opt/agent-factory/app
|
|
@@ -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|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
|
|
@@ -62,6 +62,9 @@ KEY_VAULT_NAME="$KEY_VAULT_NAME" \
|
|
|
62
62
|
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
|
+
FACTORY_STORAGE_ACCOUNT="${FACTORY_STORAGE_ACCOUNT:-}" \
|
|
66
|
+
FACTORY_NAME="${FACTORY_NAME:-Factory AI}" \
|
|
67
|
+
FACTORY_PURPOSE="${FACTORY_PURPOSE:-Ship secure reviewed software continuously}" \
|
|
65
68
|
AWS_REGION="${AWS_REGION:-}" \
|
|
66
69
|
FACTORY_MODEL_SCOUT="${FACTORY_MODEL_SCOUT:-}" \
|
|
67
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
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Publish Factory AI operator dashboard snapshot
|
|
3
|
+
After=network-online.target agent-factory-control.service
|
|
4
|
+
Wants=network-online.target
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
Type=oneshot
|
|
8
|
+
User=factory
|
|
9
|
+
Group=factory
|
|
10
|
+
WorkingDirectory=/opt/agent-factory/app
|
|
11
|
+
EnvironmentFile=/etc/agent-factory-control.env
|
|
12
|
+
ExecStart=/usr/bin/node /opt/agent-factory/app/src/snapshot.js
|
|
13
|
+
NoNewPrivileges=true
|
|
14
|
+
PrivateTmp=true
|
|
15
|
+
ProtectSystem=strict
|
|
16
|
+
ProtectHome=true
|
|
17
|
+
UMask=0027
|
|
18
|
+
SyslogIdentifier=factory-ai-snapshot
|
package/bootstrap/setup.sh
CHANGED
|
@@ -35,6 +35,7 @@ test "$(node --version | cut -d. -f1)" = "v20"
|
|
|
35
35
|
id "$FACTORY_USER" >/dev/null 2>&1 || useradd --system --create-home --shell /usr/sbin/nologin "$FACTORY_USER"
|
|
36
36
|
test -f "$APP_DIR/package-lock.json"
|
|
37
37
|
npm ci --omit=dev --prefix "$APP_DIR"
|
|
38
|
+
factory_version=$(node -p "require('$APP_DIR/package.json').version")
|
|
38
39
|
docker build --file "$APP_DIR/Dockerfile.worker" --tag "$FACTORY_WORKER_IMAGE" "$APP_DIR"
|
|
39
40
|
for scanner_image in \
|
|
40
41
|
aquasec/trivy@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f \
|
|
@@ -43,6 +44,8 @@ for scanner_image in \
|
|
|
43
44
|
semgrep/semgrep@sha256:183a149fb3e9700ab5294a7b4ab0241a826fd046bc8b721062fbea80fdfa438f; do
|
|
44
45
|
docker pull "$scanner_image"
|
|
45
46
|
done
|
|
47
|
+
docker pull ollama/ollama@sha256:3d8a05e3432d50ea57594fabe971e46cc8fe963a0f9f8c40400bd56cd5388e47
|
|
48
|
+
docker pull qdrant/qdrant@sha256:31407c0e8e32eb771b71718f1a4772e2ad47a07557917b21ac96792f40eb8007
|
|
46
49
|
|
|
47
50
|
STATE_DEVICE=/dev/disk/azure/scsi1/lun0
|
|
48
51
|
if [[ -b $STATE_DEVICE ]]; then
|
|
@@ -64,7 +67,8 @@ for secret_name in "${GITHUB_TOKEN_SECRET:-github-token}"; do
|
|
|
64
67
|
az keyvault secret show --vault-name "$KEY_VAULT_NAME" --name "$secret_name" --query id --output none
|
|
65
68
|
done
|
|
66
69
|
|
|
67
|
-
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
|
|
68
72
|
install -m 0600 -o root -g root /dev/null /etc/agent-factory.env
|
|
69
73
|
{
|
|
70
74
|
printf 'SERVICE_BUS_NAMESPACE=%s\n' "$SERVICE_BUS_NAMESPACE"
|
|
@@ -74,13 +78,20 @@ install -m 0600 -o root -g root /dev/null /etc/agent-factory.env
|
|
|
74
78
|
printf 'KEY_VAULT_NAME=%s\n' "$KEY_VAULT_NAME"
|
|
75
79
|
printf 'AZURE_SUBSCRIPTION_ID=%s\n' "$subscription_id"
|
|
76
80
|
printf 'FACTORY_RESOURCE_GROUP=%s\n' "$resource_group"
|
|
81
|
+
[[ -n ${FACTORY_STORAGE_ACCOUNT:-} ]] && printf 'FACTORY_STORAGE_ACCOUNT=%s\n' "$FACTORY_STORAGE_ACCOUNT"
|
|
77
82
|
printf 'FACTORY_STATE_DIR=/opt/agent-factory/state\n'
|
|
78
83
|
printf 'FACTORY_WORKSPACE_DIR=/opt/agent-factory/workspaces\n'
|
|
79
84
|
printf 'FACTORY_REGISTRY=%s/config/capabilities.json\n' "$APP_DIR"
|
|
80
85
|
printf 'FACTORY_WORKER_IMAGE=%s\n' "$FACTORY_WORKER_IMAGE"
|
|
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}"
|
|
81
89
|
printf 'MAX_CONCURRENCY=3\n'
|
|
82
90
|
printf 'TASK_TIMEOUT_MS=1800000\n'
|
|
83
91
|
printf 'MAX_DELIVERY_COUNT=8\n'
|
|
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}"
|
|
84
95
|
[[ -n ${AWS_REGION:-} ]] && printf 'AWS_REGION=%s\nAWS_DEFAULT_REGION=%s\n' "$AWS_REGION" "$AWS_REGION"
|
|
85
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
|
|
86
97
|
[[ -n ${!variable:-} ]] && printf '%s=%s\n' "$variable" "${!variable}"
|
|
@@ -96,6 +107,7 @@ install -m 0600 -o root -g root /dev/null /etc/agent-factory-control.env
|
|
|
96
107
|
printf 'KEY_VAULT_NAME=%s\n' "$KEY_VAULT_NAME"
|
|
97
108
|
printf 'AZURE_SUBSCRIPTION_ID=%s\n' "$subscription_id"
|
|
98
109
|
printf 'FACTORY_RESOURCE_GROUP=%s\n' "$resource_group"
|
|
110
|
+
[[ -n ${FACTORY_STORAGE_ACCOUNT:-} ]] && printf 'FACTORY_STORAGE_ACCOUNT=%s\n' "$FACTORY_STORAGE_ACCOUNT"
|
|
99
111
|
printf 'FACTORY_STATE_DIR=/opt/agent-factory/state\n'
|
|
100
112
|
printf 'FACTORY_REGISTRY=%s/config/capabilities.json\n' "$APP_DIR"
|
|
101
113
|
printf 'MAX_DELIVERY_COUNT=8\n'
|
|
@@ -106,6 +118,8 @@ install -m 0600 -o root -g root /dev/null /etc/agent-factory-control.env
|
|
|
106
118
|
} > /etc/agent-factory-control.env
|
|
107
119
|
|
|
108
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
|
|
109
123
|
chown -R root:root "$APP_DIR"
|
|
110
124
|
chmod -R go-w "$APP_DIR"
|
|
111
125
|
install -m 0644 "$APP_DIR/bootstrap/agent-factory-worker.service" /etc/systemd/system/agent-factory-worker.service
|
|
@@ -117,6 +131,10 @@ install -m 0644 "$APP_DIR/bootstrap/agent-factory-telegram.service" /etc/systemd
|
|
|
117
131
|
chmod 0755 "$APP_DIR/bootstrap/auto-update.sh"
|
|
118
132
|
install -m 0644 "$APP_DIR/bootstrap/factory-ai-update.service" /etc/systemd/system/factory-ai-update.service
|
|
119
133
|
install -m 0644 "$APP_DIR/bootstrap/factory-ai-update.timer" /etc/systemd/system/factory-ai-update.timer
|
|
134
|
+
install -m 0644 "$APP_DIR/bootstrap/factory-ai-snapshot.service" /etc/systemd/system/factory-ai-snapshot.service
|
|
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
|
|
120
138
|
systemctl daemon-reload
|
|
121
139
|
systemctl enable --now agent-factory-worker.service
|
|
122
140
|
systemctl enable --now agent-factory-control.service
|
|
@@ -124,5 +142,9 @@ systemctl enable --now agent-factory-release.service
|
|
|
124
142
|
systemctl enable --now agent-factory-reporter.timer
|
|
125
143
|
systemctl enable --now agent-factory-telegram.service
|
|
126
144
|
systemctl enable --now factory-ai-update.timer
|
|
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
|
|
127
149
|
systemctl restart agent-factory-control.service agent-factory-worker.service agent-factory-release.service
|
|
128
150
|
echo "Agent factory worker installed"
|
package/infra/main.bicep
CHANGED
|
@@ -20,6 +20,7 @@ var prefix = 'agent-factory'
|
|
|
20
20
|
var unique = uniqueString(subscription().subscriptionId, resourceGroup().id)
|
|
21
21
|
var keyVaultName = 'af${take(unique, 18)}'
|
|
22
22
|
var serviceBusName = 'af-${unique}'
|
|
23
|
+
var storageName = 'fa${take(unique, 20)}'
|
|
23
24
|
|
|
24
25
|
resource nsg 'Microsoft.Network/networkSecurityGroups@2024-05-01' = {
|
|
25
26
|
name: '${prefix}-nsg'
|
|
@@ -147,6 +148,43 @@ resource vault 'Microsoft.KeyVault/vaults@2024-11-01' = {
|
|
|
147
148
|
}
|
|
148
149
|
}
|
|
149
150
|
|
|
151
|
+
resource storage 'Microsoft.Storage/storageAccounts@2025-01-01' = {
|
|
152
|
+
name: storageName
|
|
153
|
+
location: location
|
|
154
|
+
sku: {
|
|
155
|
+
name: 'Standard_ZRS'
|
|
156
|
+
}
|
|
157
|
+
kind: 'StorageV2'
|
|
158
|
+
properties: {
|
|
159
|
+
allowBlobPublicAccess: false
|
|
160
|
+
allowSharedKeyAccess: false
|
|
161
|
+
defaultToOAuthAuthentication: true
|
|
162
|
+
minimumTlsVersion: 'TLS1_2'
|
|
163
|
+
publicNetworkAccess: 'Enabled'
|
|
164
|
+
supportsHttpsTrafficOnly: true
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2025-01-01' = {
|
|
169
|
+
parent: storage
|
|
170
|
+
name: 'default'
|
|
171
|
+
properties: {
|
|
172
|
+
deleteRetentionPolicy: {
|
|
173
|
+
enabled: true
|
|
174
|
+
days: 30
|
|
175
|
+
}
|
|
176
|
+
isVersioningEnabled: true
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
resource operatorContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2025-01-01' = {
|
|
181
|
+
parent: blobService
|
|
182
|
+
name: 'operator'
|
|
183
|
+
properties: {
|
|
184
|
+
publicAccess: 'None'
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
150
188
|
resource serviceBus 'Microsoft.ServiceBus/namespaces@2024-01-01' = {
|
|
151
189
|
name: serviceBusName
|
|
152
190
|
location: location
|
|
@@ -299,11 +337,32 @@ resource workerCostReader 'Microsoft.Authorization/roleAssignments@2022-04-01' =
|
|
|
299
337
|
}
|
|
300
338
|
}
|
|
301
339
|
|
|
340
|
+
resource workerBlobContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
|
|
341
|
+
name: guid(storage.id, vm.id, 'storage-blob-data-contributor')
|
|
342
|
+
scope: storage
|
|
343
|
+
properties: {
|
|
344
|
+
principalId: vm.identity.principalId
|
|
345
|
+
principalType: 'ServicePrincipal'
|
|
346
|
+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
resource operatorBlobReader 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
|
|
351
|
+
name: guid(storage.id, operatorObjectId, 'storage-blob-data-reader')
|
|
352
|
+
scope: storage
|
|
353
|
+
properties: {
|
|
354
|
+
principalId: operatorObjectId
|
|
355
|
+
principalType: 'User'
|
|
356
|
+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1')
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
302
360
|
output vmName string = vm.name
|
|
303
361
|
output privateIp string = nic.properties.ipConfigurations[0].properties.privateIPAddress
|
|
304
362
|
output egressIp string = egressIp.properties.ipAddress
|
|
305
363
|
output keyVaultName string = vault.name
|
|
306
364
|
output serviceBusNamespace string = serviceBus.name
|
|
365
|
+
output storageAccount string = storage.name
|
|
307
366
|
output controlQueue string = queues[0].name
|
|
308
367
|
output agentQueue string = queues[1].name
|
|
309
368
|
output releaseQueue string = queues[2].name
|
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",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"agent-factory-release": "src/release-service.js",
|
|
41
41
|
"agent-factory-task": "src/task-entry.js",
|
|
42
42
|
"factory-ai-telegram": "src/telegram-service.js",
|
|
43
|
+
"factory-ai-snapshot": "src/snapshot.js",
|
|
43
44
|
"agent-factory-worker": "src/worker.js"
|
|
44
45
|
},
|
|
45
46
|
"files": [
|
|
@@ -63,6 +64,7 @@
|
|
|
63
64
|
"@azure/identity": "4.13.1",
|
|
64
65
|
"@azure/keyvault-secrets": "4.10.0",
|
|
65
66
|
"@azure/service-bus": "7.9.5",
|
|
67
|
+
"@azure/storage-blob": "12.28.0",
|
|
66
68
|
"@inquirer/prompts": "8.5.2",
|
|
67
69
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
68
70
|
"@modelcontextprotocol/server-memory": "2026.7.4",
|
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/azure-harness.js
CHANGED
|
@@ -31,6 +31,7 @@ export class AzureResponsesHarness {
|
|
|
31
31
|
retries = 4,
|
|
32
32
|
timeoutMs = 180_000,
|
|
33
33
|
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
34
|
+
userAgent = `factory-ai/${process.env.FACTORY_VERSION ?? "dev"}`,
|
|
34
35
|
}) {
|
|
35
36
|
this.endpoint = `${baseUrl.replace(/\/$/, "")}/responses`;
|
|
36
37
|
this.apiKey = apiKey;
|
|
@@ -42,6 +43,7 @@ export class AzureResponsesHarness {
|
|
|
42
43
|
this.retries = retries;
|
|
43
44
|
this.timeoutMs = timeoutMs;
|
|
44
45
|
this.sleep = sleep;
|
|
46
|
+
this.userAgent = userAgent;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
async request(body) {
|
|
@@ -56,6 +58,8 @@ export class AzureResponsesHarness {
|
|
|
56
58
|
"content-type": "application/json",
|
|
57
59
|
"api-key": this.apiKey,
|
|
58
60
|
authorization: `Bearer ${this.apiKey}`,
|
|
61
|
+
"user-agent": this.userAgent,
|
|
62
|
+
"x-ms-useragent": this.userAgent,
|
|
59
63
|
},
|
|
60
64
|
body: JSON.stringify(body),
|
|
61
65
|
signal: controller.signal,
|
package/src/bedrock-harness.js
CHANGED
|
@@ -12,7 +12,7 @@ function definitions(tools) {
|
|
|
12
12
|
|
|
13
13
|
export class BedrockHarness {
|
|
14
14
|
constructor({ client, region = process.env.AWS_REGION ?? "us-east-1", model, tools, maxSteps = 40, maxOutputTokens = 4096 }) {
|
|
15
|
-
this.client = client ?? new BedrockRuntimeClient({ region });
|
|
15
|
+
this.client = client ?? new BedrockRuntimeClient({ region, customUserAgent: `factory-ai/${process.env.FACTORY_VERSION ?? "dev"}` });
|
|
16
16
|
this.model = model;
|
|
17
17
|
this.tools = tools;
|
|
18
18
|
this.maxSteps = maxSteps;
|
package/src/ceo.js
CHANGED
|
@@ -6,6 +6,8 @@ import { loadConfig } from "./config.js";
|
|
|
6
6
|
import { createBus, sendMessage } from "./bus.js";
|
|
7
7
|
import { parseObjective } from "./validation.js";
|
|
8
8
|
|
|
9
|
+
process.title = "factory-ai-ceo";
|
|
10
|
+
|
|
9
11
|
function argumentsFrom(argv) {
|
|
10
12
|
const options = { baseBranch: "main", wait: false, timeoutMs: 3_600_000, words: [] };
|
|
11
13
|
for (let index = 0; index < argv.length; index += 1) {
|
package/src/config.js
CHANGED
|
@@ -9,6 +9,9 @@ const environmentSchema = z.object({
|
|
|
9
9
|
KEY_VAULT_NAME: z.string().min(3),
|
|
10
10
|
AZURE_SUBSCRIPTION_ID: z.string().optional(),
|
|
11
11
|
FACTORY_RESOURCE_GROUP: z.string().default("factory-ai-rg"),
|
|
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"),
|
|
12
15
|
FACTORY_STATE_DIR: z.string().default("/opt/agent-factory/state"),
|
|
13
16
|
FACTORY_WORKSPACE_DIR: z.string().default("/opt/agent-factory/workspaces"),
|
|
14
17
|
FACTORY_REGISTRY: z.string().default("/opt/agent-factory/app/config/capabilities.json"),
|
|
@@ -41,6 +44,9 @@ export function loadConfig(environment = process.env) {
|
|
|
41
44
|
keyVaultUrl: `https://${env.KEY_VAULT_NAME}.vault.azure.net`,
|
|
42
45
|
subscriptionId: env.AZURE_SUBSCRIPTION_ID,
|
|
43
46
|
resourceGroup: env.FACTORY_RESOURCE_GROUP,
|
|
47
|
+
storageAccount: env.FACTORY_STORAGE_ACCOUNT,
|
|
48
|
+
factoryName: env.FACTORY_NAME,
|
|
49
|
+
factoryPurpose: env.FACTORY_PURPOSE,
|
|
44
50
|
secretNames: {
|
|
45
51
|
TEXTVED_AZURE_API_KEY: env.AZURE_PRIMARY_API_KEY_SECRET,
|
|
46
52
|
TEXTVED_AZURE_BASE_URL: env.AZURE_PRIMARY_BASE_URL_SECRET,
|
package/src/container-runner.js
CHANGED
|
@@ -19,6 +19,7 @@ const AZURE_ENVIRONMENT = [
|
|
|
19
19
|
"FACTORY_MODEL_REVIEWER",
|
|
20
20
|
"FACTORY_MODEL_SECURITY",
|
|
21
21
|
"FACTORY_MODEL_RELEASE",
|
|
22
|
+
"FACTORY_VERSION",
|
|
22
23
|
];
|
|
23
24
|
|
|
24
25
|
function parseOutput(stdout) {
|
|
@@ -37,7 +38,7 @@ export class ContainerAgentRunner {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
async executePacket(packet, directory) {
|
|
40
|
-
const name = `
|
|
41
|
+
const name = `factory-ai-${packet.objective.id}-${packet.task?.id ?? "planner"}`.toLowerCase().replaceAll(/[^a-z0-9_.-]/g, "-").slice(0, 63);
|
|
41
42
|
const args = [
|
|
42
43
|
"run", "-i", "--rm", "--name", name,
|
|
43
44
|
"--read-only",
|
package/src/control-service.js
CHANGED
|
@@ -7,6 +7,7 @@ import { loadRegistry } from "./registry.js";
|
|
|
7
7
|
import { log } from "./log.js";
|
|
8
8
|
import { ProjectMemory } from "./project-memory.js";
|
|
9
9
|
|
|
10
|
+
process.title = "factory-ai-control";
|
|
10
11
|
const config = loadConfig();
|
|
11
12
|
const bus = createBus(config, config.controlQueue, config.agentQueue);
|
|
12
13
|
const releaseSender = bus.client.createSender(config.releaseQueue);
|
package/src/operator.js
CHANGED
|
@@ -2,6 +2,8 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { gunzipSync } from "node:zlib";
|
|
5
|
+
import { DefaultAzureCredential } from "@azure/identity";
|
|
6
|
+
import { BlobServiceClient } from "@azure/storage-blob";
|
|
5
7
|
import { run } from "./process.js";
|
|
6
8
|
|
|
7
9
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -21,6 +23,9 @@ export function createOperator(environment = process.env) {
|
|
|
21
23
|
const vm = environment.FACTORY_VM ?? "agent-factory-vm";
|
|
22
24
|
const namespace = environment.FACTORY_SERVICE_BUS ?? "";
|
|
23
25
|
const vault = environment.FACTORY_KEY_VAULT ?? "";
|
|
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";
|
|
24
29
|
const remote = async (script) => {
|
|
25
30
|
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
26
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"])); }
|
|
@@ -41,6 +46,15 @@ export function createOperator(environment = process.env) {
|
|
|
41
46
|
};
|
|
42
47
|
return {
|
|
43
48
|
dashboard: async () => {
|
|
49
|
+
if (storageAccount) {
|
|
50
|
+
try {
|
|
51
|
+
const service = new BlobServiceClient(`https://${storageAccount}.blob.core.windows.net`, new DefaultAzureCredential());
|
|
52
|
+
const value = await service.getContainerClient("operator").getBlockBlobClient("dashboard.json").downloadToBuffer();
|
|
53
|
+
return JSON.parse(value.toString("utf8"));
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (!["BlobNotFound", "ContainerNotFound"].includes(error.code)) throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
44
58
|
const encoded = await remote("sudo -u factory env $(xargs < /etc/agent-factory-control.env) node /opt/agent-factory/app/src/dashboard.js --json | gzip -c | base64 -w0");
|
|
45
59
|
return JSON.parse(gunzipSync(Buffer.from(encoded, "base64")).toString("utf8"));
|
|
46
60
|
},
|
|
@@ -56,7 +70,7 @@ export function createOperator(environment = process.env) {
|
|
|
56
70
|
return command(path.join(root, "bin/factory"), [action]);
|
|
57
71
|
},
|
|
58
72
|
capabilities: async () => JSON.parse(await readFile(path.join(root, "config/capabilities.json"), "utf8")),
|
|
59
|
-
config: () => ({ resourceGroup, vm, namespace, vault, 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" } }),
|
|
60
74
|
listSecrets: async () => withVault(async () => JSON.parse(await command("az", ["keyvault", "secret", "list", "--vault-name", vault, "--query", "[].{name:name,updated:attributes.updated}", "--output", "json"]))),
|
|
61
75
|
setSecret: async (name, value) => withVault(async () => {
|
|
62
76
|
if (!/^[A-Za-z0-9-]{1,127}$/.test(name) || !value) throw new Error("Valid secret name and value are required");
|
package/src/release-service.js
CHANGED
|
@@ -7,6 +7,7 @@ import { ReleaseBot } from "./release-bot.js";
|
|
|
7
7
|
import { GitHubRelease } from "./release.js";
|
|
8
8
|
import { log } from "./log.js";
|
|
9
9
|
|
|
10
|
+
process.title = "factory-ai-release";
|
|
10
11
|
const config = loadConfig();
|
|
11
12
|
Object.assign(process.env, await loadRuntimeSecrets(config, undefined, ["GH_TOKEN"]));
|
|
12
13
|
await run("gh", ["auth", "setup-git"], { timeoutMs: 60_000 });
|
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
|
@@ -4,11 +4,15 @@ import path from "node:path";
|
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { aggregateDashboard, loadAzureCost, loadLocalState, loadQueueMetrics, stableStringify } from "./dashboard.js";
|
|
6
6
|
import { loadConfig } from "./config.js";
|
|
7
|
+
import { DefaultAzureCredential } from "@azure/identity";
|
|
8
|
+
import { BlobServiceClient } from "@azure/storage-blob";
|
|
9
|
+
|
|
10
|
+
process.title = "factory-ai-reporter";
|
|
7
11
|
|
|
8
12
|
function markdown(dashboard) {
|
|
9
13
|
const objectives = Object.entries(dashboard.summary.objectives).map(([state, count]) => `${state}=${count}`).join(", ") || "none";
|
|
10
14
|
const cost = dashboard.cost ? `\nAzure month-to-date: ${dashboard.cost.currency} ${dashboard.cost.monthToDate.toFixed(2)} (billing data may be delayed)\n` : "";
|
|
11
|
-
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`;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
async function atomicWrite(file, content) {
|
|
@@ -30,16 +34,31 @@ export async function writeHourlyReport(root, dashboard, { now = new Date(), ret
|
|
|
30
34
|
return stem;
|
|
31
35
|
}
|
|
32
36
|
|
|
37
|
+
export async function uploadDashboardSnapshot(config, dashboard, {
|
|
38
|
+
credential = new DefaultAzureCredential(),
|
|
39
|
+
createClient = (url, auth) => new BlobServiceClient(url, auth),
|
|
40
|
+
} = {}) {
|
|
41
|
+
if (!config.storageAccount) return false;
|
|
42
|
+
const service = createClient(`https://${config.storageAccount}.blob.core.windows.net`, credential);
|
|
43
|
+
const blob = service.getContainerClient("operator").getBlockBlobClient("dashboard.json");
|
|
44
|
+
await blob.uploadData(Buffer.from(stableStringify(dashboard)), {
|
|
45
|
+
blobHTTPHeaders: { blobContentType: "application/json; charset=utf-8", blobCacheControl: "no-store" },
|
|
46
|
+
});
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
async function main() {
|
|
34
51
|
const root = process.env.FACTORY_STATE_DIR ?? "/opt/agent-factory/state";
|
|
52
|
+
const config = loadConfig();
|
|
35
53
|
const loaded = await loadLocalState(root);
|
|
36
|
-
const queue = process.env.SERVICE_BUS_NAMESPACE ? await loadQueueMetrics(
|
|
54
|
+
const queue = process.env.SERVICE_BUS_NAMESPACE ? await loadQueueMetrics(config) : {};
|
|
37
55
|
let cost = null;
|
|
38
56
|
if (process.env.AZURE_SUBSCRIPTION_ID) {
|
|
39
|
-
try { cost = await loadAzureCost(
|
|
57
|
+
try { cost = await loadAzureCost(config); } catch (error) { loaded.warnings.push(`Cost unavailable: ${error.message}`); }
|
|
40
58
|
}
|
|
41
59
|
const dashboard = aggregateDashboard({ ...loaded, queue, cost, runtime: { status: "running" } });
|
|
42
60
|
const stem = await writeHourlyReport(root, dashboard);
|
|
61
|
+
await uploadDashboardSnapshot(config, dashboard);
|
|
43
62
|
process.stdout.write(`${JSON.stringify({ event: "hourly_report", report: stem })}\n`);
|
|
44
63
|
}
|
|
45
64
|
|
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/snapshot.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { aggregateDashboard, loadAzureCost, loadLocalState, loadQueueMetrics } from "./dashboard.js";
|
|
3
|
+
import { loadConfig } from "./config.js";
|
|
4
|
+
import { uploadDashboardSnapshot } from "./reporter.js";
|
|
5
|
+
|
|
6
|
+
process.title = "factory-ai-snapshot";
|
|
7
|
+
const config = loadConfig();
|
|
8
|
+
const root = process.env.FACTORY_STATE_DIR ?? "/opt/agent-factory/state";
|
|
9
|
+
const loaded = await loadLocalState(root);
|
|
10
|
+
const queue = await loadQueueMetrics(config);
|
|
11
|
+
let cost = null;
|
|
12
|
+
try { cost = await loadAzureCost(config); } catch (error) { loaded.warnings.push(`Cost unavailable: ${error.message}`); }
|
|
13
|
+
const dashboard = aggregateDashboard({ ...loaded, queue, cost, runtime: { status: "running" } });
|
|
14
|
+
await uploadDashboardSnapshot(config, dashboard);
|
|
15
|
+
process.stdout.write(`${JSON.stringify({ event: "dashboard_snapshot", generatedAt: dashboard.generatedAt })}\n`);
|
package/src/task-entry.js
CHANGED
|
@@ -10,6 +10,7 @@ const input = await new Promise((resolve, reject) => {
|
|
|
10
10
|
process.stdin.on("error", reject);
|
|
11
11
|
});
|
|
12
12
|
const packet = JSON.parse(input);
|
|
13
|
+
process.title = `factory-ai-${packet.task?.role ?? packet.mode}`.slice(0, 63);
|
|
13
14
|
const registry = await loadRegistry("/opt/agent-factory/app/config/capabilities.json");
|
|
14
15
|
const runner = new AzureAgentRunner({ timeoutMs: 1_800_000 }, registry);
|
|
15
16
|
let result;
|
package/src/telegram-service.js
CHANGED
|
@@ -11,6 +11,7 @@ import { loadLocalState, loadQueueMetrics } from "./dashboard.js";
|
|
|
11
11
|
import { formatObjectiveProgress, isAllowedChat, objectiveFromTelegram, parseTelegramCommand } from "./telegram.js";
|
|
12
12
|
import { log } from "./log.js";
|
|
13
13
|
|
|
14
|
+
process.title = "factory-ai-telegram";
|
|
14
15
|
const config = loadConfig();
|
|
15
16
|
Object.assign(process.env, await loadRuntimeSecrets(config, undefined, ["TELEGRAM_BOT_TOKEN", "TELEGRAM_ALLOWED_CHAT_IDS"]));
|
|
16
17
|
const token = process.env.TELEGRAM_BOT_TOKEN;
|
|
@@ -69,7 +70,7 @@ async function statusText() {
|
|
|
69
70
|
const [{ states }, queue] = await Promise.all([loadLocalState(config.stateDir), loadQueueMetrics(config)]);
|
|
70
71
|
const counts = {};
|
|
71
72
|
for (const state of states) counts[state.status ?? "unknown"] = (counts[state.status ?? "unknown"] ?? 0) + 1;
|
|
72
|
-
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");
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
async function processUpdate(update) {
|
|
@@ -92,7 +93,7 @@ async function processUpdate(update) {
|
|
|
92
93
|
}
|
|
93
94
|
if (command.type === "objective") {
|
|
94
95
|
const state = JSON.parse(await readFile(path.join(config.stateDir, command.objectiveId, "state.json"), "utf8"));
|
|
95
|
-
return reply(message.chat.id, formatObjectiveProgress(state));
|
|
96
|
+
return reply(message.chat.id, formatObjectiveProgress(state, config.factoryName));
|
|
96
97
|
}
|
|
97
98
|
const objective = objectiveFromTelegram(update.update_id, command);
|
|
98
99
|
await sendMessage(sender, objective, objective.id);
|
|
@@ -110,7 +111,7 @@ async function notifyProgress() {
|
|
|
110
111
|
let state;
|
|
111
112
|
try { state = JSON.parse(await readFile(path.join(config.stateDir, objectiveId, "state.json"), "utf8")); }
|
|
112
113
|
catch (error) { if (error.code === "ENOENT") continue; throw error; }
|
|
113
|
-
const text = formatObjectiveProgress(state);
|
|
114
|
+
const text = formatObjectiveProgress(state, config.factoryName);
|
|
114
115
|
const digest = createHash("sha256").update(text).digest("hex");
|
|
115
116
|
if (digest === subscription.lastDigest) continue;
|
|
116
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" });
|
|
@@ -65,7 +67,7 @@ function renderCapabilities() {
|
|
|
65
67
|
|
|
66
68
|
function renderSettings() {
|
|
67
69
|
const config = operator.config();
|
|
68
|
-
main.setContent(`{bold}Runtime{/}\n\n Resource group ${config.resourceGroup}\n VM ${config.vm}\n Service Bus ${config.namespace}\n Key Vault ${config.vault}\n\n{bold}Model policy{/}\n\n Scout GPT-5.4 nano\n Simple builder Kimi K2.7-Code\n Builder GPT-5.5\n Tester GPT-5.4\n Critical roles GPT-5.6\n\n{#7f8b99-fg}Run factory setup to change providers or role routes.{/}`);
|
|
70
|
+
main.setContent(`{bold}Runtime{/}\n\n Resource group ${config.resourceGroup}\n VM ${config.vm}\n Service Bus ${config.namespace}\n Key Vault ${config.vault}\n Operator state ${config.storageAccount || "Run Command fallback"}\n\n{bold}Model policy{/}\n\n Scout GPT-5.4 nano\n Simple builder Kimi K2.7-Code\n Builder GPT-5.5\n Tester GPT-5.4\n Critical roles GPT-5.6\n\n{#7f8b99-fg}Run factory setup to change providers or role routes.{/}`);
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
function render() {
|
package/src/updater.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
|
|
5
|
+
process.title = "factory-ai-updater";
|
|
6
|
+
|
|
5
7
|
function parts(value) {
|
|
6
8
|
if (!/^\d+\.\d+\.\d+$/.test(value)) throw new Error(`Unsupported version: ${value}`);
|
|
7
9
|
return value.split(".").map(Number);
|
package/src/worker.js
CHANGED
|
@@ -9,7 +9,9 @@ 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
|
|
|
14
|
+
process.title = "factory-ai-worker";
|
|
13
15
|
const config = loadConfig();
|
|
14
16
|
Object.assign(process.env, await loadRuntimeSecrets(config));
|
|
15
17
|
await run("gh", ["auth", "setup-git"], { timeoutMs: 60_000 });
|
|
@@ -19,6 +21,7 @@ const executor = new AgentExecutor({
|
|
|
19
21
|
workspaces: new WorkspaceManager(config.workspaceDir, config.timeoutMs),
|
|
20
22
|
agentRunner: new ContainerAgentRunner({ image: config.workerImage, memoryDir: config.memoryDir, timeoutMs: config.timeoutMs }),
|
|
21
23
|
scannerSuite: new ScannerSuite(),
|
|
24
|
+
retriever: new LocalRetriever({ stateDir: config.stateDir }),
|
|
22
25
|
sendControl,
|
|
23
26
|
});
|
|
24
27
|
|