@indigoai-us/hq-cli 5.108.0 → 5.108.2

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 (108) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/assets/mesh-daemon/README.md +23 -0
  3. package/assets/mesh-daemon/launchd.plist.template +33 -0
  4. package/assets/mesh-daemon/systemd.system.service.template +27 -0
  5. package/assets/mesh-daemon/systemd.user.service.template +18 -0
  6. package/assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh +267 -0
  7. package/dist/commands/cloud.d.ts +6 -0
  8. package/dist/commands/cloud.js +26 -3
  9. package/dist/commands/core.js +5 -4
  10. package/dist/commands/mesh.d.ts +15 -0
  11. package/dist/commands/mesh.js +732 -4
  12. package/dist/commands/search.d.ts +23 -0
  13. package/dist/commands/search.js +42 -1
  14. package/dist/lib/doctor/checks/work-context.d.ts +23 -0
  15. package/dist/lib/doctor/checks/work-context.js +195 -0
  16. package/dist/lib/doctor/registry.js +4 -0
  17. package/dist/lib/mesh/api.d.ts +18 -0
  18. package/dist/lib/mesh/api.js +55 -0
  19. package/dist/lib/mesh/cache.js +35 -4
  20. package/dist/lib/mesh/client.d.ts +118 -0
  21. package/dist/lib/mesh/client.js +268 -0
  22. package/dist/lib/mesh/live/backoff.d.ts +16 -0
  23. package/dist/lib/mesh/live/backoff.js +20 -0
  24. package/dist/lib/mesh/live/daemon/board-refresh.d.ts +49 -0
  25. package/dist/lib/mesh/live/daemon/board-refresh.js +185 -0
  26. package/dist/lib/mesh/live/daemon/credentials.d.ts +71 -0
  27. package/dist/lib/mesh/live/daemon/credentials.js +227 -0
  28. package/dist/lib/mesh/live/daemon/doctor.d.ts +37 -0
  29. package/dist/lib/mesh/live/daemon/doctor.js +139 -0
  30. package/dist/lib/mesh/live/daemon/index.d.ts +24 -0
  31. package/dist/lib/mesh/live/daemon/index.js +14 -0
  32. package/dist/lib/mesh/live/daemon/install.d.ts +83 -0
  33. package/dist/lib/mesh/live/daemon/install.js +330 -0
  34. package/dist/lib/mesh/live/daemon/log.d.ts +11 -0
  35. package/dist/lib/mesh/live/daemon/log.js +79 -0
  36. package/dist/lib/mesh/live/daemon/paths.d.ts +18 -0
  37. package/dist/lib/mesh/live/daemon/paths.js +31 -0
  38. package/dist/lib/mesh/live/daemon/pid-lock.d.ts +48 -0
  39. package/dist/lib/mesh/live/daemon/pid-lock.js +134 -0
  40. package/dist/lib/mesh/live/daemon/presence.d.ts +101 -0
  41. package/dist/lib/mesh/live/daemon/presence.js +250 -0
  42. package/dist/lib/mesh/live/daemon/presign.d.ts +18 -0
  43. package/dist/lib/mesh/live/daemon/presign.js +65 -0
  44. package/dist/lib/mesh/live/daemon/run.d.ts +74 -0
  45. package/dist/lib/mesh/live/daemon/run.js +400 -0
  46. package/dist/lib/mesh/live/daemon/state.d.ts +24 -0
  47. package/dist/lib/mesh/live/daemon/state.js +57 -0
  48. package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +170 -0
  49. package/dist/lib/mesh/live/daemon/transcript-watch.js +520 -0
  50. package/dist/lib/mesh/live/enqueue.d.ts +48 -0
  51. package/dist/lib/mesh/live/enqueue.js +100 -0
  52. package/dist/lib/mesh/live/flush.d.ts +56 -0
  53. package/dist/lib/mesh/live/flush.js +360 -0
  54. package/dist/lib/mesh/live/format-spool-line.d.ts +42 -0
  55. package/dist/lib/mesh/live/format-spool-line.js +106 -0
  56. package/dist/lib/mesh/live/index.d.ts +22 -0
  57. package/dist/lib/mesh/live/index.js +16 -0
  58. package/dist/lib/mesh/live/paths.d.ts +13 -0
  59. package/dist/lib/mesh/live/paths.js +30 -0
  60. package/dist/lib/mesh/live/session-events-client.d.ts +58 -0
  61. package/dist/lib/mesh/live/session-events-client.js +120 -0
  62. package/dist/lib/mesh/live/session-identity.d.ts +39 -0
  63. package/dist/lib/mesh/live/session-identity.js +91 -0
  64. package/dist/lib/mesh/live/spool.d.ts +52 -0
  65. package/dist/lib/mesh/live/spool.js +193 -0
  66. package/dist/lib/mesh/live/ulid.d.ts +12 -0
  67. package/dist/lib/mesh/live/ulid.js +39 -0
  68. package/dist/lib/mesh/live/validate-session-event.d.ts +39 -0
  69. package/dist/lib/mesh/live/validate-session-event.js +128 -0
  70. package/dist/lib/search-index/index.d.ts +45 -0
  71. package/dist/lib/search-index/index.js +76 -0
  72. package/dist/lib/work-context/atomic.d.ts +18 -0
  73. package/dist/lib/work-context/atomic.js +132 -0
  74. package/dist/lib/work-context/company.d.ts +103 -0
  75. package/dist/lib/work-context/company.js +274 -0
  76. package/dist/lib/work-context/config.d.ts +59 -0
  77. package/dist/lib/work-context/config.js +172 -0
  78. package/dist/lib/work-context/contract.d.ts +81 -0
  79. package/dist/lib/work-context/contract.js +65 -0
  80. package/dist/lib/work-context/errors.d.ts +27 -0
  81. package/dist/lib/work-context/errors.js +46 -0
  82. package/dist/lib/work-context/index.d.ts +23 -0
  83. package/dist/lib/work-context/index.js +20 -0
  84. package/dist/lib/work-context/migrate.d.ts +84 -0
  85. package/dist/lib/work-context/migrate.js +287 -0
  86. package/dist/lib/work-context/organize.d.ts +130 -0
  87. package/dist/lib/work-context/organize.js +471 -0
  88. package/dist/lib/work-context/outbox.d.ts +97 -0
  89. package/dist/lib/work-context/outbox.js +357 -0
  90. package/dist/lib/work-context/paths.d.ts +14 -0
  91. package/dist/lib/work-context/paths.js +39 -0
  92. package/dist/lib/work-context/project.d.ts +78 -0
  93. package/dist/lib/work-context/project.js +247 -0
  94. package/dist/lib/work-context/reconcile.d.ts +85 -0
  95. package/dist/lib/work-context/reconcile.js +651 -0
  96. package/dist/lib/work-context/repo-remote.d.ts +43 -0
  97. package/dist/lib/work-context/repo-remote.js +200 -0
  98. package/dist/lib/work-context/state.d.ts +112 -0
  99. package/dist/lib/work-context/state.js +162 -0
  100. package/dist/main.js +40 -3
  101. package/dist/utils/cognito-session.d.ts +85 -9
  102. package/dist/utils/cognito-session.js +216 -9
  103. package/dist/utils/qmd-model-download-error.d.ts +27 -0
  104. package/dist/utils/qmd-model-download-error.js +114 -0
  105. package/dist/utils/qmd-query-document-error.d.ts +23 -0
  106. package/dist/utils/qmd-query-document-error.js +117 -0
  107. package/dist/utils/sentry-fingerprint.js +2 -0
  108. package/package.json +5 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,85 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.108.2] — 2026-09-04
