@osfactory/har 0.64.2 → 1.0.0-rc.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/dist/index.js +15675 -6536
- package/dist/prompts/system-authoring.md +155 -0
- package/dist/templates/adaptation-prompt-init.md +67 -43
- package/dist/templates/adaptation-prompt-maintain.md +38 -38
- package/dist/templates/agent-skills/har-maintain.md +2 -2
- package/dist/templates/agent-skills/setup-har.md +6 -4
- package/dist/templates/cursor-rule.mdc.template +3 -3
- package/dist/templates/har-boilerplate/docker-compose.agent.yml +1 -1
- package/dist/templates/har-boilerplate/harness.env +11 -155
- package/dist/templates/har-boilerplate/stages.json +51 -2
- package/dist/templates/har-boilerplate-cli/docker-compose.agent.yml +1 -1
- package/dist/templates/har-boilerplate-cli/harness.env +12 -138
- package/dist/templates/har-boilerplate-cli/stages.json +42 -2
- package/dist/templates/har-boilerplate-ios/harness.env +10 -126
- package/dist/templates/har-boilerplate-ios/stages.json +42 -2
- package/dist/templates/migration-prompt.md +54 -0
- package/dist/templates/plugins/custom-stage-skeleton.sh +11 -20
- package/dist/templates/plugins/gitleaks/.github/workflows/gitleaks.yml +1 -1
- package/dist/templates/plugins/gitleaks/.har/stages/GITLEAKS.md +1 -1
- package/dist/templates/plugins/gitleaks/.har/stages/secrets-scan.sh +8 -23
- package/dist/templates/plugins/gitleaks/README.md +1 -1
- package/dist/templates/plugins/gitleaks/template.manifest.json +20 -5
- package/dist/templates/plugins/kerno/.github/workflows/kerno.yml +1 -1
- package/dist/templates/plugins/kerno/.har/stages/backend-validation.sh +8 -23
- package/dist/templates/plugins/kerno/README.md +1 -1
- package/dist/templates/plugins/kerno/template.manifest.json +20 -9
- package/dist/templates/plugins/local-plugin-skeleton/README.md +46 -0
- package/dist/templates/plugins/playwright/.har/stages/browser-e2e.sh +8 -23
- package/dist/templates/plugins/playwright/template.manifest.json +36 -13
- package/dist/templates/plugins/rocketsim/.har/stages/rocketsim-flows.sh +8 -23
- package/dist/templates/plugins/rocketsim/template.manifest.json +20 -8
- package/dist/templates/plugins/semgrep/.har/stages/sast.sh +10 -26
- package/dist/templates/plugins/semgrep/README.md +1 -1
- package/dist/templates/plugins/semgrep/template.manifest.json +16 -7
- package/dist/templates/plugins/trivy/.har/stages/vuln-scan.sh +8 -23
- package/dist/templates/plugins/trivy/README.md +1 -1
- package/dist/templates/plugins/trivy/template.manifest.json +20 -8
- package/dist/templates/{har-boilerplate-cli/CLAUDE.agent.md → profiles/cli/docs/claude-agent/definition-of-done.md} +0 -42
- package/dist/templates/profiles/cli/docs/claude-agent/do-not.md +6 -0
- package/dist/templates/profiles/cli/docs/claude-agent/environment.md +12 -0
- package/dist/templates/profiles/cli/docs/claude-agent/project-commands.md +10 -0
- package/dist/templates/profiles/cli/docs/claude-agent/readiness.md +6 -0
- package/dist/templates/profiles/cli/docs/readme/for-coding-agents.md +9 -0
- package/dist/templates/profiles/cli/docs/readme/intro.md +7 -0
- package/dist/templates/profiles/cli/docs/readme/port-shared-services-cli-profile.md +25 -0
- package/dist/templates/profiles/cli/docs/readme/quick-start.md +24 -0
- package/dist/templates/profiles/cli/docs/readme/verification-contract.md +22 -0
- package/dist/templates/profiles/cli/docs/readme/what-s-in-here.md +27 -0
- package/dist/templates/profiles/cli/profile.manifest.json +56 -1
- package/dist/templates/profiles/default/docs/claude-agent/definition-of-done.md +24 -0
- package/dist/templates/profiles/default/docs/claude-agent/do-not.md +10 -0
- package/dist/templates/profiles/default/docs/claude-agent/environment.md +19 -0
- package/dist/templates/profiles/default/docs/claude-agent/project-commands.md +10 -0
- package/dist/templates/profiles/default/docs/claude-agent/readiness.md +12 -0
- package/dist/templates/profiles/default/docs/readme/architecture.md +11 -0
- package/dist/templates/profiles/default/docs/readme/for-coding-agents.md +9 -0
- package/dist/templates/profiles/default/docs/readme/intro.md +7 -0
- package/dist/templates/profiles/default/docs/readme/port-shared-services.md +41 -0
- package/dist/templates/profiles/default/docs/readme/quick-start.md +24 -0
- package/dist/templates/profiles/default/docs/readme/readiness-layers.md +16 -0
- package/dist/templates/profiles/default/docs/readme/verification-contract.md +17 -0
- package/dist/templates/profiles/default/docs/readme/what-s-in-here.md +28 -0
- package/dist/templates/profiles/default/profile.manifest.json +62 -1
- package/dist/templates/profiles/ios/docs/claude-agent/definition-of-done.md +23 -0
- package/dist/templates/profiles/ios/docs/claude-agent/do-not.md +6 -0
- package/dist/templates/profiles/ios/docs/claude-agent/environment.md +14 -0
- package/dist/templates/profiles/ios/docs/claude-agent/intro.md +3 -0
- package/dist/templates/profiles/ios/docs/claude-agent/project-commands.md +21 -0
- package/dist/templates/profiles/ios/docs/claude-agent/readiness.md +6 -0
- package/dist/templates/profiles/ios/docs/claude-agent/user-flow-validation-rocketsim.md +11 -0
- package/dist/templates/profiles/ios/docs/readme/configuration.md +7 -0
- package/dist/templates/profiles/ios/docs/readme/intro.md +7 -0
- package/dist/templates/profiles/ios/docs/readme/port-shared-services-ios-profile.md +25 -0
- package/dist/templates/profiles/ios/docs/readme/quick-start.md +22 -0
- package/dist/templates/profiles/ios/docs/readme/simulators.md +45 -0
- package/dist/templates/profiles/ios/docs/readme/user-flow-validation.md +9 -0
- package/dist/templates/profiles/ios/docs/readme/verification-contract.md +13 -0
- package/dist/templates/profiles/ios/docs/readme/what-s-in-here.md +27 -0
- package/dist/templates/profiles/ios/profile.manifest.json +59 -3
- package/dist/templates/runtime-bundles/pm2-runtime/attach.sh +8 -15
- package/dist/templates/runtime-bundles/pm2-runtime/env.template +1 -1
- package/dist/templates/runtime-bundles/pm2-runtime/stages/api-health.sh +9 -0
- package/dist/templates/runtime-bundles/shared-kernel/STAGES.md +32 -18
- package/dist/templates/runtime-bundles/shared-kernel/agent-cli.sh +35 -0
- package/dist/templates/runtime-bundles/shared-kernel/launch.sh +35 -0
- package/dist/templates/runtime-bundles/shared-kernel/lib/verify-runner.mjs +129 -0
- package/dist/templates/runtime-bundles/shared-kernel/preflight.sh +35 -0
- package/dist/templates/runtime-bundles/shared-kernel/setup-infra.sh +35 -0
- package/dist/templates/runtime-bundles/shared-kernel/stages/README.sh +1 -1
- package/dist/templates/runtime-bundles/shared-kernel/stages/readiness.sh +14 -0
- package/dist/templates/runtime-bundles/shared-kernel/teardown.sh +35 -0
- package/dist/templates/runtime-bundles/shared-kernel/verify.sh +35 -0
- package/dist/templates/shared-docs/claude-agent/intro.md +3 -0
- package/dist/templates/shared-docs/readme/maintaining-this-harness.md +14 -0
- package/dist/templates/shared-docs/readme/run-history.md +5 -0
- package/dist/templates/shared-docs/readme/session-lifecycle.md +20 -0
- package/package.json +1 -1
- package/dist/templates/har-boilerplate/CLAUDE.agent.md +0 -83
- package/dist/templates/har-boilerplate/README.md +0 -199
- package/dist/templates/har-boilerplate/STAGES.md +0 -99
- package/dist/templates/har-boilerplate/agent-cli.sh +0 -225
- package/dist/templates/har-boilerplate/agent-slot.sh +0 -850
- package/dist/templates/har-boilerplate/attach.sh +0 -22
- package/dist/templates/har-boilerplate/ecosystem.agent.template.cjs +0 -52
- package/dist/templates/har-boilerplate/env.template +0 -40
- package/dist/templates/har-boilerplate/launch.sh +0 -323
- package/dist/templates/har-boilerplate/preflight.sh +0 -32
- package/dist/templates/har-boilerplate/provision-toolchain.sh +0 -766
- package/dist/templates/har-boilerplate/setup-infra.sh +0 -178
- package/dist/templates/har-boilerplate/stages/README.sh +0 -11
- package/dist/templates/har-boilerplate/teardown.sh +0 -87
- package/dist/templates/har-boilerplate/verify.sh +0 -249
- package/dist/templates/har-boilerplate-cli/README.md +0 -157
- package/dist/templates/har-boilerplate-cli/STAGES.md +0 -99
- package/dist/templates/har-boilerplate-cli/agent-cli.sh +0 -160
- package/dist/templates/har-boilerplate-cli/agent-slot.sh +0 -832
- package/dist/templates/har-boilerplate-cli/launch.sh +0 -216
- package/dist/templates/har-boilerplate-cli/preflight.sh +0 -28
- package/dist/templates/har-boilerplate-cli/provision-toolchain.sh +0 -766
- package/dist/templates/har-boilerplate-cli/setup-infra.sh +0 -178
- package/dist/templates/har-boilerplate-cli/stages/README.sh +0 -11
- package/dist/templates/har-boilerplate-cli/teardown.sh +0 -66
- package/dist/templates/har-boilerplate-cli/verify.sh +0 -210
- package/dist/templates/har-boilerplate-ios/ADAPT-PROMPT.md +0 -63
- package/dist/templates/har-boilerplate-ios/CLAUDE.agent.md +0 -90
- package/dist/templates/har-boilerplate-ios/README.md +0 -173
- package/dist/templates/har-boilerplate-ios/STAGES.md +0 -99
- package/dist/templates/har-boilerplate-ios/agent-cli.sh +0 -149
- package/dist/templates/har-boilerplate-ios/agent-slot.sh +0 -479
- package/dist/templates/har-boilerplate-ios/launch.sh +0 -206
- package/dist/templates/har-boilerplate-ios/provision-toolchain.sh +0 -766
- package/dist/templates/har-boilerplate-ios/setup-infra.sh +0 -112
- package/dist/templates/har-boilerplate-ios/simulator.sh +0 -492
- package/dist/templates/har-boilerplate-ios/stages/README.sh +0 -11
- package/dist/templates/har-boilerplate-ios/teardown.sh +0 -68
- package/dist/templates/har-boilerplate-ios/verify.sh +0 -185
- package/dist/templates/runtime-bundles/pm2-runtime/README.md +0 -4
- package/dist/templates/runtime-bundles/shared-kernel/README.md +0 -3
- package/dist/templates/runtime-bundles/shared-kernel/provision-toolchain.sh +0 -766
- package/dist/templates/runtime-bundles/xcode-sim/README.md +0 -4
- package/dist/templates/runtime-bundles/xcode-sim/simulator.sh +0 -492
|
@@ -1,850 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Shared slot helpers: agent-id validation, slot registry, work-dir resolution.
|
|
3
|
-
# Source after harness.env (requires $SCRIPT_DIR pointing at the .har directory):
|
|
4
|
-
# source "$SCRIPT_DIR/harness.env"
|
|
5
|
-
# source "$SCRIPT_DIR/agent-slot.sh"
|
|
6
|
-
# validate_agent_id "$AGENT_ID"
|
|
7
|
-
|
|
8
|
-
# Canonical slot limits live in stages.json (agentSlots); harness.env is legacy fallback.
|
|
9
|
-
har_load_agent_slot_limits() {
|
|
10
|
-
local registry="${SCRIPT_DIR}/stages.json"
|
|
11
|
-
if [[ -f "$registry" ]]; then
|
|
12
|
-
local parsed
|
|
13
|
-
parsed="$(node -e '
|
|
14
|
-
try {
|
|
15
|
-
const slots = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).agentSlots;
|
|
16
|
-
if (slots && Number.isInteger(slots.min) && Number.isInteger(slots.max)) {
|
|
17
|
-
process.stdout.write(String(slots.min) + " " + String(slots.max));
|
|
18
|
-
}
|
|
19
|
-
} catch {}
|
|
20
|
-
' "$registry" 2>/dev/null || true)"
|
|
21
|
-
if [[ -n "$parsed" ]]; then
|
|
22
|
-
HARNESS_AGENT_SLOT_MIN="${parsed%% *}"
|
|
23
|
-
HARNESS_AGENT_SLOT_MAX="${parsed#* }"
|
|
24
|
-
export HARNESS_AGENT_SLOT_MIN HARNESS_AGENT_SLOT_MAX
|
|
25
|
-
return 0
|
|
26
|
-
fi
|
|
27
|
-
fi
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
har_suggest_launch() {
|
|
31
|
-
local id="$1"
|
|
32
|
-
echo " Launch: har env launch ${id} # or har_launch_environment (MCP)" >&2
|
|
33
|
-
echo " Fallback: ./.har/launch.sh ${id} # when har CLI/MCP unavailable" >&2
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
har_suggest_status() {
|
|
37
|
-
local id="${1:-}"
|
|
38
|
-
echo " Status: har env status # or har_get_status (MCP)" >&2
|
|
39
|
-
if [[ -n "$id" ]]; then
|
|
40
|
-
echo " Fallback: ./.har/agent-cli.sh ${id} status" >&2
|
|
41
|
-
fi
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
validate_agent_id() {
|
|
45
|
-
local id="${1:-}"
|
|
46
|
-
har_load_agent_slot_limits
|
|
47
|
-
local min="${HARNESS_AGENT_SLOT_MIN:-1}"
|
|
48
|
-
local max="${HARNESS_AGENT_SLOT_MAX:-}"
|
|
49
|
-
|
|
50
|
-
if [[ -z "$max" ]]; then
|
|
51
|
-
echo "Error: configure agentSlots in .har/stages.json or HARNESS_AGENT_SLOT_MAX in harness.env" >&2
|
|
52
|
-
exit 1
|
|
53
|
-
fi
|
|
54
|
-
|
|
55
|
-
if [[ -z "$id" ]] || ! [[ "$id" =~ ^[0-9]+$ ]]; then
|
|
56
|
-
echo "Error: invalid agent slot id: ${id:-"(missing)"}" >&2
|
|
57
|
-
echo " Must be an integer between ${min} and ${max} (see .har/stages.json → agentSlots)" >&2
|
|
58
|
-
exit 1
|
|
59
|
-
fi
|
|
60
|
-
|
|
61
|
-
if (( id < min || id > max )); then
|
|
62
|
-
echo "Error: invalid agent slot id: ${id}" >&2
|
|
63
|
-
echo " Valid slots: ${min}–${max} (configured in .har/stages.json → agentSlots)" >&2
|
|
64
|
-
echo " Run: har env status # see which slots are in use" >&2
|
|
65
|
-
if (( id > max )); then
|
|
66
|
-
echo " To allow slot ${id}, raise agentSlots.max in .har/stages.json." >&2
|
|
67
|
-
fi
|
|
68
|
-
exit 1
|
|
69
|
-
fi
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
# ── Port allocation ─────────────────────────────────────────────────────────────
|
|
73
|
-
# Try the configured default first; scan the slot lane or infra range when busy.
|
|
74
|
-
|
|
75
|
-
port_in_use() {
|
|
76
|
-
local port="$1"
|
|
77
|
-
(exec 3<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null && { exec 3>&- || true; return 0; }
|
|
78
|
-
return 1
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
har_port_step() {
|
|
82
|
-
echo "${HARNESS_PORT_STEP:-10}"
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
har_default_app_port() {
|
|
86
|
-
local base="$1"
|
|
87
|
-
local agent_id="$2"
|
|
88
|
-
echo $(( base + agent_id * $(har_port_step) ))
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
har_slot_port_lane_end() {
|
|
92
|
-
local default_port="$1"
|
|
93
|
-
echo $(( default_port + $(har_port_step) - 1 ))
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
# har_pick_free_port <start> <end> — echoes the first free port in [start, end].
|
|
97
|
-
har_pick_free_port() {
|
|
98
|
-
local start="$1"
|
|
99
|
-
local end="$2"
|
|
100
|
-
local port
|
|
101
|
-
for ((port=start; port<=end; port++)); do
|
|
102
|
-
if ! port_in_use "$port"; then
|
|
103
|
-
echo "$port"
|
|
104
|
-
return 0
|
|
105
|
-
fi
|
|
106
|
-
done
|
|
107
|
-
echo "Error: no free port in range ${start}-${end}" >&2
|
|
108
|
-
return 1
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
# har_allocate_port <default> <scan_start> <scan_end>
|
|
112
|
-
har_allocate_port() {
|
|
113
|
-
local default_port="$1"
|
|
114
|
-
local scan_start="$2"
|
|
115
|
-
local scan_end="$3"
|
|
116
|
-
if ! port_in_use "$default_port"; then
|
|
117
|
-
echo "$default_port"
|
|
118
|
-
return 0
|
|
119
|
-
fi
|
|
120
|
-
har_pick_free_port "$scan_start" "$scan_end"
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
# Load persisted infra host ports from .har/state/infra.env (written by setup-infra.sh).
|
|
124
|
-
# Optional repo_root: when launching from a worktree, infra may live in the main checkout.
|
|
125
|
-
har_load_infra_state() {
|
|
126
|
-
local repo_root="${1:-${REPO_ROOT:-}}"
|
|
127
|
-
local infra_state="$SCRIPT_DIR/state/infra.env"
|
|
128
|
-
if [ ! -f "$infra_state" ] && [ -n "$repo_root" ]; then
|
|
129
|
-
local common_git_dir
|
|
130
|
-
common_git_dir="$(git -C "$repo_root" rev-parse --git-common-dir 2>/dev/null || true)"
|
|
131
|
-
if [ -n "$common_git_dir" ]; then
|
|
132
|
-
infra_state="$(cd "$(dirname "$common_git_dir")" && pwd)/.har/state/infra.env"
|
|
133
|
-
fi
|
|
134
|
-
fi
|
|
135
|
-
if [ -f "$infra_state" ]; then
|
|
136
|
-
# shellcheck source=/dev/null
|
|
137
|
-
source "$infra_state"
|
|
138
|
-
fi
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
# Allocate FE/API/DEBUG ports for a slot. Sets FE_PORT, API_PORT, DEBUG_PORT.
|
|
142
|
-
har_allocate_slot_app_ports() {
|
|
143
|
-
local agent_id="$1"
|
|
144
|
-
local step fe_default api_default debug_default
|
|
145
|
-
step="$(har_port_step)"
|
|
146
|
-
fe_default="$(har_default_app_port "${HARNESS_FE_BASE_PORT}" "$agent_id")"
|
|
147
|
-
api_default="$(har_default_app_port "${HARNESS_API_BASE_PORT}" "$agent_id")"
|
|
148
|
-
debug_default=$(( 9200 + agent_id * step ))
|
|
149
|
-
|
|
150
|
-
FE_PORT="$(har_allocate_port "$fe_default" "$fe_default" "$(har_slot_port_lane_end "$fe_default")")"
|
|
151
|
-
API_PORT="$(har_allocate_port "$api_default" "$api_default" "$(har_slot_port_lane_end "$api_default")")"
|
|
152
|
-
DEBUG_PORT="$(har_allocate_port "$debug_default" "$debug_default" $(( debug_default + step - 1 )) )"
|
|
153
|
-
export FE_PORT API_PORT DEBUG_PORT
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
# ── Launch preflight (#36) ─────────────────────────────────────────────────────
|
|
157
|
-
# Readiness gate before worktree/install — ports, foreign PM2, Docker conflicts.
|
|
158
|
-
|
|
159
|
-
har_harness_uses_pm2() {
|
|
160
|
-
[ -f "${SCRIPT_DIR}/ecosystem.agent.template.cjs" ]
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
har_port_docker_occupant() {
|
|
164
|
-
local port="$1"
|
|
165
|
-
docker ps --format '{{.Names}}\t{{.Ports}}' 2>/dev/null \
|
|
166
|
-
| grep -E ":${port}->|:${port}/" | head -1 | cut -f1 || true
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
# Detect port conflicts with `har control up` (Docker dashboard) — not project-specific;
|
|
170
|
-
# warns when the customer's packaged Mission Control container shares a slot app port.
|
|
171
|
-
har_check_control_port_conflict() {
|
|
172
|
-
local port="$1"
|
|
173
|
-
local name
|
|
174
|
-
name="$(docker ps --format '{{.Names}}\t{{.Ports}}' 2>/dev/null \
|
|
175
|
-
| grep -i control | grep -E ":${port}->|:${port}/" | head -1 | cut -f1 || true)"
|
|
176
|
-
if [ -n "$name" ]; then
|
|
177
|
-
echo "ERROR: har control up (container \"${name}\") occupies port ${port}." >&2
|
|
178
|
-
echo " Run: har control down — or use a different agent slot." >&2
|
|
179
|
-
return 1
|
|
180
|
-
fi
|
|
181
|
-
return 0
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
har_warn_control_on_default_port() {
|
|
185
|
-
local agent_id="$1"
|
|
186
|
-
har_harness_uses_pm2 || return 0
|
|
187
|
-
local default_port
|
|
188
|
-
default_port="$(har_default_app_port "${HARNESS_FE_BASE_PORT:-3000}" "$agent_id")"
|
|
189
|
-
if [ "$FE_PORT" = "$default_port" ]; then
|
|
190
|
-
return 0
|
|
191
|
-
fi
|
|
192
|
-
local name
|
|
193
|
-
name="$(docker ps --format '{{.Names}}\t{{.Ports}}' 2>/dev/null \
|
|
194
|
-
| grep -i control | grep -E ":${default_port}->|:${default_port}/" | head -1 | cut -f1 || true)"
|
|
195
|
-
if [ -n "$name" ]; then
|
|
196
|
-
echo "WARN: har control up holds port ${default_port} (container \"${name}\")." >&2
|
|
197
|
-
echo " Harness will use port ${FE_PORT} instead. Run: har control down — to reclaim the default port." >&2
|
|
198
|
-
fi
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
har_check_foreign_pm2() {
|
|
202
|
-
local agent_id="$1"
|
|
203
|
-
local pm2_raw
|
|
204
|
-
har_harness_uses_pm2 || return 0
|
|
205
|
-
pm2_raw="$($(har_pkg_exec) pm2 jlist 2>/dev/null || true)"
|
|
206
|
-
[ -n "$pm2_raw" ] || return 0
|
|
207
|
-
set +e
|
|
208
|
-
echo "$pm2_raw" | node -e "
|
|
209
|
-
const agentId = process.argv[1];
|
|
210
|
-
const project = process.argv[2];
|
|
211
|
-
const slotPrefix = 'har-' + project + '-agent-' + agentId + '-';
|
|
212
|
-
const legacyPrefix = 'agent-' + agentId + '-';
|
|
213
|
-
let raw = '';
|
|
214
|
-
process.stdin.on('data', c => raw += c);
|
|
215
|
-
process.stdin.on('end', () => {
|
|
216
|
-
try {
|
|
217
|
-
const arr = JSON.parse(raw);
|
|
218
|
-
if (!Array.isArray(arr)) process.exit(0);
|
|
219
|
-
const foreign = arr.filter(x =>
|
|
220
|
-
x.name && (
|
|
221
|
-
(x.name.startsWith('har-') && x.name.includes('-agent-' + agentId + '-') && !x.name.startsWith(slotPrefix)) ||
|
|
222
|
-
(x.name.startsWith(legacyPrefix) && !x.name.startsWith('har-'))
|
|
223
|
-
));
|
|
224
|
-
if (foreign.length === 0) process.exit(0);
|
|
225
|
-
console.error('ERROR: foreign PM2 processes match agent ' + agentId + ':');
|
|
226
|
-
foreign.forEach(p => {
|
|
227
|
-
const cwd = p.pm2_env?.pm_cwd || p.pm2_env?.cwd || 'unknown';
|
|
228
|
-
console.error(' ' + p.name + ' cwd=' + cwd);
|
|
229
|
-
});
|
|
230
|
-
console.error(' Stop the other harness session or use a different slot.');
|
|
231
|
-
process.exit(1);
|
|
232
|
-
} catch {
|
|
233
|
-
process.exit(0);
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
" "$agent_id" "$HARNESS_PROJECT_NAME"
|
|
237
|
-
local rc=$?
|
|
238
|
-
set -e
|
|
239
|
-
return "$rc"
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
# ── Resume failed launch (#38) ───────────────────────────────────────────────
|
|
243
|
-
|
|
244
|
-
slot_session_status() {
|
|
245
|
-
local agent_id="$1"
|
|
246
|
-
read_slot_field "$(slot_registry_file "$agent_id")" status || true
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
# Exit 0 when the slot registry records a partial launch (failed or starting).
|
|
250
|
-
slot_is_resumable() {
|
|
251
|
-
local agent_id="$1"
|
|
252
|
-
local status
|
|
253
|
-
status="$(slot_session_status "$agent_id")"
|
|
254
|
-
[ "$status" = "failed" ] || [ "$status" = "starting" ]
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
# Echo shell assignments for launch.sh — eval after validating resumability.
|
|
258
|
-
har_resume_session_assignments() {
|
|
259
|
-
local agent_id="$1"
|
|
260
|
-
local reg work_dir worktree branch suffix base_branch base_commit mode env_file
|
|
261
|
-
|
|
262
|
-
if ! slot_is_resumable "$agent_id"; then
|
|
263
|
-
local status
|
|
264
|
-
status="$(slot_session_status "$agent_id")"
|
|
265
|
-
echo "echo \"ERROR: slot ${agent_id} is not resumable (status=${status:-none}; need failed or starting).\" >&2" >&2
|
|
266
|
-
echo "echo \" Free the slot first: har env teardown ${agent_id} (or complete ${agent_id}), then har env launch ${agent_id}.\" >&2"
|
|
267
|
-
echo 'exit 2'
|
|
268
|
-
return 1
|
|
269
|
-
fi
|
|
270
|
-
|
|
271
|
-
reg="$(slot_registry_file "$agent_id")"
|
|
272
|
-
work_dir="$(read_slot_field "$reg" workDir || true)"
|
|
273
|
-
worktree="$(read_slot_field "$reg" worktreePath || true)"
|
|
274
|
-
branch="$(read_slot_field "$reg" branch || true)"
|
|
275
|
-
suffix="$(read_slot_field "$reg" suffix || true)"
|
|
276
|
-
base_branch="$(read_slot_field "$reg" baseBranch || true)"
|
|
277
|
-
base_commit="$(read_slot_field "$reg" baseCommit || true)"
|
|
278
|
-
mode="$(read_slot_field "$reg" mode || true)"
|
|
279
|
-
|
|
280
|
-
if [ -z "$work_dir" ] || [ ! -d "$work_dir" ]; then
|
|
281
|
-
echo "echo \"ERROR: resume requires work dir from registry (missing or not found).\" >&2"
|
|
282
|
-
echo 'exit 1'
|
|
283
|
-
return 1
|
|
284
|
-
fi
|
|
285
|
-
|
|
286
|
-
env_file="$work_dir/.env.agent.${agent_id}"
|
|
287
|
-
if [ ! -f "$env_file" ]; then
|
|
288
|
-
echo "echo \"ERROR: resume requires env file: ${env_file}\" >&2"
|
|
289
|
-
echo 'exit 1'
|
|
290
|
-
return 1
|
|
291
|
-
fi
|
|
292
|
-
|
|
293
|
-
printf '%s\n' \
|
|
294
|
-
"WORK_DIR='${work_dir}'" \
|
|
295
|
-
"WORKTREE_DIR='${worktree}'" \
|
|
296
|
-
"BRANCH='${branch}'" \
|
|
297
|
-
"SUFFIX='${suffix}'" \
|
|
298
|
-
"BASE_BRANCH='${base_branch}'" \
|
|
299
|
-
"BASE_COMMIT='${base_commit}'" \
|
|
300
|
-
"USE_WORKTREE=$([ "$mode" = worktree ] && echo true || echo false)" \
|
|
301
|
-
"ENV_FILE='${env_file}'"
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
# Skip provision-toolchain when deps were installed before the failed step.
|
|
305
|
-
har_toolchain_ready() {
|
|
306
|
-
local work_dir="$1"
|
|
307
|
-
if [ -f "$work_dir/package.json" ] && [ -d "$work_dir/node_modules" ]; then
|
|
308
|
-
return 0
|
|
309
|
-
fi
|
|
310
|
-
if [ -d "$work_dir/.har/venv" ]; then
|
|
311
|
-
return 0
|
|
312
|
-
fi
|
|
313
|
-
return 1
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
# Rewrite agent env from env.template when ports changed during resume preflight.
|
|
317
|
-
har_regenerate_agent_env_file() {
|
|
318
|
-
local agent_id="$1"
|
|
319
|
-
local work_dir="$2"
|
|
320
|
-
local env_file="$3"
|
|
321
|
-
local template="${SCRIPT_DIR}/env.template"
|
|
322
|
-
if [ ! -f "$template" ]; then
|
|
323
|
-
return 0
|
|
324
|
-
fi
|
|
325
|
-
har_load_infra_state "${work_dir:-${REPO_ROOT:-}}"
|
|
326
|
-
AGENT_ID="$agent_id" \
|
|
327
|
-
API_PORT="$API_PORT" \
|
|
328
|
-
FE_PORT="$FE_PORT" \
|
|
329
|
-
DEBUG_PORT="$DEBUG_PORT" \
|
|
330
|
-
DB_PORT="${DB_PORT:-${AGENT_DB_PORT:-${HARNESS_DB_PORT_DEFAULT:-15432}}}" \
|
|
331
|
-
MINIO_PORT="${MINIO_PORT:-${AGENT_MINIO_PORT:-${HARNESS_MINIO_PORT_DEFAULT:-19000}}}" \
|
|
332
|
-
BROWSER_PORT="${BROWSER_PORT:-${AGENT_BROWSER_PORT:-${HARNESS_BROWSER_PORT_DEFAULT:-13001}}}" \
|
|
333
|
-
REPO_ROOT="$work_dir" \
|
|
334
|
-
envsubst '${AGENT_ID} ${API_PORT} ${FE_PORT} ${DEBUG_PORT} ${DB_PORT} ${MINIO_PORT} ${BROWSER_PORT} ${REPO_ROOT}' \
|
|
335
|
-
< "$template" > "$env_file"
|
|
336
|
-
if command -v har >/dev/null 2>&1; then
|
|
337
|
-
har telemetry write-env \
|
|
338
|
-
--agent-id "$agent_id" \
|
|
339
|
-
--repo "${HARNESS_ROOT:-$REPO_ROOT}" \
|
|
340
|
-
--env-file "$env_file" \
|
|
341
|
-
--work-dir "$work_dir" \
|
|
342
|
-
${SLOT_BRANCH:+--branch "$SLOT_BRANCH"} \
|
|
343
|
-
${SLOT_SUFFIX:+--suffix "$SLOT_SUFFIX"} \
|
|
344
|
-
>/dev/null 2>&1 || true
|
|
345
|
-
fi
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
# Untracked (not ignored) paths stay in the main checkout when a session
|
|
349
|
-
# worktree is created from HEAD. Advisory — never fails the launch.
|
|
350
|
-
har_warn_untracked_worktree() {
|
|
351
|
-
case "${USE_WORKTREE:-${HARNESS_USE_WORKTREE:-true}}" in
|
|
352
|
-
true|TRUE|yes|YES|1) ;;
|
|
353
|
-
*) return 0 ;;
|
|
354
|
-
esac
|
|
355
|
-
local root="${REPO_ROOT:-}"
|
|
356
|
-
if [ -z "$root" ] && [ -n "${SCRIPT_DIR:-}" ]; then
|
|
357
|
-
root="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
358
|
-
fi
|
|
359
|
-
[ -n "$root" ] || return 0
|
|
360
|
-
git -C "$root" rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0
|
|
361
|
-
|
|
362
|
-
local paths=()
|
|
363
|
-
local entry
|
|
364
|
-
while IFS= read -r -d '' entry; do
|
|
365
|
-
paths+=("$entry")
|
|
366
|
-
done < <(git -C "$root" --literal-pathspecs ls-files --others --exclude-standard --directory -z 2>/dev/null || true)
|
|
367
|
-
|
|
368
|
-
local count="${#paths[@]}"
|
|
369
|
-
[ "$count" -gt 0 ] || return 0
|
|
370
|
-
|
|
371
|
-
local max=8
|
|
372
|
-
local listed=""
|
|
373
|
-
local i
|
|
374
|
-
for ((i = 0; i < count && i < max; i++)); do
|
|
375
|
-
[ -n "$listed" ] && listed+=", "
|
|
376
|
-
listed+="${paths[$i]}"
|
|
377
|
-
done
|
|
378
|
-
if [ "$count" -gt "$max" ]; then
|
|
379
|
-
listed+=" (+$((count - max)) more)"
|
|
380
|
-
fi
|
|
381
|
-
|
|
382
|
-
local noun="paths"
|
|
383
|
-
local them="They are"
|
|
384
|
-
if [ "$count" -eq 1 ]; then
|
|
385
|
-
noun="path"
|
|
386
|
-
them="It is"
|
|
387
|
-
fi
|
|
388
|
-
echo "WARN: ${count} untracked ${noun} will not appear in the session worktree: ${listed}. ${them} only in the main checkout — track them, or launch with --no-worktree." >&2
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
# har_launch_preflight <agent_id> [resume]
|
|
392
|
-
# Exit 0 when ready; 2 when occupied (free the slot first) or not resumable; 1 on machine blockers.
|
|
393
|
-
# Sets FE_PORT/API_PORT/DEBUG_PORT when this harness uses PM2.
|
|
394
|
-
har_launch_preflight() {
|
|
395
|
-
local agent_id="$1"
|
|
396
|
-
local resume="${2:-false}"
|
|
397
|
-
|
|
398
|
-
if [ "$resume" = true ]; then
|
|
399
|
-
if ! slot_is_resumable "$agent_id"; then
|
|
400
|
-
local status
|
|
401
|
-
status="$(slot_session_status "$agent_id")"
|
|
402
|
-
echo "ERROR: slot ${agent_id} is not resumable (status=${status:-none}; need failed or starting)." >&2
|
|
403
|
-
echo " Free the slot first: har env teardown ${agent_id} (or complete ${agent_id}), then har env launch ${agent_id}." >&2
|
|
404
|
-
return 2
|
|
405
|
-
fi
|
|
406
|
-
echo "==> [agent-${agent_id}] Resuming partial launch (worktree and deps preserved)..." >&2
|
|
407
|
-
elif slot_is_occupied "$agent_id"; then
|
|
408
|
-
print_slot_occupied_warning "$agent_id"
|
|
409
|
-
echo "ERROR: slot ${agent_id} is occupied." >&2
|
|
410
|
-
echo " Free it first: har env teardown ${agent_id} (or complete ${agent_id}), then har env launch ${agent_id}." >&2
|
|
411
|
-
return 2
|
|
412
|
-
fi
|
|
413
|
-
|
|
414
|
-
if har_harness_uses_pm2; then
|
|
415
|
-
har_check_foreign_pm2 "$agent_id" || return 1
|
|
416
|
-
har_allocate_slot_app_ports "$agent_id" || return 1
|
|
417
|
-
har_warn_control_on_default_port "$agent_id"
|
|
418
|
-
local port occupant
|
|
419
|
-
for port in $(printf '%s\n' "$FE_PORT" "$API_PORT" | sort -u); do
|
|
420
|
-
har_check_control_port_conflict "$port" || return 1
|
|
421
|
-
occupant="$(har_port_docker_occupant "$port")"
|
|
422
|
-
if [ -n "$occupant" ] && [[ "$occupant" != har-${HARNESS_PROJECT_NAME}-* ]]; then
|
|
423
|
-
echo "ERROR: Docker container \"${occupant}\" binds port ${port}." >&2
|
|
424
|
-
echo " Stop it with: docker stop ${occupant}" >&2
|
|
425
|
-
return 1
|
|
426
|
-
fi
|
|
427
|
-
done
|
|
428
|
-
fi
|
|
429
|
-
har_warn_untracked_worktree
|
|
430
|
-
return 0
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
# Load persisted app/infra ports from .env.agent.<id> or the slot registry.
|
|
434
|
-
load_agent_ports() {
|
|
435
|
-
local agent_id="$1"
|
|
436
|
-
local repo_root="$2"
|
|
437
|
-
local env_file reg ports_json
|
|
438
|
-
env_file="$(resolve_agent_env_file "$agent_id" "$repo_root" 2>/dev/null || true)"
|
|
439
|
-
if [ -n "$env_file" ] && [ -f "$env_file" ]; then
|
|
440
|
-
# shellcheck source=/dev/null
|
|
441
|
-
source "$env_file"
|
|
442
|
-
FE_PORT="${FE_PORT:-}"
|
|
443
|
-
API_PORT="${API_PORT:-${PORT:-}}"
|
|
444
|
-
DEBUG_PORT="${DEBUG_PORT:-}"
|
|
445
|
-
DB_PORT="${PGPORT:-${DB_PORT:-}}"
|
|
446
|
-
return 0
|
|
447
|
-
fi
|
|
448
|
-
reg="$(slot_registry_file "$agent_id")"
|
|
449
|
-
if [ -f "$reg" ]; then
|
|
450
|
-
ports_json="$(node -e '
|
|
451
|
-
try {
|
|
452
|
-
const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).ports;
|
|
453
|
-
if (p) process.stdout.write(JSON.stringify(p));
|
|
454
|
-
} catch {}
|
|
455
|
-
' "$reg" 2>/dev/null || true)"
|
|
456
|
-
if [ -n "$ports_json" ]; then
|
|
457
|
-
FE_PORT="$(node -e "const p=JSON.parse(process.argv[1]);process.stdout.write(String(p.frontend??''))" "$ports_json")"
|
|
458
|
-
API_PORT="$(node -e "const p=JSON.parse(process.argv[1]);process.stdout.write(String(p.api??''))" "$ports_json")"
|
|
459
|
-
DEBUG_PORT="$(node -e "const p=JSON.parse(process.argv[1]);process.stdout.write(String(p.debug??''))" "$ports_json")"
|
|
460
|
-
DB_PORT="$(node -e "const p=JSON.parse(process.argv[1]);process.stdout.write(String(p.db??''))" "$ports_json")"
|
|
461
|
-
export FE_PORT API_PORT DEBUG_PORT DB_PORT
|
|
462
|
-
return 0
|
|
463
|
-
fi
|
|
464
|
-
fi
|
|
465
|
-
har_allocate_slot_app_ports "$agent_id"
|
|
466
|
-
har_load_infra_state "$repo_root"
|
|
467
|
-
DB_PORT="${AGENT_DB_PORT:-${HARNESS_DB_PORT_DEFAULT:-15432}}"
|
|
468
|
-
export FE_PORT API_PORT DEBUG_PORT DB_PORT
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
# ── Project-scoped PM2 names ────────────────────────────────────────────────────
|
|
472
|
-
# Pattern: har-<project>-agent-<id>-<service> (machine-global PM2 namespace).
|
|
473
|
-
|
|
474
|
-
har_pm2_slot_prefix() {
|
|
475
|
-
echo "har-${HARNESS_PROJECT_NAME}-agent-${1}"
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
har_pm2_delete_regex() {
|
|
479
|
-
echo "/^har-${HARNESS_PROJECT_NAME}-agent-${1}-/"
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
har_tmux_session() {
|
|
483
|
-
echo "har-${HARNESS_PROJECT_NAME}-agent-${1}"
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
# ── Slot registry ─────────────────────────────────────────────────────────────
|
|
487
|
-
# .har/slots/agent-<id>.json is the source of truth for where a session lives
|
|
488
|
-
# (worktree path, work dir, branch, base commit). Written by launch.sh, removed
|
|
489
|
-
# by teardown.sh. Location resolution must go through it — worktree paths carry
|
|
490
|
-
# a random per-session suffix and cannot be derived from the agent id alone.
|
|
491
|
-
|
|
492
|
-
slot_registry_file() {
|
|
493
|
-
echo "${SCRIPT_DIR}/slots/agent-${1}.json"
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
# read_slot_field <registry-file> <field> — echoes the scalar value, empty if absent.
|
|
497
|
-
read_slot_field() {
|
|
498
|
-
[ -f "${1:-}" ] || return 1
|
|
499
|
-
node -e '
|
|
500
|
-
try {
|
|
501
|
-
const v = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"))[process.argv[2]];
|
|
502
|
-
if (v != null && typeof v !== "object") process.stdout.write(String(v));
|
|
503
|
-
} catch {}
|
|
504
|
-
' "$1" "$2"
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
# Writes the registry entry from SLOT_* variables:
|
|
508
|
-
# required: SLOT_AGENT_ID, SLOT_MODE (worktree|root), SLOT_WORK_DIR
|
|
509
|
-
# optional: SLOT_SUFFIX, SLOT_WORKTREE_PATH, SLOT_BRANCH, SLOT_BASE_BRANCH,
|
|
510
|
-
# SLOT_BASE_COMMIT, SLOT_PORTS_JSON, SLOT_PREVIEW_URLS_JSON,
|
|
511
|
-
# SLOT_STATUS, SLOT_LAST_ERROR, SLOT_WORK_UNIT_ID, SLOT_ATTEMPT_ID
|
|
512
|
-
# Mission Control re-reads slot state only when something syncs, and a sync takes
|
|
513
|
-
# about twenty seconds — too long to make a slot change wait for it. Detached, and
|
|
514
|
-
# coalesced so a run of slot changes leaves one sync going, then one more if needed.
|
|
515
|
-
har_notify_control() {
|
|
516
|
-
local root bin lock
|
|
517
|
-
root="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
518
|
-
bin="$(command -v har 2>/dev/null || echo "${root}/node_modules/.bin/har")"
|
|
519
|
-
[ -x "$bin" ] || return 0
|
|
520
|
-
|
|
521
|
-
lock="${TMPDIR:-/tmp}/har-control-sync.$(id -u)"
|
|
522
|
-
: > "${lock}.pending"
|
|
523
|
-
mkdir "$lock" 2>/dev/null || return 0
|
|
524
|
-
(
|
|
525
|
-
trap "" HUP
|
|
526
|
-
trap 'rmdir "$lock" 2>/dev/null || true' EXIT
|
|
527
|
-
cd "$root" || exit 0
|
|
528
|
-
while [ -f "${lock}.pending" ]; do
|
|
529
|
-
rm -f "${lock}.pending"
|
|
530
|
-
if command -v timeout >/dev/null 2>&1; then
|
|
531
|
-
timeout 120 "$bin" control sync || true
|
|
532
|
-
else
|
|
533
|
-
"$bin" control sync || true
|
|
534
|
-
fi
|
|
535
|
-
done
|
|
536
|
-
# Redirected as a whole, not per command: a background job that keeps the inherited
|
|
537
|
-
# stdout open makes any caller reading that pipe wait for it.
|
|
538
|
-
) >/dev/null 2>&1 &
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
write_slot_registry() {
|
|
542
|
-
local file
|
|
543
|
-
file="$(slot_registry_file "$SLOT_AGENT_ID")"
|
|
544
|
-
mkdir -p "$(dirname "$file")"
|
|
545
|
-
node -e '
|
|
546
|
-
const fs = require("fs");
|
|
547
|
-
const e = process.env;
|
|
548
|
-
const entry = {
|
|
549
|
-
version: 1,
|
|
550
|
-
agentId: Number(e.SLOT_AGENT_ID),
|
|
551
|
-
projectName: e.HARNESS_PROJECT_NAME || "",
|
|
552
|
-
mode: e.SLOT_MODE,
|
|
553
|
-
workDir: e.SLOT_WORK_DIR,
|
|
554
|
-
createdAt: new Date().toISOString(),
|
|
555
|
-
status: e.SLOT_STATUS || "active",
|
|
556
|
-
};
|
|
557
|
-
if (e.SLOT_SUFFIX) entry.suffix = e.SLOT_SUFFIX;
|
|
558
|
-
if (e.SLOT_WORKTREE_PATH) entry.worktreePath = e.SLOT_WORKTREE_PATH;
|
|
559
|
-
if (e.SLOT_BRANCH) entry.branch = e.SLOT_BRANCH;
|
|
560
|
-
if (e.SLOT_BASE_BRANCH) entry.baseBranch = e.SLOT_BASE_BRANCH;
|
|
561
|
-
if (e.SLOT_BASE_COMMIT) entry.baseCommit = e.SLOT_BASE_COMMIT;
|
|
562
|
-
if (e.SLOT_LAST_ERROR) entry.lastError = e.SLOT_LAST_ERROR;
|
|
563
|
-
if (e.SLOT_WORK_UNIT_ID) entry.workUnitId = e.SLOT_WORK_UNIT_ID;
|
|
564
|
-
if (e.SLOT_ATTEMPT_ID) entry.attemptId = e.SLOT_ATTEMPT_ID;
|
|
565
|
-
for (const [key, env] of [["ports", "SLOT_PORTS_JSON"], ["previewUrls", "SLOT_PREVIEW_URLS_JSON"]]) {
|
|
566
|
-
if (e[env]) try { entry[key] = JSON.parse(e[env]); } catch {}
|
|
567
|
-
}
|
|
568
|
-
fs.writeFileSync(process.argv[1], JSON.stringify(entry, null, 2) + "\n");
|
|
569
|
-
' "$file"
|
|
570
|
-
har_notify_control
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
remove_slot_registry() {
|
|
574
|
-
rm -f "$(slot_registry_file "$1")"
|
|
575
|
-
har_notify_control
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
# Exit 0 when the worktree has uncommitted or untracked changes.
|
|
579
|
-
slot_worktree_dirty() {
|
|
580
|
-
[ -d "${1:-}" ] || return 1
|
|
581
|
-
[ -n "$(git -C "$1" status --porcelain 2>/dev/null)" ]
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
# Exit 0 when a slot registry entry or worktree path exists for this agent id.
|
|
585
|
-
slot_is_occupied() {
|
|
586
|
-
local agent_id="$1"
|
|
587
|
-
[ -f "$(slot_registry_file "$agent_id")" ] || [ -n "$(existing_slot_worktree "$agent_id")" ]
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
# Echo "clean" or "dirty (N changed)" for a worktree path.
|
|
591
|
-
slot_dirty_summary() {
|
|
592
|
-
local wt="${1:-}"
|
|
593
|
-
if [ -z "$wt" ] || [ ! -d "$wt" ]; then
|
|
594
|
-
echo "unknown"
|
|
595
|
-
return 0
|
|
596
|
-
fi
|
|
597
|
-
if ! slot_worktree_dirty "$wt"; then
|
|
598
|
-
echo "clean"
|
|
599
|
-
return 0
|
|
600
|
-
fi
|
|
601
|
-
local count
|
|
602
|
-
count="$(git -C "$wt" status --porcelain 2>/dev/null | wc -l | tr -d ' ')"
|
|
603
|
-
echo "dirty (${count} changed)"
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
# Print details about an occupied slot (stdout — visible to agents).
|
|
607
|
-
print_slot_occupied_warning() {
|
|
608
|
-
local agent_id="$1"
|
|
609
|
-
local reg wt branch work_dir created status last_error dirty_summary head
|
|
610
|
-
reg="$(slot_registry_file "$agent_id")"
|
|
611
|
-
wt="$(existing_slot_worktree "$agent_id")"
|
|
612
|
-
branch="$(read_slot_field "$reg" branch || true)"
|
|
613
|
-
work_dir="$(read_slot_field "$reg" workDir || true)"
|
|
614
|
-
created="$(read_slot_field "$reg" createdAt || true)"
|
|
615
|
-
status="$(read_slot_field "$reg" status || true)"
|
|
616
|
-
last_error="$(read_slot_field "$reg" lastError || true)"
|
|
617
|
-
dirty_summary="$(slot_dirty_summary "$wt")"
|
|
618
|
-
if [ -n "$wt" ] && [ -d "$wt" ]; then
|
|
619
|
-
head="$(git -C "$wt" rev-parse --short HEAD 2>/dev/null || true)"
|
|
620
|
-
fi
|
|
621
|
-
|
|
622
|
-
echo "" >&2
|
|
623
|
-
echo "⚠ Slot ${agent_id} is already in use." >&2
|
|
624
|
-
[ -n "$wt" ] && echo " Worktree: ${wt}" >&2
|
|
625
|
-
[ -n "$work_dir" ] && echo " Work dir: ${work_dir}" >&2
|
|
626
|
-
[ -n "$branch" ] && echo " Branch: ${branch}${head:+ @ ${head}}" >&2
|
|
627
|
-
[ -n "$status" ] && echo " Status: ${status}" >&2
|
|
628
|
-
[ -n "$last_error" ] && echo " Error: ${last_error}" >&2
|
|
629
|
-
[ -n "$created" ] && echo " Since: ${created}" >&2
|
|
630
|
-
echo " Git: ${dirty_summary}" >&2
|
|
631
|
-
echo "" >&2
|
|
632
|
-
local base_root="${HARNESS_ROOT:-${REPO_ROOT:-}}"
|
|
633
|
-
if [ -n "$base_root" ] && [ -d "$base_root" ]; then
|
|
634
|
-
local base_branch base_sha
|
|
635
|
-
base_branch="$(git -C "$base_root" rev-parse --abbrev-ref HEAD 2>/dev/null || echo detached)"
|
|
636
|
-
base_sha="$(git -C "$base_root" rev-parse --short HEAD 2>/dev/null || true)"
|
|
637
|
-
echo " New session will be based on: ${base_branch}${base_sha:+ @ ${base_sha}}" >&2
|
|
638
|
-
echo " (HEAD of ${base_root})" >&2
|
|
639
|
-
echo " Switch that checkout to your intended base before launching a new session." >&2
|
|
640
|
-
echo "" >&2
|
|
641
|
-
fi
|
|
642
|
-
if [ "$dirty_summary" != "clean" ] && [ "$dirty_summary" != "unknown" ]; then
|
|
643
|
-
echo " Commit or discard changes in the worktree, then teardown/complete the slot:" >&2
|
|
644
|
-
else
|
|
645
|
-
echo " Free the slot, then launch again:" >&2
|
|
646
|
-
fi
|
|
647
|
-
echo " har env complete ${agent_id} # or: har env teardown ${agent_id}" >&2
|
|
648
|
-
echo " har env launch ${agent_id}" >&2
|
|
649
|
-
echo "" >&2
|
|
650
|
-
echo " complete/teardown remove the worktree. The session branch is kept only if" >&2
|
|
651
|
-
echo " you committed. Gitignored paths (state/, runs/, local clones, .env.local)" >&2
|
|
652
|
-
echo " are NOT preserved." >&2
|
|
653
|
-
echo "" >&2
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
git_common_dir() {
|
|
657
|
-
local cwd="$1"
|
|
658
|
-
local out
|
|
659
|
-
out="$(git -C "$cwd" rev-parse --git-common-dir 2>/dev/null)" || return 1
|
|
660
|
-
case "$out" in
|
|
661
|
-
/*) echo "$out" ;;
|
|
662
|
-
*) (cd "$cwd" && cd "$out" && pwd) ;;
|
|
663
|
-
esac
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
same_git_checkout() {
|
|
667
|
-
local left right
|
|
668
|
-
left="$(git_common_dir "$1" || true)"
|
|
669
|
-
right="$(git_common_dir "$2" || true)"
|
|
670
|
-
[ -n "$left" ] && [ -n "$right" ] && [ "$left" = "$right" ]
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
# Echo the previous session's worktree path for a slot: registry first, then
|
|
674
|
-
# legacy and randomized session worktree fallbacks. Empty output when none exists.
|
|
675
|
-
existing_slot_worktree() {
|
|
676
|
-
local agent_id="$1"
|
|
677
|
-
local reg path candidate repo_root rel_prefix
|
|
678
|
-
repo_root="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
679
|
-
rel_prefix="$(git -C "$repo_root" rev-parse --show-prefix 2>/dev/null || true)"
|
|
680
|
-
reg="$(slot_registry_file "$agent_id")"
|
|
681
|
-
if [ -f "$reg" ]; then
|
|
682
|
-
path="$(read_slot_field "$reg" worktreePath || true)"
|
|
683
|
-
if [ -n "$path" ] && [ -d "$path" ]; then
|
|
684
|
-
echo "$path"
|
|
685
|
-
return 0
|
|
686
|
-
fi
|
|
687
|
-
fi
|
|
688
|
-
path="$HOME/worktrees/${HARNESS_PROJECT_NAME}-agent-${agent_id}"
|
|
689
|
-
if [ -d "$path" ]; then
|
|
690
|
-
echo "$path"
|
|
691
|
-
return 0
|
|
692
|
-
fi
|
|
693
|
-
for candidate in "$HOME"/worktrees/*-har-agent-"${agent_id}"-*; do
|
|
694
|
-
[ -d "$candidate" ] || continue
|
|
695
|
-
same_git_checkout "$repo_root" "$candidate" || continue
|
|
696
|
-
[ -f "$candidate/${rel_prefix}.env.agent.${agent_id}" ] || continue
|
|
697
|
-
echo "$candidate"
|
|
698
|
-
return 0
|
|
699
|
-
done
|
|
700
|
-
return 0
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
# Resolve .env.agent.<id> — registry work dir first, then legacy and
|
|
704
|
-
# randomized session worktree fallbacks.
|
|
705
|
-
resolve_agent_env_file() {
|
|
706
|
-
local agent_id="$1"
|
|
707
|
-
local repo_root="$2"
|
|
708
|
-
local reg work_dir
|
|
709
|
-
reg="$(slot_registry_file "$agent_id")"
|
|
710
|
-
if [ -f "$reg" ]; then
|
|
711
|
-
work_dir="$(read_slot_field "$reg" workDir || true)"
|
|
712
|
-
if [ -n "$work_dir" ] && [ -f "$work_dir/.env.agent.${agent_id}" ]; then
|
|
713
|
-
echo "$work_dir/.env.agent.${agent_id}"
|
|
714
|
-
return 0
|
|
715
|
-
fi
|
|
716
|
-
fi
|
|
717
|
-
# Worktrees are repo-rooted — if the project lives in a subdirectory (monorepo),
|
|
718
|
-
# the env file sits under that prefix inside the worktree.
|
|
719
|
-
local rel_prefix
|
|
720
|
-
rel_prefix="$(git -C "$repo_root" rev-parse --show-prefix 2>/dev/null || true)"
|
|
721
|
-
local candidate
|
|
722
|
-
for candidate in \
|
|
723
|
-
"$repo_root/.env.agent.${agent_id}" \
|
|
724
|
-
"$HOME/worktrees/${HARNESS_PROJECT_NAME}-agent-${agent_id}/${rel_prefix}.env.agent.${agent_id}"; do
|
|
725
|
-
if [ -f "$candidate" ]; then
|
|
726
|
-
echo "$candidate"
|
|
727
|
-
return 0
|
|
728
|
-
fi
|
|
729
|
-
done
|
|
730
|
-
local candidate_dir
|
|
731
|
-
for candidate in "$HOME"/worktrees/*-har-agent-"${agent_id}"-*/${rel_prefix}.env.agent."${agent_id}"; do
|
|
732
|
-
if [ -f "$candidate" ]; then
|
|
733
|
-
candidate_dir="$(cd "$(dirname "$candidate")" && pwd)"
|
|
734
|
-
same_git_checkout "$repo_root" "$candidate_dir" || continue
|
|
735
|
-
echo "$candidate"
|
|
736
|
-
return 0
|
|
737
|
-
fi
|
|
738
|
-
done
|
|
739
|
-
return 1
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
# Resolve work dir for verify/e2e — registry first, else the agent env file's
|
|
743
|
-
# variables (call after sourcing the env file). Optional 2nd arg: agent id.
|
|
744
|
-
resolve_agent_work_dir() {
|
|
745
|
-
local env_file="$1"
|
|
746
|
-
local agent_id="${2:-${AGENT_ID:-}}"
|
|
747
|
-
if [ -n "$agent_id" ]; then
|
|
748
|
-
local reg wd
|
|
749
|
-
reg="$(slot_registry_file "$agent_id")"
|
|
750
|
-
if [ -f "$reg" ]; then
|
|
751
|
-
wd="$(read_slot_field "$reg" workDir || true)"
|
|
752
|
-
if [ -n "$wd" ] && [ -d "$wd" ]; then
|
|
753
|
-
echo "$wd"
|
|
754
|
-
return 0
|
|
755
|
-
fi
|
|
756
|
-
fi
|
|
757
|
-
fi
|
|
758
|
-
local work_dir="${REPO_ROOT:-}"
|
|
759
|
-
if [ -z "$work_dir" ] || [ ! -d "$work_dir" ]; then
|
|
760
|
-
if [ -n "${WORKTREE_DIR:-}" ] && [ -d "$WORKTREE_DIR" ]; then
|
|
761
|
-
work_dir="$WORKTREE_DIR"
|
|
762
|
-
else
|
|
763
|
-
work_dir="$(cd "$(dirname "$env_file")" && pwd)"
|
|
764
|
-
fi
|
|
765
|
-
fi
|
|
766
|
-
echo "$work_dir"
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
# Portable millisecond clock (GNU date %N is unavailable on macOS/BSD).
|
|
770
|
-
now_ms() {
|
|
771
|
-
node -e 'process.stdout.write(String(Date.now()))' 2>/dev/null || echo 0
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
# JSON-escape step output; truncate to 50 lines in node (avoids SIGPIPE under pipefail).
|
|
775
|
-
escape_step_output() {
|
|
776
|
-
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 '""'
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
# Append one verify.sh step to RESULTS_JSON (caller global).
|
|
780
|
-
# Passing steps omit output — it already appeared as ✓ on stderr.
|
|
781
|
-
record_step_result() {
|
|
782
|
-
local name="$1"
|
|
783
|
-
local pass_bool="$2"
|
|
784
|
-
local elapsed="$3"
|
|
785
|
-
local output="${4:-}"
|
|
786
|
-
local step_json
|
|
787
|
-
if [ "$pass_bool" = "true" ]; then
|
|
788
|
-
step_json=$(NAME="$name" MS="$elapsed" node -e 'process.stdout.write(JSON.stringify({name:process.env.NAME,pass:true,ms:Number(process.env.MS)}))' 2>/dev/null) || return 0
|
|
789
|
-
else
|
|
790
|
-
local escaped
|
|
791
|
-
escaped=$(escape_step_output "$output")
|
|
792
|
-
step_json=$(NAME="$name" MS="$elapsed" node -e "process.stdout.write(JSON.stringify({name:process.env.NAME,pass:false,ms:Number(process.env.MS),output:$escaped}))" 2>/dev/null) || return 0
|
|
793
|
-
fi
|
|
794
|
-
RESULTS_JSON=$(echo "$RESULTS_JSON" | node -e "
|
|
795
|
-
const fs = require('fs');
|
|
796
|
-
const arr = JSON.parse(fs.readFileSync('/dev/stdin','utf8'));
|
|
797
|
-
arr.push($step_json);
|
|
798
|
-
process.stdout.write(JSON.stringify(arr));
|
|
799
|
-
" 2>/dev/null || echo "$RESULTS_JSON")
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
# Emit "<id>\t<command>" for each registered stage listed in stages.json
|
|
803
|
-
# verificationStages (used by verify --full). Ids without a matching registered
|
|
804
|
-
# stage are inline verify.sh steps and skipped here; lifecycle stages
|
|
805
|
-
# (setup/launch/reset/teardown/inspect) and the verify stage itself never run.
|
|
806
|
-
# Authoring contract: .har/STAGES.md
|
|
807
|
-
list_registered_verification_stage_commands() {
|
|
808
|
-
local script_dir="$1"
|
|
809
|
-
local agent_id="$2"
|
|
810
|
-
local registry="$script_dir/stages.json"
|
|
811
|
-
[ -f "$registry" ] || return 0
|
|
812
|
-
HAR_STAGE_REGISTRY="$registry" HAR_SCRIPT_DIR="$script_dir" HAR_AGENT_ID="$agent_id" node <<'NODE' 2>/dev/null || true
|
|
813
|
-
const fs = require('fs');
|
|
814
|
-
const { HAR_STAGE_REGISTRY, HAR_SCRIPT_DIR, HAR_AGENT_ID } = process.env;
|
|
815
|
-
const shq = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
|
|
816
|
-
let reg;
|
|
817
|
-
try { reg = JSON.parse(fs.readFileSync(HAR_STAGE_REGISTRY, 'utf8')); } catch { process.exit(0); }
|
|
818
|
-
const ids = Array.isArray(reg.verificationStages) ? reg.verificationStages : [];
|
|
819
|
-
const stages = Array.isArray(reg.stages) ? reg.stages : [];
|
|
820
|
-
const runnable = new Set(['test', 'custom']);
|
|
821
|
-
for (const id of ids) {
|
|
822
|
-
const stage = stages.find((s) => s && s.id === id);
|
|
823
|
-
if (!stage || stage.id === 'verify' || !runnable.has(stage.kind)) continue;
|
|
824
|
-
const needsAgent = stage.requiresAgentId !== false;
|
|
825
|
-
let cmd;
|
|
826
|
-
if (stage.script) {
|
|
827
|
-
cmd = shq(HAR_SCRIPT_DIR + '/' + stage.script) + (needsAgent ? ' ' + shq(HAR_AGENT_ID) : '');
|
|
828
|
-
} else if (stage.command) {
|
|
829
|
-
cmd = stage.command.split('{agentId}').join(HAR_AGENT_ID);
|
|
830
|
-
} else {
|
|
831
|
-
continue;
|
|
832
|
-
}
|
|
833
|
-
if (stage.cwd) cmd = 'cd ' + shq(stage.cwd) + ' && ' + cmd;
|
|
834
|
-
const env = stage.env && typeof stage.env === 'object' ? stage.env : {};
|
|
835
|
-
const prefix = Object.entries(env).map(([k, v]) => k + '=' + shq(v)).join(' ');
|
|
836
|
-
process.stdout.write(id + '\t' + (prefix ? prefix + ' ' : '') + cmd + '\n');
|
|
837
|
-
}
|
|
838
|
-
NODE
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
# Optional project-owned "agent usable" smoke beyond health.
|
|
842
|
-
run_readiness_if_configured() {
|
|
843
|
-
local agent_id="$1"
|
|
844
|
-
if [ -z "${HARNESS_READINESS_CMD:-}" ]; then
|
|
845
|
-
echo "No HARNESS_READINESS_CMD configured; skipping readiness smoke."
|
|
846
|
-
return 0
|
|
847
|
-
fi
|
|
848
|
-
local cmd="${HARNESS_READINESS_CMD//\{agentId\}/$agent_id}"
|
|
849
|
-
eval "$cmd"
|
|
850
|
-
}
|