@osfactory/har 0.6.0 → 0.8.0

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.
@@ -3,7 +3,7 @@
3
3
  # Every launch starts a FRESH session: any previous session for the slot is torn
4
4
  # down (its branch is kept) and a new suffixed worktree is created from HEAD.
5
5
  #
6
- # Usage: ./.har/launch.sh <agent-id> [--no-worktree] [--claude] [--replace] [--force]
6
+ # Usage: ./.har/launch.sh <agent-id> [--no-worktree] [--claude] [--replace] [--force] [--resume]
7
7
  set -euo pipefail
8
8
 
9
9
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -19,6 +19,7 @@ USE_WORKTREE="${HARNESS_USE_WORKTREE:-true}"
19
19
  USE_CLAUDE=false
20
20
  FORCE=false
21
21
  REPLACE=false
22
+ RESUME=false
22
23
  PURPOSE="${HAR_SESSION_PURPOSE:-}"
23
24
 
24
25
  for arg in "$@"; do
@@ -28,12 +29,13 @@ for arg in "$@"; do
28
29
  --claude) USE_CLAUDE=true ;;
29
30
  --replace) REPLACE=true ;;
30
31
  --force) FORCE=true ;;
32
+ --resume) RESUME=true ;;
31
33
  --purpose=*) PURPOSE="${arg#--purpose=}" ;;
32
34
  esac
33
35
  done
34
36
 
35
37
  if [[ -z "$AGENT_ID" ]]; then
36
- echo "Usage: $0 <agent-id> [--no-worktree] [--claude] [--replace] [--force] [--purpose=label]" >&2
38
+ echo "Usage: $0 <agent-id> [--no-worktree] [--claude] [--replace] [--force] [--resume] [--purpose=label]" >&2
37
39
  echo " agent-id must be between ${HARNESS_AGENT_SLOT_MIN} and ${HARNESS_AGENT_SLOT_MAX}" >&2
38
40
  exit 1
39
41
  fi
@@ -42,24 +44,65 @@ validate_agent_id "$AGENT_ID"
42
44
 
43
45
  log() { echo "==> [agent-$AGENT_ID] $*" >&2; }
44
46
 
45
- FE_PORT=$(( HARNESS_FE_BASE_PORT + AGENT_ID * 10 ))
46
- API_PORT=$(( HARNESS_API_BASE_PORT + AGENT_ID * 10 ))
47
- DEBUG_PORT=$(( 9200 + AGENT_ID * 10 ))
48
- DB_PORT="${AGENT_DB_PORT:-15432}"
49
- MINIO_PORT="${AGENT_MINIO_PORT:-19000}"
50
- BROWSER_PORT="${AGENT_BROWSER_PORT:-13001}"
47
+ WORK_DIR="$REPO_ROOT"
48
+ WORKTREE_DIR=""
49
+ BRANCH=""
50
+ SUFFIX=""
51
+ BASE_BRANCH=""
52
+ BASE_COMMIT=""
53
+ ENV_FILE=""
54
+ REGISTRY_WRITTEN=false
55
+
56
+ if [ "$RESUME" = true ]; then
57
+ har_launch_preflight "$AGENT_ID" "$FORCE" "$REPLACE" true || exit $?
58
+ eval "$(har_resume_session_assignments "$AGENT_ID")"
59
+ REGISTRY_WRITTEN=true
60
+ mark_slot_failed() {
61
+ local exit_code="$?"
62
+ if [ "$exit_code" != "0" ] && [ "$REGISTRY_WRITTEN" = true ]; then
63
+ log "Resume failed. Recording failed slot state..."
64
+ set +e
65
+ SLOT_AGENT_ID="$AGENT_ID" \
66
+ SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
67
+ SLOT_WORK_DIR="$WORK_DIR" \
68
+ SLOT_SUFFIX="${SUFFIX:-}" \
69
+ SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
70
+ SLOT_BRANCH="${BRANCH:-}" \
71
+ SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
72
+ SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
73
+ SLOT_PURPOSE="${PURPOSE}" \
74
+ SLOT_PORTS_JSON="{\"frontend\":${FE_PORT},\"api\":${API_PORT},\"debug\":${DEBUG_PORT},\"db\":${DB_PORT}}" \
75
+ SLOT_PREVIEW_URLS_JSON="{\"frontend\":\"http://localhost:${FE_PORT}\",\"api\":\"http://localhost:${API_PORT}\"}" \
76
+ SLOT_STATUS="failed" \
77
+ SLOT_LAST_ERROR="launch.sh --resume exited with code ${exit_code}" \
78
+ write_slot_registry
79
+ log " Work dir: ${WORK_DIR}"
80
+ log " Env file: ${ENV_FILE}"
81
+ log " Recovery: ./.har/launch.sh ${AGENT_ID} --resume"
82
+ fi
83
+ }
84
+ trap mark_slot_failed EXIT
85
+ else
86
+ # Preflight before worktree/install — ports, foreign PM2, Docker, occupied slot.
87
+ har_launch_preflight "$AGENT_ID" "$FORCE" "$REPLACE" || exit $?
51
88
 
