@indigoai-us/hq-cli 5.345.11 → 5.345.13

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 (54) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-grant.sh +16 -16
  3. package/assets/scaffold/core/scripts/register-project.sh +5 -5
  4. package/assets/scaffold/core/scripts/repo-run-registry.sh +8 -8
  5. package/assets/scaffold/core/scripts/resume-thread-lock.sh +4 -4
  6. package/assets/scaffold/core/scripts/verify-story-deliverables.sh +1 -1
  7. package/assets/scaffold/core/scripts/worktree-gc.sh +1 -1
  8. package/dist/commands/core-delegate-w1b.js +14 -9
  9. package/dist/commands/core-delegate.d.ts +1 -0
  10. package/dist/commands/core-delegate.js +10 -2
  11. package/dist/commands/core-handoff.js +69 -34
  12. package/dist/commands/core-hq-session-native.js +26 -5
  13. package/dist/commands/core-job-notify-fast.d.ts +2 -0
  14. package/dist/commands/core-job-notify-fast.js +6 -0
  15. package/dist/commands/core-job-probe-fast.d.ts +2 -0
  16. package/dist/commands/core-job-probe-fast.js +11 -0
  17. package/dist/commands/core-job-probe-native.js +1 -1
  18. package/dist/commands/core-job-remediate-fast.d.ts +2 -0
  19. package/dist/commands/core-job-remediate-fast.js +6 -0
  20. package/dist/commands/core-job-run-fast.d.ts +2 -0
  21. package/dist/commands/core-job-run-fast.js +6 -0
  22. package/dist/commands/core-pin-fast.d.ts +2 -0
  23. package/dist/commands/core-pin-fast.js +15 -0
  24. package/dist/commands/core-rebuild-index-fast.d.ts +6 -0
  25. package/dist/commands/core-rebuild-index-fast.js +39 -0
  26. package/dist/commands/core-register-project-native.js +11 -2
  27. package/dist/commands/core-repo-run-registry-native.js +47 -4
  28. package/dist/commands/core-session-journal-native.d.ts +1 -1
  29. package/dist/commands/core-session-journal-native.js +2 -2
  30. package/dist/commands/core-share-suggestion-state-native.js +6 -2
  31. package/dist/commands/core-work-mesh-live-rebind-native.js +5 -2
  32. package/dist/commands/core-worker-lint-fast.d.ts +2 -0
  33. package/dist/commands/core-worker-lint-fast.js +11 -0
  34. package/dist/commands/core.js +9 -6
  35. package/dist/fast-scaffold.js +49 -0
  36. package/dist/lib/core-utils/worktree-resume.d.ts +1 -0
  37. package/dist/lib/core-utils/worktree-resume.js +68 -31
  38. package/dist/lib/daemon/catalog.d.ts +10 -0
  39. package/dist/lib/daemon/catalog.js +8 -0
  40. package/dist/lib/daemon/lanes-reconcile-guard.d.ts +43 -0
  41. package/dist/lib/daemon/lanes-reconcile-guard.js +115 -0
  42. package/dist/lib/daemon/run.js +2 -0
  43. package/dist/lib/daemon/supervisor.d.ts +2 -0
  44. package/dist/lib/daemon/supervisor.js +28 -0
  45. package/dist/lib/lanes/autopilot-registry.d.ts +2 -5
  46. package/dist/lib/lanes/autopilot-registry.js +4 -4
  47. package/dist/lib/lanes/compaction.d.ts +2 -2
  48. package/dist/lib/lanes/compaction.js +221 -54
  49. package/dist/lib/lanes/move-account.js +24 -1
  50. package/dist/lib/lanes/reconcile.d.ts +0 -5
  51. package/dist/lib/lanes/reconcile.js +4 -4
  52. package/dist/lib/lanes/types.d.ts +6 -5
  53. package/dist/utils/run-bundled-script.js +1 -1
  54. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.13] — 2026-10-06
6
+ - Stream provider transcript scans and account-move comparisons so large session files do not require whole-file reads.
7
+
8
+ ## [5.345.12] — 2026-10-06
9
+ - Route rebuild-index, pin, scheduled-job commands, and worker-skill lint through the fast CLI entry path to reduce command startup time.
10
+ - `hq daemon` on desktop machines no longer starts a lane reconcile process every 15 seconds when no lane needs it. It checks the lane store first: live lanes, keep-alive lanes, pending resumes and pending questions keep the 15-second cadence; a store with only settled lanes is reconciled once a minute, as before 15-second polling; an HQ folder with no lanes, questions, lead registry or lanes config is not reconciled at all. Outposts and self-hosted machines are unchanged.
11
+ - Fixed Windows shell/native parity for jq output and Git Bash path display in hook-state and worktree helpers.
12
+ - Fixed Windows parity for clipboard command shims and CRLF output from bundled scaffold scripts.
13
+ - Made the repo-run-registry clock fixture deterministic and stripped jq.exe carriage returns before writing registry state files.
14
+
5
15
  ## [5.345.11] — 2026-10-05
6
16
  - Fix Windows parity for delegate, scheduled job, and session helpers.
7
17
 
@@ -65,11 +65,11 @@ done
65
65
  [ -f "$MANIFEST" ] || die "manifest not found: $MANIFEST"
66
66
  jq -e . "$MANIFEST" >/dev/null 2>&1 || die "manifest is not valid JSON: $MANIFEST"
67
67
 