6
+
7
+ ### Fixed
8
+
9
+ - Hybrid search (`hq search --mode hybrid`) no longer fails on a multi-line
10
+ query. A whole multi-line message (for example a Slack message forwarded as
11
+ the query) is now collapsed to a single line before it reaches the search
12
+ engine, and a genuinely malformed structured query prints a short "put your
13
+ terms on one line" hint instead of crashing (HQ-CLI-1B).
14
+ - An interrupted local search-model download now reports an actionable "re-run,
15
+ it resumes" message instead of crashing, so two searches racing to download
16
+ the same model on one machine no longer file a crash (HQ-CLI-1B).
17
+ - Machine-credential auth is opt-in via `HQ_MACHINE_CREDS_FILE` / the daemon
18
+ (`{ tokenSource: "machine" }`); the mere presence of
19
+ `~/.hq-agent/machine-creds.json` no longer flips other CLI commands onto
20
+ Cognito machine-token minting.
21
+
22
+ ### Added
23
+
24
+ - Machine-credential auth for the mesh daemon on fleet agent boxes: when
25
+ `HQ_MACHINE_CREDS_FILE` is set (or the daemon passes
26
+ `{ tokenSource: "machine" }`), `ensureCognitoToken` mints/refreshes via Cognito
27
+ `USER_PASSWORD_AUTH`, caches under the daemon state dir (never a person's
28
+ `~/.hq/cognito-tokens.json`), and `HQ_AGENT_IDENTITY_FILE` supplies
29
+ `companyUid` as trusted_explicit company scope; `hq mesh daemon doctor`
30
+ reports token source and actor kind.
31
+ - Work Mesh Live session spool: `hq mesh session
32
+ start|turn-start|turn-end|end|task-status|blocked|note --enqueue` appends
33
+ one event line under `~/.hq/work-mesh/` with no network I/O, so hooks stay
34
+ fast. `hq mesh session flush` claims the spool and held queue by rename,
35
+ holds events that still need company context, drops untracked sessions,
36
+ dead-letters non-retryable failures, and POSTs batches of up to 100 to the
37
+ vault API (default base `https://hqapi.hq.computer`). A matching
38
+ printf-only shell helper ships at
39
+ `assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh` for hq-core hooks.
40
+ - Work context: `hq mesh context reconcile`, `default get|set|clear`,
41
+ `untracked`, `organize`, and `correct`, plus `hq mesh session status --company`.
42
+ Company resolution follows a fixed precedence (trusted explicit → existing
43
+ scope → session meta → device default → deterministic cwd/remote), gates on
44
+ conflict without tenant I/O, asks once for project/task after work signals,
45
+ and trusted or deterministic evidence auto-binds without prompting. Device
46
+ default company set validates current membership and ships dark behind
47
+ `DEFAULT_COMPANY_LOCKED` until `GET /v1/work-mesh/capabilities` reports
48
+ `migration: true` for every company the caller belongs to.
49
+ `hq mesh context correct` is the explicit, receipted cross-company migrate
50
+ path (confirmation gate, `migration_pending` until destination receipt).
51
+ - Resident mesh daemon: `hq mesh daemon run|install|uninstall|status|doctor`.
52
+ Publishes MQTT presence with keepalive 15 s and retained online (offline is
53
+ server-only via IoT lifecycle), drains the spool and outbox, and installs as
54
+ LaunchAgent `ai.getindigo.hq-mesh-daemon` on macOS or a systemd user unit on
55
+ Linux (`--system-user` writes a system-unit fallback for agent boxes).
56
+ `hq doctor` gained a `work-context` check family covering sessions, outbox,
57
+ spool/held/dead-letter, and daemon health.
58
+ - Transcript-watch fallback for hosts without hooks: the daemon enumerates
59
+ Claude/Codex transcript paths every 15 s with readdir+stat only (mtime),
60
+ never opens transcript bytes, and synthesizes
61
+ session_start/turn_end/session_end when hooks are absent.
62
+ - Session-event schema copy (verbatim from hq-pro) with Ajv draft 2020-12
63
+ validation, a fixture corpus, and a cross-repo SHA-256 checksum test. New
64
+ dependencies: `ajv`, and `mqtt` pinned at 5.15.2.
65
+
66
+ ### Changed
67
+
68
+ - hq-core hooks from the matching Work Mesh Live release require hq-cli
69
+ 5.109.0 or newer (spool enqueue format and flush semantics).
70
+ - Mesh daemon presence: offline is now **server-only**. Graceful stop
71
+ flushes once more and issues a clean MQTT `DISCONNECT` (broker lifecycle
72
+ event); the client no longer publishes retained offline. Retained online
73
+ on connect/renew is unchanged.
74
+
75
+ ### Fixed
76
+
77
+ - Session-event flush now accounts 2xx batch responses honestly: parses
78
+ `accepted` / `duplicates` / `rejected[{eventId,code}]`, dead-letters each
79
+ rejected event with its code, and retains any eventId not accounted for
80
+ on the spool instead of deleting the whole chunk as posted.
81
+
82
+ ## [5.108.1] — 2026-09-04
83
+
5
84
  ## [5.108.0] — 2026-09-03
