@indigoai-us/hq-cli 5.289.0 → 5.291.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 (65) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/assets/scaffold/core/scripts/handoff-finalize.sh +670 -0
  3. package/assets/scaffold/core/scripts/handoff-open-steps.sh +81 -0
  4. package/assets/scaffold/core/scripts/handoff-post.sh +187 -0
  5. package/assets/scaffold/core/scripts/hook-lib.sh +345 -50
  6. package/assets/scaffold/core/scripts/hq-session.sh +182 -21
  7. package/assets/scaffold/core/scripts/jobs-schedule-parse.sh +310 -0
  8. package/assets/scaffold/core/scripts/jobs-validate.sh +451 -0
  9. package/assets/scaffold/core/scripts/lib/session-id.sh +3 -2
  10. package/assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh +2 -1
  11. package/assets/scaffold/core/scripts/qmd-reindex-bg.sh +40 -0
  12. package/dist/command-catalog.generated.d.ts +110 -0
  13. package/dist/command-catalog.generated.js +136 -0
  14. package/dist/commands/cloud.d.ts +26 -14
  15. package/dist/commands/cloud.js +207 -56
  16. package/dist/commands/core-handoff.d.ts +22 -0
  17. package/dist/commands/core-handoff.js +1240 -0
  18. package/dist/commands/core-jobs-native.d.ts +8 -0
  19. package/dist/commands/core-jobs-native.js +624 -0
  20. package/dist/commands/core-native-jobs-flag.d.ts +4 -0
  21. package/dist/commands/core-native-jobs-flag.js +7 -0
  22. package/dist/commands/core-ontology.d.ts +6 -1
  23. package/dist/commands/core-ontology.js +42 -7
  24. package/dist/commands/core-policy.js +24 -5
  25. package/dist/commands/core.js +15 -1
  26. package/dist/commands/integrations-core.js +7 -0
  27. package/dist/commands/lanes.js +111 -35
  28. package/dist/commands/members.js +5 -0
  29. package/dist/commands/mesh.js +46 -25
  30. package/dist/commands/personal-targeted-push-flag.d.ts +4 -0
  31. package/dist/commands/personal-targeted-push-flag.js +7 -0
  32. package/dist/commands/personal-targeted-push-paths.d.ts +3 -0
  33. package/dist/commands/personal-targeted-push-paths.js +67 -0
  34. package/dist/commands/secrets.js +9 -0
  35. package/dist/commands/skill.d.ts +3 -0
  36. package/dist/commands/skill.js +28 -0
  37. package/dist/fast-core.js +1 -0
  38. package/dist/lib/billing/free-month.d.ts +18 -0
  39. package/dist/lib/billing/free-month.js +31 -0
  40. package/dist/lib/billing/plan-limit-refusals.d.ts +69 -0
  41. package/dist/lib/billing/plan-limit-refusals.js +187 -0
  42. package/dist/lib/billing/plan-lock.d.ts +2 -6
  43. package/dist/lib/billing/plan-lock.js +5 -16
  44. package/dist/lib/checkpoint-ontology/entity-file.d.ts +21 -0
  45. package/dist/lib/checkpoint-ontology/entity-file.js +100 -0
  46. package/dist/lib/checkpoint-ontology/merge-copies.d.ts +34 -2
  47. package/dist/lib/checkpoint-ontology/merge-copies.js +541 -27
  48. package/dist/lib/core-utils/secret-patterns.d.ts +3 -1
  49. package/dist/lib/core-utils/secret-patterns.js +3 -2
  50. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  51. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  52. package/dist/lib/lanes/autopilot.d.ts +69 -0
  53. package/dist/lib/lanes/autopilot.js +418 -0
  54. package/dist/lib/lanes/mesh-events.js +2 -0
  55. package/dist/lib/lanes/store.d.ts +4 -2
  56. package/dist/lib/lanes/store.js +18 -9
  57. package/dist/lib/mesh/api.js +4 -0
  58. package/dist/lib/mesh/live/daemon/state.d.ts +7 -1
  59. package/dist/lib/mesh/live/daemon/state.js +34 -2
  60. package/dist/main.js +23 -12
  61. package/dist/utils/plan-gate-error.d.ts +24 -0
  62. package/dist/utils/plan-gate-error.js +219 -9
  63. package/dist/utils/self-update.d.ts +3 -0
  64. package/dist/utils/self-update.js +232 -1
  65. package/package.json +2 -2
