@indigoai-us/hq-cli 5.345.113 → 5.345.115

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 (61) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-send.sh +5 -1
  3. package/dist/commands/bot-jobs.d.ts +1 -11
  4. package/dist/commands/bot-jobs.js +13 -34
  5. package/dist/commands/capacity.js +2 -0
  6. package/dist/commands/core-audit-log-native.js +1 -1
  7. package/dist/commands/core-checkpoint-native.d.ts +27 -0
  8. package/dist/commands/core-checkpoint-native.js +85 -2
  9. package/dist/commands/core-dm-bind-native.js +1 -1
  10. package/dist/commands/core-git-pack-extension-check-native.js +1 -1
  11. package/dist/commands/core-job-native-utils.d.ts +2 -2
  12. package/dist/commands/core-job-native-utils.js +3 -11
  13. package/dist/commands/core-job-notify-native.js +1 -2
  14. package/dist/commands/core-job-probe-native.d.ts +2 -3
  15. package/dist/commands/core-job-probe-native.js +14 -22
  16. package/dist/commands/core-job-remediate-native.js +1 -2
  17. package/dist/commands/core-job-run-native.js +58 -54
  18. package/dist/commands/core-jobs-native.d.ts +1 -3
  19. package/dist/commands/core-jobs-native.js +16 -271
  20. package/dist/commands/core-jobs-schedule.d.ts +7 -0
  21. package/dist/commands/core-jobs-schedule.js +243 -0
  22. package/dist/commands/core-native-utilities.d.ts +2 -3
  23. package/dist/commands/core-native-utilities.js +2 -19
  24. package/dist/commands/core-pin-native.js +1 -1
  25. package/dist/commands/core-refresh-vault-access-native.js +1 -1
  26. package/dist/commands/core-resolve-company-command.js +2 -17
  27. package/dist/commands/core-work-mesh-bind-trusted-native.js +1 -1
  28. package/dist/commands/core.js +1 -17
  29. package/dist/commands/lanes.js +1 -0
  30. package/dist/fast-scaffold.d.ts +1 -1
  31. package/dist/fast-scaffold.js +0 -38
  32. package/dist/lib/capacity/lane-login.js +73 -6
  33. package/dist/lib/capacity/route.d.ts +1 -1
  34. package/dist/lib/capacity/route.js +5 -1
  35. package/dist/lib/capacity/sign-in.d.ts +12 -0
  36. package/dist/lib/capacity/sign-in.js +65 -16
  37. package/dist/lib/daemon/catalog.d.ts +0 -2
  38. package/dist/lib/daemon/catalog.js +3 -6
  39. package/dist/lib/daemon/run.js +0 -7
  40. package/dist/utils/resident-package-preload.js +2 -7
  41. package/package.json +1 -1
  42. package/assets/scaffold/core/scripts/audit-log.sh +0 -384
  43. package/assets/scaffold/core/scripts/git-pack-extension-check.sh +0 -164
  44. package/assets/scaffold/core/scripts/hq-dm-bind.sh +0 -590
  45. package/assets/scaffold/core/scripts/hq-job-notify.sh +0 -389
  46. package/assets/scaffold/core/scripts/hq-job-probe.sh +0 -865
  47. package/assets/scaffold/core/scripts/hq-job-remediate.sh +0 -359
  48. package/assets/scaffold/core/scripts/hq-job-run.sh +0 -1025
  49. package/assets/scaffold/core/scripts/hq-pin.sh +0 -118
  50. package/assets/scaffold/core/scripts/jobs-schedule-parse.sh +0 -310
  51. package/assets/scaffold/core/scripts/jobs-validate.sh +0 -482
  52. package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +0 -836
  53. package/assets/scaffold/core/scripts/refresh-vault-access.sh +0 -188
  54. package/assets/scaffold/core/scripts/resolve-company.sh +0 -331
  55. package/assets/scaffold/core/scripts/work-mesh-live-bind-trusted.sh +0 -184
  56. package/dist/commands/core-job-post-exit-assets.d.ts +0 -8
  57. package/dist/commands/core-job-post-exit-assets.js +0 -37
  58. package/dist/commands/core-native-jobs-flag.d.ts +0 -4
  59. package/dist/commands/core-native-jobs-flag.js +0 -7
  60. package/dist/commands/core-native-utilities-flag.d.ts +0 -4
  61. package/dist/commands/core-native-utilities-flag.js +0 -7
