@osfactory/har 0.48.0 → 0.49.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/README.md CHANGED
@@ -14,7 +14,7 @@ HAR is an open-source, agent-agnostic framework for building multi-agent coding
14
14
  <tr>
15
15
  <td align="center">
16
16
  <a href="https://youtu.be/XKl4ZzWy7mQ">
17
- <img src="https://img.youtube.com/vi/XKl4ZzWy7mQ/maxresdefault.jpg" alt="HAR introduction demo" width="540">
17
+ <img src="http://img.youtube.com/vi/XKl4ZzWy7mQ/maxresdefault.jpg" alt="HAR introduction demo" width="600"/>
18
18
  </a>
19
19
  </td>
20
20
  </tr>
@@ -108,7 +108,7 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for local setup, the dogfood harness lo
108
108
 
109
109
  ## Sponsors
110
110
 
111
- HAR is sponsored by [Kerno](https://kerno.io), runtime code and security review for coding agents.
111
+ HAR is sponsored by [Kerno](https://kerno.io), runtime code and security tester for coding agents.
112
112
 
113
113
  <p align="left">
114
114
  <a href="https://kerno.io">
@@ -30,7 +30,9 @@ pt_log() {
30
30
  append_env() {
31
31
  local key="$1"
32
32
  local value="$2"
33
- printf '%s=%s\n' "$key" "$value" >> "$HAR_ENV_FILE"
33
+ # %q, not a raw value: an unquoted value with spaces (an Xcode scheme, a
34
+ # simulator name, a path under /Applications) breaks `source .env.agent.<id>`.
35
+ printf '%s=%q\n' "$key" "$value" >> "$HAR_ENV_FILE"
34
36
  }
35
37
 
36
38
  append_path_prefix() {
@@ -235,9 +237,8 @@ provision_ios() {
235
237
  if [ -n "${HARNESS_XCODE_SCHEME:-}" ]; then
236
238
  append_env "HARNESS_XCODE_SCHEME" "$HARNESS_XCODE_SCHEME"
237
239
  fi
238
- if [ -n "${HARNESS_SIMULATOR_NAME:-}" ]; then
239
- append_env "HARNESS_SIMULATOR_NAME" "$HARNESS_SIMULATOR_NAME"
240
- fi
240
+ # The simulator is not recorded here: launch.sh writes the device reserved for
241
+ # this slot, and harness.env already carries the shared one.
241
242
  if [ -n "${HARNESS_BUNDLE_ID:-}" ]; then
242
243
  append_env "HARNESS_BUNDLE_ID" "$HARNESS_BUNDLE_ID"
243
244
  fi
@@ -30,7 +30,9 @@ pt_log() {
30
30
  append_env() {
31
31
  local key="$1"
32
32
  local value="$2"
33
- printf '%s=%s\n' "$key" "$value" >> "$HAR_ENV_FILE"
33
+ # %q, not a raw value: an unquoted value with spaces (an Xcode scheme, a
34
+ # simulator name, a path under /Applications) breaks `source .env.agent.<id>`.
35
+ printf '%s=%q\n' "$key" "$value" >> "$HAR_ENV_FILE"
34
36
  }
35
37
 
36
38
  append_path_prefix() {
@@ -235,9 +237,8 @@ provision_ios() {
235
237
  if [ -n "${HARNESS_XCODE_SCHEME:-}" ]; then
236
238
  append_env "HARNESS_XCODE_SCHEME" "$HARNESS_XCODE_SCHEME"
237
239
  fi
238
- if [ -n "${HARNESS_SIMULATOR_NAME:-}" ]; then
239
- append_env "HARNESS_SIMULATOR_NAME" "$HARNESS_SIMULATOR_NAME"
240
- fi
240
+ # The simulator is not recorded here: launch.sh writes the device reserved for
241
+ # this slot, and harness.env already carries the shared one.
241
242
  if [ -n "${HARNESS_BUNDLE_ID:-}" ]; then
242
243
  append_env "HARNESS_BUNDLE_ID" "$HARNESS_BUNDLE_ID"
243
244
  fi
@@ -8,7 +8,7 @@
8
8
  |--|--|
9
9
  | **Agent ID** | ${AGENT_ID} |
10
10
  | **Work dir** | Fresh session worktree per launch — see the launch output or `.har/slots/agent-${AGENT_ID}.json` |
11
- | **Simulator** | Configured in `.har/harness.env` — `HARNESS_SIMULATOR_NAME` |
11
+ | **Simulator** | Created for this slot at launch — `HARNESS_SIMULATOR_UDID` in `.env.agent.${AGENT_ID}` |
12
12
  | **Scheme** | Configured in `.har/harness.env` — `HARNESS_XCODE_SCHEME` |
13
13
 
14
14
  **Never edit the main checkout** — launch FIRST, then make ALL file edits under the work dir from the launch output. An occupied slot always blocks a new launch — run `har env teardown <id>` (or `complete <id>`) first, then launch again.
@@ -50,21 +50,25 @@ Stages are the harness's single vocabulary for checks: templates and custom stag
50
50
 
51
51
  ## Project commands
52
52
 
53
+ Target **this slot's** simulator, never `booted` — another agent's device may also
54
+ be booted. `HARNESS_IOS_DESTINATION` and `HARNESS_SIMULATOR_UDID` come from
55
+ `.env.agent.${AGENT_ID}`.
56
+
53
57
  ```bash
54
- # Build
55
- xcodebuild build -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 16' CODE_SIGNING_ALLOWED=NO
58
+ source .env.agent.${AGENT_ID}
56
59
 
57
- # Run unit tests
58
- xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 16' CODE_SIGNING_ALLOWED=NO
60
+ # Build
61
+ xcodebuild build -scheme MyApp -destination "$HARNESS_IOS_DESTINATION" CODE_SIGNING_ALLOWED=NO
59
62
 
60
- # Install app on booted simulator
61
- xcrun simctl install booted path/to/MyApp.app
63
+ # Run unit tests — signed, or an app with entitlements traps at launch
64
+ xcodebuild test -scheme MyApp -destination "$HARNESS_IOS_DESTINATION"
62
65
 
63
- # Launch app
64
- xcrun simctl launch booted com.example.myapp
66
+ # Install and launch on this slot's device
67
+ ./.har/agent-cli.sh ${AGENT_ID} install path/to/MyApp.app
68
+ ./.har/agent-cli.sh ${AGENT_ID} launch-app
65
69
  ```
66
70
 
67
- Adapt for your scheme and simulator — see `.har/harness.env`.
71
+ Adapt for your scheme — see `.har/harness.env`.
68
72
 
69
73
  ## User-flow validation (RocketSim)
70
74
 
@@ -18,8 +18,9 @@ Generated and maintained by [`har`](https://github.com/os-factory/har). Run `har
18
18
  | `runs/` | Run history from `har env` / MCP — gitignored |
19
19
  | `artifacts/` | Stage outputs: test results, screenshots, logs |
20
20
  | `agent-slot.sh` | Shared agent-id validation and slot registry helpers |
21
- | `setup-infra.sh` | Boot the iOS Simulator; start optional Docker services |
22
- | `launch.sh` | Launch one agent slot (git worktree, toolchain provisioning, env file) |
21
+ | `setup-infra.sh` | Check the toolchain; start optional Docker services |
22
+ | `simulator.sh` | Create, boot and delete one iOS Simulator per agent slot |
23
+ | `launch.sh` | Launch one agent slot (git worktree, toolchain provisioning, simulator, env file) |
23
24
  | `provision-toolchain.sh` | Write Xcode/simulator paths (`XCODEBUILD_BIN`, …) to `.env.agent.<id>` |
24
25
  | `verify.sh` | Verification pipeline (build smoke by default; --full adds tests, lint, flows) |
25
26
  | `teardown.sh` | Tear down one agent slot (worktree + env file) |
@@ -82,9 +83,37 @@ dev setup and add a readiness command when agents need a real workflow to pass.
82
83
  Edit **`harness.env`** to set:
83
84
  - `HARNESS_XCODE_SCHEME` — your app's shared Xcode scheme
84
85
  - `HARNESS_XCODE_WORKSPACE` or `HARNESS_XCODE_PROJECT` — path to the project file
85
- - `HARNESS_SIMULATOR_NAME` — target simulator (must be listed in `xcrun simctl list devices`)
86
+ - `HARNESS_SIMULATOR_NAME` — preferred simulator, matched by exact name
86
87
  - `HARNESS_BUNDLE_ID` — app bundle identifier
87
88
 
89
+ ## Simulators
90
+
91
+ Slots do not share a simulator. Each launch **creates**
92
+ `har-<project>-agent-<id>-<model>` — `har-storefront-agent-2-iPhone-17`, say — and
93
+ teardown deletes it, so two agents never collide on a destination, on an installed
94
+ bundle id, or on a UI session, and the simulators you use by hand are never touched.
95
+ Every launch also starts from a pristine device: no app installed, no leftover
96
+ store, nothing carried over from the previous session.
97
+
98
+ | Setting | Effect |
99
+ |---------|--------|
100
+ | `HARNESS_SIMULATOR_NAME` | The model to create — `iPhone 16`, `iPhone 17 Pro`, `iPad Air 11-inch (M2)`. Empty means the newest model of the family. See `xcrun simctl list devicetypes`. |
101
+ | `HARNESS_SIMULATOR_FAMILY` | `auto` (read from the name) \| `iPhone` \| `iPad`. An iPad is only created when this resolves to iPad. |
102
+ | `HARNESS_SIMULATOR_UDID` | Run every slot on one existing device instead. Slots then share it — for one-off debugging. |
103
+ | `HARNESS_SIMULATOR_SHARED` | `true` restores one shared simulator for all slots, booted by `setup-infra.sh`. |
104
+
105
+ The runtime is the newest installed iOS that supports the model — a model retired
106
+ from recent runtimes still resolves against an older one. When nothing matches,
107
+ launch fails and lists the models this machine can create.
108
+
109
+ If `HARNESS_SIMULATOR_NAME` is not a model but names an existing device, that
110
+ device is used as-is and never deleted — how a hand-renamed simulator is targeted.
111
+
112
+ The device lands in `.env.agent.<id>` as `HARNESS_SIMULATOR_UDID`,
113
+ `HARNESS_SIMULATOR_DEVICE_NAME` and `HARNESS_IOS_DESTINATION` — the model stays in
114
+ `HARNESS_SIMULATOR_NAME`, so the two never mean the same thing in one place; what a slot holds is tracked in `.har/simulators/`.
115
+ Use `./.har/agent-cli.sh <id> simulator` to see it.
116
+
88
117
  ## Port & shared services (iOS profile)
89
118
 
90
119
  Pure iOS apps have **no per-slot TCP ports** — agents build and run on a shared iOS Simulator. Optional backend containers (mock API, etc.) use the same shared-infra model as other profiles.
@@ -94,7 +123,7 @@ Pure iOS apps have **no per-slot TCP ports** — agents build and run on a share
94
123
  | Layer | Scope | Rule | On conflict |
95
124
  |-------|-------|------|-------------|
96
125
  | Optional backend (compose) | Per machine | `HARNESS_*_PORT_DEFAULT` for that service | Scan configured ranges in `harness.env` |
97
- | iOS Simulator | Shared | No harness port — one booted simulator for all slots | N/A |
126
+ | iOS Simulator | Per slot | One device created per agent — no harness port | None: `har-<project>-agent-<id>-<model>` is unique per slot |
98
127
 
99
128
  When your app talks to a local backend, read the resolved host port from `.env.agent.<id>` (set by `setup-infra.sh`) — never hardcode `15432` or similar in tests or flow scripts.
100
129
 
@@ -102,7 +131,7 @@ When your app talks to a local backend, read the resolved host port from `.env.a
102
131
 
103
132
  | Resource | Model | Configuration |
104
133
  |----------|-------|---------------|
105
- | iOS Simulator | One booted simulator shared by all slots | `HARNESS_SIMULATOR_NAME`, `setup-infra.sh` |
134
+ | iOS Simulator | One device created per slot, deleted at teardown | `HARNESS_SIMULATOR_*`, `launch.sh` |
106
135
  | Optional backend | One shared compose service | `HARNESS_INFRA_SERVICES` (e.g. `"mock-server"`) |
107
136
  | Application code | Isolated git worktree per slot | `HARNESS_USE_WORKTREE=true` |
108
137
 
@@ -11,6 +11,8 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
11
11
  source "$SCRIPT_DIR/harness.env"
12
12
  # shellcheck source=/dev/null
13
13
  source "$SCRIPT_DIR/agent-slot.sh"
14
+ # shellcheck source=/dev/null
15
+ source "$SCRIPT_DIR/simulator.sh"
14
16
 
15
17
  AGENT_ID="${1:?Usage: agent-cli.sh <agent-id> <command> [args...]}"
16
18
  COMMAND="${2:?Usage: agent-cli.sh <agent-id> <command> [args...]}"
@@ -19,6 +21,18 @@ validate_agent_id "$AGENT_ID"
19
21
 
20
22
  WORKTREE_DIR="$(existing_slot_worktree "$AGENT_ID")"
21
23
 
24
+ # simctl target for this slot: its own device when one is reserved, else whatever
25
+ # is booted — never another slot's simulator.
26
+ slot_sim_target() {
27
+ local claim udid
28
+ claim="$(har_sim_claim_file "$AGENT_ID")"
29
+ if [ -f "$claim" ]; then
30
+ udid="$(read_slot_field "$claim" udid || true)"
31
+ [ -n "$udid" ] && { echo "$udid"; return; }
32
+ fi
33
+ echo "booted"
34
+ }
35
+
22
36
  resolve_work_dir() {
23
37
  local env_file
24
38
  env_file="$(resolve_agent_env_file "$AGENT_ID" "$REPO_ROOT")" || {
@@ -42,7 +56,7 @@ case "$COMMAND" in
42
56
  echo " Work dir: $(resolve_agent_work_dir "$ENV_FILE" "$AGENT_ID")"
43
57
  [ -n "$WORKTREE_DIR" ] && [ -d "$WORKTREE_DIR" ] && echo " Worktree: $WORKTREE_DIR"
44
58
  echo " Scheme: ${HARNESS_XCODE_SCHEME:-not set}"
45
- echo " Simulator: ${HARNESS_SIMULATOR_NAME:-not set}"
59
+ echo " Simulator: ${HARNESS_SIMULATOR_DEVICE_NAME:-${HARNESS_SIMULATOR_NAME:-not set}} ($(slot_sim_target))"
46
60
  echo " Bundle ID: ${HARNESS_BUNDLE_ID:-not set}"
47
61
  else
48
62
  echo "No active environment for agent ${AGENT_ID}"
@@ -51,14 +65,22 @@ case "$COMMAND" in
51
65
  ;;
52
66
 
53
67
  simulator)
54
- # Show simulator state and UDID for the configured device.
55
- SIM_NAME="${HARNESS_SIMULATOR_NAME:-iPhone 16}"
56
- echo "Simulator: ${SIM_NAME}"
57
- xcrun simctl list devices available 2>/dev/null \
58
- | grep "${SIM_NAME}" \
59
- | grep -E '\([A-F0-9-]{36}\)' \
60
- | head -5 \
61
- || echo " (no matching simulator found — check HARNESS_SIMULATOR_NAME in harness.env)"
68
+ # Show the device reserved for this slot, or the configured one when idle.
69
+ TARGET="$(slot_sim_target)"
70
+ if [ "$TARGET" != "booted" ]; then
71
+ echo "Simulator: $(har_sim_device_name "$TARGET") (${TARGET})"
72
+ echo "State: $(har_sim_device_state "$TARGET")"
73
+ echo "Reserved: $(har_sim_claim_file "$AGENT_ID")"
74
+ else
75
+ SIM_NAME="${HARNESS_SIMULATOR_NAME:-iPhone 16}"
76
+ echo "Simulator: ${SIM_NAME} (no device reserved — the slot is not launched)"
77
+ RESOLVED="$(har_sim_resolve_configured)"
78
+ if [ -n "$RESOLVED" ]; then
79
+ echo "Configured device: ${RESOLVED#*$'\t'} (${RESOLVED%%$'\t'*})"
80
+ else
81
+ echo " (no matching simulator found — check HARNESS_SIMULATOR_NAME in harness.env)"
82
+ fi
83
+ fi
62
84
  ;;
63
85
 
64
86
  logs)
@@ -70,7 +92,7 @@ case "$COMMAND" in
70
92
  restart)
71
93
  echo "iOS profile has no managed processes to restart." >&2
72
94
  echo "Relaunch the slot or reinstall the app with:" >&2
73
- echo " ./.har/agent-cli.sh ${AGENT_ID} exec xcrun simctl install booted <path-to.app>" >&2
95
+ echo " ./.har/agent-cli.sh ${AGENT_ID} install <path-to.app>" >&2
74
96
  exit 1
75
97
  ;;
76
98
 
@@ -80,8 +102,9 @@ case "$COMMAND" in
80
102
  echo "Usage: agent-cli.sh ${AGENT_ID} install <path-to.app>" >&2
81
103
  exit 1
82
104
  fi
83
- echo "==> Installing ${APP_PATH} on simulator..."
84
- xcrun simctl install booted "$APP_PATH"
105
+ TARGET="$(slot_sim_target)"
106
+ echo "==> Installing ${APP_PATH} on ${TARGET}..."
107
+ xcrun simctl install "$TARGET" "$APP_PATH"
85
108
  echo "✓ Installed"
86
109
  ;;
87
110
 
@@ -92,8 +115,9 @@ case "$COMMAND" in
92
115
  echo " or set HARNESS_BUNDLE_ID in harness.env" >&2
93
116
  exit 1
94
117
  fi
95
- echo "==> Launching ${BUNDLE} on simulator..."
96
- xcrun simctl launch booted "$BUNDLE"
118
+ TARGET="$(slot_sim_target)"
119
+ echo "==> Launching ${BUNDLE} on ${TARGET}..."
120
+ xcrun simctl launch "$TARGET" "$BUNDLE"
97
121
  ;;
98
122
 
99
123
  url)
@@ -101,7 +125,7 @@ case "$COMMAND" in
101
125
  echo "Work dir: $WORK_DIR"
102
126
  [ -d "$WORKTREE_DIR" ] && echo "Worktree: $WORKTREE_DIR"
103
127
  echo "Scheme: ${HARNESS_XCODE_SCHEME:-not set}"
104
- echo "Simulator: ${HARNESS_SIMULATOR_NAME:-not set}"
128
+ echo "Simulator: ${HARNESS_SIMULATOR_DEVICE_NAME:-${HARNESS_SIMULATOR_NAME:-not set}} ($(slot_sim_target))"
105
129
  echo "Bundle ID: ${HARNESS_BUNDLE_ID:-not set}"
106
130
  har_infra_enabled mock-server && echo "Mock: check docker-compose.agent.yml for port"
107
131
  ;;
@@ -13,6 +13,7 @@ AGENT.md.proposed.meta.json
13
13
  maintain/
14
14
  ADAPT-PROMPT.md
15
15
  slots/
16
+ simulators/
16
17
 
17
18
  # Generated by launch.sh
18
19
  .env.agent.*
@@ -19,14 +19,38 @@ export HARNESS_XCODE_PROJECT="" # e.g. MyApp.xcodeproj (relative to repo r
19
19
  # Xcode scheme to build and test (must be marked as shared in Xcode).
20
20
  export HARNESS_XCODE_SCHEME="MyApp"
21
21
 
22
- # iOS Simulator to target. Use: xcrun simctl list devices | grep Booted
23
- # Common values: "iPhone 16", "iPhone 16 Pro", "iPhone SE (3rd generation)"
22
+ # Simulator model each slot runs on, e.g. "iPhone 16", "iPhone 16 Pro", "iPad Air 11-inch (M2)".
23
+ # Use: xcrun simctl list devicetypes. Leave empty for the newest model of the family.
24
24
  export HARNESS_SIMULATOR_NAME="iPhone 16"
25
25
 
26
- # Bundle ID of the app under test. Used by setup-infra.sh / RocketSim flows.
26
+ # Bundle ID of the app under test. Used by agent-cli.sh / RocketSim flows.
27
27
  export HARNESS_BUNDLE_ID="com.example.myapp"
28
28
 
29
- # Derived xcodebuild destination string — override only if targeting a specific UDID.
29
+ # ── Simulator allocation ──────────────────────────────────────────────────────
30
+ # Every slot gets its own simulator, "har-<project>-agent-<id>-<model>", created
31
+ # at launch from HARNESS_SIMULATOR_NAME and deleted at teardown. Concurrent agents
32
+ # therefore never share a destination, an installed bundle id, or a UI session,
33
+ # each launch starts from a pristine device, and the simulators you use by hand
34
+ # are never touched. launch.sh writes the device into .env.agent.<id>.
35
+ #
36
+ # The runtime is the newest installed iOS that supports the model; launch fails
37
+ # with the list of available models when none does.
38
+
39
+ # Family used when HARNESS_SIMULATOR_NAME is empty, and enforced when it is set:
40
+ # auto (infer from the name) | iPhone | iPad. An iPad is only ever created when
41
+ # this resolves to iPad.
42
+ export HARNESS_SIMULATOR_FAMILY="auto"
43
+
44
+ # Run every slot on one existing device instead of creating any (bypasses model
45
+ # resolution — no fallback). Slots then share it, so use it for one-off debugging.
46
+ # export HARNESS_SIMULATOR_UDID=""
47
+
48
+ # Set true to go back to one shared simulator for all slots, resolved by exact
49
+ # name from HARNESS_SIMULATOR_NAME and booted by setup-infra.sh.
50
+ export HARNESS_SIMULATOR_SHARED=false
51
+
52
+ # Destination used when slots share a simulator; per-slot launches override it
53
+ # with the reserved device's UDID.
30
54
  export HARNESS_IOS_DESTINATION="platform=iOS Simulator,name=${HARNESS_SIMULATOR_NAME}"
31
55
 
32
56
  # Optional SwiftLint path (leave empty to use system-installed swiftlint).
@@ -14,6 +14,8 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
14
14
  source "$SCRIPT_DIR/harness.env"
15
15
  # shellcheck source=/dev/null
16
16
  source "$SCRIPT_DIR/agent-slot.sh"
17
+ # shellcheck source=/dev/null
18
+ source "$SCRIPT_DIR/simulator.sh"
17
19
 
18
20
  AGENT_ID="${1:-}"
19
21
  USE_WORKTREE="${HARNESS_USE_WORKTREE:-true}"
@@ -86,7 +88,9 @@ fi
86
88
  # whose .gitignore doesn't know about har (applies to all worktrees too).
87
89
  GIT_EXCLUDE="$(git -C "$REPO_ROOT" rev-parse --git-common-dir 2>/dev/null)/info/exclude"
88
90
  if [ -n "$GIT_EXCLUDE" ] && [ -d "$(dirname "$GIT_EXCLUDE")" ]; then
89
- for pattern in '.env.agent.*' 'ecosystem.agent.*.config.cjs' '.har/venv'; do
91
+ # .har/simulators covers harnesses scaffolded before it reached
92
+ # gitignore.template, which only lands on init or maintain.
93
+ for pattern in '.env.agent.*' 'ecosystem.agent.*.config.cjs' '.har/venv' '.har/simulators'; do
90
94
  grep -qxF "$pattern" "$GIT_EXCLUDE" 2>/dev/null || echo "$pattern" >> "$GIT_EXCLUDE"
91
95
  done
92
96
  fi
@@ -107,6 +111,7 @@ mark_slot_failed() {
107
111
  if [ "$exit_code" != "0" ] && [ "$REGISTRY_WRITTEN" = true ]; then
108
112
  log "Launch failed after creating the session. Recording failed slot state..."
109
113
  set +e
114
+ har_sim_release "$AGENT_ID"
110
115
  SLOT_AGENT_ID="$AGENT_ID" \
111
116
  SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
112
117
  SLOT_WORK_DIR="$WORK_DIR" \
@@ -147,6 +152,30 @@ HAR_REL_PREFIX="${REL_PREFIX:-}" \
147
152
  HAR_AGENT_ID="$AGENT_ID" \
148
153
  "$SCRIPT_DIR/provision-toolchain.sh"
149
154
 
155
+ # Give this slot its own simulator. Written after provision-toolchain.sh so the
156
+ # per-slot device wins over the shared default when verify.sh sources the file.
157
+ if har_sim_per_slot_enabled; then
158
+ log "Preparing this slot's simulator..."
159
+ SIM_RESULT=""
160
+ SIM_RESULT="$(har_sim_acquire "$AGENT_ID")" || {
161
+ log "Could not prepare a simulator — see the messages above."
162
+ exit 1
163
+ }
164
+ SIM_UDID="${SIM_RESULT%%$'\t'*}"
165
+ SIM_NAME="${SIM_RESULT#*$'\t'}"
166
+ # The device name goes under its own key: HARNESS_SIMULATOR_NAME means "model
167
+ # to create" in harness.env, and overwriting it here would make its meaning
168
+ # depend on which file was sourced last.
169
+ {
170
+ printf 'HARNESS_SIMULATOR_UDID=%q\n' "$SIM_UDID"
171
+ printf 'HARNESS_SIMULATOR_DEVICE_NAME=%q\n' "$SIM_NAME"
172
+ printf 'HARNESS_IOS_DESTINATION=%q\n' "platform=iOS Simulator,id=${SIM_UDID}"
173
+ } >> "$ENV_FILE"
174
+ log "Simulator: ${SIM_NAME} (${SIM_UDID})"
175
+ else
176
+ log "Shared simulator mode (HARNESS_SIMULATOR_SHARED=true) — slots share ${HARNESS_SIMULATOR_NAME:-the configured device}."
177
+ fi
178
+
150
179
  # Record the session in the slot registry — the source of truth for where
151
180
  # this slot's code lives (status/verify/teardown resolve through it).
152
181
  SLOT_AGENT_ID="$AGENT_ID" \
@@ -167,6 +196,9 @@ log " ${WORK_DIR}"
167
196
  if [ "$USE_WORKTREE" = true ]; then
168
197
  log " Branch: ${BRANCH} (based on ${BASE_BRANCH} @ ${BASE_COMMIT})"
169
198
  fi
199
+ if [ -n "${SIM_NAME:-}" ]; then
200
+ log " Simulator: ${SIM_NAME} (${SIM_UDID})"
201
+ fi
170
202
  log ""
171
203
  log " Verify: ./.har/verify.sh $AGENT_ID"
172
204
  log " Teardown: ./.har/teardown.sh $AGENT_ID (keeps the branch)"
@@ -30,7 +30,9 @@ pt_log() {
30
30
  append_env() {
31
31
  local key="$1"
32
32
  local value="$2"
33
- printf '%s=%s\n' "$key" "$value" >> "$HAR_ENV_FILE"
33
+ # %q, not a raw value: an unquoted value with spaces (an Xcode scheme, a
34
+ # simulator name, a path under /Applications) breaks `source .env.agent.<id>`.
35
+ printf '%s=%q\n' "$key" "$value" >> "$HAR_ENV_FILE"
34
36
  }
35
37
 
36
38
  append_path_prefix() {
@@ -235,9 +237,8 @@ provision_ios() {
235
237
  if [ -n "${HARNESS_XCODE_SCHEME:-}" ]; then
236
238
  append_env "HARNESS_XCODE_SCHEME" "$HARNESS_XCODE_SCHEME"
237
239
  fi
238
- if [ -n "${HARNESS_SIMULATOR_NAME:-}" ]; then
239
- append_env "HARNESS_SIMULATOR_NAME" "$HARNESS_SIMULATOR_NAME"
240
- fi
240
+ # The simulator is not recorded here: launch.sh writes the device reserved for
241
+ # this slot, and harness.env already carries the shared one.
241
242
  if [ -n "${HARNESS_BUNDLE_ID:-}" ]; then
242
243
  append_env "HARNESS_BUNDLE_ID" "$HARNESS_BUNDLE_ID"
243
244
  fi
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env bash
2
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.
3
+ # Checks the toolchain, and boots the configured simulator when slots share one
4
+ # (HARNESS_SIMULATOR_SHARED=true) — otherwise launch.sh boots each slot's own
5
+ # device. No Docker required for pure iOS apps — extend only when a local
6
+ # backend container is needed. Idempotent — safe to run multiple times.
6
7
  #
7
8
  # Usage: ./.har/setup-infra.sh
8
9
  set -euo pipefail
@@ -11,6 +12,10 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
12
 
12
13
  # shellcheck source=/dev/null
13
14
  source "$SCRIPT_DIR/harness.env"
15
+ # shellcheck source=/dev/null
16
+ source "$SCRIPT_DIR/agent-slot.sh"
17
+ # shellcheck source=/dev/null
18
+ source "$SCRIPT_DIR/simulator.sh"
14
19
 
15
20
  log() { echo "==> $*" >&2; }
16
21
 
@@ -26,46 +31,55 @@ log "Found: $XCODE_VERSION"
26
31
  # ── Simulator ─────────────────────────────────────────────────────────────────
27
32
  SIMULATOR_NAME="${HARNESS_SIMULATOR_NAME:-iPhone 16}"
28
33
 
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
34
+ if har_sim_per_slot_enabled; then
35
+ # Each slot creates and boots its own device at launch — booting a shared one
36
+ # here would only waste a device no slot is going to use. Resolve the model now
37
+ # so a missing runtime is reported before any worktree is created.
38
+ FAMILY="$(har_sim_preferred_family)"
39
+ PLAN="$(har_sim_plan_creation "${HARNESS_SIMULATOR_NAME:-}" "$FAMILY")"
40
+ PLAN_STATUS="${PLAN%%$'\t'*}"
41
+ if [ "$PLAN_STATUS" = "OK" ]; then
42
+ log "Per-slot simulators: launch creates one $(printf '%s' "$PLAN" | cut -f3) per agent."
43
+ elif [ "$PLAN_STATUS" = "NO_MODEL" ] && [ -n "$(har_sim_device_by_name "${HARNESS_SIMULATOR_NAME:-}")" ]; then
44
+ # Supported fallback: the name is an existing device rather than a model.
45
+ log "Per-slot simulators: launch reuses the existing device '${SIMULATOR_NAME}'."
46
+ else
47
+ # Fail here rather than at launch, which would already have created a
48
+ # worktree, a branch and an env file the user then has to tear down.
49
+ echo "Error: no simulator can be prepared for '${SIMULATOR_NAME}'." >&2
50
+ if [ "$PLAN" != "$PLAN_STATUS" ]; then
51
+ echo " Available ${FAMILY} models: ${PLAN#*$'\t'}" >&2
52
+ else
53
+ echo " No iOS runtime is installed — add one in Xcode → Settings → Components." >&2
54
+ fi
55
+ echo " Update HARNESS_SIMULATOR_NAME in .har/harness.env." >&2
56
+ exit 1
57
+ fi
58
+ else
59
+ log "Checking simulator: ${SIMULATOR_NAME}"
45
60
 
46
- STATE="$(xcrun simctl list devices 2>/dev/null \
47
- | grep "$UDID" \
48
- | grep -oE 'Booted|Shutdown|Unavailable' \
49
- | head -1 || echo "unknown")"
61
+ # Exact name match: a substring match on "iPhone 16" also hits "iPhone 16 Pro Max".
62
+ RESOLVED="$(har_sim_resolve_configured)"
63
+ UDID="${RESOLVED%%$'\t'*}"
50
64
 
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
65
+ if [ -z "$UDID" ]; then
66
+ echo "Error: Simulator '${SIMULATOR_NAME}' not found in available devices." >&2
67
+ echo " Available simulators:" >&2
68
+ xcrun simctl list devices available 2>/dev/null | grep -E 'iPhone|iPad' | head -20 >&2
69
+ echo " Update HARNESS_SIMULATOR_NAME in .har/harness.env." >&2
70
+ exit 1
71
+ fi
56
72
 
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
73
+ if [ "$(har_sim_device_state "$UDID")" = "Booted" ]; then
74
+ log "Simulator '${SIMULATOR_NAME}' ($UDID) is already booted."
75
+ else
76
+ log "Booting simulator '${SIMULATOR_NAME}' ($UDID)..."
77
+ if har_sim_boot "$UDID"; then
61
78
  log "Simulator is ready."
62
- break
79
+ else
80
+ echo "Warning: Simulator did not reach Booted state." >&2
63
81
  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
82
+ fi
69
83
  fi
70
84
 
71
85
  # ── Optional Docker infra ─────────────────────────────────────────────────────
@@ -79,6 +93,9 @@ fi
79
93
 
80
94
  log ""
81
95
  log "Infrastructure is ready."
82
- log " Simulator: ${SIMULATOR_NAME} ($UDID)"
96
+ # In per-slot mode the device is reported by launch, once it exists.
97
+ if [ -n "${UDID:-}" ]; then
98
+ log " Simulator: ${SIMULATOR_NAME} ($UDID)"
99
+ fi
83
100
  har_infra_enabled mock-server && log " Mock server: running (see docker-compose.agent.yml)"
84
101
  exit 0
@@ -0,0 +1,400 @@
1
+ #!/usr/bin/env bash
2
+ # Per-slot iOS Simulator allocation.
3
+ #
4
+ # One booted simulator shared by every agent slot means concurrent xcodebuild
5
+ # destinations, installs of the same bundle id, and UI flows collide. Each slot
6
+ # instead gets a simulator of its own, created at launch and deleted at teardown:
7
+ # unique by construction, pristine on every launch, and never a device the
8
+ # developer is using by hand.
9
+ #
10
+ # Source AFTER harness.env and agent-slot.sh — read_slot_field, slot_registry_file
11
+ # and SCRIPT_DIR come from there.
12
+ #
13
+ # What a slot holds is recorded in .har/simulators/agent-<id>.json, so teardown
14
+ # knows whether the device was HAR's to delete.
15
+
16
+ # ── Configuration ─────────────────────────────────────────────────────────────
17
+
18
+ # Preferred device family: auto (infer from HARNESS_SIMULATOR_NAME) | iPhone | iPad
19
+ har_sim_preferred_family() {
20
+ local configured="${HARNESS_SIMULATOR_FAMILY:-auto}"
21
+ case "$configured" in
22
+ iPhone|iPad) echo "$configured"; return ;;
23
+ esac
24
+ case "${HARNESS_SIMULATOR_NAME:-}" in
25
+ iPad*|iPAD*|ipad*) echo "iPad" ;;
26
+ *) echo "iPhone" ;;
27
+ esac
28
+ }
29
+
30
+ har_sim_per_slot_enabled() {
31
+ [ "${HARNESS_SIMULATOR_SHARED:-false}" != "true" ]
32
+ }
33
+
34
+ har_sim_log() { echo "==> simulator: $*" >&2; }
35
+
36
+ # "iPad Air 11-inch (M4)" → "iPad-Air-11-inch-M4"
37
+ har_sim_slug() {
38
+ printf '%s' "$1" | tr -c 'A-Za-z0-9._-' '-' | tr -s '-' | sed 's/^-//; s/-$//'
39
+ }
40
+
41
+ # Short digest of the repository path. Two checkouts can share a folder name,
42
+ # and therefore HARNESS_PROJECT_NAME, but not a path — without it the prefix
43
+ # sweep below would delete the other checkout's live device.
44
+ har_sim_repo_digest() {
45
+ local repo
46
+ # -P so the same checkout reached through a symlink yields the same digest.
47
+ repo="$(cd -P "${SCRIPT_DIR}/.." 2>/dev/null && pwd -P || echo "${SCRIPT_DIR}")"
48
+ node -e 'process.stdout.write(require("crypto").createHash("sha1").update(process.argv[1]).digest("hex").slice(0, 6))' "$repo"
49
+ }
50
+
51
+ # Stable part of a slot's device name. The model is appended after it, so a
52
+ # device left over from a launch on another model is still recognised as this
53
+ # slot's and cleaned up.
54
+ har_sim_device_label_prefix() {
55
+ echo "har-$(har_sim_slug "${HARNESS_PROJECT_NAME:-har}")-$(har_sim_repo_digest)-agent-${1}"
56
+ }
57
+
58
+ har_sim_device_label() {
59
+ local prefix model
60
+ prefix="$(har_sim_device_label_prefix "$1")"
61
+ model="$(har_sim_slug "${2:-}")"
62
+ [ -n "$model" ] && echo "${prefix}-${model}" || echo "$prefix"
63
+ }
64
+
65
+ # ── Claims ────────────────────────────────────────────────────────────────────
66
+ # Slots never compete for a device, so a claim only records what teardown must
67
+ # release — no locking, no cross-slot exclusion.
68
+
69
+ har_sim_claims_dir() { echo "${SCRIPT_DIR}/simulators"; }
70
+ har_sim_claim_file() { echo "$(har_sim_claims_dir)/agent-${1}.json"; }
71
+
72
+ har_sim_write_claim() {
73
+ local agent_id="$1" udid="$2" name="$3" created="$4" file
74
+ file="$(har_sim_claim_file "$agent_id")"
75
+ mkdir -p "$(dirname "$file")"
76
+ node -e '
77
+ const fs = require("fs");
78
+ const [file, agentId, udid, name, created] = process.argv.slice(1);
79
+ fs.writeFileSync(file, JSON.stringify({
80
+ agentId: Number(agentId),
81
+ udid,
82
+ name,
83
+ createdByHar: created === "true",
84
+ claimedAt: new Date().toISOString(),
85
+ }, null, 2) + "\n");
86
+ ' "$file" "$agent_id" "$udid" "$name" "$created"
87
+ }
88
+
89
+ # Agent id of another live slot holding this udid, if any. Only a pinned
90
+ # HARNESS_SIMULATOR_UDID can produce that overlap.
91
+ har_sim_claim_owner_of() {
92
+ local udid="$1" self="$2" dir file id
93
+ dir="$(har_sim_claims_dir)"
94
+ [ -d "$dir" ] || return 0
95
+ for file in "$dir"/agent-*.json; do
96
+ [ -f "$file" ] || continue
97
+ id="$(basename "$file")"
98
+ id="${id#agent-}"
99
+ id="${id%.json}"
100
+ [ "$id" = "$self" ] && continue
101
+ [ -f "$(slot_registry_file "$id")" ] || continue
102
+ if [ "$(read_slot_field "$file" udid || true)" = "$udid" ]; then
103
+ echo "$id"
104
+ return 0
105
+ fi
106
+ done
107
+ }
108
+
109
+ # ── Device discovery ──────────────────────────────────────────────────────────
110
+
111
+ # Unavailable devices are skipped: a device whose runtime was uninstalled must
112
+ # not pass validation only to fail at boot.
113
+ har_sim_device_field() {
114
+ xcrun simctl list devices --json 2>/dev/null | node -e '
115
+ const data = JSON.parse(require("fs").readFileSync(0, "utf8"));
116
+ const [want, field] = process.argv.slice(1);
117
+ const device = Object.values(data.devices || {})
118
+ .flat()
119
+ .filter((entry) => entry && entry.isAvailable !== false)
120
+ .find((entry) => entry.udid === want);
121
+ if (device && device[field]) process.stdout.write(String(device[field]));
122
+ ' "$1" "$2" 2>/dev/null || true
123
+ }
124
+
125
+ har_sim_device_state() { har_sim_device_field "$1" state; }
126
+ har_sim_device_name() { har_sim_device_field "$1" name; }
127
+
128
+ # UDID of an existing device with this exact name, newest runtime first.
129
+ # A substring match would resolve "iPhone 16" to "iPhone 16 Pro Max".
130
+ har_sim_device_by_name() {
131
+ local want="${1:-}"
132
+ [ -n "$want" ] || return 0
133
+ xcrun simctl list devices available --json 2>/dev/null | node -e '
134
+ const data = JSON.parse(require("fs").readFileSync(0, "utf8"));
135
+ const want = process.argv[1];
136
+ const rows = [];
137
+ for (const [runtime, devices] of Object.entries(data.devices || {})) {
138
+ if (!runtime.includes("SimRuntime.iOS-")) continue;
139
+ const parts = runtime.match(/iOS-(\d+)(?:-(\d+))?/);
140
+ const version = parts ? Number(parts[1]) * 1000 + Number(parts[2] || 0) : 0;
141
+ for (const device of devices || []) {
142
+ if (!device.udid || device.isAvailable === false) continue;
143
+ if (device.name !== want) continue;
144
+ rows.push({ udid: device.udid, version });
145
+ }
146
+ }
147
+ rows.sort((a, b) => b.version - a.version);
148
+ if (rows[0]) process.stdout.write(rows[0].udid);
149
+ ' "$want" 2>/dev/null || true
150
+ }
151
+
152
+ # The shared-mode device, as "<udid>\t<name>".
153
+ har_sim_resolve_configured() {
154
+ local udid
155
+ udid="$(har_sim_device_by_name "${HARNESS_SIMULATOR_NAME:-}")"
156
+ [ -n "$udid" ] || return 0
157
+ printf '%s\t%s' "$udid" "${HARNESS_SIMULATOR_NAME}"
158
+ }
159
+
160
+ # `simctl list` is a subprocess and a JSON parse; neither answer changes during
161
+ # a launch, so each is fetched at most once per process.
162
+ har_sim_devicetypes_json() {
163
+ if [ -z "${_HAR_SIM_TYPES_JSON:-}" ]; then
164
+ _HAR_SIM_TYPES_JSON="$(xcrun simctl list devicetypes --json 2>/dev/null || echo '{}')"
165
+ fi
166
+ printf '%s' "$_HAR_SIM_TYPES_JSON"
167
+ }
168
+
169
+ har_sim_runtimes_json() {
170
+ if [ -z "${_HAR_SIM_RUNTIMES_JSON:-}" ]; then
171
+ _HAR_SIM_RUNTIMES_JSON="$(xcrun simctl list runtimes --json 2>/dev/null || echo '{}')"
172
+ fi
173
+ printf '%s' "$_HAR_SIM_RUNTIMES_JSON"
174
+ }
175
+
176
+ # Model and runtime to create this slot's device from. Failures carry the models
177
+ # this machine can create, so the caller never has to re-derive that list.
178
+ # Prints one of:
179
+ # OK <deviceTypeId> <deviceTypeName> <runtimeId> <runtimeVersion>
180
+ # NO_RUNTIME no iOS runtime installed
181
+ # NO_MODEL <models> HARNESS_SIMULATOR_NAME is not a device model
182
+ # NO_RUNTIME_FOR_MODEL <models> model exists, no installed runtime supports it
183
+ har_sim_plan_creation() {
184
+ local types runtimes
185
+ types="$(har_sim_devicetypes_json)"
186
+ runtimes="$(har_sim_runtimes_json)"
187
+ node -e '
188
+ const [typesRaw, runtimesRaw, wantName, family] = process.argv.slice(1);
189
+ const types = JSON.parse(typesRaw).devicetypes || [];
190
+ const version = (value) => {
191
+ const [major = 0, minor = 0] = String(value || "0").split(".").map(Number);
192
+ return major * 1000 + minor;
193
+ };
194
+ const runtimes = (JSON.parse(runtimesRaw).runtimes || [])
195
+ .filter((runtime) => runtime.isAvailable !== false)
196
+ .filter((runtime) => String(runtime.identifier || "").includes("SimRuntime.iOS-"))
197
+ .sort((a, b) => version(b.version) - version(a.version));
198
+
199
+ const isPad = (entry) => /ipad/i.test(entry.productFamily || entry.name || "");
200
+ const inFamily = (entry) => (family === "iPad" ? isPad(entry) : !isPad(entry));
201
+
202
+ // Models the newest runtime could create, named so an error can list them.
203
+ // Truncation is stated rather than silent — a model cut off the list still exists.
204
+ function availableModels() {
205
+ const runtime = runtimes[0];
206
+ const supported =
207
+ runtime && Array.isArray(runtime.supportedDeviceTypes) && runtime.supportedDeviceTypes.length
208
+ ? runtime.supportedDeviceTypes
209
+ : types;
210
+ const names = supported.filter(inFamily).map((entry) => entry.name);
211
+ const shown = names.slice(0, 8).join(", ");
212
+ return names.length > 8 ? `${shown}, … (${names.length - 8} more)` : shown;
213
+ }
214
+
215
+ function plan() {
216
+ if (!runtimes.length) return "NO_RUNTIME";
217
+ if (wantName && !types.some((type) => type.name === wantName)) {
218
+ return `NO_MODEL\t${availableModels()}`;
219
+ }
220
+
221
+ for (const runtime of runtimes) {
222
+ const supported =
223
+ Array.isArray(runtime.supportedDeviceTypes) && runtime.supportedDeviceTypes.length
224
+ ? runtime.supportedDeviceTypes
225
+ : types;
226
+ // Apple lists supported device types newest first within each family.
227
+ const pool = supported.filter(inFamily);
228
+ const chosen = wantName ? pool.find((type) => type.name === wantName) : pool[0];
229
+ if (chosen) {
230
+ return `OK\t${chosen.identifier}\t${chosen.name}\t${runtime.identifier}\t${runtime.version}`;
231
+ }
232
+ }
233
+ return wantName ? `NO_RUNTIME_FOR_MODEL\t${availableModels()}` : "NO_RUNTIME";
234
+ }
235
+
236
+ process.stdout.write(plan());
237
+ ' "$types" "$runtimes" "${1:-}" "${2:-iPhone}" 2>/dev/null || true
238
+ }
239
+
240
+ # Every device whose name is <prefix> or starts with "<prefix>-", so a slot's
241
+ # leftovers are found whatever model they were created from.
242
+ har_sim_devices_with_prefix() {
243
+ xcrun simctl list devices --json 2>/dev/null | node -e '
244
+ const data = JSON.parse(require("fs").readFileSync(0, "utf8"));
245
+ const prefix = process.argv[1];
246
+ const matches = Object.values(data.devices || {})
247
+ .flat()
248
+ .filter((device) => device && device.udid)
249
+ .filter((device) => device.name === prefix || String(device.name || "").startsWith(`${prefix}-`))
250
+ .map((device) => device.udid);
251
+ process.stdout.write(matches.join("\n"));
252
+ ' "$1" 2>/dev/null || true
253
+ }
254
+
255
+ har_sim_delete_devices_with_prefix() {
256
+ local udid
257
+ while read -r udid; do
258
+ [ -n "$udid" ] || continue
259
+ xcrun simctl delete "$udid" >/dev/null 2>&1 || true
260
+ done <<< "$(har_sim_devices_with_prefix "$1")"
261
+ }
262
+
263
+ # ── Boot ──────────────────────────────────────────────────────────────────────
264
+
265
+ har_sim_boot() {
266
+ local udid="$1" i
267
+ xcrun simctl boot "$udid" >/dev/null 2>&1 || true
268
+ if xcrun simctl bootstatus "$udid" -b >/dev/null 2>&1; then
269
+ return 0
270
+ fi
271
+ # Fallback for a toolchain without bootstatus: the plain listing is one
272
+ # subprocess, where the JSON form costs xcrun plus node on every poll.
273
+ for i in $(seq 1 30); do
274
+ if xcrun simctl list devices 2>/dev/null | grep -q "${udid}.*Booted"; then
275
+ return 0
276
+ fi
277
+ sleep 2
278
+ done
279
+ return 1
280
+ }
281
+
282
+ # ── Acquire / release ─────────────────────────────────────────────────────────
283
+
284
+ # Prints "<udid>\t<name>\t<createdByHar>".
285
+ _har_sim_select() {
286
+ local agent_id="$1"
287
+ local want="${HARNESS_SIMULATOR_NAME:-}"
288
+ local family owner name plan status models device_type model runtime label udid
289
+
290
+ # An explicit UDID is a deliberate override — never fall back off it.
291
+ if [ -n "${HARNESS_SIMULATOR_UDID:-}" ]; then
292
+ name="$(har_sim_device_name "$HARNESS_SIMULATOR_UDID")"
293
+ if [ -z "$name" ]; then
294
+ har_sim_log "HARNESS_SIMULATOR_UDID=${HARNESS_SIMULATOR_UDID} is not a known device"
295
+ return 1
296
+ fi
297
+ owner="$(har_sim_claim_owner_of "$HARNESS_SIMULATOR_UDID" "$agent_id")"
298
+ if [ -n "$owner" ]; then
299
+ har_sim_log "warning: agent ${owner} already holds ${name} — a pinned HARNESS_SIMULATOR_UDID"
300
+ har_sim_log " gives every slot the same device; unset it to get one per slot."
301
+ fi
302
+ printf '%s\t%s\t%s' "$HARNESS_SIMULATOR_UDID" "$name" "false"
303
+ return 0
304
+ fi
305
+
306
+ family="$(har_sim_preferred_family)"
307
+ plan="$(har_sim_plan_creation "$want" "$family")"
308
+ status="${plan%%$'\t'*}"
309
+ # Failure statuses carry the creatable models after the first tab.
310
+ models=""
311
+ [ "$plan" != "$status" ] && models="${plan#*$'\t'}"
312
+
313
+ if [ "$status" = "OK" ]; then
314
+ IFS=$'\t' read -r _ device_type model runtime _ <<< "$plan"
315
+ label="$(har_sim_device_label "$agent_id" "$model")"
316
+ # A crashed launch can leave the slot's device behind — start from scratch,
317
+ # including a device this slot created from a different model.
318
+ har_sim_delete_devices_with_prefix "$(har_sim_device_label_prefix "$agent_id")"
319
+ har_sim_log "creating ${label} (${model}, ${runtime##*SimRuntime.})"
320
+ udid="$(xcrun simctl create "$label" "$device_type" "$runtime" 2>/dev/null | tr -d '[:space:]')"
321
+ if [ -z "$udid" ]; then
322
+ har_sim_log "xcrun simctl create failed for ${label} (${device_type}, ${runtime})"
323
+ return 1
324
+ fi
325
+ printf '%s\t%s\t%s' "$udid" "$label" "true"
326
+ return 0
327
+ fi
328
+
329
+ # Not a model: fall back to an existing device carrying that name, which is
330
+ # how a hand-renamed simulator is targeted.
331
+ if [ "$status" = "NO_MODEL" ]; then
332
+ udid="$(har_sim_device_by_name "$want")"
333
+ if [ -n "$udid" ]; then
334
+ har_sim_log "'${want}' is not a device model — using the existing device with that name"
335
+ printf '%s\t%s\t%s' "$udid" "$want" "false"
336
+ return 0
337
+ fi
338
+ har_sim_log "HARNESS_SIMULATOR_NAME='${want}' is neither a device model nor an existing device"
339
+ har_sim_log " available ${family} models: ${models}"
340
+ return 1
341
+ fi
342
+
343
+ if [ "$status" = "NO_RUNTIME_FOR_MODEL" ]; then
344
+ har_sim_log "no installed iOS runtime supports '${want}'"
345
+ har_sim_log " install a newer runtime in Xcode → Settings → Components, or pick another model"
346
+ har_sim_log " available ${family} models: ${models}"
347
+ return 1
348
+ fi
349
+
350
+ har_sim_log "no iOS simulator runtime is installed"
351
+ har_sim_log " install one in Xcode → Settings → Components"
352
+ return 1
353
+ }
354
+
355
+ # Reserve a simulator for a slot, boot it, and record the claim.
356
+ # Prints "<udid>\t<name>" on success.
357
+ har_sim_acquire() {
358
+ local agent_id="$1" udid name created
359
+ local result="" rc=0
360
+ # `|| rc=$?` keeps a failed selection from tripping `set -e` in the caller.
361
+ result="$(_har_sim_select "$agent_id")" || rc=$?
362
+ [ "$rc" -eq 0 ] || return "$rc"
363
+
364
+ udid="$(printf '%s' "$result" | cut -f1)"
365
+ name="$(printf '%s' "$result" | cut -f2)"
366
+ created="$(printf '%s' "$result" | cut -f3)"
367
+ if [ -z "$udid" ]; then
368
+ har_sim_log "no simulator could be selected"
369
+ return 1
370
+ fi
371
+ har_sim_write_claim "$agent_id" "$udid" "$name" "$created"
372
+
373
+ if ! har_sim_boot "$udid"; then
374
+ har_sim_log "device ${name} (${udid}) did not reach Booted state"
375
+ return 1
376
+ fi
377
+ printf '%s\t%s' "$udid" "$name"
378
+ }
379
+
380
+ # Drop a slot's claim. Devices HAR created are deleted; a device the developer
381
+ # owns is left alone, and never even shut down.
382
+ har_sim_release() {
383
+ local agent_id="$1" file udid created
384
+ file="$(har_sim_claim_file "$agent_id")"
385
+ if [ -f "$file" ]; then
386
+ udid="$(read_slot_field "$file" udid || true)"
387
+ created="$(read_slot_field "$file" createdByHar || true)"
388
+ rm -f "$file"
389
+ if [ "$created" = "true" ] && [ -n "$udid" ]; then
390
+ xcrun simctl shutdown "$udid" >/dev/null 2>&1 || true
391
+ xcrun simctl delete "$udid" >/dev/null 2>&1 || true
392
+ echo "✓ Deleted simulator created for this slot ($udid)"
393
+ fi
394
+ fi
395
+
396
+ # A launch killed between `simctl create` and the claim leaves a device no
397
+ # claim points at. Only HAR creates devices under this prefix, so sweeping it
398
+ # can never remove one of the developer's.
399
+ har_sim_delete_devices_with_prefix "$(har_sim_device_label_prefix "$agent_id")"
400
+ }
@@ -13,6 +13,8 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
13
13
  source "$SCRIPT_DIR/harness.env"
14
14
  # shellcheck source=/dev/null
15
15
  source "$SCRIPT_DIR/agent-slot.sh"
16
+ # shellcheck source=/dev/null
17
+ source "$SCRIPT_DIR/simulator.sh"
16
18
 
17
19
  AGENT_ID="${1:?Usage: teardown.sh <agent-id> [--delete-branch]}"
18
20
  DELETE_BRANCH=false
@@ -59,6 +61,8 @@ if [ -n "$BRANCH" ]; then
59
61
  fi
60
62
  fi
61
63
 
64
+ har_sim_release "$AGENT_ID"
65
+
62
66
  remove_slot_registry "$AGENT_ID"
63
67
 
64
68
  echo "✓ Agent ${AGENT_ID} torn down"
@@ -118,6 +118,8 @@ process.stdout.write(JSON.stringify(arr));
118
118
  }
119
119
 
120
120
  # Quick (default): compile-only — use XCODEBUILD_BIN from .env.agent.<id> (written by launch)
121
+ # Nothing is installed or run, so CODE_SIGNING_ALLOWED=NO keeps the smoke build
122
+ # from needing a signing identity.
121
123
  run_step "build" '${XCODEBUILD_BIN:-xcodebuild} build \
122
124
  ${XC_FLAGS} \
123
125
  -scheme "${XC_SCHEME}" \
@@ -133,18 +135,20 @@ run_step "build" '${XCODEBUILD_BIN:-xcodebuild} build \
133
135
 
134
136
  if [ -n "$FULL" ]; then
135
137
  # Full: project-specific checks — add/remove/reorder steps for this repo.
138
+ # No CODE_SIGNING_ALLOWED=NO here, unlike the build step: tests install and run
139
+ # the host app on the simulator, and an unsigned app gets no entitlements. Apps
140
+ # using CloudKit, NSUbiquitousKeyValueStore, or push then trap at launch, before
141
+ # the test harness connects — the whole bundle is lost, not one test.
136
142
  run_step "unit-tests" '${XCODEBUILD_BIN:-xcodebuild} test \
137
143
  ${XC_FLAGS} \
138
144
  -scheme "${XC_SCHEME}" \
139
145
  -destination "${XC_DESTINATION}" \
140
146
  -derivedDataPath "${XC_DERIVED}" \
141
- CODE_SIGNING_ALLOWED=NO \
142
147
  | xcbeautify 2>/dev/null || ${XCODEBUILD_BIN:-xcodebuild} test \
143
148
  ${XC_FLAGS} \
144
149
  -scheme "${XC_SCHEME}" \
145
150
  -destination "${XC_DESTINATION}" \
146
- -derivedDataPath "${XC_DERIVED}" \
147
- CODE_SIGNING_ALLOWED=NO' || true
151
+ -derivedDataPath "${XC_DERIVED}"' || true
148
152
 
149
153
  # SwiftLint — optional, skip gracefully when not installed
150
154
  run_step "lint" "command -v \"${HARNESS_SWIFTLINT_CMD:-swiftlint}\" >/dev/null 2>&1 && \
@@ -17,7 +17,9 @@ The runner sets these environment variables for each flow:
17
17
  | `FLOW_ARTIFACT_DIR` | Directory for screenshots and output of this flow |
18
18
  | `WORK_DIR` | Path to the agent's session worktree |
19
19
  | `HARNESS_BUNDLE_ID` | App bundle identifier (from `harness.env`) |
20
- | `HARNESS_SIMULATOR_NAME` | Target simulator name (from `harness.env`) |
20
+ | `HARNESS_SIMULATOR_NAME` | Simulator model configured in `harness.env` |
21
+ | `HARNESS_SIMULATOR_DEVICE_NAME` | Name of the device created for this slot |
22
+ | `HARNESS_SIMULATOR_UDID` | UDID of that device — target it rather than `booted` |
21
23
  | `AGENT_ID` | Active agent slot id |
22
24
 
23
25
  ## Key RocketSim CLI commands
@@ -130,6 +130,8 @@ for FLOW_SCRIPT in "${FLOW_SCRIPTS[@]}"; do
130
130
  FLOW_ARTIFACT_DIR="$FLOW_ARTIFACT_DIR" \
131
131
  HARNESS_BUNDLE_ID="${HARNESS_BUNDLE_ID:-}" \
132
132
  HARNESS_SIMULATOR_NAME="${HARNESS_SIMULATOR_NAME:-}" \
133
+ HARNESS_SIMULATOR_DEVICE_NAME="${HARNESS_SIMULATOR_DEVICE_NAME:-}" \
134
+ HARNESS_SIMULATOR_UDID="${HARNESS_SIMULATOR_UDID:-}" \
133
135
  bash "$FLOW_SCRIPT" 2>&1
134
136
  )
135
137
  FLOW_EXIT=$?
@@ -32,5 +32,7 @@ The runner (`rocketsim-flows.sh`) sets these variables for every flow:
32
32
  |----------|-------------|
33
33
  | `FLOW_ARTIFACT_DIR` | Save screenshots here (`$FLOW_ARTIFACT_DIR/success.png`, `failure.png`) |
34
34
  | `HARNESS_BUNDLE_ID` | App bundle ID from `harness.env` |
35
- | `HARNESS_SIMULATOR_NAME` | Simulator name from `harness.env` |
35
+ | `HARNESS_SIMULATOR_NAME` | Simulator model configured in `harness.env` |
36
+ | `HARNESS_SIMULATOR_DEVICE_NAME` | Name of the device created for this slot |
37
+ | `HARNESS_SIMULATOR_UDID` | UDID of that device — target it instead of `booted`, which may be another slot's |
36
38
  | `WORK_DIR` | Path to the session worktree |
@@ -12,10 +12,10 @@ set -euo pipefail
12
12
  log() { echo "==> [example-smoke] $*" >&2; }
13
13
 
14
14
  # ── Preflight ─────────────────────────────────────────────────────────────────
15
- # The app must be running on the simulator. Install and launch it before running
16
- # this flow. Example:
17
- # xcrun simctl install booted path/to/MyApp.app
18
- # xcrun simctl launch booted "${HARNESS_BUNDLE_ID}"
15
+ # The app must be running on THIS slot's simulator. Install and launch it before
16
+ # running this flow — target the reserved device, not `booted`. Example:
17
+ # xcrun simctl install "${HARNESS_SIMULATOR_UDID:-booted}" path/to/MyApp.app
18
+ # xcrun simctl launch "${HARNESS_SIMULATOR_UDID:-booted}" "${HARNESS_BUNDLE_ID}"
19
19
 
20
20
  log "Checking simulator focus..."
21
21
  FOCUSED=$(rocketsim simulator focused 2>/dev/null || echo "")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osfactory/har",
3
- "version": "0.48.0",
3
+ "version": "0.49.1",
4
4
  "description": "Open harness for reproducible multi-agent coding workflows",
5
5
  "keywords": [
6
6
  "har",