@@ -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
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env bash
2
+ # FORWARDER — the implementation of this script now lives in the hq CLI.
3
+ #
4
+ # It runs as `hq index background`. This file stays behind so every existing
5
+ # caller — skills, other scripts, CI, and muscle memory — keeps working against
6
+ # the path it already knows.
7
+ #
8
+ # Why the implementation moved: the single-flight background reindex is a
9
+ # durable state machine (lock ownership, stale reclaim, completion stamps,
10
+ # signal cleanup) that belongs in tested code rather than 700 lines of shell.
11
+ #
12
+ # Equivalence is not assumed. hq-cli's e2e/qmd-background-differential.test.ts
13
+ # runs a byte-frozen copy of this script's pre-migration source against the CLI
14
+ # over identical injected worlds and compares exit status, stdout, the qmd argv
15
+ # sequence, and the resulting lock/stamp state: agent short-circuit before any
16
+ # qmd lookup or lock, empty-HOME silence, cleanup->update->embed ordering with a
17
+ # completion stamp, no stamp after a failed update, and no competing writer
18
+ # behind a held lock.
19
+ #
20
+ # ONE INTENDED DIVERGENCE: this script printed "skipped" when no qmd was on
21
+ # PATH. The CLI bundles qmd (@tobilu/qmd is a dependency, resolved via
22
+ # node_modules/.bin since hq-cli 5.94.1), so it proceeds instead of skipping.
23
+ # That is the point of bundling; see hq-cli#333.
24
+ #
25
+ # The ABI is otherwise preserved: arguments are forwarded unchanged, stdin is
26
+ # never read by this file, stdout and stderr are inherited untouched, and the
27
+ # child replaces this process so its exit code and signal disposition become the
28
+ # caller's.
29
+
30
+ set -euo pipefail
31
+
32
+ HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
33
+
34
+ if ! command -v hq >/dev/null 2>&1; then
35
+ echo "qmd-reindex-bg.sh: requires the hq CLI — this script's implementation now ships with it." >&2
36
+ echo "Install it with: npm install -g @indigoai-us/hq-cli" >&2
37
+ exit 127
38
+ fi
39
+
40
+ exec hq index background --hq-root "$HQ_ROOT" "$@"
@@ -5208,6 +5208,9 @@ export declare const COMMAND_CATALOG: readonly [{
5208
5208
  }, {
5209
5209
  readonly flags: "--only <file>";
5210
5210
  readonly description: "handle only the copy -> original pairs listed in this JSON file";
5211
+ }, {
5212
+ readonly flags: "--rewrite-refs";
5213
+ readonly description: "also re-point signal refs, invoice, source and knowledge links to the survivor and carry external ids and web domains (checkpoint.ontology.merge-rewrite-refs)";
5211
5214
  }, {
5212
5215
  readonly flags: "--json";
5213
5216
  readonly description: "print JSON";
@@ -5402,6 +5405,54 @@ export declare const COMMAND_CATALOG: readonly [{
5402
5405
  readonly options: readonly [];
5403
5406
  readonly subcommands: readonly [];
5404
5407
  }];
5408
+ }, {
5409
+ readonly name: "handoff";
5410
+ readonly description: "Handoff session helpers";
5411
+ readonly aliases: readonly [];
5412
+ readonly hidden: false;
5413
+ readonly usage: "[options] [command]";
5414
+ readonly arguments: readonly [];
5415
+ readonly options: readonly [];
5416
+ readonly subcommands: readonly [{
5417
+ readonly name: "finalize";
5418
+ readonly description: "Write and commit the current session handoff";
5419
+ readonly aliases: readonly [];
5420
+ readonly hidden: false;
5421
+ readonly usage: "[args...]";
5422
+ readonly arguments: readonly [{
5423
+ readonly name: "args";
5424
+ readonly required: false;
5425
+ readonly variadic: true;
5426
+ }];
5427
+ readonly options: readonly [];
5428
+ readonly subcommands: readonly [];
5429
+ }, {
5430
+ readonly name: "post";
5431
+ readonly description: "Run best-effort post-handoff maintenance";
5432
+ readonly aliases: readonly [];
5433
+ readonly hidden: false;
5434
+ readonly usage: "[args...]";
5435
+ readonly arguments: readonly [{
5436
+ readonly name: "args";
5437
+ readonly required: false;
5438
+ readonly variadic: true;
5439
+ }];
5440
+ readonly options: readonly [];
5441
+ readonly subcommands: readonly [];
5442
+ }, {
5443
+ readonly name: "open-steps";
5444
+ readonly description: "List and update open handoff steps";
5445
+ readonly aliases: readonly [];
5446
+ readonly hidden: false;
5447
+ readonly usage: "[args...]";
5448
+ readonly arguments: readonly [{
5449
+ readonly name: "args";
5450
+ readonly required: false;
5451
+ readonly variadic: true;
5452
+ }];
5453
+ readonly options: readonly [];
5454
+ readonly subcommands: readonly [];
5455
+ }];
5405
5456
  }, {
5406
5457
  readonly name: "knowledge-prefs";
5407
5458
  readonly description: "Resolve a company knowledge-store preference";
@@ -5463,6 +5514,41 @@ export declare const COMMAND_CATALOG: readonly [{
5463
5514
  readonly options: readonly [];
5464
5515
  readonly subcommands: readonly [];
5465
5516
  }];
5517
+ }, {
5518
+ readonly name: "jobs";
5519
+ readonly description: "Scheduled jobs helpers";
5520
+ readonly aliases: readonly [];
5521
+ readonly hidden: false;
5522
+ readonly usage: "[options] [command]";
5523
+ readonly arguments: readonly [];
5524
+ readonly options: readonly [];
5525
+ readonly subcommands: readonly [{
5526
+ readonly name: "schedule-parse";
5527
+ readonly description: "Resolve natural-language or raw cron schedules";
5528
+ readonly aliases: readonly [];
5529
+ readonly hidden: false;
5530
+ readonly usage: "[args...]";
5531
+ readonly arguments: readonly [{
5532
+ readonly name: "args";
5533
+ readonly required: false;
5534
+ readonly variadic: true;
5535
+ }];
5536
+ readonly options: readonly [];
5537
+ readonly subcommands: readonly [];
5538
+ }, {
5539
+ readonly name: "validate";
5540
+ readonly description: "Validate scheduled-job YAML files";
5541
+ readonly aliases: readonly [];
5542
+ readonly hidden: false;
5543
+ readonly usage: "[args...]";
5544
+ readonly arguments: readonly [{
5545
+ readonly name: "args";
5546
+ readonly required: false;
5547
+ readonly variadic: true;
5548
+ }];
5549
+ readonly options: readonly [];
5550
+ readonly subcommands: readonly [];
5551
+ }];
5466
5552
  }, {
5467
5553
  readonly name: "worktree-gc";
5468
5554
  readonly description: "Safely collect old, preserved HQ worktrees";
@@ -8798,6 +8884,30 @@ export declare const COMMAND_CATALOG: readonly [{
8798
8884
  readonly description: "JSON on stdout (always)";
8799
8885
  }];
8800
8886
  readonly subcommands: readonly [];
8887
+ }, {
8888
+ readonly name: "autopilot";
8889
+ readonly description: "Check registered team leads and wake them for ready work";
8890
+ readonly aliases: readonly [];
8891
+ readonly hidden: false;
8892
+ readonly usage: "[options] [command]";
8893
+ readonly arguments: readonly [];
8894
+ readonly options: readonly [];
8895
+ readonly subcommands: readonly [{
8896
+ readonly name: "run";
8897
+ readonly description: "Run one lead autopilot check pass";
8898
+ readonly aliases: readonly [];
8899
+ readonly hidden: false;
8900
+ readonly usage: "[options]";
8901
+ readonly arguments: readonly [];
8902
+ readonly options: readonly [{
8903
+ readonly flags: "--dry-run";
8904
+ readonly description: "Report proposed wakes without writing them";
8905
+ }, {
8906
+ readonly flags: "--json";
8907
+ readonly description: "Emit JSON on stdout (default)";
8908
+ }];
8909
+ readonly subcommands: readonly [];
8910
+ }];
8801
8911
  }, {
8802
8912
  readonly name: "reconcile";
8803
8913
  readonly description: "Run one machine-owned reconcile pass and exit";