@indigoai-us/hq-cli 5.108.1 → 5.108.3

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 (109) 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/core.js +5 -4
  8. package/dist/commands/index-cmd.js +13 -0
  9. package/dist/commands/mesh.d.ts +15 -0
  10. package/dist/commands/mesh.js +732 -4
  11. package/dist/commands/search.d.ts +23 -0
  12. package/dist/commands/search.js +42 -1
  13. package/dist/lib/doctor/checks/work-context.d.ts +23 -0
  14. package/dist/lib/doctor/checks/work-context.js +195 -0
  15. package/dist/lib/doctor/registry.js +4 -0
  16. package/dist/lib/mesh/api.d.ts +18 -0
  17. package/dist/lib/mesh/api.js +55 -0
  18. package/dist/lib/mesh/cache.js +35 -4
  19. package/dist/lib/mesh/client.d.ts +118 -0
  20. package/dist/lib/mesh/client.js +268 -0
  21. package/dist/lib/mesh/live/backoff.d.ts +16 -0
  22. package/dist/lib/mesh/live/backoff.js +20 -0
  23. package/dist/lib/mesh/live/daemon/board-refresh.d.ts +49 -0
  24. package/dist/lib/mesh/live/daemon/board-refresh.js +185 -0
  25. package/dist/lib/mesh/live/daemon/credentials.d.ts +71 -0
  26. package/dist/lib/mesh/live/daemon/credentials.js +227 -0
  27. package/dist/lib/mesh/live/daemon/doctor.d.ts +37 -0
  28. package/dist/lib/mesh/live/daemon/doctor.js +139 -0
  29. package/dist/lib/mesh/live/daemon/index.d.ts +24 -0
  30. package/dist/lib/mesh/live/daemon/index.js +14 -0
  31. package/dist/lib/mesh/live/daemon/install.d.ts +83 -0
  32. package/dist/lib/mesh/live/daemon/install.js +330 -0
  33. package/dist/lib/mesh/live/daemon/log.d.ts +11 -0
  34. package/dist/lib/mesh/live/daemon/log.js +79 -0
  35. package/dist/lib/mesh/live/daemon/paths.d.ts +18 -0
  36. package/dist/lib/mesh/live/daemon/paths.js +31 -0
  37. package/dist/lib/mesh/live/daemon/pid-lock.d.ts +48 -0
  38. package/dist/lib/mesh/live/daemon/pid-lock.js +134 -0
  39. package/dist/lib/mesh/live/daemon/presence.d.ts +101 -0
  40. package/dist/lib/mesh/live/daemon/presence.js +250 -0
  41. package/dist/lib/mesh/live/daemon/presign.d.ts +18 -0
  42. package/dist/lib/mesh/live/daemon/presign.js +65 -0
  43. package/dist/lib/mesh/live/daemon/run.d.ts +74 -0
  44. package/dist/lib/mesh/live/daemon/run.js +400 -0
  45. package/dist/lib/mesh/live/daemon/state.d.ts +24 -0
  46. package/dist/lib/mesh/live/daemon/state.js +57 -0
  47. package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +170 -0
  48. package/dist/lib/mesh/live/daemon/transcript-watch.js +520 -0
  49. package/dist/lib/mesh/live/enqueue.d.ts +48 -0
  50. package/dist/lib/mesh/live/enqueue.js +100 -0
  51. package/dist/lib/mesh/live/flush.d.ts +56 -0
  52. package/dist/lib/mesh/live/flush.js +360 -0
  53. package/dist/lib/mesh/live/format-spool-line.d.ts +42 -0
  54. package/dist/lib/mesh/live/format-spool-line.js +106 -0
  55. package/dist/lib/mesh/live/index.d.ts +22 -0
  56. package/dist/lib/mesh/live/index.js +16 -0
  57. package/dist/lib/mesh/live/paths.d.ts +13 -0
  58. package/dist/lib/mesh/live/paths.js +30 -0
  59. package/dist/lib/mesh/live/session-events-client.d.ts +58 -0
  60. package/dist/lib/mesh/live/session-events-client.js +120 -0
  61. package/dist/lib/mesh/live/session-identity.d.ts +39 -0
  62. package/dist/lib/mesh/live/session-identity.js +91 -0
  63. package/dist/lib/mesh/live/spool.d.ts +52 -0
  64. package/dist/lib/mesh/live/spool.js +193 -0
  65. package/dist/lib/mesh/live/ulid.d.ts +12 -0
  66. package/dist/lib/mesh/live/ulid.js +39 -0
  67. package/dist/lib/mesh/live/validate-session-event.d.ts +39 -0
  68. package/dist/lib/mesh/live/validate-session-event.js +128 -0
  69. package/dist/lib/search-index/index.d.ts +98 -0
  70. package/dist/lib/search-index/index.js +174 -2
  71. package/dist/lib/work-context/atomic.d.ts +18 -0
  72. package/dist/lib/work-context/atomic.js +132 -0
  73. package/dist/lib/work-context/company.d.ts +103 -0
  74. package/dist/lib/work-context/company.js +274 -0
  75. package/dist/lib/work-context/config.d.ts +59 -0
  76. package/dist/lib/work-context/config.js +172 -0
  77. package/dist/lib/work-context/contract.d.ts +81 -0
  78. package/dist/lib/work-context/contract.js +65 -0
  79. package/dist/lib/work-context/errors.d.ts +27 -0
  80. package/dist/lib/work-context/errors.js +46 -0
  81. package/dist/lib/work-context/index.d.ts +23 -0
  82. package/dist/lib/work-context/index.js +20 -0
  83. package/dist/lib/work-context/migrate.d.ts +84 -0
  84. package/dist/lib/work-context/migrate.js +287 -0
  85. package/dist/lib/work-context/organize.d.ts +130 -0
  86. package/dist/lib/work-context/organize.js +471 -0
  87. package/dist/lib/work-context/outbox.d.ts +97 -0
  88. package/dist/lib/work-context/outbox.js +357 -0
  89. package/dist/lib/work-context/paths.d.ts +14 -0
  90. package/dist/lib/work-context/paths.js +39 -0
  91. package/dist/lib/work-context/project.d.ts +78 -0
  92. package/dist/lib/work-context/project.js +247 -0
  93. package/dist/lib/work-context/reconcile.d.ts +85 -0
  94. package/dist/lib/work-context/reconcile.js +651 -0
  95. package/dist/lib/work-context/repo-remote.d.ts +43 -0
  96. package/dist/lib/work-context/repo-remote.js +200 -0
  97. package/dist/lib/work-context/state.d.ts +112 -0
  98. package/dist/lib/work-context/state.js +162 -0
  99. package/dist/main.js +93 -4
  100. package/dist/utils/cognito-session.d.ts +85 -9
  101. package/dist/utils/cognito-session.js +216 -9
  102. package/dist/utils/qmd-model-download-error.d.ts +27 -0
  103. package/dist/utils/qmd-model-download-error.js +114 -0
  104. package/dist/utils/qmd-query-document-error.d.ts +23 -0
  105. package/dist/utils/qmd-query-document-error.js +117 -0
  106. package/dist/utils/qmd-workdir-missing-error.d.ts +18 -0
  107. package/dist/utils/qmd-workdir-missing-error.js +78 -0
  108. package/dist/utils/sentry-fingerprint.js +7 -0
  109. package/package.json +5 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,85 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.108.3] — 2026-09-04