52
- log "Ports: frontend=$FE_PORT api=$API_PORT debug=$DEBUG_PORT"
89
+ # Replace any previous session for this slot — requires explicit confirmation.
90
+ if slot_is_occupied "$AGENT_ID"; then
91
+ require_slot_replace_confirm "$AGENT_ID" "$FORCE" "$REPLACE"
92
+ log "Replacing previous session for slot ${AGENT_ID}..."
93
+ "$SCRIPT_DIR/teardown.sh" "$AGENT_ID" >&2
94
+ fi
95
+ fi
53
96
 
54
- # Replace any previous session for this slot — requires explicit confirmation.
55
- if slot_is_occupied "$AGENT_ID"; then
56
- require_slot_replace_confirm "$AGENT_ID" "$FORCE" "$REPLACE"
57
- log "Replacing previous session for slot ${AGENT_ID}..."
58
- "$SCRIPT_DIR/teardown.sh" "$AGENT_ID" >&2
97
+ if har_harness_uses_pm2; then
98
+ log "Ports: frontend=$FE_PORT api=$API_PORT debug=$DEBUG_PORT"
59
99
  fi
60
100
 
61
- # Ensure shared infra is running
101
+ # Ensure shared infra is running (persists host ports in .har/state/infra.env).
62
102
  "$SCRIPT_DIR/setup-infra.sh"
103
+ DB_PORT="${AGENT_DB_PORT:-${HARNESS_DB_PORT_DEFAULT:-15432}}"
104
+ MINIO_PORT="${AGENT_MINIO_PORT:-${HARNESS_MINIO_PORT_DEFAULT:-19000}}"
105
+ BROWSER_PORT="${AGENT_BROWSER_PORT:-${HARNESS_BROWSER_PORT_DEFAULT:-13001}}"
63
106
 
64
107
  # Clone agent database from template
65
108
  if har_infra_enabled db && [ -n "${HARNESS_TEMPLATE_DB:-}" ]; then
@@ -85,105 +128,113 @@ if har_infra_enabled minio; then
85
128
  fi
86
129
 
87
130
  # Session worktree (default — use --no-worktree to work in repo root).
88
- # Name encodes what the session is based on: <base-branch>-<sha4>-har-agent-<id>-<rand4>.
89
- WORK_DIR="$REPO_ROOT"
90
- WORKTREE_DIR=""
91
- BRANCH=""
92
- SUFFIX=""
93
- BASE_BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "detached")"
94
- BASE_COMMIT="$(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null || true)"
95
- if [ "$USE_WORKTREE" = true ]; then
96
- SHORT_SHA="$(git -C "$REPO_ROOT" rev-parse --short=4 HEAD)"
97
- SUFFIX="$(LC_ALL=C tr -dc 'a-z0-9' </dev/urandom 2>/dev/null | head -c 4 || true)"
98
- [ -n "$SUFFIX" ] || SUFFIX="$(printf '%04d' $(( RANDOM % 10000 )))"
99
- SESSION_NAME="${BASE_BRANCH//\//-}-${SHORT_SHA}-har-agent-${AGENT_ID}-${SUFFIX}"
100
- BRANCH="$SESSION_NAME"
101
- WORKTREE_DIR="$HOME/worktrees/${SESSION_NAME}"
102
- log "Creating session worktree at $WORKTREE_DIR (branch $BRANCH)..."
103
- git -C "$REPO_ROOT" worktree add "$WORKTREE_DIR" -b "$BRANCH"
104
- # Worktrees are always rooted at the git repo if this project lives in a
105
- # subdirectory (monorepo), point WORK_DIR at the project inside the worktree.
106
- REL_PREFIX="$(git -C "$REPO_ROOT" rev-parse --show-prefix 2>/dev/null || true)"
107
- WORK_DIR="${WORKTREE_DIR%/}/${REL_PREFIX}"
108
- WORK_DIR="${WORK_DIR%/}"
109
- else
110
- log "Using repo root (worktree disabled)"
111
- fi
131
+ if [ "$RESUME" != true ]; then
132
+ # Name encodes what the session is based on: <base-branch>-<sha4>-har-agent-<id>-<rand4>.
133
+ WORK_DIR="$REPO_ROOT"
134
+ WORKTREE_DIR=""
135
+ BRANCH=""
136
+ SUFFIX=""
137
+ BASE_BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "detached")"
138
+ BASE_COMMIT="$(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null || true)"
139
+ if [ "$USE_WORKTREE" = true ]; then
140
+ SHORT_SHA="$(git -C "$REPO_ROOT" rev-parse --short=4 HEAD)"
141
+ SUFFIX="$(LC_ALL=C tr -dc 'a-z0-9' </dev/urandom 2>/dev/null | head -c 4 || true)"
142
+ [ -n "$SUFFIX" ] || SUFFIX="$(printf '%04d' $(( RANDOM % 10000 )))"
143
+ SESSION_NAME="${BASE_BRANCH//\//-}-${SHORT_SHA}-har-agent-${AGENT_ID}-${SUFFIX}"
144
+ BRANCH="$SESSION_NAME"
145
+ WORKTREE_DIR="$HOME/worktrees/${SESSION_NAME}"
146
+ log "Creating session worktree at $WORKTREE_DIR (branch $BRANCH)..."
147
+ git -C "$REPO_ROOT" worktree add "$WORKTREE_DIR" -b "$BRANCH"
148
+ REL_PREFIX="$(git -C "$REPO_ROOT" rev-parse --show-prefix 2>/dev/null || true)"
149
+ WORK_DIR="${WORKTREE_DIR%/}/${REL_PREFIX}"
150
+ WORK_DIR="${WORK_DIR%/}"
151
+ else
152
+ log "Using repo root (worktree disabled)"
153
+ REL_PREFIX="$(git -C "$REPO_ROOT" rev-parse --show-prefix 2>/dev/null || true)"
154
+ fi
112
155
 
