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