@indigoai-us/hq-cli 5.290.0 → 5.292.0

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 (83) hide show
  1. package/CHANGELOG.md +66 -3
  2. package/assets/scaffold/core/scripts/handoff-finalize.sh +18 -0
  3. package/assets/scaffold/core/scripts/handoff-post.sh +71 -6
  4. package/assets/scaffold/core/scripts/hook-lib.sh +345 -50
  5. package/assets/scaffold/core/scripts/hq-delegate-bundle.sh +307 -0
  6. package/assets/scaffold/core/scripts/hq-delegate-grant.sh +226 -0
  7. package/assets/scaffold/core/scripts/hq-delegate-publish.sh +91 -0
  8. package/assets/scaffold/core/scripts/hq-delegate-send.sh +259 -0
  9. package/assets/scaffold/core/scripts/hq-delegate-verify.sh +213 -0
  10. package/assets/scaffold/core/scripts/hq-job-notify.sh +389 -0
  11. package/assets/scaffold/core/scripts/hq-job-probe.sh +835 -0
  12. package/assets/scaffold/core/scripts/hq-job-remediate.sh +359 -0
  13. package/assets/scaffold/core/scripts/hq-job-run.sh +1025 -0
  14. package/assets/scaffold/core/scripts/hq-session.sh +182 -21
  15. package/assets/scaffold/core/scripts/jobs-schedule-parse.sh +310 -0
  16. package/assets/scaffold/core/scripts/jobs-validate.sh +451 -0
  17. package/assets/scaffold/core/scripts/lib/session-id.sh +3 -2
  18. package/assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh +2 -1
  19. package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +830 -0
  20. package/dist/command-catalog.generated.d.ts +165 -1
  21. package/dist/command-catalog.generated.js +205 -1
  22. package/dist/commands/cloud.d.ts +26 -14
  23. package/dist/commands/cloud.js +207 -56
  24. package/dist/commands/core-delegate-w1b.d.ts +8 -0
  25. package/dist/commands/core-delegate-w1b.js +1049 -0
  26. package/dist/commands/core-delegate.d.ts +28 -0
  27. package/dist/commands/core-delegate.js +15 -13
  28. package/dist/commands/core-handoff.js +152 -23
  29. package/dist/commands/core-jobs-native.d.ts +23 -0
  30. package/dist/commands/core-jobs-native.js +1331 -0
  31. package/dist/commands/core-native-jobs-flag.d.ts +4 -0
  32. package/dist/commands/core-native-jobs-flag.js +7 -0
  33. package/dist/commands/core-ontology.d.ts +22 -4
  34. package/dist/commands/core-ontology.js +90 -8
  35. package/dist/commands/core-policy.js +24 -5
  36. package/dist/commands/core.js +17 -0
  37. package/dist/commands/integrations-core.js +7 -0
  38. package/dist/commands/lanes.js +111 -35
  39. package/dist/commands/members.js +5 -0
  40. package/dist/commands/mesh.js +46 -25
  41. package/dist/commands/personal-targeted-push-flag.d.ts +4 -0
  42. package/dist/commands/personal-targeted-push-flag.js +7 -0
  43. package/dist/commands/personal-targeted-push-paths.d.ts +3 -0
  44. package/dist/commands/personal-targeted-push-paths.js +67 -0
  45. package/dist/commands/secrets.js +9 -0
  46. package/dist/commands/skill.d.ts +3 -0
  47. package/dist/commands/skill.js +28 -0
  48. package/dist/fast-core.js +1 -0
  49. package/dist/lib/billing/free-month.d.ts +18 -0
  50. package/dist/lib/billing/free-month.js +31 -0
  51. package/dist/lib/billing/plan-limit-refusals.d.ts +69 -0
  52. package/dist/lib/billing/plan-limit-refusals.js +187 -0
  53. package/dist/lib/billing/plan-lock.d.ts +2 -6
  54. package/dist/lib/billing/plan-lock.js +5 -16
  55. package/dist/lib/checkpoint-ontology/drop-aliases.d.ts +41 -0
  56. package/dist/lib/checkpoint-ontology/drop-aliases.js +76 -0
  57. package/dist/lib/checkpoint-ontology/entity-file.d.ts +6 -3
  58. package/dist/lib/checkpoint-ontology/entity-file.js +12 -6
  59. package/dist/lib/checkpoint-ontology/merge-copies.d.ts +27 -8
  60. package/dist/lib/checkpoint-ontology/merge-copies.js +64 -97
  61. package/dist/lib/core-utils/secret-patterns.d.ts +3 -1
  62. package/dist/lib/core-utils/secret-patterns.js +3 -2
  63. package/dist/lib/daemon/flag.d.ts +2 -0
  64. package/dist/lib/daemon/flag.js +2 -0
  65. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  66. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  67. package/dist/lib/lanes/autopilot.d.ts +69 -0
  68. package/dist/lib/lanes/autopilot.js +418 -0
  69. package/dist/lib/lanes/mesh-events.js +2 -0
  70. package/dist/lib/lanes/store.js +11 -6
  71. package/dist/lib/mesh/api.js +4 -0
  72. package/dist/lib/mesh/live/daemon/state.d.ts +7 -1
  73. package/dist/lib/mesh/live/daemon/state.js +34 -2
  74. package/dist/main.js +23 -12
  75. package/dist/utils/plan-gate-error.d.ts +24 -0
  76. package/dist/utils/plan-gate-error.js +219 -9
  77. package/dist/utils/scaffold-sibling-references.js +4 -4
  78. package/dist/utils/self-update.d.ts +6 -0
  79. package/dist/utils/self-update.js +28 -6
  80. package/dist/utils/staged-install.d.ts +34 -0
  81. package/dist/utils/staged-install.js +296 -0
  82. package/package.json +2 -2
  83. package/scripts/mesh-daemon-if-stale.mjs +4 -0
