@osfactory/har 0.1.1
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/LICENSE +674 -0
- package/README.md +208 -0
- package/control/docker-compose.build.yml +7 -0
- package/control/docker-compose.yml +31 -0
- package/dist/index.js +41753 -0
- package/dist/prompts/system-authoring.md +104 -0
- package/dist/templates/AGENT.md.template +40 -0
- package/dist/templates/CLAUDE.md.template +5 -0
- package/dist/templates/adaptation-prompt-init.md +124 -0
- package/dist/templates/adaptation-prompt-maintain.md +73 -0
- package/dist/templates/cursor-rule.mdc.template +101 -0
- package/dist/templates/har-boilerplate/CLAUDE.agent.md +54 -0
- package/dist/templates/har-boilerplate/README.md +134 -0
- package/dist/templates/har-boilerplate/agent-cli.sh +172 -0
- package/dist/templates/har-boilerplate/agent-slot.sh +270 -0
- package/dist/templates/har-boilerplate/attach.sh +16 -0
- package/dist/templates/har-boilerplate/docker-compose.agent.yml +66 -0
- package/dist/templates/har-boilerplate/ecosystem.agent.template.cjs +44 -0
- package/dist/templates/har-boilerplate/env.template +37 -0
- package/dist/templates/har-boilerplate/harness.env +50 -0
- package/dist/templates/har-boilerplate/justfile +39 -0
- package/dist/templates/har-boilerplate/launch.sh +229 -0
- package/dist/templates/har-boilerplate/setup-infra.sh +102 -0
- package/dist/templates/har-boilerplate/stages/README.sh +9 -0
- package/dist/templates/har-boilerplate/stages.json +67 -0
- package/dist/templates/har-boilerplate/teardown.sh +86 -0
- package/dist/templates/har-boilerplate/verify.sh +163 -0
- package/dist/templates/har-boilerplate-cli/CLAUDE.agent.md +41 -0
- package/dist/templates/har-boilerplate-cli/README.md +112 -0
- package/dist/templates/har-boilerplate-cli/agent-cli.sh +162 -0
- package/dist/templates/har-boilerplate-cli/agent-slot.sh +270 -0
- package/dist/templates/har-boilerplate-cli/docker-compose.agent.yml +64 -0
- package/dist/templates/har-boilerplate-cli/harness.env +40 -0
- package/dist/templates/har-boilerplate-cli/justfile +27 -0
- package/dist/templates/har-boilerplate-cli/launch.sh +132 -0
- package/dist/templates/har-boilerplate-cli/setup-infra.sh +102 -0
- package/dist/templates/har-boilerplate-cli/stages/README.sh +9 -0
- package/dist/templates/har-boilerplate-cli/stages.json +61 -0
- package/dist/templates/har-boilerplate-cli/teardown.sh +66 -0
- package/dist/templates/har-boilerplate-cli/verify.sh +113 -0
- package/dist/templates/har-boilerplate-ios/ADAPT-PROMPT.md +57 -0
- package/dist/templates/har-boilerplate-ios/CLAUDE.agent.md +65 -0
- package/dist/templates/har-boilerplate-ios/README.md +88 -0
- package/dist/templates/har-boilerplate-ios/agent-cli.sh +125 -0
- package/dist/templates/har-boilerplate-ios/agent-slot.sh +270 -0
- package/dist/templates/har-boilerplate-ios/docker-compose.agent.yml +17 -0
- package/dist/templates/har-boilerplate-ios/harness.env +42 -0
- package/dist/templates/har-boilerplate-ios/justfile +33 -0
- package/dist/templates/har-boilerplate-ios/launch.sh +132 -0
- package/dist/templates/har-boilerplate-ios/setup-infra.sh +84 -0
- package/dist/templates/har-boilerplate-ios/stages/README.sh +12 -0
- package/dist/templates/har-boilerplate-ios/stages.json +61 -0
- package/dist/templates/har-boilerplate-ios/teardown.sh +64 -0
- package/dist/templates/har-boilerplate-ios/verify.sh +165 -0
- package/dist/templates/stage-templates/playwright/.github/workflows/playwright.yml +30 -0
- package/dist/templates/stage-templates/playwright/.har/stages/PLAYWRIGHT.md +21 -0
- package/dist/templates/stage-templates/playwright/.har/stages/browser-e2e.sh +83 -0
- package/dist/templates/stage-templates/playwright/README.md +14 -0
- package/dist/templates/stage-templates/playwright/package.fragment.json +14 -0
- package/dist/templates/stage-templates/playwright/playwright.config.js +30 -0
- package/dist/templates/stage-templates/playwright/template.manifest.json +44 -0
- package/dist/templates/stage-templates/playwright/tests/a11y/smoke.spec.js +14 -0
- package/dist/templates/stage-templates/playwright/tests/api/health.spec.js +10 -0
- package/dist/templates/stage-templates/playwright/tests/frontend/smoke.spec.js +9 -0
- package/dist/templates/stage-templates/rocketsim/.har/stages/ROCKETSIM.md +126 -0
- package/dist/templates/stage-templates/rocketsim/.har/stages/rocketsim-flows.sh +188 -0
- package/dist/templates/stage-templates/rocketsim/README.md +66 -0
- package/dist/templates/stage-templates/rocketsim/flows/README.md +36 -0
- package/dist/templates/stage-templates/rocketsim/flows/example-smoke.sh +60 -0
- package/dist/templates/stage-templates/rocketsim/template.manifest.json +38 -0
- package/package.json +78 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared slot helpers: agent-id validation, slot registry, work-dir resolution.
|
|
3
|
+
# Source after harness.env (requires $SCRIPT_DIR pointing at the .har directory):
|
|
4
|
+
# source "$SCRIPT_DIR/harness.env"
|
|
5
|
+
# source "$SCRIPT_DIR/agent-slot.sh"
|
|
6
|
+
# validate_agent_id "$AGENT_ID"
|
|
7
|
+
|
|
8
|
+
validate_agent_id() {
|
|
9
|
+
local id="${1:-}"
|
|
10
|
+
local min="${HARNESS_AGENT_SLOT_MIN:-1}"
|
|
11
|
+
local max="${HARNESS_AGENT_SLOT_MAX:-}"
|
|
12
|
+
|
|
13
|
+
if [[ -z "$max" ]]; then
|
|
14
|
+
echo "Error: HARNESS_AGENT_SLOT_MAX is not set in harness.env" >&2
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
if [[ -z "$id" ]] || ! [[ "$id" =~ ^[0-9]+$ ]]; then
|
|
19
|
+
echo "Error: agent-id must be a positive integer between ${min} and ${max}" >&2
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
if (( id < min || id > max )); then
|
|
24
|
+
echo "Error: agent-id must be between ${min} and ${max}" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# ── Slot registry ─────────────────────────────────────────────────────────────
|
|
30
|
+
# .har/slots/agent-<id>.json is the source of truth for where a session lives
|
|
31
|
+
# (worktree path, work dir, branch, base commit). Written by launch.sh, removed
|
|
32
|
+
# by teardown.sh. Location resolution must go through it — worktree paths carry
|
|
33
|
+
# a random per-session suffix and cannot be derived from the agent id alone.
|
|
34
|
+
|
|
35
|
+
slot_registry_file() {
|
|
36
|
+
echo "${SCRIPT_DIR}/slots/agent-${1}.json"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# read_slot_field <registry-file> <field> — echoes the scalar value, empty if absent.
|
|
40
|
+
read_slot_field() {
|
|
41
|
+
[ -f "${1:-}" ] || return 1
|
|
42
|
+
node -e '
|
|
43
|
+
try {
|
|
44
|
+
const v = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"))[process.argv[2]];
|
|
45
|
+
if (v != null && typeof v !== "object") process.stdout.write(String(v));
|
|
46
|
+
} catch {}
|
|
47
|
+
' "$1" "$2"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Writes the registry entry from SLOT_* variables:
|
|
51
|
+
# required: SLOT_AGENT_ID, SLOT_MODE (worktree|root), SLOT_WORK_DIR
|
|
52
|
+
# optional: SLOT_SUFFIX, SLOT_WORKTREE_PATH, SLOT_BRANCH, SLOT_BASE_BRANCH,
|
|
53
|
+
# SLOT_BASE_COMMIT, SLOT_PORTS_JSON, SLOT_PREVIEW_URLS_JSON, SLOT_PURPOSE
|
|
54
|
+
write_slot_registry() {
|
|
55
|
+
local file
|
|
56
|
+
file="$(slot_registry_file "$SLOT_AGENT_ID")"
|
|
57
|
+
mkdir -p "$(dirname "$file")"
|
|
58
|
+
node -e '
|
|
59
|
+
const fs = require("fs");
|
|
60
|
+
const e = process.env;
|
|
61
|
+
const entry = {
|
|
62
|
+
version: 1,
|
|
63
|
+
agentId: Number(e.SLOT_AGENT_ID),
|
|
64
|
+
projectName: e.HARNESS_PROJECT_NAME || "",
|
|
65
|
+
mode: e.SLOT_MODE,
|
|
66
|
+
workDir: e.SLOT_WORK_DIR,
|
|
67
|
+
createdAt: new Date().toISOString(),
|
|
68
|
+
status: "active",
|
|
69
|
+
};
|
|
70
|
+
if (e.SLOT_SUFFIX) entry.suffix = e.SLOT_SUFFIX;
|
|
71
|
+
if (e.SLOT_WORKTREE_PATH) entry.worktreePath = e.SLOT_WORKTREE_PATH;
|
|
72
|
+
if (e.SLOT_BRANCH) entry.branch = e.SLOT_BRANCH;
|
|
73
|
+
if (e.SLOT_BASE_BRANCH) entry.baseBranch = e.SLOT_BASE_BRANCH;
|
|
74
|
+
if (e.SLOT_BASE_COMMIT) entry.baseCommit = e.SLOT_BASE_COMMIT;
|
|
75
|
+
if (e.SLOT_PURPOSE) entry.purpose = e.SLOT_PURPOSE;
|
|
76
|
+
for (const [key, env] of [["ports", "SLOT_PORTS_JSON"], ["previewUrls", "SLOT_PREVIEW_URLS_JSON"]]) {
|
|
77
|
+
if (e[env]) try { entry[key] = JSON.parse(e[env]); } catch {}
|
|
78
|
+
}
|
|
79
|
+
fs.writeFileSync(process.argv[1], JSON.stringify(entry, null, 2) + "\n");
|
|
80
|
+
' "$file"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
remove_slot_registry() {
|
|
84
|
+
rm -f "$(slot_registry_file "$1")"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Exit 0 when the worktree has uncommitted or untracked changes.
|
|
88
|
+
slot_worktree_dirty() {
|
|
89
|
+
[ -d "${1:-}" ] || return 1
|
|
90
|
+
[ -n "$(git -C "$1" status --porcelain 2>/dev/null)" ]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Exit 0 when a slot registry entry or worktree path exists for this agent id.
|
|
94
|
+
slot_is_occupied() {
|
|
95
|
+
local agent_id="$1"
|
|
96
|
+
[ -f "$(slot_registry_file "$agent_id")" ] || [ -n "$(existing_slot_worktree "$agent_id")" ]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Echo "clean" or "dirty (N changed)" for a worktree path.
|
|
100
|
+
slot_dirty_summary() {
|
|
101
|
+
local wt="${1:-}"
|
|
102
|
+
if [ -z "$wt" ] || [ ! -d "$wt" ]; then
|
|
103
|
+
echo "unknown"
|
|
104
|
+
return 0
|
|
105
|
+
fi
|
|
106
|
+
if ! slot_worktree_dirty "$wt"; then
|
|
107
|
+
echo "clean"
|
|
108
|
+
return 0
|
|
109
|
+
fi
|
|
110
|
+
local count
|
|
111
|
+
count="$(git -C "$wt" status --porcelain 2>/dev/null | wc -l | tr -d ' ')"
|
|
112
|
+
echo "dirty (${count} changed)"
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Print a warning before replacing an occupied slot (stdout — visible to agents).
|
|
116
|
+
print_slot_replace_warning() {
|
|
117
|
+
local agent_id="$1"
|
|
118
|
+
local reg wt branch work_dir purpose created dirty_summary head
|
|
119
|
+
reg="$(slot_registry_file "$agent_id")"
|
|
120
|
+
wt="$(existing_slot_worktree "$agent_id")"
|
|
121
|
+
branch="$(read_slot_field "$reg" branch || true)"
|
|
122
|
+
work_dir="$(read_slot_field "$reg" workDir || true)"
|
|
123
|
+
purpose="$(read_slot_field "$reg" purpose || true)"
|
|
124
|
+
created="$(read_slot_field "$reg" createdAt || true)"
|
|
125
|
+
dirty_summary="$(slot_dirty_summary "$wt")"
|
|
126
|
+
if [ -n "$wt" ] && [ -d "$wt" ]; then
|
|
127
|
+
head="$(git -C "$wt" rev-parse --short HEAD 2>/dev/null || true)"
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
echo "" >&2
|
|
131
|
+
echo "⚠ Slot ${agent_id} is already in use — replacing will REMOVE the worktree." >&2
|
|
132
|
+
[ -n "$purpose" ] && echo " Purpose: ${purpose}" >&2
|
|
133
|
+
[ -n "$wt" ] && echo " Worktree: ${wt}" >&2
|
|
134
|
+
[ -n "$work_dir" ] && echo " Work dir: ${work_dir}" >&2
|
|
135
|
+
[ -n "$branch" ] && echo " Branch: ${branch}${head:+ @ ${head}}" >&2
|
|
136
|
+
[ -n "$created" ] && echo " Since: ${created}" >&2
|
|
137
|
+
echo " Git: ${dirty_summary}" >&2
|
|
138
|
+
echo "" >&2
|
|
139
|
+
echo " The session branch is kept only if you committed. Gitignored paths" >&2
|
|
140
|
+
echo " (state/, runs/, local clones, .env.local) are NOT preserved." >&2
|
|
141
|
+
echo "" >&2
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Require explicit confirmation before replacing an occupied slot.
|
|
145
|
+
# $1 agent_id $2 force(true/false) $3 replace(true/false)
|
|
146
|
+
# Exits 1 when replacement must not proceed.
|
|
147
|
+
require_slot_replace_confirm() {
|
|
148
|
+
local agent_id="$1"
|
|
149
|
+
local force="$2"
|
|
150
|
+
local replace="$3"
|
|
151
|
+
local wt
|
|
152
|
+
|
|
153
|
+
slot_is_occupied "$agent_id" || return 0
|
|
154
|
+
|
|
155
|
+
print_slot_replace_warning "$agent_id"
|
|
156
|
+
wt="$(existing_slot_worktree "$agent_id")"
|
|
157
|
+
|
|
158
|
+
if [ -n "$wt" ] && slot_worktree_dirty "$wt" && [ "$force" != true ]; then
|
|
159
|
+
echo "ERROR: previous session for slot ${agent_id} has uncommitted changes." >&2
|
|
160
|
+
echo " Commit them in the worktree (branch is kept on teardown), or pass --force to discard." >&2
|
|
161
|
+
echo " --force destroys uncommitted work — only use after explicit user approval." >&2
|
|
162
|
+
exit 1
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
if [ "$replace" = true ] || [ "${HAR_CONFIRM_REPLACE:-}" = "1" ]; then
|
|
166
|
+
return 0
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
if [ -t 0 ] && [ -t 1 ]; then
|
|
170
|
+
local answer
|
|
171
|
+
read -r -p "Replace slot ${agent_id}? Uncommitted work is lost unless committed. [y/N] " answer
|
|
172
|
+
if [[ "$answer" =~ ^[Yy]$ ]]; then
|
|
173
|
+
return 0
|
|
174
|
+
fi
|
|
175
|
+
echo "Aborted — slot ${agent_id} left unchanged." >&2
|
|
176
|
+
exit 2
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
echo "ERROR: slot ${agent_id} is occupied." >&2
|
|
180
|
+
echo " Pass --replace (or set HAR_CONFIRM_REPLACE=1) after reviewing the warning above." >&2
|
|
181
|
+
echo " If the worktree is dirty, also pass --force (only after explicit user approval)." >&2
|
|
182
|
+
exit 2
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# Echo the previous session's worktree path for a slot: registry first, then
|
|
186
|
+
# the legacy fixed path (pre-registry sessions). Empty output when none exists.
|
|
187
|
+
existing_slot_worktree() {
|
|
188
|
+
local agent_id="$1"
|
|
189
|
+
local reg path
|
|
190
|
+
reg="$(slot_registry_file "$agent_id")"
|
|
191
|
+
if [ -f "$reg" ]; then
|
|
192
|
+
path="$(read_slot_field "$reg" worktreePath || true)"
|
|
193
|
+
if [ -n "$path" ] && [ -d "$path" ]; then
|
|
194
|
+
echo "$path"
|
|
195
|
+
return 0
|
|
196
|
+
fi
|
|
197
|
+
fi
|
|
198
|
+
path="$HOME/worktrees/${HARNESS_PROJECT_NAME}-agent-${agent_id}"
|
|
199
|
+
if [ -d "$path" ]; then
|
|
200
|
+
echo "$path"
|
|
201
|
+
fi
|
|
202
|
+
return 0
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# Resolve .env.agent.<id> — registry work dir first, then legacy locations
|
|
206
|
+
# (repo root, pre-registry fixed worktree path).
|
|
207
|
+
resolve_agent_env_file() {
|
|
208
|
+
local agent_id="$1"
|
|
209
|
+
local repo_root="$2"
|
|
210
|
+
local reg work_dir
|
|
211
|
+
reg="$(slot_registry_file "$agent_id")"
|
|
212
|
+
if [ -f "$reg" ]; then
|
|
213
|
+
work_dir="$(read_slot_field "$reg" workDir || true)"
|
|
214
|
+
if [ -n "$work_dir" ] && [ -f "$work_dir/.env.agent.${agent_id}" ]; then
|
|
215
|
+
echo "$work_dir/.env.agent.${agent_id}"
|
|
216
|
+
return 0
|
|
217
|
+
fi
|
|
218
|
+
fi
|
|
219
|
+
# Worktrees are repo-rooted — if the project lives in a subdirectory (monorepo),
|
|
220
|
+
# the env file sits under that prefix inside the worktree.
|
|
221
|
+
local rel_prefix
|
|
222
|
+
rel_prefix="$(git -C "$repo_root" rev-parse --show-prefix 2>/dev/null || true)"
|
|
223
|
+
local candidate
|
|
224
|
+
for candidate in \
|
|
225
|
+
"$repo_root/.env.agent.${agent_id}" \
|
|
226
|
+
"$HOME/worktrees/${HARNESS_PROJECT_NAME}-agent-${agent_id}/${rel_prefix}.env.agent.${agent_id}"; do
|
|
227
|
+
if [ -f "$candidate" ]; then
|
|
228
|
+
echo "$candidate"
|
|
229
|
+
return 0
|
|
230
|
+
fi
|
|
231
|
+
done
|
|
232
|
+
return 1
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
# Resolve work dir for verify/e2e — registry first, else the agent env file's
|
|
236
|
+
# variables (call after sourcing the env file). Optional 2nd arg: agent id.
|
|
237
|
+
resolve_agent_work_dir() {
|
|
238
|
+
local env_file="$1"
|
|
239
|
+
local agent_id="${2:-${AGENT_ID:-}}"
|
|
240
|
+
if [ -n "$agent_id" ]; then
|
|
241
|
+
local reg wd
|
|
242
|
+
reg="$(slot_registry_file "$agent_id")"
|
|
243
|
+
if [ -f "$reg" ]; then
|
|
244
|
+
wd="$(read_slot_field "$reg" workDir || true)"
|
|
245
|
+
if [ -n "$wd" ] && [ -d "$wd" ]; then
|
|
246
|
+
echo "$wd"
|
|
247
|
+
return 0
|
|
248
|
+
fi
|
|
249
|
+
fi
|
|
250
|
+
fi
|
|
251
|
+
local work_dir="${REPO_ROOT:-}"
|
|
252
|
+
if [ -z "$work_dir" ] || [ ! -d "$work_dir" ]; then
|
|
253
|
+
if [ -n "${WORKTREE_DIR:-}" ] && [ -d "$WORKTREE_DIR" ]; then
|
|
254
|
+
work_dir="$WORKTREE_DIR"
|
|
255
|
+
else
|
|
256
|
+
work_dir="$(cd "$(dirname "$env_file")" && pwd)"
|
|
257
|
+
fi
|
|
258
|
+
fi
|
|
259
|
+
echo "$work_dir"
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
# Run browser-e2e on verify --full when the Playwright stage template is installed.
|
|
263
|
+
run_browser_e2e_if_present() {
|
|
264
|
+
local script_dir="$1"
|
|
265
|
+
local agent_id="$2"
|
|
266
|
+
local e2e="$script_dir/stages/browser-e2e.sh"
|
|
267
|
+
if [ -x "$e2e" ]; then
|
|
268
|
+
"$e2e" "$agent_id"
|
|
269
|
+
fi
|
|
270
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Shared infrastructure — ONE instance serves all agent slots (fixed ports).
|
|
2
|
+
#
|
|
3
|
+
# This file is a MENU of common external dependencies. When adapting:
|
|
4
|
+
# - DELETE services this project does not use (and their volumes)
|
|
5
|
+
# - ADD anything missing (redis, nats, keycloak, ...)
|
|
6
|
+
# Only services listed in HARNESS_INFRA_SERVICES (harness.env) are started
|
|
7
|
+
# by setup-infra.sh.
|
|
8
|
+
services:
|
|
9
|
+
db:
|
|
10
|
+
image: pgvector/pgvector:pg16
|
|
11
|
+
restart: always
|
|
12
|
+
ports:
|
|
13
|
+
- "${AGENT_DB_PORT:-15432}:5432"
|
|
14
|
+
environment:
|
|
15
|
+
POSTGRES_PASSWORD: password
|
|
16
|
+
volumes:
|
|
17
|
+
- agent-db-data:/var/lib/postgresql/data
|
|
18
|
+
healthcheck:
|
|
19
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
20
|
+
interval: 5s
|
|
21
|
+
timeout: 3s
|
|
22
|
+
retries: 5
|
|
23
|
+
command:
|
|
24
|
+
- postgres
|
|
25
|
+
- -c
|
|
26
|
+
- shared_preload_libraries=pg_stat_statements
|
|
27
|
+
- -c
|
|
28
|
+
- pg_stat_statements.track=all
|
|
29
|
+
shm_size: 256m
|
|
30
|
+
|
|
31
|
+
minio:
|
|
32
|
+
image: coollabsio/minio:latest
|
|
33
|
+
ports:
|
|
34
|
+
- "${AGENT_MINIO_PORT:-19000}:9000"
|
|
35
|
+
- "${AGENT_MINIO_CONSOLE_PORT:-19001}:9001"
|
|
36
|
+
volumes:
|
|
37
|
+
- agent-minio-data:/minio/data
|
|
38
|
+
environment:
|
|
39
|
+
MINIO_ROOT_USER: minioadmin
|
|
40
|
+
MINIO_ROOT_PASSWORD: minioadmin
|
|
41
|
+
MINIO_DEFAULT_BUCKETS: default,results
|
|
42
|
+
MINIO_BROWSER: "off"
|
|
43
|
+
|
|
44
|
+
headless-browser:
|
|
45
|
+
image: ghcr.io/browserless/chromium:v2.38.2
|
|
46
|
+
restart: always
|
|
47
|
+
ports:
|
|
48
|
+
- "${AGENT_BROWSER_PORT:-13001}:3000"
|
|
49
|
+
shm_size: 512m
|
|
50
|
+
|
|
51
|
+
mailpit:
|
|
52
|
+
image: axllent/mailpit:latest
|
|
53
|
+
restart: unless-stopped
|
|
54
|
+
ports:
|
|
55
|
+
- "${AGENT_MAILPIT_WEB_PORT:-18025}:8025"
|
|
56
|
+
- "${AGENT_MAILPIT_SMTP_PORT:-11025}:1025"
|
|
57
|
+
environment:
|
|
58
|
+
MP_MAX_MESSAGES: 5000
|
|
59
|
+
MP_SMTP_AUTH_ACCEPT_ANY: 1
|
|
60
|
+
MP_SMTP_AUTH_ALLOW_INSECURE: 1
|
|
61
|
+
|
|
62
|
+
volumes:
|
|
63
|
+
agent-db-data:
|
|
64
|
+
agent-minio-data:
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# CLI / library harness — optional Docker via HARNESS_INFRA_SERVICES, no PM2
|
|
2
|
+
# Generated by har env init --profile cli
|
|
3
|
+
|
|
4
|
+
export HARNESS_PROJECT_NAME=__PROJECT_NAME__
|
|
5
|
+
export HARNESS_USE_WORKTREE=true
|
|
6
|
+
|
|
7
|
+
export HARNESS_FE_BASE_PORT=3000
|
|
8
|
+
export HARNESS_API_BASE_PORT=8000
|
|
9
|
+
export HARNESS_HEALTH_CHECK_PATH=
|
|
10
|
+
|
|
11
|
+
export HARNESS_AGENT_SLOT_MIN=1
|
|
12
|
+
export HARNESS_AGENT_SLOT_MAX=3
|
|
13
|
+
|
|
14
|
+
# Shared infrastructure — space-separated docker compose service names from
|
|
15
|
+
# docker-compose.agent.yml, started ONCE by setup-infra.sh and shared by all
|
|
16
|
+
# agent slots on fixed ports. Empty = no containers. Prune/add services in the
|
|
17
|
+
# compose file, then list the ones this project needs here (e.g. "db redis").
|
|
18
|
+
export HARNESS_INFRA_SERVICES=""
|
|
19
|
+
|
|
20
|
+
# Returns 0 when a shared infra service is enabled: har_infra_enabled db
|
|
21
|
+
har_infra_enabled() {
|
|
22
|
+
case " ${HARNESS_INFRA_SERVICES:-} " in
|
|
23
|
+
*" $1 "*) return 0 ;;
|
|
24
|
+
*) return 1 ;;
|
|
25
|
+
esac
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Postgres client — uses host tools when installed, otherwise docker exec into
|
|
29
|
+
# the shared db container. Usage: har_pg psql -d postgres -c "SELECT 1"
|
|
30
|
+
har_pg() {
|
|
31
|
+
local tool="$1"; shift
|
|
32
|
+
if command -v "$tool" >/dev/null 2>&1; then
|
|
33
|
+
PGPASSWORD=password "$tool" -h localhost -p "${AGENT_DB_PORT:-15432}" -U postgres "$@"
|
|
34
|
+
else
|
|
35
|
+
docker exec -i -e PGPASSWORD=password "har-${HARNESS_PROJECT_NAME}-db-1" "$tool" -U postgres "$@"
|
|
36
|
+
fi
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export HARNESS_DB_MIGRATE_CMD="true"
|
|
40
|
+
export HARNESS_DB_SEED_CMD="true"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Agent harness shortcuts — requires `just` (https://github.com/casey/just)
|
|
2
|
+
|
|
3
|
+
set shell := ["bash", "-cu"]
|
|
4
|
+
|
|
5
|
+
setup:
|
|
6
|
+
./setup-infra.sh
|
|
7
|
+
|
|
8
|
+
launch id:
|
|
9
|
+
./launch.sh {{id}}
|
|
10
|
+
|
|
11
|
+
teardown id:
|
|
12
|
+
./teardown.sh {{id}}
|
|
13
|
+
|
|
14
|
+
verify id:
|
|
15
|
+
./verify.sh {{id}}
|
|
16
|
+
|
|
17
|
+
verify-full id:
|
|
18
|
+
./verify.sh {{id}} --full
|
|
19
|
+
|
|
20
|
+
status:
|
|
21
|
+
#!/usr/bin/env bash
|
|
22
|
+
for id in 1 2 3; do
|
|
23
|
+
./agent-cli.sh "$id" status 2>/dev/null || true
|
|
24
|
+
done
|
|
25
|
+
|
|
26
|
+
cli id cmd *args:
|
|
27
|
+
./agent-cli.sh {{id}} {{cmd}} {{args}}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Launch an agent slot for CLI/library repos (git worktree by default, optional Docker infra).
|
|
3
|
+
# Every launch starts a FRESH session: any previous session for the slot is torn
|
|
4
|
+
# down (its branch is kept) and a new suffixed worktree is created from HEAD.
|
|
5
|
+
#
|
|
6
|
+
# Usage: ./.har/launch.sh <agent-id> [--no-worktree] [--replace] [--force]
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
11
|
+
|
|
12
|
+
# shellcheck source=/dev/null
|
|
13
|
+
source "$SCRIPT_DIR/harness.env"
|
|
14
|
+
# shellcheck source=/dev/null
|
|
15
|
+
source "$SCRIPT_DIR/agent-slot.sh"
|
|
16
|
+
|
|
17
|
+
AGENT_ID="${1:-}"
|
|
18
|
+
USE_WORKTREE="${HARNESS_USE_WORKTREE:-true}"
|
|
19
|
+
FORCE=false
|
|
20
|
+
REPLACE=false
|
|
21
|
+
PURPOSE="${HAR_SESSION_PURPOSE:-}"
|
|
22
|
+
|
|
23
|
+
for arg in "$@"; do
|
|
24
|
+
case "$arg" in
|
|
25
|
+
--no-worktree) USE_WORKTREE=false ;;
|
|
26
|
+
--worktree) USE_WORKTREE=true ;;
|
|
27
|
+
--replace) REPLACE=true ;;
|
|
28
|
+
--force) FORCE=true ;;
|
|
29
|
+
--purpose=*) PURPOSE="${arg#--purpose=}" ;;
|
|
30
|
+
esac
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
if [[ -z "$AGENT_ID" ]]; then
|
|
34
|
+
echo "Usage: $0 <agent-id> [--no-worktree] [--replace] [--force] [--purpose=label]" >&2
|
|
35
|
+
echo " agent-id must be between ${HARNESS_AGENT_SLOT_MIN} and ${HARNESS_AGENT_SLOT_MAX}" >&2
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
validate_agent_id "$AGENT_ID"
|
|
40
|
+
|
|
41
|
+
log() { echo "==> [agent-$AGENT_ID] $*" >&2; }
|
|
42
|
+
|
|
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
|
+
WORK_DIR="$REPO_ROOT"
|
|
55
|
+
WORKTREE_DIR=""
|
|
56
|
+
BRANCH=""
|
|
57
|
+
SUFFIX=""
|
|
58
|
+
BASE_BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "detached")"
|
|
59
|
+
BASE_COMMIT="$(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null || true)"
|
|
60
|
+
if [ "$USE_WORKTREE" = true ]; then
|
|
61
|
+
SHORT_SHA="$(git -C "$REPO_ROOT" rev-parse --short=4 HEAD)"
|
|
62
|
+
SUFFIX="$(LC_ALL=C tr -dc 'a-z0-9' </dev/urandom 2>/dev/null | head -c 4 || true)"
|
|
63
|
+
[ -n "$SUFFIX" ] || SUFFIX="$(printf '%04d' $(( RANDOM % 10000 )))"
|
|
64
|
+
SESSION_NAME="${BASE_BRANCH//\//-}-${SHORT_SHA}-har-agent-${AGENT_ID}-${SUFFIX}"
|
|
65
|
+
BRANCH="$SESSION_NAME"
|
|
66
|
+
WORKTREE_DIR="$HOME/worktrees/${SESSION_NAME}"
|
|
67
|
+
log "Creating session worktree at $WORKTREE_DIR (branch $BRANCH)..."
|
|
68
|
+
git -C "$REPO_ROOT" worktree add "$WORKTREE_DIR" -b "$BRANCH"
|
|
69
|
+
# Worktrees are always rooted at the git repo — if this project lives in a
|
|
70
|
+
# subdirectory (monorepo), point WORK_DIR at the project inside the worktree.
|
|
71
|
+
REL_PREFIX="$(git -C "$REPO_ROOT" rev-parse --show-prefix 2>/dev/null || true)"
|
|
72
|
+
WORK_DIR="${WORKTREE_DIR%/}/${REL_PREFIX}"
|
|
73
|
+
WORK_DIR="${WORK_DIR%/}"
|
|
74
|
+
else
|
|
75
|
+
log "Using repo root (worktree disabled)"
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# Keep harness-generated files out of accidental `git add -A` commits in repos
|
|
79
|
+
# whose .gitignore doesn't know about har (applies to all worktrees too).
|
|
80
|
+
GIT_EXCLUDE="$(git -C "$REPO_ROOT" rev-parse --git-common-dir 2>/dev/null)/info/exclude"
|
|
81
|
+
if [ -n "$GIT_EXCLUDE" ] && [ -d "$(dirname "$GIT_EXCLUDE")" ]; then
|
|
82
|
+
for pattern in '.env.agent.*' 'ecosystem.agent.*.config.cjs'; do
|
|
83
|
+
grep -qxF "$pattern" "$GIT_EXCLUDE" 2>/dev/null || echo "$pattern" >> "$GIT_EXCLUDE"
|
|
84
|
+
done
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
ENV_FILE="$WORK_DIR/.env.agent.${AGENT_ID}"
|
|
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
|
|
96
|
+
|
|
97
|
+
if [ -f "$WORK_DIR/package.json" ]; then
|
|
98
|
+
log "Installing dependencies..."
|
|
99
|
+
(cd "$WORK_DIR" && npm install --silent)
|
|
100
|
+
elif [ -f "$WORK_DIR/go.mod" ]; then
|
|
101
|
+
log "Go module detected in work dir (run go mod download if needed)"
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# Monorepo: file:/workspace deps may resolve modules from the repo root — install there too
|
|
105
|
+
if [ -n "${REL_PREFIX:-}" ] && [ -f "$WORKTREE_DIR/package.json" ] && [ ! -d "$WORKTREE_DIR/node_modules" ]; then
|
|
106
|
+
log "Installing monorepo root dependencies in $WORKTREE_DIR..."
|
|
107
|
+
(cd "$WORKTREE_DIR" && npm install --silent)
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# Record the session in the slot registry — the source of truth for where
|
|
111
|
+
# this slot's code lives (status/verify/teardown resolve through it).
|
|
112
|
+
SLOT_AGENT_ID="$AGENT_ID" \
|
|
113
|
+
SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
|
|
114
|
+
SLOT_WORK_DIR="$WORK_DIR" \
|
|
115
|
+
SLOT_SUFFIX="${SUFFIX:-}" \
|
|
116
|
+
SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
|
|
117
|
+
SLOT_BRANCH="${BRANCH:-}" \
|
|
118
|
+
SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
|
|
119
|
+
SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
|
|
120
|
+
SLOT_PURPOSE="${PURPOSE}" \
|
|
121
|
+
write_slot_registry
|
|
122
|
+
|
|
123
|
+
log "Agent $AGENT_ID is ready."
|
|
124
|
+
log ""
|
|
125
|
+
log " WORK DIR (make ALL file edits under this path — never the main checkout):"
|
|
126
|
+
log " ${WORK_DIR}"
|
|
127
|
+
if [ "$USE_WORKTREE" = true ]; then
|
|
128
|
+
log " Branch: ${BRANCH} (based on ${BASE_BRANCH} @ ${BASE_COMMIT})"
|
|
129
|
+
fi
|
|
130
|
+
log ""
|
|
131
|
+
log " Verify: ./.har/verify.sh $AGENT_ID"
|
|
132
|
+
log " Teardown: ./.har/teardown.sh $AGENT_ID (keeps the branch)"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Sets up shared infrastructure for all agents.
|
|
3
|
+
# Starts the docker compose services listed in HARNESS_INFRA_SERVICES, creates
|
|
4
|
+
# the template database (when "db" is enabled), and starts optional shared app
|
|
5
|
+
# services (ecosystem.shared.config.cjs). One instance serves every agent slot.
|
|
6
|
+
# Idempotent — safe to run multiple times.
|
|
7
|
+
#
|
|
8
|
+
# Usage: ./.har/setup-infra.sh
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
12
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
13
|
+
COMPOSE_FILE="$SCRIPT_DIR/docker-compose.agent.yml"
|
|
14
|
+
|
|
15
|
+
# shellcheck source=/dev/null
|
|
16
|
+
source "$SCRIPT_DIR/harness.env"
|
|
17
|
+
|
|
18
|
+
COMPOSE_PROJECT="har-${HARNESS_PROJECT_NAME}"
|
|
19
|
+
DB_PORT="${AGENT_DB_PORT:-15432}"
|
|
20
|
+
PSQL="har_pg psql -d postgres"
|
|
21
|
+
|
|
22
|
+
log() { echo "==> $*" >&2; }
|
|
23
|
+
|
|
24
|
+
SERVICES="${HARNESS_INFRA_SERVICES:-}"
|
|
25
|
+
|
|
26
|
+
if [ -n "$SERVICES" ]; then
|
|
27
|
+
log "Starting shared infrastructure (project: $COMPOSE_PROJECT): $SERVICES"
|
|
28
|
+
AGENT_DB_PORT="$DB_PORT" \
|
|
29
|
+
AGENT_MINIO_PORT="${AGENT_MINIO_PORT:-19000}" \
|
|
30
|
+
AGENT_MINIO_CONSOLE_PORT="${AGENT_MINIO_CONSOLE_PORT:-19001}" \
|
|
31
|
+
AGENT_BROWSER_PORT="${AGENT_BROWSER_PORT:-13001}" \
|
|
32
|
+
AGENT_MAILPIT_WEB_PORT="${AGENT_MAILPIT_WEB_PORT:-18025}" \
|
|
33
|
+
AGENT_MAILPIT_SMTP_PORT="${AGENT_MAILPIT_SMTP_PORT:-11025}" \
|
|
34
|
+
docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" up -d $SERVICES
|
|
35
|
+
else
|
|
36
|
+
log "No shared infra services enabled in harness.env (HARNESS_INFRA_SERVICES)"
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Wait for PostgreSQL and prepare the template database
|
|
40
|
+
if har_infra_enabled db; then
|
|
41
|
+
log "Waiting for PostgreSQL on port $DB_PORT..."
|
|
42
|
+
for i in $(seq 1 30); do
|
|
43
|
+
if har_pg pg_isready -q 2>/dev/null; then
|
|
44
|
+
log "PostgreSQL is ready."
|
|
45
|
+
break
|
|
46
|
+
fi
|
|
47
|
+
if [ "$i" = "30" ]; then
|
|
48
|
+
echo "Error: PostgreSQL did not become ready within 30 seconds." >&2
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
sleep 1
|
|
52
|
+
done
|
|
53
|
+
|
|
54
|
+
log "Enabling pg_stat_statements extension..."
|
|
55
|
+
$PSQL -c "CREATE EXTENSION IF NOT EXISTS pg_stat_statements" 2>/dev/null || true
|
|
56
|
+
|
|
57
|
+
if [ -n "${HARNESS_TEMPLATE_DB:-}" ]; then
|
|
58
|
+
if $PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '$HARNESS_TEMPLATE_DB'" | grep -q 1; then
|
|
59
|
+
log "Template database '$HARNESS_TEMPLATE_DB' already exists. Skipping creation."
|
|
60
|
+
else
|
|
61
|
+
log "Creating template database '$HARNESS_TEMPLATE_DB'..."
|
|
62
|
+
$PSQL -c "CREATE DATABASE $HARNESS_TEMPLATE_DB"
|
|
63
|
+
|
|
64
|
+
if [ -n "${HARNESS_DB_MIGRATE_CMD:-}" ] && [ "$HARNESS_DB_MIGRATE_CMD" != "echo 'TODO: set migrate command'" ]; then
|
|
65
|
+
log "Running migrations..."
|
|
66
|
+
PGPASSWORD=password PGHOST=localhost PGPORT="$DB_PORT" PGUSER=postgres \
|
|
67
|
+
PGDATABASE="$HARNESS_TEMPLATE_DB" \
|
|
68
|
+
bash -c "cd '$REPO_ROOT' && $HARNESS_DB_MIGRATE_CMD"
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
if [ -n "${HARNESS_DB_SEED_CMD:-}" ] && [ "$HARNESS_DB_SEED_CMD" != "echo 'TODO: set seed command'" ]; then
|
|
72
|
+
log "Running seeds..."
|
|
73
|
+
PGPASSWORD=password PGHOST=localhost PGPORT="$DB_PORT" PGUSER=postgres \
|
|
74
|
+
PGDATABASE="$HARNESS_TEMPLATE_DB" \
|
|
75
|
+
bash -c "cd '$REPO_ROOT' && $HARNESS_DB_SEED_CMD"
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
log "Marking '$HARNESS_TEMPLATE_DB' as a PostgreSQL template..."
|
|
79
|
+
$PSQL -c "UPDATE pg_database SET datistemplate = true WHERE datname = '$HARNESS_TEMPLATE_DB'"
|
|
80
|
+
log "Template database ready: $HARNESS_TEMPLATE_DB"
|
|
81
|
+
fi
|
|
82
|
+
fi
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# Shared app services — supporting services of a monolith/monorepo that agents
|
|
86
|
+
# depend on but do not modify. Started ONCE on fixed ports, shared by all slots.
|
|
87
|
+
# Create .har/ecosystem.shared.config.cjs (PM2 format, processes named
|
|
88
|
+
# "har-shared-<name>") only when the primary app needs sibling services running.
|
|
89
|
+
SHARED_ECOSYSTEM="$SCRIPT_DIR/ecosystem.shared.config.cjs"
|
|
90
|
+
if [ -f "$SHARED_ECOSYSTEM" ]; then
|
|
91
|
+
log "Starting shared app services from ecosystem.shared.config.cjs..."
|
|
92
|
+
(cd "$REPO_ROOT" && npx --yes pm2 startOrReload "$SHARED_ECOSYSTEM" >/dev/null)
|
|
93
|
+
log "Shared app services running (pm2 ls | grep har-shared-)."
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
echo ""
|
|
97
|
+
log "Infrastructure is ready."
|
|
98
|
+
har_infra_enabled db && log " PostgreSQL: localhost:$DB_PORT"
|
|
99
|
+
har_infra_enabled minio && log " MinIO: http://localhost:${AGENT_MINIO_CONSOLE_PORT:-19001}"
|
|
100
|
+
har_infra_enabled headless-browser && log " Browser: http://localhost:${AGENT_BROWSER_PORT:-13001}"
|
|
101
|
+
har_infra_enabled mailpit && log " Mailpit: http://localhost:${AGENT_MAILPIT_WEB_PORT:-18025}"
|
|
102
|
+
exit 0
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Optional per-stage scripts live here.
|
|
3
|
+
# Example: ./.har/stages/browser-e2e.sh <agent-id>
|
|
4
|
+
#
|
|
5
|
+
# Register custom stages in .har/stages.json with a project-owned command:
|
|
6
|
+
# "command": "./.har/stages/browser-e2e.sh {agentId}"
|
|
7
|
+
|
|
8
|
+
echo "Add stage scripts under .har/stages/ and register them in stages.json." >&2
|
|
9
|
+
exit 1
|