@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,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