6
85
 
7
86
  ### Fixed
@@ -0,0 +1,23 @@
1
+ # HQ mesh daemon service templates
2
+
3
+ Rendered at install time by `hq mesh daemon install` with the resolved `hq`
4
+ and `node` binaries written into the unit.
5
+
6
+ | Platform | Unit | Label / name |
7
+ |----------|------|----------------|
8
+ | macOS | LaunchAgent | `ai.getindigo.hq-mesh-daemon` |
9
+ | Linux (user) | systemd user unit | `hq-mesh-daemon.service` |
10
+ | Linux (agent box) | systemd system unit | `hq-mesh-daemon.service` as `ec2-user` |
11
+
12
+ ## Agent-box fallback
13
+
14
+ When no user session bus exists (typical EC2 agent boxes):
15
+
16
+ ```bash
17
+ hq mesh daemon install --system-user ec2-user
18
+ sudo cp ~/.hq/work-mesh/daemon/hq-mesh-daemon.service /etc/systemd/system/
19
+ sudo systemctl daemon-reload
20
+ sudo systemctl enable --now hq-mesh-daemon.service
21
+ ```
22
+
23
+ Logs: `~/.hq/work-mesh/daemon/log` (rotated at 5MB by the daemon process).
@@ -0,0 +1,33 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key>
6
+ <string>ai.getindigo.hq-mesh-daemon</string>
7
+ <key>ProgramArguments</key>
8
+ <array>
9
+ <string>{{NODE_BINARY}}</string>
10
+ <string>{{HQ_BINARY}}</string>
11
+ <string>mesh</string>
12
+ <string>daemon</string>
13
+ <string>run</string>
14
+ </array>
15
+ <key>WorkingDirectory</key>
16
+ <string>{{DAEMON_DIR}}</string>
17
+ <key>RunAtLoad</key>
18
+ <true/>
19
+ <key>KeepAlive</key>
20
+ <true/>
21
+ <key>StandardOutPath</key>
22
+ <string>{{LOG_PATH}}</string>
23
+ <key>StandardErrorPath</key>
24
+ <string>{{LOG_PATH}}</string>
25
+ <key>EnvironmentVariables</key>
26
+ <dict>
27
+ <key>HOME</key>
28
+ <string>{{HOME}}</string>
29
+ <key>PATH</key>
30
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
31
+ </dict>
32
+ </dict>
33
+ </plist>
@@ -0,0 +1,27 @@
1
+ [Unit]
2
+ Description=HQ Work Mesh Live daemon (system, user={{SYSTEM_USER}})
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ # Fallback for agent boxes where no user session bus exists:
7
+ # hq mesh daemon install --system-user {{SYSTEM_USER}}
8
+ # sudo cp ~/.hq/work-mesh/daemon/hq-mesh-daemon.service /etc/systemd/system/
9
+ # sudo systemctl daemon-reload && sudo systemctl enable --now hq-mesh-daemon.service
10
+
11
+ [Service]
12
+ Type=simple
13
+ User={{SYSTEM_USER}}
14
+ Group={{SYSTEM_USER}}
15
+ ExecStart={{NODE_BINARY}} {{HQ_BINARY}} mesh daemon run
16
+ WorkingDirectory=/home/{{SYSTEM_USER}}/.hq/work-mesh/daemon
17
+ Restart=on-failure
18
+ RestartSec=5
19
+ Environment=HOME=/home/{{SYSTEM_USER}}
20
+ Environment=PATH=/usr/local/bin:/usr/bin:/bin
21
+ Environment=HQ_MACHINE_CREDS_FILE=/home/{{SYSTEM_USER}}/.hq-agent/machine-creds.json
22
+ Environment=HQ_AGENT_IDENTITY_FILE=/var/lib/hq-agent/identity.json
23
+ StandardOutput=append:/home/{{SYSTEM_USER}}/.hq/work-mesh/daemon/log
24
+ StandardError=append:/home/{{SYSTEM_USER}}/.hq/work-mesh/daemon/log
25
+
26
+ [Install]
27
+ WantedBy=multi-user.target
@@ -0,0 +1,18 @@
1
+ [Unit]
2
+ Description=HQ Work Mesh Live daemon
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ [Service]
7
+ Type=simple
8
+ ExecStart={{NODE_BINARY}} {{HQ_BINARY}} mesh daemon run
9
+ WorkingDirectory={{DAEMON_DIR}}
10
+ Restart=on-failure
11
+ RestartSec=5
12
+ Environment=HOME={{HOME}}
13
+ Environment=PATH=/usr/local/bin:/usr/bin:/bin
14
+ StandardOutput=append:{{LOG_PATH}}
15
+ StandardError=append:{{LOG_PATH}}
16
+
17
+ [Install]
18
+ WantedBy=default.target
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env bash
2
+ # hq-core: public
3
+ # work-mesh-enqueue.sh — append one Work Mesh Live spool line with printf only.
4
+ #
5
+ # Sourced by hooks. Never execute directly.
6
+ # No jq. No node. One write under PIPE_BUF via printf >> spool (O_APPEND).
7
+ #
8
+ # Line format is byte-identical to hq-cli formatSpoolLine() for the same inputs
9
+ # (fixed key order, compact JSON). See src/lib/mesh/live/format-spool-line.ts.
10
+ #
11
+ # Usage:
12
+ # source "$HQ_ROOT/core/scripts/lib/work-mesh-enqueue.sh"
13
+ # work_mesh_enqueue \
14
+ # --kind session_start \
15
+ # --session-id "$sid" \
16
+ # --harness claude-code \
17
+ # --adapter-version 1.0.0 \
18
+ # --seq 1 \
19
+ # [--event-id ULID] [--at ISO8601] [--runtime-version V] \
20
+ # [--task-id T] [--status S] [--reason R] [--summary S] \
21
+ # [--cwd P] [--hq-root P] [--company-slug S] [--project P] [--task T] \
22
+ # [--tool-writes N] [--spool PATH]
23
+
24
+ # Crockford base32 alphabet (ULID) — no I L O U
25
+ _WORK_MESH_CROCKFORD='0123456789ABCDEFGHJKMNPQRSTVWXYZ'
26
+
27
+ # work_mesh_json_quote <string>
28
+ # Set REPLY to a JSON string value (with surrounding quotes). No subshell.
29
+ # Fast path for values without \ " or controls (typical session metadata).
30
+ work_mesh_json_quote() {
31
+ local s=$1
32
+ case "$s" in
33
+ *'\\'*|*'\"'*|*$'\n'*|*$'\r'*|*$'\t'*)
34
+ s=${s//'\'/'\\'}
35
+ s=${s//'"'/'\"'}
36
+ s=${s//$'\n'/'\n'}
37
+ s=${s//$'\r'/'\r'}
38
+ s=${s//$'\t'/'\t'}
39
+ REPLY="\"${s}\""
40
+ ;;
41
+ *)
42
+ REPLY="\"${s}\""
43
+ ;;
44
+ esac
45
+ }
46
+
47
+ # work_mesh_json_escape <string>
48
+ # Print a JSON string value (compat wrapper for tests / callers).
49
+ work_mesh_json_escape() {
50
+ work_mesh_json_quote "$1"
51
+ printf '%s' "$REPLY"
52
+ }
53
+
54
+ # Cached: bash 5.1+ has $SRANDOM (re-read yields new entropy).
55
+ _WORK_MESH_HAS_SRANDOM=0
56
+ if [[ ${BASH_VERSINFO[0]} -gt 5 || ( ${BASH_VERSINFO[0]} -eq 5 && ${BASH_VERSINFO[1]} -ge 1 ) ]]; then
57
+ _WORK_MESH_HAS_SRANDOM=1
58
+ fi
59
+
60
+ # work_mesh_ulid
61
+ # Print a 26-char Crockford ULID (48-bit ms time + 80-bit entropy).
62
+ # Prefer bash 5+ builtins ($EPOCHREALTIME, $SRANDOM) to stay under the
63
+ # 20ms hook budget; fall back to date/dd/od on older bash.
64
+ # Also sets REPLY (avoids command-substitution fork when callers use it).
65
+ work_mesh_ulid() {
66
+ local alphabet=$_WORK_MESH_CROCKFORD
67
+ local now_ms time_part="" rand_part=""
68
+ local -i i rem val
69
+
70
+ if [[ -n "${EPOCHREALTIME-}" ]]; then
71
+ local sec=${EPOCHREALTIME%.*}
72
+ local frac=${EPOCHREALTIME#*.}000
73
+ frac=${frac:0:3}
74
+ now_ms=$((10#$sec * 1000 + 10#$frac))
75
+ else
76
+ now_ms=$(date +%s%3N 2>/dev/null || true)
77
+ case "$now_ms" in
78
+ ""|*[!0-9]*)
79
+ now_ms=$(date +%s)
80
+ now_ms=$((now_ms * 1000))
81
+ ;;
82
+ esac
83
+ fi
84
+
85
+ # 48-bit timestamp → 10 Crockford chars (fits in signed 64-bit).
86
+ val=$((now_ms & 0xFFFFFFFFFFFF))
87
+ for ((i = 0; i < 10; i++)); do
88
+ rem=$((val & 31))
89
+ time_part="${alphabet:rem:1}${time_part}"
90
+ val=$((val >> 5))
91
+ done
92
+
93
+ local -a b=()
94
+ if [ "$_WORK_MESH_HAS_SRANDOM" -eq 1 ]; then
95
+ for ((i = 0; i < 10; i++)); do
96
+ b+=($((SRANDOM & 255)))
97
+ done
98
+ else
99
+ local bytes
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
+ # shellcheck disable=SC2206
102
+ local -a bb=($bytes)
103
+ b=("${bb[@]}")
104
+ while [ "${#b[@]}" -lt 10 ]; do
105
+ b+=($((RANDOM % 256)))
106
+ done
107
+ fi
108
+
109
+ # Encode 80 bits MSB-first as 16×5-bit Crockford chars without a 80-bit int:
110
+ # maintain a bit buffer that never exceeds ~16 bits of pending state.
111
+ local -i bitbuf=0 bits=0 bi=0 byte
112
+ rand_part=
113
+ for ((i = 0; i < 16; i++)); do
114
+ while [ "$bits" -lt 5 ]; do
115
+ if [ "$bi" -lt 10 ]; then
116
+ byte=${b[bi]}
117
+ bi=$((bi + 1))
118
+ bitbuf=$(((bitbuf << 8) | (byte & 255)))
119
+ bits=$((bits + 8))
120
+ else
121
+ bitbuf=$((bitbuf << 5))
122
+ bits=$((bits + 5))
123
+ fi
124
+ done
125
+ rem=$(((bitbuf >> (bits - 5)) & 31))
126
+ bits=$((bits - 5))
127
+ rand_part+="${alphabet:rem:1}"
128
+ done
129
+
130
+ REPLY="${time_part}${rand_part}"
131
+ printf '%s' "$REPLY"
132
+ }
133
+
134
+ # work_mesh_ensure_spool <path>
135
+ # Create parent dir 0700 and spool file 0600 when missing.
136
+ # Hot path: existing spool returns immediately (no chmod/stat storms).
137
+ work_mesh_ensure_spool() {
138
+ local spool=$1 dir
139
+ if [ -f "$spool" ]; then
140
+ return 0
141
+ fi
142
+ dir=$(dirname -- "$spool")
143
+ mkdir -p -- "$dir" 2>/dev/null || true
144
+ chmod 700 -- "$dir" 2>/dev/null || true
145
+ (umask 077; printf '' >"$spool") || return 1
146
+ chmod 600 -- "$spool" 2>/dev/null || true
147
+ }
148
+
149
+ # work_mesh_enqueue [options]
150
+ # Build one compact JSON line and append with a single printf write.
151
+ work_mesh_enqueue() {
152
+ local kind="" session_id="" harness="" adapter_version="" runtime_version=""
153
+ local at="" seq="" event_id="" task_id="" status="" reason="" summary=""
154
+ local cwd="" hq_root="" company_slug="" project="" task="" tool_writes=""
155
+ local spool=${WORK_MESH_SPOOL:-"${HOME}/.hq/work-mesh/spool.jsonl"}
156
+
157
+ while [ $# -gt 0 ]; do
158
+ case "$1" in
159
+ --kind) kind=$2; shift 2 ;;
160
+ --session-id) session_id=$2; shift 2 ;;
161
+ --harness) harness=$2; shift 2 ;;
162
+ --adapter-version) adapter_version=$2; shift 2 ;;
163
+ --runtime-version) runtime_version=$2; shift 2 ;;
164
+ --at) at=$2; shift 2 ;;
165
+ --seq) seq=$2; shift 2 ;;
166
+ --event-id) event_id=$2; shift 2 ;;
167
+ --task-id) task_id=$2; shift 2 ;;
168
+ --status) status=$2; shift 2 ;;
169
+ --reason) reason=$2; shift 2 ;;
170
+ --summary) summary=$2; shift 2 ;;
171
+ --cwd) cwd=$2; shift 2 ;;
172
+ --hq-root) hq_root=$2; shift 2 ;;
173
+ --company-slug) company_slug=$2; shift 2 ;;
174
+ --project) project=$2; shift 2 ;;
175
+ --task) task=$2; shift 2 ;;
176
+ --tool-writes) tool_writes=$2; shift 2 ;;
177
+ --spool) spool=$2; shift 2 ;;
178
+ *) printf 'work_mesh_enqueue: unknown arg %s\n' "$1" >&2; return 2 ;;
179
+ esac
180
+ done
181
+
182
+ if [ -z "$session_id" ]; then
183
+ printf 'work_mesh_enqueue: sessionId required\n' >&2
184
+ return 1
185
+ fi
186
+ if [ -z "$kind" ] || [ -z "$harness" ] || [ -z "$adapter_version" ] || [ -z "$seq" ]; then
187
+ printf 'work_mesh_enqueue: kind, harness, adapter-version, seq required\n' >&2
188
+ return 1
189
+ fi
190
+
191
+ if [ -z "$event_id" ]; then
192
+ work_mesh_ulid >/dev/null
193
+ event_id=$REPLY
194
+ fi
195
+ if [ -z "$at" ]; then
196
+ # Builtin UTC clock (bash 4.2+ printf %( )T) — no date(1) fork.
197
+ if TZ=UTC printf -v at '%(%Y-%m-%dT%H:%M:%S)T.000Z' -1 2>/dev/null && [ -n "$at" ]; then
198
+ :
199
+ else
200
+ at=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z" 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ")
201
+ fi
202
+ fi
203
+
204
+ # Build compact JSON without command-substitution forks (keeps hook p95 low).
205
+ local line q
206
+ work_mesh_json_quote "$event_id"; q=$REPLY
207
+ line='{"v":1,"eventId":'"$q"
208
+ work_mesh_json_quote "$kind"; q=$REPLY
209
+ line+=',"kind":'"$q"
210
+ work_mesh_json_quote "$session_id"; q=$REPLY
211
+ line+=',"sessionId":'"$q"
212
+ work_mesh_json_quote "$harness"; q=$REPLY
213
+ line+=',"harness":'"$q"
214
+ work_mesh_json_quote "$adapter_version"; q=$REPLY
215
+ line+=',"adapterVersion":'"$q"
216
+ if [ -n "$runtime_version" ]; then
217
+ work_mesh_json_quote "$runtime_version"; q=$REPLY
218
+ line+=',"runtimeVersion":'"$q"
219
+ fi
220
+ work_mesh_json_quote "$at"; q=$REPLY
221
+ line+=',"at":'"$q"',"seq":'"$seq"
222
+ if [ -n "$task_id" ]; then
223
+ work_mesh_json_quote "$task_id"; q=$REPLY
224
+ line+=',"taskId":'"$q"
225
+ fi
226
+ if [ -n "$status" ]; then
227
+ work_mesh_json_quote "$status"; q=$REPLY
228
+ line+=',"status":'"$q"
229
+ fi
230
+ if [ -n "$reason" ]; then
231
+ work_mesh_json_quote "$reason"; q=$REPLY
232
+ line+=',"reason":'"$q"
233
+ fi
234
+ if [ -n "$summary" ]; then
235
+ work_mesh_json_quote "$summary"; q=$REPLY
236
+ line+=',"summary":'"$q"
237
+ fi
238
+ if [ -n "$cwd" ]; then
239
+ work_mesh_json_quote "$cwd"; q=$REPLY
240
+ line+=',"cwd":'"$q"
241
+ fi
242
+ if [ -n "$hq_root" ]; then
243
+ work_mesh_json_quote "$hq_root"; q=$REPLY
244
+ line+=',"hqRoot":'"$q"
245
+ fi
246
+ if [ -n "$company_slug" ]; then
247
+ work_mesh_json_quote "$company_slug"; q=$REPLY
248
+ line+=',"companySlug":'"$q"
249
+ fi
250
+ if [ -n "$project" ]; then
251
+ work_mesh_json_quote "$project"; q=$REPLY
252
+ line+=',"project":'"$q"
253
+ fi
254
+ if [ -n "$task" ]; then
255
+ work_mesh_json_quote "$task"; q=$REPLY
256
+ line+=',"task":'"$q"
257
+ fi
258
+ if [ -n "$tool_writes" ]; then
259
+ line+=',"toolWrites":'"$tool_writes"
260
+ fi
261
+ line+='}'
262
+
263
+ work_mesh_ensure_spool "$spool" || return 1
264
+ # Single write under O_APPEND (printf → one write syscall for the line).
265
+ printf '%s\n' "$line" >>"$spool" || return 1
266
+ return 0
267
+ }
@@ -30,6 +30,12 @@ export declare class SyncExitError extends Error {
30
30
  readonly code: number;
31
31
  constructor(code: number);
32
32
  }