113
- # Generate .env.agent.N
114
- # Keep harness-generated files out of accidental `git add -A` commits in repos
115
- # whose .gitignore doesn't know about har (applies to all worktrees too).
116
- GIT_EXCLUDE="$(git -C "$REPO_ROOT" rev-parse --git-common-dir 2>/dev/null)/info/exclude"
117
- if [ -n "$GIT_EXCLUDE" ] && [ -d "$(dirname "$GIT_EXCLUDE")" ]; then
118
- for pattern in '.env.agent.*' 'ecosystem.agent.*.config.cjs' '.har/venv'; do
119
- grep -qxF "$pattern" "$GIT_EXCLUDE" 2>/dev/null || echo "$pattern" >> "$GIT_EXCLUDE"
120
- done
121
- fi
156
+ GIT_EXCLUDE="$(git -C "$REPO_ROOT" rev-parse --git-common-dir 2>/dev/null)/info/exclude"
157
+ if [ -n "$GIT_EXCLUDE" ] && [ -d "$(dirname "$GIT_EXCLUDE")" ]; then
158
+ for pattern in '.env.agent.*' 'ecosystem.agent.*.config.cjs' '.har/venv'; do
159
+ grep -qxF "$pattern" "$GIT_EXCLUDE" 2>/dev/null || echo "$pattern" >> "$GIT_EXCLUDE"
160
+ done
161
+ fi
122
162
 
123
- ENV_FILE="$WORK_DIR/.env.agent.${AGENT_ID}"
124
- log "Generating $ENV_FILE..."
125
- AGENT_ID="$AGENT_ID" \
126
- API_PORT="$API_PORT" \
127
- FE_PORT="$FE_PORT" \
128
- DEBUG_PORT="$DEBUG_PORT" \
129
- DB_PORT="$DB_PORT" \
130
- MINIO_PORT="$MINIO_PORT" \
131
- BROWSER_PORT="$BROWSER_PORT" \
132
- REPO_ROOT="$WORK_DIR" \
133
- envsubst '${AGENT_ID} ${API_PORT} ${FE_PORT} ${DEBUG_PORT} ${DB_PORT} ${MINIO_PORT} ${BROWSER_PORT} ${REPO_ROOT}' \
134
- < "$SCRIPT_DIR/env.template" > "$ENV_FILE"
163
+ ENV_FILE="$WORK_DIR/.env.agent.${AGENT_ID}"
164
+ log "Generating $ENV_FILE..."
165
+ har_regenerate_agent_env_file "$AGENT_ID" "$WORK_DIR" "$ENV_FILE"
135
166
 