68
- COMPANY="$(jq -r '.company // empty' "$MANIFEST")"
69
- PROJECT="$(jq -r '.project.name // empty' "$MANIFEST")"
70
- PRINCIPAL="$(jq -r '.to.principal // empty' "$MANIFEST")"
71
- DISPLAY="$(jq -r '.to.displayName // .to.principal // empty' "$MANIFEST")"
72
- STATUS="$(jq -r '.status // empty' "$MANIFEST")"
68
+ COMPANY="$(jq -r '.company // empty' "$MANIFEST" | tr -d '\r')"
69
+ PROJECT="$(jq -r '.project.name // empty' "$MANIFEST" | tr -d '\r')"
70
+ PRINCIPAL="$(jq -r '.to.principal // empty' "$MANIFEST" | tr -d '\r')"
71
+ DISPLAY="$(jq -r '.to.displayName // .to.principal // empty' "$MANIFEST" | tr -d '\r')"
72
+ STATUS="$(jq -r '.status // empty' "$MANIFEST" | tr -d '\r')"
73
73
  [ -n "$COMPANY" ] || die "manifest has no company"
74
74
  [ -n "$PROJECT" ] || die "manifest has no project.name"
75
75
  [ -n "$PRINCIPAL" ] || die "manifest has no to.principal"
@@ -96,7 +96,7 @@ case "$PRINCIPAL" in
96
96
  TAIL="$(printf '%s' "$PRINCIPAL" | tail -c 9 | tr '[:upper:]' '[:lower:]')"
97
97
  LEGACY_GROUP="grp_dlg-$TAIL"
98
98
  NEXT_GROUP="$(agent_group_id "$PRINCIPAL")"
99
- RECORDED_GROUP="$(jq -r '.grantPrincipal // empty' "$MANIFEST")"
99
+ RECORDED_GROUP="$(jq -r '.grantPrincipal // empty' "$MANIFEST" | tr -d '\r')"
100
100
  case "$RECORDED_GROUP" in
101
101
  "$LEGACY_GROUP"|"$NEXT_GROUP") GRANT_PRINCIPAL="$RECORDED_GROUP" ;;
102
102
  *) GRANT_PRINCIPAL="$NEXT_GROUP" ;;
@@ -109,7 +109,7 @@ case "$STATUS" in
109
109
  *) die "manifest status is '$STATUS' — grants run from 'building' (or re-run from 'granted'), not from there" ;;
110
110
  esac
111
111
 
112
- PREFIX_COUNT="$(jq '.vaultPrefixes | length' "$MANIFEST")"
112
+ PREFIX_COUNT="$(jq '.vaultPrefixes | length' "$MANIFEST" | tr -d '\r')"
113
113
  [ "$PREFIX_COUNT" -gt 0 ] || die "manifest has no vaultPrefixes"
114
114
 
115
115
  # --- validate every prefix BEFORE touching anything --------------------------
@@ -118,7 +118,7 @@ PREFIX_COUNT="$(jq '.vaultPrefixes | length' "$MANIFEST")"
118
118
  # not exist in the bucket. Either is a hard error, never a silent grant.
119
119
 
120
120
  BAD="$(jq -r '.vaultPrefixes[] | .prefix
121
- | select((endswith("/") | not) or startswith("companies/") or startswith("/"))' "$MANIFEST")"
121
+ | select((endswith("/") | not) or startswith("companies/") or startswith("/"))' "$MANIFEST" | tr -d '\r')"
122
122
  if [ -n "$BAD" ]; then
123
123
  die "prefix violates the hq-files prefix conventions (must be company-relative folder form ending in '/'): $BAD"
124
124
  fi
@@ -130,8 +130,8 @@ if [ "$GRANT_PRINCIPAL" != "$PRINCIPAL" ]; then
130
130
  echo " 0. Agent recipient: grants flow through group $GRANT_PRINCIPAL (created if absent, containing exactly $DISPLAY)"
131
131
  fi
132
132
  echo " 1. Push companies/$COMPANY/projects/$PROJECT/ to the vault (on-conflict keep)"
133
- jq -r '.vaultPrefixes[] | " 2. Grant \(.permission) on \(.prefix) — \(.reason // "")"' "$MANIFEST"
134
- WRITE_PREFIXES="$(jq -r '.vaultPrefixes[] | select(.permission == "write") | .prefix' "$MANIFEST")"
133
+ jq -r '.vaultPrefixes[] | " 2. Grant \(.permission) on \(.prefix) — \(.reason // "")"' "$MANIFEST" | tr -d '\r'
134
+ WRITE_PREFIXES="$(jq -r '.vaultPrefixes[] | select(.permission == "write") | .prefix' "$MANIFEST" | tr -d '\r')"
135
135
  if [ -n "$WRITE_PREFIXES" ]; then
136
136
  echo
137
137
  echo "NOTE: granting 'write' is a privilege escalation. The recipient will be able"
@@ -155,7 +155,7 @@ hq sync push "companies/$COMPANY/projects/$PROJECT/" --company "$COMPANY" --on-c
155
155
 
156
156
  jq -r --arg co "$COMPANY" \
157
157
  '((.knowledge // []) + (.policies // []))[] | select(startswith("companies/" + $co + "/"))' \
158
- "$MANIFEST" | while IFS= read -r kpath; do
158
+ "$MANIFEST" | tr -d '\r' | while IFS= read -r kpath; do
159
159
  if [ -e "$HQ_ROOT/$kpath" ]; then
160
160
  hq sync push "$kpath" --company "$COMPANY" --on-conflict keep \
161
161
  || die "vault push failed for referenced knowledge: $kpath"
@@ -195,8 +195,8 @@ case "$GRANT_PRINCIPAL" in
195
195
  if printf '%s' "$RESOLVED" | jq -e '.status == "found" and (.person.personUid | type == "string") and (.person.companyUid | type == "string")' >/dev/null; then
196
196
  [ "$RESOLVE_RC" -eq 0 ] || die "recipient resolution failed"
197
197
  EXPECTED_TYPE=person