6
+
7
+ ## [5.108.2] — 2026-09-04
8
+
9
+ ### Fixed
10
+
11
+ - Hybrid search (`hq search --mode hybrid`) no longer fails on a multi-line
12
+ query. A whole multi-line message (for example a Slack message forwarded as
13
+ the query) is now collapsed to a single line before it reaches the search
14
+ engine, and a genuinely malformed structured query prints a short "put your
15
+ terms on one line" hint instead of crashing (HQ-CLI-1B).
16
+ - An interrupted local search-model download now reports an actionable "re-run,
17
+ it resumes" message instead of crashing, so two searches racing to download
18
+ the same model on one machine no longer file a crash (HQ-CLI-1B).
19
+ - Machine-credential auth is opt-in via `HQ_MACHINE_CREDS_FILE` / the daemon
20
+ (`{ tokenSource: "machine" }`); the mere presence of
21
+ `~/.hq-agent/machine-creds.json` no longer flips other CLI commands onto
22
+ Cognito machine-token minting.
23
+
24
+ ### Added
25
+
26
+ - Machine-credential auth for the mesh daemon on fleet agent boxes: when
27
+ `HQ_MACHINE_CREDS_FILE` is set (or the daemon passes
28
+ `{ tokenSource: "machine" }`), `ensureCognitoToken` mints/refreshes via Cognito
29
+ `USER_PASSWORD_AUTH`, caches under the daemon state dir (never a person's
30
+ `~/.hq/cognito-tokens.json`), and `HQ_AGENT_IDENTITY_FILE` supplies
31
+ `companyUid` as trusted_explicit company scope; `hq mesh daemon doctor`
32
+ reports token source and actor kind.
33
+ - Work Mesh Live session spool: `hq mesh session
34
+ start|turn-start|turn-end|end|task-status|blocked|note --enqueue` appends
35
+ one event line under `~/.hq/work-mesh/` with no network I/O, so hooks stay
36
+ fast. `hq mesh session flush` claims the spool and held queue by rename,
37
+ holds events that still need company context, drops untracked sessions,
38
+ dead-letters non-retryable failures, and POSTs batches of up to 100 to the
39
+ vault API (default base `https://hqapi.hq.computer`). A matching
40
+ printf-only shell helper ships at
41
+ `assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh` for hq-core hooks.
42
+ - Work context: `hq mesh context reconcile`, `default get|set|clear`,
43
+ `untracked`, `organize`, and `correct`, plus `hq mesh session status --company`.
44
+ Company resolution follows a fixed precedence (trusted explicit → existing
45
+ scope → session meta → device default → deterministic cwd/remote), gates on
46
+ conflict without tenant I/O, asks once for project/task after work signals,
47
+ and trusted or deterministic evidence auto-binds without prompting. Device
48
+ default company set validates current membership and ships dark behind
49
+ `DEFAULT_COMPANY_LOCKED` until `GET /v1/work-mesh/capabilities` reports
50
+ `migration: true` for every company the caller belongs to.
51
+ `hq mesh context correct` is the explicit, receipted cross-company migrate
52
+ path (confirmation gate, `migration_pending` until destination receipt).
53
+ - Resident mesh daemon: `hq mesh daemon run|install|uninstall|status|doctor`.
54
+ Publishes MQTT presence with keepalive 15 s and retained online (offline is
55
+ server-only via IoT lifecycle), drains the spool and outbox, and installs as
56
+ LaunchAgent `ai.getindigo.hq-mesh-daemon` on macOS or a systemd user unit on
57
+ Linux (`--system-user` writes a system-unit fallback for agent boxes).
58
+ `hq doctor` gained a `work-context` check family covering sessions, outbox,
59
+ spool/held/dead-letter, and daemon health.
60
+ - Transcript-watch fallback for hosts without hooks: the daemon enumerates
61
+ Claude/Codex transcript paths every 15 s with readdir+stat only (mtime),
62
+ never opens transcript bytes, and synthesizes
63
+ session_start/turn_end/session_end when hooks are absent.
64
+ - Session-event schema copy (verbatim from hq-pro) with Ajv draft 2020-12
65
+ validation, a fixture corpus, and a cross-repo SHA-256 checksum test. New
66
+ dependencies: `ajv`, and `mqtt` pinned at 5.15.2.
67
+
68
+ ### Changed
69
+
70
+ - hq-core hooks from the matching Work Mesh Live release require hq-cli
71
+ 5.109.0 or newer (spool enqueue format and flush semantics).
72
+ - Mesh daemon presence: offline is now **server-only**. Graceful stop
73
+ flushes once more and issues a clean MQTT `DISCONNECT` (broker lifecycle
74
+ event); the client no longer publishes retained offline. Retained online
75
+ on connect/renew is unchanged.
76
+
77
+ ### Fixed
78
+
79
+ - Session-event flush now accounts 2xx batch responses honestly: parses
80
+ `accepted` / `duplicates` / `rejected[{eventId,code}]`, dead-letters each
81
+ rejected event with its code, and retains any eventId not accounted for
82
+ on the spool instead of deleting the whole chunk as posted.
83
+
5
84
  ## [5.108.1] — 2026-09-04