136
- REGISTRY_WRITTEN=false
137
- mark_slot_failed() {
138
- local exit_code="$?"
139
- if [ "$exit_code" != "0" ] && [ "$REGISTRY_WRITTEN" = true ]; then
140
- log "Launch failed after creating the session. Recording failed slot state..."
141
- set +e
142
- SLOT_AGENT_ID="$AGENT_ID" \
143
- SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
144
- SLOT_WORK_DIR="$WORK_DIR" \
145
- SLOT_SUFFIX="${SUFFIX:-}" \
146
- SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
147
- SLOT_BRANCH="${BRANCH:-}" \
148
- SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
149
- SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
150
- SLOT_PURPOSE="${PURPOSE}" \
151
- SLOT_PORTS_JSON="{\"frontend\":${FE_PORT},\"api\":${API_PORT},\"debug\":${DEBUG_PORT}}" \
152
- SLOT_PREVIEW_URLS_JSON="{\"frontend\":\"http://localhost:${FE_PORT}\",\"api\":\"http://localhost:${API_PORT}\"}" \
153
- SLOT_STATUS="failed" \
154
- SLOT_LAST_ERROR="launch.sh exited with code ${exit_code}" \
155
- write_slot_registry
156
- log " Work dir: ${WORK_DIR}"
157
- log " Env file: ${ENV_FILE}"
158
- log " Recovery: fix the failure, then relaunch with --replace when ready."
159
- fi
160
- }
161
- trap mark_slot_failed EXIT
167
+ REGISTRY_WRITTEN=false
168
+ mark_slot_failed() {
169
+ local exit_code="$?"
170
+ if [ "$exit_code" != "0" ] && [ "$REGISTRY_WRITTEN" = true ]; then
171
+ log "Launch failed after creating the session. Recording failed slot state..."
172
+ set +e
173
+ SLOT_AGENT_ID="$AGENT_ID" \
174
+ SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
175
+ SLOT_WORK_DIR="$WORK_DIR" \
176
+ SLOT_SUFFIX="${SUFFIX:-}" \
177
+ SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
178
+ SLOT_BRANCH="${BRANCH:-}" \
179
+ SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
180
+ SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
181
+ SLOT_PURPOSE="${PURPOSE}" \
182
+ SLOT_PORTS_JSON="{\"frontend\":${FE_PORT},\"api\":${API_PORT},\"debug\":${DEBUG_PORT},\"db\":${DB_PORT}}" \
183
+ SLOT_PREVIEW_URLS_JSON="{\"frontend\":\"http://localhost:${FE_PORT}\",\"api\":\"http://localhost:${API_PORT}\"}" \
184
+ SLOT_STATUS="failed" \
185
+ SLOT_LAST_ERROR="launch.sh exited with code ${exit_code}" \
186
+ write_slot_registry
187
+ log " Work dir: ${WORK_DIR}"
188
+ log " Env file: ${ENV_FILE}"
189
+ log " Recovery: ./.har/launch.sh ${AGENT_ID} --resume"
190
+ fi
191
+ }
192
+ trap mark_slot_failed EXIT
162
193
 
163
- # Record the session before slow or fragile setup so verify/status/teardown can
164
- # recover partial launches that already created a worktree and env file.
165
- SLOT_AGENT_ID="$AGENT_ID" \
166
- SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
167
- SLOT_WORK_DIR="$WORK_DIR" \
168
- SLOT_SUFFIX="${SUFFIX:-}" \
169
- SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
170
- SLOT_BRANCH="${BRANCH:-}" \
171
- SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
172
- SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
173
- SLOT_PURPOSE="${PURPOSE}" \
174
- SLOT_PORTS_JSON="{\"frontend\":${FE_PORT},\"api\":${API_PORT},\"debug\":${DEBUG_PORT}}" \
175
- SLOT_PREVIEW_URLS_JSON="{\"frontend\":\"http://localhost:${FE_PORT}\",\"api\":\"http://localhost:${API_PORT}\"}" \
176
- SLOT_STATUS="starting" \
177
- write_slot_registry
178
- REGISTRY_WRITTEN=true
194
+ SLOT_AGENT_ID="$AGENT_ID" \
195
+ SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
196
+ SLOT_WORK_DIR="$WORK_DIR" \
197
+ SLOT_SUFFIX="${SUFFIX:-}" \
198
+ SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
199
+ SLOT_BRANCH="${BRANCH:-}" \
200
+ SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
201
+ SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
202
+ SLOT_PURPOSE="${PURPOSE}" \
203
+ SLOT_PORTS_JSON="{\"frontend\":${FE_PORT},\"api\":${API_PORT},\"debug\":${DEBUG_PORT},\"db\":${DB_PORT}}" \
204
+ SLOT_PREVIEW_URLS_JSON="{\"frontend\":\"http://localhost:${FE_PORT}\",\"api\":\"http://localhost:${API_PORT}\"}" \
205
+ SLOT_STATUS="starting" \
206
+ write_slot_registry
207
+ REGISTRY_WRITTEN=true
208
+ else
209
+ REL_PREFIX="$(git -C "$REPO_ROOT" rev-parse --show-prefix 2>/dev/null || true)"
210
+ log "Resuming session at ${WORK_DIR}"
211
+ har_regenerate_agent_env_file "$AGENT_ID" "$WORK_DIR" "$ENV_FILE"
212
+ SLOT_AGENT_ID="$AGENT_ID" \
213
+ SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
214
+ SLOT_WORK_DIR="$WORK_DIR" \
215
+ SLOT_SUFFIX="${SUFFIX:-}" \
216
+ SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
217
+ SLOT_BRANCH="${BRANCH:-}" \
218
+ SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
219
+ SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
220
+ SLOT_PURPOSE="${PURPOSE}" \
221
+ SLOT_PORTS_JSON="{\"frontend\":${FE_PORT},\"api\":${API_PORT},\"debug\":${DEBUG_PORT},\"db\":${DB_PORT}}" \
222
+ SLOT_PREVIEW_URLS_JSON="{\"frontend\":\"http://localhost:${FE_PORT}\",\"api\":\"http://localhost:${API_PORT}\"}" \
223
+ SLOT_STATUS="starting" \
224
+ write_slot_registry
225
+ fi
179
226
 
