@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.
Files changed (71) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +208 -0
  3. package/control/docker-compose.build.yml +7 -0
  4. package/control/docker-compose.yml +31 -0
  5. package/dist/index.js +41753 -0
  6. package/dist/prompts/system-authoring.md +104 -0
  7. package/dist/templates/AGENT.md.template +40 -0
  8. package/dist/templates/CLAUDE.md.template +5 -0
  9. package/dist/templates/adaptation-prompt-init.md +124 -0
  10. package/dist/templates/adaptation-prompt-maintain.md +73 -0
  11. package/dist/templates/cursor-rule.mdc.template +101 -0
  12. package/dist/templates/har-boilerplate/CLAUDE.agent.md +54 -0
  13. package/dist/templates/har-boilerplate/README.md +134 -0
  14. package/dist/templates/har-boilerplate/agent-cli.sh +172 -0
  15. package/dist/templates/har-boilerplate/agent-slot.sh +270 -0
  16. package/dist/templates/har-boilerplate/attach.sh +16 -0
  17. package/dist/templates/har-boilerplate/docker-compose.agent.yml +66 -0
  18. package/dist/templates/har-boilerplate/ecosystem.agent.template.cjs +44 -0
  19. package/dist/templates/har-boilerplate/env.template +37 -0
  20. package/dist/templates/har-boilerplate/harness.env +50 -0
  21. package/dist/templates/har-boilerplate/justfile +39 -0
  22. package/dist/templates/har-boilerplate/launch.sh +229 -0
  23. package/dist/templates/har-boilerplate/setup-infra.sh +102 -0
  24. package/dist/templates/har-boilerplate/stages/README.sh +9 -0
  25. package/dist/templates/har-boilerplate/stages.json +67 -0
  26. package/dist/templates/har-boilerplate/teardown.sh +86 -0
  27. package/dist/templates/har-boilerplate/verify.sh +163 -0
  28. package/dist/templates/har-boilerplate-cli/CLAUDE.agent.md +41 -0
  29. package/dist/templates/har-boilerplate-cli/README.md +112 -0
  30. package/dist/templates/har-boilerplate-cli/agent-cli.sh +162 -0
  31. package/dist/templates/har-boilerplate-cli/agent-slot.sh +270 -0
  32. package/dist/templates/har-boilerplate-cli/docker-compose.agent.yml +64 -0
  33. package/dist/templates/har-boilerplate-cli/harness.env +40 -0
  34. package/dist/templates/har-boilerplate-cli/justfile +27 -0
  35. package/dist/templates/har-boilerplate-cli/launch.sh +132 -0
  36. package/dist/templates/har-boilerplate-cli/setup-infra.sh +102 -0
  37. package/dist/templates/har-boilerplate-cli/stages/README.sh +9 -0
  38. package/dist/templates/har-boilerplate-cli/stages.json +61 -0
  39. package/dist/templates/har-boilerplate-cli/teardown.sh +66 -0
  40. package/dist/templates/har-boilerplate-cli/verify.sh +113 -0
  41. package/dist/templates/har-boilerplate-ios/ADAPT-PROMPT.md +57 -0
  42. package/dist/templates/har-boilerplate-ios/CLAUDE.agent.md +65 -0
  43. package/dist/templates/har-boilerplate-ios/README.md +88 -0
  44. package/dist/templates/har-boilerplate-ios/agent-cli.sh +125 -0
  45. package/dist/templates/har-boilerplate-ios/agent-slot.sh +270 -0
  46. package/dist/templates/har-boilerplate-ios/docker-compose.agent.yml +17 -0
  47. package/dist/templates/har-boilerplate-ios/harness.env +42 -0
  48. package/dist/templates/har-boilerplate-ios/justfile +33 -0
  49. package/dist/templates/har-boilerplate-ios/launch.sh +132 -0
  50. package/dist/templates/har-boilerplate-ios/setup-infra.sh +84 -0
  51. package/dist/templates/har-boilerplate-ios/stages/README.sh +12 -0
  52. package/dist/templates/har-boilerplate-ios/stages.json +61 -0
  53. package/dist/templates/har-boilerplate-ios/teardown.sh +64 -0
  54. package/dist/templates/har-boilerplate-ios/verify.sh +165 -0
  55. package/dist/templates/stage-templates/playwright/.github/workflows/playwright.yml +30 -0
  56. package/dist/templates/stage-templates/playwright/.har/stages/PLAYWRIGHT.md +21 -0
  57. package/dist/templates/stage-templates/playwright/.har/stages/browser-e2e.sh +83 -0
  58. package/dist/templates/stage-templates/playwright/README.md +14 -0
  59. package/dist/templates/stage-templates/playwright/package.fragment.json +14 -0
  60. package/dist/templates/stage-templates/playwright/playwright.config.js +30 -0
  61. package/dist/templates/stage-templates/playwright/template.manifest.json +44 -0
  62. package/dist/templates/stage-templates/playwright/tests/a11y/smoke.spec.js +14 -0
  63. package/dist/templates/stage-templates/playwright/tests/api/health.spec.js +10 -0
  64. package/dist/templates/stage-templates/playwright/tests/frontend/smoke.spec.js +9 -0
  65. package/dist/templates/stage-templates/rocketsim/.har/stages/ROCKETSIM.md +126 -0
  66. package/dist/templates/stage-templates/rocketsim/.har/stages/rocketsim-flows.sh +188 -0
  67. package/dist/templates/stage-templates/rocketsim/README.md +66 -0
  68. package/dist/templates/stage-templates/rocketsim/flows/README.md +36 -0
  69. package/dist/templates/stage-templates/rocketsim/flows/example-smoke.sh +60 -0
  70. package/dist/templates/stage-templates/rocketsim/template.manifest.json +38 -0
  71. 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'"