@@ -1,865 +0,0 @@
1
- #!/usr/bin/env bash
2
- # hq-core: public
3
- # hq-job-probe.sh — Outpost requirements probe → hq-pro jobs status (US-008).
4
- #
5
- # On the Outpost, deterministically checks a job's requirements checklist and
6
- # POSTs readiness to POST /outpost/internal/jobs-status. Never rewrites job YAML.
7
- #
8
- # Usage:
9
- # core/scripts/hq-job-probe.sh [options] <job.yaml|job-id>
10
- # core/scripts/hq-job-probe.sh [options] --all
11
- #
12
- # Options:
13
- # --hq-root DIR HQ tree root (default: HQ_ROOT or repo root above core/)
14
- # --dry-run Run checks + print JSON; skip ingest
15
- # --no-ingest Alias of --dry-run
16
- # --json Force JSON on stdout (default)
17
- # -h, --help Show this help
18
- #
19
- # Checks (per requirements / job fields):
20
- # - runtime CLI auth: claude → ~/.claude/.credentials.json; codex → ~/.codex/auth.json
21
- # - requirements.secrets[] visible via `hq secrets list` (names only; never values)
22
- # - optional cwd exists (HQ-root-relative or absolute)
23
- # - company path present when requirements.company / company scope set
24
- #
25
- # Ingest auth (box identity — same trust model as disk-status / relay-status):
26
- # Header: x-outpost-instance-token
27
- # Body: userId + jobId + kind=probe + readiness + probed_at + next_actions + checks
28
- # Identity from (first match):
29
- # 1. OUTPOST_USER_ID + OUTPOST_INSTANCE_TOKEN (+ optional OUTPOST_ID)
30
- # 2. /etc/outpost/codex-identity.env (OUTPOST_USER_ID / OUTPOST_INSTANCE_TOKEN)
31
- # 3. USER_ID= / INSTANCE_TOKEN= / OUTPOST_ID= lines in /usr/local/bin/outpost-runner.sh
32
- # API base: HQ_PRO_API_URL → HQ_API_URL → HQ_VAULT_API_URL → https://hqapi.hq.computer
33
- #
34
- # StatusIngestError (unreachable / 5xx / retryable body):
35
- # Bounded backoff retries; writes ~/.hq/jobs/probes/{id}/last-attempt.json;
36
- # never reports readiness=ready solely because ingest failed; surfaces
37
- # next_action "status ingest failed — retry probe".
38
- #
39
- # Env (tests / overrides):
40
- # HQ_ROOT, HOME, HQ_PRO_API_URL, OUTPOST_USER_ID, OUTPOST_INSTANCE_TOKEN, OUTPOST_ID
41
- # HQ_JOB_PROBE_CLAUDE_CRED, HQ_JOB_PROBE_CODEX_AUTH
42
- # HQ_JOB_PROBE_STATE_DIR, HQ_JOB_PROBE_MAX_ATTEMPTS, HQ_JOB_PROBE_BACKOFF_BASE_SEC
43
- # HQ_JOB_PROBE_SLEEP (set to ":" to no-op sleep), HQ_JOB_PROBE_NOW (ISO timestamp)
44
- # HQ_JOB_PROBE_CURL (curl binary override), HQ_JOB_PROBE_RUNNER (outpost-runner path)
45
- #
46
- # Exit: 0 ready+ingested (or dry-run ready); 1 blocked; 2 usage/config; 3 StatusIngestError
47
-
48
- set -euo pipefail
49
-
50
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
51
- HQ_ROOT="${HQ_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
52
- # shellcheck disable=SC1091
53
- . "$SCRIPT_DIR/hook-lib.sh"
54
- AUTH_HEADER_FILE=""
55
-
56
- cleanup_auth_header_file() {
57
- if [ -n "${AUTH_HEADER_FILE:-}" ]; then
58
- rm -f -- "$AUTH_HEADER_FILE" 2>/dev/null || true
59
- AUTH_HEADER_FILE=""
60
- fi
61
- }
62
- trap cleanup_auth_header_file EXIT
63
-
64
- DRY_RUN=0
65
- PROBE_ALL=0
66
- TARGET=""
67
-
68
- usage() {
69
- sed -n '3,48p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
70
- }
71
-
72
- die() {
73
- echo "hq-job-probe: $*" >&2
74
- exit 2
75
- }
76
-
77
- log() {
78
- echo "hq-job-probe: $*" >&2
79
- }
80
-
81
- while [ $# -gt 0 ]; do
82
- case "$1" in
83
- --hq-root)
84
- HQ_ROOT="${2:-}"
85
- [ -n "$HQ_ROOT" ] || die "--hq-root requires a directory"
86
- shift 2
87
- ;;
88
- --dry-run|--no-ingest)
89
- DRY_RUN=1
90
- shift
91
- ;;
92
- --all)
93
- PROBE_ALL=1
94
- shift
95
- ;;
96
- --json)
97
- shift
98
- ;;
99
- -h|--help)
100
- usage
101
- exit 0
102
- ;;
103
- --)
104
- shift
105
- break
106
- ;;
107
- -*)
108
- die "unknown option: $1"
109
- ;;
110
- *)
111
- if [ -n "$TARGET" ]; then
112
- die "unexpected argument: $1"
113
- fi
114
- TARGET="$1"
115
- shift
116
- ;;
117
- esac
118
- done
119
-
120
- command -v yq >/dev/null 2>&1 || die "yq is required (mikefarah/yq)"
121
- command -v jq >/dev/null 2>&1 || die "jq is required"
122
-
123
- # jq.exe on MSYS may use CRT text mode unless --binary is supported.
124
- case "${OSTYPE:-}:${MSYSTEM:-}" in
125
- msys*:*|*:MINGW*)
126
- # Probe --binary once per script run, in the top-level shell. Most jq
127
- # calls run inside $(...) subshells, so a cache filled lazily inside jq()
128
- # would be lost and the probe would repeat on every call.
129
- if command jq --binary -n 'null' >/dev/null 2>&1; then
130
- HQ_JQ_BINARY_SUPPORTED=1
131
- else
132
- HQ_JQ_BINARY_SUPPORTED=0
133
- fi
134
- jq() {
135
- local stdout_file stderr_file status
136
- stdout_file="$(mktemp)" || return $?
137
- stderr_file="$(mktemp)" || { status=$?; rm -f "$stdout_file"; return "$status"; }
138
- if [[ "$HQ_JQ_BINARY_SUPPORTED" == 1 ]]; then
139
- if command jq --binary "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
140
- command cat "$stdout_file"
141
- command cat "$stderr_file" >&2
142
- else
143
- if command jq "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
144
- command sed -b 's/\r$//' "$stdout_file"
145
- command sed -b 's/\r$//' "$stderr_file" >&2
146
- fi
147
- rm -f "$stdout_file" "$stderr_file"
148
- return "$status"
149
- }
150
- ;;
151
- esac
152
-
153
- iso_now() {
154
- if [ -n "${HQ_JOB_PROBE_NOW:-}" ]; then
155
- printf '%s' "$HQ_JOB_PROBE_NOW"
156
- return
157
- fi
158
- date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null \
159
- || date -u +"%Y-%m-%dT%H:%M:%S+00:00"
160
- }
161
-
162
- probe_sleep() {
163
- local secs="${1:-1}"
164
- # HQ_JOB_PROBE_SLEEP=':'|true → no-op for tests. Never eval arbitrary env (review CRITICAL 4).
165
- if [ "${HQ_JOB_PROBE_SLEEP:-}" = ":" ] || [ "${HQ_JOB_PROBE_SLEEP:-}" = "true" ]; then
166
- return 0
167
- fi
168
- if [[ "$secs" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
169
- sleep "$secs" 2>/dev/null || true
170
- fi
171
- }
172
-
173
- api_base() {
174
- local base
175
- base="${HQ_PRO_API_URL:-${HQ_API_URL:-${HQ_VAULT_API_URL:-https://hqapi.hq.computer}}}"
176
- base="${base%/}"
177
- printf '%s' "$base"
178
- }
179
-
180
- # Load box identity into OUTPOST_USER_ID / OUTPOST_INSTANCE_TOKEN / OUTPOST_ID.
181
- resolve_box_identity() {
182
- local xtrace_was_set=0
183
- case "$-" in
184
- *x*) xtrace_was_set=1; set +x ;;
185
- esac
186
-
187
- if [ -n "${OUTPOST_USER_ID:-}" ] && [ -n "${OUTPOST_INSTANCE_TOKEN:-}" ]; then
188
- [ "$xtrace_was_set" -eq 0 ] || set -x
189
- return 0
190
- fi
191
-
192
- local envf="/etc/outpost/codex-identity.env"
193
- if [ -f "$envf" ]; then
194
- # shellcheck disable=SC1090
195
- . "$envf" 2>/dev/null || true
196
- if [ -n "${OUTPOST_USER_ID:-}" ] && [ -n "${OUTPOST_INSTANCE_TOKEN:-}" ]; then
197
- [ "$xtrace_was_set" -eq 0 ] || set -x
198
- return 0
199
- fi
200
- fi
201
-
202
- local runner="${HQ_JOB_PROBE_RUNNER:-/usr/local/bin/outpost-runner.sh}"
203
- if [ -f "$runner" ]; then
204
- OUTPOST_USER_ID="${OUTPOST_USER_ID:-$(sed -n 's/^USER_ID="\(.*\)"$/\1/p' "$runner" 2>/dev/null | head -1)}"
205
- OUTPOST_INSTANCE_TOKEN="${OUTPOST_INSTANCE_TOKEN:-$(sed -n 's/^INSTANCE_TOKEN="\(.*\)"$/\1/p' "$runner" 2>/dev/null | head -1)}"
206
- OUTPOST_ID="${OUTPOST_ID:-$(sed -n 's/^OUTPOST_ID="\(.*\)"$/\1/p' "$runner" 2>/dev/null | head -1)}"
207
- fi
208
-
209
- if [ -n "${OUTPOST_USER_ID:-}" ] && [ -n "${OUTPOST_INSTANCE_TOKEN:-}" ]; then
210
- [ "$xtrace_was_set" -eq 0 ] || set -x
211
- return 0
212
- fi
213
- [ "$xtrace_was_set" -eq 0 ] || set -x
214
- return 1
215
- }
216
-
217
- create_auth_header_file() {
218
- local xtrace_was_set=0 write_rc=0
219
- AUTH_HEADER_FILE="$(mktemp "${TMPDIR:-/tmp}/hq-job-probe-auth-header.XXXXXX")"
220
- chmod 600 "$AUTH_HEADER_FILE"
221
- case "$-" in
222
- *x*) xtrace_was_set=1; set +x ;;
223
- esac
224
- if printf 'x-outpost-instance-token: %s\n' "$OUTPOST_INSTANCE_TOKEN" >"$AUTH_HEADER_FILE"; then
225
- write_rc=0
226
- else
227
- write_rc=$?
228
- fi
229
- [ "$xtrace_was_set" -eq 0 ] || set -x
230
- return "$write_rc"
231
- }
232
-
233
- find_job_file_by_id() {
234
- local id="$1" f found=""
235
- while IFS= read -r f; do
236
- [ -n "$f" ] || continue
237
- if [ "$(yq -r '.id // ""' "$f" 2>/dev/null)" = "$id" ]; then
238
- if [ -n "$found" ]; then
239
- die "duplicate job id '$id' in registry: $found and $f"
240
- fi
241
- found="$f"
242
- fi
243
- done < <(
244
- {
245
- find "$HQ_ROOT/personal/jobs" \( -name '*.yaml' -o -name '*.yml' \) -type f 2>/dev/null || true
246
- find "$HQ_ROOT/companies" -path '*/jobs/*' \( -name '*.yaml' -o -name '*.yml' \) -type f 2>/dev/null || true
247
- } | sort
248
- )
249
- [ -n "$found" ] || return 1
250
- printf '%s' "$found"
251
- }
252
-
253
- collect_all_job_files() {
254
- {
255
- find "$HQ_ROOT/personal/jobs" \( -name '*.yaml' -o -name '*.yml' \) -type f 2>/dev/null || true
256
- find "$HQ_ROOT/companies" -path '*/jobs/*' \( -name '*.yaml' -o -name '*.yml' \) -type f 2>/dev/null || true
257
- } | sort
258
- }
259
-
260
- claude_cred_path() {
261
- printf '%s' "${HQ_JOB_PROBE_CLAUDE_CRED:-${HOME}/.claude/.credentials.json}"
262
- }
263
-
264
- codex_auth_path() {
265
- printf '%s' "${HQ_JOB_PROBE_CODEX_AUTH:-${HOME}/.codex/auth.json}"
266
- }
267
-
268
- check_runtime_auth() {
269
- # prints ok|missing|no_cli ; uses global RUNTIME
270
- local runtime="$1"
271
- case "$runtime" in
272
- claude)
273
- if ! command -v claude >/dev/null 2>&1; then
274
- printf 'no_cli'
275
- return 1
276
- fi
277
- if [ -s "$(claude_cred_path)" ]; then
278
- printf 'ok'
279
- return 0
280
- fi
281
- printf 'missing'
282
- return 1
283
- ;;
284
- codex)
285
- if ! command -v codex >/dev/null 2>&1; then
286
- printf 'no_cli'
287
- return 1
288
- fi
289
- if [ -s "$(codex_auth_path)" ]; then
290
- printf 'ok'
291
- return 0
292
- fi
293
- printf 'missing'
294
- return 1
295
- ;;
296
- *)
297
- printf 'unknown_runtime'
298
- return 1
299
- ;;
300
- esac
301
- }
302
-
303
- # List visible secret NAMES only (never values). stdout: one name per line.
304
- list_secret_names() {
305
- local company="${1:-}"
306
- local out rc=0
307
- if [ -n "$company" ]; then
308
- out="$(hq secrets list --company "$company" 2>/dev/null)" || rc=$?
309
- else
310
- out="$(hq secrets list --personal 2>/dev/null)" || rc=$?
311
- fi
312
- if [ "$rc" -ne 0 ]; then
313
- return "$rc"
314
- fi
315
- # Skip banner / header rows; emit first whitespace-delimited field when it
316
- # looks like a vault key name (UPPER_SNAKE / alnum._:-).
317
- printf '%s\n' "$out" | awk '
318
- BEGIN { ignore = 1 }
319
- /^NAME([[:space:]]|$)/ { ignore = 0; next }
320
- ignore { next }
321
- /^[[:space:]]*$/ { next }
322
- /^Secrets / { next }
323
- {
324
- name = $1
325
- if (name ~ /^[A-Za-z][A-Za-z0-9._:\/-]{0,127}$/) print name
326
- }
327
- '
328
- }
329
-
330
- secret_name_visible() {
331
- local needle="$1"
332
- local names_file="$2"
333
- grep -Fxq -- "$needle" "$names_file" 2>/dev/null
334
- }
335
-
336
- state_dir_for_job() {
337
- local id="$1"
338
- local base="${HQ_JOB_PROBE_STATE_DIR:-${HOME}/.hq/jobs/probes}"
339
- printf '%s/%s' "$base" "$id"
340
- }
341
-
342
- write_attempt_marker() {
343
- local id="$1" payload="$2"
344
- local dir
345
- dir="$(state_dir_for_job "$id")"
346
- mkdir -p "$dir"
347
- printf '%s\n' "$payload" >"$dir/last-attempt.json"
348
- # Never chmod secrets; marker is non-secret forensics.
349
- chmod 600 "$dir/last-attempt.json" 2>/dev/null || true
350
- }
351
-
352
- # Box-local readiness cache for reconciler (US-004). Written only after a
353
- # successful ingest (or dry-run) so timers never arm on a failed ingest.
354
- write_status_cache() {
355
- local id="$1" readiness="$2" source="${3:-probe-ingest}"
356
- local base="${HQ_JOB_STATUS_CACHE_DIR:-${HOME}/.hq/jobs/status-cache}"
357
- mkdir -p "$base"
358
- jq -nc \
359
- --arg job_id "$id" \
360
- --arg readiness "$readiness" \
361
- --arg updated_at "$(iso_now)" \
362
- --arg source "$source" \
363
- '{job_id:$job_id,readiness:$readiness,updated_at:$updated_at,source:$source}' \
364
- >"$base/${id}.json"
365
- chmod 600 "$base/${id}.json" 2>/dev/null || true
366
- }
367
-
368
- # POST probe ingest with bounded backoff. Sets INGEST_HTTP, INGEST_BODY,
369
- # and INGEST_RETRYABLE.
370
- ingest_probe() {
371
- local payload="$1"
372
- local max_attempts base_sec attempt delay http body curl_bin url tmp_body tmp_hdr
373
- max_attempts="${HQ_JOB_PROBE_MAX_ATTEMPTS:-3}"
374
- base_sec="${HQ_JOB_PROBE_BACKOFF_BASE_SEC:-1}"
375
- curl_bin="${HQ_JOB_PROBE_CURL:-curl}"
376
- url="$(api_base)/outpost/internal/jobs-status"
377
-
378
- INGEST_HTTP=0
379
- INGEST_BODY=""
380
- INGEST_RETRYABLE=0
381
-
382
- if ! command -v "$curl_bin" >/dev/null 2>&1 && [ "$curl_bin" = "curl" ]; then
383
- if ! command -v curl >/dev/null 2>&1; then
384
- INGEST_BODY='{"error":true,"code":"StatusIngestError","step":"config","message":"curl not found","retryable":true}'
385
- INGEST_RETRYABLE=1
386
- return 1
387
- fi
388
- fi
389
-
390
- attempt=1
391
- while [ "$attempt" -le "$max_attempts" ]; do
392
- tmp_body="$(mktemp "${TMPDIR:-/tmp}/hq-job-probe-body.XXXXXX")"
393
- tmp_hdr="$(mktemp "${TMPDIR:-/tmp}/hq-job-probe-hdr.XXXXXX")"
394
- create_auth_header_file
395
- http=0
396
- set +e
397
- "$curl_bin" -sS -m 20 -X POST "$url" \
398
- -H "content-type: application/json" \
399
- -H "@${AUTH_HEADER_FILE}" \
400
- --data "$payload" \
401
- -D "$tmp_hdr" \
402
- -o "$tmp_body"
403
- local curl_rc=$?
404
- set -e
405
- if [ "$curl_rc" -eq 0 ]; then
406
- http="$(awk 'BEGIN{c=0} /^HTTP\//{c=$2} END{print c+0}' "$tmp_hdr" 2>/dev/null || echo 0)"
407
- body="$(cat "$tmp_body" 2>/dev/null || true)"
408
- else
409
- http=0
410
- body="$(printf '{"error":true,"code":"StatusIngestError","step":"lookup","message":"curl failed rc=%s (API unreachable)","retryable":true}' "$curl_rc")"
411
- fi
412
- rm -f "$tmp_body" "$tmp_hdr" "$AUTH_HEADER_FILE"
413
- AUTH_HEADER_FILE=""
414
-
415
- INGEST_HTTP="$http"
416
- INGEST_BODY="$body"
417
-
418
- if [ "$http" -ge 200 ] && [ "$http" -lt 300 ]; then
419
- INGEST_RETRYABLE=0
420
- return 0
421
- fi
422
-
423
- # Retry on transport failure, 5xx, or explicit retryable StatusIngestError.
424
- local retry=0
425
- if [ "$http" -eq 0 ] || [ "$http" -ge 500 ]; then
426
- retry=1
427
- elif printf '%s' "$body" | jq -e '.retryable == true' >/dev/null 2>&1; then
428
- retry=1
429
- elif printf '%s' "$body" | jq -e '.code == "StatusIngestError" and (.step == "persist" or .step == "lookup" or .step == "config")' >/dev/null 2>&1; then
430
- retry=1
431
- fi
432
- INGEST_RETRYABLE="$retry"
433
-
434
- if [ "$retry" -ne 1 ] || [ "$attempt" -ge "$max_attempts" ]; then
435
- break
436
- fi
437
- delay=$((base_sec * (1 << (attempt - 1))))
438
- log "ingest attempt $attempt/$max_attempts failed (http=$http); backoff ${delay}s"
439
- probe_sleep "$delay"
440
- attempt=$((attempt + 1))
441
- done
442
-
443
- return 1
444
- }
445
-
446
- probe_one_file() {
447
- local job_file="$1"
448
- [ -f "$job_file" ] || die "job file not found: $job_file"
449
-
450
- local id name runtime req_runtime company cwd_rel
451
- id="$(yq -r '.id // ""' "$job_file")"
452
- name="$(yq -r '.name // ""' "$job_file")"
453
- runtime="$(yq -r '.runtime // ""' "$job_file")"
454
- req_runtime="$(yq -r '.requirements.runtime // ""' "$job_file")"
455
- company="$(yq -r '.requirements.company // ""' "$job_file")"
456
- cwd_rel="$(yq -r '.requirements.cwd // .cwd // ""' "$job_file")"
457
-
458
- [ -n "$id" ] || die "job missing id: $job_file"
459
- if [ -n "$req_runtime" ]; then
460
- runtime="$req_runtime"
461
- fi
462
- [ -n "$runtime" ] || die "job missing runtime: $job_file"
463
-
464
- local probed_at event_id
465
- probed_at="$(iso_now)"
466
- event_id="probe-${probed_at}-${id}"
467
-
468
- # bash 3.2 + set -u: keep action/missing lists as newline strings, not arrays
469
- local next_actions_nl=""
470
- local auth_status secrets_status cwd_status company_status
471
- local blocked=0
472
- local secrets_missing_json="[]"
473
- local names_tmp
474
- local _action_line
475
-
476
- # --- runtime auth ---
477
- auth_status="$(check_runtime_auth "$runtime" || true)"
478
- case "$auth_status" in
479
- ok) ;;
480
- no_cli)
481
- blocked=1
482
- _action_line="install ${runtime} CLI on Outpost and re-login"
483
- next_actions_nl="${next_actions_nl:+${next_actions_nl}$'\n'}${_action_line}"
484
- ;;
485
- missing)
486
- blocked=1
487
- _action_line="device re-login for ${runtime} on Outpost (cached credentials missing)"
488
- next_actions_nl="${next_actions_nl:+${next_actions_nl}$'\n'}${_action_line}"
489
- ;;
490
- *)
491
- blocked=1
492
- _action_line="unsupported runtime '${runtime}' — fix job requirements.runtime"
493
- next_actions_nl="${next_actions_nl:+${next_actions_nl}$'\n'}${_action_line}"
494
- auth_status="unknown_runtime"
495
- ;;
496
- esac
497
-
498
- # --- secrets (names only via hq secrets list) ---
499
- secrets_status="ok"
500
- names_tmp="$(mktemp "${TMPDIR:-/tmp}/hq-job-probe-secrets.XXXXXX")"
501
- local list_rc=0
502
- list_secret_names "$company" >"$names_tmp" || list_rc=$?
503
- if [ "$list_rc" -ne 0 ]; then
504
- blocked=1
505
- secrets_status="list_failed"
506
- if [ -n "$company" ]; then
507
- _action_line="hq secrets list --company ${company} failed on Outpost — check hq login / vault access"
508
- else
509
- _action_line="hq secrets list --personal failed on Outpost — check hq login / vault access"
510
- fi
511
- next_actions_nl="${next_actions_nl:+${next_actions_nl}$'\n'}${_action_line}"
512
- else
513
- local missing_names_nl=""
514
- local secret_count
515
- secret_count="$(yq -r '.requirements.secrets // [] | length' "$job_file")"
516
- if [ "$secret_count" != "null" ] && [ "${secret_count:-0}" -gt 0 ] 2>/dev/null; then
517
- local i secret_name
518
- i=0
519
- while [ "$i" -lt "$secret_count" ]; do
520
- secret_name="$(yq -r ".requirements.secrets[$i]" "$job_file")"
521
- i=$((i + 1))
522
- [ -n "$secret_name" ] && [ "$secret_name" != "null" ] || continue
523
- if ! secret_name_visible "$secret_name" "$names_tmp"; then
524
- if [ -z "$missing_names_nl" ]; then
525
- missing_names_nl="$secret_name"
526
- else
527
- missing_names_nl="${missing_names_nl}"$'\n'"${secret_name}"
528
- fi
529
- fi
530
- done
531
- fi
532
- if [ -n "$missing_names_nl" ]; then
533
- blocked=1
534
- secrets_status="missing"
535
- local sn
536
- while IFS= read -r sn; do
537
- [ -n "$sn" ] || continue
538
- _action_line="hq secrets share ${sn} (grant Outpost owner visibility; names only — never paste values)"
539
- next_actions_nl="${next_actions_nl:+${next_actions_nl}$'\n'}${_action_line}"
540
- done <<EOF
541
- $missing_names_nl
542
- EOF
543
- secrets_missing_json="$(printf '%s\n' "$missing_names_nl" | jq -R . | jq -s -c .)"
544
- fi
545
- fi
546
- rm -f "$names_tmp"
547
-
548
- # --- cwd (HQ-root-relative only; no absolute / no .. escape) ---
549
- cwd_status="ok"
550
- if [ -n "$cwd_rel" ] && [ "$cwd_rel" != "null" ]; then
551
- local cwd_abs cwd_part cwd_bad=0
552
- case "$cwd_rel" in
553
- /*)
554
- blocked=1
555
- cwd_status="invalid"
556
- next_actions_nl="${next_actions_nl:+$next_actions_nl$'\n'}fix cwd to HQ-root-relative path (absolute not allowed)"
557
- cwd_bad=1
558
- ;;
559
- esac
560
- if [ "$cwd_bad" -eq 0 ]; then
561
- local IFS='/'
562
- # shellcheck disable=SC2086
563
- for cwd_part in $cwd_rel; do
564
- if [ "$cwd_part" = ".." ]; then
565
- blocked=1
566
- cwd_status="invalid"
567
- next_actions_nl="${next_actions_nl:+$next_actions_nl$'\n'}fix cwd — remove '..' path components"
568
- cwd_bad=1
569
- break
570
- fi
571
- done
572
- unset IFS
573
- fi
574
- if [ "$cwd_bad" -eq 0 ]; then
575
- # Portable resolve: GNU realpath -m, BSD realpath (existing paths), else hq_normpath.
576
- local hq_canon=""
577
- hq_canon="$(realpath -m "$HQ_ROOT" 2>/dev/null || realpath "$HQ_ROOT" 2>/dev/null || true)"
578
- cwd_abs="$(realpath -m "$HQ_ROOT/$cwd_rel" 2>/dev/null || true)"
579
- if [ -z "$cwd_abs" ] && [ -d "$HQ_ROOT/$cwd_rel" ]; then
580
- cwd_abs="$(realpath "$HQ_ROOT/$cwd_rel" 2>/dev/null || true)"
581
- fi
582
- if [ -z "$hq_canon" ] || [ -z "$cwd_abs" ]; then
583
- hq_canon="$(hq_normpath "$HQ_ROOT")"
584
- cwd_abs="$(hq_normpath "$HQ_ROOT/$cwd_rel")"
585
- fi
586
- case "$cwd_abs" in
587
- "$hq_canon"|"$hq_canon"/*) ;;
588
- *)
589
- blocked=1
590
- cwd_status="invalid"
591
- next_actions_nl="${next_actions_nl:+$next_actions_nl$'\n'}fix cwd — must resolve under HQ root"
592
- cwd_bad=1
593
- ;;
594
- esac
595
- fi
596
- if [ "$cwd_bad" -eq 0 ]; then
597
- if [ ! -d "$cwd_abs" ]; then
598
- blocked=1
599
- cwd_status="missing"
600
- next_actions_nl="${next_actions_nl:+$next_actions_nl$'\n'}create cwd on Outpost HQ tree: ${cwd_rel}"
601
- fi
602
- fi
603
- else
604
- cwd_status="skipped"
605
- fi
606
-
607
- # --- company path ---
608
- company_status="ok"
609
- if [ -n "$company" ] && [ "$company" != "null" ]; then
610
- if [ ! -d "$HQ_ROOT/companies/$company" ]; then
611
- blocked=1
612
- company_status="missing"
613
- next_actions+=("company path missing on Outpost: companies/${company} — sync or join company")
614
- fi
615
- else
616
- company_status="skipped"
617
- fi
618
-
619
- local readiness="ready"
620
- if [ "$blocked" -eq 1 ]; then
621
- readiness="blocked"
622
- fi
623
-
624
- local actions_json
625
- if [ -z "$next_actions_nl" ]; then
626
- actions_json='[]'
627
- else
628
- actions_json="$(printf '%s\n' "$next_actions_nl" | awk 'NF && !seen[$0]++' | jq -R . | jq -s -c .)"
629
- fi
630
-
631
- local checks_json
632
- checks_json="$(jq -nc \
633
- --arg runtime "$runtime" \
634
- --arg auth "$auth_status" \
635
- --arg secrets "$secrets_status" \
636
- --argjson missing "$secrets_missing_json" \
637
- --arg cwd "$cwd_status" \
638
- --arg company "$company_status" \
639
- --arg company_slug "$company" \
640
- '{
641
- runtime: $runtime,
642
- auth: $auth,
643
- secrets: {status: $secrets, missing: $missing},
644
- cwd: $cwd,
645
- company: (if $company_slug == "" then {status: $company} else {status: $company, slug: $company_slug} end)
646
- }')"
647
-
648
- local user_id="${OUTPOST_USER_ID:-}"
649
- local outpost_id="${OUTPOST_ID:-}"
650
-
651
- # Build ingest payload (snake_case fields accepted by US-009 handler).
652
- local payload
653
- payload="$(jq -nc \
654
- --arg userId "$user_id" \
655
- --arg jobId "$id" \
656
- --arg readiness "$readiness" \
657
- --arg probed_at "$probed_at" \
658
- --arg event_id "$event_id" \
659
- --arg outpostId "$outpost_id" \
660
- --argjson next_actions "$actions_json" \
661
- --argjson checks "$checks_json" \
662
- '{
663
- userId: $userId,
664
- jobId: $jobId,
665
- kind: "probe",
666
- readiness: $readiness,
667
- probed_at: $probed_at,
668
- event_id: $event_id,
669
- next_actions: $next_actions,
670
- checks: $checks
671
- } + (if $outpostId != "" then {outpostId: $outpostId} else {} end)')"
672
-
673
- local result_ingest="skipped"
674
- local result_http=0
675
- local ingest_error=0
676
-
677
- if [ "$DRY_RUN" -eq 1 ]; then
678
- result_ingest="dry_run"
679
- write_status_cache "$id" "$readiness" "probe-dry-run"
680
- else
681
- if ! resolve_box_identity; then
682
- # Cannot authenticate to hq-pro — treat as StatusIngestError path.
683
- ingest_error=1
684
- result_ingest="StatusIngestError"
685
- local marker
686
- marker="$(jq -nc \
687
- --arg job_id "$id" \
688
- --arg probed_at "$probed_at" \
689
- --arg local_readiness "$readiness" \
690
- --arg message "missing Outpost identity (OUTPOST_USER_ID / OUTPOST_INSTANCE_TOKEN)" \
691
- --argjson checks "$checks_json" \
692
- --argjson next_actions "$(jq -nc --argjson a "$actions_json" '$a + ["status ingest failed — retry probe"]')" \
693
- '{
694
- job_id: $job_id,
695
- probed_at: $probed_at,
696
- local_readiness: $local_readiness,
697
- ingest: {ok: false, code: "StatusIngestError", step: "config", message: $message, retryable: true},
698
- reported_readiness: null,
699
- next_actions: $next_actions,
700
- checks: $checks
701
- }')"
702
- write_attempt_marker "$id" "$marker"
703
- # Never claim ready when ingest cannot run.
704
- readiness="unknown"
705
- actions_json="$(jq -nc --argjson a "$actions_json" '
706
- ($a + ["status ingest failed — retry probe"]) as $x
707
- | reduce $x[] as $i ([]; if index($i) then . else . + [$i] end)
708
- ')"
709
- else
710
- # Refresh payload with resolved userId
711
- payload="$(jq -nc \
712
- --arg userId "$OUTPOST_USER_ID" \
713
- --arg jobId "$id" \
714
- --arg readiness "$readiness" \
715
- --arg probed_at "$probed_at" \
716
- --arg event_id "$event_id" \
717
- --arg outpostId "${OUTPOST_ID:-}" \
718
- --argjson next_actions "$actions_json" \
719
- --argjson checks "$checks_json" \
720
- '{
721
- userId: $userId,
722
- jobId: $jobId,
723
- kind: "probe",
724
- readiness: $readiness,
725
- probed_at: $probed_at,
726
- event_id: $event_id,
727
- next_actions: $next_actions,
728
- checks: $checks
729
- } + (if $outpostId != "" then {outpostId: $outpostId} else {} end)')"
730
-
731
- local intended_readiness="$readiness"
732
- if ingest_probe "$payload"; then
733
- result_ingest="ok"
734
- result_http="$INGEST_HTTP"
735
- local marker_ok
736
- marker_ok="$(jq -nc \
737
- --arg job_id "$id" \
738
- --arg probed_at "$probed_at" \
739
- --arg local_readiness "$intended_readiness" \
740
- --argjson http "$INGEST_HTTP" \
741
- --argjson checks "$checks_json" \
742
- --argjson next_actions "$actions_json" \
743
- '{
744
- job_id: $job_id,
745
- probed_at: $probed_at,
746
- local_readiness: $local_readiness,
747
- ingest: {ok: true, http: $http},
748
- reported_readiness: $local_readiness,
749
- next_actions: $next_actions,
750
- checks: $checks
751
- }')"
752
- write_attempt_marker "$id" "$marker_ok"
753
- write_status_cache "$id" "$intended_readiness" "probe-ingest"
754
- else
755
- ingest_error=1
756
- result_ingest="StatusIngestError"
757
- result_http="$INGEST_HTTP"
758
- # Leave prior API readiness unchanged (we did not successfully write).
759
- # Never report ready solely because ingest failed.
760
- local fail_actions body_raw
761
- fail_actions="$(jq -nc --argjson a "$actions_json" '$a + ["status ingest failed — retry probe"]')"
762
- body_raw="${INGEST_BODY:-}"
763
- [ -n "$body_raw" ] || body_raw="{}"
764
- # Prefer --arg + fromjson so nested JSON cannot break ${:-{}} brace parsing.
765
- local marker_fail
766
- marker_fail="$(jq -nc \
767
- --arg job_id "$id" \
768
- --arg probed_at "$probed_at" \
769
- --arg local_readiness "$intended_readiness" \
770
- --argjson http "$INGEST_HTTP" \
771
- --arg body "$body_raw" \
772
- --argjson checks "$checks_json" \
773
- --argjson next_actions "$fail_actions" \
774
- --argjson retryable "${INGEST_RETRYABLE:-1}" \
775
- '{
776
- job_id: $job_id,
777
- probed_at: $probed_at,
778
- local_readiness: $local_readiness,
779
- ingest: {
780
- ok: false,
781
- code: "StatusIngestError",
782
- http: $http,
783
- retryable: ($retryable == 1),
784
- body: (try ($body | fromjson) catch {raw: $body})
785
- },
786
- reported_readiness: null,
787
- next_actions: $next_actions,
788
- checks: $checks
789
- }')"
790
- write_attempt_marker "$id" "$marker_fail"
791
- readiness="unknown"
792
- actions_json="$fail_actions"
793
- fi
794
- fi
795
- fi
796
-
797
- local result
798
- result="$(jq -nc \
799
- --arg job_id "$id" \
800
- --arg name "$name" \
801
- --arg job_file "$job_file" \
802
- --arg readiness "$readiness" \
803
- --arg probed_at "$probed_at" \
804
- --arg ingest "$result_ingest" \
805
- --argjson http "$result_http" \
806
- --argjson checks "$checks_json" \
807
- --argjson next_actions "$actions_json" \
808
- --argjson dry_run "$DRY_RUN" \
809
- '{
810
- job_id: $job_id,
811
- name: $name,
812
- job_file: $job_file,
813
- readiness: $readiness,
814
- probed_at: $probed_at,
815
- ingest: $ingest,
816
- http: $http,
817
- dry_run: ($dry_run == 1),
818
- checks: $checks,
819
- next_actions: $next_actions
820
- }')"
821
-
822
- printf '%s\n' "$result"
823
-
824
- if [ "$ingest_error" -eq 1 ]; then
825
- return 3
826
- fi
827
- if [ "$blocked" -eq 1 ]; then
828
- return 1
829
- fi
830
- return 0
831
- }
832
-
833
- # --- main ---
834
-
835
- if [ "$PROBE_ALL" -eq 1 ]; then
836
- [ -z "$TARGET" ] || die "--all does not take a job argument"
837
- overall=0
838
- any=0
839
- while IFS= read -r f; do
840
- [ -n "$f" ] || continue
841
- any=1
842
- rc=0
843
- probe_one_file "$f" || rc=$?
844
- if [ "$rc" -gt "$overall" ]; then
845
- overall="$rc"
846
- fi
847
- done < <(collect_all_job_files)
848
- [ "$any" -eq 1 ] || die "no job YAML found under personal/jobs or companies/*/jobs in $HQ_ROOT"
849
- exit "$overall"
850
- fi
851
-
852
- [ -n "$TARGET" ] || die "usage: hq-job-probe.sh [--dry-run] <job.yaml|job-id> | --all"
853
-
854
- JOB_FILE=""
855
- if [ -f "$TARGET" ]; then
856
- JOB_FILE="$TARGET"
857
- elif [ -f "$HQ_ROOT/$TARGET" ]; then
858
- JOB_FILE="$HQ_ROOT/$TARGET"
859
- else
860
- JOB_FILE="$(find_job_file_by_id "$TARGET")" || die "job id not found in registry: $TARGET"
861
- fi
862
-
863
- rc=0
864
- probe_one_file "$JOB_FILE" || rc=$?
865
- exit "$rc"