180
- log "Provisioning toolchain (see harness.env: HARNESS_ECOSYSTEM, HARNESS_INSTALL_CMD)..."
181
- HAR_WORK_DIR="$WORK_DIR" \
182
- HAR_ENV_FILE="$ENV_FILE" \
183
- HAR_WORKTREE_DIR="${WORKTREE_DIR:-}" \
184
- HAR_REL_PREFIX="${REL_PREFIX:-}" \
185
- HAR_AGENT_ID="$AGENT_ID" \
186
- "$SCRIPT_DIR/provision-toolchain.sh"
227
+ if [ "$RESUME" = true ] && har_toolchain_ready "$WORK_DIR"; then
228
+ log "Toolchain already provisioned — skipping install."
229
+ else
230
+ log "Provisioning toolchain (see harness.env: HARNESS_ECOSYSTEM, HARNESS_INSTALL_CMD)..."
231
+ HAR_WORK_DIR="$WORK_DIR" \
232
+ HAR_ENV_FILE="$ENV_FILE" \
233
+ HAR_WORKTREE_DIR="${WORKTREE_DIR:-}" \
234
+ HAR_REL_PREFIX="${REL_PREFIX:-}" \
235
+ HAR_AGENT_ID="$AGENT_ID" \
236
+ "$SCRIPT_DIR/provision-toolchain.sh"
237
+ fi
187
238
 
188
239
  if [ -n "${HARNESS_DB_MINIMAL_BOOTSTRAP_CMD:-}" ]; then
189
240
  log "Running minimal data bootstrap..."
@@ -194,13 +245,15 @@ fi
194
245
  ECOSYSTEM_FILE="$WORK_DIR/ecosystem.agent.${AGENT_ID}.config.cjs"
195
246
  log "Generating $ECOSYSTEM_FILE..."
196
247
  AGENT_ID="$AGENT_ID" \
248
+ HARNESS_PROJECT_NAME="$HARNESS_PROJECT_NAME" \
197
249
  FE_PORT="$FE_PORT" \
198
250
  DEBUG_PORT="$DEBUG_PORT" \
199
- envsubst '${AGENT_ID} ${FE_PORT} ${DEBUG_PORT}' \
251
+ envsubst '${AGENT_ID} ${HARNESS_PROJECT_NAME} ${FE_PORT} ${DEBUG_PORT}' \
200
252
  < "$SCRIPT_DIR/ecosystem.agent.template.cjs" > "$ECOSYSTEM_FILE"
201
253
 
202
- # Stop existing processes for this agent
203
- npx --yes pm2 delete "/^agent-${AGENT_ID}-/" 2>/dev/null || true
254
+ PM2_REGEX="$(har_pm2_delete_regex "$AGENT_ID")"
255
+ # Stop existing processes for this agent (project-scoped never touch other harnesses).
256
+ npx --yes pm2 delete "$PM2_REGEX" 2>/dev/null || true
204
257
 
205
258
  # Start PM2 processes
206
259
  log "Starting PM2 processes..."
@@ -240,14 +293,14 @@ SLOT_BRANCH="${BRANCH:-}" \
240
293
  SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
241
294
  SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
242
295
  SLOT_PURPOSE="${PURPOSE}" \
243
- SLOT_PORTS_JSON="{\"frontend\":${FE_PORT},\"api\":${API_PORT},\"debug\":${DEBUG_PORT}}" \
296
+ SLOT_PORTS_JSON="{\"frontend\":${FE_PORT},\"api\":${API_PORT},\"debug\":${DEBUG_PORT},\"db\":${DB_PORT}}" \
244
297
  SLOT_PREVIEW_URLS_JSON="{\"frontend\":\"http://localhost:${FE_PORT}\",\"api\":\"http://localhost:${API_PORT}\"}" \