@@ -0,0 +1,451 @@
1
+ #!/usr/bin/env bash
2
+ # hq-core: public
3
+ # jobs-validate.sh — validate Outpost scheduled job YAML registry files.
4
+ #
5
+ # Usage:
6
+ # core/scripts/jobs-validate.sh <file-or-dir> [file-or-dir...]
7
+ #
8
+ # Exits 0 when all jobs are valid; non-zero with per-field errors on stderr.
9
+ # Schema: core/knowledge/public/hq-core/outpost-jobs-spec.md
10
+ #
11
+ # Requires: yq (mikefarah), jq. Timezone checked via IANA-looking regex only.
12
+
13
+ set -euo pipefail
14
+
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+
17
+ # shellcheck source=lib/secret-patterns.sh
18
+ . "$SCRIPT_DIR/lib/secret-patterns.sh"
19
+
20
+ ERRORS=0
21
+ declare -a SEEN_IDS=()
22
+
23
+ die_usage() {
24
+ echo "usage: jobs-validate.sh <file-or-dir> [file-or-dir...]" >&2
25
+ exit 2
26
+ }
27
+
28
+ err() {
29
+ # err <file> <field> <message>
30
+ printf 'jobs-validate: %s: %s: %s\n' "$1" "$2" "$3" >&2
31
+ ERRORS=$((ERRORS + 1))
32
+ }
33
+
34
+ command -v yq >/dev/null 2>&1 || {
35
+ echo "jobs-validate: yq is required (mikefarah/yq)" >&2
36
+ exit 2
37
+ }
38
+ command -v jq >/dev/null 2>&1 || {
39
+ echo "jobs-validate: jq is required" >&2
40
+ exit 2
41
+ }
42
+
43
+ [ "$#" -ge 1 ] || die_usage
44
+
45
+ collect_files() {
46
+ local arg="$1"
47
+ if [ -f "$arg" ]; then
48
+ case "$arg" in
49
+ *.yaml|*.yml) printf '%s\n' "$arg" ;;
50
+ *) err "$arg" "path" "not a .yaml/.yml job file" ;;
51
+ esac
52
+ return
53
+ fi
54
+ if [ -d "$arg" ]; then
55
+ # Prefer find -print0 / sort for stable duplicate-id detection order
56
+ find "$arg" \( -name '*.yaml' -o -name '*.yml' \) -type f | sort
57
+ return
58
+ fi
59
+ err "$arg" "path" "file or directory not found"
60
+ }
61
+
62
+ cron_num_in_range() {
63
+ local n="$1" min="$2" max="$3"
64
+ [[ "$n" =~ ^[0-9]+$ ]] || return 1
65
+ ((10#$n >= min && 10#$n <= max))
66
+ }
67
+
68
+ is_valid_cron_field() {
69
+ # $1=value $2=min $3=max — *, */n, a-b, a-b/n, lists, dow/month names
70
+ local v="$1" min="$2" max="$3" part base step a b
71
+ local IFS=','
72
+ set -f
73
+ # shellcheck disable=SC2086
74
+ set -- $v
75
+ set +f
76
+ for part in "$@"; do
77
+ case "$part" in
78
+ \*) continue ;;
79
+ \*/*)
80
+ step="${part#*/}"
81
+ [[ "$step" =~ ^[1-9][0-9]*$ ]] || return 1
82
+ continue
83
+ ;;
84
+ sun|mon|tue|wed|thu|fri|sat|SUN|MON|TUE|WED|THU|FRI|SAT)
85
+ [ "$max" -eq 7 ] || return 1
86
+ continue
87
+ ;;
88
+ jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)
89
+ [ "$max" -eq 12 ] || return 1
90
+ continue
91
+ ;;
92
+ esac
93
+ if [[ "$part" =~ ^([0-9]+)-([0-9]+)(/([1-9][0-9]*))?$ ]]; then
94
+ a="${BASH_REMATCH[1]}"
95
+ b="${BASH_REMATCH[2]}"
96
+ cron_num_in_range "$a" "$min" "$max" || return 1
97
+ cron_num_in_range "$b" "$min" "$max" || return 1
98
+ ((10#$a <= 10#$b)) || return 1
99
+ continue
100
+ fi
101
+ if [[ "$part" =~ ^([0-9]+)(/([1-9][0-9]*))?$ ]]; then
102
+ base="${BASH_REMATCH[1]}"
103
+ cron_num_in_range "$base" "$min" "$max" || return 1
104
+ continue
105
+ fi
106
+ return 1
107
+ done
108
+ return 0
109
+ }
110
+
111
+ validate_cron() {
112
+ local file="$1" cron="$2"
113
+ # strip surrounding quotes leftovers
114
+ cron="${cron#"${cron%%[![:space:]]*}"}"
115
+ cron="${cron%"${cron##*[![:space:]]}"}"
116
+ local -a fields
117
+ # noglob: cron uses '*' which must not expand against the filesystem
118
+ set -f
119
+ # shellcheck disable=SC2206
120
+ fields=($cron)
121
+ set +f
122
+ if [ "${#fields[@]}" -ne 5 ]; then
123
+ err "$file" "schedule" "invalid cron — expected 5 fields (minute hour dom month dow), got ${#fields[@]}"
124
+ return
125
+ fi
126
+ is_valid_cron_field "${fields[0]}" 0 59 || err "$file" "schedule" "invalid cron minute field '${fields[0]}'"
127
+ is_valid_cron_field "${fields[1]}" 0 23 || err "$file" "schedule" "invalid cron hour field '${fields[1]}'"
128
+ is_valid_cron_field "${fields[2]}" 1 31 || err "$file" "schedule" "invalid cron day-of-month field '${fields[2]}'"
129
+ is_valid_cron_field "${fields[3]}" 1 12 || err "$file" "schedule" "invalid cron month field '${fields[3]}'"
130
+ is_valid_cron_field "${fields[4]}" 0 7 || err "$file" "schedule" "invalid cron day-of-week field '${fields[4]}'"
131
+ }
132
+
133
+ looks_like_secret_name_ok() {
134
+ # vault key name: UPPER_SNAKE / mixed alnum._:- without whitespace or credential shapes
135
+ local s="$1"
136
+ [[ "$s" =~ ^[A-Za-z][A-Za-z0-9._:/-]{0,127}$ ]] || return 1
137
+ return 0
138
+ }
139
+
140
+ scan_string_for_secrets() {
141
+ local file="$1" field="$2" value="$3"
142
+ local tmp entry pattern name
143
+ tmp="$(mktemp "${TMPDIR:-/tmp}/jobs-validate-secret.XXXXXX")"
144
+ printf '%s' "$value" >"$tmp"
145
+ for entry in "${SECRET_PATTERNS[@]}"; do
146
+ pattern="${entry%%:*}"
147
+ name="${entry#*:}"
148
+ if grep -Eq "$pattern" "$tmp" 2>/dev/null; then
149
+ err "$file" "$field" "inline credential-shaped value matched pattern '$name' (names only; never values)"
150
+ fi
151
+ done
152
+ rm -f "$tmp"
153
+ }
154
+
155
+ validate_timezone() {
156
+ local file="$1" tz="$2"
157
+ if [[ ! "$tz" =~ ^[A-Za-z_]+(/[A-Za-z0-9_+-]+)+$ ]] && [[ "$tz" != "UTC" ]]; then
158
+ err "$file" "timezone" "not an IANA-looking timezone '$tz'"
159
+ return
160
+ fi
161
+ }
162
+
163
+ json_type() {
164
+ jq -r "$1 | type" <<<"$2" 2>/dev/null || echo "null"
165
+ }
166
+
167
+ # cwd must be HQ-root-relative: no absolute paths, no "."/".." components.
168
+ # Runtime resolves under HQ_ROOT; validate shape only (directory need not exist).
169
+ assert_cwd_safe() {
170
+ local file="$1" field="$2" cwd="$3"
171
+ [ -z "$cwd" ] && return 0
172
+ case "$cwd" in
173
+ /*)
174
+ err "$file" "$field" "absolute cwd not allowed — use an HQ-root-relative path"
175
+ return
176
+ ;;
177
+ esac
178
+ local part
179
+ local IFS='/'
180
+ # shellcheck disable=SC2086
181
+ for part in $cwd; do
182
+ case "$part" in
183
+ ''|.) ;;
184
+ ..)
185
+ err "$file" "$field" "cwd must not contain '..' path components"
186
+ return
187
+ ;;
188
+ esac
189
+ done
190
+ }
191
+
192
+ validate_job_file() {
193
+ local file="$1"
194
+ local json
195
+ if ! json="$(yq -o=json '.' "$file" 2>/dev/null)"; then
196
+ err "$file" "yaml" "failed to parse YAML"
197
+ return
198
+ fi
199
+ if [ "$(jq -r 'type' <<<"$json")" != "object" ]; then
200
+ err "$file" "yaml" "job root must be a mapping"
201
+ return
202
+ fi
203
+
204
+ local id name schedule timezone runtime timeout notify enabled owner created_at
205
+ id="$(jq -r '.id // empty' <<<"$json")"
206
+ name="$(jq -r '.name // empty' <<<"$json")"
207
+ schedule="$(jq -r '.schedule // empty' <<<"$json")"
208
+ if jq -e 'has("timezone") and ((.timezone | type) != "string" or .timezone == "")' \
209
+ <<<"$json" >/dev/null; then
210
+ err "$file" "timezone" "must be a non-empty string when present"
211
+ fi
212
+ timezone="$(jq -r '.timezone // empty' <<<"$json")"
213
+ runtime="$(jq -r '.runtime // empty' <<<"$json")"
214
+ timeout="$(jq -r '.timeout_seconds // empty' <<<"$json")"
215
+ notify="$(jq -r '.notify // empty' <<<"$json")"
216
+ # jq's // treats boolean false as missing — use has() for enabled.
217
+ enabled="$(jq -r 'if has("enabled") then (.enabled|tostring) else empty end' <<<"$json")"
218
+ owner="$(jq -r '.owner // empty' <<<"$json")"
219
+ created_at="$(jq -r '.created_at // empty' <<<"$json")"
220
+
221
+ [ -n "$id" ] || err "$file" "id" "missing required field"
222
+ [ -n "$name" ] || err "$file" "name" "missing required field"
223
+ [ -n "$schedule" ] || err "$file" "schedule" "missing required field"
224
+ [ -n "$runtime" ] || err "$file" "runtime" "missing required field"
225
+ [ -n "$timeout" ] || err "$file" "timeout_seconds" "missing required field"
226
+ [ -n "$notify" ] || err "$file" "notify" "missing required field"
227
+ [ -n "$enabled" ] || err "$file" "enabled" "missing required field"
228
+ [ -n "$owner" ] || err "$file" "owner" "missing required field"
229
+ [ -n "$created_at" ] || err "$file" "created_at" "missing required field"
230
+
231
+ if [ -n "$id" ]; then
232
+ if [[ ! "$id" =~ ^[a-z][a-z0-9-]{1,62}$ ]]; then
233
+ err "$file" "id" "must match [a-z][a-z0-9-]{1,62}, got '$id'"
234
+ fi
235
+ local seen
236
+ for seen in "${SEEN_IDS[@]+"${SEEN_IDS[@]}"}"; do
237
+ if [ "$seen" = "$id" ]; then
238
+ err "$file" "id" "duplicate id '$id'"
239
+ break
240
+ fi
241
+ done
242
+ SEEN_IDS+=("$id")
243
+ fi
244
+
245
+ # Reject natural-language leftovers — canonical form is 5 cron fields only.
246
+ if [ -n "$schedule" ]; then
247
+ local -a sf
248
+ set -f
249
+ # shellcheck disable=SC2206
250
+ sf=($schedule)
251
+ set +f
252
+ if [ "${#sf[@]}" -ne 5 ]; then
253
+ if [[ "$schedule" =~ [[:alpha:]] ]]; then
254
+ err "$file" "schedule" "natural-language schedule is not allowed — store canonical cron only"
255
+ else
256
+ err "$file" "schedule" "invalid cron — expected 5 fields (minute hour dom month dow), got ${#sf[@]}"
257
+ fi
258
+ else
259
+ validate_cron "$file" "$schedule"
260
+ fi
261
+ fi
262
+
263
+ [ -n "$timezone" ] && validate_timezone "$file" "$timezone"
264
+
265
+ case "$runtime" in
266
+ '' ) ;;
267
+ claude|codex) ;;
268
+ *) err "$file" "runtime" "unknown runtime '$runtime' (allowed: claude|codex)" ;;
269
+ esac
270
+
271
+ # exec
272
+ local exec_type prompt skill
273
+ exec_type="$(json_type '.exec' "$json")"
274
+ if [ "$exec_type" = "null" ]; then
275
+ err "$file" "exec" "missing required field"
276
+ elif [ "$exec_type" != "object" ]; then
277
+ err "$file" "exec" "must be an object with prompt or skill"
278
+ else
279
+ prompt="$(jq -r '.exec.prompt // empty' <<<"$json")"
280
+ skill="$(jq -r '.exec.skill // empty' <<<"$json")"
281
+ if [ -z "$prompt" ] && [ -z "$skill" ]; then
282
+ err "$file" "exec" "must set prompt or skill"
283
+ elif [ -n "$prompt" ] && [ -n "$skill" ]; then
284
+ err "$file" "exec" "set exactly one of prompt or skill, not both"
285
+ fi
286
+ [ -n "$prompt" ] && scan_string_for_secrets "$file" "exec.prompt" "$prompt"
287
+ [ -n "$skill" ] && scan_string_for_secrets "$file" "exec.skill" "$skill"
288
+ # scan args values
289
+ if jq -e '.exec.args != null' <<<"$json" >/dev/null 2>&1; then
290
+ if [ "$(json_type '.exec.args' "$json")" != "object" ]; then
291
+ err "$file" "exec.args" "must be a mapping when present"
292
+ else
293
+ while IFS= read -r aval; do
294
+ [ -z "$aval" ] && continue
295
+ scan_string_for_secrets "$file" "exec.args" "$aval"
296
+ done < <(jq -r '.exec.args | .. | strings' <<<"$json" 2>/dev/null)
297
+ fi
298
+ fi
299
+ surface="$(jq -r '.exec.surface // empty' <<<"$json")"
300
+ case "$surface" in
301
+ ''|headless|remote) ;;
302
+ *) err "$file" "exec.surface" "unknown surface '$surface' (allowed: headless|remote)" ;;
303
+ esac
304
+ # reject unknown exec keys
305
+ while IFS= read -r ekey; do
306
+ [ -z "$ekey" ] && continue
307
+ case "$ekey" in
308
+ prompt|skill|args|surface) ;;
309
+ *) err "$file" "exec.$ekey" "unknown exec key (allowed: prompt, skill, args, surface)" ;;
310
+ esac
311
+ done < <(jq -r '.exec | keys[]' <<<"$json")
312
+ fi
313
+
314
+ # timeout
315
+ if [ -n "$timeout" ]; then
316
+ if [[ ! "$timeout" =~ ^[0-9]+$ ]]; then
317
+ err "$file" "timeout_seconds" "must be an integer, got '$timeout'"
318
+ else
319
+ if ((timeout < 60 || timeout > 14400)); then
320
+ err "$file" "timeout_seconds" "must be between 60 and 14400, got $timeout"
321
+ fi
322
+ fi
323
+ fi
324
+
325
+ case "$notify" in
326
+ '' ) ;;
327
+ dm|none|profile) ;;
328
+ *) err "$file" "notify" "unknown notify '$notify' (allowed: dm|none|profile)" ;;
329
+ esac
330
+
331
+ case "$enabled" in
332
+ '' ) ;;
333
+ true|false) ;;
334
+ *)
335
+ # yq may emit boolean; jq -r on bool gives true/false — if someone used string "yes"
336
+ err "$file" "enabled" "must be boolean true|false, got '$enabled'"
337
+ ;;
338
+ esac
339
+
340
+ if [ -n "$created_at" ]; then
341
+ if [[ ! "$created_at" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(Z|[+-][0-9]{2}:[0-9]{2})$ ]]; then
342
+ err "$file" "created_at" "must be ISO-8601 datetime, got '$created_at'"
343
+ fi
344
+ fi
345
+
346
+ # requirements — fixed vocabulary
347
+ if jq -e '.requirements != null' <<<"$json" >/dev/null 2>&1; then
348
+ if [ "$(json_type '.requirements' "$json")" != "object" ]; then
349
+ err "$file" "requirements" "must be a mapping"
350
+ else
351
+ local key
352
+ while IFS= read -r key; do
353
+ [ -z "$key" ] && continue
354
+ case "$key" in
355
+ runtime|secrets|company|cwd|tools) ;;
356
+ readiness|last_probe|last_run|failure_class|pending_probe)
357
+ # status-shaped keys under requirements are unknown requirements keys
358
+ err "$file" "requirements.$key" "unknown requirements key (status belongs in hq-pro, not requirements)"
359
+ ;;
360
+ *) err "$file" "requirements.$key" "unknown requirements key (allowed: runtime, secrets, company, cwd, tools)" ;;
361
+ esac
362
+ done < <(jq -r '.requirements | keys[]' <<<"$json")
363
+
364
+ local req_runtime
365
+ req_runtime="$(jq -r '.requirements.runtime // empty' <<<"$json")"
366
+ if [ -n "$req_runtime" ]; then
367
+ case "$req_runtime" in
368
+ claude|codex) ;;
369
+ *) err "$file" "requirements.runtime" "unknown runtime '$req_runtime' (allowed: claude|codex)" ;;
370
+ esac
371
+ if [ -n "$runtime" ] && [ "$req_runtime" != "$runtime" ]; then
372
+ err "$file" "requirements.runtime" "mismatches top-level runtime ('$req_runtime' vs '$runtime')"
373
+ fi
374
+ fi
375
+
376
+ if jq -e '.requirements.secrets != null' <<<"$json" >/dev/null 2>&1; then
377
+ if [ "$(json_type '.requirements.secrets' "$json")" != "array" ]; then
378
+ err "$file" "requirements.secrets" "must be an array of vault key names"
379
+ else
380
+ local secret
381
+ while IFS= read -r secret; do
382
+ [ -z "$secret" ] && continue
383
+ scan_string_for_secrets "$file" "requirements.secrets" "$secret"
384
+ if ! looks_like_secret_name_ok "$secret"; then
385
+ # If it already matched a secret pattern, we still want the name-shape error for odd values
386
+ err "$file" "requirements.secrets" "invalid vault key name '$secret' (use names only)"
387
+ fi
388
+ # Heuristic: KEY=value or PEM-ish or long high-entropy blob
389
+ if [[ "$secret" == *"="* ]]; then
390
+ err "$file" "requirements.secrets" "looks like KEY=value inline credential — names only"
391
+ fi
392
+ done < <(jq -r '.requirements.secrets[]?' <<<"$json")
393
+ fi
394
+ fi
395
+
396
+ if jq -e '.requirements.tools != null' <<<"$json" >/dev/null 2>&1; then
397
+ if [ "$(json_type '.requirements.tools' "$json")" != "array" ]; then
398
+ err "$file" "requirements.tools" "must be an array of strings"
399
+ fi
400
+ fi
401
+
402
+ local req_cwd
403
+ req_cwd="$(jq -r '.requirements.cwd // empty' <<<"$json")"
404
+ if [ -n "$req_cwd" ]; then
405
+ assert_cwd_safe "$file" "requirements.cwd" "$req_cwd"
406
+ fi
407
+
408
+ local req_company
409
+ req_company="$(jq -r '.requirements.company // empty' <<<"$json")"
410
+ if [ -n "$req_company" ]; then
411
+ if [[ ! "$req_company" =~ ^[a-z0-9]([a-z0-9_-]*[a-z0-9])?$ ]]; then
412
+ err "$file" "requirements.company" "invalid company slug '$req_company' (lowercase kebab/underscore)"
413
+ fi
414
+ fi
415
+ fi
416
+ fi
417
+
418
+ # Top-level cwd (legacy) — same containment rules
419
+ local top_cwd
420
+ top_cwd="$(jq -r '.cwd // empty' <<<"$json")"
421
+ if [ -n "$top_cwd" ]; then
422
+ assert_cwd_safe "$file" "cwd" "$top_cwd"
423
+ fi
424
+
425
+ # Top-level readiness is informational only — ignore (no error). Documented SoT is hq-pro.
426
+ }
427
+
428
+ # Expand args → file list
429
+ FILES=()
430
+ for arg in "$@"; do
431
+ while IFS= read -r f; do
432
+ [ -z "$f" ] && continue
433
+ FILES+=("$f")
434
+ done < <(collect_files "$arg")
435
+ done
436
+
437
+ if [ "${#FILES[@]}" -eq 0 ]; then
438
+ echo "jobs-validate: no job YAML files found" >&2
439
+ exit 1
440
+ fi
441
+
442
+ for f in "${FILES[@]}"; do
443
+ validate_job_file "$f"
444
+ done
445
+
446
+ if [ "$ERRORS" -gt 0 ]; then
447
+ echo "jobs-validate: $ERRORS error(s)" >&2
448
+ exit 1
449
+ fi
450
+
451
+ exit 0
@@ -33,7 +33,8 @@
33
33
  # 3. CLAUDE_SESSION_ID — Claude Code (alternate/legacy)
34
34
  # 4. CODEX_SESSION_ID — Codex
35
35
  # 5. CODEX_THREAD_ID — Codex, thread-scoped runs
36
- # 6. workspace/sessions/.current — no session env; last-writer-wins fallback
36
+ # 6. GROK_SESSION_ID — Grok Build (Desktop / CLI)
37
+ # 7. workspace/sessions/.current — no session env; last-writer-wins fallback
37
38
  #
38
39
  # The env precedence list matches .claude/skills/_shared/journal.sh so every
39
40
  # per-session artifact (journal, session meta, scope capability) keys off the
@@ -59,7 +60,7 @@ session_id_is_valid() {
59
60
  session_id_from_env() {
60
61
  local var val
61
62
  for var in HQ_SESSION_ID CLAUDE_CODE_SESSION_ID CLAUDE_SESSION_ID \
62
- CODEX_SESSION_ID CODEX_THREAD_ID; do
63
+ CODEX_SESSION_ID CODEX_THREAD_ID GROK_SESSION_ID; do
63
64
  val="${!var:-}"
64
65
  val="$(printf '%s' "$val" | tr -d '[:space:]')"
65
66
  if session_id_is_valid "$val"; then
@@ -100,7 +100,8 @@ work_mesh_ulid() {
100
100
  bytes=$(dd if=/dev/urandom bs=10 count=1 2>/dev/null | od -An -tu1 | tr -s ' ' '\n' | grep -E '^[0-9]+$' | head -n 10 | tr '\n' ' ')
101
101
  # shellcheck disable=SC2206
102
102
  local -a bb=($bytes)
103
- b=("${bb[@]}")
103
+ # Bash 3.2 + set -u: empty bb[@] is unbound; keep the copy portable.
104
+ b=(${bb[@]+"${bb[@]}"})
104
105
  while [ "${#b[@]}" -lt 10 ]; do
105
106
  b+=($((RANDOM % 256)))
106
107
  done