@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,17 @@
|
|
|
1
|
+
version: "3.8"
|
|
2
|
+
# Add backend services needed by your iOS app here.
|
|
3
|
+
# Example: a mock API server or a local database.
|
|
4
|
+
#
|
|
5
|
+
# List services in HARNESS_INFRA_SERVICES in harness.env to enable them.
|
|
6
|
+
# They start once on fixed ports and are shared by all agent slots.
|
|
7
|
+
#
|
|
8
|
+
# services:
|
|
9
|
+
# mock-server:
|
|
10
|
+
# image: stoplight/prism:4
|
|
11
|
+
# command: mock -h 0.0.0.0 /api/openapi.yaml
|
|
12
|
+
# ports:
|
|
13
|
+
# - "4010:4010"
|
|
14
|
+
# volumes:
|
|
15
|
+
# - ../api:/api:ro
|
|
16
|
+
|
|
17
|
+
services: {}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# iOS mobile app harness — Xcode / iOS Simulator, no PM2, no Docker infra
|
|
2
|
+
# Generated by har env init --profile ios
|
|
3
|
+
|
|
4
|
+
export HARNESS_PROJECT_NAME=__PROJECT_NAME__
|
|
5
|
+
export HARNESS_USE_WORKTREE=true
|
|
6
|
+
|
|
7
|
+
export HARNESS_AGENT_SLOT_MIN=1
|
|
8
|
+
export HARNESS_AGENT_SLOT_MAX=3
|
|
9
|
+
|
|
10
|
+
# ── Xcode project ─────────────────────────────────────────────────────────────
|
|
11
|
+
# Set one of HARNESS_XCODE_WORKSPACE or HARNESS_XCODE_PROJECT (workspace wins).
|
|
12
|
+
export HARNESS_XCODE_WORKSPACE="" # e.g. MyApp.xcworkspace (relative to repo root)
|
|
13
|
+
export HARNESS_XCODE_PROJECT="" # e.g. MyApp.xcodeproj (relative to repo root)
|
|
14
|
+
|
|
15
|
+
# Xcode scheme to build and test (must be marked as shared in Xcode).
|
|
16
|
+
export HARNESS_XCODE_SCHEME="MyApp"
|
|
17
|
+
|
|
18
|
+
# iOS Simulator to target. Use: xcrun simctl list devices | grep Booted
|
|
19
|
+
# Common values: "iPhone 16", "iPhone 16 Pro", "iPhone SE (3rd generation)"
|
|
20
|
+
export HARNESS_SIMULATOR_NAME="iPhone 16"
|
|
21
|
+
|
|
22
|
+
# Bundle ID of the app under test. Used by setup-infra.sh / RocketSim flows.
|
|
23
|
+
export HARNESS_BUNDLE_ID="com.example.myapp"
|
|
24
|
+
|
|
25
|
+
# Derived xcodebuild destination string — override only if targeting a specific UDID.
|
|
26
|
+
export HARNESS_IOS_DESTINATION="platform=iOS Simulator,name=${HARNESS_SIMULATOR_NAME}"
|
|
27
|
+
|
|
28
|
+
# Optional SwiftLint path (leave empty to use system-installed swiftlint).
|
|
29
|
+
export HARNESS_SWIFTLINT_CMD="${HARNESS_SWIFTLINT_CMD:-swiftlint}"
|
|
30
|
+
|
|
31
|
+
# ── Infrastructure ────────────────────────────────────────────────────────────
|
|
32
|
+
# No Docker infra for pure iOS apps. Set a service name here (space-separated)
|
|
33
|
+
# only if your app needs a local backend container (e.g. a mock server).
|
|
34
|
+
export HARNESS_INFRA_SERVICES=""
|
|
35
|
+
|
|
36
|
+
# Returns 0 when a shared infra service is enabled: har_infra_enabled mock-server
|
|
37
|
+
har_infra_enabled() {
|
|
38
|
+
case " ${HARNESS_INFRA_SERVICES:-} " in
|
|
39
|
+
*" $1 "*) return 0 ;;
|
|
40
|
+
*) return 1 ;;
|
|
41
|
+
esac
|
|
42
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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}}
|
|
28
|
+
|
|
29
|
+
simulator:
|
|
30
|
+
xcrun simctl list devices available | grep -E 'iPhone|iPad' | head -20
|
|
31
|
+
|
|
32
|
+
flows id:
|
|
33
|
+
./stages/rocketsim-flows.sh {{id}}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Launch an agent slot for iOS mobile app repos (git worktree by default).
|
|
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,84 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Sets up shared infrastructure for all iOS agent slots.
|
|
3
|
+
# Boots the target iOS Simulator if not already running. No Docker required
|
|
4
|
+
# for pure iOS apps — extend only when a local backend container is needed.
|
|
5
|
+
# Idempotent — safe to run multiple times.
|
|
6
|
+
#
|
|
7
|
+
# Usage: ./.har/setup-infra.sh
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
|
|
12
|
+
# shellcheck source=/dev/null
|
|
13
|
+
source "$SCRIPT_DIR/harness.env"
|
|
14
|
+
|
|
15
|
+
log() { echo "==> $*" >&2; }
|
|
16
|
+
|
|
17
|
+
# ── Xcode check ───────────────────────────────────────────────────────────────
|
|
18
|
+
if ! command -v xcodebuild >/dev/null 2>&1; then
|
|
19
|
+
echo "Error: xcodebuild not found — install Xcode from the App Store." >&2
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
XCODE_VERSION="$(xcodebuild -version 2>/dev/null | head -1 || echo 'Xcode (unknown)')"
|
|
24
|
+
log "Found: $XCODE_VERSION"
|
|
25
|
+
|
|
26
|
+
# ── Simulator ─────────────────────────────────────────────────────────────────
|
|
27
|
+
SIMULATOR_NAME="${HARNESS_SIMULATOR_NAME:-iPhone 16}"
|
|
28
|
+
|
|
29
|
+
log "Checking simulator: ${SIMULATOR_NAME}"
|
|
30
|
+
|
|
31
|
+
UDID="$(xcrun simctl list devices available 2>/dev/null \
|
|
32
|
+
| grep "${SIMULATOR_NAME}" \
|
|
33
|
+
| grep -E '\([A-F0-9-]{36}\)' \
|
|
34
|
+
| head -1 \
|
|
35
|
+
| grep -oE '[A-F0-9-]{36}' \
|
|
36
|
+
| head -1 || true)"
|
|
37
|
+
|
|
38
|
+
if [ -z "$UDID" ]; then
|
|
39
|
+
echo "Error: Simulator '${SIMULATOR_NAME}' not found in available devices." >&2
|
|
40
|
+
echo " Available simulators:" >&2
|
|
41
|
+
xcrun simctl list devices available 2>/dev/null | grep -E 'iPhone|iPad' | head -20 >&2
|
|
42
|
+
echo " Update HARNESS_SIMULATOR_NAME in .har/harness.env." >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
STATE="$(xcrun simctl list devices 2>/dev/null \
|
|
47
|
+
| grep "$UDID" \
|
|
48
|
+
| grep -oE 'Booted|Shutdown|Unavailable' \
|
|
49
|
+
| head -1 || echo "unknown")"
|
|
50
|
+
|
|
51
|
+
if [ "$STATE" = "Booted" ]; then
|
|
52
|
+
log "Simulator '${SIMULATOR_NAME}' ($UDID) is already booted."
|
|
53
|
+
else
|
|
54
|
+
log "Booting simulator '${SIMULATOR_NAME}' ($UDID)..."
|
|
55
|
+
xcrun simctl boot "$UDID" 2>/dev/null || true
|
|
56
|
+
|
|
57
|
+
log "Waiting for simulator to become ready..."
|
|
58
|
+
for i in $(seq 1 30); do
|
|
59
|
+
CURRENT="$(xcrun simctl list devices 2>/dev/null | grep "$UDID" | grep -oE 'Booted' | head -1 || true)"
|
|
60
|
+
if [ "$CURRENT" = "Booted" ]; then
|
|
61
|
+
log "Simulator is ready."
|
|
62
|
+
break
|
|
63
|
+
fi
|
|
64
|
+
if [ "$i" = "30" ]; then
|
|
65
|
+
echo "Warning: Simulator did not reach Booted state within 30 seconds." >&2
|
|
66
|
+
fi
|
|
67
|
+
sleep 1
|
|
68
|
+
done
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# ── Optional Docker infra ─────────────────────────────────────────────────────
|
|
72
|
+
SERVICES="${HARNESS_INFRA_SERVICES:-}"
|
|
73
|
+
if [ -n "$SERVICES" ]; then
|
|
74
|
+
COMPOSE_FILE="$SCRIPT_DIR/docker-compose.agent.yml"
|
|
75
|
+
COMPOSE_PROJECT="har-${HARNESS_PROJECT_NAME}"
|
|
76
|
+
log "Starting shared infrastructure (project: $COMPOSE_PROJECT): $SERVICES"
|
|
77
|
+
docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" up -d $SERVICES
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
log ""
|
|
81
|
+
log "Infrastructure is ready."
|
|
82
|
+
log " Simulator: ${SIMULATOR_NAME} ($UDID)"
|
|
83
|
+
har_infra_enabled mock-server && log " Mock server: running (see docker-compose.agent.yml)"
|
|
84
|
+
exit 0
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Optional per-stage scripts live here.
|
|
3
|
+
# Example: ./.har/stages/rocketsim-flows.sh <agent-id>
|
|
4
|
+
#
|
|
5
|
+
# Register custom stages in .har/stages.json with a project-owned command:
|
|
6
|
+
# "command": "./.har/stages/rocketsim-flows.sh {agentId}"
|
|
7
|
+
#
|
|
8
|
+
# To add the RocketSim user-flow stage template:
|
|
9
|
+
# har env add-stage rocketsim
|
|
10
|
+
|
|
11
|
+
echo "Add stage scripts under .har/stages/ and register them in stages.json." >&2
|
|
12
|
+
exit 1
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1",
|
|
3
|
+
"agentSlots": {
|
|
4
|
+
"min": 1,
|
|
5
|
+
"max": 3
|
|
6
|
+
},
|
|
7
|
+
"stages": [
|
|
8
|
+
{
|
|
9
|
+
"id": "setup-infra",
|
|
10
|
+
"kind": "setup",
|
|
11
|
+
"description": "Boot iOS Simulator and start optional shared infra",
|
|
12
|
+
"command": "./.har/setup-infra.sh"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "launch",
|
|
16
|
+
"kind": "launch",
|
|
17
|
+
"description": "Prepare agent slot (git worktree, iOS deps, env file)",
|
|
18
|
+
"command": "./.har/launch.sh {agentId}",
|
|
19
|
+
"artifacts": [
|
|
20
|
+
{
|
|
21
|
+
"path": ".env.agent.{agentId}",
|
|
22
|
+
"kind": "file",
|
|
23
|
+
"description": "Generated per-agent environment file"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "verify",
|
|
29
|
+
"kind": "verify",
|
|
30
|
+
"description": "Verification pipeline (quick: build + unit-tests; --full adds lint + rocketsim-flows)",
|
|
31
|
+
"command": "./.har/verify.sh {agentId}",
|
|
32
|
+
"acceptsArgs": ["--full"],
|
|
33
|
+
"artifacts": [
|
|
34
|
+
{
|
|
35
|
+
"path": ".har/artifacts",
|
|
36
|
+
"kind": "directory",
|
|
37
|
+
"description": "Optional verification outputs (test results, screenshots)"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "status",
|
|
43
|
+
"kind": "inspect",
|
|
44
|
+
"description": "Inspect agent slot status, simulator, and worktree path",
|
|
45
|
+
"command": "./.har/agent-cli.sh {agentId} status",
|
|
46
|
+
"artifacts": [
|
|
47
|
+
{
|
|
48
|
+
"path": ".har/logs",
|
|
49
|
+
"kind": "directory",
|
|
50
|
+
"description": "Optional harness logs"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"id": "teardown",
|
|
56
|
+
"kind": "teardown",
|
|
57
|
+
"description": "Remove worktree and env file for the slot",
|
|
58
|
+
"command": "./.har/teardown.sh {agentId}"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|