245
298
  SLOT_STATUS="active" \
246
299
  write_slot_registry
247
300
 
248
301
  # Launch Claude Code in tmux (optional)
249
302
  if [ "$USE_CLAUDE" = true ]; then
250
- TMUX_SESSION="agent-${AGENT_ID}"
303
+ TMUX_SESSION="$(har_tmux_session "$AGENT_ID")"
251
304
  if tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then
252
305
  tmux kill-session -t "$TMUX_SESSION"
253
306
  fi
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env bash
2
+ # Launch readiness gate — run before launch.sh or standalone.
3
+ # Usage: ./.har/preflight.sh <agent-id> [--replace] [--force]
4
+ # JSON: har env preflight <id> --json
5
+ set -euo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
9
+
10
+ # shellcheck source=/dev/null
11
+ source "$SCRIPT_DIR/harness.env"
12
+ # shellcheck source=/dev/null
13
+ source "$SCRIPT_DIR/agent-slot.sh"
14
+
15
+ AGENT_ID="${1:-}"
16
+ FORCE=false
17
+ REPLACE=false
18
+
19
+ for arg in "$@"; do
20
+ case "$arg" in
21
+ --replace) REPLACE=true ;;
22
+ --force) FORCE=true ;;
23
+ esac
24
+ done
25
+
26
+ if [[ -z "$AGENT_ID" ]]; then
27
+ echo "Usage: $0 <agent-id> [--replace] [--force]" >&2
28
+ exit 1
29
+ fi
30
+
31
+ validate_agent_id "$AGENT_ID"
32
+
33
+ if har_launch_preflight "$AGENT_ID" "$FORCE" "$REPLACE"; then
34
+ echo "Slot ${AGENT_ID}: ready to launch."
35
+ if har_harness_uses_pm2; then
36
+ echo " Ports: frontend=${FE_PORT} api=${API_PORT} debug=${DEBUG_PORT}"
37
+ fi
38
+ exit 0
39
+ fi
40
+
41
+ exit $?
@@ -11,26 +11,101 @@ set -euo pipefail
11
11
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
12
  REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
13
13
  COMPOSE_FILE="$SCRIPT_DIR/docker-compose.agent.yml"
14
+ INFRA_STATE="$SCRIPT_DIR/state/infra.env"
14
15
 
15
16
  # shellcheck source=/dev/null
16
17
  source "$SCRIPT_DIR/harness.env"
18
+ # shellcheck source=/dev/null
19
+ source "$SCRIPT_DIR/agent-slot.sh"
17
20
 
18
21
  COMPOSE_PROJECT="har-${HARNESS_PROJECT_NAME}"
19
- DB_PORT="${AGENT_DB_PORT:-15432}"
20
22
  PSQL="har_pg psql -d postgres"
21
23
 
22
24
  log() { echo "==> $*" >&2; }
23
25
 
