@osfactory/har 0.1.1 → 0.3.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.
Files changed (27) hide show
  1. package/dist/index.js +719 -4688
  2. package/dist/prompts/system-authoring.md +26 -0
  3. package/dist/templates/adaptation-prompt-init.md +37 -1
  4. package/dist/templates/adaptation-prompt-maintain.md +30 -0
  5. package/dist/templates/har-boilerplate/CLAUDE.agent.md +14 -0
  6. package/dist/templates/har-boilerplate/README.md +18 -1
  7. package/dist/templates/har-boilerplate/agent-cli.sh +7 -1
  8. package/dist/templates/har-boilerplate/agent-slot.sh +70 -7
  9. package/dist/templates/har-boilerplate/harness.env +15 -0
  10. package/dist/templates/har-boilerplate/launch.sh +50 -0
  11. package/dist/templates/har-boilerplate/stages.json +1 -1
  12. package/dist/templates/har-boilerplate/verify.sh +17 -12
  13. package/dist/templates/har-boilerplate-cli/CLAUDE.agent.md +8 -0
  14. package/dist/templates/har-boilerplate-cli/README.md +6 -1
  15. package/dist/templates/har-boilerplate-cli/agent-slot.sh +70 -7
  16. package/dist/templates/har-boilerplate-cli/harness.env +8 -0
  17. package/dist/templates/har-boilerplate-cli/launch.sh +41 -0
  18. package/dist/templates/har-boilerplate-cli/stages.json +1 -1
  19. package/dist/templates/har-boilerplate-cli/verify.sh +14 -7
  20. package/dist/templates/har-boilerplate-ios/CLAUDE.agent.md +8 -0
  21. package/dist/templates/har-boilerplate-ios/README.md +5 -1
  22. package/dist/templates/har-boilerplate-ios/agent-slot.sh +70 -7
  23. package/dist/templates/har-boilerplate-ios/harness.env +4 -0
  24. package/dist/templates/har-boilerplate-ios/launch.sh +41 -0
  25. package/dist/templates/har-boilerplate-ios/stages.json +1 -1
  26. package/dist/templates/har-boilerplate-ios/verify.sh +15 -7
  27. package/package.json +7 -7