198
- EXPECTED_ID="$(printf '%s' "$RESOLVED" | jq -r '.person.personUid')"
199
- EXPECTED_COMPANY="$(printf '%s' "$RESOLVED" | jq -r '.person.companyUid')"
198
+ EXPECTED_ID="$(printf '%s' "$RESOLVED" | jq -r '.person.personUid' | tr -d '\r')"
199
+ EXPECTED_COMPANY="$(printf '%s' "$RESOLVED" | jq -r '.person.companyUid' | tr -d '\r')"
200
200
  elif printf '%s' "$RESOLVED" | jq -e '.status == "not_found"' >/dev/null; then
201
201
  case "$PRINCIPAL" in *@*) EXPECTED_TYPE=email; EXPECTED_ID="$(printf '%s' "$PRINCIPAL" | tr '[:upper:]' '[:lower:]' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" ;; *) die "unresolved recipient" ;; esac
202
202
  else
@@ -223,15 +223,15 @@ read_acl() {
223
223
  NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
224
224
  i=0
225
225
  while [ "$i" -lt "$PREFIX_COUNT" ]; do
226
- PFX="$(jq -r ".vaultPrefixes[$i].prefix" "$MANIFEST")"
227
- PERM="$(jq -r ".vaultPrefixes[$i].permission" "$MANIFEST")"
226
+ PFX="$(jq -r ".vaultPrefixes[$i].prefix" "$MANIFEST" | tr -d '\r')"
227
+ PERM="$(jq -r ".vaultPrefixes[$i].permission" "$MANIFEST" | tr -d '\r')"
228
228
  # Always address the recursive "folder/*" pattern explicitly. Since hq-pro
229
229
  # #3662 (2026-09-23) the server echoes the raw pattern back on read, and a
230
230
  # bare trailing "/" can mean a PRIVATE folder pattern when that flag is on.
231
231
  # A delegation grant is always the recursive share, never a private folder.
232
232
  ACL_PATTERN="${PFX}*"
233
233
  BEFORE="$(read_acl "$ACL_PATTERN")" || die "ACL preflight failed; no share attempted for $PFX"
234
- EXPECTED_COMPANY="$(printf '%s' "$BEFORE" | jq -r '.companyUid')"
234
+ EXPECTED_COMPANY="$(printf '%s' "$BEFORE" | jq -r '.companyUid' | tr -d '\r')"
235
235
  SHARE_RC=0
236
236
  hq files share "$ACL_PATTERN" --with "$GRANT_PRINCIPAL" --permission "$PERM" --company "$COMPANY" || SHARE_RC=$?
237
237
  [ "$SHARE_RC" -eq 0 ] || echo "hq-delegate-grant: share reported an error on $PFX; checking exact grant" >&2
@@ -73,7 +73,7 @@ mark_pending() {
73
73
  [ -n "$title" ] || title="$project"
74
74
  mkdir -p "$(dirname "$board")"
75
75
  if [ ! -f "$board" ]; then
76
- jq -n --arg company "$company" '{company:$company, projects:[]}' > "$board"
76
+ jq -n --arg company "$company" '{company:$company, projects:[]}' | tr -d '\r' > "$board"
77
77
  fi
78
78
  tmp="$(mktemp)"
79
79
  jq \
@@ -101,7 +101,7 @@ mark_pending() {
101
101
  updated_at:$now
102
102
  }]
103
103
  end
104
- ' "$board" > "$tmp"
104
+ ' "$board" | tr -d '\r' > "$tmp"
105
105
  mv "$tmp" "$board"
106
106
  }
107
107
 
@@ -132,7 +132,7 @@ register_one() {
132
132
  [ -n "$title" ] || title="$project"
133
133
  mkdir -p "$(dirname "$board")"
134
134
  if [ ! -f "$board" ]; then
135
- jq -n --arg company "$company" '{company:$company, projects:[]}' > "$board"
135
+ jq -n --arg company "$company" '{company:$company, projects:[]}' | tr -d '\r' > "$board"
136
136
  fi
137
137
  tmp="$(mktemp)"
138
138
  jq \
@@ -164,7 +164,7 @@ register_one() {
164
164
  updated_at:$now
165
165
  }]
166
166
  end
167
- ' "$board" > "$tmp"
167
+ ' "$board" | tr -d '\r' > "$tmp"
168
168
  mv "$tmp" "$board"
169
169
  jq -e \
170
170
  --arg id "$project" \
@@ -245,7 +245,7 @@ retry_pending() {
245
245
  count=$((count + 1))
246
246
  [ "$count" -le 3 ] || break
247
247
  ( register_one "$company" "$id" ) || true
248
- done < <(jq -r '[.projects[]? | select(.pending_registration == true) | .id // empty] | .[]' "$board" 2>/dev/null || true)
248
+ done < <(jq -r '[.projects[]? | select(.pending_registration == true) | .id // empty] | .[]' "$board" 2>/dev/null | tr -d '\r' || true)
249
249
  }
250
250
 
251
251
  if [ "${1:-}" = "--audit" ]; then
@@ -219,7 +219,7 @@ _prune_stale() {
219
219
  kept=$(echo "$kept" | jq --argjson e "$entry" '. + [$e]')
220
220
  done <<< "$runs"
221
221
  fi
222
- jq --argjson runs "$kept" '.runs = $runs' "$REG_FILE" > "$tmp"
222
+ jq --argjson runs "$kept" '.runs = $runs' "$REG_FILE" | tr -d '\r' > "$tmp"
223
223
  mv "$tmp" "$REG_FILE"
224
224
  [[ $pruned -gt 0 ]] && _log "pruned $pruned stale entries"
225
225
  return 0
@@ -277,7 +277,7 @@ _cmd_register() {
277
277
  host: $host,
278
278
  started_at: $now,
279
279
  heartbeat_at: $now
280
- }]' "$REG_FILE" > "$tmp"
280
+ }]' "$REG_FILE" | tr -d '\r' > "$tmp"
281
281
  mv "$tmp" "$REG_FILE"