26
+ har_compose_service_running() {
27
+ local service="$1"
28
+ docker ps --filter "name=${COMPOSE_PROJECT}-${service}-1" --format '{{.Names}}' 2>/dev/null \
29
+ | grep -q "^${COMPOSE_PROJECT}-${service}-1\$"
30
+ }
31
+
32
+ har_resolve_infra_port() {
33
+ local var_name="$1"
34
+ local default_port="$2"
35
+ local scan_start="$3"
36
+ local scan_end="$4"
37
+ local service="${5:-}"
38
+ local current="${!var_name:-}"
39
+
40
+ if [ -n "$current" ] && { ! port_in_use "$current" || { [ -n "$service" ] && har_compose_service_running "$service"; }; }; then
41
+ echo "$current"
42
+ return 0
43
+ fi
44
+ har_allocate_port "$default_port" "$scan_start" "$scan_end"
45
+ }
46
+
47
+ mkdir -p "$(dirname "$INFRA_STATE")"
48
+ if [ -f "$INFRA_STATE" ]; then
49
+ # shellcheck source=/dev/null
50
+ source "$INFRA_STATE"
51
+ fi
52
+
53
+ DB_PORT="$(har_resolve_infra_port AGENT_DB_PORT \
54
+ "${HARNESS_DB_PORT_DEFAULT:-15432}" \
55
+ "${HARNESS_DB_PORT_SCAN_START:-15432}" \
56
+ "${HARNESS_DB_PORT_SCAN_END:-15499}" \
57
+ db)"
58
+ MINIO_PORT="$(har_resolve_infra_port AGENT_MINIO_PORT \
59
+ "${HARNESS_MINIO_PORT_DEFAULT:-19000}" \
60
+ "${HARNESS_MINIO_PORT_SCAN_START:-19000}" \
61
+ "${HARNESS_MINIO_PORT_SCAN_END:-19099}" \
62
+ minio)"
63
+ MINIO_CONSOLE_PORT="$(har_resolve_infra_port AGENT_MINIO_CONSOLE_PORT \
64
+ "${HARNESS_MINIO_CONSOLE_PORT_DEFAULT:-19001}" \
65
+ "${HARNESS_MINIO_CONSOLE_PORT_SCAN_START:-19001}" \
66
+ "${HARNESS_MINIO_CONSOLE_PORT_SCAN_END:-19099}")"
67
+ BROWSER_PORT="$(har_resolve_infra_port AGENT_BROWSER_PORT \
68
+ "${HARNESS_BROWSER_PORT_DEFAULT:-13001}" \
69
+ "${HARNESS_BROWSER_PORT_SCAN_START:-13001}" \
70
+ "${HARNESS_BROWSER_PORT_SCAN_END:-13099}" \
71
+ headless-browser)"
72
+ MAILPIT_WEB_PORT="$(har_resolve_infra_port AGENT_MAILPIT_WEB_PORT \
73
+ "${HARNESS_MAILPIT_WEB_PORT_DEFAULT:-18025}" \
74
+ "${HARNESS_MAILPIT_WEB_PORT_SCAN_START:-18025}" \
75
+ "${HARNESS_MAILPIT_WEB_PORT_SCAN_END:-18099}" \
76
+ mailpit)"
77
+ MAILPIT_SMTP_PORT="$(har_resolve_infra_port AGENT_MAILPIT_SMTP_PORT \
78
+ "${HARNESS_MAILPIT_SMTP_PORT_DEFAULT:-11025}" \
79
+ "${HARNESS_MAILPIT_SMTP_PORT_SCAN_START:-11025}" \
80
+ "${HARNESS_MAILPIT_SMTP_PORT_SCAN_END:-11099}")"
81
+
82
+ export AGENT_DB_PORT="$DB_PORT"
83
+ export AGENT_MINIO_PORT="$MINIO_PORT"
84
+ export AGENT_MINIO_CONSOLE_PORT="$MINIO_CONSOLE_PORT"
85
+ export AGENT_BROWSER_PORT="$BROWSER_PORT"
86
+ export AGENT_MAILPIT_WEB_PORT="$MAILPIT_WEB_PORT"
87
+ export AGENT_MAILPIT_SMTP_PORT="$MAILPIT_SMTP_PORT"
88
+
89
+ cat > "$INFRA_STATE" <<EOF
90
+ # Persisted by setup-infra.sh — host ports for shared docker compose services.
91
+ export AGENT_DB_PORT=${DB_PORT}
92
+ export AGENT_MINIO_PORT=${MINIO_PORT}
93
+ export AGENT_MINIO_CONSOLE_PORT=${MINIO_CONSOLE_PORT}
94
+ export AGENT_BROWSER_PORT=${BROWSER_PORT}
95
+ export AGENT_MAILPIT_WEB_PORT=${MAILPIT_WEB_PORT}
96
+ export AGENT_MAILPIT_SMTP_PORT=${MAILPIT_SMTP_PORT}
97
+ EOF
98
+
24
99
  SERVICES="${HARNESS_INFRA_SERVICES:-}"
25
100
 
26
101
  if [ -n "$SERVICES" ]; then
27
102
  log "Starting shared infrastructure (project: $COMPOSE_PROJECT): $SERVICES"
28
103
  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}" \
104
+ AGENT_MINIO_PORT="$MINIO_PORT" \
105
+ AGENT_MINIO_CONSOLE_PORT="$MINIO_CONSOLE_PORT" \
106
+ AGENT_BROWSER_PORT="$BROWSER_PORT" \
107
+ AGENT_MAILPIT_WEB_PORT="$MAILPIT_WEB_PORT" \
108
+ AGENT_MAILPIT_SMTP_PORT="$MAILPIT_SMTP_PORT" \
34
109
  docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" up -d $SERVICES
35
110
  else
36
111
  log "No shared infra services enabled in harness.env (HARNESS_INFRA_SERVICES)"
@@ -85,18 +160,19 @@ fi
85
160
  # Shared app services — supporting services of a monolith/monorepo that agents
86
161
  # depend on but do not modify. Started ONCE on fixed ports, shared by all slots.
87
162
  # Create .har/ecosystem.shared.config.cjs (PM2 format, processes named
88
- # "har-shared-<name>") only when the primary app needs sibling services running.
163
+ # "har-${HARNESS_PROJECT_NAME}-shared-<name>") only when the primary app needs
164
+ # sibling services running.
89
165
  SHARED_ECOSYSTEM="$SCRIPT_DIR/ecosystem.shared.config.cjs"
