robisie 0.1.0 → 0.1.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 (87) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +76 -12
  3. package/dist/active-recovery.d.ts +58 -0
  4. package/dist/active-recovery.js +143 -0
  5. package/dist/agents.d.ts +34 -0
  6. package/dist/agents.js +77 -16
  7. package/dist/attempt-identity.d.ts +21 -0
  8. package/dist/attempt-identity.js +39 -0
  9. package/dist/ci-failure-result.d.ts +23 -0
  10. package/dist/ci-failure-result.js +90 -0
  11. package/dist/claude-headless.d.ts +7 -0
  12. package/dist/claude-headless.js +28 -0
  13. package/dist/cli.d.ts +17 -0
  14. package/dist/cli.js +55 -15
  15. package/dist/commands.d.ts +18 -0
  16. package/dist/commands.js +190 -85
  17. package/dist/constants.d.ts +32 -0
  18. package/dist/constants.js +3 -3
  19. package/dist/context.d.ts +24 -0
  20. package/dist/context.js +22 -6
  21. package/dist/dependency-readiness.d.ts +13 -0
  22. package/dist/dependency-readiness.js +80 -0
  23. package/dist/dispatch-scope.d.ts +18 -0
  24. package/dist/dispatch-scope.js +46 -0
  25. package/dist/done-evidence.d.ts +31 -0
  26. package/dist/done-evidence.js +216 -0
  27. package/dist/engine-config.d.ts +65 -0
  28. package/dist/engine-config.js +139 -0
  29. package/dist/fsops.d.ts +87 -0
  30. package/dist/fsops.js +117 -24
  31. package/dist/helpers.d.ts +15 -0
  32. package/dist/helpers.js +262 -45
  33. package/dist/intent-rules/admission.d.ts +10 -0
  34. package/dist/intent-rules/admission.js +35 -0
  35. package/dist/intent-rules/done-when-roles.d.ts +21 -0
  36. package/dist/intent-rules/done-when-roles.js +101 -0
  37. package/dist/intent-rules/hooks.d.ts +29 -0
  38. package/dist/intent-rules/hooks.js +23 -0
  39. package/dist/k3-codex-role.d.ts +20 -0
  40. package/dist/k3-codex-role.js +68 -0
  41. package/dist/manifest.d.ts +41 -0
  42. package/dist/manifest.js +59 -14
  43. package/dist/mcp.d.ts +18 -0
  44. package/dist/mcp.js +89 -39
  45. package/dist/opencode-result.d.ts +19 -0
  46. package/dist/opencode-result.js +136 -0
  47. package/dist/opencode-review-guard.d.ts +1 -0
  48. package/dist/opencode-review-guard.js +19 -0
  49. package/dist/opencode-session-export.d.ts +5 -0
  50. package/dist/opencode-session-export.js +28 -0
  51. package/dist/outcome-moves.d.ts +15 -0
  52. package/dist/outcome-moves.js +39 -0
  53. package/dist/planer-client.d.ts +214 -0
  54. package/dist/planer-client.js +351 -0
  55. package/dist/restart-recovery.d.ts +47 -0
  56. package/dist/restart-recovery.js +70 -0
  57. package/dist/review-source-packet.d.ts +17 -0
  58. package/dist/review-source-packet.js +44 -0
  59. package/dist/role-registry.d.ts +19 -0
  60. package/dist/role-registry.js +83 -0
  61. package/dist/role-results.d.ts +15 -0
  62. package/dist/role-results.js +42 -0
  63. package/dist/roles.d.ts +30 -0
  64. package/dist/roles.js +20 -16
  65. package/dist/seed-validation.d.ts +28 -0
  66. package/dist/seed-validation.js +294 -0
  67. package/dist/selection-result.d.ts +44 -0
  68. package/dist/selection-result.js +144 -0
  69. package/dist/sparse-policy.d.ts +8 -0
  70. package/dist/sparse-policy.js +49 -0
  71. package/dist/tester-identity.d.ts +11 -0
  72. package/dist/tester-identity.js +53 -0
  73. package/dist/worker-env.d.ts +5 -0
  74. package/dist/worker-env.js +34 -0
  75. package/docs/ENGINE-API.md +78 -0
  76. package/docs/ENGINE-CONFIG.md +115 -0
  77. package/docs/README.en.md +24 -15
  78. package/docs/README.md +26 -15
  79. package/docs/RELEASE-0.1.0.md +81 -8
  80. package/docs/RELEASE-0.1.1.md +110 -0
  81. package/helpers/ceo-client-launcher-mcp.mjs +36 -8
  82. package/helpers/client-spawn-session.sh +181 -0
  83. package/helpers/client-spawn.sh +825 -0
  84. package/package.json +9 -4
  85. package/roles/ceo-client.md +3 -2
  86. package/roles/manager-builder-client.md +18 -4
  87. package/roles/quality-officer-client.md +20 -6
