@indigoai-us/hq-cli 5.345.114 → 5.345.116

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 (57) hide show
  1. package/CHANGELOG.md +13 -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/core-audit-log-native.js +1 -1
  6. package/dist/commands/core-checkpoint-native.d.ts +27 -0
  7. package/dist/commands/core-checkpoint-native.js +85 -2
  8. package/dist/commands/core-dm-bind-native.js +1 -1
  9. package/dist/commands/core-git-pack-extension-check-native.js +1 -1
  10. package/dist/commands/core-job-native-utils.d.ts +2 -2
  11. package/dist/commands/core-job-native-utils.js +3 -11
  12. package/dist/commands/core-job-notify-native.js +1 -2
  13. package/dist/commands/core-job-probe-native.d.ts +2 -3
  14. package/dist/commands/core-job-probe-native.js +14 -22
  15. package/dist/commands/core-job-remediate-native.js +1 -2
  16. package/dist/commands/core-job-run-native.js +58 -54
  17. package/dist/commands/core-jobs-native.d.ts +1 -3
  18. package/dist/commands/core-jobs-native.js +16 -271
  19. package/dist/commands/core-jobs-schedule.d.ts +7 -0
  20. package/dist/commands/core-jobs-schedule.js +243 -0
  21. package/dist/commands/core-native-utilities.d.ts +2 -3
  22. package/dist/commands/core-native-utilities.js +2 -19
  23. package/dist/commands/core-pin-native.js +1 -1
  24. package/dist/commands/core-refresh-vault-access-native.js +1 -1
  25. package/dist/commands/core-resolve-company-command.js +2 -17
  26. package/dist/commands/core-work-mesh-bind-trusted-native.js +1 -1
  27. package/dist/commands/core.js +1 -17
  28. package/dist/fast-scaffold.d.ts +1 -1
  29. package/dist/fast-scaffold.js +0 -38
  30. package/dist/lib/daemon/catalog.d.ts +0 -2
  31. package/dist/lib/daemon/catalog.js +3 -6
  32. package/dist/lib/daemon/run.js +0 -7
  33. package/dist/lib/scan-packages/index.js +6 -0
  34. package/dist/lib/workers-registry/index.d.ts +16 -0
  35. package/dist/lib/workers-registry/index.js +1 -1
  36. package/dist/utils/resident-package-preload.js +2 -7
  37. package/package.json +1 -1
  38. package/assets/scaffold/core/scripts/audit-log.sh +0 -384
  39. package/assets/scaffold/core/scripts/git-pack-extension-check.sh +0 -164
  40. package/assets/scaffold/core/scripts/hq-dm-bind.sh +0 -590
  41. package/assets/scaffold/core/scripts/hq-job-notify.sh +0 -389
  42. package/assets/scaffold/core/scripts/hq-job-probe.sh +0 -865
  43. package/assets/scaffold/core/scripts/hq-job-remediate.sh +0 -359
  44. package/assets/scaffold/core/scripts/hq-job-run.sh +0 -1025
  45. package/assets/scaffold/core/scripts/hq-pin.sh +0 -118
  46. package/assets/scaffold/core/scripts/jobs-schedule-parse.sh +0 -310
  47. package/assets/scaffold/core/scripts/jobs-validate.sh +0 -482
  48. package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +0 -836
  49. package/assets/scaffold/core/scripts/refresh-vault-access.sh +0 -188
  50. package/assets/scaffold/core/scripts/resolve-company.sh +0 -331
  51. package/assets/scaffold/core/scripts/work-mesh-live-bind-trusted.sh +0 -184
  52. package/dist/commands/core-job-post-exit-assets.d.ts +0 -8
  53. package/dist/commands/core-job-post-exit-assets.js +0 -37
  54. package/dist/commands/core-native-jobs-flag.d.ts +0 -4
  55. package/dist/commands/core-native-jobs-flag.js +0 -7
  56. package/dist/commands/core-native-utilities-flag.d.ts +0 -4
  57. package/dist/commands/core-native-utilities-flag.js +0 -7