33
+ /**
34
+ * Preserve the vault's typed write-scope refusal at the CLI boundary. This is
35
+ * deliberately duck-typed so hq-cli remains compatible while hq-cloud and the
36
+ * CLI are released independently.
37
+ */
38
+ export declare function formatSyncFailure(err: unknown): string;
33
39
  /**
34
40
  * US-003: run one sync fanout under a client-health attempt/outcome report.
35
41
  * The report is best-effort, bounded, and swallowed — it can only append a
@@ -38,6 +38,29 @@ export class SyncExitError extends Error {
38
38
  this.name = "SyncExitError";
39
39
  }
40
40
  }
41
+ /**
42
+ * Preserve the vault's typed write-scope refusal at the CLI boundary. This is
43
+ * deliberately duck-typed so hq-cli remains compatible while hq-cloud and the
44
+ * CLI are released independently.
45
+ */
46
+ export function formatSyncFailure(err) {
47
+ if (typeof err === "object" && err !== null) {
48
+ const diagnostic = err;
49
+ if (diagnostic.code === "POLICY_WRITE_SCOPE_TRUNCATED") {
50
+ const dropped = Array.isArray(diagnostic.droppedWriteGrantPrefixes)
51
+ ? diagnostic.droppedWriteGrantPrefixes.filter((prefix) => typeof prefix === "string")
52
+ : [];
53
+ const retained = Array.isArray(diagnostic.retainedGrantPrefixes)
54
+ ? diagnostic.retainedGrantPrefixes.filter((prefix) => typeof prefix === "string")
55
+ : [];
56
+ return ("Vault credentials were refused because the requested write scope could not be represented exactly. " +
57
+ `Dropped write prefixes: ${dropped.length > 0 ? dropped.join(", ") : "unknown"}. ` +
58
+ `Retained prefixes: ${retained.length > 0 ? retained.join(", ") : "none"}. ` +
59
+ "No files were uploaded. Narrow or consolidate the grants, then retry.");
60
+ }
61
+ }
62
+ return err instanceof Error ? err.message : String(err);
63
+ }
41
64
  /**
42
65
  * US-003: run one sync fanout under a client-health attempt/outcome report.
43
66
  * The report is best-effort, bounded, and swallowed — it can only append a
@@ -324,7 +347,7 @@ export async function pullAll(options, deps) {
324
347
  }
325
348
  }
326
349
  catch (err) {
327
- const message = err instanceof Error ? err.message : String(err);
350
+ const message = formatSyncFailure(err);
328
351
  result.errors.push({ company: entry.slug, message });
329
352
  result.perCompany.push({ slug: entry.slug, error: message });
330
353
  }
@@ -416,7 +439,7 @@ export async function pushAll(options, deps) {
416
439
  result.perCompany.push({ slug: entry.slug, result: r });
417
440
  }
418
441
  catch (err) {
419
- const message = err instanceof Error ? err.message : String(err);
442
+ const message = formatSyncFailure(err);
420
443
  result.errors.push({ company: entry.slug, message });
421
444
  result.perCompany.push({ slug: entry.slug, error: message });
422
445
  }
@@ -810,7 +833,7 @@ export function registerCloudCommands(program) {
810
833
  catch (err) {
811
834
  if (syncHealth)
812
835
  await syncHealth.failed();
813
- const message = err instanceof Error ? err.message : String(err);
836
+ const message = formatSyncFailure(err);
814
837
  if (jsonMode) {
815
838
  // In JSON mode, the parent process is parsing stderr for ndjson —
816
839
  // human-formatted error lines would corrupt the stream. Emit a
@@ -191,13 +191,14 @@ export const SCAFFOLD_COMMANDS = [
191
191
  * direct `hq core` command.
192
192
  */