282
282
  _unlock
283
283
  trap - EXIT
@@ -297,7 +297,7 @@ _cmd_deregister() {
297
297
  trap '_unlock' EXIT
298
298
  _ensure_reg
299
299
  local tmp="$REG_FILE.tmp.$$"
300
- jq --arg run_id "$run_id" '.runs |= map(select(.run_id != $run_id))' "$REG_FILE" > "$tmp"
300
+ jq --arg run_id "$run_id" '.runs |= map(select(.run_id != $run_id))' "$REG_FILE" | tr -d '\r' > "$tmp"
301
301
  mv "$tmp" "$REG_FILE"
302
302
  _unlock
303
303
  trap - EXIT
@@ -320,7 +320,7 @@ _cmd_heartbeat() {
320
320
  local tmp="$REG_FILE.tmp.$$"
321
321
  jq --arg run_id "$run_id" --arg now "$now" \
322
322
  '.runs |= map(if .run_id == $run_id then .heartbeat_at = $now else . end)' \
323
- "$REG_FILE" > "$tmp"
323
+ "$REG_FILE" | tr -d '\r' > "$tmp"
324
324
  mv "$tmp" "$REG_FILE"
325
325
  _unlock
326
326
  trap - EXIT
@@ -329,7 +329,7 @@ _cmd_heartbeat() {
329
329
  _cmd_list() {
330
330
  _ensure_reg
331
331
  _prune_stale_locked
332
- jq '.runs' "$REG_FILE"
332
+ jq '.runs' "$REG_FILE" | tr -d '\r'
333
333
  }
334
334
 
335
335
  _cmd_clean_stale() {
@@ -371,14 +371,14 @@ _cmd_owner_of() {
371
371
  ($entry.scope == "repo" and $entry.repo_path == $repo)
372
372
  or (($entry.scope | startswith("worktree:")) and
373
373
  worktree_covers($target; $entry.scope))
374
- ) | $entry]' "$REG_FILE"
374
+ ) | $entry]' "$REG_FILE" | tr -d '\r'
375
375
  else
376
376
  jq --arg repo "$repo" --arg target "$(_abs_path "$path")" \
377
377
  '[.runs[] | select(
378
378
  (.scope == "repo" and .repo_path == $repo)
379
379
  or (.scope | startswith("worktree:")) and
380
380
  ($target | startswith(.scope | sub("^worktree:"; "")))
381
- )]' "$REG_FILE"
381
+ )]' "$REG_FILE" | tr -d '\r'
382
382
  fi
383
383
  }
384
384
 
@@ -451,7 +451,7 @@ _cmd_check() {
451
451
 
452
452
  # Emit one summary line per owner to stderr for the calling hook
453
453
  echo "$matches" | jq -r '.[] |
454
- "run_id=\(.run_id) pid=\(.pid) command=\(.command) project=\(.project) scope=\(.scope) repo=\(.repo_path) started=\(.started_at) heartbeat=\(.heartbeat_at)"' >&2
454
+ "run_id=\(.run_id) pid=\(.pid) command=\(.command) project=\(.project) scope=\(.scope) repo=\(.repo_path) started=\(.started_at) heartbeat=\(.heartbeat_at)"' | tr -d '\r' >&2
455
455
  return 2
456
456
  }
457
457
 
@@ -138,7 +138,7 @@ emit_unlocked() {
138
138
  jq -cn \
139
139
  --arg thread_id "$thread_id" \
140
140
  --arg lock_path "$relative_lock_path" \
141
- '{status:"unlocked", thread_id:$thread_id, lock_path:$lock_path}'
141
+ '{status:"unlocked", thread_id:$thread_id, lock_path:$lock_path}' | tr -d '\r'
142
142
  }
143
143
 