6
85
 
7
86
  ## [5.108.0] — 2026-09-03
@@ -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
+ }
@@ -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" },
@@ -5,6 +5,7 @@ import { findHqRoot } from '../utils/manifest.js';
5
5
  import { QMD_NATIVE_BINDING_REMEDY, isQmdNativeBindingError, } from '../utils/qmd-native-binding-error.js';
6
6
  import { isQmdStoreMissingError, qmdStoreMissingMessage, } from '../utils/qmd-store-missing-error.js';
7
7
  import { isQmdStoreUnopenableError, qmdStoreUnopenableMessage, } from '../utils/qmd-store-unopenable-error.js';
8
+ import { isQmdWorkdirMissingError, qmdWorkdirMissingMessage, } from '../utils/qmd-workdir-missing-error.js';
8
9
  const defaults = {
9
10
  reconcileCollections,
10
11
  deriveCollections,
@@ -147,6 +148,18 @@ export function registerIndexCommand(program, dependencies = defaults) {
147
148
  process.stderr.write(`qmd: unusable — ${remedy}\n`);
148
149
  process.exitCode = 1;
149
150
  }
151
+ else if (isQmdWorkdirMissingError(error)) {
152
+ // The working directory hq handed qmd (the resolved hq root) does not
153
+ // exist, so the spawn failed with ENOENT (HQ-CLI-1A). Like the
154
+ // store-missing/unopenable cases, this diagnostic command should
155
+ // DESCRIBE the broken working directory, not crash on it or send the
156
+ // user to reinstall qmd: print the classified reason + remedy naming
157
+ // the directory and exit 1 without rethrowing. Every OTHER qmd failure
158
+ // keeps propagating.
159
+ const remedy = qmdWorkdirMissingMessage(error) ?? 'its working directory does not exist';
160
+ process.stderr.write(`qmd: unusable — ${remedy}\n`);
161
+ process.exitCode = 1;
162
+ }
150
163
  else {
151
164
  throw error;
152
165
  }
@@ -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