@@ -1,482 +0,0 @@
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
- # jq.exe on MSYS may use CRT text mode unless --binary is supported.
44
- case "${OSTYPE:-}:${MSYSTEM:-}" in
45
- msys*:*|*:MINGW*)
46
- # Probe --binary once per script run, in the top-level shell. Most jq
47
- # calls run inside $(...) subshells, so a cache filled lazily inside jq()
48
- # would be lost and the probe would repeat on every call.
49
- if command jq --binary -n 'null' >/dev/null 2>&1; then
50
- HQ_JQ_BINARY_SUPPORTED=1
51
- else
52
- HQ_JQ_BINARY_SUPPORTED=0
53
- fi
54
- jq() {
55
- local stdout_file stderr_file status
56
- stdout_file="$(mktemp)" || return $?
57
- stderr_file="$(mktemp)" || { status=$?; rm -f "$stdout_file"; return "$status"; }
58
- if [[ "$HQ_JQ_BINARY_SUPPORTED" == 1 ]]; then
59
- if command jq --binary "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
60
- command cat "$stdout_file"
61
- command cat "$stderr_file" >&2
62
- else
63
- if command jq "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
64
- command sed -b 's/\r$//' "$stdout_file"
65
- command sed -b 's/\r$//' "$stderr_file" >&2
66
- fi
67
- rm -f "$stdout_file" "$stderr_file"
68
- return "$status"
69
- }
70
- ;;
71
- esac
72
-
73
- [ "$#" -ge 1 ] || die_usage
74
-
75
- collect_files() {
76
- local arg="$1"
77
- if [ -f "$arg" ]; then
78
- case "$arg" in
79
- *.yaml|*.yml) printf '%s\n' "$arg" ;;
80
- *) err "$arg" "path" "not a .yaml/.yml job file" ;;
81
- esac
82
- return
83
- fi
84
- if [ -d "$arg" ]; then
85
- # Prefer find -print0 / sort for stable duplicate-id detection order
86
- find "$arg" \( -name '*.yaml' -o -name '*.yml' \) -type f | sort
87
- return
88
- fi
89
- err "$arg" "path" "file or directory not found"
90
- }
91
-
92
- cron_num_in_range() {
93
- local n="$1" min="$2" max="$3"
94
- [[ "$n" =~ ^[0-9]+$ ]] || return 1
95
- ((10#$n >= min && 10#$n <= max))
96
- }
97
-
98
- is_valid_cron_field() {
99
- # $1=value $2=min $3=max — *, */n, a-b, a-b/n, lists, dow/month names
100
- local v="$1" min="$2" max="$3" part base step a b
101
- local IFS=','
102
- set -f
103
- # shellcheck disable=SC2086
104
- set -- $v
105
- set +f
106
- for part in "$@"; do
107
- case "$part" in
108
- \*) continue ;;
109
- \*/*)
110
- step="${part#*/}"
111
- [[ "$step" =~ ^[1-9][0-9]*$ ]] || return 1
112
- continue
113
- ;;
114
- sun|mon|tue|wed|thu|fri|sat|SUN|MON|TUE|WED|THU|FRI|SAT)
115
- [ "$max" -eq 7 ] || return 1
116
- continue
117
- ;;
118
- jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)
119
- [ "$max" -eq 12 ] || return 1
120
- continue
121
- ;;
122
- esac
123
- if [[ "$part" =~ ^([0-9]+)-([0-9]+)(/([1-9][0-9]*))?$ ]]; then
124
- a="${BASH_REMATCH[1]}"
125
- b="${BASH_REMATCH[2]}"
126
- cron_num_in_range "$a" "$min" "$max" || return 1
127
- cron_num_in_range "$b" "$min" "$max" || return 1
128
- ((10#$a <= 10#$b)) || return 1
129
- continue
130
- fi
131
- if [[ "$part" =~ ^([0-9]+)(/([1-9][0-9]*))?$ ]]; then
132
- base="${BASH_REMATCH[1]}"
133
- cron_num_in_range "$base" "$min" "$max" || return 1
134
- continue
135
- fi
136
- return 1
137
- done
138
- return 0
139
- }
140
-
141
- validate_cron() {
142
- local file="$1" cron="$2"
143
- # strip surrounding quotes leftovers
144
- cron="${cron#"${cron%%[![:space:]]*}"}"
145
- cron="${cron%"${cron##*[![:space:]]}"}"
146
- local -a fields
147
- # noglob: cron uses '*' which must not expand against the filesystem
148
- set -f
149
- # shellcheck disable=SC2206
150
- fields=($cron)
151
- set +f
152
- if [ "${#fields[@]}" -ne 5 ]; then
153
- err "$file" "schedule" "invalid cron — expected 5 fields (minute hour dom month dow), got ${#fields[@]}"
154
- return
155
- fi
156
- is_valid_cron_field "${fields[0]}" 0 59 || err "$file" "schedule" "invalid cron minute field '${fields[0]}'"
157
- is_valid_cron_field "${fields[1]}" 0 23 || err "$file" "schedule" "invalid cron hour field '${fields[1]}'"
158
- is_valid_cron_field "${fields[2]}" 1 31 || err "$file" "schedule" "invalid cron day-of-month field '${fields[2]}'"
159
- is_valid_cron_field "${fields[3]}" 1 12 || err "$file" "schedule" "invalid cron month field '${fields[3]}'"
160
- is_valid_cron_field "${fields[4]}" 0 7 || err "$file" "schedule" "invalid cron day-of-week field '${fields[4]}'"
161
- }
162
-
163
- looks_like_secret_name_ok() {
164
- # vault key name: UPPER_SNAKE / mixed alnum._:- without whitespace or credential shapes
165
- local s="$1"
166
- [[ "$s" =~ ^[A-Za-z][A-Za-z0-9._:/-]{0,127}$ ]] || return 1
167
- return 0
168
- }
169
-
170
- scan_string_for_secrets() {
171
- local file="$1" field="$2" value="$3"
172
- local tmp entry pattern name
173
- tmp="$(mktemp "${TMPDIR:-/tmp}/jobs-validate-secret.XXXXXX")"
174
- printf '%s' "$value" >"$tmp"
175
- for entry in "${SECRET_PATTERNS[@]}"; do
176
- pattern="${entry%%:*}"
177
- name="${entry#*:}"
178
- if grep -Eq -e "$pattern" "$tmp" 2>/dev/null; then
179
- err "$file" "$field" "inline credential-shaped value matched pattern '$name' (names only; never values)"
180
- fi
181
- done
182
- rm -f "$tmp"
183
- }
184
-
185
- validate_timezone() {
186
- local file="$1" tz="$2"
187
- if [[ ! "$tz" =~ ^[A-Za-z_]+(/[A-Za-z0-9_+-]+)+$ ]] && [[ "$tz" != "UTC" ]]; then
188
- err "$file" "timezone" "not an IANA-looking timezone '$tz'"
189
- return
190
- fi
191
- }
192
-
193
- json_type() {
194
- jq -r "$1 | type" <<<"$2" 2>/dev/null || echo "null"
195
- }
196
-
197
- # cwd must be HQ-root-relative: no absolute paths, no "."/".." components.
198
- # Runtime resolves under HQ_ROOT; validate shape only (directory need not exist).
199
- assert_cwd_safe() {
200
- local file="$1" field="$2" cwd="$3"
201
- [ -z "$cwd" ] && return 0
202
- case "$cwd" in
203
- /*)
204
- err "$file" "$field" "absolute cwd not allowed — use an HQ-root-relative path"
205
- return
206
- ;;
207
- esac
208
- local part
209
- local IFS='/'
210
- # shellcheck disable=SC2086
211
- for part in $cwd; do
212
- case "$part" in
213
- ''|.) ;;
214
- ..)
215
- err "$file" "$field" "cwd must not contain '..' path components"
216
- return
217
- ;;
218
- esac
219
- done
220
- }
221
-
222
- validate_job_file() {
223
- local file="$1"
224
- local json
225
- if ! json="$(yq -o=json '.' "$file" 2>/dev/null)"; then
226
- err "$file" "yaml" "failed to parse YAML"
227
- return
228
- fi
229
- # Use jq's exit status instead of its text output: jq.exe writes CRLF on Windows.
230
- if ! jq -es 'length == 1 and (.[0] | type == "object")' <<<"$json" >/dev/null; then
231
- err "$file" "yaml" "job root must be a mapping"
232
- return
233
- fi
234
-
235
- local id name schedule timezone runtime timeout notify enabled owner created_at
236
- id="$(jq -r '.id // empty' <<<"$json")"
237
- name="$(jq -r '.name // empty' <<<"$json")"
238
- schedule="$(jq -r '.schedule // empty' <<<"$json")"
239
- if jq -e 'has("timezone") and ((.timezone | type) != "string" or .timezone == "")' \
240
- <<<"$json" >/dev/null; then
241
- err "$file" "timezone" "must be a non-empty string when present"
242
- fi
243
- timezone="$(jq -r '.timezone // empty' <<<"$json")"
244
- runtime="$(jq -r '.runtime // empty' <<<"$json")"
245
- timeout="$(jq -r '.timeout_seconds // empty' <<<"$json")"
246
- notify="$(jq -r '.notify // empty' <<<"$json")"
247
- # jq's // treats boolean false as missing — use has() for enabled.
248
- enabled="$(jq -r 'if has("enabled") then (.enabled|tostring) else empty end' <<<"$json")"
249
- owner="$(jq -r '.owner // empty' <<<"$json")"
250
- created_at="$(jq -r '.created_at // empty' <<<"$json")"
251
-
252
- [ -n "$id" ] || err "$file" "id" "missing required field"
253
- [ -n "$name" ] || err "$file" "name" "missing required field"
254
- [ -n "$schedule" ] || err "$file" "schedule" "missing required field"
255
- [ -n "$runtime" ] || err "$file" "runtime" "missing required field"
256
- [ -n "$timeout" ] || err "$file" "timeout_seconds" "missing required field"
257
- [ -n "$notify" ] || err "$file" "notify" "missing required field"
258
- [ -n "$enabled" ] || err "$file" "enabled" "missing required field"
259
- [ -n "$owner" ] || err "$file" "owner" "missing required field"
260
- [ -n "$created_at" ] || err "$file" "created_at" "missing required field"
261
-
262
- if [ -n "$id" ]; then
263
- if [[ ! "$id" =~ ^[a-z][a-z0-9-]{1,62}$ ]]; then
264
- err "$file" "id" "must match [a-z][a-z0-9-]{1,62}, got '$id'"
265
- fi
266
- local seen
267
- for seen in "${SEEN_IDS[@]+"${SEEN_IDS[@]}"}"; do
268
- if [ "$seen" = "$id" ]; then
269
- err "$file" "id" "duplicate id '$id'"
270
- break
271
- fi
272
- done
273
- SEEN_IDS+=("$id")
274
- fi
275
-
276
- # Reject natural-language leftovers — canonical form is 5 cron fields only.
277
- if [ -n "$schedule" ]; then
278
- local -a sf
279
- set -f
280
- # shellcheck disable=SC2206
281
- sf=($schedule)
282
- set +f
283
- if [ "${#sf[@]}" -ne 5 ]; then
284
- if [[ "$schedule" =~ [[:alpha:]] ]]; then
285
- err "$file" "schedule" "natural-language schedule is not allowed — store canonical cron only"
286
- else
287
- err "$file" "schedule" "invalid cron — expected 5 fields (minute hour dom month dow), got ${#sf[@]}"
288
- fi
289
- else
290
- validate_cron "$file" "$schedule"
291
- fi
292
- fi
293
-
294
- [ -n "$timezone" ] && validate_timezone "$file" "$timezone"
295
-
296
- case "$runtime" in
297
- '' ) ;;
298
- claude|codex) ;;
299
- *) err "$file" "runtime" "unknown runtime '$runtime' (allowed: claude|codex)" ;;
300
- esac
301
-
302
- # exec
303
- local exec_type prompt skill
304
- exec_type="$(json_type '.exec' "$json")"
305
- if [ "$exec_type" = "null" ]; then
306
- err "$file" "exec" "missing required field"
307
- elif [ "$exec_type" != "object" ]; then
308
- err "$file" "exec" "must be an object with prompt or skill"
309
- else
310
- prompt="$(jq -r '.exec.prompt // empty' <<<"$json")"
311
- skill="$(jq -r '.exec.skill // empty' <<<"$json")"
312
- if [ -z "$prompt" ] && [ -z "$skill" ]; then
313
- err "$file" "exec" "must set prompt or skill"
314
- elif [ -n "$prompt" ] && [ -n "$skill" ]; then
315
- err "$file" "exec" "set exactly one of prompt or skill, not both"
316
- fi
317
- [ -n "$prompt" ] && scan_string_for_secrets "$file" "exec.prompt" "$prompt"
318
- [ -n "$skill" ] && scan_string_for_secrets "$file" "exec.skill" "$skill"
319
- # scan args values
320
- if jq -e '.exec.args != null' <<<"$json" >/dev/null 2>&1; then
321
- if [ "$(json_type '.exec.args' "$json")" != "object" ]; then
322
- err "$file" "exec.args" "must be a mapping when present"
323
- else
324
- while IFS= read -r aval; do
325
- [ -z "$aval" ] && continue
326
- scan_string_for_secrets "$file" "exec.args" "$aval"
327
- done < <(jq -r '.exec.args | .. | strings' <<<"$json" 2>/dev/null)
328
- fi
329
- fi
330
- surface="$(jq -r '.exec.surface // empty' <<<"$json")"
331
- case "$surface" in
332
- ''|headless|remote) ;;
333
- *) err "$file" "exec.surface" "unknown surface '$surface' (allowed: headless|remote)" ;;
334
- esac
335
- # reject unknown exec keys
336
- while IFS= read -r ekey; do
337
- [ -z "$ekey" ] && continue
338
- case "$ekey" in
339
- prompt|skill|args|surface) ;;
340
- *) err "$file" "exec.$ekey" "unknown exec key (allowed: prompt, skill, args, surface)" ;;
341
- esac
342
- done < <(jq -r '.exec | keys[]' <<<"$json")
343
- fi
344
-
345
- # timeout
346
- if [ -n "$timeout" ]; then
347
- if [[ ! "$timeout" =~ ^[0-9]+$ ]]; then
348
- err "$file" "timeout_seconds" "must be an integer, got '$timeout'"
349
- else
350
- if ((timeout < 60 || timeout > 14400)); then
351
- err "$file" "timeout_seconds" "must be between 60 and 14400, got $timeout"
352
- fi
353
- fi
354
- fi
355
-
356
- case "$notify" in
357
- '' ) ;;
358
- dm|none|profile) ;;
359
- *) err "$file" "notify" "unknown notify '$notify' (allowed: dm|none|profile)" ;;
360
- esac
361
-
362
- case "$enabled" in
363
- '' ) ;;
364
- true|false) ;;
365
- *)
366
- # yq may emit boolean; jq -r on bool gives true/false — if someone used string "yes"
367
- err "$file" "enabled" "must be boolean true|false, got '$enabled'"
368
- ;;
369
- esac
370
-
371
- if [ -n "$created_at" ]; then
372
- 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
373
- err "$file" "created_at" "must be ISO-8601 datetime, got '$created_at'"
374
- fi
375
- fi
376
-
377
- # requirements — fixed vocabulary
378
- if jq -e '.requirements != null' <<<"$json" >/dev/null 2>&1; then
379
- if [ "$(json_type '.requirements' "$json")" != "object" ]; then
380
- err "$file" "requirements" "must be a mapping"
381
- else
382
- local key
383
- while IFS= read -r key; do
384
- [ -z "$key" ] && continue
385
- case "$key" in
386
- runtime|secrets|company|cwd|tools) ;;
387
- readiness|last_probe|last_run|failure_class|pending_probe)
388
- # status-shaped keys under requirements are unknown requirements keys
389
- err "$file" "requirements.$key" "unknown requirements key (status belongs in hq-pro, not requirements)"
390
- ;;
391
- *) err "$file" "requirements.$key" "unknown requirements key (allowed: runtime, secrets, company, cwd, tools)" ;;
392
- esac
393
- done < <(jq -r '.requirements | keys[]' <<<"$json")
394
-
395
- local req_runtime
396
- req_runtime="$(jq -r '.requirements.runtime // empty' <<<"$json")"
397
- if [ -n "$req_runtime" ]; then
398
- case "$req_runtime" in
399
- claude|codex) ;;
400
- *) err "$file" "requirements.runtime" "unknown runtime '$req_runtime' (allowed: claude|codex)" ;;
401
- esac
402
- if [ -n "$runtime" ] && [ "$req_runtime" != "$runtime" ]; then
403
- err "$file" "requirements.runtime" "mismatches top-level runtime ('$req_runtime' vs '$runtime')"
404
- fi
405
- fi
406
-
407
- if jq -e '.requirements.secrets != null' <<<"$json" >/dev/null 2>&1; then
408
- if [ "$(json_type '.requirements.secrets' "$json")" != "array" ]; then
409
- err "$file" "requirements.secrets" "must be an array of vault key names"
410
- else
411
- local secret
412
- while IFS= read -r secret; do
413
- [ -z "$secret" ] && continue
414
- scan_string_for_secrets "$file" "requirements.secrets" "$secret"
415
- if ! looks_like_secret_name_ok "$secret"; then
416
- # If it already matched a secret pattern, we still want the name-shape error for odd values
417
- err "$file" "requirements.secrets" "invalid vault key name '$secret' (use names only)"
418
- fi
419
- # Heuristic: KEY=value or PEM-ish or long high-entropy blob
420
- if [[ "$secret" == *"="* ]]; then
421
- err "$file" "requirements.secrets" "looks like KEY=value inline credential — names only"
422
- fi
423
- done < <(jq -r '.requirements.secrets[]?' <<<"$json")
424
- fi
425
- fi
426
-
427
- if jq -e '.requirements.tools != null' <<<"$json" >/dev/null 2>&1; then
428
- if [ "$(json_type '.requirements.tools' "$json")" != "array" ]; then
429
- err "$file" "requirements.tools" "must be an array of strings"
430
- fi
431
- fi
432
-
433
- local req_cwd
434
- req_cwd="$(jq -r '.requirements.cwd // empty' <<<"$json")"
435
- if [ -n "$req_cwd" ]; then
436
- assert_cwd_safe "$file" "requirements.cwd" "$req_cwd"
437
- fi
438
-
439
- local req_company
440
- req_company="$(jq -r '.requirements.company // empty' <<<"$json")"
441
- if [ -n "$req_company" ]; then
442
- if [[ ! "$req_company" =~ ^[a-z0-9]([a-z0-9_-]*[a-z0-9])?$ ]]; then
443
- err "$file" "requirements.company" "invalid company slug '$req_company' (lowercase kebab/underscore)"
444
- fi
445
- fi
446
- fi
447
- fi
448
-
449
- # Top-level cwd (legacy) — same containment rules
450
- local top_cwd
451
- top_cwd="$(jq -r '.cwd // empty' <<<"$json")"
452
- if [ -n "$top_cwd" ]; then
453
- assert_cwd_safe "$file" "cwd" "$top_cwd"
454
- fi
455
-
456
- # Top-level readiness is informational only — ignore (no error). Documented SoT is hq-pro.
457
- }
458
-
459
- # Expand args → file list
460
- FILES=()
461
- for arg in "$@"; do
462
- while IFS= read -r f; do
463
- [ -z "$f" ] && continue
464
- FILES+=("$f")
465
- done < <(collect_files "$arg")
466
- done
467
-
468
- if [ "${#FILES[@]}" -eq 0 ]; then
469
- echo "jobs-validate: no job YAML files found" >&2
470
- exit 1
471
- fi
472
-
473
- for f in "${FILES[@]}"; do
474
- validate_job_file "$f"
475
- done
476
-
477
- if [ "$ERRORS" -gt 0 ]; then
478
- echo "jobs-validate: $ERRORS error(s)" >&2
479
- exit 1
480
- fi
481
-
482
- exit 0