@osfactory/har 0.64.2 → 1.0.0-rc.2

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 (142) hide show
  1. package/dist/index.js +15751 -6548
  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/adaptation-prompt-plugin.md +40 -0
  6. package/dist/templates/agent-skills/har-maintain.md +2 -2
  7. package/dist/templates/agent-skills/setup-har.md +6 -4
  8. package/dist/templates/cursor-rule.mdc.template +3 -3
  9. package/dist/templates/har-boilerplate/docker-compose.agent.yml +1 -1
  10. package/dist/templates/har-boilerplate/harness.env +11 -155
  11. package/dist/templates/har-boilerplate/stages.json +51 -2
  12. package/dist/templates/har-boilerplate-cli/docker-compose.agent.yml +1 -1
  13. package/dist/templates/har-boilerplate-cli/harness.env +12 -138
  14. package/dist/templates/har-boilerplate-cli/stages.json +42 -2
  15. package/dist/templates/har-boilerplate-ios/harness.env +10 -126
  16. package/dist/templates/har-boilerplate-ios/stages.json +42 -2
  17. package/dist/templates/migration-prompt.md +54 -0
  18. package/dist/templates/plugins/custom-stage-skeleton.sh +11 -20
  19. package/dist/templates/plugins/gitleaks/.github/workflows/gitleaks.yml +1 -1
  20. package/dist/templates/plugins/gitleaks/.har/stages/GITLEAKS.md +1 -1
  21. package/dist/templates/plugins/gitleaks/.har/stages/secrets-scan.sh +8 -23
  22. package/dist/templates/plugins/gitleaks/README.md +1 -1
  23. package/dist/templates/plugins/gitleaks/template.manifest.json +20 -5
  24. package/dist/templates/plugins/kerno/.github/workflows/kerno.yml +1 -1
  25. package/dist/templates/plugins/kerno/.har/stages/backend-validation.sh +8 -23
  26. package/dist/templates/plugins/kerno/README.md +1 -1
  27. package/dist/templates/plugins/kerno/template.manifest.json +20 -9
  28. package/dist/templates/plugins/local-plugin-skeleton/README.md +46 -0
  29. package/dist/templates/plugins/playwright/.har/stages/browser-e2e.sh +8 -23
  30. package/dist/templates/plugins/playwright/template.manifest.json +36 -13
  31. package/dist/templates/plugins/rocketsim/.har/stages/rocketsim-flows.sh +8 -23
  32. package/dist/templates/plugins/rocketsim/template.manifest.json +20 -8
  33. package/dist/templates/plugins/semgrep/.har/stages/sast.sh +10 -26
  34. package/dist/templates/plugins/semgrep/README.md +1 -1
  35. package/dist/templates/plugins/semgrep/template.manifest.json +16 -7
  36. package/dist/templates/plugins/trivy/.har/stages/vuln-scan.sh +8 -23
  37. package/dist/templates/plugins/trivy/README.md +1 -1
  38. package/dist/templates/plugins/trivy/template.manifest.json +20 -8
  39. package/dist/templates/{har-boilerplate-cli/CLAUDE.agent.md → profiles/cli/docs/claude-agent/definition-of-done.md} +0 -42
  40. package/dist/templates/profiles/cli/docs/claude-agent/do-not.md +6 -0
  41. package/dist/templates/profiles/cli/docs/claude-agent/environment.md +12 -0
  42. package/dist/templates/profiles/cli/docs/claude-agent/project-commands.md +10 -0
  43. package/dist/templates/profiles/cli/docs/claude-agent/readiness.md +6 -0
  44. package/dist/templates/profiles/cli/docs/readme/for-coding-agents.md +9 -0
  45. package/dist/templates/profiles/cli/docs/readme/intro.md +7 -0
  46. package/dist/templates/profiles/cli/docs/readme/port-shared-services-cli-profile.md +25 -0
  47. package/dist/templates/profiles/cli/docs/readme/quick-start.md +24 -0
  48. package/dist/templates/profiles/cli/docs/readme/verification-contract.md +22 -0
  49. package/dist/templates/profiles/cli/docs/readme/what-s-in-here.md +27 -0
  50. package/dist/templates/profiles/cli/profile.manifest.json +56 -1
  51. package/dist/templates/profiles/default/docs/claude-agent/definition-of-done.md +24 -0
  52. package/dist/templates/profiles/default/docs/claude-agent/do-not.md +10 -0
  53. package/dist/templates/profiles/default/docs/claude-agent/environment.md +19 -0
  54. package/dist/templates/profiles/default/docs/claude-agent/project-commands.md +10 -0
  55. package/dist/templates/profiles/default/docs/claude-agent/readiness.md +12 -0
  56. package/dist/templates/profiles/default/docs/readme/architecture.md +11 -0
  57. package/dist/templates/profiles/default/docs/readme/for-coding-agents.md +9 -0
  58. package/dist/templates/profiles/default/docs/readme/intro.md +7 -0
  59. package/dist/templates/profiles/default/docs/readme/port-shared-services.md +41 -0
  60. package/dist/templates/profiles/default/docs/readme/quick-start.md +24 -0
  61. package/dist/templates/profiles/default/docs/readme/readiness-layers.md +16 -0
  62. package/dist/templates/profiles/default/docs/readme/verification-contract.md +17 -0
  63. package/dist/templates/profiles/default/docs/readme/what-s-in-here.md +28 -0
  64. package/dist/templates/profiles/default/profile.manifest.json +62 -1
  65. package/dist/templates/profiles/ios/docs/claude-agent/definition-of-done.md +23 -0
  66. package/dist/templates/profiles/ios/docs/claude-agent/do-not.md +6 -0
  67. package/dist/templates/profiles/ios/docs/claude-agent/environment.md +14 -0
  68. package/dist/templates/profiles/ios/docs/claude-agent/intro.md +3 -0
  69. package/dist/templates/profiles/ios/docs/claude-agent/project-commands.md +21 -0
  70. package/dist/templates/profiles/ios/docs/claude-agent/readiness.md +6 -0
  71. package/dist/templates/profiles/ios/docs/claude-agent/user-flow-validation-rocketsim.md +11 -0
  72. package/dist/templates/profiles/ios/docs/readme/configuration.md +7 -0
  73. package/dist/templates/profiles/ios/docs/readme/intro.md +7 -0
  74. package/dist/templates/profiles/ios/docs/readme/port-shared-services-ios-profile.md +25 -0
  75. package/dist/templates/profiles/ios/docs/readme/quick-start.md +22 -0
  76. package/dist/templates/profiles/ios/docs/readme/simulators.md +45 -0
  77. package/dist/templates/profiles/ios/docs/readme/user-flow-validation.md +9 -0
  78. package/dist/templates/profiles/ios/docs/readme/verification-contract.md +13 -0
  79. package/dist/templates/profiles/ios/docs/readme/what-s-in-here.md +27 -0
  80. package/dist/templates/profiles/ios/profile.manifest.json +59 -3
  81. package/dist/templates/runtime-bundles/pm2-runtime/attach.sh +8 -15
  82. package/dist/templates/runtime-bundles/pm2-runtime/env.template +1 -1
  83. package/dist/templates/runtime-bundles/pm2-runtime/stages/api-health.sh +9 -0
  84. package/dist/templates/runtime-bundles/shared-kernel/STAGES.md +32 -18
  85. package/dist/templates/runtime-bundles/shared-kernel/agent-cli.sh +35 -0
  86. package/dist/templates/runtime-bundles/shared-kernel/launch.sh +35 -0
  87. package/dist/templates/runtime-bundles/shared-kernel/lib/verify-runner.mjs +129 -0
  88. package/dist/templates/runtime-bundles/shared-kernel/preflight.sh +35 -0
  89. package/dist/templates/runtime-bundles/shared-kernel/setup-infra.sh +35 -0
  90. package/dist/templates/runtime-bundles/shared-kernel/stages/README.sh +1 -1
  91. package/dist/templates/runtime-bundles/shared-kernel/stages/readiness.sh +14 -0
  92. package/dist/templates/runtime-bundles/shared-kernel/teardown.sh +35 -0
  93. package/dist/templates/runtime-bundles/shared-kernel/verify.sh +35 -0
  94. package/dist/templates/shared-docs/claude-agent/intro.md +3 -0
  95. package/dist/templates/shared-docs/readme/maintaining-this-harness.md +14 -0
  96. package/dist/templates/shared-docs/readme/run-history.md +5 -0
  97. package/dist/templates/shared-docs/readme/session-lifecycle.md +20 -0
  98. package/package.json +1 -1
  99. package/dist/templates/har-boilerplate/CLAUDE.agent.md +0 -83
  100. package/dist/templates/har-boilerplate/README.md +0 -199
  101. package/dist/templates/har-boilerplate/STAGES.md +0 -99
  102. package/dist/templates/har-boilerplate/agent-cli.sh +0 -225
  103. package/dist/templates/har-boilerplate/agent-slot.sh +0 -850
  104. package/dist/templates/har-boilerplate/attach.sh +0 -22
  105. package/dist/templates/har-boilerplate/ecosystem.agent.template.cjs +0 -52
  106. package/dist/templates/har-boilerplate/env.template +0 -40
  107. package/dist/templates/har-boilerplate/launch.sh +0 -323
  108. package/dist/templates/har-boilerplate/preflight.sh +0 -32
  109. package/dist/templates/har-boilerplate/provision-toolchain.sh +0 -766
  110. package/dist/templates/har-boilerplate/setup-infra.sh +0 -178
  111. package/dist/templates/har-boilerplate/stages/README.sh +0 -11
  112. package/dist/templates/har-boilerplate/teardown.sh +0 -87
  113. package/dist/templates/har-boilerplate/verify.sh +0 -249
  114. package/dist/templates/har-boilerplate-cli/README.md +0 -157
  115. package/dist/templates/har-boilerplate-cli/STAGES.md +0 -99
  116. package/dist/templates/har-boilerplate-cli/agent-cli.sh +0 -160
  117. package/dist/templates/har-boilerplate-cli/agent-slot.sh +0 -832
  118. package/dist/templates/har-boilerplate-cli/launch.sh +0 -216
  119. package/dist/templates/har-boilerplate-cli/preflight.sh +0 -28
  120. package/dist/templates/har-boilerplate-cli/provision-toolchain.sh +0 -766
  121. package/dist/templates/har-boilerplate-cli/setup-infra.sh +0 -178
  122. package/dist/templates/har-boilerplate-cli/stages/README.sh +0 -11
  123. package/dist/templates/har-boilerplate-cli/teardown.sh +0 -66
  124. package/dist/templates/har-boilerplate-cli/verify.sh +0 -210
  125. package/dist/templates/har-boilerplate-ios/ADAPT-PROMPT.md +0 -63
  126. package/dist/templates/har-boilerplate-ios/CLAUDE.agent.md +0 -90
  127. package/dist/templates/har-boilerplate-ios/README.md +0 -173
  128. package/dist/templates/har-boilerplate-ios/STAGES.md +0 -99
  129. package/dist/templates/har-boilerplate-ios/agent-cli.sh +0 -149
  130. package/dist/templates/har-boilerplate-ios/agent-slot.sh +0 -479
  131. package/dist/templates/har-boilerplate-ios/launch.sh +0 -206
  132. package/dist/templates/har-boilerplate-ios/provision-toolchain.sh +0 -766
  133. package/dist/templates/har-boilerplate-ios/setup-infra.sh +0 -112
  134. package/dist/templates/har-boilerplate-ios/simulator.sh +0 -492
  135. package/dist/templates/har-boilerplate-ios/stages/README.sh +0 -11
  136. package/dist/templates/har-boilerplate-ios/teardown.sh +0 -68
  137. package/dist/templates/har-boilerplate-ios/verify.sh +0 -185
  138. package/dist/templates/runtime-bundles/pm2-runtime/README.md +0 -4
  139. package/dist/templates/runtime-bundles/shared-kernel/README.md +0 -3
  140. package/dist/templates/runtime-bundles/shared-kernel/provision-toolchain.sh +0 -766
  141. package/dist/templates/runtime-bundles/xcode-sim/README.md +0 -4
  142. 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
- }