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.
- package/CHANGELOG.md +69 -0
- package/README.md +76 -12
- package/dist/active-recovery.d.ts +58 -0
- package/dist/active-recovery.js +143 -0
- package/dist/agents.d.ts +34 -0
- package/dist/agents.js +77 -16
- package/dist/attempt-identity.d.ts +21 -0
- package/dist/attempt-identity.js +39 -0
- package/dist/ci-failure-result.d.ts +23 -0
- package/dist/ci-failure-result.js +90 -0
- package/dist/claude-headless.d.ts +7 -0
- package/dist/claude-headless.js +28 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +55 -15
- package/dist/commands.d.ts +18 -0
- package/dist/commands.js +190 -85
- package/dist/constants.d.ts +32 -0
- package/dist/constants.js +3 -3
- package/dist/context.d.ts +24 -0
- package/dist/context.js +22 -6
- package/dist/dependency-readiness.d.ts +13 -0
- package/dist/dependency-readiness.js +80 -0
- package/dist/dispatch-scope.d.ts +18 -0
- package/dist/dispatch-scope.js +46 -0
- package/dist/done-evidence.d.ts +31 -0
- package/dist/done-evidence.js +216 -0
- package/dist/engine-config.d.ts +65 -0
- package/dist/engine-config.js +139 -0
- package/dist/fsops.d.ts +87 -0
- package/dist/fsops.js +117 -24
- package/dist/helpers.d.ts +15 -0
- package/dist/helpers.js +262 -45
- package/dist/intent-rules/admission.d.ts +10 -0
- package/dist/intent-rules/admission.js +35 -0
- package/dist/intent-rules/done-when-roles.d.ts +21 -0
- package/dist/intent-rules/done-when-roles.js +101 -0
- package/dist/intent-rules/hooks.d.ts +29 -0
- package/dist/intent-rules/hooks.js +23 -0
- package/dist/k3-codex-role.d.ts +20 -0
- package/dist/k3-codex-role.js +68 -0
- package/dist/manifest.d.ts +41 -0
- package/dist/manifest.js +59 -14
- package/dist/mcp.d.ts +18 -0
- package/dist/mcp.js +89 -39
- package/dist/opencode-result.d.ts +19 -0
- package/dist/opencode-result.js +136 -0
- package/dist/opencode-review-guard.d.ts +1 -0
- package/dist/opencode-review-guard.js +19 -0
- package/dist/opencode-session-export.d.ts +5 -0
- package/dist/opencode-session-export.js +28 -0
- package/dist/outcome-moves.d.ts +15 -0
- package/dist/outcome-moves.js +39 -0
- package/dist/planer-client.d.ts +214 -0
- package/dist/planer-client.js +351 -0
- package/dist/restart-recovery.d.ts +47 -0
- package/dist/restart-recovery.js +70 -0
- package/dist/review-source-packet.d.ts +17 -0
- package/dist/review-source-packet.js +44 -0
- package/dist/role-registry.d.ts +19 -0
- package/dist/role-registry.js +83 -0
- package/dist/role-results.d.ts +15 -0
- package/dist/role-results.js +42 -0
- package/dist/roles.d.ts +30 -0
- package/dist/roles.js +20 -16
- package/dist/seed-validation.d.ts +28 -0
- package/dist/seed-validation.js +294 -0
- package/dist/selection-result.d.ts +44 -0
- package/dist/selection-result.js +144 -0
- package/dist/sparse-policy.d.ts +8 -0
- package/dist/sparse-policy.js +49 -0
- package/dist/tester-identity.d.ts +11 -0
- package/dist/tester-identity.js +53 -0
- package/dist/worker-env.d.ts +5 -0
- package/dist/worker-env.js +34 -0
- package/docs/ENGINE-API.md +78 -0
- package/docs/ENGINE-CONFIG.md +115 -0
- package/docs/README.en.md +24 -15
- package/docs/README.md +26 -15
- package/docs/RELEASE-0.1.0.md +81 -8
- package/docs/RELEASE-0.1.1.md +110 -0
- package/helpers/ceo-client-launcher-mcp.mjs +36 -8
- package/helpers/client-spawn-session.sh +181 -0
- package/helpers/client-spawn.sh +825 -0
- package/package.json +9 -4
- package/roles/ceo-client.md +3 -2
- package/roles/manager-builder-client.md +18 -4
- 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"
|