@@ -0,0 +1,825 @@
1
+ #!/usr/bin/env bash
2
+ # ROB_113 T1 — explicitly spawn one client-mode builder in a new terminal.
3
+ # Credentials cross the terminal boundary only through the environment (or,
4
+ # for the osascript fallback, a private one-shot FIFO). They never enter argv.
5
+ set -euo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ SESSION_RUNNER="$SCRIPT_DIR/client-spawn-session.sh"
9
+ OWNER_PREFIX="robisie-client-spawn-v1"
10
+ OWNER_VALUE=""
11
+ RECLAIM_CLAIM_DIR=""
12
+ RECLAIM_CLAIM_ALT_DIR=""
13
+ RECLAIM_CLAIM_TOKEN=""
14
+ TERMINAL_PROCESS_PID=""
15
+ TERMINAL_IS_FOREGROUND=0
16
+
17
+ usage() {
18
+ echo "usage: client-spawn.sh <card-ref> [--dry-run]" >&2
19
+ }
20
+
21
+ die() {
22
+ local code="$1"
23
+ shift
24
+ echo "client-spawn: $*" >&2
25
+ exit "$code"
26
+ }
27
+
28
+ is_nonnegative_number() {
29
+ [[ "$1" =~ ^[0-9]+([.][0-9]+)?$ ]]
30
+ }
31
+
32
+ is_positive_integer() {
33
+ [[ "$1" =~ ^[1-9][0-9]*$ ]]
34
+ }
35
+
36
+ render_command() {
37
+ local rendered="" arg quoted
38
+ for arg in "$@"; do
39
+ printf -v quoted '%q' "$arg"
40
+ [[ -n "$rendered" ]] && rendered+=" "
41
+ rendered+="$quoted"
42
+ done
43
+ printf '%s' "$rendered"
44
+ }
45
+
46
+ sha256_text() {
47
+ local value="$1" digest=""
48
+ if command -v sha256sum >/dev/null 2>&1; then
49
+ digest="$(printf '%s' "$value" | sha256sum)"
50
+ elif command -v shasum >/dev/null 2>&1; then
51
+ digest="$(printf '%s' "$value" | shasum -a 256)"
52
+ else
53
+ return 1
54
+ fi
55
+ digest="${digest%% *}"
56
+ [[ "$digest" =~ ^[0-9a-f]{64}$ ]] || return 1
57
+ printf '%s' "$digest"
58
+ }
59
+
60
+ path_mode_octal() {
61
+ local path="$1" value=""
62
+ if value="$(stat -f '%Lp' "$path" 2>/dev/null)" && [[ "$value" =~ ^[0-9]+$ ]]; then
63
+ printf '%s' "$value"
64
+ return 0
65
+ fi
66
+ value="$(stat -c '%a' "$path" 2>/dev/null)" || return 1
67
+ [[ "$value" =~ ^[0-9]+$ ]] || return 1
68
+ printf '%s' "$value"
69
+ }
70
+
71
+ find_terminal() {
72
+ local name="$1" found=""
73
+
74
+ if [[ -n "${ROBISIE_SPAWN_TERMINAL_PATH:-}" ]]; then
75
+ found="$(PATH="$ROBISIE_SPAWN_TERMINAL_PATH" command -v "$name" 2>/dev/null || true)"
76
+ elif [[ "$PLATFORM" == "darwin" && "$name" == "open" && -x /usr/bin/open ]]; then
77
+ found="/usr/bin/open"
78
+ else
79
+ found="$(command -v "$name" 2>/dev/null || true)"
80
+ fi
81
+
82
+ [[ -n "$found" ]] || return 1
83
+ printf '%s' "$found"
84
+ }
85
+
86
+ build_terminal_command() {
87
+ local bin=""
88
+ BACKEND=""
89
+ CMD=()
90
+ TERMINAL_IS_FOREGROUND=0
91
+
92
+ case "$PLATFORM" in
93
+ darwin)
94
+ if bin="$(find_terminal open)"; then
95
+ BACKEND="open"
96
+ CMD=("$bin" -n -a Terminal "$LAUNCH_FILE")
97
+ elif bin="$(find_terminal osascript)"; then
98
+ BACKEND="osascript"
99
+ CMD=(
100
+ "$bin"
101
+ -e 'on run argv'
102
+ -e 'set launchPath to item 1 of argv'
103
+ -e 'tell application "Terminal"'
104
+ -e 'activate'
105
+ -e 'do script "/bin/bash " & quoted form of launchPath'
106
+ -e 'end tell'
107
+ -e 'end run'
108
+ "$LAUNCH_FILE"
109
+ )
110
+ else
111
+ die 3 "no supported terminal for darwin (tried: open, osascript)"
112
+ fi
113
+ ;;
114
+ linux)
115
+ if bin="$(find_terminal gnome-terminal)"; then
116
+ BACKEND="gnome-terminal"
117
+ CMD=("$bin" --window -- "$LAUNCH_FILE")
118
+ elif bin="$(find_terminal xterm)"; then
119
+ BACKEND="xterm"
120
+ TERMINAL_IS_FOREGROUND=1
121
+ CMD=("$bin" -T Robisie -e "$LAUNCH_FILE")
122
+ elif bin="$(find_terminal alacritty)"; then
123
+ BACKEND="alacritty"
124
+ TERMINAL_IS_FOREGROUND=1
125
+ CMD=("$bin" --title Robisie --command "$LAUNCH_FILE")
126
+ else
127
+ die 3 "no supported terminal for linux (tried: gnome-terminal, xterm, alacritty)"
128
+ fi
129
+ ;;
130
+ windows)
131
+ if bin="$(find_terminal wt.exe)" || bin="$(find_terminal wt)"; then
132
+ BACKEND="wt"
133
+ CMD=("$bin" -w new nt --inheritEnvironment bash "$LAUNCH_FILE")
134
+ elif bin="$(find_terminal cmd.exe)"; then
135
+ BACKEND="start"
136
+ CMD=("$bin" /d /v:off /s /c 'set "BASH_ENV=%ROBISIE_BOOTSTRAP_CMD%" && start "" bash -c :')
137
+ elif bin="$(find_terminal powershell.exe)" || bin="$(find_terminal pwsh.exe)"; then
138
+ BACKEND="powershell"
139
+ CMD=(
140
+ "$bin" -NoLogo -NoProfile -NonInteractive -Command
141
+ "\$env:BASH_ENV=\$env:ROBISIE_BOOTSTRAP_CMD; Start-Process -FilePath 'bash' -ArgumentList @('-c', ':')"
142
+ )
143
+ else
144
+ die 3 "no supported terminal for windows (tried: wt, start, PowerShell)"
145
+ fi
146
+ ;;
147
+ *)
148
+ die 2 "unsupported platform: $PLATFORM"
149
+ ;;
150
+ esac
151
+ }
152
+
153
+ pid_file_is_alive() {
154
+ local file="$1" pid=""
155
+ [[ -f "$file" ]] || return 1
156
+ IFS= read -r pid < "$file" || return 1
157
+ [[ "$pid" =~ ^[0-9]+$ ]] || return 1
158
+ kill -0 "$pid" 2>/dev/null
159
+ }
160
+
161
+ path_mtime_epoch() {
162
+ local path="$1" value=""
163
+ if value="$(stat -f '%m' "$path" 2>/dev/null)" && [[ "$value" =~ ^[0-9]+$ ]]; then
164
+ printf '%s' "$value"
165
+ return 0
166
+ fi
167
+ value="$(stat -c '%Y' "$path" 2>/dev/null)" || return 1
168
+ [[ "$value" =~ ^[0-9]+$ ]] || return 1
169
+ printf '%s' "$value"
170
+ }
171
+
172
+ read_state_owner() {
173
+ local dir="$1" marker=""
174
+ [[ -f "$dir/.owner" ]] || return 1
175
+ IFS= read -r marker < "$dir/.owner" || return 1
176
+ [[ "$marker" =~ ^robisie-client-spawn-v1:[[:alnum:]._-]+$ ]] || return 1
177
+ printf '%s' "$marker"
178
+ }
179
+
180
+ state_artifact_path() {
181
+ local dir="$1" owner="$2" artifact="$3" generation
182
+ generation="${owner#*:}"
183
+ [[ -n "$generation" && "$generation" != "$owner" ]] || return 1
184
+ case "$artifact" in
185
+ runner-claim) printf '%s/runner.claim.%s' "$dir" "$generation" ;;
186
+ runner-pid) printf '%s/runner.%s.pid' "$dir" "$generation" ;;
187
+ agent-pid) printf '%s/agent.%s.pid' "$dir" "$generation" ;;
188
+ ready) printf '%s/ready.%s' "$dir" "$generation" ;;
189
+ failed) printf '%s/failed.%s' "$dir" "$generation" ;;
190
+ token-fifo) printf '%s/token.%s.fifo' "$dir" "$generation" ;;
191
+ token-writer-pid) printf '%s/token-writer.%s.pid' "$dir" "$generation" ;;
192
+ *) return 1 ;;
193
+ esac
194
+ }
195
+
196
+ owned_state_dir() {
197
+ local dir="$1" expected="${2:-}" marker=""
198
+ case "$dir" in
199
+ "$STATE_ROOT"/*.lock|"$STATE_ROOT"/*.lock.stale.*) ;;
200
+ *) return 1 ;;
201
+ esac
202
+ marker="$(read_state_owner "$dir")" || return 1
203
+ [[ -z "$expected" || "$marker" == "$expected" ]]
204
+ }
205
+
206
+ cleanup_owned_state() {
207
+ local dir="$1" expected="$2"
208
+ [[ -n "$expected" ]] || return 1
209
+ owned_state_dir "$dir" "$expected" || return 1
210
+ rm -rf -- "$dir"
211
+ }
212
+
213
+ existing_state_is_active() {
214
+ local created="" now age owner agent_pid runner_pid failed
215
+
216
+ owner="$(read_state_owner "$STATE_DIR")" || return 1
217
+ agent_pid="$(state_artifact_path "$STATE_DIR" "$owner" agent-pid)" || return 1
218
+ runner_pid="$(state_artifact_path "$STATE_DIR" "$owner" runner-pid)" || return 1
219
+ failed="$(state_artifact_path "$STATE_DIR" "$owner" failed)" || return 1
220
+
221
+ if pid_file_is_alive "$agent_pid" || pid_file_is_alive "$runner_pid"; then
222
+ return 0
223
+ fi
224
+
225
+ [[ -f "$failed" ]] && return 1
226
+ if [[ -f "$STATE_DIR/created-at" ]]; then
227
+ IFS= read -r created < "$STATE_DIR/created-at" || return 1
228
+ [[ "$created" =~ ^[0-9]+$ ]] || return 1
229
+ else
230
+ # A second launcher can observe the directory between atomic mkdir and
231
+ # the created-at write. Use the owner marker's mtime for that tiny window
232
+ # instead of reclaiming a lock that is still being initialized.
233
+ created="$(path_mtime_epoch "$STATE_DIR/.owner" || true)"
234
+ [[ "$created" =~ ^[0-9]+$ ]] || return 0
235
+ fi
236
+ now="$(date +%s)"
237
+ age=$((now - created))
238
+ [[ "$age" -ge 0 && "$age" -lt "$STALE_SEC" ]]
239
+ }
240
+
241
+ initialize_card_lock() {
242
+ local created_now
243
+ mkdir "$STATE_DIR" 2>/dev/null || return 1
244
+ created_now="$(date +%s)"
245
+ if {
246
+ printf '%s\n' "$OWNER_VALUE" > "$STATE_DIR/.owner" &&
247
+ printf '%s\n' "$created_now" > "$STATE_DIR/created-at"
248
+ }; then
249
+ return 0
250
+ fi
251
+
252
+ rm -f "$STATE_DIR/.owner" "$STATE_DIR/created-at" 2>/dev/null || true
253
+ rmdir "$STATE_DIR" 2>/dev/null || true
254
+ return 2
255
+ }
256
+
257
+ release_reclaim_claim() {
258
+ local claim_dir="$1" claim_token="$2" recorded=""
259
+ [[ -f "$claim_dir/claimant" ]] || return 1
260
+ IFS= read -r recorded < "$claim_dir/claimant" || return 1
261
+ [[ "$recorded" == "$claim_token" ]] || return 1
262
+ rm -f "$claim_dir/claimant" "$claim_dir/pid" "$claim_dir/created-at" || return 1
263
+ rmdir "$claim_dir"
264
+ }
265
+
266
+ clear_reclaim_claim_tracking() {
267
+ RECLAIM_CLAIM_DIR=""
268
+ RECLAIM_CLAIM_ALT_DIR=""
269
+ RECLAIM_CLAIM_TOKEN=""
270
+ }
271
+
272
+ # Invoked indirectly by the EXIT trap installed before argument parsing.
273
+ # shellcheck disable=SC2329
274
+ cleanup_reclaim_claim_on_exit() {
275
+ if [[ -n "$RECLAIM_CLAIM_TOKEN" ]]; then
276
+ [[ -z "$RECLAIM_CLAIM_DIR" ]] || release_reclaim_claim "$RECLAIM_CLAIM_DIR" "$RECLAIM_CLAIM_TOKEN" 2>/dev/null || true
277
+ [[ -z "$RECLAIM_CLAIM_ALT_DIR" ]] || release_reclaim_claim "$RECLAIM_CLAIM_ALT_DIR" "$RECLAIM_CLAIM_TOKEN" 2>/dev/null || true
278
+ fi
279
+ }
280
+
281
+ reclaim_claim_is_abandoned() {
282
+ local claim_dir="$1" expected_owner="$2" current_owner="" claimant_pid="" created="" now age
283
+ [[ -d "$claim_dir" ]] || return 1
284
+ current_owner="$(read_state_owner "$STATE_DIR" || true)"
285
+ [[ "$current_owner" == "$expected_owner" ]] || return 1
286
+
287
+ if [[ -f "$claim_dir/pid" ]]; then
288
+ IFS= read -r claimant_pid < "$claim_dir/pid" || claimant_pid=""
289
+ if [[ "$claimant_pid" =~ ^[0-9]+$ ]]; then
290
+ kill -0 "$claimant_pid" 2>/dev/null && return 1
291
+ return 0
292
+ fi
293
+ fi
294
+
295
+ if [[ -f "$claim_dir/created-at" ]]; then
296
+ IFS= read -r created < "$claim_dir/created-at" || created=""
297
+ else
298
+ created="$(path_mtime_epoch "$claim_dir" || true)"
299
+ fi
300
+ [[ "$created" =~ ^[0-9]+$ ]] || return 1
301
+ now="$(date +%s)"
302
+ age=$((now - created))
303
+ [[ "$age" -ge 2 ]]
304
+ }
305
+
306
+ remove_abandoned_reclaim_claim() {
307
+ local claim_dir="$1" expected_owner="$2" current_owner=""
308
+ reclaim_claim_is_abandoned "$claim_dir" "$expected_owner" || return 1
309
+ current_owner="$(read_state_owner "$STATE_DIR" || true)"
310
+ [[ "$current_owner" == "$expected_owner" ]] || return 1
311
+ rm -f "$claim_dir/claimant" "$claim_dir/pid" "$claim_dir/created-at" || return 1
312
+ rmdir "$claim_dir"
313
+ }
314
+
315
+ acquire_card_lock() {
316
+ local stale_dir stale_owner moved_owner moved_claim init_rc candidate_owner generation
317
+ local claim_dir claim_token current_owner claim_deadline
318
+ claim_deadline=$((SECONDS + 5))
319
+
320
+ while true; do
321
+ if initialize_card_lock; then
322
+ return 0
323
+ else
324
+ init_rc=$?
325
+ fi
326
+ [[ "$init_rc" -eq 1 ]] || die 4 "cannot initialize card lock"
327
+
328
+ [[ -d "$STATE_DIR" ]] || {
329
+ [[ "$SECONDS" -lt "$claim_deadline" ]] || die 4 "cannot create card lock: $STATE_DIR"
330
+ sleep "$POLL_INTERVAL_SEC"
331
+ continue
332
+ }
333
+ if ! owned_state_dir "$STATE_DIR"; then
334
+ # Another launcher can be between its atomic mkdir and the first complete
335
+ # owner/created-at writes. Wait only for that incomplete shape; a fully
336
+ # formed foreign marker still fails closed immediately.
337
+ if [[ "$SECONDS" -lt "$claim_deadline" ]] && { [[ ! -s "$STATE_DIR/.owner" ]] || [[ ! -f "$STATE_DIR/created-at" ]]; }; then
338
+ sleep "$POLL_INTERVAL_SEC"
339
+ continue
340
+ fi
341
+ die 4 "refusing unowned state collision for card=$CARD_REF"
342
+ fi
343
+
344
+ if existing_state_is_active; then
345
+ echo "ALREADY_RUNNING card=$CARD_REF"
346
+ exit 0
347
+ fi
348
+
349
+ candidate_owner="$(read_state_owner "$STATE_DIR")" || die 4 "cannot read stale state owner for card=$CARD_REF"
350
+ generation="${candidate_owner#*:}"
351
+ claim_dir="$STATE_DIR/reclaim.claim.$generation"
352
+ claim_token="$$-$RANDOM-$(date +%s)"
353
+
354
+ if ! mkdir "$claim_dir" 2>/dev/null; then
355
+ if remove_abandoned_reclaim_claim "$claim_dir" "$candidate_owner" 2>/dev/null; then
356
+ continue
357
+ fi
358
+ [[ "$SECONDS" -lt "$claim_deadline" ]] || die 4 "stale reclaim is busy for card=$CARD_REF"
359
+ sleep "$POLL_INTERVAL_SEC"
360
+ continue
361
+ fi
362
+ if ! {
363
+ printf '%s\n' "$claim_token" > "$claim_dir/claimant" &&
364
+ printf '%s\n' "$$" > "$claim_dir/pid" &&
365
+ printf '%s\n' "$(date +%s)" > "$claim_dir/created-at"
366
+ }; then
367
+ rm -f "$claim_dir/claimant" "$claim_dir/pid" "$claim_dir/created-at" 2>/dev/null || true
368
+ rmdir "$claim_dir" 2>/dev/null || true
369
+ die 4 "cannot initialize stale reclaim claim for card=$CARD_REF"
370
+ fi
371
+ RECLAIM_CLAIM_DIR="$claim_dir"
372
+ RECLAIM_CLAIM_ALT_DIR=""
373
+ RECLAIM_CLAIM_TOKEN="$claim_token"
374
+
375
+ current_owner="$(read_state_owner "$STATE_DIR" || true)"
376
+ if [[ "$current_owner" != "$candidate_owner" ]]; then
377
+ release_reclaim_claim "$claim_dir" "$claim_token" || true
378
+ clear_reclaim_claim_tracking
379
+ [[ "$SECONDS" -lt "$claim_deadline" ]] || die 4 "state generation changed during stale reclaim for card=$CARD_REF"
380
+ sleep "$POLL_INTERVAL_SEC"
381
+ continue
382
+ fi
383
+
384
+ if existing_state_is_active; then
385
+ release_reclaim_claim "$claim_dir" "$claim_token" || die 4 "cannot release reclaim claim for active card=$CARD_REF"
386
+ clear_reclaim_claim_tracking
387
+ echo "ALREADY_RUNNING card=$CARD_REF"
388
+ exit 0
389
+ fi
390
+
391
+ stale_owner="$candidate_owner"
392
+ stale_dir="$STATE_DIR.stale.$$-$RANDOM"
393
+ RECLAIM_CLAIM_ALT_DIR="$stale_dir/reclaim.claim.$generation"
394
+ if ! mv "$STATE_DIR" "$stale_dir" 2>/dev/null; then
395
+ release_reclaim_claim "$claim_dir" "$claim_token" 2>/dev/null || true
396
+ clear_reclaim_claim_tracking
397
+ [[ "$SECONDS" -lt "$claim_deadline" ]] || die 4 "could not reclaim stale card lock for card=$CARD_REF"
398
+ sleep "$POLL_INTERVAL_SEC"
399
+ continue
400
+ fi
401
+ RECLAIM_CLAIM_DIR="$RECLAIM_CLAIM_ALT_DIR"
402
+ RECLAIM_CLAIM_ALT_DIR=""
403
+
404
+ moved_owner="$(read_state_owner "$stale_dir" || true)"
405
+ if [[ "$moved_owner" != "$stale_owner" ]]; then
406
+ moved_claim="$stale_dir/reclaim.claim.$generation"
407
+ release_reclaim_claim "$moved_claim" "$claim_token" 2>/dev/null || true
408
+ clear_reclaim_claim_tracking
409
+ if [[ ! -e "$STATE_DIR" ]]; then
410
+ mv "$stale_dir" "$STATE_DIR" 2>/dev/null || true
411
+ fi
412
+ die 4 "state generation changed while moving stale lock for card=$CARD_REF"
413
+ fi
414
+
415
+ cleanup_owned_state "$stale_dir" "$stale_owner" || die 4 "could not remove stale owned state for card=$CARD_REF"
416
+ clear_reclaim_claim_tracking
417
+
418
+ if initialize_card_lock; then
419
+ return 0
420
+ else
421
+ init_rc=$?
422
+ fi
423
+ [[ "$init_rc" -eq 1 ]] || die 4 "cannot initialize reclaimed card lock"
424
+ if [[ -d "$STATE_DIR" ]] && owned_state_dir "$STATE_DIR" && existing_state_is_active; then
425
+ echo "ALREADY_RUNNING card=$CARD_REF"
426
+ exit 0
427
+ fi
428
+ [[ "$SECONDS" -lt "$claim_deadline" ]] || die 4 "card lock raced during reclaim for card=$CARD_REF"
429
+ sleep "$POLL_INTERVAL_SEC"
430
+ done
431
+ }
432
+
433
+ write_owned_line() {
434
+ local path="$1" value="$2"
435
+ owned_state_dir "$STATE_DIR" "$OWNER_VALUE" || return 1
436
+ printf '%s\n' "$value" > "$path"
437
+ }
438
+
439
+ write_spawn_state() {
440
+ local runner_q state_q owner_q card_q
441
+ printf -v runner_q '%q' "$SESSION_RUNNER"
442
+ printf -v state_q '%q' "$STATE_DIR"
443
+ printf -v owner_q '%q' "$OWNER_VALUE"
444
+ printf -v card_q '%q' "$CARD_REF"
445
+
446
+ if ! {
447
+ write_owned_line "$STATE_DIR/card-ref" "$CARD_REF" &&
448
+ write_owned_line "$STATE_DIR/project-context" "$PROJECT_CONTEXT" &&
449
+ write_owned_line "$STATE_DIR/agent-command" "$AGENT_COMMAND" &&
450
+ write_owned_line "$STATE_DIR/boot-grace" "$BOOT_GRACE_SEC" &&
451
+ write_owned_line "$STATE_DIR/transcript-path" "${ROBISIE_SPAWN_TRANSCRIPT:-}" &&
452
+ owned_state_dir "$STATE_DIR" "$OWNER_VALUE" &&
453
+ {
454
+ printf '%s\n' '#!/usr/bin/env bash'
455
+ printf '%s\n' 'unset BASH_ENV'
456
+ printf 'exec /bin/bash %s %s %s %s\n' "$runner_q" "$state_q" "$owner_q" "$card_q"
457
+ } > "$LAUNCH_FILE" &&
458
+ owned_state_dir "$STATE_DIR" "$OWNER_VALUE" &&
459
+ chmod 700 "$LAUNCH_FILE"
460
+ }; then
461
+ cleanup_owned_state "$STATE_DIR" "$OWNER_VALUE" || true
462
+ die 4 "failed to write spawn state"
463
+ fi
464
+ }
465
+
466
+ stop_token_writer() {
467
+ if [[ -n "${TOKEN_WRITER_PID:-}" ]]; then
468
+ if kill -0 "$TOKEN_WRITER_PID" 2>/dev/null; then
469
+ kill "$TOKEN_WRITER_PID" 2>/dev/null || true
470
+ fi
471
+ wait "$TOKEN_WRITER_PID" 2>/dev/null || true
472
+ TOKEN_WRITER_PID=""
473
+ fi
474
+ if [[ -n "${TOKEN_WRITER_PID_FILE:-}" ]] && owned_state_dir "${STATE_DIR:-}" "$OWNER_VALUE"; then
475
+ rm -f "$TOKEN_WRITER_PID_FILE" 2>/dev/null || true
476
+ fi
477
+ }
478
+
479
+ # Invoked from the EXIT-trap call graph below.
480
+ # shellcheck disable=SC2329
481
+ wait_for_token_writer_delivery() {
482
+ local rounds=0 writer_rc=0
483
+ [[ -n "${TOKEN_WRITER_PID:-}" ]] || return 0
484
+ while kill -0 "$TOKEN_WRITER_PID" 2>/dev/null && [[ "$rounds" -lt 100 ]]; do
485
+ sleep 0.05
486
+ rounds=$((rounds + 1))
487
+ done
488
+ kill -0 "$TOKEN_WRITER_PID" 2>/dev/null && return 1
489
+ if wait "$TOKEN_WRITER_PID" 2>/dev/null; then writer_rc=0; else writer_rc=$?; fi
490
+ TOKEN_WRITER_PID=""
491
+ if [[ -n "${TOKEN_WRITER_PID_FILE:-}" ]] && owned_state_dir "${STATE_DIR:-}" "$OWNER_VALUE"; then
492
+ rm -f "$TOKEN_WRITER_PID_FILE" 2>/dev/null || true
493
+ fi
494
+ [[ "$writer_rc" -eq 0 ]]
495
+ }
496
+
497
+ # Invoked from the EXIT-trap call graph below.
498
+ # shellcheck disable=SC2329
499
+ abort_token_handoff() {
500
+ local rounds=0 runner_pid=""
501
+ stop_token_writer
502
+ if [[ -n "${TOKEN_FIFO:-}" ]] && owned_state_dir "${STATE_DIR:-}" "$OWNER_VALUE"; then
503
+ rm -f "$TOKEN_FIFO" 2>/dev/null || true
504
+ fi
505
+ while [[ "$rounds" -lt 100 ]]; do
506
+ if [[ -n "${RUNNER_PID_FILE:-}" ]] && pid_file_is_alive "$RUNNER_PID_FILE"; then
507
+ :
508
+ elif [[ "$rounds" -lt 20 && -n "${RUNNER_CLAIM:-}" && -d "$RUNNER_CLAIM" && ! -f "${RUNNER_PID_FILE:-}" ]]; then
509
+ : # Claim-to-PID initialization window.
510
+ else
511
+ break
512
+ fi
513
+ sleep 0.05
514
+ rounds=$((rounds + 1))
515
+ done
516
+ if [[ -n "${RUNNER_PID_FILE:-}" ]] && pid_file_is_alive "$RUNNER_PID_FILE"; then
517
+ IFS= read -r runner_pid < "$RUNNER_PID_FILE" || runner_pid=""
518
+ [[ "$runner_pid" =~ ^[0-9]+$ ]] && kill -TERM "$runner_pid" 2>/dev/null || true
519
+ sleep 0.1
520
+ fi
521
+ if [[ -n "${STATE_DIR:-}" && -n "$OWNER_VALUE" ]] &&
522
+ ! pid_file_is_alive "${RUNNER_PID_FILE:-}" &&
523
+ ! pid_file_is_alive "${AGENT_PID_FILE:-}"; then
524
+ cleanup_owned_state "$STATE_DIR" "$OWNER_VALUE" 2>/dev/null || true
525
+ fi
526
+ }
527
+
528
+ stop_terminal_process() {
529
+ if [[ -n "$TERMINAL_PROCESS_PID" ]] && kill -0 "$TERMINAL_PROCESS_PID" 2>/dev/null; then
530
+ kill "$TERMINAL_PROCESS_PID" 2>/dev/null || true
531
+ wait "$TERMINAL_PROCESS_PID" 2>/dev/null || true
532
+ fi
533
+ TERMINAL_PROCESS_PID=""
534
+ }
535
+
536
+ # Invoked from the signal-handler call graph below.
537
+ # shellcheck disable=SC2329
538
+ current_generation_has_runner() {
539
+ [[ -n "${RUNNER_CLAIM:-}" && -d "$RUNNER_CLAIM" ]] ||
540
+ { [[ -n "${RUNNER_PID_FILE:-}" ]] && pid_file_is_alive "$RUNNER_PID_FILE"; } ||
541
+ { [[ -n "${AGENT_PID_FILE:-}" ]] && pid_file_is_alive "$AGENT_PID_FILE"; }
542
+ }
543
+
544
+ # Invoked indirectly by INT/TERM traps installed below.
545
+ # shellcheck disable=SC2329
546
+ handle_signal() {
547
+ local code="$1"
548
+ if ! current_generation_has_runner; then
549
+ stop_token_writer
550
+ stop_terminal_process
551
+ if [[ -n "${STATE_DIR:-}" && -n "$OWNER_VALUE" ]]; then
552
+ cleanup_owned_state "$STATE_DIR" "$OWNER_VALUE" 2>/dev/null || true
553
+ fi
554
+ fi
555
+ exit "$code"
556
+ }
557
+
558
+ # Invoked indirectly by the EXIT trap installed below.
559
+ # shellcheck disable=SC2329
560
+ cleanup_on_exit() {
561
+ if [[ -n "${TOKEN_WRITER_PID:-}" ]]; then
562
+ if current_generation_has_runner; then
563
+ wait_for_token_writer_delivery || abort_token_handoff
564
+ else
565
+ abort_token_handoff
566
+ if [[ -n "${STATE_DIR:-}" && -n "$OWNER_VALUE" ]]; then
567
+ cleanup_owned_state "$STATE_DIR" "$OWNER_VALUE" 2>/dev/null || true
568
+ fi
569
+ fi
570
+ fi
571
+ cleanup_reclaim_claim_on_exit
572
+ }
573
+
574
+ trap cleanup_on_exit EXIT
575
+ trap 'handle_signal 130' INT
576
+ trap 'handle_signal 143' TERM
577
+ trap 'handle_signal 129' HUP
578
+ trap 'handle_signal 131' QUIT
579
+
580
+ CARD_REF=""
581
+ DRY_RUN=0
582
+ while [[ $# -gt 0 ]]; do
583
+ case "$1" in
584
+ --dry-run)
585
+ DRY_RUN=1
586
+ shift
587
+ ;;
588
+ -h|--help)
589
+ usage
590
+ exit 0
591
+ ;;
592
+ --*)
593
+ usage
594
+ die 2 "unknown option: $1"
595
+ ;;
596
+ *)
597
+ [[ -z "$CARD_REF" ]] || {
598
+ usage
599
+ die 2 "exactly one card-ref is required"
600
+ }
601
+ CARD_REF="$1"
602
+ shift
603
+ ;;
604
+ esac
605
+ done
606
+
607
+ [[ -n "$CARD_REF" ]] || {
608
+ usage
609
+ die 2 "card-ref is required"
610
+ }
611
+ [[ "$CARD_REF" =~ ^[[:alnum:]][[:alnum:].:_-]{0,127}$ ]] || die 2 "invalid card-ref: use 1-128 alphanumeric/._:- characters"
612
+ CARD_DIGEST="$(sha256_text "$CARD_REF")" || die 2 "sha256sum or shasum is required to derive a portable card lock"
613
+ STATE_KEY="card-$CARD_DIGEST"
614
+
615
+ PROJECT_CONTEXT="${ROBISIE_PROJECT_CONTEXT:-$PWD}"
616
+ [[ "$PROJECT_CONTEXT" != *$'\n'* && -d "$PROJECT_CONTEXT" ]] || die 2 "ROBISIE_PROJECT_CONTEXT must be an existing single-line directory"
617
+ if [[ "$PROJECT_CONTEXT" != /* ]]; then
618
+ PROJECT_CONTEXT="$PWD/$PROJECT_CONTEXT"
619
+ fi
620
+
621
+ AGENT_COMMAND="${ROBISIE_AGENT_COMMAND:-claude --agent manager-builder}"
622
+ [[ -n "$AGENT_COMMAND" && "$AGENT_COMMAND" != *$'\n'* ]] || die 2 "ROBISIE_AGENT_COMMAND must be a non-empty single line"
623
+
624
+ BOOT_TIMEOUT_SEC="${ROBISIE_SPAWN_BOOT_TIMEOUT_SEC:-20}"
625
+ BOOT_GRACE_SEC="${ROBISIE_SPAWN_BOOT_GRACE_SEC:-1}"
626
+ POLL_INTERVAL_SEC="${ROBISIE_SPAWN_POLL_INTERVAL_SEC:-0.25}"
627
+ MAX_ATTEMPTS="${ROBISIE_SPAWN_MAX_ATTEMPTS:-3}"
628
+ BACKOFF_SEC="${ROBISIE_SPAWN_BACKOFF_SEC:-1}"
629
+ STALE_SEC="${ROBISIE_SPAWN_STALE_SEC:-60}"
630
+ is_positive_integer "$BOOT_TIMEOUT_SEC" || die 2 "ROBISIE_SPAWN_BOOT_TIMEOUT_SEC must be a positive integer"
631
+ is_nonnegative_number "$BOOT_GRACE_SEC" || die 2 "ROBISIE_SPAWN_BOOT_GRACE_SEC must be non-negative"
632
+ is_nonnegative_number "$POLL_INTERVAL_SEC" || die 2 "ROBISIE_SPAWN_POLL_INTERVAL_SEC must be non-negative"
633
+ is_positive_integer "$MAX_ATTEMPTS" || die 2 "ROBISIE_SPAWN_MAX_ATTEMPTS must be a positive integer"
634
+ is_nonnegative_number "$BACKOFF_SEC" || die 2 "ROBISIE_SPAWN_BACKOFF_SEC must be non-negative"
635
+ is_positive_integer "$STALE_SEC" || die 2 "ROBISIE_SPAWN_STALE_SEC must be a positive integer"
636
+
637
+ PLATFORM="${ROBISIE_SPAWN_PLATFORM:-}"
638
+ if [[ -z "$PLATFORM" ]]; then
639
+ case "$(uname -s 2>/dev/null || true)" in
640
+ Darwin) PLATFORM="darwin" ;;
641
+ Linux) PLATFORM="linux" ;;
642
+ MINGW*|MSYS*|CYGWIN*) PLATFORM="windows" ;;
643
+ *) die 2 "unsupported platform: $(uname -s 2>/dev/null || echo unknown)" ;;
644
+ esac
645
+ fi
646
+ case "$PLATFORM" in darwin|linux|windows) ;; *) die 2 "unsupported platform: $PLATFORM" ;; esac
647
+
648
+ if [[ "$PLATFORM" == "windows" && -z "${ROBISIE_SPAWN_STATE_ROOT:-}" ]]; then
649
+ WINDOWS_USER_STATE_BASE="${LOCALAPPDATA:-${USERPROFILE:-}}"
650
+ [[ -n "$WINDOWS_USER_STATE_BASE" ]] || die 2 "LOCALAPPDATA or USERPROFILE is required for private Windows state"
651
+ command -v cygpath >/dev/null 2>&1 || die 2 "cygpath is required for private Windows state"
652
+ DEFAULT_STATE_BASE="$(cygpath -u "$WINDOWS_USER_STATE_BASE")/Robisie"
653
+ STATE_ROOT="$DEFAULT_STATE_BASE/client-spawn"
654
+ else
655
+ DEFAULT_STATE_BASE="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}"
656
+ STATE_ROOT="${ROBISIE_SPAWN_STATE_ROOT:-$DEFAULT_STATE_BASE/robisie-client-spawn-${UID:-$(id -u)}}"
657
+ fi
658
+ [[ -n "$STATE_ROOT" && "$STATE_ROOT" == /* && "$STATE_ROOT" != *$'\n'* ]] || die 2 "ROBISIE_SPAWN_STATE_ROOT must be an absolute single-line path"
659
+ STATE_DIR="$STATE_ROOT/$STATE_KEY.lock"
660
+ LAUNCH_FILE="$STATE_DIR/launch.command"
661
+
662
+ if [[ "$DRY_RUN" -eq 1 ]]; then
663
+ build_terminal_command
664
+ echo "DRY-RUN platform=$PLATFORM backend=$BACKEND"
665
+ echo "ENV ROBISIE_CARD_REF=$CARD_REF ROBISIE_KEY=<redacted> ROBISIE_PROJECT_CONTEXT=$PROJECT_CONTEXT"
666
+ echo "COMMAND $(render_command "${CMD[@]}")"
667
+ exit 0
668
+ fi
669
+
670
+ [[ -n "${ROBISIE_KEY:-}" ]] || die 2 "ROBISIE_KEY is required for a real spawn"
671
+ [[ -x "$SESSION_RUNNER" ]] || die 4 "session runner is missing or not executable: $SESSION_RUNNER"
672
+ if [[ "$PLATFORM" == "windows" && -n "${ROBISIE_SPAWN_STATE_ROOT:-}" ]]; then
673
+ die 2 "real Windows spawn requires the per-user LOCALAPPDATA state root"
674
+ fi
675
+
676
+ umask 077
677
+ if [[ -L "$STATE_ROOT" || ( -e "$STATE_ROOT" && ! -d "$STATE_ROOT" ) ]]; then
678
+ die 4 "private state root must be a regular directory, not a symlink: $STATE_ROOT"
679
+ fi
680
+ mkdir -p "$STATE_ROOT" || die 4 "cannot create state root: $STATE_ROOT"
681
+ if [[ -L "$STATE_ROOT" || ! -d "$STATE_ROOT" || ! -O "$STATE_ROOT" ]]; then
682
+ die 4 "private state root must be owned by the current user: $STATE_ROOT"
683
+ fi
684
+ chmod 700 "$STATE_ROOT" || die 4 "cannot secure private state root: $STATE_ROOT"
685
+ if [[ "$PLATFORM" != "windows" ]]; then
686
+ [[ "$(path_mode_octal "$STATE_ROOT" || true)" == "700" ]] || die 4 "private state root permissions must be 700: $STATE_ROOT"
687
+ fi
688
+ STATE_ROOT="$(cd "$STATE_ROOT" && pwd -P)"
689
+ [[ -d "$STATE_ROOT" && -O "$STATE_ROOT" ]] || die 4 "private state root ownership changed during setup"
690
+ STATE_DIR="$STATE_ROOT/$STATE_KEY.lock"
691
+ LAUNCH_FILE="$STATE_DIR/launch.command"
692
+
693
+ if [[ -d "$STATE_DIR" ]] && owned_state_dir "$STATE_DIR" && existing_state_is_active; then
694
+ echo "ALREADY_RUNNING card=$CARD_REF"
695
+ exit 0
696
+ fi
697
+
698
+ build_terminal_command
699
+ OWNER_VALUE="$OWNER_PREFIX:$(date +%s)-$$-$RANDOM"
700
+ RUNNER_CLAIM="$(state_artifact_path "$STATE_DIR" "$OWNER_VALUE" runner-claim)"
701
+ RUNNER_PID_FILE="$(state_artifact_path "$STATE_DIR" "$OWNER_VALUE" runner-pid)"
702
+ AGENT_PID_FILE="$(state_artifact_path "$STATE_DIR" "$OWNER_VALUE" agent-pid)"
703
+ READY_FILE="$(state_artifact_path "$STATE_DIR" "$OWNER_VALUE" ready)"
704
+ FAILED_FILE="$(state_artifact_path "$STATE_DIR" "$OWNER_VALUE" failed)"
705
+ TOKEN_FIFO="$(state_artifact_path "$STATE_DIR" "$OWNER_VALUE" token-fifo)"
706
+ TOKEN_WRITER_PID_FILE="$(state_artifact_path "$STATE_DIR" "$OWNER_VALUE" token-writer-pid)"
707
+ acquire_card_lock
708
+ write_spawn_state
709
+
710
+ export ROBISIE_BOOTSTRAP_CMD="$LAUNCH_FILE"
711
+ TOKEN_WRITER_PID=""
712
+ if [[ "$BACKEND" == "osascript" ]]; then
713
+ if ! owned_state_dir "$STATE_DIR" "$OWNER_VALUE" || ! mkfifo "$TOKEN_FIFO"; then
714
+ cleanup_owned_state "$STATE_DIR" "$OWNER_VALUE" || true
715
+ die 4 "cannot create private credential FIFO"
716
+ fi
717
+ if ! owned_state_dir "$STATE_DIR" "$OWNER_VALUE" || ! chmod 600 "$TOKEN_FIFO"; then
718
+ cleanup_owned_state "$STATE_DIR" "$OWNER_VALUE" || true
719
+ die 4 "cannot secure private credential FIFO"
720
+ fi
721
+ (printf '%s\n' "$ROBISIE_KEY" > "$TOKEN_FIFO") &
722
+ TOKEN_WRITER_PID=$!
723
+ if ! write_owned_line "$TOKEN_WRITER_PID_FILE" "$TOKEN_WRITER_PID"; then
724
+ stop_token_writer
725
+ cleanup_owned_state "$STATE_DIR" "$OWNER_VALUE" || true
726
+ die 4 "cannot record credential writer pid"
727
+ fi
728
+ fi
729
+
730
+ attempt=1
731
+ launched=0
732
+ last_rc=0
733
+ boot_deadline=$((SECONDS + BOOT_TIMEOUT_SEC))
734
+ while [[ "$attempt" -le "$MAX_ATTEMPTS" ]]; do
735
+ if [[ "$TERMINAL_IS_FOREGROUND" -eq 1 ]]; then
736
+ "${CMD[@]}" &
737
+ TERMINAL_PROCESS_PID=$!
738
+ while [[ "$SECONDS" -lt "$boot_deadline" ]]; do
739
+ if owned_state_dir "$STATE_DIR" "$OWNER_VALUE" && { [[ -d "$RUNNER_CLAIM" ]] || pid_file_is_alive "$RUNNER_PID_FILE"; }; then
740
+ launched=1
741
+ break
742
+ fi
743
+ kill -0 "$TERMINAL_PROCESS_PID" 2>/dev/null || break
744
+ sleep "$POLL_INTERVAL_SEC"
745
+ done
746
+ if [[ "$launched" -eq 1 ]]; then
747
+ break
748
+ fi
749
+ if kill -0 "$TERMINAL_PROCESS_PID" 2>/dev/null; then
750
+ # A foreground emulator that remains alive owns the window; the common
751
+ # readiness deadline below will report a bounded boot timeout.
752
+ launched=1
753
+ break
754
+ fi
755
+ if wait "$TERMINAL_PROCESS_PID"; then last_rc=0; else last_rc=$?; fi
756
+ TERMINAL_PROCESS_PID=""
757
+ elif "${CMD[@]}"; then
758
+ launched=1
759
+ break
760
+ else
761
+ last_rc=$?
762
+ fi
763
+
764
+ if owned_state_dir "$STATE_DIR" "$OWNER_VALUE" && { [[ -d "$RUNNER_CLAIM" ]] || pid_file_is_alive "$RUNNER_PID_FILE"; }; then
765
+ launched=1
766
+ break
767
+ fi
768
+
769
+ if [[ "$attempt" -lt "$MAX_ATTEMPTS" ]]; then
770
+ sleep "$BACKOFF_SEC"
771
+ fi
772
+ attempt=$((attempt + 1))
773
+ done
774
+
775
+ if [[ "$launched" -ne 1 ]]; then
776
+ stop_token_writer
777
+ stop_terminal_process
778
+ cleanup_owned_state "$STATE_DIR" "$OWNER_VALUE" || true
779
+ die 4 "terminal launch failed after $MAX_ATTEMPTS attempt(s) (last exit $last_rc)"
780
+ fi
781
+
782
+ deadline="$boot_deadline"
783
+ while [[ "$SECONDS" -lt "$deadline" ]]; do
784
+ if ! owned_state_dir "$STATE_DIR" "$OWNER_VALUE"; then
785
+ stop_token_writer
786
+ stop_terminal_process
787
+ die 4 "spawn state generation changed before readiness"
788
+ fi
789
+
790
+ if [[ -f "$READY_FILE" ]]; then
791
+ stop_token_writer
792
+ echo "SPAWNED card=$CARD_REF platform=$PLATFORM backend=$BACKEND"
793
+ exit 0
794
+ fi
795
+
796
+ if [[ -f "$FAILED_FILE" ]]; then
797
+ reason="agent exited before readiness"
798
+ IFS= read -r reason < "$FAILED_FILE" || true
799
+ stop_token_writer
800
+ stop_terminal_process
801
+ cleanup_owned_state "$STATE_DIR" "$OWNER_VALUE" || true
802
+ die 4 "$reason"
803
+ fi
804
+
805
+ sleep "$POLL_INTERVAL_SEC"
806
+ done
807
+
808
+ if [[ -n "${TOKEN_WRITER_PID:-}" ]]; then
809
+ if pid_file_is_alive "$AGENT_PID_FILE"; then
810
+ wait_for_token_writer_delivery || abort_token_handoff
811
+ elif kill -0 "$TOKEN_WRITER_PID" 2>/dev/null; then
812
+ abort_token_handoff
813
+ else
814
+ wait_for_token_writer_delivery || abort_token_handoff
815
+ fi
816
+ fi
817
+ if owned_state_dir "$STATE_DIR" "$OWNER_VALUE"; then
818
+ if pid_file_is_alive "$AGENT_PID_FILE" || pid_file_is_alive "$RUNNER_PID_FILE" || [[ -d "$RUNNER_CLAIM" ]]; then
819
+ : # Preserve the live generation; a later invocation must remain a no-op.
820
+ else
821
+ stop_terminal_process
822
+ cleanup_owned_state "$STATE_DIR" "$OWNER_VALUE" || true
823
+ fi
824
+ fi
825
+ die 4 "boot timeout after ${BOOT_TIMEOUT_SEC}s"