90
166
  if [ -f "$SHARED_ECOSYSTEM" ]; then
91
167
  log "Starting shared app services from ecosystem.shared.config.cjs..."
92
168
  (cd "$REPO_ROOT" && npx --yes pm2 startOrReload "$SHARED_ECOSYSTEM" >/dev/null)
93
- log "Shared app services running (pm2 ls | grep har-shared-)."
169
+ log "Shared app services running (pm2 ls | grep har-${HARNESS_PROJECT_NAME}-shared-)."
94
170
  fi
95
171
 
96
172
  echo ""
97
173
  log "Infrastructure is ready."
98
174
  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}"
175
+ har_infra_enabled minio && log " MinIO: http://localhost:${MINIO_CONSOLE_PORT}"
176
+ har_infra_enabled headless-browser && log " Browser: http://localhost:${BROWSER_PORT}"
177
+ har_infra_enabled mailpit && log " Mailpit: http://localhost:${MAILPIT_WEB_PORT}"
102
178
  exit 0
@@ -40,7 +40,8 @@ if [ -f "$REGISTRY_FILE" ]; then
40
40
  fi
41
41
  [ -n "$WORKTREE_PATH" ] || WORKTREE_PATH="$HOME/worktrees/${HARNESS_PROJECT_NAME}-agent-${AGENT_ID}"
42
42
 
43
- npx --yes pm2 delete "/^agent-${AGENT_ID}-/" 2>/dev/null || true
43
+ PM2_REGEX="$(har_pm2_delete_regex "$AGENT_ID")"
44
+ npx --yes pm2 delete "$PM2_REGEX" 2>/dev/null || true
44
45
  echo "✓ Stopped PM2 processes"
45
46
 
46
47
  if har_infra_enabled db; then
@@ -97,6 +97,32 @@ Work in the isolated git worktree created by launch. Use `./.har/agent-cli.sh <i
97
97
 
98
98
  When the project needs Postgres, Redis, or similar, add the service to `docker-compose.agent.yml` (or keep one from the menu), list it in `HARNESS_INFRA_SERVICES` in `harness.env`, and use `setup-infra.sh` — never run raw `docker compose` for shared infra. Shared services run once on fixed ports and serve every agent slot.
99
99
 
100
+ ## Port & shared services (CLI profile)
101
+
102
+ This profile has **no PM2 app ports** — agents run project commands directly in their worktree. Port variables in `harness.env` exist for optional test servers and for shared Docker infra.
103
+
104
+ ### Port allocation
105
+
106
+ | Layer | Scope | Rule | On conflict |
107
+ |-------|-------|------|-------------|
108
+ | Shared Postgres | Per machine | `HARNESS_DB_PORT_DEFAULT` | Scan `HARNESS_DB_PORT_SCAN_START..END` |
109
+ | Other compose services | Per machine | `HARNESS_*_PORT_DEFAULT` for that service | Scan configured ranges in `harness.env` |
110
+
111
+ When a repo adds a local HTTP server for integration tests, prefer reading ports from `.env.agent.<id>` or `./.har/agent-cli.sh <id>` rather than hardcoding values.
112
+
113
+ ### Shared vs per-slot
114
+
115
+ | Resource | Model | Configuration |
116
+ |----------|-------|---------------|
117
+ | Postgres / Redis / mail / … | One shared container on a scanned host port | `HARNESS_INFRA_SERVICES` + matching vars in `harness.env` |
118
+ | Per-slot databases | Cloned from template DB when `db` is enabled | `launch.sh` |
119
+ | Application code | Isolated git worktree per slot | `HARNESS_USE_WORKTREE=true` |
120
+
121
+ ### Do not
122
+
123
+ - Hardcode `15432` or other default infra ports in tests — read `AGENT_DB_PORT` from `.env.agent.<id>` or `har_pg`
124
+ - Run raw `docker compose` for harness infrastructure — use `setup-infra.sh`
125
+
100
126
  ## Maintaining this harness
101
127
 
102
128
  When the project stack changes (new test commands, database needs, env vars):
@@ -122,6 +148,8 @@ work dir printed by launch, never in the main checkout.
122
148
  after explicit user approval.
123
149
  - `teardown` removes the worktree but **keeps the session branch** so you can push it
124
150
  or open a PR (`--delete-branch` to drop it).
151
+ - If launch fails after creating a worktree/env file, resume with
152
+ `har env launch <id> --resume` or `har env recover <id>`.
125
153
  - `har env complete <id>` finishes a session: full verify (recorded as a validation),
126
154
  then teardown — branch kept.
127
155
  - `--no-worktree` runs the slot from the repo root instead (single-agent mode).