193
193
  export const SCAFFOLD_ONLY_ASSETS = [
194
- // Shared shell libs sourced by the two relocated scripts. Never dispatched on
195
- // their own (so no `hq core` command), but they MUST ship in the tarball, or
196
- // the bundled hq-session / checkpoint-stop-gate die at `source` time on a
197
- // user's box. `root` is inert here — they are sourced, not run.
194
+ // Shared shell libs sourced by the two relocated scripts (and hooks). Never
195
+ // dispatched on their own (so no `hq core` command), but they MUST ship in the
196
+ // tarball, or the bundled hq-session / checkpoint-stop-gate die at `source`
197
+ // time on a user's box. `root` is inert here — they are sourced, not run.
198
198
  { asset: "core/scripts/hook-lib.sh", root: "live", summary: "Shared shell primitives sourced by the checkpoint gate" },
199
199
  { asset: "core/scripts/lib/session-scope-capability.sh", root: "live", summary: "Session scope-capability lib sourced by hq-session" },
200
200
  { asset: "core/scripts/lib/session-id.sh", root: "live", summary: "Session-id resolver lib sourced by hq-session" },
201
+ { asset: "core/scripts/lib/work-mesh-enqueue.sh", root: "live", summary: "Work Mesh Live spool enqueue lib sourced by hooks" },
201
202
  { asset: "core/scripts/archive-old-threads.sh", root: "live", summary: "Scaffold-only native utility companion" },
202
203
  { asset: "core/scripts/codex-skill-bridge.sh", root: "live", summary: "Scaffold-only native utility companion" },
203
204
  { asset: "core/scripts/qmd-reindex-after-sync.sh", root: "cwd", summary: "Scaffold-only native utility companion" },
@@ -8,5 +8,20 @@
8
8
  import { Command } from "commander";
9
9
  import { type MeshCompany, type MeshThread } from "../lib/mesh/api.js";
10
10
  export declare function formatCheckLines(threads: MeshThread[], company: MeshCompany, projectId?: string): string[];
11
+ /** Injectable confirmation seam for `hq mesh context correct` (stubbed in tests). */
12
+ export type MeshConfirmFn = (message: string) => Promise<boolean>;
13
+ /** Exported for confirmation-gate unit tests (US-017B). */
14
+ export declare function runContextCorrect(opts: {
15
+ session?: string;
16
+ toCompany?: string;
17
+ project?: string;
18
+ task?: string;
19
+ yes?: boolean;
20
+ json?: boolean;
21
+ machine?: boolean;
22
+ expectedVersion?: string;
23
+ }, deps?: {
24
+ confirm?: MeshConfirmFn;
25
+ }): Promise<void>;
11
26
  export declare function registerMeshCommand(program: Command): void;
12
27
  //# sourceMappingURL=mesh.d.ts.map