@osfactory/har 0.5.0 → 0.7.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/README.md +2 -0
- package/dist/index.js +1817 -651
- package/dist/prompts/system-authoring.md +27 -2
- package/dist/templates/adaptation-prompt-init.md +60 -5
- package/dist/templates/adaptation-prompt-maintain.md +7 -4
- package/dist/templates/har-boilerplate/CLAUDE.agent.md +5 -3
- package/dist/templates/har-boilerplate/README.md +51 -9
- package/dist/templates/har-boilerplate/agent-cli.sh +72 -26
- package/dist/templates/har-boilerplate/agent-slot.sh +351 -0
- package/dist/templates/har-boilerplate/attach.sh +7 -1
- package/dist/templates/har-boilerplate/ecosystem.agent.template.cjs +3 -1
- package/dist/templates/har-boilerplate/env.template +4 -1
- package/dist/templates/har-boilerplate/harness.env +32 -0
- package/dist/templates/har-boilerplate/launch.sh +167 -120
- package/dist/templates/har-boilerplate/preflight.sh +41 -0
- package/dist/templates/har-boilerplate/provision-toolchain.sh +285 -0
- package/dist/templates/har-boilerplate/setup-infra.sh +87 -11
- package/dist/templates/har-boilerplate/teardown.sh +2 -1
- package/dist/templates/har-boilerplate/verify.sh +91 -11
- package/dist/templates/har-boilerplate-cli/CLAUDE.agent.md +5 -2
- package/dist/templates/har-boilerplate-cli/README.md +40 -6
- package/dist/templates/har-boilerplate-cli/agent-cli.sh +1 -1
- package/dist/templates/har-boilerplate-cli/agent-slot.sh +331 -0
- package/dist/templates/har-boilerplate-cli/harness.env +13 -0
- package/dist/templates/har-boilerplate-cli/launch.sh +140 -95
- package/dist/templates/har-boilerplate-cli/preflight.sh +37 -0
- package/dist/templates/har-boilerplate-cli/provision-toolchain.sh +285 -0
- package/dist/templates/har-boilerplate-cli/setup-infra.sh +87 -11
- package/dist/templates/har-boilerplate-cli/verify.sh +93 -12
- package/dist/templates/har-boilerplate-ios/README.md +29 -2
- package/dist/templates/har-boilerplate-ios/harness.env +6 -1
- package/dist/templates/har-boilerplate-ios/launch.sh +8 -13
- package/dist/templates/har-boilerplate-ios/provision-toolchain.sh +285 -0
- package/dist/templates/har-boilerplate-ios/verify.sh +20 -20
- package/package.json +1 -1
- package/dist/templates/har-boilerplate-ios/ADAPT-PROMPT.md +0 -57
|
@@ -26,6 +26,337 @@ validate_agent_id() {
|
|
|
26
26
|
fi
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
# ── Port allocation ─────────────────────────────────────────────────────────────
|
|
30
|
+
# Try the configured default first; scan the slot lane or infra range when busy.
|
|
31
|
+
|
|
32
|
+
port_in_use() {
|
|
33
|
+
local port="$1"
|
|
34
|
+
(exec 3<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null && { exec 3>&- || true; return 0; }
|
|
35
|
+
return 1
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
har_port_step() {
|
|
39
|
+
echo "${HARNESS_PORT_STEP:-10}"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
har_default_app_port() {
|
|
43
|
+
local base="$1"
|
|
44
|
+
local agent_id="$2"
|
|
45
|
+
echo $(( base + agent_id * $(har_port_step) ))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
har_slot_port_lane_end() {
|
|
49
|
+
local default_port="$1"
|
|
50
|
+
echo $(( default_port + $(har_port_step) - 1 ))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# har_pick_free_port <start> <end> — echoes the first free port in [start, end].
|
|
54
|
+
har_pick_free_port() {
|
|
55
|
+
local start="$1"
|
|
56
|
+
local end="$2"
|
|
57
|
+
local port
|
|
58
|
+
for ((port=start; port<=end; port++)); do
|
|
59
|
+
if ! port_in_use "$port"; then
|
|
60
|
+
echo "$port"
|
|
61
|
+
return 0
|
|
62
|
+
fi
|
|
63
|
+
done
|
|
64
|
+
echo "Error: no free port in range ${start}-${end}" >&2
|
|
65
|
+
return 1
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# har_allocate_port <default> <scan_start> <scan_end>
|
|
69
|
+
har_allocate_port() {
|
|
70
|
+
local default_port="$1"
|
|
71
|
+
local scan_start="$2"
|
|
72
|
+
local scan_end="$3"
|
|
73
|
+
if ! port_in_use "$default_port"; then
|
|
74
|
+
echo "$default_port"
|
|
75
|
+
return 0
|
|
76
|
+
fi
|
|
77
|
+
har_pick_free_port "$scan_start" "$scan_end"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Allocate FE/API/DEBUG ports for a slot. Sets FE_PORT, API_PORT, DEBUG_PORT.
|
|
81
|
+
har_allocate_slot_app_ports() {
|
|
82
|
+
local agent_id="$1"
|
|
83
|
+
local step fe_default api_default debug_default
|
|
84
|
+
step="$(har_port_step)"
|
|
85
|
+
fe_default="$(har_default_app_port "${HARNESS_FE_BASE_PORT}" "$agent_id")"
|
|
86
|
+
api_default="$(har_default_app_port "${HARNESS_API_BASE_PORT}" "$agent_id")"
|
|
87
|
+
debug_default=$(( 9200 + agent_id * step ))
|
|
88
|
+
|
|
89
|
+
FE_PORT="$(har_allocate_port "$fe_default" "$fe_default" "$(har_slot_port_lane_end "$fe_default")")"
|
|
90
|
+
API_PORT="$(har_allocate_port "$api_default" "$api_default" "$(har_slot_port_lane_end "$api_default")")"
|
|
91
|
+
DEBUG_PORT="$(har_allocate_port "$debug_default" "$debug_default" $(( debug_default + step - 1 )) )"
|
|
92
|
+
export FE_PORT API_PORT DEBUG_PORT
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# ── Launch preflight (#36) ─────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
har_harness_uses_pm2() {
|
|
98
|
+
[ -f "${SCRIPT_DIR}/ecosystem.agent.template.cjs" ]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
har_port_docker_occupant() {
|
|
102
|
+
local port="$1"
|
|
103
|
+
docker ps --format '{{.Names}}\t{{.Ports}}' 2>/dev/null \
|
|
104
|
+
| grep -E ":${port}->|:${port}/" | head -1 | cut -f1 || true
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
har_check_control_port_conflict() {
|
|
108
|
+
local port="$1"
|
|
109
|
+
local name
|
|
110
|
+
name="$(docker ps --format '{{.Names}}\t{{.Ports}}' 2>/dev/null \
|
|
111
|
+
| grep -i control | grep -E ":${port}->|:${port}/" | head -1 | cut -f1 || true)"
|
|
112
|
+
if [ -n "$name" ]; then
|
|
113
|
+
echo "ERROR: Mission Control container \"${name}\" occupies port ${port}." >&2
|
|
114
|
+
echo " Run: har control down — or use a different agent slot." >&2
|
|
115
|
+
return 1
|
|
116
|
+
fi
|
|
117
|
+
return 0
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
har_check_foreign_pm2() {
|
|
121
|
+
local agent_id="$1"
|
|
122
|
+
local pm2_raw
|
|
123
|
+
har_harness_uses_pm2 || return 0
|
|
124
|
+
pm2_raw="$(npx --yes pm2 jlist 2>/dev/null || true)"
|
|
125
|
+
[ -n "$pm2_raw" ] || return 0
|
|
126
|
+
set +e
|
|
127
|
+
echo "$pm2_raw" | node -e "
|
|
128
|
+
const agentId = process.argv[1];
|
|
129
|
+
const project = process.argv[2];
|
|
130
|
+
const slotPrefix = 'har-' + project + '-agent-' + agentId + '-';
|
|
131
|
+
const legacyPrefix = 'agent-' + agentId + '-';
|
|
132
|
+
let raw = '';
|
|
133
|
+
process.stdin.on('data', c => raw += c);
|
|
134
|
+
process.stdin.on('end', () => {
|
|
135
|
+
try {
|
|
136
|
+
const arr = JSON.parse(raw);
|
|
137
|
+
if (!Array.isArray(arr)) process.exit(0);
|
|
138
|
+
const foreign = arr.filter(x =>
|
|
139
|
+
x.name && (
|
|
140
|
+
(x.name.startsWith('har-') && x.name.includes('-agent-' + agentId + '-') && !x.name.startsWith(slotPrefix)) ||
|
|
141
|
+
(x.name.startsWith(legacyPrefix) && !x.name.startsWith('har-'))
|
|
142
|
+
));
|
|
143
|
+
if (foreign.length === 0) process.exit(0);
|
|
144
|
+
console.error('ERROR: foreign PM2 processes match agent ' + agentId + ':');
|
|
145
|
+
foreign.forEach(p => {
|
|
146
|
+
const cwd = p.pm2_env?.pm_cwd || p.pm2_env?.cwd || 'unknown';
|
|
147
|
+
console.error(' ' + p.name + ' cwd=' + cwd);
|
|
148
|
+
});
|
|
149
|
+
console.error(' Stop the other harness session or use a different slot.');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
} catch {
|
|
152
|
+
process.exit(0);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
" "$agent_id" "$HARNESS_PROJECT_NAME"
|
|
156
|
+
local rc=$?
|
|
157
|
+
set -e
|
|
158
|
+
return "$rc"
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# ── Resume failed launch (#38) ───────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
slot_session_status() {
|
|
164
|
+
local agent_id="$1"
|
|
165
|
+
read_slot_field "$(slot_registry_file "$agent_id")" status || true
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
slot_is_resumable() {
|
|
169
|
+
local agent_id="$1"
|
|
170
|
+
local status
|
|
171
|
+
status="$(slot_session_status "$agent_id")"
|
|
172
|
+
[ "$status" = "failed" ] || [ "$status" = "starting" ]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
har_resume_session_assignments() {
|
|
176
|
+
local agent_id="$1"
|
|
177
|
+
local reg work_dir worktree branch suffix base_branch base_commit purpose mode env_file
|
|
178
|
+
|
|
179
|
+
if ! slot_is_resumable "$agent_id"; then
|
|
180
|
+
local status
|
|
181
|
+
status="$(slot_session_status "$agent_id")"
|
|
182
|
+
echo "echo \"ERROR: slot ${agent_id} is not resumable (status=${status:-none}; need failed or starting).\" >&2" >&2
|
|
183
|
+
echo "echo \" Use a normal launch, or --replace to start a fresh session.\" >&2"
|
|
184
|
+
echo 'exit 2'
|
|
185
|
+
return 1
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
reg="$(slot_registry_file "$agent_id")"
|
|
189
|
+
work_dir="$(read_slot_field "$reg" workDir || true)"
|
|
190
|
+
worktree="$(read_slot_field "$reg" worktreePath || true)"
|
|
191
|
+
branch="$(read_slot_field "$reg" branch || true)"
|
|
192
|
+
suffix="$(read_slot_field "$reg" suffix || true)"
|
|
193
|
+
base_branch="$(read_slot_field "$reg" baseBranch || true)"
|
|
194
|
+
base_commit="$(read_slot_field "$reg" baseCommit || true)"
|
|
195
|
+
purpose="$(read_slot_field "$reg" purpose || true)"
|
|
196
|
+
mode="$(read_slot_field "$reg" mode || true)"
|
|
197
|
+
|
|
198
|
+
if [ -z "$work_dir" ] || [ ! -d "$work_dir" ]; then
|
|
199
|
+
echo "echo \"ERROR: resume requires work dir from registry (missing or not found).\" >&2"
|
|
200
|
+
echo 'exit 1'
|
|
201
|
+
return 1
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
env_file="$work_dir/.env.agent.${agent_id}"
|
|
205
|
+
if [ ! -f "$env_file" ]; then
|
|
206
|
+
echo "echo \"ERROR: resume requires env file: ${env_file}\" >&2"
|
|
207
|
+
echo 'exit 1'
|
|
208
|
+
return 1
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
printf '%s\n' \
|
|
212
|
+
"WORK_DIR='${work_dir}'" \
|
|
213
|
+
"WORKTREE_DIR='${worktree}'" \
|
|
214
|
+
"BRANCH='${branch}'" \
|
|
215
|
+
"SUFFIX='${suffix}'" \
|
|
216
|
+
"BASE_BRANCH='${base_branch}'" \
|
|
217
|
+
"BASE_COMMIT='${base_commit}'" \
|
|
218
|
+
"PURPOSE='${purpose}'" \
|
|
219
|
+
"USE_WORKTREE=$([ "$mode" = worktree ] && echo true || echo false)" \
|
|
220
|
+
"ENV_FILE='${env_file}'"
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
har_toolchain_ready() {
|
|
224
|
+
local work_dir="$1"
|
|
225
|
+
if [ -f "$work_dir/package.json" ] && [ -d "$work_dir/node_modules" ]; then
|
|
226
|
+
return 0
|
|
227
|
+
fi
|
|
228
|
+
if [ -d "$work_dir/.har/venv" ]; then
|
|
229
|
+
return 0
|
|
230
|
+
fi
|
|
231
|
+
return 1
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
har_regenerate_agent_env_file() {
|
|
235
|
+
local agent_id="$1"
|
|
236
|
+
local work_dir="$2"
|
|
237
|
+
local env_file="$3"
|
|
238
|
+
local worktree_dir="${4:-}"
|
|
239
|
+
local template="${SCRIPT_DIR}/env.template"
|
|
240
|
+
if [ -f "$template" ]; then
|
|
241
|
+
AGENT_ID="$agent_id" \
|
|
242
|
+
API_PORT="${API_PORT:-}" \
|
|
243
|
+
FE_PORT="${FE_PORT:-}" \
|
|
244
|
+
DEBUG_PORT="${DEBUG_PORT:-}" \
|
|
245
|
+
DB_PORT="${DB_PORT:-${AGENT_DB_PORT:-${HARNESS_DB_PORT_DEFAULT:-15432}}}" \
|
|
246
|
+
MINIO_PORT="${MINIO_PORT:-${AGENT_MINIO_PORT:-${HARNESS_MINIO_PORT_DEFAULT:-19000}}}" \
|
|
247
|
+
BROWSER_PORT="${BROWSER_PORT:-${AGENT_BROWSER_PORT:-${HARNESS_BROWSER_PORT_DEFAULT:-13001}}}" \
|
|
248
|
+
REPO_ROOT="$work_dir" \
|
|
249
|
+
envsubst '${AGENT_ID} ${API_PORT} ${FE_PORT} ${DEBUG_PORT} ${DB_PORT} ${MINIO_PORT} ${BROWSER_PORT} ${REPO_ROOT}' \
|
|
250
|
+
< "$template" > "$env_file"
|
|
251
|
+
return 0
|
|
252
|
+
fi
|
|
253
|
+
cat > "$env_file" <<EOF
|
|
254
|
+
# Agent environment — generated by launch.sh
|
|
255
|
+
AGENT_ID=${agent_id}
|
|
256
|
+
REPO_ROOT=${work_dir}
|
|
257
|
+
WORKTREE_DIR=${worktree_dir}
|
|
258
|
+
NODE_ENV=test
|
|
259
|
+
EOF
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
har_launch_preflight() {
|
|
263
|
+
local agent_id="$1"
|
|
264
|
+
local force="${2:-false}"
|
|
265
|
+
local replace="${3:-false}"
|
|
266
|
+
local resume="${4:-false}"
|
|
267
|
+
|
|
268
|
+
if [ "$resume" = true ]; then
|
|
269
|
+
if ! slot_is_resumable "$agent_id"; then
|
|
270
|
+
local status
|
|
271
|
+
status="$(slot_session_status "$agent_id")"
|
|
272
|
+
echo "ERROR: slot ${agent_id} is not resumable (status=${status:-none}; need failed or starting)." >&2
|
|
273
|
+
echo " Use a normal launch, or --replace to start a fresh session." >&2
|
|
274
|
+
return 2
|
|
275
|
+
fi
|
|
276
|
+
echo "==> [agent-${agent_id}] Resuming partial launch (worktree and deps preserved)..." >&2
|
|
277
|
+
elif slot_is_occupied "$agent_id"; then
|
|
278
|
+
if [ "$replace" != true ] && [ "${HAR_CONFIRM_REPLACE:-}" != "1" ]; then
|
|
279
|
+
print_slot_replace_warning "$agent_id"
|
|
280
|
+
echo "ERROR: slot ${agent_id} is occupied — pass --replace to proceed." >&2
|
|
281
|
+
return 2
|
|
282
|
+
fi
|
|
283
|
+
local wt
|
|
284
|
+
wt="$(existing_slot_worktree "$agent_id")"
|
|
285
|
+
if [ -n "$wt" ] && slot_worktree_dirty "$wt" && [ "$force" != true ]; then
|
|
286
|
+
echo "ERROR: dirty worktree requires --force after explicit user approval." >&2
|
|
287
|
+
return 2
|
|
288
|
+
fi
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
if har_harness_uses_pm2; then
|
|
292
|
+
har_check_foreign_pm2 "$agent_id" || return 1
|
|
293
|
+
har_allocate_slot_app_ports "$agent_id" || return 1
|
|
294
|
+
local port occupant
|
|
295
|
+
for port in $(printf '%s\n' "$FE_PORT" "$API_PORT" | sort -u); do
|
|
296
|
+
har_check_control_port_conflict "$port" || return 1
|
|
297
|
+
occupant="$(har_port_docker_occupant "$port")"
|
|
298
|
+
if [ -n "$occupant" ] && [[ "$occupant" != har-${HARNESS_PROJECT_NAME}-* ]]; then
|
|
299
|
+
echo "ERROR: Docker container \"${occupant}\" binds port ${port}." >&2
|
|
300
|
+
echo " Stop it with: docker stop ${occupant}" >&2
|
|
301
|
+
return 1
|
|
302
|
+
fi
|
|
303
|
+
done
|
|
304
|
+
fi
|
|
305
|
+
return 0
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# Load persisted app/infra ports from .env.agent.<id> or the slot registry.
|
|
309
|
+
load_agent_ports() {
|
|
310
|
+
local agent_id="$1"
|
|
311
|
+
local repo_root="$2"
|
|
312
|
+
local env_file reg ports_json
|
|
313
|
+
env_file="$(resolve_agent_env_file "$agent_id" "$repo_root" 2>/dev/null || true)"
|
|
314
|
+
if [ -n "$env_file" ] && [ -f "$env_file" ]; then
|
|
315
|
+
# shellcheck source=/dev/null
|
|
316
|
+
source "$env_file"
|
|
317
|
+
FE_PORT="${FE_PORT:-}"
|
|
318
|
+
API_PORT="${API_PORT:-${PORT:-}}"
|
|
319
|
+
DEBUG_PORT="${DEBUG_PORT:-}"
|
|
320
|
+
DB_PORT="${PGPORT:-${DB_PORT:-}}"
|
|
321
|
+
return 0
|
|
322
|
+
fi
|
|
323
|
+
reg="$(slot_registry_file "$agent_id")"
|
|
324
|
+
if [ -f "$reg" ]; then
|
|
325
|
+
ports_json="$(node -e '
|
|
326
|
+
try {
|
|
327
|
+
const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).ports;
|
|
328
|
+
if (p) process.stdout.write(JSON.stringify(p));
|
|
329
|
+
} catch {}
|
|
330
|
+
' "$reg" 2>/dev/null || true)"
|
|
331
|
+
if [ -n "$ports_json" ]; then
|
|
332
|
+
FE_PORT="$(node -e "const p=JSON.parse(process.argv[1]);process.stdout.write(String(p.frontend??''))" "$ports_json")"
|
|
333
|
+
API_PORT="$(node -e "const p=JSON.parse(process.argv[1]);process.stdout.write(String(p.api??''))" "$ports_json")"
|
|
334
|
+
DEBUG_PORT="$(node -e "const p=JSON.parse(process.argv[1]);process.stdout.write(String(p.debug??''))" "$ports_json")"
|
|
335
|
+
DB_PORT="$(node -e "const p=JSON.parse(process.argv[1]);process.stdout.write(String(p.db??''))" "$ports_json")"
|
|
336
|
+
export FE_PORT API_PORT DEBUG_PORT DB_PORT
|
|
337
|
+
return 0
|
|
338
|
+
fi
|
|
339
|
+
fi
|
|
340
|
+
har_allocate_slot_app_ports "$agent_id"
|
|
341
|
+
DB_PORT="${AGENT_DB_PORT:-${HARNESS_DB_PORT_DEFAULT:-15432}}"
|
|
342
|
+
export FE_PORT API_PORT DEBUG_PORT DB_PORT
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
# ── Project-scoped PM2 names ────────────────────────────────────────────────────
|
|
346
|
+
# Pattern: har-<project>-agent-<id>-<service> (machine-global PM2 namespace).
|
|
347
|
+
|
|
348
|
+
har_pm2_slot_prefix() {
|
|
349
|
+
echo "har-${HARNESS_PROJECT_NAME}-agent-${1}"
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
har_pm2_delete_regex() {
|
|
353
|
+
echo "/^har-${HARNESS_PROJECT_NAME}-agent-${1}-/"
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
har_tmux_session() {
|
|
357
|
+
echo "har-${HARNESS_PROJECT_NAME}-agent-${1}"
|
|
358
|
+
}
|
|
359
|
+
|
|
29
360
|
# ── Slot registry ─────────────────────────────────────────────────────────────
|
|
30
361
|
# .har/slots/agent-<id>.json is the source of truth for where a session lives
|
|
31
362
|
# (worktree path, work dir, branch, base commit). Written by launch.sh, removed
|
|
@@ -4,13 +4,26 @@
|
|
|
4
4
|
export HARNESS_PROJECT_NAME=__PROJECT_NAME__
|
|
5
5
|
export HARNESS_USE_WORKTREE=true
|
|
6
6
|
|
|
7
|
+
# Optional per-slot HTTP ports for integration tests (unused when no local server).
|
|
7
8
|
export HARNESS_FE_BASE_PORT=3000
|
|
8
9
|
export HARNESS_API_BASE_PORT=8000
|
|
10
|
+
export HARNESS_PORT_STEP=10
|
|
9
11
|
export HARNESS_HEALTH_CHECK_PATH=
|
|
10
12
|
|
|
13
|
+
# Shared infra host ports — setup-infra.sh tries DEFAULT first, scans when busy.
|
|
14
|
+
export HARNESS_DB_PORT_DEFAULT=15432
|
|
15
|
+
export HARNESS_DB_PORT_SCAN_START=15432
|
|
16
|
+
export HARNESS_DB_PORT_SCAN_END=15499
|
|
17
|
+
|
|
11
18
|
export HARNESS_AGENT_SLOT_MIN=1
|
|
12
19
|
export HARNESS_AGENT_SLOT_MAX=3
|
|
13
20
|
|
|
21
|
+
# Toolchain provisioning — launch writes resolved paths into .env.agent.<id>
|
|
22
|
+
# Ecosystem: auto (detect from manifests) | node | python | go | rust | java | ruby | none
|
|
23
|
+
export HARNESS_ECOSYSTEM="auto"
|
|
24
|
+
# Optional install override (eval in work dir after env file is created)
|
|
25
|
+
# export HARNESS_INSTALL_CMD="make deps"
|
|
26
|
+
|
|
14
27
|
# Shared infrastructure — space-separated docker compose service names from
|
|
15
28
|
# docker-compose.agent.yml, started ONCE by setup-infra.sh and shared by all
|
|
16
29
|
# agent slots on fixed ports. Empty = no containers. Prune/add services in the
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Every launch starts a FRESH session: any previous session for the slot is torn
|
|
4
4
|
# down (its branch is kept) and a new suffixed worktree is created from HEAD.
|
|
5
5
|
#
|
|
6
|
-
# Usage: ./.har/launch.sh <agent-id> [--no-worktree] [--replace] [--force]
|
|
6
|
+
# Usage: ./.har/launch.sh <agent-id> [--no-worktree] [--replace] [--force] [--resume]
|
|
7
7
|
set -euo pipefail
|
|
8
8
|
|
|
9
9
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
@@ -18,6 +18,7 @@ AGENT_ID="${1:-}"
|
|
|
18
18
|
USE_WORKTREE="${HARNESS_USE_WORKTREE:-true}"
|
|
19
19
|
FORCE=false
|
|
20
20
|
REPLACE=false
|
|
21
|
+
RESUME=false
|
|
21
22
|
PURPOSE="${HAR_SESSION_PURPOSE:-}"
|
|
22
23
|
|
|
23
24
|
for arg in "$@"; do
|
|
@@ -26,12 +27,13 @@ for arg in "$@"; do
|
|
|
26
27
|
--worktree) USE_WORKTREE=true ;;
|
|
27
28
|
--replace) REPLACE=true ;;
|
|
28
29
|
--force) FORCE=true ;;
|
|
30
|
+
--resume) RESUME=true ;;
|
|
29
31
|
--purpose=*) PURPOSE="${arg#--purpose=}" ;;
|
|
30
32
|
esac
|
|
31
33
|
done
|
|
32
34
|
|
|
33
35
|
if [[ -z "$AGENT_ID" ]]; then
|
|
34
|
-
echo "Usage: $0 <agent-id> [--no-worktree] [--replace] [--force] [--purpose=label]" >&2
|
|
36
|
+
echo "Usage: $0 <agent-id> [--no-worktree] [--replace] [--force] [--resume] [--purpose=label]" >&2
|
|
35
37
|
echo " agent-id must be between ${HARNESS_AGENT_SLOT_MIN} and ${HARNESS_AGENT_SLOT_MAX}" >&2
|
|
36
38
|
exit 1
|
|
37
39
|
fi
|
|
@@ -40,115 +42,158 @@ validate_agent_id "$AGENT_ID"
|
|
|
40
42
|
|
|
41
43
|
log() { echo "==> [agent-$AGENT_ID] $*" >&2; }
|
|
42
44
|
|
|
43
|
-
# Replace any previous session for this slot — requires explicit confirmation.
|
|
44
|
-
if slot_is_occupied "$AGENT_ID"; then
|
|
45
|
-
require_slot_replace_confirm "$AGENT_ID" "$FORCE" "$REPLACE"
|
|
46
|
-
log "Replacing previous session for slot ${AGENT_ID}..."
|
|
47
|
-
"$SCRIPT_DIR/teardown.sh" "$AGENT_ID" >&2
|
|
48
|
-
fi
|
|
49
|
-
|
|
50
|
-
"$SCRIPT_DIR/setup-infra.sh"
|
|
51
|
-
|
|
52
|
-
# Session worktree (default — use --no-worktree to work in repo root).
|
|
53
|
-
# Name encodes what the session is based on: <base-branch>-<sha4>-har-agent-<id>-<rand4>.
|
|
54
45
|
WORK_DIR="$REPO_ROOT"
|
|
55
46
|
WORKTREE_DIR=""
|
|
56
47
|
BRANCH=""
|
|
57
48
|
SUFFIX=""
|
|
58
|
-
BASE_BRANCH="
|
|
59
|
-
BASE_COMMIT="
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
49
|
+
BASE_BRANCH=""
|
|
50
|
+
BASE_COMMIT=""
|
|
51
|
+
ENV_FILE=""
|
|
52
|
+
REGISTRY_WRITTEN=false
|
|
53
|
+
|
|
54
|
+
if [ "$RESUME" = true ]; then
|
|
55
|
+
har_launch_preflight "$AGENT_ID" "$FORCE" "$REPLACE" true || exit $?
|
|
56
|
+
eval "$(har_resume_session_assignments "$AGENT_ID")"
|
|
57
|
+
REGISTRY_WRITTEN=true
|
|
58
|
+
mark_slot_failed() {
|
|
59
|
+
local exit_code="$?"
|
|
60
|
+
if [ "$exit_code" != "0" ] && [ "$REGISTRY_WRITTEN" = true ]; then
|
|
61
|
+
log "Resume failed. Recording failed slot state..."
|
|
62
|
+
set +e
|
|
63
|
+
SLOT_AGENT_ID="$AGENT_ID" \
|
|
64
|
+
SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
|
|
65
|
+
SLOT_WORK_DIR="$WORK_DIR" \
|
|
66
|
+
SLOT_SUFFIX="${SUFFIX:-}" \
|
|
67
|
+
SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
|
|
68
|
+
SLOT_BRANCH="${BRANCH:-}" \
|
|
69
|
+
SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
|
|
70
|
+
SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
|
|
71
|
+
SLOT_PURPOSE="${PURPOSE}" \
|
|
72
|
+
SLOT_STATUS="failed" \
|
|
73
|
+
SLOT_LAST_ERROR="launch.sh --resume exited with code ${exit_code}" \
|
|
74
|
+
write_slot_registry
|
|
75
|
+
log " Work dir: ${WORK_DIR}"
|
|
76
|
+
log " Env file: ${ENV_FILE}"
|
|
77
|
+
log " Recovery: ./.har/launch.sh ${AGENT_ID} --resume"
|
|
78
|
+
fi
|
|
79
|
+
}
|
|
80
|
+
trap mark_slot_failed EXIT
|
|
74
81
|
else
|
|
75
|
-
|
|
76
|
-
fi
|
|
82
|
+
har_launch_preflight "$AGENT_ID" "$FORCE" "$REPLACE" || exit $?
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
grep -qxF "$pattern" "$GIT_EXCLUDE" 2>/dev/null || echo "$pattern" >> "$GIT_EXCLUDE"
|
|
84
|
-
done
|
|
84
|
+
if slot_is_occupied "$AGENT_ID"; then
|
|
85
|
+
require_slot_replace_confirm "$AGENT_ID" "$FORCE" "$REPLACE"
|
|
86
|
+
log "Replacing previous session for slot ${AGENT_ID}..."
|
|
87
|
+
"$SCRIPT_DIR/teardown.sh" "$AGENT_ID" >&2
|
|
88
|
+
fi
|
|
85
89
|
fi
|
|
86
90
|
|
|
87
|
-
|
|
88
|
-
log "Generating $ENV_FILE..."
|
|
89
|
-
cat > "$ENV_FILE" <<EOF
|
|
90
|
-
# Agent environment — generated by launch.sh
|
|
91
|
-
AGENT_ID=${AGENT_ID}
|
|
92
|
-
REPO_ROOT=${WORK_DIR}
|
|
93
|
-
WORKTREE_DIR=${WORKTREE_DIR}
|
|
94
|
-
NODE_ENV=test
|
|
95
|
-
EOF
|
|
91
|
+
"$SCRIPT_DIR/setup-infra.sh"
|
|
96
92
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
log "
|
|
93
|
+
if [ "$RESUME" != true ]; then
|
|
94
|
+
WORK_DIR="$REPO_ROOT"
|
|
95
|
+
WORKTREE_DIR=""
|
|
96
|
+
BRANCH=""
|
|
97
|
+
SUFFIX=""
|
|
98
|
+
BASE_BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "detached")"
|
|
99
|
+
BASE_COMMIT="$(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null || true)"
|
|
100
|
+
if [ "$USE_WORKTREE" = true ]; then
|
|
101
|
+
SHORT_SHA="$(git -C "$REPO_ROOT" rev-parse --short=4 HEAD)"
|
|
102
|
+
SUFFIX="$(LC_ALL=C tr -dc 'a-z0-9' </dev/urandom 2>/dev/null | head -c 4 || true)"
|
|
103
|
+
[ -n "$SUFFIX" ] || SUFFIX="$(printf '%04d' $(( RANDOM % 10000 )))"
|
|
104
|
+
SESSION_NAME="${BASE_BRANCH//\//-}-${SHORT_SHA}-har-agent-${AGENT_ID}-${SUFFIX}"
|
|
105
|
+
BRANCH="$SESSION_NAME"
|
|
106
|
+
WORKTREE_DIR="$HOME/worktrees/${SESSION_NAME}"
|
|
107
|
+
log "Creating session worktree at $WORKTREE_DIR (branch $BRANCH)..."
|
|
108
|
+
git -C "$REPO_ROOT" worktree add "$WORKTREE_DIR" -b "$BRANCH"
|
|
109
|
+
REL_PREFIX="$(git -C "$REPO_ROOT" rev-parse --show-prefix 2>/dev/null || true)"
|
|
110
|
+
WORK_DIR="${WORKTREE_DIR%/}/${REL_PREFIX}"
|
|
111
|
+
WORK_DIR="${WORK_DIR%/}"
|
|
112
|
+
else
|
|
113
|
+
log "Using repo root (worktree disabled)"
|
|
114
|
+
REL_PREFIX="$(git -C "$REPO_ROOT" rev-parse --show-prefix 2>/dev/null || true)"
|
|
118
115
|
fi
|
|
119
|
-
}
|
|
120
|
-
trap mark_slot_failed EXIT
|
|
121
116
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
|
|
129
|
-
SLOT_BRANCH="${BRANCH:-}" \
|
|
130
|
-
SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
|
|
131
|
-
SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
|
|
132
|
-
SLOT_PURPOSE="${PURPOSE}" \
|
|
133
|
-
SLOT_STATUS="starting" \
|
|
134
|
-
write_slot_registry
|
|
135
|
-
REGISTRY_WRITTEN=true
|
|
117
|
+
GIT_EXCLUDE="$(git -C "$REPO_ROOT" rev-parse --git-common-dir 2>/dev/null)/info/exclude"
|
|
118
|
+
if [ -n "$GIT_EXCLUDE" ] && [ -d "$(dirname "$GIT_EXCLUDE")" ]; then
|
|
119
|
+
for pattern in '.env.agent.*' 'ecosystem.agent.*.config.cjs' '.har/venv'; do
|
|
120
|
+
grep -qxF "$pattern" "$GIT_EXCLUDE" 2>/dev/null || echo "$pattern" >> "$GIT_EXCLUDE"
|
|
121
|
+
done
|
|
122
|
+
fi
|
|
136
123
|
|
|
137
|
-
|
|
138
|
-
log "
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
124
|
+
ENV_FILE="$WORK_DIR/.env.agent.${AGENT_ID}"
|
|
125
|
+
log "Generating $ENV_FILE..."
|
|
126
|
+
har_regenerate_agent_env_file "$AGENT_ID" "$WORK_DIR" "$ENV_FILE" "${WORKTREE_DIR:-}"
|
|
127
|
+
|
|
128
|
+
REGISTRY_WRITTEN=false
|
|
129
|
+
mark_slot_failed() {
|
|
130
|
+
local exit_code="$?"
|
|
131
|
+
if [ "$exit_code" != "0" ] && [ "$REGISTRY_WRITTEN" = true ]; then
|
|
132
|
+
log "Launch failed after creating the session. Recording failed slot state..."
|
|
133
|
+
set +e
|
|
134
|
+
SLOT_AGENT_ID="$AGENT_ID" \
|
|
135
|
+
SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
|
|
136
|
+
SLOT_WORK_DIR="$WORK_DIR" \
|
|
137
|
+
SLOT_SUFFIX="${SUFFIX:-}" \
|
|
138
|
+
SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
|
|
139
|
+
SLOT_BRANCH="${BRANCH:-}" \
|
|
140
|
+
SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
|
|
141
|
+
SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
|
|
142
|
+
SLOT_PURPOSE="${PURPOSE}" \
|
|
143
|
+
SLOT_STATUS="failed" \
|
|
144
|
+
SLOT_LAST_ERROR="launch.sh exited with code ${exit_code}" \
|
|
145
|
+
write_slot_registry
|
|
146
|
+
log " Work dir: ${WORK_DIR}"
|
|
147
|
+
log " Env file: ${ENV_FILE}"
|
|
148
|
+
log " Recovery: ./.har/launch.sh ${AGENT_ID} --resume"
|
|
149
|
+
fi
|
|
150
|
+
}
|
|
151
|
+
trap mark_slot_failed EXIT
|
|
152
|
+
|
|
153
|
+
SLOT_AGENT_ID="$AGENT_ID" \
|
|
154
|
+
SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
|
|
155
|
+
SLOT_WORK_DIR="$WORK_DIR" \
|
|
156
|
+
SLOT_SUFFIX="${SUFFIX:-}" \
|
|
157
|
+
SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
|
|
158
|
+
SLOT_BRANCH="${BRANCH:-}" \
|
|
159
|
+
SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
|
|
160
|
+
SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
|
|
161
|
+
SLOT_PURPOSE="${PURPOSE}" \
|
|
162
|
+
SLOT_STATUS="starting" \
|
|
163
|
+
write_slot_registry
|
|
164
|
+
REGISTRY_WRITTEN=true
|
|
165
|
+
else
|
|
166
|
+
REL_PREFIX="$(git -C "$REPO_ROOT" rev-parse --show-prefix 2>/dev/null || true)"
|
|
167
|
+
log "Resuming session at ${WORK_DIR}"
|
|
168
|
+
har_regenerate_agent_env_file "$AGENT_ID" "$WORK_DIR" "$ENV_FILE" "${WORKTREE_DIR:-}"
|
|
169
|
+
SLOT_AGENT_ID="$AGENT_ID" \
|
|
170
|
+
SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
|
|
171
|
+
SLOT_WORK_DIR="$WORK_DIR" \
|
|
172
|
+
SLOT_SUFFIX="${SUFFIX:-}" \
|
|
173
|
+
SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
|
|
174
|
+
SLOT_BRANCH="${BRANCH:-}" \
|
|
175
|
+
SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
|
|
176
|
+
SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
|
|
177
|
+
SLOT_PURPOSE="${PURPOSE}" \
|
|
178
|
+
SLOT_STATUS="starting" \
|
|
179
|
+
write_slot_registry
|
|
142
180
|
fi
|
|
143
181
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
182
|
+
if [ "$RESUME" = true ] && har_toolchain_ready "$WORK_DIR"; then
|
|
183
|
+
log "Toolchain already provisioned — skipping install."
|
|
184
|
+
elif [ -f "$SCRIPT_DIR/provision-toolchain.sh" ]; then
|
|
185
|
+
log "Provisioning toolchain (see harness.env: HARNESS_ECOSYSTEM, HARNESS_INSTALL_CMD)..."
|
|
186
|
+
HAR_WORK_DIR="$WORK_DIR" \
|
|
187
|
+
HAR_ENV_FILE="$ENV_FILE" \
|
|
188
|
+
HAR_WORKTREE_DIR="${WORKTREE_DIR:-}" \
|
|
189
|
+
HAR_REL_PREFIX="${REL_PREFIX:-}" \
|
|
190
|
+
HAR_AGENT_ID="$AGENT_ID" \
|
|
191
|
+
"$SCRIPT_DIR/provision-toolchain.sh"
|
|
192
|
+
elif [ -f "$WORK_DIR/package.json" ] && [ ! -d "$WORK_DIR/node_modules" ]; then
|
|
193
|
+
log "Installing dependencies in $WORK_DIR..."
|
|
194
|
+
(cd "$WORK_DIR" && npm install --silent)
|
|
148
195
|
fi
|
|
149
196
|
|
|
150
|
-
# Record the session in the slot registry — the source of truth for where
|
|
151
|
-
# this slot's code lives (status/verify/teardown resolve through it).
|
|
152
197
|
SLOT_AGENT_ID="$AGENT_ID" \
|
|
153
198
|
SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
|
|
154
199
|
SLOT_WORK_DIR="$WORK_DIR" \
|