@indigoai-us/hq-cli 5.312.2-beta.0 → 5.313.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -2
- package/assets/scaffold/core/scripts/lib/work-mesh-live-hook.sh +378 -0
- package/assets/scaffold/core/scripts/lib/work-mesh-live-rebind.sh +141 -0
- package/assets/scaffold/core/scripts/migrate-policy-triggers.sh +373 -0
- package/assets/scaffold/core/scripts/register-project.sh +262 -0
- package/assets/scaffold/core/scripts/work-mesh-live-rebind.sh +59 -0
- package/dist/command-catalog.generated.d.ts +80 -0
- package/dist/command-catalog.generated.js +99 -0
- package/dist/commands/cloud.js +0 -1
- package/dist/commands/company-rename-flag.d.ts +4 -0
- package/dist/commands/company-rename-flag.js +7 -0
- package/dist/commands/company-rename.d.ts +4 -0
- package/dist/commands/company-rename.js +90 -0
- package/dist/commands/company.js +2 -0
- package/dist/commands/core-eval-trigger-native.d.ts +4 -0
- package/dist/commands/core-eval-trigger-native.js +12 -0
- package/dist/commands/core-knowledge-prefs.d.ts +0 -1
- package/dist/commands/core-knowledge-prefs.js +1 -1
- package/dist/commands/core-migrate-policy-triggers-native.d.ts +21 -0
- package/dist/commands/core-migrate-policy-triggers-native.js +378 -0
- package/dist/commands/core-native-hook-helper-commands.d.ts +4 -0
- package/dist/commands/core-native-hook-helper-commands.js +56 -0
- package/dist/commands/core-native-hook-helpers-flag.d.ts +4 -0
- package/dist/commands/core-native-hook-helpers-flag.js +7 -0
- package/dist/commands/core-register-project-native.d.ts +30 -0
- package/dist/commands/core-register-project-native.js +347 -0
- package/dist/commands/core-work-mesh-live-rebind-native.d.ts +18 -0
- package/dist/commands/core-work-mesh-live-rebind-native.js +262 -0
- package/dist/commands/core-worker-share-native.d.ts +0 -1
- package/dist/commands/core-worker-share-native.js +1 -1
- package/dist/commands/core.js +7 -0
- package/dist/commands/lanes-message.js +3 -0
- package/dist/commands/lanes-questions.js +2 -1
- package/dist/commands/lanes-resume.js +3 -0
- package/dist/commands/lanes.js +151 -21
- package/dist/fast-core.js +10 -3
- package/dist/fast-scaffold.d.ts +2 -0
- package/dist/fast-scaffold.js +11 -0
- package/dist/lib/doctor/checks/work-context.js +9 -1
- package/dist/lib/lanes/config.d.ts +3 -0
- package/dist/lib/lanes/config.js +14 -0
- package/dist/lib/lanes/disk-guard.d.ts +66 -0
- package/dist/lib/lanes/disk-guard.js +172 -0
- package/dist/lib/lanes/flag-keys.d.ts +2 -0
- package/dist/lib/lanes/flag-keys.js +2 -0
- package/dist/lib/lanes/grants.d.ts +4 -3
- package/dist/lib/lanes/grants.js +12 -7
- package/dist/lib/lanes/hire-grant-dispatch.js +2 -3
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/reconcile.d.ts +7 -1
- package/dist/lib/lanes/reconcile.js +142 -11
- package/dist/lib/lanes/registry-flag.d.ts +6 -0
- package/dist/lib/lanes/registry-flag.js +25 -0
- package/dist/lib/lanes/spawn.d.ts +5 -0
- package/dist/lib/lanes/spawn.js +26 -1
- package/dist/lib/lanes/store.js +11 -0
- package/dist/lib/lanes/types.d.ts +11 -1
- package/dist/lib/lanes/types.js +1 -0
- package/dist/lib/mesh/client.js +23 -5
- package/dist/lib/mesh/denial-codes.d.ts +5 -0
- package/dist/lib/mesh/denial-codes.js +14 -0
- package/dist/lib/mesh/live/daemon/doctor.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/doctor.js +11 -1
- package/dist/lib/search-index/embed-lock.d.ts +2 -0
- package/dist/lib/search-index/embed-lock.js +10 -1
- package/dist/lib/work-context/contract.d.ts +3 -0
- package/dist/lib/work-context/contract.schema.json +3 -1
- package/dist/lib/work-context/outbox.d.ts +14 -5
- package/dist/lib/work-context/outbox.js +48 -5
- package/dist/lib/work-context/reconcile.js +26 -5
- package/dist/main.d.ts +20 -0
- package/dist/main.js +8 -1
- package/dist/utils/core-eval-trigger.d.ts +4 -0
- package/dist/utils/core-eval-trigger.js +88 -0
- package/dist/utils/pack-contributions.d.ts +0 -5
- package/dist/utils/pack-contributions.js +1 -1
- package/dist/utils/redact-error-text.d.ts +0 -2
- package/dist/utils/redact-error-text.js +1 -1
- package/dist/utils/sandbox-runner-client.d.ts +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## [5.
|
|
5
|
+
## [5.313.0] — 2026-10-02
|
|
6
|
+
- Gate live-only lane hire-grant counting by company; blocked and awaiting-input children no longer occupy slots when enabled, and their resume is checked against current capacity.
|
|
7
|
+
- Add company-gated lane low-disk admission protection and Codex rollout retention.
|
|
8
|
+
|
|
9
|
+
## [5.312.1] — 2026-10-02
|
|
6
10
|
- Fix checkpoint Stop hook-state creation and the jq-absent early exit to match the bundled shell.
|
|
7
|
-
-
|
|
11
|
+
- Add native `hq core eval-trigger`, `migrate-policy-triggers`, `register-project`, and `work-mesh-live-rebind` on the fast route behind the default-off `core-native-hook-helpers` flag.
|
|
12
|
+
- Prevent concurrent QMD reindex workers from replacing each other's lock owner record.
|
|
13
|
+
- Add a default-off `hq company rename <new name>` command to update a cloud-backed company's display name.
|
|
14
|
+
- Preserve HTTP status and safe server denial codes in work-mesh registration diagnostics.
|
|
8
15
|
|
|
9
16
|
## [5.312.0] — 2026-10-02
|
|
10
17
|
- The desktop app can turn event-driven daemon sync on or off with its Instant Sync setting. The daemon stores the preference and restarts when a running daemon needs to apply it.
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# hq-core: public
|
|
3
|
+
# work-mesh-live-hook.sh — shared helpers for Work Mesh Live enqueue-only hooks.
|
|
4
|
+
# Sourced by core/hooks/*/3*-work-mesh-*.sh. Never execute directly.
|
|
5
|
+
|
|
6
|
+
# work_mesh_live_disabled
|
|
7
|
+
# Exit 0 (true) when kill switches say these hooks must no-op.
|
|
8
|
+
work_mesh_live_disabled() {
|
|
9
|
+
case "${HQ_WORK_MESH_DISABLED:-}" in
|
|
10
|
+
1|true|TRUE|yes|YES|on|ON) return 0 ;;
|
|
11
|
+
esac
|
|
12
|
+
case ",${HQ_DISABLED_HOOKS:-}," in
|
|
13
|
+
*,work-mesh,*|*,work-mesh-live,*|*,\*,*) return 0 ;;
|
|
14
|
+
esac
|
|
15
|
+
return 1
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# work_mesh_live_bootstrap
|
|
19
|
+
# Resolve HQ_ROOT from the calling hook path; source enqueue + session-id.
|
|
20
|
+
# Caller must set _WM_HOOK_DIR to dirname of BASH_SOURCE[0] before calling,
|
|
21
|
+
# OR pass the hook file path as $1.
|
|
22
|
+
work_mesh_live_bootstrap() {
|
|
23
|
+
local hook_file="${1:-${BASH_SOURCE[1]:-}}"
|
|
24
|
+
local hook_dir hq
|
|
25
|
+
hook_dir="$(cd "$(dirname "$hook_file")" 2>/dev/null && pwd)" || return 1
|
|
26
|
+
# core/hooks/<Event>/<file> -> HQ root is ../../..
|
|
27
|
+
hq="${HQ_ROOT:-$(cd "$hook_dir/../../.." 2>/dev/null && pwd)}"
|
|
28
|
+
[ -n "$hq" ] || return 1
|
|
29
|
+
export HQ_ROOT="$hq"
|
|
30
|
+
# shellcheck source=core/scripts/lib/work-mesh-enqueue.sh
|
|
31
|
+
. "$HQ_ROOT/core/scripts/lib/work-mesh-enqueue.sh" 2>/dev/null || return 1
|
|
32
|
+
# shellcheck source=core/scripts/lib/session-id.sh
|
|
33
|
+
. "$HQ_ROOT/core/scripts/lib/session-id.sh" 2>/dev/null || true
|
|
34
|
+
return 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# work_mesh_live_session_id <stdin-json>
|
|
38
|
+
# Prefer env (session_id_from_env); else payload session_id / sessionId.
|
|
39
|
+
work_mesh_live_session_id() {
|
|
40
|
+
local input="${1:-}" id=""
|
|
41
|
+
if command -v session_id_from_env >/dev/null 2>&1; then
|
|
42
|
+
id="$(session_id_from_env)"
|
|
43
|
+
fi
|
|
44
|
+
if [ -z "$id" ]; then
|
|
45
|
+
for var in HQ_SESSION_ID CLAUDE_CODE_SESSION_ID CLAUDE_SESSION_ID CODEX_SESSION_ID CODEX_THREAD_ID; do
|
|
46
|
+
id="${!var:-}"
|
|
47
|
+
id="$(printf '%s' "$id" | tr -d '[:space:]')"
|
|
48
|
+
[ -n "$id" ] && break
|
|
49
|
+
done
|
|
50
|
+
fi
|
|
51
|
+
if [ -z "$id" ] && [ -n "$input" ]; then
|
|
52
|
+
# Prefer jq when present; pure-bash fallback for the common keys.
|
|
53
|
+
if command -v jq >/dev/null 2>&1; then
|
|
54
|
+
id="$(printf '%s' "$input" | jq -r '.session_id // .sessionId // .conversation_id // .conversationId // .thread_id // .threadId // empty' 2>/dev/null || true)"
|
|
55
|
+
else
|
|
56
|
+
id="$(printf '%s' "$input" | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p; t; s/.*"sessionId"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)"
|
|
57
|
+
fi
|
|
58
|
+
id="$(printf '%s' "$id" | tr -d '[:space:]')"
|
|
59
|
+
fi
|
|
60
|
+
if command -v session_id_is_valid >/dev/null 2>&1; then
|
|
61
|
+
session_id_is_valid "$id" || id=""
|
|
62
|
+
else
|
|
63
|
+
case "$id" in
|
|
64
|
+
""|.|..|*[!A-Za-z0-9._-]*) id="" ;;
|
|
65
|
+
esac
|
|
66
|
+
fi
|
|
67
|
+
printf '%s' "$id"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# work_mesh_live_harness
|
|
71
|
+
# HQ_HARNESS / HQ_WORK_MESH_HARNESS / HQ_CHECKPOINT_RUNTIME -> contract harness.
|
|
72
|
+
work_mesh_live_harness() {
|
|
73
|
+
local h="${HQ_HARNESS:-${HQ_WORK_MESH_HARNESS:-${HQ_CHECKPOINT_RUNTIME:-claude-code}}}"
|
|
74
|
+
case "$h" in
|
|
75
|
+
claude|claude-code|Claude|ClaudeCode) printf 'claude-code' ;;
|
|
76
|
+
codex|Codex) printf 'codex' ;;
|
|
77
|
+
grok|Grok) printf 'grok' ;;
|
|
78
|
+
hq-sessions|hq_sessions|sessions) printf 'hq-sessions' ;;
|
|
79
|
+
agent-box|agent_box|box) printf 'agent-box' ;;
|
|
80
|
+
*) printf '%s' "$h" ;;
|
|
81
|
+
esac
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# work_mesh_live_adapter_version
|
|
85
|
+
work_mesh_live_adapter_version() {
|
|
86
|
+
if [ -n "${HQ_ADAPTER_CONTRACT_VERSION:-}" ]; then
|
|
87
|
+
printf '%s' "$HQ_ADAPTER_CONTRACT_VERSION"
|
|
88
|
+
return 0
|
|
89
|
+
fi
|
|
90
|
+
if [ -f "${HQ_ROOT:-}/core/scripts/lib/provider-adapter-version.sh" ]; then
|
|
91
|
+
# shellcheck source=core/scripts/lib/provider-adapter-version.sh
|
|
92
|
+
. "$HQ_ROOT/core/scripts/lib/provider-adapter-version.sh" 2>/dev/null || true
|
|
93
|
+
fi
|
|
94
|
+
printf '%s' "${HQ_ADAPTER_CONTRACT_VERSION:-1.0.0}"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# work_mesh_live_runtime_version
|
|
98
|
+
work_mesh_live_runtime_version() {
|
|
99
|
+
printf '%s' "${HQ_RUNTIME_VERSION:-${CLAUDE_CODE_VERSION:-${CODEX_VERSION:-${GROK_VERSION:-}}}}"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# work_mesh_live_next_seq <session-id>
|
|
103
|
+
# Monotonic seq under ~/.hq/work-mesh/seq/<sid> (or WORK_MESH_SEQ_DIR).
|
|
104
|
+
# Hot path: bash file read, no tr/chmod when the dir already exists.
|
|
105
|
+
work_mesh_live_next_seq() {
|
|
106
|
+
local sid="$1" dir file n
|
|
107
|
+
dir="${WORK_MESH_SEQ_DIR:-${HOME}/.hq/work-mesh/seq}"
|
|
108
|
+
if [ ! -d "$dir" ]; then
|
|
109
|
+
mkdir -p -- "$dir" 2>/dev/null || true
|
|
110
|
+
chmod 700 -- "$dir" 2>/dev/null || true
|
|
111
|
+
fi
|
|
112
|
+
file="$dir/$sid"
|
|
113
|
+
n=0
|
|
114
|
+
if [ -f "$file" ]; then
|
|
115
|
+
n="$(<"$file")"
|
|
116
|
+
n="${n//[[:space:]]/}"
|
|
117
|
+
case "$n" in
|
|
118
|
+
""|*[!0-9]*) n=0 ;;
|
|
119
|
+
esac
|
|
120
|
+
fi
|
|
121
|
+
n=$((n + 1))
|
|
122
|
+
printf '%s\n' "$n" >"$file" 2>/dev/null || return 1
|
|
123
|
+
printf '%s' "$n"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# work_mesh_live_meta_get <root> <sid> <key>
|
|
127
|
+
work_mesh_live_meta_get() {
|
|
128
|
+
local root="$1" sid="$2" key="$3" meta
|
|
129
|
+
meta="$root/workspace/sessions/$sid/meta.yaml"
|
|
130
|
+
[ -f "$meta" ] || return 0
|
|
131
|
+
awk -v k="$key" '$1==k":"{ sub(/^[^:]+:[[:space:]]*/,""); gsub(/^"|"$/,""); print; exit }' "$meta" 2>/dev/null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# work_mesh_live_state_path <sid>
|
|
135
|
+
work_mesh_live_state_path() {
|
|
136
|
+
printf '%s/.hq/work-context/sessions/%s.json' "${WORK_MESH_HOME:-$HOME}" "$1"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# work_mesh_live_board_path <sid>
|
|
140
|
+
work_mesh_live_board_path() {
|
|
141
|
+
printf '%s/.hq/work-context/sessions/%s/board.md' "${WORK_MESH_HOME:-$HOME}" "$1"
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# work_mesh_live_ask_surfaced_path <sid>
|
|
145
|
+
work_mesh_live_ask_surfaced_path() {
|
|
146
|
+
printf '%s/.hq/work-context/sessions/%s.ask-surfaced' "${WORK_MESH_HOME:-$HOME}" "$1"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# work_mesh_live_pending_decision_path <sid>
|
|
150
|
+
work_mesh_live_pending_decision_path() {
|
|
151
|
+
printf '%s/.hq/work-context/sessions/%s.pending-decision' "${WORK_MESH_HOME:-$HOME}" "$1"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# work_mesh_live_word_count <text> — count whitespace-separated tokens.
|
|
155
|
+
work_mesh_live_word_count() {
|
|
156
|
+
local t="$1"
|
|
157
|
+
# shellcheck disable=SC2086
|
|
158
|
+
set -- $t
|
|
159
|
+
printf '%s' "$#"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# work_mesh_live_is_slash_or_short <prompt>
|
|
163
|
+
# True (0) when slash command or fewer than 3 words.
|
|
164
|
+
work_mesh_live_is_slash_or_short() {
|
|
165
|
+
local p="$1" n
|
|
166
|
+
case "$p" in
|
|
167
|
+
/*) return 0 ;;
|
|
168
|
+
esac
|
|
169
|
+
n="$(work_mesh_live_word_count "$p")"
|
|
170
|
+
[ "$n" -lt 3 ]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# work_mesh_live_bump_tool_writes <sid>
|
|
174
|
+
# Atomically increment toolWrites on the state JSON (create stub if absent).
|
|
175
|
+
# Sets REPLY to the new count. Uses temp+mv and chmod 600.
|
|
176
|
+
# Merge-safe: only toolWrites and updatedAt change; every other field the
|
|
177
|
+
# hq-cli reconcile/ack path wrote (companyUid, companySlug, projectId, taskId,
|
|
178
|
+
# startedAt, bindingEpisodeId, decision, contextStatus, ...) is preserved
|
|
179
|
+
# byte-for-byte. Prefers jq; the no-jq fallback edits the two values in place
|
|
180
|
+
# in pure bash (compact or pretty-printed JSON, any key order) and only
|
|
181
|
+
# appends a missing key. contextStatus is set to "unresolved" only on a fresh
|
|
182
|
+
# stub. Bash 3.2 compatible; no sed/awk/GNU flags.
|
|
183
|
+
# Concurrency: hq-cli writes the same path with same-dir tmp + rename and has
|
|
184
|
+
# no lock convention (hq-cli src/lib/work-context/atomic.ts). Two lines of
|
|
185
|
+
# defense against a reconcile write landing between our read and mv:
|
|
186
|
+
# 1. mkdir lock at <path>.lock (Bash 3.2 safe), bounded wait ~200ms, stale
|
|
187
|
+
# lock reclaimed by age (>5s). If the lock cannot be taken, the bump is
|
|
188
|
+
# skipped (return 1) rather than risk a clobber.
|
|
189
|
+
# 2. Re-read immediately before mv; if the file changed since the first
|
|
190
|
+
# read, rebuild from the new body.
|
|
191
|
+
# Test seam: WORK_MESH_BUMP_TEST_SLEEP_AFTER_READ=<secs> sleeps between the
|
|
192
|
+
# first read and the re-read so a test can inject a concurrent writer.
|
|
193
|
+
work_mesh_live_bump_tool_writes() {
|
|
194
|
+
local sid="$1" path dir lock rc
|
|
195
|
+
path="$(work_mesh_live_state_path "$sid")"
|
|
196
|
+
dir="${path%/*}"
|
|
197
|
+
if [ ! -d "$dir" ]; then
|
|
198
|
+
mkdir -p -- "$dir" 2>/dev/null || true
|
|
199
|
+
chmod 700 -- "$dir" 2>/dev/null || true
|
|
200
|
+
fi
|
|
201
|
+
lock="$path.lock"
|
|
202
|
+
_work_mesh_live_lock_acquire "$lock" || return 1
|
|
203
|
+
_work_mesh_live_bump_locked "$sid" "$path"
|
|
204
|
+
rc=$?
|
|
205
|
+
rm -rf -- "$lock" 2>/dev/null || true
|
|
206
|
+
return $rc
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# _work_mesh_live_lock_acquire <lockdir>
|
|
210
|
+
# mkdir lock; ~20 x 10ms bounded wait; reclaim when the holder's stamp is
|
|
211
|
+
# older than 5s (or missing after the full wait). 0 = held, 1 = give up.
|
|
212
|
+
_work_mesh_live_lock_acquire() {
|
|
213
|
+
local lock="$1" i=0 now stamp
|
|
214
|
+
while :; do
|
|
215
|
+
if mkdir -- "$lock" 2>/dev/null; then
|
|
216
|
+
date +%s >"$lock/ts" 2>/dev/null || true
|
|
217
|
+
return 0
|
|
218
|
+
fi
|
|
219
|
+
i=$((i + 1))
|
|
220
|
+
if [ "$i" -ge 20 ]; then
|
|
221
|
+
now="$(date +%s 2>/dev/null || printf '0')"
|
|
222
|
+
stamp="$(cat "$lock/ts" 2>/dev/null || printf '')"
|
|
223
|
+
case "$stamp" in ""|*[!0-9]*) stamp=0 ;; esac
|
|
224
|
+
if [ "$stamp" -eq 0 ] || [ $((now - stamp)) -gt 5 ]; then
|
|
225
|
+
rm -rf -- "$lock" 2>/dev/null || true
|
|
226
|
+
if mkdir -- "$lock" 2>/dev/null; then
|
|
227
|
+
date +%s >"$lock/ts" 2>/dev/null || true
|
|
228
|
+
return 0
|
|
229
|
+
fi
|
|
230
|
+
fi
|
|
231
|
+
return 1
|
|
232
|
+
fi
|
|
233
|
+
sleep 0.01 2>/dev/null || sleep 1
|
|
234
|
+
done
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
# _work_mesh_live_bump_render <sid> <path> <tmp>
|
|
238
|
+
# Read <path> (if any), build the bumped document into <tmp>.
|
|
239
|
+
# Sets _WM_SEEN to the body that was read ("" when absent) and REPLY to the
|
|
240
|
+
# new count. Returns 1 when the file must not be touched.
|
|
241
|
+
_work_mesh_live_bump_render() {
|
|
242
|
+
local sid="$1" path="$2" tmp="$3" cur="" next ts body="" rest have_jq=0 written=0
|
|
243
|
+
_WM_SEEN=""
|
|
244
|
+
command -v jq >/dev/null 2>&1 && have_jq=1
|
|
245
|
+
if [ -f "$path" ]; then
|
|
246
|
+
body="$(<"$path")"
|
|
247
|
+
_WM_SEEN="$body"
|
|
248
|
+
case "$body" in
|
|
249
|
+
*"\"toolWrites\""*)
|
|
250
|
+
rest="${body#*\"toolWrites\"}"; rest="${rest#*:}"
|
|
251
|
+
while [ "${rest#"${rest%%[![:space:]]*}"}" != "$rest" ]; do rest="${rest#?}"; done
|
|
252
|
+
cur="${rest%%[!0-9]*}"
|
|
253
|
+
;;
|
|
254
|
+
esac
|
|
255
|
+
fi
|
|
256
|
+
case "$cur" in ""|*[!0-9]*) cur=0 ;; esac
|
|
257
|
+
next=$((cur + 1))
|
|
258
|
+
ts=""
|
|
259
|
+
# %(...)T needs Bash 4.2+; macOS Bash 3.2 falls back to date -u (BSD + GNU).
|
|
260
|
+
TZ=UTC printf -v ts '%(%Y-%m-%dT%H:%M:%S)T.000Z' -1 2>/dev/null || ts=""
|
|
261
|
+
case "$ts" in
|
|
262
|
+
""|*%*) ts="$(TZ=UTC date -u '+%Y-%m-%dT%H:%M:%S.000Z' 2>/dev/null || true)" ;;
|
|
263
|
+
esac
|
|
264
|
+
[ -n "$ts" ] || ts="1970-01-01T00:00:00.000Z"
|
|
265
|
+
if [ ! -f "$path" ]; then
|
|
266
|
+
printf '{"contractVersion":1,"sessionId":"%s","contextStatus":"unresolved","toolWrites":%s,"updatedAt":"%s"}\n' \
|
|
267
|
+
"$sid" "$next" "$ts" >"$tmp" || return 1
|
|
268
|
+
REPLY="$next"
|
|
269
|
+
return 0
|
|
270
|
+
fi
|
|
271
|
+
if [ "$have_jq" -eq 1 ]; then
|
|
272
|
+
if printf '%s' "$body" | jq --argjson n "$next" --arg ts "$ts" \
|
|
273
|
+
'.toolWrites = $n | .updatedAt = $ts' >"$tmp" 2>/dev/null \
|
|
274
|
+
&& [ -s "$tmp" ]; then
|
|
275
|
+
written=1
|
|
276
|
+
elif jq -n empty >/dev/null 2>&1; then
|
|
277
|
+
# jq works but could not parse the file (malformed/partial JSON).
|
|
278
|
+
# Never replace the CLI's file with a stub: leave it untouched and skip.
|
|
279
|
+
rm -f -- "$tmp" 2>/dev/null
|
|
280
|
+
return 1
|
|
281
|
+
fi
|
|
282
|
+
# else: jq on PATH is unusable; take the pure-bash path below.
|
|
283
|
+
fi
|
|
284
|
+
if [ "$written" -eq 0 ]; then
|
|
285
|
+
# No-jq fallback: pure-bash in-place edit of the two values (no extra
|
|
286
|
+
# spawns, Bash 3.2 safe). Works for compact and pretty JSON in any key
|
|
287
|
+
# order; every other byte of the CLI's file is left as is.
|
|
288
|
+
local pre digits add
|
|
289
|
+
case "$body" in
|
|
290
|
+
*"\"toolWrites\""*)
|
|
291
|
+
pre="${body%%\"toolWrites\"*}"
|
|
292
|
+
rest="${body#*\"toolWrites\"}"
|
|
293
|
+
while [ "${rest#"${rest%%[![:space:]:]*}"}" != "$rest" ]; do rest="${rest#?}"; done
|
|
294
|
+
digits="${rest%%[!0-9]*}"
|
|
295
|
+
rest="${rest#"$digits"}"
|
|
296
|
+
body="$pre\"toolWrites\": $next$rest"
|
|
297
|
+
;;
|
|
298
|
+
esac
|
|
299
|
+
case "$body" in
|
|
300
|
+
*"\"updatedAt\""*)
|
|
301
|
+
pre="${body%%\"updatedAt\"*}"
|
|
302
|
+
rest="${body#*\"updatedAt\"}"
|
|
303
|
+
while [ "${rest#"${rest%%[![:space:]:]*}"}" != "$rest" ]; do rest="${rest#?}"; done
|
|
304
|
+
case "$rest" in
|
|
305
|
+
\"*) rest="${rest#\"}"; rest="${rest#*\"}" ;;
|
|
306
|
+
esac
|
|
307
|
+
body="$pre\"updatedAt\": \"$ts\"$rest"
|
|
308
|
+
;;
|
|
309
|
+
esac
|
|
310
|
+
add=""
|
|
311
|
+
case "$body" in *"\"toolWrites\""*) ;; *) add="\"toolWrites\": $next" ;; esac
|
|
312
|
+
case "$body" in *"\"updatedAt\""*) ;; *) add="${add:+$add, }\"updatedAt\": \"$ts\"" ;; esac
|
|
313
|
+
if [ -n "$add" ]; then
|
|
314
|
+
# Insert right after the opening brace so the closing brace stays put.
|
|
315
|
+
case "$body" in
|
|
316
|
+
*\{*)
|
|
317
|
+
pre="${body%%\{*}"
|
|
318
|
+
rest="${body#*\{}"
|
|
319
|
+
local peek="$rest"
|
|
320
|
+
while [ "${peek#"${peek%%[![:space:]]*}"}" != "$peek" ]; do peek="${peek#?}"; done
|
|
321
|
+
case "$peek" in
|
|
322
|
+
\}*) body="$pre{$add$peek" ;;
|
|
323
|
+
*) body="$pre{$add, $rest" ;;
|
|
324
|
+
esac
|
|
325
|
+
;;
|
|
326
|
+
*) body="{$add}" ;;
|
|
327
|
+
esac
|
|
328
|
+
fi
|
|
329
|
+
if printf '%s\n' "$body" >"$tmp" 2>/dev/null && [ -s "$tmp" ]; then
|
|
330
|
+
written=1
|
|
331
|
+
fi
|
|
332
|
+
fi
|
|
333
|
+
if [ "$written" -eq 0 ]; then
|
|
334
|
+
rm -f -- "$tmp" 2>/dev/null
|
|
335
|
+
return 1
|
|
336
|
+
fi
|
|
337
|
+
REPLY="$next"
|
|
338
|
+
return 0
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
# _work_mesh_live_bump_locked <sid> <path> (caller holds the lock)
|
|
342
|
+
_work_mesh_live_bump_locked() {
|
|
343
|
+
local sid="$1" path="$2" tmp seen again
|
|
344
|
+
tmp="$path.tmp.$$"
|
|
345
|
+
_work_mesh_live_bump_render "$sid" "$path" "$tmp" || return 1
|
|
346
|
+
seen="$_WM_SEEN"
|
|
347
|
+
if [ -n "${WORK_MESH_BUMP_TEST_SLEEP_AFTER_READ:-}" ]; then
|
|
348
|
+
sleep "$WORK_MESH_BUMP_TEST_SLEEP_AFTER_READ" 2>/dev/null || true
|
|
349
|
+
fi
|
|
350
|
+
# Second line of defense: re-read right before mv; if a concurrent writer
|
|
351
|
+
# (hq-cli reconcile/ack) changed the file since our read, rebuild from it.
|
|
352
|
+
again=""
|
|
353
|
+
[ -f "$path" ] && again="$(<"$path")"
|
|
354
|
+
if [ "$again" != "$seen" ]; then
|
|
355
|
+
rm -f -- "$tmp" 2>/dev/null
|
|
356
|
+
_work_mesh_live_bump_render "$sid" "$path" "$tmp" || return 1
|
|
357
|
+
fi
|
|
358
|
+
mv -f -- "$tmp" "$path" 2>/dev/null || { rm -f -- "$tmp"; return 1; }
|
|
359
|
+
chmod 600 -- "$path" 2>/dev/null || true
|
|
360
|
+
return 0
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
# work_mesh_live_is_file_write_tool <tool_name> <bash_command?>
|
|
364
|
+
work_mesh_live_is_file_write_tool() {
|
|
365
|
+
local tool="$1" cmd="${2:-}"
|
|
366
|
+
case "$tool" in
|
|
367
|
+
Edit|Write|MultiEdit|NotebookEdit|apply_patch|StrReplace|search_replace|write) return 0 ;;
|
|
368
|
+
Bash|Shell|run_terminal_command)
|
|
369
|
+
# Redirect / common file-mutating bash forms (no nested quotes in case arms).
|
|
370
|
+
case "$cmd" in
|
|
371
|
+
*\>*|*tee\ *|*mv\ *|*cp\ *|*truncate\ *) return 0 ;;
|
|
372
|
+
*sed\ -i*|*dd\ if=*) return 0 ;;
|
|
373
|
+
*) return 1 ;;
|
|
374
|
+
esac
|
|
375
|
+
;;
|
|
376
|
+
*) return 1 ;;
|
|
377
|
+
esac
|
|
378
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# hq-core: public
|
|
3
|
+
# US-011 — mid-session rebind: session_end (old) + session_start (new) in spool.
|
|
4
|
+
# Sourced by hooks/scripts. Never execute directly.
|
|
5
|
+
|
|
6
|
+
# work_mesh_live_binding_marker_path <sid>
|
|
7
|
+
work_mesh_live_binding_marker_path() {
|
|
8
|
+
printf '%s/.hq/work-context/sessions/%s.live-binding' "${WORK_MESH_HOME:-$HOME}" "$1"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
# work_mesh_live_read_binding_marker <sid>
|
|
12
|
+
# Sets REPLY to "company|project|task" (may be empty fields).
|
|
13
|
+
work_mesh_live_read_binding_marker() {
|
|
14
|
+
local sid="$1" path body
|
|
15
|
+
path="$(work_mesh_live_binding_marker_path "$sid")"
|
|
16
|
+
REPLY="||"
|
|
17
|
+
[ -f "$path" ] || return 0
|
|
18
|
+
body="$(tr -d '\r' <"$path" 2>/dev/null || true)"
|
|
19
|
+
# companySlug=.. project=.. task=..
|
|
20
|
+
local co="" pr="" tk=""
|
|
21
|
+
co="$(printf '%s\n' "$body" | awk -F= '/^companySlug=/{print $2; exit}')"
|
|
22
|
+
pr="$(printf '%s\n' "$body" | awk -F= '/^project=/{print $2; exit}')"
|
|
23
|
+
tk="$(printf '%s\n' "$body" | awk -F= '/^task=/{print $2; exit}')"
|
|
24
|
+
REPLY="${co}|${pr}|${tk}"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# work_mesh_live_write_binding_marker <sid> <company> <project> <task>
|
|
28
|
+
work_mesh_live_write_binding_marker() {
|
|
29
|
+
local sid="$1" co="${2:-}" pr="${3:-}" tk="${4:-}" path dir
|
|
30
|
+
path="$(work_mesh_live_binding_marker_path "$sid")"
|
|
31
|
+
dir="${path%/*}"
|
|
32
|
+
mkdir -p -- "$dir" 2>/dev/null || true
|
|
33
|
+
chmod 700 -- "$dir" 2>/dev/null || true
|
|
34
|
+
printf 'companySlug=%s\nproject=%s\ntask=%s\n' "$co" "$pr" "$tk" >"$path"
|
|
35
|
+
chmod 600 -- "$path" 2>/dev/null || true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# work_mesh_live_enqueue_lifecycle <kind> <sid> <harness> <adapter> <company> <project> <task>
|
|
39
|
+
work_mesh_live_enqueue_lifecycle() {
|
|
40
|
+
local kind="$1" sid="$2" harness="$3" adapter="$4"
|
|
41
|
+
local company="${5:-}" project="${6:-}" task="${7:-}"
|
|
42
|
+
local seq
|
|
43
|
+
if ! command -v work_mesh_enqueue >/dev/null 2>&1; then
|
|
44
|
+
return 1
|
|
45
|
+
fi
|
|
46
|
+
if command -v work_mesh_live_next_seq >/dev/null 2>&1; then
|
|
47
|
+
seq="$(work_mesh_live_next_seq "$sid")" || return 1
|
|
48
|
+
else
|
|
49
|
+
seq=1
|
|
50
|
+
fi
|
|
51
|
+
local args=(
|
|
52
|
+
--kind "$kind"
|
|
53
|
+
--session-id "$sid"
|
|
54
|
+
--harness "$harness"
|
|
55
|
+
--adapter-version "$adapter"
|
|
56
|
+
--seq "$seq"
|
|
57
|
+
--cwd "${PWD:-}"
|
|
58
|
+
--hq-root "${HQ_ROOT:-}"
|
|
59
|
+
)
|
|
60
|
+
[ -n "$company" ] && args+=(--company-slug "$company")
|
|
61
|
+
[ -n "$project" ] && args+=(--project "$project")
|
|
62
|
+
[ -n "$task" ] && args+=(--task "$task")
|
|
63
|
+
work_mesh_enqueue "${args[@]}" || true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# work_mesh_live_rebind <sid> <old_co> <old_pr> <old_tk> <new_co> <new_pr> <new_tk>
|
|
67
|
+
# Always appends session_end then session_start when old project differs from new
|
|
68
|
+
# and new project is non-empty. Updates the live-binding marker to the new binding.
|
|
69
|
+
work_mesh_live_rebind() {
|
|
70
|
+
local sid="$1"
|
|
71
|
+
local old_co="${2:-}" old_pr="${3:-}" old_tk="${4:-}"
|
|
72
|
+
local new_co="${5:-}" new_pr="${6:-}" new_tk="${7:-}"
|
|
73
|
+
local harness adapter
|
|
74
|
+
[ -n "$sid" ] || return 1
|
|
75
|
+
[ -n "$new_pr" ] || return 1
|
|
76
|
+
[ "$old_pr" != "$new_pr" ] || {
|
|
77
|
+
work_mesh_live_write_binding_marker "$sid" "$new_co" "$new_pr" "$new_tk"
|
|
78
|
+
return 0
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
harness="${HQ_HARNESS:-${HQ_WORK_MESH_HARNESS:-${HQ_CHECKPOINT_RUNTIME:-claude-code}}}"
|
|
82
|
+
case "$harness" in
|
|
83
|
+
claude|Claude|ClaudeCode) harness=claude-code ;;
|
|
84
|
+
Codex) harness=codex ;;
|
|
85
|
+
Grok) harness=grok ;;
|
|
86
|
+
esac
|
|
87
|
+
adapter="${HQ_ADAPTER_CONTRACT_VERSION:-1.0.0}"
|
|
88
|
+
|
|
89
|
+
# Event order: session_end then session_start. Each call takes the next seq from
|
|
90
|
+
# the same per-session counter, so session_start always has a strictly higher
|
|
91
|
+
# seq than session_end. The server REOPENs a session when a session_start with
|
|
92
|
+
# a higher seq and a new binding follows the session_end.
|
|
93
|
+
if [ -n "$old_pr" ]; then
|
|
94
|
+
work_mesh_live_enqueue_lifecycle session_end "$sid" "$harness" "$adapter" \
|
|
95
|
+
"$old_co" "$old_pr" "$old_tk"
|
|
96
|
+
fi
|
|
97
|
+
work_mesh_live_enqueue_lifecycle session_start "$sid" "$harness" "$adapter" \
|
|
98
|
+
"$new_co" "$new_pr" "$new_tk"
|
|
99
|
+
work_mesh_live_write_binding_marker "$sid" "$new_co" "$new_pr" "$new_tk"
|
|
100
|
+
return 0
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# work_mesh_live_maybe_rebind_from_state <sid>
|
|
104
|
+
# Compare state file projectId to live-binding marker; rebind if changed.
|
|
105
|
+
work_mesh_live_maybe_rebind_from_state() {
|
|
106
|
+
local sid="$1" state co pr tk old old_co old_pr old_tk
|
|
107
|
+
local wc="${WORK_MESH_HOME:-$HOME}"
|
|
108
|
+
state="$wc/.hq/work-context/sessions/$sid.json"
|
|
109
|
+
[ -f "$state" ] || return 0
|
|
110
|
+
|
|
111
|
+
if command -v jq >/dev/null 2>&1; then
|
|
112
|
+
co="$(jq -r '.companySlug // empty' "$state" 2>/dev/null || true)"
|
|
113
|
+
pr="$(jq -r '.projectId // empty' "$state" 2>/dev/null || true)"
|
|
114
|
+
tk="$(jq -r '.taskId // empty' "$state" 2>/dev/null || true)"
|
|
115
|
+
else
|
|
116
|
+
return 0
|
|
117
|
+
fi
|
|
118
|
+
[ -n "$pr" ] || return 0
|
|
119
|
+
|
|
120
|
+
work_mesh_live_read_binding_marker "$sid"
|
|
121
|
+
old=$REPLY
|
|
122
|
+
old_co="${old%%|*}"
|
|
123
|
+
rest="${old#*|}"
|
|
124
|
+
old_pr="${rest%%|*}"
|
|
125
|
+
old_tk="${rest#*|}"
|
|
126
|
+
|
|
127
|
+
if [ -z "$old_pr" ]; then
|
|
128
|
+
# First bind observed — record marker without closing a card (session_start
|
|
129
|
+
# already fired at SessionStart). Still emit session_start when marker empty
|
|
130
|
+
# but only if organize rebound after an unbound start: treat empty old as
|
|
131
|
+
# "no prior card" and only update marker unless caller forces rebind.
|
|
132
|
+
work_mesh_live_write_binding_marker "$sid" "$co" "$pr" "$tk"
|
|
133
|
+
return 0
|
|
134
|
+
fi
|
|
135
|
+
if [ "$old_pr" = "$pr" ]; then
|
|
136
|
+
# Same project — refresh task/company on marker.
|
|
137
|
+
work_mesh_live_write_binding_marker "$sid" "$co" "$pr" "$tk"
|
|
138
|
+
return 0
|
|
139
|
+
fi
|
|
140
|
+
work_mesh_live_rebind "$sid" "$old_co" "$old_pr" "$old_tk" "$co" "$pr" "$tk"
|
|
141
|
+
}
|