@@ -50,7 +50,8 @@ try {
50
50
  # Writes the registry entry from SLOT_* variables:
51
51
  # required: SLOT_AGENT_ID, SLOT_MODE (worktree|root), SLOT_WORK_DIR
52
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
53
+ # SLOT_BASE_COMMIT, SLOT_PORTS_JSON, SLOT_PREVIEW_URLS_JSON,
54
+ # SLOT_PURPOSE, SLOT_STATUS, SLOT_LAST_ERROR
54
55
  write_slot_registry() {
55
56
  local file
56
57
  file="$(slot_registry_file "$SLOT_AGENT_ID")"
@@ -65,7 +66,7 @@ const entry = {
65
66
  mode: e.SLOT_MODE,
66
67
  workDir: e.SLOT_WORK_DIR,
67
68
  createdAt: new Date().toISOString(),
68
- status: "active",
69
+ status: e.SLOT_STATUS || "active",
69
70
  };
70
71
  if (e.SLOT_SUFFIX) entry.suffix = e.SLOT_SUFFIX;
71
72
  if (e.SLOT_WORKTREE_PATH) entry.worktreePath = e.SLOT_WORKTREE_PATH;
@@ -73,6 +74,7 @@ if (e.SLOT_BRANCH) entry.branch = e.SLOT_BRANCH;
73
74
  if (e.SLOT_BASE_BRANCH) entry.baseBranch = e.SLOT_BASE_BRANCH;
74
75
  if (e.SLOT_BASE_COMMIT) entry.baseCommit = e.SLOT_BASE_COMMIT;
75
76
  if (e.SLOT_PURPOSE) entry.purpose = e.SLOT_PURPOSE;
77
+ if (e.SLOT_LAST_ERROR) entry.lastError = e.SLOT_LAST_ERROR;
76
78
  for (const [key, env] of [["ports", "SLOT_PORTS_JSON"], ["previewUrls", "SLOT_PREVIEW_URLS_JSON"]]) {
77
79
  if (e[env]) try { entry[key] = JSON.parse(e[env]); } catch {}
78
80
  }
@@ -115,13 +117,15 @@ slot_dirty_summary() {
115
117
  # Print a warning before replacing an occupied slot (stdout — visible to agents).
116
118
  print_slot_replace_warning() {
117
119
  local agent_id="$1"
118
- local reg wt branch work_dir purpose created dirty_summary head
120
+ local reg wt branch work_dir purpose created status last_error dirty_summary head
119
121
  reg="$(slot_registry_file "$agent_id")"
120
122
  wt="$(existing_slot_worktree "$agent_id")"
121
123
  branch="$(read_slot_field "$reg" branch || true)"
122
124
  work_dir="$(read_slot_field "$reg" workDir || true)"
123
125
  purpose="$(read_slot_field "$reg" purpose || true)"
124
126
  created="$(read_slot_field "$reg" createdAt || true)"
127
+ status="$(read_slot_field "$reg" status || true)"
128
+ last_error="$(read_slot_field "$reg" lastError || true)"
125
129
  dirty_summary="$(slot_dirty_summary "$wt")"
126
130
  if [ -n "$wt" ] && [ -d "$wt" ]; then
127
131
  head="$(git -C "$wt" rev-parse --short HEAD 2>/dev/null || true)"
@@ -133,6 +137,8 @@ print_slot_replace_warning() {
133
137
  [ -n "$wt" ] && echo " Worktree: ${wt}" >&2
134
138
  [ -n "$work_dir" ] && echo " Work dir: ${work_dir}" >&2
135
139
  [ -n "$branch" ] && echo " Branch: ${branch}${head:+ @ ${head}}" >&2
140
+ [ -n "$status" ] && echo " Status: ${status}" >&2
141
+ [ -n "$last_error" ] && echo " Error: ${last_error}" >&2
136
142
  [ -n "$created" ] && echo " Since: ${created}" >&2
137
143
  echo " Git: ${dirty_summary}" >&2
138
144
  echo "" >&2
@@ -182,11 +188,30 @@ require_slot_replace_confirm() {
182
188
  exit 2
183
189
  }
184
190
 
191
+ git_common_dir() {
192
+ local cwd="$1"
193
+ local out
194
+ out="$(git -C "$cwd" rev-parse --git-common-dir 2>/dev/null)" || return 1
195
+ case "$out" in
196
+ /*) echo "$out" ;;
197
+ *) (cd "$cwd" && cd "$out" && pwd) ;;
198
+ esac
199
+ }
200
+
201
+ same_git_checkout() {
202
+ local left right
203
+ left="$(git_common_dir "$1" || true)"
204
+ right="$(git_common_dir "$2" || true)"
205
+ [ -n "$left" ] && [ -n "$right" ] && [ "$left" = "$right" ]
206
+ }
207
+
185
208
  # 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.
209
+ # legacy and randomized session worktree fallbacks. Empty output when none exists.
187
210
  existing_slot_worktree() {
188
211
  local agent_id="$1"
189
- local reg path
212
+ local reg path candidate repo_root rel_prefix
213
+ repo_root="$(cd "$SCRIPT_DIR/.." && pwd)"
214
+ rel_prefix="$(git -C "$repo_root" rev-parse --show-prefix 2>/dev/null || true)"
190
215
  reg="$(slot_registry_file "$agent_id")"
191
216
  if [ -f "$reg" ]; then
192
217
  path="$(read_slot_field "$reg" worktreePath || true)"
@@ -198,12 +223,20 @@ existing_slot_worktree() {
198
223
  path="$HOME/worktrees/${HARNESS_PROJECT_NAME}-agent-${agent_id}"
199
224
  if [ -d "$path" ]; then
200
225
  echo "$path"
226
+ return 0
201
227
  fi
228
+ for candidate in "$HOME"/worktrees/*-har-agent-"${agent_id}"-*; do
229
+ [ -d "$candidate" ] || continue
230
+ same_git_checkout "$repo_root" "$candidate" || continue
231
+ [ -f "$candidate/${rel_prefix}.env.agent.${agent_id}" ] || continue
232
+ echo "$candidate"
233
+ return 0
234
+ done
202
235
  return 0
203
236
  }
204
237
 
205
- # Resolve .env.agent.<id> — registry work dir first, then legacy locations
206
- # (repo root, pre-registry fixed worktree path).
238
+ # Resolve .env.agent.<id> — registry work dir first, then legacy and
239
+ # randomized session worktree fallbacks.
207
240
  resolve_agent_env_file() {
208
241
  local agent_id="$1"
209
242
  local repo_root="$2"
@@ -229,6 +262,15 @@ resolve_agent_env_file() {
229
262
  return 0
230
263
  fi
231
264
  done
265
+ local candidate_dir
266
+ for candidate in "$HOME"/worktrees/*-har-agent-"${agent_id}"-*/${rel_prefix}.env.agent."${agent_id}"; do
267
+ if [ -f "$candidate" ]; then
268
+ candidate_dir="$(cd "$(dirname "$candidate")" && pwd)"
269
+ same_git_checkout "$repo_root" "$candidate_dir" || continue
270
+ echo "$candidate"
271
+ return 0
272
+ fi
273
+ done
232
274
  return 1
233
275
  }
234
276
 
@@ -259,6 +301,16 @@ resolve_agent_work_dir() {
259
301
  echo "$work_dir"
260
302
  }
261
303
 
304
+ # Portable millisecond clock (GNU date %N is unavailable on macOS/BSD).
305
+ now_ms() {
306
+ node -e 'process.stdout.write(String(Date.now()))' 2>/dev/null || echo 0
307
+ }
308
+
309
+ # JSON-escape step output; truncate to 50 lines in node (avoids SIGPIPE under pipefail).
310
+ escape_step_output() {
311
+ printf '%s' "$1" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const s=d.trim().split('\n').slice(0,50).join('\n');process.stdout.write(JSON.stringify(s))})" 2>/dev/null || echo '""'
312
+ }
313
+
262
314
  # Run browser-e2e on verify --full when the Playwright stage template is installed.
263
315
  run_browser_e2e_if_present() {
264
316
  local script_dir="$1"
@@ -268,3 +320,14 @@ run_browser_e2e_if_present() {
268
320
  "$e2e" "$agent_id"
269
321
  fi
270
322
  }
323
+
324
+ # Optional project-owned "agent usable" smoke beyond health.
325
+ run_readiness_if_configured() {
326
+ local agent_id="$1"
327
+ if [ -z "${HARNESS_READINESS_CMD:-}" ]; then
328
+ echo "No HARNESS_READINESS_CMD configured; skipping readiness smoke."
329
+ return 0
330
+ fi
331
+ local cmd="${HARNESS_READINESS_CMD//\{agentId\}/$agent_id}"
332
+ eval "$cmd"
333
+ }
@@ -38,3 +38,11 @@ har_pg() {
38
38
 
39
39
  export HARNESS_DB_MIGRATE_CMD="true"
40
40
  export HARNESS_DB_SEED_CMD="true"
41
+
42
+ # Optional readiness/data hooks for repositories that need more than static
43
+ # CLI/library checks. Enable only when this project needs them, and keep scripts
44
+ # idempotent.
45
+ #
46
+ # export HARNESS_TEMPLATE_DBS="main:template___PROJECT_NAME__"
47
+ # export HARNESS_DB_MINIMAL_BOOTSTRAP_CMD="./.har/bootstrap-data.sh"
48
+ # export HARNESS_READINESS_CMD="./.har/readiness.sh"
@@ -94,6 +94,46 @@ WORKTREE_DIR=${WORKTREE_DIR}
94
94
  NODE_ENV=test
95
95
  EOF
96
96
 
97
+ REGISTRY_WRITTEN=false
98
+ mark_slot_failed() {
99
+ local exit_code="$?"
100
+ if [ "$exit_code" != "0" ] && [ "$REGISTRY_WRITTEN" = true ]; then
101
+ log "Launch failed after creating the session. Recording failed slot state..."
102
+ set +e
103
+ SLOT_AGENT_ID="$AGENT_ID" \
104
+ SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
105
+ SLOT_WORK_DIR="$WORK_DIR" \
106
+ SLOT_SUFFIX="${SUFFIX:-}" \
107
+ SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
108
+ SLOT_BRANCH="${BRANCH:-}" \
109
+ SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
110
+ SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
111
+ SLOT_PURPOSE="${PURPOSE}" \
112
+ SLOT_STATUS="failed" \
113
+ SLOT_LAST_ERROR="launch.sh exited with code ${exit_code}" \
114
+ write_slot_registry
115
+ log " Work dir: ${WORK_DIR}"
116
+ log " Env file: ${ENV_FILE}"
117
+ log " Recovery: fix the failure, then relaunch with --replace when ready."
118
+ fi
119
+ }
120
+ trap mark_slot_failed EXIT
121
+
122
+ # Record the session before slow or fragile setup so verify/status/teardown can
123
+ # recover partial launches that already created a worktree and env file.
124
+ SLOT_AGENT_ID="$AGENT_ID" \
125
+ SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
126
+ SLOT_WORK_DIR="$WORK_DIR" \
127
+ SLOT_SUFFIX="${SUFFIX:-}" \
128
+ SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
129
+ SLOT_BRANCH="${BRANCH:-}" \
130
+ SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
131
+ SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
132
+ SLOT_PURPOSE="${PURPOSE}" \
133
+ SLOT_STATUS="starting" \
134
+ write_slot_registry
135
+ REGISTRY_WRITTEN=true
136
+
97
137
  if [ -f "$WORK_DIR/package.json" ]; then
98
138
  log "Installing dependencies..."
99
139
  (cd "$WORK_DIR" && npm install --silent)
@@ -118,6 +158,7 @@ SLOT_BRANCH="${BRANCH:-}" \
118
158
  SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
119
159
  SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
120
160
  SLOT_PURPOSE="${PURPOSE}" \
161
+ SLOT_STATUS="active" \
121
162
  write_slot_registry
122
163
 
123
164
  log "Agent $AGENT_ID is ready."
@@ -20,7 +20,7 @@
20
20
  {
21
21
  "path": ".env.agent.{agentId}",
22
22
  "kind": "file",
23
- "description": "Generated per-agent environment file"
23
+ "description": "Generated per-agent environment file in the session work dir"
24
24
  }
25
25
  ]
26
26
  },
@@ -35,9 +35,17 @@ set +a
35
35
  WORK_DIR="$(resolve_agent_work_dir "$ENV_FILE")"
36
36
 
37
37
  echo "==> Verifying agent ${AGENT_ID} in ${WORK_DIR}..." >&2
38
+ REG_FILE="$(slot_registry_file "$AGENT_ID")"
39
+ echo " Work dir: ${WORK_DIR}" >&2
40
+ echo " Env file: ${ENV_FILE}" >&2
41
+ if [ -f "$REG_FILE" ]; then
42
+ echo " Registry: ${REG_FILE}" >&2
43
+ else
44
+ echo " Registry: missing (${REG_FILE})" >&2
45
+ fi
38
46
 
39
47
  OVERALL_PASS=true
40
- START_TOTAL=$(date +%s%3N 2>/dev/null || echo "0")
48
+ START_TOTAL=$(now_ms)
41
49
  RESULTS_JSON="[]"
42
50
 
43
51
  run_step() {
@@ -46,14 +54,14 @@ run_step() {
46
54
  local start end elapsed exit_code output
47
55
 
48
56
  printf " → %-40s" "$name..." >&2
49
- start=$(date +%s%3N 2>/dev/null || echo "0")
57
+ start=$(now_ms)
50
58
 
51
59
  set +e
52
60
  output=$(cd "$WORK_DIR" && eval "$cmd" 2>&1)
53
61
  exit_code=$?
54
62
  set -e
55
63
 
56
- end=$(date +%s%3N 2>/dev/null || echo "0")
64
+ end=$(now_ms)
57
65
  elapsed=$(( end - start ))
58
66
 
59
67
  local pass_bool step_output_escaped
@@ -67,9 +75,7 @@ run_step() {
67
75
  OVERALL_PASS=false
68
76
  fi
69
77
 
70
- step_output_escaped=$(echo "$output" | head -50 | \
71
- node -e "let d=''; process.stdin.on('data',c=>d+=c); process.stdin.on('end',()=>process.stdout.write(JSON.stringify(d.trim())))" \
72
- 2>/dev/null || echo '""')
78
+ step_output_escaped=$(escape_step_output "$output")
73
79
 
74
80
  RESULTS_JSON=$(echo "$RESULTS_JSON" | node -e "
75
81
  const fs = require('fs');
@@ -90,10 +96,11 @@ run_step "unit-tests" "npm test" || { [ -z "$FULL" ] && true; }
90
96
 
91
97
  if [ -n "$FULL" ]; then
92
98
  run_step "lint" "npm run lint" || true
99
+ run_step "readiness" "run_readiness_if_configured \"$AGENT_ID\"" || true
93
100
  run_step "browser-e2e" "run_browser_e2e_if_present \"$SCRIPT_DIR\" \"$AGENT_ID\"" || true
94
101
  fi
95
102
 
96
- END_TOTAL=$(date +%s%3N 2>/dev/null || echo "0")
103
+ END_TOTAL=$(now_ms)
97
104
  TOTAL_MS=$(( END_TOTAL - START_TOTAL ))
98
105
 
99
106
  node -e "
@@ -17,9 +17,17 @@
17
17
  ./.har/agent-cli.sh ${AGENT_ID} status
18
18
  ```
19
19
 
20
+ ## Readiness
21
+
22
+ Adapt this section for the repository. A successful build/test run does not
23
+ always mean an agent can use the app. If the app needs a backend, credentials,
24
+ seeded data, or a simulator flow, document it here and wire the smoke into
25
+ `HARNESS_READINESS_CMD`, RocketSim flows, or full verify.
26
+
20
27
  ## Definition of done
21
28
 
22
29
  - [ ] Full verification returns `"status": "pass"` (`har env verify ${AGENT_ID} --full`, MCP `har_run_verification` with `full: true`, or `./.har/verify.sh ${AGENT_ID} --full`)
30
+ - [ ] The slot is agent-usable for this repo's documented smoke workflow when runtime services are involved
23
31
  - [ ] When `stages/rocketsim-flows.sh` exists, full verify includes user-flow validation — add or update flow scripts in `flows/` for UI changes
24
32
  - [ ] New behavior has automated test coverage (unit tests via XCTest)
25
33
  - [ ] Changes committed **in the session worktree** with a clear message
@@ -67,7 +67,11 @@ This installs `.har/stages/rocketsim-flows.sh` and a `flows/` directory. Add flo
67
67
  | Mode | Command | Typical steps |
68
68
  |------|---------|---------------|
69
69
  | Quick | `har env verify <id>` | build, unit-tests |
70
- | Full | `har env verify <id> --full` | + lint, **rocketsim-flows** when installed |
70
+ | Full | `har env verify <id> --full` | + lint, optional readiness smoke, **rocketsim-flows** when installed |
71
+
72
+ For apps that depend on local backends, auth, seeded state, or simulator flows,
73
+ distinguish build/test health from agent usability. Document any skipped full
74
+ dev setup and add a readiness command when agents need a real workflow to pass.
71
75
 
72
76
  ## Configuration
73
77
 
@@ -50,7 +50,8 @@ try {
50
50
  # Writes the registry entry from SLOT_* variables:
51
51
  # required: SLOT_AGENT_ID, SLOT_MODE (worktree|root), SLOT_WORK_DIR
52
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
53
+ # SLOT_BASE_COMMIT, SLOT_PORTS_JSON, SLOT_PREVIEW_URLS_JSON,
54
+ # SLOT_PURPOSE, SLOT_STATUS, SLOT_LAST_ERROR
54
55
  write_slot_registry() {
55
56
  local file
56
57
  file="$(slot_registry_file "$SLOT_AGENT_ID")"
@@ -65,7 +66,7 @@ const entry = {
65
66
  mode: e.SLOT_MODE,
66
67
  workDir: e.SLOT_WORK_DIR,
67
68
  createdAt: new Date().toISOString(),
68
- status: "active",
69
+ status: e.SLOT_STATUS || "active",
69
70
  };
70
71
  if (e.SLOT_SUFFIX) entry.suffix = e.SLOT_SUFFIX;
71
72
  if (e.SLOT_WORKTREE_PATH) entry.worktreePath = e.SLOT_WORKTREE_PATH;
@@ -73,6 +74,7 @@ if (e.SLOT_BRANCH) entry.branch = e.SLOT_BRANCH;
73
74
  if (e.SLOT_BASE_BRANCH) entry.baseBranch = e.SLOT_BASE_BRANCH;
74
75
  if (e.SLOT_BASE_COMMIT) entry.baseCommit = e.SLOT_BASE_COMMIT;
75
76
  if (e.SLOT_PURPOSE) entry.purpose = e.SLOT_PURPOSE;
77
+ if (e.SLOT_LAST_ERROR) entry.lastError = e.SLOT_LAST_ERROR;
76
78
  for (const [key, env] of [["ports", "SLOT_PORTS_JSON"], ["previewUrls", "SLOT_PREVIEW_URLS_JSON"]]) {
77
79
  if (e[env]) try { entry[key] = JSON.parse(e[env]); } catch {}
78
80
  }
@@ -115,13 +117,15 @@ slot_dirty_summary() {
115
117
  # Print a warning before replacing an occupied slot (stdout — visible to agents).
116
118
  print_slot_replace_warning() {
117
119
  local agent_id="$1"
118
- local reg wt branch work_dir purpose created dirty_summary head
120
+ local reg wt branch work_dir purpose created status last_error dirty_summary head
119
121
  reg="$(slot_registry_file "$agent_id")"
120
122
  wt="$(existing_slot_worktree "$agent_id")"
121
123
  branch="$(read_slot_field "$reg" branch || true)"
122
124
  work_dir="$(read_slot_field "$reg" workDir || true)"
123
125
  purpose="$(read_slot_field "$reg" purpose || true)"
124
126
  created="$(read_slot_field "$reg" createdAt || true)"
127
+ status="$(read_slot_field "$reg" status || true)"
128
+ last_error="$(read_slot_field "$reg" lastError || true)"
125
129
  dirty_summary="$(slot_dirty_summary "$wt")"
126
130
  if [ -n "$wt" ] && [ -d "$wt" ]; then
127
131
  head="$(git -C "$wt" rev-parse --short HEAD 2>/dev/null || true)"
@@ -133,6 +137,8 @@ print_slot_replace_warning() {
133
137
  [ -n "$wt" ] && echo " Worktree: ${wt}" >&2
134
138
  [ -n "$work_dir" ] && echo " Work dir: ${work_dir}" >&2
135
139
  [ -n "$branch" ] && echo " Branch: ${branch}${head:+ @ ${head}}" >&2
140
+ [ -n "$status" ] && echo " Status: ${status}" >&2
141
+ [ -n "$last_error" ] && echo " Error: ${last_error}" >&2
136
142
  [ -n "$created" ] && echo " Since: ${created}" >&2
137
143
  echo " Git: ${dirty_summary}" >&2
138
144
  echo "" >&2
@@ -182,11 +188,30 @@ require_slot_replace_confirm() {
182
188
  exit 2
183
189
  }
184
190
 
191
+ git_common_dir() {
192
+ local cwd="$1"
193
+ local out
194
+ out="$(git -C "$cwd" rev-parse --git-common-dir 2>/dev/null)" || return 1
195
+ case "$out" in
196
+ /*) echo "$out" ;;
197
+ *) (cd "$cwd" && cd "$out" && pwd) ;;
198
+ esac
199
+ }
200
+
201
+ same_git_checkout() {
202
+ local left right
203
+ left="$(git_common_dir "$1" || true)"
204
+ right="$(git_common_dir "$2" || true)"
205
+ [ -n "$left" ] && [ -n "$right" ] && [ "$left" = "$right" ]
206
+ }
207
+
185
208
  # 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.
209
+ # legacy and randomized session worktree fallbacks. Empty output when none exists.
187
210
  existing_slot_worktree() {
188
211
  local agent_id="$1"
189
- local reg path
212
+ local reg path candidate repo_root rel_prefix
213
+ repo_root="$(cd "$SCRIPT_DIR/.." && pwd)"
214
+ rel_prefix="$(git -C "$repo_root" rev-parse --show-prefix 2>/dev/null || true)"
190
215
  reg="$(slot_registry_file "$agent_id")"
191
216
  if [ -f "$reg" ]; then
192
217
  path="$(read_slot_field "$reg" worktreePath || true)"
@@ -198,12 +223,20 @@ existing_slot_worktree() {
198
223
  path="$HOME/worktrees/${HARNESS_PROJECT_NAME}-agent-${agent_id}"
199
224
  if [ -d "$path" ]; then
200
225
  echo "$path"
226
+ return 0
201
227
  fi
228
+ for candidate in "$HOME"/worktrees/*-har-agent-"${agent_id}"-*; do
229
+ [ -d "$candidate" ] || continue
230
+ same_git_checkout "$repo_root" "$candidate" || continue
231
+ [ -f "$candidate/${rel_prefix}.env.agent.${agent_id}" ] || continue
232
+ echo "$candidate"
233
+ return 0
234
+ done
202
235
  return 0
203
236
  }
204
237
 
205
- # Resolve .env.agent.<id> — registry work dir first, then legacy locations
206
- # (repo root, pre-registry fixed worktree path).
238
+ # Resolve .env.agent.<id> — registry work dir first, then legacy and
239
+ # randomized session worktree fallbacks.
207
240
  resolve_agent_env_file() {
208
241
  local agent_id="$1"
209
242
  local repo_root="$2"
@@ -229,6 +262,15 @@ resolve_agent_env_file() {
229
262
  return 0
230
263
  fi
231
264
  done
265
+ local candidate_dir
266
+ for candidate in "$HOME"/worktrees/*-har-agent-"${agent_id}"-*/${rel_prefix}.env.agent."${agent_id}"; do
267
+ if [ -f "$candidate" ]; then
268
+ candidate_dir="$(cd "$(dirname "$candidate")" && pwd)"
269
+ same_git_checkout "$repo_root" "$candidate_dir" || continue
270
+ echo "$candidate"
271
+ return 0
272
+ fi
273
+ done
232
274
  return 1
233
275
  }
234
276
 
@@ -259,6 +301,16 @@ resolve_agent_work_dir() {
259
301
  echo "$work_dir"
260
302
  }
261
303
 
304
+ # Portable millisecond clock (GNU date %N is unavailable on macOS/BSD).
305
+ now_ms() {
306
+ node -e 'process.stdout.write(String(Date.now()))' 2>/dev/null || echo 0
307
+ }
308
+
309
+ # JSON-escape step output; truncate to 50 lines in node (avoids SIGPIPE under pipefail).
310
+ escape_step_output() {
311
+ printf '%s' "$1" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const s=d.trim().split('\n').slice(0,50).join('\n');process.stdout.write(JSON.stringify(s))})" 2>/dev/null || echo '""'
312
+ }
313
+
262
314
  # Run browser-e2e on verify --full when the Playwright stage template is installed.
263
315
  run_browser_e2e_if_present() {
264
316
  local script_dir="$1"
@@ -268,3 +320,14 @@ run_browser_e2e_if_present() {
268
320
  "$e2e" "$agent_id"
269
321
  fi
270
322
  }
323
+
324
+ # Optional project-owned "agent usable" smoke beyond health.
325
+ run_readiness_if_configured() {
326
+ local agent_id="$1"
327
+ if [ -z "${HARNESS_READINESS_CMD:-}" ]; then
328
+ echo "No HARNESS_READINESS_CMD configured; skipping readiness smoke."
329
+ return 0
330
+ fi
331
+ local cmd="${HARNESS_READINESS_CMD//\{agentId\}/$agent_id}"
332
+ eval "$cmd"
333
+ }
@@ -40,3 +40,7 @@ har_infra_enabled() {
40
40
  *) return 1 ;;
41
41
  esac
42
42
  }
43
+
44
+ # Optional "agent usable" smoke beyond build/test, such as a simulator flow or
45
+ # backend-auth check. Enable only when this project needs it.
46
+ # export HARNESS_READINESS_CMD="./.har/readiness.sh"
@@ -94,6 +94,46 @@ WORKTREE_DIR=${WORKTREE_DIR}
94
94
  NODE_ENV=test
95
95
  EOF
96
96
 
97
+ REGISTRY_WRITTEN=false
98
+ mark_slot_failed() {
99
+ local exit_code="$?"
100
+ if [ "$exit_code" != "0" ] && [ "$REGISTRY_WRITTEN" = true ]; then
101
+ log "Launch failed after creating the session. Recording failed slot state..."
102
+ set +e
103
+ SLOT_AGENT_ID="$AGENT_ID" \
104
+ SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
105
+ SLOT_WORK_DIR="$WORK_DIR" \
106
+ SLOT_SUFFIX="${SUFFIX:-}" \
107
+ SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
108
+ SLOT_BRANCH="${BRANCH:-}" \
109
+ SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
110
+ SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
111
+ SLOT_PURPOSE="${PURPOSE}" \
112
+ SLOT_STATUS="failed" \
113
+ SLOT_LAST_ERROR="launch.sh exited with code ${exit_code}" \
114
+ write_slot_registry
115
+ log " Work dir: ${WORK_DIR}"
116
+ log " Env file: ${ENV_FILE}"
117
+ log " Recovery: fix the failure, then relaunch with --replace when ready."
118
+ fi
119
+ }
120
+ trap mark_slot_failed EXIT
121
+
122
+ # Record the session before slow or fragile setup so verify/status/teardown can
123
+ # recover partial launches that already created a worktree and env file.
124
+ SLOT_AGENT_ID="$AGENT_ID" \
125
+ SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
126
+ SLOT_WORK_DIR="$WORK_DIR" \
127
+ SLOT_SUFFIX="${SUFFIX:-}" \
128
+ SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
129
+ SLOT_BRANCH="${BRANCH:-}" \
130
+ SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
131
+ SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
132
+ SLOT_PURPOSE="${PURPOSE}" \
133
+ SLOT_STATUS="starting" \
134
+ write_slot_registry
135
+ REGISTRY_WRITTEN=true
136
+
97
137
  if [ -f "$WORK_DIR/package.json" ]; then
98
138
  log "Installing dependencies..."
99
139
  (cd "$WORK_DIR" && npm install --silent)
@@ -118,6 +158,7 @@ SLOT_BRANCH="${BRANCH:-}" \
118
158
  SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
119
159
  SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
120
160
  SLOT_PURPOSE="${PURPOSE}" \
161
+ SLOT_STATUS="active" \
121
162
  write_slot_registry
122
163
 
123
164
  log "Agent $AGENT_ID is ready."
@@ -20,7 +20,7 @@
20
20
  {
21
21
  "path": ".env.agent.{agentId}",
22
22
  "kind": "file",
23
- "description": "Generated per-agent environment file"
23
+ "description": "Generated per-agent environment file in the session work dir"
24
24
  }
25
25
  ]
26
26
  },
@@ -38,6 +38,14 @@ set +a
38
38
  WORK_DIR="$(resolve_agent_work_dir "$ENV_FILE")"
39
39
 
40
40
  echo "==> Verifying agent ${AGENT_ID} in ${WORK_DIR}..." >&2
41
+ REG_FILE="$(slot_registry_file "$AGENT_ID")"
42
+ echo " Work dir: ${WORK_DIR}" >&2
43
+ echo " Env file: ${ENV_FILE}" >&2
44
+ if [ -f "$REG_FILE" ]; then
45
+ echo " Registry: ${REG_FILE}" >&2
46
+ else
47
+ echo " Registry: missing (${REG_FILE})" >&2
48
+ fi
41
49
 
42
50
  # Build xcodebuild project/workspace flags
43
51
  xc_target_flags() {
@@ -62,7 +70,7 @@ XC_DESTINATION="${HARNESS_IOS_DESTINATION:-platform=iOS Simulator,name=iPhone 16
62
70
  XC_DERIVED="${WORK_DIR}/build/DerivedData"
63
71
 
64
72
  OVERALL_PASS=true
65
- START_TOTAL=$(date +%s%3N 2>/dev/null || echo "0")
73
+ START_TOTAL=$(now_ms)
66
74
  RESULTS_JSON="[]"
67
75
 
68
76
  run_step() {
@@ -71,14 +79,14 @@ run_step() {
71
79
  local start end elapsed exit_code output
72
80
 
73
81
  printf " → %-40s" "$name..." >&2
74
- start=$(date +%s%3N 2>/dev/null || echo "0")
82
+ start=$(now_ms)
75
83
 
76
84
  set +e
77
85
  output=$(cd "$WORK_DIR" && eval "$cmd" 2>&1)
78
86
  exit_code=$?
79
87
  set -e
80
88
 
81
- end=$(date +%s%3N 2>/dev/null || echo "0")
89
+ end=$(now_ms)
82
90
  elapsed=$(( end - start ))
83
91
 
84
92
  local pass_bool step_output_escaped
@@ -92,9 +100,7 @@ run_step() {
92
100
  OVERALL_PASS=false
93
101
  fi
94
102
 
95
- step_output_escaped=$(echo "$output" | head -50 | \
96
- node -e "let d=''; process.stdin.on('data',c=>d+=c); process.stdin.on('end',()=>process.stdout.write(JSON.stringify(d.trim())))" \
97
- 2>/dev/null || echo '""')
103
+ step_output_escaped=$(escape_step_output "$output")
98
104
 
99
105
  RESULTS_JSON=$(echo "$RESULTS_JSON" | node -e "
100
106
  const fs = require('fs');
@@ -141,11 +147,13 @@ if [ -n "$FULL" ]; then
141
147
  run_step "lint" "command -v \"${HARNESS_SWIFTLINT_CMD:-swiftlint}\" >/dev/null 2>&1 && \
142
148
  \"${HARNESS_SWIFTLINT_CMD:-swiftlint}\" --quiet 2>&1 || echo 'swiftlint not installed — skipping'" || true
143
149
 
150
+ run_step "readiness" "run_readiness_if_configured \"$AGENT_ID\"" || true
151
+
144
152
  # RocketSim user-flow validation — installed via: har env add-stage rocketsim
145
153
  run_rocketsim_flows_if_present "$SCRIPT_DIR" "$AGENT_ID" || true
146
154
  fi
147
155
 
148
- END_TOTAL=$(date +%s%3N 2>/dev/null || echo "0")
156
+ END_TOTAL=$(now_ms)
149
157
  TOTAL_MS=$(( END_TOTAL - START_TOTAL ))
150
158
 
151
159
  node -e "
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osfactory/har",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "LLM-powered CLI for reproducible agent development environments",
5
5
  "keywords": [
6
6
  "har",
@@ -40,6 +40,11 @@
40
40
  "zod": "^3.23.8"
41
41
  },
42
42
  "devDependencies": {
43
+ "@semantic-release/changelog": "^6.0.3",
44
+ "@semantic-release/exec": "^6.0.3",
45
+ "@semantic-release/git": "^10.0.1",
46
+ "@semantic-release/github": "^11.0.1",
47
+ "@semantic-release/npm": "^13.1.5",
43
48
  "@types/inquirer": "^9.0.7",
44
49
  "@types/jest": "^29.5.12",
45
50
  "@types/js-yaml": "^4.0.9",
@@ -49,11 +54,6 @@
49
54
  "@typescript-eslint/parser": "^8.62.0",
50
55
  "esbuild": "^0.23.0",
51
56
  "eslint": "^8.57.1",
52
- "@semantic-release/changelog": "^6.0.3",
53
- "@semantic-release/exec": "^6.0.3",
54
- "@semantic-release/git": "^10.0.1",
55
- "@semantic-release/github": "^11.0.1",
56
- "@semantic-release/npm": "^12.0.1",
57
57
  "jest": "^29.7.0",
58
58
  "semantic-release": "^24.2.3",
59
59
  "ts-jest": "^29.2.4",
@@ -73,6 +73,6 @@
73
73
  "license": "AGPL-3.0-only",
74
74
  "publishConfig": {
75
75
  "access": "public",
76
- "registry": "https://registry.npmjs.org"
76
+ "registry": "https://registry.npmjs.org/"
77
77
  }
78
78
  }