@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,172 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Namespaced CLI for managing a running agent environment.
|
|
3
|
+
#
|
|
4
|
+
# Usage: ./.har/agent-cli.sh <agent-id> <command> [args...]
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
9
|
+
|
|
10
|
+
# shellcheck source=/dev/null
|
|
11
|
+
source "$SCRIPT_DIR/harness.env"
|
|
12
|
+
# shellcheck source=/dev/null
|
|
13
|
+
source "$SCRIPT_DIR/agent-slot.sh"
|
|
14
|
+
|
|
15
|
+
AGENT_ID="${1:?Usage: agent-cli.sh <agent-id> <command> [args...]}"
|
|
16
|
+
COMMAND="${2:?Usage: agent-cli.sh <agent-id> <command> [args...]}"
|
|
17
|
+
|
|
18
|
+
validate_agent_id "$AGENT_ID"
|
|
19
|
+
|
|
20
|
+
FE_PORT=$(( HARNESS_FE_BASE_PORT + AGENT_ID * 10 ))
|
|
21
|
+
API_PORT=$(( HARNESS_API_BASE_PORT + AGENT_ID * 10 ))
|
|
22
|
+
DB_PORT="${AGENT_DB_PORT:-15432}"
|
|
23
|
+
export PGPASSWORD="password"
|
|
24
|
+
|
|
25
|
+
case "$COMMAND" in
|
|
26
|
+
status)
|
|
27
|
+
ENV_FILE="$(resolve_agent_env_file "$AGENT_ID" "$REPO_ROOT" || true)"
|
|
28
|
+
WORKTREE_DIR="$(existing_slot_worktree "$AGENT_ID")"
|
|
29
|
+
|
|
30
|
+
PM2_RAW=$(npx --yes pm2 jlist 2>/dev/null || true)
|
|
31
|
+
PM2_FOUND=false
|
|
32
|
+
|
|
33
|
+
if [ -n "$PM2_RAW" ]; then
|
|
34
|
+
echo "$PM2_RAW" | node -e "
|
|
35
|
+
const agentId = '${AGENT_ID}';
|
|
36
|
+
let raw = '';
|
|
37
|
+
process.stdin.on('data', c => raw += c);
|
|
38
|
+
process.stdin.on('end', () => {
|
|
39
|
+
try {
|
|
40
|
+
const arr = JSON.parse(raw);
|
|
41
|
+
if (!Array.isArray(arr)) process.exit(1);
|
|
42
|
+
const procs = arr.filter(x => x.name && x.name.startsWith('agent-' + agentId + '-'));
|
|
43
|
+
if (procs.length === 0) process.exit(1);
|
|
44
|
+
console.log('Agent ' + agentId + ' processes:');
|
|
45
|
+
procs.forEach(p => {
|
|
46
|
+
const s = (p.pm2_env?.status || 'unknown').padEnd(10);
|
|
47
|
+
const mem = Math.round((p.monit?.memory || 0) / 1024 / 1024) + 'MB';
|
|
48
|
+
const cpu = (p.monit?.cpu || 0) + '%';
|
|
49
|
+
console.log(' ' + p.name.padEnd(42) + s + ' mem=' + mem + ' cpu=' + cpu);
|
|
50
|
+
});
|
|
51
|
+
} catch {
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
" && PM2_FOUND=true
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
if [ "$PM2_FOUND" = true ]; then
|
|
59
|
+
:
|
|
60
|
+
elif [ -n "$ENV_FILE" ] && [ -f "$ENV_FILE" ]; then
|
|
61
|
+
echo "Agent ${AGENT_ID}: active (no PM2 processes)"
|
|
62
|
+
# shellcheck source=/dev/null
|
|
63
|
+
source "$ENV_FILE" 2>/dev/null || true
|
|
64
|
+
echo " Work dir: $(resolve_agent_work_dir "$ENV_FILE" "$AGENT_ID")"
|
|
65
|
+
[ -n "$WORKTREE_DIR" ] && [ -d "$WORKTREE_DIR" ] && echo " Worktree: $WORKTREE_DIR"
|
|
66
|
+
else
|
|
67
|
+
echo "No active environment for agent ${AGENT_ID}"
|
|
68
|
+
echo " Run: ./.har/launch.sh ${AGENT_ID}"
|
|
69
|
+
fi
|
|
70
|
+
;;
|
|
71
|
+
|
|
72
|
+
logs)
|
|
73
|
+
SERVICE="${3:-}"
|
|
74
|
+
if [ -n "$SERVICE" ]; then
|
|
75
|
+
npx pm2 logs "agent-${AGENT_ID}-${SERVICE}" --lines 100
|
|
76
|
+
else
|
|
77
|
+
npx pm2 logs --name "agent-${AGENT_ID}" --lines 100
|
|
78
|
+
fi
|
|
79
|
+
;;
|
|
80
|
+
|
|
81
|
+
restart)
|
|
82
|
+
SERVICE="${3:-}"
|
|
83
|
+
if [ -n "$SERVICE" ]; then
|
|
84
|
+
npx pm2 restart "agent-${AGENT_ID}-${SERVICE}"
|
|
85
|
+
else
|
|
86
|
+
npx pm2 jlist 2>/dev/null | node -e "
|
|
87
|
+
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
|
|
88
|
+
const names = d.filter(x => x.name.startsWith('agent-${AGENT_ID}-')).map(x => x.name);
|
|
89
|
+
names.forEach(n => require('child_process').execSync('npx pm2 restart ' + n, {stdio:'inherit'}));
|
|
90
|
+
if (names.length === 0) console.log('No processes found for agent ${AGENT_ID}');
|
|
91
|
+
" 2>/dev/null || true
|
|
92
|
+
fi
|
|
93
|
+
;;
|
|
94
|
+
|
|
95
|
+
psql)
|
|
96
|
+
QUERY="${3:-}"
|
|
97
|
+
if [ -n "$QUERY" ]; then
|
|
98
|
+
har_pg psql -d "agent_${AGENT_ID}" -c "$QUERY"
|
|
99
|
+
else
|
|
100
|
+
har_pg psql -d "agent_${AGENT_ID}"
|
|
101
|
+
fi
|
|
102
|
+
;;
|
|
103
|
+
|
|
104
|
+
health)
|
|
105
|
+
if [ -n "${HARNESS_HEALTH_CHECK_PATH:-}" ]; then
|
|
106
|
+
curl -sf "http://localhost:${API_PORT}${HARNESS_HEALTH_CHECK_PATH}" | node -e "
|
|
107
|
+
const d = require('fs').readFileSync('/dev/stdin','utf8');
|
|
108
|
+
try { console.log(JSON.stringify(JSON.parse(d), null, 2)); } catch { console.log(d); }
|
|
109
|
+
" || curl -v "http://localhost:${API_PORT}${HARNESS_HEALTH_CHECK_PATH}"
|
|
110
|
+
else
|
|
111
|
+
echo "No health check path configured in harness.env"
|
|
112
|
+
fi
|
|
113
|
+
;;
|
|
114
|
+
|
|
115
|
+
url)
|
|
116
|
+
echo "Frontend: http://localhost:${FE_PORT}"
|
|
117
|
+
echo "API: http://localhost:${API_PORT}"
|
|
118
|
+
har_infra_enabled db && echo "Database: agent_${AGENT_ID} @ localhost:${DB_PORT}"
|
|
119
|
+
har_infra_enabled minio && echo "MinIO: http://localhost:19001"
|
|
120
|
+
har_infra_enabled headless-browser && echo "Browser: http://localhost:13001"
|
|
121
|
+
har_infra_enabled mailpit && echo "Mailpit: http://localhost:18025"
|
|
122
|
+
;;
|
|
123
|
+
|
|
124
|
+
reset-db)
|
|
125
|
+
echo "==> Resetting database for agent ${AGENT_ID}..."
|
|
126
|
+
har_pg psql -d postgres -c \
|
|
127
|
+
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='agent_${AGENT_ID}';" \
|
|
128
|
+
>/dev/null
|
|
129
|
+
har_pg dropdb --if-exists "agent_${AGENT_ID}"
|
|
130
|
+
har_pg createdb -T "$HARNESS_TEMPLATE_DB" "agent_${AGENT_ID}"
|
|
131
|
+
echo "✓ Database reset to clean state"
|
|
132
|
+
;;
|
|
133
|
+
|
|
134
|
+
slow-queries)
|
|
135
|
+
har_pg psql -d "agent_${AGENT_ID}" -c "
|
|
136
|
+
SELECT round(mean_exec_time::numeric, 2) AS mean_ms,
|
|
137
|
+
calls,
|
|
138
|
+
left(query, 120) AS query
|
|
139
|
+
FROM pg_stat_statements
|
|
140
|
+
ORDER BY mean_exec_time DESC
|
|
141
|
+
LIMIT 20;" 2>/dev/null || echo "pg_stat_statements extension not available"
|
|
142
|
+
;;
|
|
143
|
+
|
|
144
|
+
exec)
|
|
145
|
+
shift 2
|
|
146
|
+
if [ $# -eq 0 ]; then
|
|
147
|
+
echo "Usage: agent-cli.sh ${AGENT_ID} exec <command>" >&2
|
|
148
|
+
exit 1
|
|
149
|
+
fi
|
|
150
|
+
WORK_DIR="$(resolve_agent_work_dir "" "$AGENT_ID" 2>/dev/null || true)"
|
|
151
|
+
[ -n "$WORK_DIR" ] && [ -d "$WORK_DIR" ] || WORK_DIR="$REPO_ROOT"
|
|
152
|
+
PGHOST=localhost PGPORT="$DB_PORT" PGUSER=postgres PGDATABASE="agent_${AGENT_ID}" \
|
|
153
|
+
bash -c "cd '$WORK_DIR' && $*"
|
|
154
|
+
;;
|
|
155
|
+
|
|
156
|
+
attach)
|
|
157
|
+
SESSION="agent-${AGENT_ID}"
|
|
158
|
+
if ! tmux has-session -t "$SESSION" 2>/dev/null; then
|
|
159
|
+
echo "No tmux session found: $SESSION" >&2
|
|
160
|
+
exit 1
|
|
161
|
+
fi
|
|
162
|
+
tmux attach -t "$SESSION"
|
|
163
|
+
;;
|
|
164
|
+
|
|
165
|
+
*)
|
|
166
|
+
echo "Unknown command: $COMMAND" >&2
|
|
167
|
+
echo ""
|
|
168
|
+
echo "Commands: status, logs [service], restart [service], psql [query],"
|
|
169
|
+
echo " health, url, reset-db, slow-queries, exec <cmd>, attach"
|
|
170
|
+
exit 1
|
|
171
|
+
;;
|
|
172
|
+
esac
|
|
@@ -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,16 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Attach to the tmux session for an agent.
|
|
3
|
+
#
|
|
4
|
+
# Usage: ./.har/attach.sh <agent-id>
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
AGENT_ID="${1:?Usage: attach.sh <agent-id>}"
|
|
8
|
+
SESSION="agent-${AGENT_ID}"
|
|
9
|
+
|
|
10
|
+
if ! tmux has-session -t "$SESSION" 2>/dev/null; then
|
|
11
|
+
echo "No tmux session found: $SESSION" >&2
|
|
12
|
+
echo "Launch with: ./.har/launch.sh ${AGENT_ID} --claude" >&2
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
exec tmux attach -t "$SESSION"
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
# - Internal supporting services of a monolith/monorepo can also live here
|
|
7
|
+
# (image: or build:) so agent slots only run the primary application
|
|
8
|
+
# Only services listed in HARNESS_INFRA_SERVICES (harness.env) are started
|
|
9
|
+
# by setup-infra.sh.
|
|
10
|
+
services:
|
|
11
|
+
db:
|
|
12
|
+
image: pgvector/pgvector:pg16
|
|
13
|
+
restart: always
|
|
14
|
+
ports:
|
|
15
|
+
- "${AGENT_DB_PORT:-15432}:5432"
|
|
16
|
+
environment:
|
|
17
|
+
POSTGRES_PASSWORD: password
|
|
18
|
+
volumes:
|
|
19
|
+
- agent-db-data:/var/lib/postgresql/data
|
|
20
|
+
healthcheck:
|
|
21
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
22
|
+
interval: 5s
|
|
23
|
+
timeout: 3s
|
|
24
|
+
retries: 5
|
|
25
|
+
command:
|
|
26
|
+
- postgres
|
|
27
|
+
- -c
|
|
28
|
+
- shared_preload_libraries=pg_stat_statements
|
|
29
|
+
- -c
|
|
30
|
+
- pg_stat_statements.track=all
|
|
31
|
+
shm_size: 256m
|
|
32
|
+
|
|
33
|
+
minio:
|
|
34
|
+
image: coollabsio/minio:latest
|
|
35
|
+
ports:
|
|
36
|
+
- "${AGENT_MINIO_PORT:-19000}:9000"
|
|
37
|
+
- "${AGENT_MINIO_CONSOLE_PORT:-19001}:9001"
|
|
38
|
+
volumes:
|
|
39
|
+
- agent-minio-data:/minio/data
|
|
40
|
+
environment:
|
|
41
|
+
MINIO_ROOT_USER: minioadmin
|
|
42
|
+
MINIO_ROOT_PASSWORD: minioadmin
|
|
43
|
+
MINIO_DEFAULT_BUCKETS: default,results
|
|
44
|
+
MINIO_BROWSER: "off"
|
|
45
|
+
|
|
46
|
+
headless-browser:
|
|
47
|
+
image: ghcr.io/browserless/chromium:v2.38.2
|
|
48
|
+
restart: always
|
|
49
|
+
ports:
|
|
50
|
+
- "${AGENT_BROWSER_PORT:-13001}:3000"
|
|
51
|
+
shm_size: 512m
|
|
52
|
+
|
|
53
|
+
mailpit:
|
|
54
|
+
image: axllent/mailpit:latest
|
|
55
|
+
restart: unless-stopped
|
|
56
|
+
ports:
|
|
57
|
+
- "${AGENT_MAILPIT_WEB_PORT:-18025}:8025"
|
|
58
|
+
- "${AGENT_MAILPIT_SMTP_PORT:-11025}:1025"
|
|
59
|
+
environment:
|
|
60
|
+
MP_MAX_MESSAGES: 5000
|
|
61
|
+
MP_SMTP_AUTH_ACCEPT_ANY: 1
|
|
62
|
+
MP_SMTP_AUTH_ALLOW_INSECURE: 1
|
|
63
|
+
|
|
64
|
+
volumes:
|
|
65
|
+
agent-db-data:
|
|
66
|
+
agent-minio-data:
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PM2 Ecosystem Configuration for Agent ${AGENT_ID}
|
|
3
|
+
*
|
|
4
|
+
* Auto-generated by launch.sh — do not edit manually.
|
|
5
|
+
* Customize ecosystem.agent.template.cjs in .har/ to change processes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// launch.sh writes this file (and .env.agent.N) to the work dir root,
|
|
9
|
+
// so __dirname IS the work dir — repo root or the agent's git worktree.
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const dotenv = require('dotenv');
|
|
12
|
+
|
|
13
|
+
const agentEnvPath = path.resolve(__dirname, '.env.agent.${AGENT_ID}');
|
|
14
|
+
const env = dotenv.config({ path: agentEnvPath }).parsed || {};
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
apps: [
|
|
18
|
+
// TODO: Replace with the PRIMARY application's dev processes — the ONE app
|
|
19
|
+
// agents modify (HARNESS_PRIMARY_APP in harness.env). In a monolith or
|
|
20
|
+
// monorepo with several services, do NOT start them all here: supporting
|
|
21
|
+
// services the agent depends on but does not change run ONCE for all slots,
|
|
22
|
+
// either as compose services in docker-compose.agent.yml or via an optional
|
|
23
|
+
// .har/ecosystem.shared.config.cjs (started by setup-infra.sh, processes
|
|
24
|
+
// named "har-shared-<name>", fixed ports). A primary app may still need two
|
|
25
|
+
// processes (e.g. api + frontend) — add one entry per process.
|
|
26
|
+
{
|
|
27
|
+
name: 'agent-${AGENT_ID}-api',
|
|
28
|
+
script: 'npm',
|
|
29
|
+
args: 'run dev',
|
|
30
|
+
interpreter: 'none',
|
|
31
|
+
cwd: __dirname,
|
|
32
|
+
env: {
|
|
33
|
+
...env,
|
|
34
|
+
NODE_ENV: 'development',
|
|
35
|
+
PORT: env.API_PORT || env.PORT,
|
|
36
|
+
},
|
|
37
|
+
watch: false,
|
|
38
|
+
autorestart: true,
|
|
39
|
+
kill_timeout: 5000,
|
|
40
|
+
merge_logs: true,
|
|
41
|
+
time: true,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Agent environment — generated by launch.sh from env.template
|
|
2
|
+
# DO NOT EDIT — regenerated on every launch
|
|
3
|
+
AGENT_ID=${AGENT_ID}
|
|
4
|
+
|
|
5
|
+
# Ports (formula: BASE + AGENT_ID * 10)
|
|
6
|
+
PORT=${API_PORT}
|
|
7
|
+
FE_PORT=${FE_PORT}
|
|
8
|
+
API_PORT=${API_PORT}
|
|
9
|
+
DEBUG_PORT=${DEBUG_PORT}
|
|
10
|
+
|
|
11
|
+
# PostgreSQL
|
|
12
|
+
PGHOST=localhost
|
|
13
|
+
PGPORT=${DB_PORT}
|
|
14
|
+
PGDATABASE=agent_${AGENT_ID}
|
|
15
|
+
PGUSER=postgres
|
|
16
|
+
PGPASSWORD=password
|
|
17
|
+
DATABASE_URL=postgresql://postgres:password@localhost:${DB_PORT}/agent_${AGENT_ID}
|
|
18
|
+
|
|
19
|
+
# Application
|
|
20
|
+
NODE_ENV=development
|
|
21
|
+
SITE_URL=http://localhost:${API_PORT}
|
|
22
|
+
CORS_ORIGIN=http://localhost:${FE_PORT}
|
|
23
|
+
|
|
24
|
+
# MinIO / S3 (if enabled)
|
|
25
|
+
S3_ENDPOINT=http://localhost:${MINIO_PORT}
|
|
26
|
+
S3_ACCESS_KEY=minioadmin
|
|
27
|
+
S3_SECRET_KEY=minioadmin
|
|
28
|
+
S3_BUCKET=agent-${AGENT_ID}
|
|
29
|
+
S3_REGION=us-east-1
|
|
30
|
+
S3_FORCE_PATH_STYLE=true
|
|
31
|
+
|
|
32
|
+
# Headless browser (if enabled)
|
|
33
|
+
HEADLESS_BROWSER_URL=http://localhost:${BROWSER_PORT}
|
|
34
|
+
|
|
35
|
+
# Mail (if enabled)
|
|
36
|
+
SMTP_HOST=localhost
|
|
37
|
+
SMTP_PORT=11025
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Shared harness configuration — edit to match your project
|
|
2
|
+
# Generated by har env init, maintained over time with har env maintain
|
|
3
|
+
|
|
4
|
+
export HARNESS_PROJECT_NAME=__PROJECT_NAME__
|
|
5
|
+
export HARNESS_USE_WORKTREE=true
|
|
6
|
+
|
|
7
|
+
# Primary application — the ONE app agents modify and run per-slot.
|
|
8
|
+
# In a monolith/monorepo with several services, pick the app agents work on;
|
|
9
|
+
# everything else runs once as a shared service (see docker-compose.agent.yml
|
|
10
|
+
# and optional ecosystem.shared.config.cjs).
|
|
11
|
+
export HARNESS_PRIMARY_APP=app
|
|
12
|
+
|
|
13
|
+
export HARNESS_TEMPLATE_DB=template___PROJECT_NAME__
|
|
14
|
+
export HARNESS_FE_BASE_PORT=3000
|
|
15
|
+
export HARNESS_API_BASE_PORT=8000
|
|
16
|
+
export HARNESS_HEALTH_CHECK_PATH=/health
|
|
17
|
+
|
|
18
|
+
# Agent slot limits — set based on what your machine can run in parallel
|
|
19
|
+
export HARNESS_AGENT_SLOT_MIN=1
|
|
20
|
+
export HARNESS_AGENT_SLOT_MAX=5
|
|
21
|
+
|
|
22
|
+
# Shared infrastructure — space-separated docker compose service names from
|
|
23
|
+
# docker-compose.agent.yml. Started ONCE by setup-infra.sh and shared by all
|
|
24
|
+
# agent slots on fixed ports. Prune/add services in the compose file, then
|
|
25
|
+
# list the ones this project needs here (e.g. "db redis mailpit").
|
|
26
|
+
export HARNESS_INFRA_SERVICES="db"
|
|
27
|
+
|
|
28
|
+
# Returns 0 when a shared infra service is enabled: har_infra_enabled db
|
|
29
|
+
har_infra_enabled() {
|
|
30
|
+
case " ${HARNESS_INFRA_SERVICES:-} " in
|
|
31
|
+
*" $1 "*) return 0 ;;
|
|
32
|
+
*) return 1 ;;
|
|
33
|
+
esac
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Postgres client — uses host tools when installed, otherwise docker exec into
|
|
37
|
+
# the shared db container. Usage: har_pg psql -d postgres -c "SELECT 1"
|
|
38
|
+
# har_pg createdb -T template_x agent_1
|
|
39
|
+
har_pg() {
|
|
40
|
+
local tool="$1"; shift
|
|
41
|
+
if command -v "$tool" >/dev/null 2>&1; then
|
|
42
|
+
PGPASSWORD=password "$tool" -h localhost -p "${AGENT_DB_PORT:-15432}" -U postgres "$@"
|
|
43
|
+
else
|
|
44
|
+
docker exec -i -e PGPASSWORD=password "har-${HARNESS_PROJECT_NAME}-db-1" "$tool" -U postgres "$@"
|
|
45
|
+
fi
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Database setup commands (run once against template DB in setup-infra.sh)
|
|
49
|
+
export HARNESS_DB_MIGRATE_CMD="echo 'TODO: set migrate command'"
|
|
50
|
+
export HARNESS_DB_SEED_CMD="echo 'TODO: set seed command'"
|