@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.
- package/dist/index.js +719 -4688
- package/dist/prompts/system-authoring.md +26 -0
- package/dist/templates/adaptation-prompt-init.md +37 -1
- package/dist/templates/adaptation-prompt-maintain.md +30 -0
- package/dist/templates/har-boilerplate/CLAUDE.agent.md +14 -0
- package/dist/templates/har-boilerplate/README.md +18 -1
- package/dist/templates/har-boilerplate/agent-cli.sh +7 -1
- package/dist/templates/har-boilerplate/agent-slot.sh +70 -7
- package/dist/templates/har-boilerplate/harness.env +15 -0
- package/dist/templates/har-boilerplate/launch.sh +50 -0
- package/dist/templates/har-boilerplate/stages.json +1 -1
- package/dist/templates/har-boilerplate/verify.sh +17 -12
- package/dist/templates/har-boilerplate-cli/CLAUDE.agent.md +8 -0
- package/dist/templates/har-boilerplate-cli/README.md +6 -1
- package/dist/templates/har-boilerplate-cli/agent-slot.sh +70 -7
- package/dist/templates/har-boilerplate-cli/harness.env +8 -0
- package/dist/templates/har-boilerplate-cli/launch.sh +41 -0
- package/dist/templates/har-boilerplate-cli/stages.json +1 -1
- package/dist/templates/har-boilerplate-cli/verify.sh +14 -7
- package/dist/templates/har-boilerplate-ios/CLAUDE.agent.md +8 -0
- package/dist/templates/har-boilerplate-ios/README.md +5 -1
- package/dist/templates/har-boilerplate-ios/agent-slot.sh +70 -7
- package/dist/templates/har-boilerplate-ios/harness.env +4 -0
- package/dist/templates/har-boilerplate-ios/launch.sh +41 -0
- package/dist/templates/har-boilerplate-ios/stages.json +1 -1
- package/dist/templates/har-boilerplate-ios/verify.sh +15 -7
- 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,
|
|
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
|
-
#
|
|
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
|
|
206
|
-
#
|
|
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."
|
|
@@ -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=$(
|
|
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=$(
|
|
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=$(
|
|
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=$(
|
|
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=$(
|
|
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,
|
|
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
|
-
#
|
|
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
|
|
206
|
-
#
|
|
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
|
+
}
|
|
@@ -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."
|
|
@@ -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=$(
|
|
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=$(
|
|
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=$(
|
|
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=$(
|
|
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=$(
|
|
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.
|
|
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
|
}
|