144
144
  emit_stale() {
@@ -152,7 +152,7 @@ emit_stale() {
152
152
  --arg resumed_at "$resumed_at" \
153
153
  --arg lock_generation "$generation" \
154
154
  --arg prompt "$prompt" \
155
- '{status:"stale", thread_id:$thread_id, lock_path:$lock_path, stale_reason:$stale_reason, session_id:$session_id, resumed_at:$resumed_at, lock_generation:$lock_generation, prompt:$prompt}'
155
+ '{status:"stale", thread_id:$thread_id, lock_path:$lock_path, stale_reason:$stale_reason, session_id:$session_id, resumed_at:$resumed_at, lock_generation:$lock_generation, prompt:$prompt}' | tr -d '\r'
156
156
  }
157
157
 
158
158
  emit_locked() {
@@ -165,7 +165,7 @@ emit_locked() {
165
165
  --arg resumed_at "$resumed_at" \
166
166
  --arg lock_generation "$generation" \
167
167
  --arg prompt "$prompt" \
168
- '{status:"locked", thread_id:$thread_id, lock_path:$lock_path, session_id:$session_id, resumed_at:$resumed_at, lock_generation:$lock_generation, prompt:$prompt}'
168
+ '{status:"locked", thread_id:$thread_id, lock_path:$lock_path, session_id:$session_id, resumed_at:$resumed_at, lock_generation:$lock_generation, prompt:$prompt}' | tr -d '\r'
169
169
  }
170
170
 
171
171
  # Print the existing lock's state. A damaged or expired marker is deliberately
@@ -244,7 +244,7 @@ emit_acquired() {
244
244
  --arg lock_path "$relative_lock_path" \
245
245
  --arg session_id "$session_id" \
246
246
  --arg lock_generation "$generation" \
247
- '{status:"acquired", thread_id:$thread_id, lock_path:$lock_path, session_id:$session_id, lock_generation:$lock_generation}'
247
+ '{status:"acquired", thread_id:$thread_id, lock_path:$lock_path, session_id:$session_id, lock_generation:$lock_generation}' | sed 's/\r$//'
248
248
  }
249
249
 
250
250
  case "$subcommand" in
@@ -123,7 +123,7 @@ while IFS= read -r d; do
123
123
  done < <(jq_capture -r '.[]' <<<"$REFS_JSON")
124
124
 
125
125
  if [ "$JSON" = 1 ]; then
126
- jq -cn --arg s "$STORY" \
126
+ jq_capture -cn --arg s "$STORY" \
127
127
  --argjson m "$(printf '%s\n' "${missing[@]:-}" | jq_capture -R . | jq_capture -s 'map(select(. != ""))')" \
128
128
  --argjson p "$(printf '%s\n' "${present[@]:-}" | jq_capture -R . | jq_capture -s 'map(select(. != ""))')" \
129
129
  '{story:$s, status:(if ($m|length)==0 then "present" else "missing" end), missing:$m, present:$p}'
@@ -350,7 +350,7 @@ if [[ "$JSON" -eq 1 ]]; then
350
350
  skipped_by_reason: ([$records[] | select(.action=="skipped") | .reason]
351
351
  | group_by(.) | map({(.[0]): length}) | add // {}),
352
352
  records: $records
353
- }'
353
+ }' | tr -d '\r'
354
354
  else
355
355
  if [[ "$APPLY" -eq 1 ]]; then
356
356
  echo "worktree-gc [apply]: examined $examined · removed $removed · reclaimed ${reclaimed_gb} GB · skipped ${skip_total} (retention ${DAYS}d)"
@@ -7,7 +7,7 @@ import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
7
7
  import { shellDisplayPath } from "../lib/core-utils/shell-display-path.js";
8
8
  import { matchingSecretPatterns } from "../lib/core-utils/secret-patterns.js";
9
9
  import { describeCoreCommand } from "./core-command-catalog.js";
10
- import { commandExists, isRecord, jsonAt, jqFallback, jqRawPath, jqRawPaths, moveFile, readJsonFile, subprocess, writeJsonAtomically, } from "./core-delegate.js";
10
+ import { commandExists, delegateDiagnosticPath, isRecord, jsonAt, jqFallback, jqRawPath, jqRawPaths, moveFile, readJsonFile, subprocess, writeJsonAtomically, } from "./core-delegate.js";
11
11
  import { resolveLiveRoot } from "../utils/hq-roots.js";
12
12
  import { ACL_SOURCE_DISPLAY_LABELS } from "../utils/files-browse-acl.js";
13
13
  import { runBundledScript } from "../utils/run-bundled-script.js";
@@ -179,11 +179,16 @@ function bashPath(value) {
179
179
  const drivePath = /^([A-Za-z]):\/(.*)$/u.exec(normalized);
180
180
  return drivePath ? `/${drivePath[1].toLowerCase()}/${drivePath[2]}` : normalized;
181
181
  }
182
+ function shellFallbackArgs(args) {
183
+ if (process.platform !== "win32")
184
+ return args;
185
+ return args.map((arg, index) => args[index - 1] === "--manifest" && /^[a-z]:[\\/]/i.test(arg) ? bashPath(arg) : arg);
186
+ }
182
187
  function runShellFallback(name, args, context) {
183
188
  const root = context.root ?? context.cwd;
184
189
  const result = runBundledScript({
185
190
  asset: ASSETS[name],
186
- args,
191
+ args: shellFallbackArgs(args),
187
192
  cwd: context.cwd,
188
193
  hqRoot: bashPath(root),
189
194
  captureOutput: true,
@@ -404,16 +409,16 @@ function runGrant(args, context) {
404
409
  if (!manifestPath)
405
410
  return fail("grant", "--manifest is required");
406
411
  if (!fs.existsSync(manifestPath) || !fs.statSync(manifestPath).isFile())
407
- return fail("grant", `manifest not found: ${shellDisplayPath(manifestPath)}`);
412
+ return fail("grant", `manifest not found: ${delegateDiagnosticPath(manifestPath)}`);
408
413
  let manifest;
409
414
  try {
410
415
  manifest = readJsonFile(manifestPath);
411
416
  }
412
417
  catch {
413
- return fail("grant", `manifest is not valid JSON: ${shellDisplayPath(manifestPath)}`);
418
+ return fail("grant", `manifest is not valid JSON: ${delegateDiagnosticPath(manifestPath)}`);
414
419
  }
415
420
  if (manifest === null || manifest === false)
416
- return fail("grant", `manifest is not valid JSON: ${shellDisplayPath(manifestPath)}`);
421
+ return fail("grant", `manifest is not valid JSON: ${delegateDiagnosticPath(manifestPath)}`);
417
422
  if (!isRecord(manifest))
418
423
  return fail("grant", "manifest has no company");
419
424
  const company = jqRawPath(manifest, "company");
@@ -790,10 +795,10 @@ function runSend(args, context) {
790
795
  if (!manifestPath)
791
796
  return fail("send", "--manifest is required");
792
797
  if (!fs.existsSync(manifestPath) || !fs.statSync(manifestPath).isFile())
793
- return fail("send", `manifest not found: ${shellDisplayPath(manifestPath)}`);
798
+ return fail("send", `manifest not found: ${delegateDiagnosticPath(manifestPath)}`);
794
799
  const manifest = jsonFile(manifestPath);
795
800
  if (!manifest)
796
- return fail("send", `manifest is not valid JSON: ${shellDisplayPath(manifestPath)}`);
801
+ return fail("send", `manifest is not valid JSON: ${delegateDiagnosticPath(manifestPath)}`);
797
802
  if (send) {
798
803
  const publishedPath = path.join(context.root ?? context.cwd, jqRawPath(manifest, "publication.manifestPath"));
799
804
  if (malformedJsonFile(publishedPath)) {
@@ -1047,12 +1052,12 @@ function runVerify(args, context) {
1047
1052
  if (!manifestPath)
1048
1053
  return fail("verify", "--manifest is required (or --dry-run with --company/--project/--to)");
1049
1054
  if (!fs.existsSync(manifestPath) || !fs.statSync(manifestPath).isFile())
1050
- return fail("verify", `manifest not found: ${shellDisplayPath(manifestPath)}`);
1055
+ return fail("verify", `manifest not found: ${delegateDiagnosticPath(manifestPath)}`);
1051
1056
  if (malformedJsonFile(manifestPath))
1052
1057
  return runShellFallback("verify", args, context);
1053
1058
  const manifest = jsonFile(manifestPath);
1054
1059
  if (!manifest)
1055
- return fail("verify", `jq: error: ${shellDisplayPath(manifestPath)}: Invalid JSON`);
1060
+ return fail("verify", `jq: error: ${delegateDiagnosticPath(manifestPath)}: Invalid JSON`);
1056
1061
  const lock = `${manifestPath}.send-lock`;
1057
1062
  try {
1058
1063
  fs.mkdirSync(lock);
@@ -40,6 +40,7 @@ export declare function jqRawPaths(value: unknown, dottedPaths: string[], fallba
40
40
  export declare function requireRecord(value: unknown, property: string): JsonRecord;
41
41
  export declare function moveFile(source: string, target: string): void;
42
42
  export declare function writeJsonAtomically(target: string, value: unknown, siblingTemp?: boolean): void;
43
+ export declare function delegateDiagnosticPath(argument: string): string;
43
44
  /** Fast-entry adapter; family gating and shell fallback happen before import. */
44
45
  export declare function runDelegateFast(args: readonly string[], _stdin: string, cwd: string, hqRoot: string | undefined, commandName: string): number;
45
46
  /** Register the Wave 1a delegate commands and preserve the shell rollback path. */
@@ -188,6 +188,7 @@ export function moveFile(source, target) {
188
188
  }
189
189
  export function writeJsonAtomically(target, value, siblingTemp = false) {
190
190
  const serialized = serializeJqJson(value, "pretty");
191
+ // Native delegate JSON writers use LF on every platform.
191
192
  if (siblingTemp) {
192
193
  const temporary = `${target}.tmp`;
193
194
  fs.writeFileSync(temporary, serialized);
@@ -557,6 +558,13 @@ function parseStandard(name, args) {
557
558
  }
558
559
  return { values };
559
560
  }
561
+ export function delegateDiagnosticPath(argument) {
562
+ // Git Bash receives drive-qualified native paths in MSYS spelling. Preserve
563
+ // relative and already-MSYS path arguments exactly as supplied.
564
+ if (process.platform === "win32" && /^[a-z]:[\\/]/i.test(argument))
565
+ return shellDisplayPath(argument);
566
+ return argument;
567
+ }
560
568
  function existingManifest(name, args, context) {
561
569
  const prefix = `hq-delegate-${name}:`;
562
570
  const parsed = parseStandard(name, args);
@@ -575,11 +583,11 @@ function existingManifest(name, args, context) {
575
583
  }
576
584
  const file = path.resolve(context.cwd, manifestArg);
577
585
  if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
578
- writeErr(`${prefix} manifest not found: ${shellDisplayPath(manifestArg)}\n`);
586
+ writeErr(`${prefix} manifest not found: ${delegateDiagnosticPath(manifestArg)}\n`);
579
587
  return { code: 1, parsed };
580
588
  }
581
589
  if (!jqValid(file)) {
582
- writeErr(`${prefix} manifest is not valid JSON: ${shellDisplayPath(manifestArg)}\n`);
590
+ writeErr(`${prefix} manifest is not valid JSON: ${delegateDiagnosticPath(manifestArg)}\n`);
583
591
  return { code: 1, parsed };
584
592
  }
585
593
  return { file, parsed, document: readJsonFile(file) };
@@ -12,6 +12,7 @@ import { defaultBackgroundDependencies, runBackgroundLauncher, } from "../lib/se
12
12
  import { resolveLiveRoot } from "../utils/hq-roots.js";
13
13
  import { runBundledScript } from "../utils/run-bundled-script.js";
14
14
  import { describeCoreCommand } from "./core-command-catalog.js";
15
+ import { shellDisplayPath } from "../lib/core-utils/shell-display-path.js";
15
16
  import { compactJson, isRecord, jqTruthy, prettyJson, runHandoffOpenSteps, shellDate, } from "./core-handoff-open-steps.js";
16
17
  export { runHandoffOpenSteps };
17
18
  const ASSETS = {
@@ -113,16 +114,20 @@ function git(args, cwd) {
113
114
  }
114
115
  function commandAvailable(name) {
115
116
  for (const entry of (process.env.PATH ?? "").split(path.delimiter)) {
116
- const candidate = path.resolve(entry || ".", name);
117
- try {
118
- fs.accessSync(candidate, fs.constants.X_OK);
119
- if (fs.statSync(candidate).isFile())
120
- return true;
121
- }
122
- catch (error) {
123
- const code = error.code;
124
- if (code !== "ENOENT" && code !== "ENOTDIR" && code !== "EACCES")
125
- throw error;
117
+ const candidates = process.platform === "win32" && path.extname(name) === ""
118
+ ? [`${name}.exe`, `${name}.cmd`, `${name}.bat`, name].map((candidate) => path.resolve(entry || ".", candidate))
119
+ : [path.resolve(entry || ".", name)];
120
+ for (const candidate of candidates) {
121
+ try {
122
+ fs.accessSync(candidate, fs.constants.X_OK);
123
+ if (fs.statSync(candidate).isFile())
124
+ return true;
125
+ }
126
+ catch (error) {
127
+ const code = error.code;
128
+ if (code !== "ENOENT" && code !== "ENOTDIR" && code !== "EACCES" && code !== "UNKNOWN")
129
+ throw error;
130
+ }
126
131
  }
127
132
  }
128
133
  return false;
@@ -143,6 +148,7 @@ function copyClipboardText(value, cwd) {
143
148
  env: process.env,
144
149
  input: value,
145
150
  stdio: ["pipe", "ignore", "ignore"],
151
+ shell: process.platform === "win32",
146
152
  });
147
153
  return result.status === 0;
148
154
  }
@@ -797,18 +803,45 @@ function spawnBashDetached(script, args, cwd, root, logFile) {
797
803
  let fd;
798
804
  try {
799
805
  mkdirParent(logFile);
800
- fd = fs.openSync(logFile, "a");
801
- const child = spawn("bash", [script, ...args], {
802
- cwd,
803
- env: { ...process.env, HQ_ROOT: root },
804
- detached: true,
805
- windowsHide: true,
806
- stdio: ["ignore", fd, fd],
807
- });
806
+ const childEnv = { ...process.env };
807
+ if (process.platform === "win32") {
808
+ // Windows treats environment variable names case-insensitively. Replace
809
+ // every inherited spelling with the Git Bash spelling for this root;
810
+ // if Bash derives PWD from Node's Windows cwd itself, it can truncate the
811
+ // drive prefix when starting a detached child.
812
+ for (const key of Object.keys(childEnv)) {
813
+ if (key.toUpperCase() === "PWD")
814
+ delete childEnv[key];
815
+ }
816
+ childEnv.PWD = shellDisplayPath(root);
817
+ childEnv.HQ_ROOT = shellDisplayPath(root);
818
+ childEnv.HQ_HANDOFF_CHILD_LOG_FILE = shellDisplayPath(logFile);
819
+ }
820
+ else {
821
+ fd = fs.openSync(logFile, "a");
822
+ childEnv.HQ_ROOT = root;
823
+ }
824
+ const child = process.platform === "win32"
825
+ ? spawn("bash", ["-c", 'cd -- "$HQ_ROOT" || exit; exec bash "$@" >> "$HQ_HANDOFF_CHILD_LOG_FILE" 2>&1', "_", shellDisplayPath(script), ...args], {
826
+ cwd,
827
+ env: childEnv,
828
+ detached: true,
829
+ windowsHide: true,
830
+ stdio: ["ignore", "ignore", "ignore"],
831
+ })
832
+ : spawn("bash", [script, ...args], {
833
+ cwd,
834
+ env: childEnv,
835
+ detached: true,
836
+ windowsHide: true,
837
+ stdio: ["ignore", fd, fd],
838
+ });
808
839
  child.once("error", (error) => appendLog(logFile, `${error.message}\n`));
809
840
  child.unref();
810
- fs.closeSync(fd);
811
- fd = undefined;
841
+ if (fd !== undefined) {
842
+ fs.closeSync(fd);
843
+ fd = undefined;
844
+ }
812
845
  return child.pid;
813
846
  }
814
847
  catch {
@@ -836,11 +869,18 @@ function parseLearningCount(file) {
836
869
  }
837
870
  }
838
871
  async function launchHandoffPost(args, root) {
872
+ const childEnv = { ...process.env, HQ_NATIVE_HANDOFF_POST_CHILD: "1" };
873
+ if (process.platform === "win32") {
874
+ for (const key of Object.keys(childEnv))
875
+ if (key.toUpperCase() === "PWD")
876
+ delete childEnv[key];
877
+ childEnv.PWD = shellDisplayPath(root);
878
+ }
839
879
  return await new Promise((resolve) => {
840
880
  try {
841
881
  const child = spawn("hq", ["core", "--hq-root", root, "handoff", "post", ...args], {
842
882
  cwd: root,
843
- env: { ...process.env, HQ_NATIVE_HANDOFF_POST_CHILD: "1" },
883
+ env: childEnv,
844
884
  detached: true,
845
885
  windowsHide: true,
846
886
  stdio: "ignore",
@@ -902,19 +942,14 @@ export function runHandoffPostWorker(args, context) {
902
942
  log("document-release: skipped unsupported files_touched entry");
903
943
  if (counts.scope > 0) {
904
944
  const installedScript = path.join(root, "core/scripts/skill-installed.sh");
905
- const logFd = fs.openSync(logMain, "a");
906
- let installedStatus = null;
907
- try {
908
- const installed = spawnSync("bash", [installedScript, "document-release", process.env.HQ_ACTIVE_COMPANY || ""], {
909
- cwd: root,
910
- env: process.env,
911
- stdio: ["ignore", "ignore", logFd],
912
- });
913
- installedStatus = installed.status;
914
- }
915
- finally {
916
- fs.closeSync(logFd);
917
- }
945
+ const installed = spawnSync("bash", [installedScript, "document-release", process.env.HQ_ACTIVE_COMPANY || ""], {
946
+ cwd: root,
947
+ env: process.env,
948
+ stdio: ["ignore", "ignore", "pipe"],
949
+ });
950
+ if (installed.stderr?.length)
951
+ fs.appendFileSync(logMain, installed.stderr);
952
+ const installedStatus = installed.status;
918
953
  if (installedStatus === 0) {
919
954
  log(`document-release: eligible and pending runtime dispatch by handoff skill (${counts.scope} scoped files; no dispatch proof)`);
920
955
  }
@@ -4,6 +4,7 @@ import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { spawn, spawnSync } from "node:child_process";
6
6
  import { awkCannotOpenFileDiagnostic } from "../lib/core-utils/awk-diagnostics.js";
7
+ import { shellDisplayPath } from "../lib/core-utils/shell-display-path.js";
7
8
  export const HQ_SESSION_ASSET = "core/scripts/hq-session.sh";
8
9
  export const HQ_SESSION_HELP = [
9
10
  "hq-core: public",
@@ -154,14 +155,18 @@ function findMetaValue(contents, key) {
154
155
  return { found: false, value: "" };
155
156
  }
156
157
  function getMetaValue(contents, key) { return findMetaValue(contents, key).value; }
157
- const WORK_MESH_SESSION_START_COMMAND = 'printf "%s" "$1" | bash "$2" SessionStart';
158
+ const WORK_MESH_SESSION_START_COMMAND = 'event=$HQ_CORE_SESSION_EVENT_JSON; unset HQ_CORE_SESSION_EVENT_JSON; printf "%s" "$event" | bash "$1" SessionStart';
158
159
  function recordWorkMeshSessionStart(input) {
159
160
  const hook = path.join(input.root, "core", "hooks", "SessionStart", "35-work-mesh-session-start.sh");
160
161
  // Keep the shell hook as the single owner of SessionStart's Work Mesh behavior.
161
162
  // The bundled hq-session helper skips this path if either dependency is absent.
162
163
  if (!input.isFile(hook) || !input.findExecutable("jq", input.env))
163
164
  return "";
164
- const event = JSON.stringify({ session_id: input.sessionId, cwd: input.env.PWD || input.cwd });
165
+ // hq-session.sh passes its inherited PWD into the Work Mesh event. On
166
+ // Windows that value is drive-letter based even though Git Bash's own pwd
167
+ // builtin uses /c/... for filesystem operations.
168
+ const eventCwd = input.env.PWD || input.cwd;
169
+ const event = JSON.stringify({ session_id: input.sessionId, cwd: eventCwd });
165
170
  if (!event)
166
171
  return "";
167
172
  const logFile = path.join(input.root, "workspace", "logs", "work-mesh-hook.log");
@@ -170,10 +175,15 @@ function recordWorkMeshSessionStart(input) {
170
175
  }
171
176
  catch { /* The shell also suppresses mkdir failure and attempts the redirected launch. */ }
172
177
  try {
173
- input.spawnDetached("nohup", ["bash", "-c", WORK_MESH_SESSION_START_COMMAND, "_", event, hook], input.cwd, {
178
+ input.spawnDetached("nohup", ["bash", "-c", WORK_MESH_SESSION_START_COMMAND, "_", shellDisplayPath(hook)], input.cwd, {
174
179
  ...input.env,
180
+ HQ_CORE_SESSION_EVENT_JSON: event,
175
181
  HQ_ROOT: input.root,
176
182
  CLAUDE_CODE_SESSION_ID: input.sessionId,
183
+ // Git for Windows must not rewrite the JSON payload's backslashes as it
184
+ // converts command arguments. The following hook path remains eligible
185
+ // for normal MSYS path conversion.
186
+ ...(process.platform === "win32" ? { MSYS2_ARG_CONV_EXCL: "*" } : {}),
177
187
  }, logFile);
178
188
  return "";
179
189
  }
@@ -249,6 +259,15 @@ function currentMetaPath(root, sessionOverride, deps) {
249
259
  : path.join(root, "workspace", "sessions", id, "meta.yaml")
250
260
  : "";
251
261
  }
262
+ function shellPathForDiagnostic(value) {
263
+ if (process.platform !== "win32")
264
+ return value;
265
+ const absolute = path.win32.resolve(value);
266
+ const workspaceIndex = absolute.toLowerCase().indexOf("\\workspace\\");
267
+ return workspaceIndex < 0
268
+ ? absolute
269
+ : `${absolute.slice(0, workspaceIndex)}/workspace/${absolute.slice(workspaceIndex + "\\workspace\\".length).replaceAll("\\", "/")}`;
270
+ }
252
271
  function exactCompanyDir(root, slug, deps) {
253
272
  if (!root || !slug)
254
273
  return false;
@@ -412,7 +431,9 @@ function validateSenior(value) {
412
431
  return id !== "" && id !== "." && id !== ".." && /^[A-Za-z0-9._-]+$/.test(id);
413
432
  }
414
433
  function atomicWrite(filePath, contents, deps) {
415
- const temp = deps.makeTempPath();
434
+ // Keep the staging file on the destination volume so atomic rename works
435
+ // when the system temp directory and HQ_ROOT are on different Windows drives.
436
+ const temp = path.join(path.dirname(filePath), path.basename(deps.makeTempPath()));
416
437
  try {
417
438
  deps.writeFile(temp, contents, 0o600);
418
439
  deps.rename(temp, filePath);
@@ -489,7 +510,7 @@ export function executeHqSession(argv, context, overrides = {}) {
489
510
  }
490
511
  catch (error) {
491
512
  if (error?.code === "EACCES" || error?.code === "EPERM") {
492
- return { code: 2, stdout: "", stderr: awkCannotOpenFileDiagnostic(metaPath, "Permission denied", 6, process.platform, deps.env.PATH) };
513
+ return { code: 2, stdout: "", stderr: awkCannotOpenFileDiagnostic(shellPathForDiagnostic(metaPath), "Permission denied", 6, process.platform, deps.env.PATH) };
493
514
  }
494
515
  if (isExpectedFsMiss(error))
495
516
  return { code: 1, stdout: "", stderr: `awk: cannot open ${metaPath} (Permission denied)\n` };
@@ -0,0 +1,2 @@
1
+ export declare function runJobNotifyFast(args: readonly string[], _stdin: string, _cwd: string, hqRoot: string | undefined): number;
2
+ //# sourceMappingURL=core-job-notify-fast.d.ts.map