@indigoai-us/hq-cli 5.345.18 → 5.345.20
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 +10 -0
- package/assets/scaffold/core/scripts/hq-delegate-grant.sh +12 -5
- package/assets/scaffold/core/scripts/hq-delegate-secrets.sh +1 -1
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +12 -5
- package/assets/scaffold/core/scripts/hq-job-probe.sh +12 -5
- package/assets/scaffold/core/scripts/jobs-validate.sh +12 -5
- package/assets/scaffold/core/scripts/lib/transcript-tail.sh +33 -23
- package/assets/scaffold/core/scripts/ontology-garden.mjs +86 -3
- package/assets/scaffold/core/scripts/ontology-source-worklist.sh +141 -4
- package/assets/scaffold/core/scripts/register-project.sh +82 -14
- package/assets/scaffold/core/scripts/resume-thread-lock.sh +5 -5
- package/assets/scaffold/core/scripts/session-journal.sh +55 -6
- package/assets/scaffold/core/scripts/share-suggestion-state.sh +10 -2
- package/assets/scaffold/core/scripts/work-mesh-live-bind-trusted.sh +36 -4
- package/assets/scaffold/core/scripts/worktree-gc.sh +1 -1
- package/dist/commands/core-ontology-native.js +110 -4
- package/dist/commands/core-register-project-native.js +36 -3
- package/dist/commands/core-session-journal-native.js +2 -2
- package/dist/commands/pack-install.d.ts +4 -0
- package/dist/commands/pack-install.js +12 -4
- package/dist/commands/packs.js +4 -1
- package/dist/commands/pkg-install.js +3 -2
- package/dist/lib/daemon/run.d.ts +2 -2
- package/dist/lib/daemon/run.js +5 -11
- package/dist/lib/doctor/checks/anywhere.js +2 -2
- package/package.json +1 -1
|
@@ -21,13 +21,44 @@ resolve_uid() {
|
|
|
21
21
|
tr -d '[:space:]' < "$file"
|
|
22
22
|
return
|
|
23
23
|
fi
|
|
24
|
-
|
|
24
|
+
if [ -f "$ROOT/companies/manifest.yaml" ]; then
|
|
25
|
+
local manifest_uid
|
|
26
|
+
manifest_uid="$(awk -v company="$company" '
|
|
27
|
+
/^companies:[[:space:]]*$/ { in_companies=1; next }
|
|
28
|
+
in_companies && /^[^[:space:]]/ { in_companies=0 }
|
|
29
|
+
in_companies && /^ [^ #][^:]*:/ {
|
|
30
|
+
slug=$1; sub(/[[:space:]]+$/, "", slug); sub(/:$/, "", slug); in_company=(slug == company); next
|
|
31
|
+
}
|
|
32
|
+
in_company && /^ [^[:space:]#]/ { exit }
|
|
33
|
+
in_company && $1 == "cloud_uid:" { gsub(/["\047]/, "", $2); sub(/[[:space:]]+$/, "", $2); print $2; exit }
|
|
34
|
+
' "$ROOT/companies/manifest.yaml")"
|
|
35
|
+
if [ -n "$manifest_uid" ]; then
|
|
36
|
+
printf '%s\n' "$manifest_uid"
|
|
37
|
+
return
|
|
38
|
+
fi
|
|
39
|
+
fi
|
|
40
|
+
die "no company uid for $company (set HQ_COMPANY_UID, companies/$company/.company-uid, or manifest cloud_uid)"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
mesh_project_id_for_prd() {
|
|
44
|
+
local company="$1" project="$2" board rel resolved
|
|
45
|
+
board="$(board_path "$company")"
|
|
46
|
+
rel="companies/$company/projects/$project/prd.json"
|
|
47
|
+
resolved=""
|
|
48
|
+
if [ -f "$board" ]; then
|
|
49
|
+
resolved="$(jq -r --arg path "$rel" --arg project "$project" '
|
|
50
|
+
[.projects[]? | select(.prd_path == $path or .id == $project)] | first | (.mesh_project_id // .id // empty)
|
|
51
|
+
' "$board" 2>/dev/null | tr -d '\r' || true)"
|
|
52
|
+
fi
|
|
53
|
+
printf '%s\n' "${resolved:-$project}"
|
|
25
54
|
}
|
|
26
55
|
|
|
27
56
|
has_board() {
|
|
28
|
-
local company="$1" project="$2" uid
|
|
57
|
+
local company="$1" project="$2" uid mesh_project
|
|
29
58
|
uid="$(resolve_uid "$company")"
|
|
30
|
-
|
|
59
|
+
mesh_project="$(mesh_project_id_for_prd "$company" "$project")"
|
|
60
|
+
[ -f "${HOME}/.hq/work-mesh/cache/projects/${uid}/${mesh_project}.json" ] \
|
|
61
|
+
|| [ -f "${HOME}/.hq/work-mesh/cache/projects/${uid}/${project}.json" ]
|
|
31
62
|
}
|
|
32
63
|
|
|
33
64
|
story_not_done() {
|
|
@@ -69,7 +100,7 @@ mark_pending() {
|
|
|
69
100
|
prd="$ROOT/companies/$company/projects/$project/prd.json"
|
|
70
101
|
rel="companies/$company/projects/$project/prd.json"
|
|
71
102
|
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
72
|
-
title="$(jq -r '.name // empty' "$prd" 2>/dev/null || true)"
|
|
103
|
+
title="$(jq -r '.name // empty' "$prd" 2>/dev/null | tr -d '\r' || true)"
|
|
73
104
|
[ -n "$title" ] || title="$project"
|
|
74
105
|
mkdir -p "$(dirname "$board")"
|
|
75
106
|
if [ ! -f "$board" ]; then
|
|
@@ -108,19 +139,21 @@ mark_pending() {
|
|
|
108
139
|
register_one() {
|
|
109
140
|
local company="$1" project="$2"
|
|
110
141
|
local prd="$ROOT/companies/$company/projects/$project/prd.json"
|
|
142
|
+
local mesh_project
|
|
143
|
+
mesh_project="$(mesh_project_id_for_prd "$company" "$project")"
|
|
111
144
|
[ -f "$prd" ] || die "registration incomplete: missing $prd"
|
|
112
145
|
if ! cli_has_project_ensure; then
|
|
113
146
|
mark_pending "$company" "$project"
|
|
114
147
|
die "hq-cli ${MIN_HQ_CLI} or newer is required; ${company}/${project} stays local until then"
|
|
115
148
|
fi
|
|
116
149
|
local ensure_json
|
|
117
|
-
if ! ensure_json="$(hq mesh project ensure "$
|
|
150
|
+
if ! ensure_json="$(hq mesh project ensure "$mesh_project" --company "$company" --stories-file "$prd" --json)"; then
|
|
118
151
|
echo "registration incomplete: hq mesh project ensure failed for $company/$project" >&2
|
|
119
152
|
exit 1
|
|
120
153
|
fi
|
|
121
154
|
local thread channel
|
|
122
|
-
thread="$(printf '%s' "$ensure_json" | jq -er '.threadId // empty')" || die "registration incomplete: ensure output missing threadId"
|
|
123
|
-
channel="$(printf '%s' "$ensure_json" | jq -er '.channelId // empty')" || die "registration incomplete: ensure output missing channelId"
|
|
155
|
+
thread="$(printf '%s' "$ensure_json" | jq -er '.threadId // empty' | tr -d '\r')" || die "registration incomplete: ensure output missing threadId"
|
|
156
|
+
channel="$(printf '%s' "$ensure_json" | jq -er '.channelId // empty' | tr -d '\r')" || die "registration incomplete: ensure output missing channelId"
|
|
124
157
|
[ -n "$thread" ] || die "registration incomplete: empty threadId"
|
|
125
158
|
[ -n "$channel" ] || die "registration incomplete: empty channelId"
|
|
126
159
|
|
|
@@ -128,7 +161,7 @@ register_one() {
|
|
|
128
161
|
local rel="companies/$company/projects/$project/prd.json"
|
|
129
162
|
local now title tmp
|
|
130
163
|
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
131
|
-
title="$(jq -r '.name // empty' "$prd")"
|
|
164
|
+
title="$(jq -r '.name // empty' "$prd" | tr -d '\r')"
|
|
132
165
|
[ -n "$title" ] || title="$project"
|
|
133
166
|
mkdir -p "$(dirname "$board")"
|
|
134
167
|
if [ ! -f "$board" ]; then
|
|
@@ -136,7 +169,8 @@ register_one() {
|
|
|
136
169
|
fi
|
|
137
170
|
tmp="$(mktemp)"
|
|
138
171
|
jq \
|
|
139
|
-
--arg id "$
|
|
172
|
+
--arg id "$mesh_project" \
|
|
173
|
+
--arg project "$project" \
|
|
140
174
|
--arg path "$rel" \
|
|
141
175
|
--arg thread "$thread" \
|
|
142
176
|
--arg channel "$channel" \
|
|
@@ -144,9 +178,9 @@ register_one() {
|
|
|
144
178
|
--arg title "$title" \
|
|
145
179
|
'
|
|
146
180
|
.projects = (.projects // []) |
|
|
147
|
-
if any(.projects[]; .id == $id or .prd_path == $path or .mesh_project_id == $id) then
|
|
181
|
+
if any(.projects[]; .id == $id or .id == $project or .prd_path == $path or .mesh_project_id == $id) then
|
|
148
182
|
.projects |= map(
|
|
149
|
-
if .id == $id or .prd_path == $path or .mesh_project_id == $id then
|
|
183
|
+
if .id == $id or .id == $project or .prd_path == $path or .mesh_project_id == $id then
|
|
150
184
|
. + {threadId:$thread, channelId:$channel, updated_at:$now, prd_path:(.prd_path // $path), mesh_project_id:$id} | del(.pending_registration)
|
|
151
185
|
else . end
|
|
152
186
|
)
|
|
@@ -169,14 +203,45 @@ register_one() {
|
|
|
169
203
|
jq -e \
|
|
170
204
|
--arg id "$project" \
|
|
171
205
|
--arg path "$rel" \
|
|
206
|
+
--arg mesh "$mesh_project" \
|
|
172
207
|
--arg thread "$thread" \
|
|
173
208
|
--arg channel "$channel" \
|
|
174
|
-
'any(.projects[]?; (.id == $id or .prd_path == $path or .mesh_project_id == $
|
|
209
|
+
'any(.projects[]?; (.id == $id or .prd_path == $path or .mesh_project_id == $mesh) and .mesh_project_id == $mesh and .threadId == $thread and .channelId == $channel)' \
|
|
175
210
|
"$board" >/dev/null \
|
|
176
211
|
|| die "registration incomplete: board.json entry for $project did not verify"
|
|
177
212
|
echo "registered $company/$project thread=$thread channel=$channel"
|
|
178
213
|
}
|
|
179
214
|
|
|
215
|
+
register_brainstorm() {
|
|
216
|
+
local company="$1" project="$2" board title description ensure_json thread channel tmp
|
|
217
|
+
resolve_uid "$company" >/dev/null
|
|
218
|
+
board="$(board_path "$company")"
|
|
219
|
+
[ -f "$board" ] || die "registration incomplete: missing $board"
|
|
220
|
+
title="$(jq -r --arg id "$project" '[.projects[]? | select(.id == $id or .mesh_project_id == $id)] | first | .title // empty' "$board" | tr -d '\r')"
|
|
221
|
+
description="$(jq -r --arg id "$project" '[.projects[]? | select(.id == $id or .mesh_project_id == $id)] | first | .description // empty' "$board" | tr -d '\r')"
|
|
222
|
+
[ -n "$title" ] || die "registration incomplete: missing local Board project $company/$project"
|
|
223
|
+
if ! ensure_json="$(hq mesh project set "$project" --company "$company" --name "$title" --description "$description" --create --json)"; then
|
|
224
|
+
die "registration incomplete: hq mesh project set failed for $company/$project"
|
|
225
|
+
fi
|
|
226
|
+
thread="$(printf '%s' "$ensure_json" | jq -er '.registration.threadId // .threadId // empty' | tr -d '\r')" \
|
|
227
|
+
|| die "registration incomplete: project set output missing threadId"
|
|
228
|
+
channel="$(printf '%s' "$ensure_json" | jq -er '.registration.channelId // .channelId // empty' | tr -d '\r')" \
|
|
229
|
+
|| die "registration incomplete: project set output missing channelId"
|
|
230
|
+
[ -n "$thread" ] && [ -n "$channel" ] || die "registration incomplete: empty threadId or channelId"
|
|
231
|
+
tmp="$(mktemp)"
|
|
232
|
+
jq --arg id "$project" --arg thread "$thread" --arg channel "$channel" --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '
|
|
233
|
+
.projects |= map(if .id == $id or .mesh_project_id == $id then . + {
|
|
234
|
+
threadId:$thread, channelId:$channel, mesh_project_id:$id,
|
|
235
|
+
mesh_registration_prompt_state:"accepted", updated_at:$now
|
|
236
|
+
} | del(.pending_registration) else . end)
|
|
237
|
+
' "$board" > "$tmp"
|
|
238
|
+
mv "$tmp" "$board"
|
|
239
|
+
jq -e --arg id "$project" --arg thread "$thread" --arg channel "$channel" \
|
|
240
|
+
'any(.projects[]?; (.id == $id or .mesh_project_id == $id) and .threadId == $thread and .channelId == $channel)' \
|
|
241
|
+
"$board" >/dev/null || die "registration incomplete: local Board registration did not verify"
|
|
242
|
+
echo "registered $company/$project thread=$thread channel=$channel"
|
|
243
|
+
}
|
|
244
|
+
|
|
180
245
|
audit() {
|
|
181
246
|
local company="$1"
|
|
182
247
|
[ -n "$company" ] || die "usage: register-project.sh --audit <company>"
|
|
@@ -248,7 +313,10 @@ retry_pending() {
|
|
|
248
313
|
done < <(jq -r '[.projects[]? | select(.pending_registration == true) | .id // empty] | .[]' "$board" 2>/dev/null | tr -d '\r' || true)
|
|
249
314
|
}
|
|
250
315
|
|
|
251
|
-
if [ "${1:-}" = "--
|
|
316
|
+
if [ "${1:-}" = "--brainstorm" ]; then
|
|
317
|
+
[ "$#" -eq 3 ] || die "usage: register-project.sh --brainstorm <company> <board-project-id>"
|
|
318
|
+
register_brainstorm "$2" "$3"
|
|
319
|
+
elif [ "${1:-}" = "--audit" ]; then
|
|
252
320
|
audit "${2:-}"
|
|
253
321
|
elif [ "${1:-}" = "--backfill" ]; then
|
|
254
322
|
shift
|
|
@@ -258,5 +326,5 @@ elif [ "${1:-}" = "--retry-pending" ]; then
|
|
|
258
326
|
elif [ $# -eq 2 ]; then
|
|
259
327
|
register_one "$1" "$2"
|
|
260
328
|
else
|
|
261
|
-
die "usage: register-project.sh <company> <project> | --audit <company> | --backfill <company> --only-active [--yes] | --retry-pending <company>"
|
|
329
|
+
die "usage: register-project.sh <company> <project> | --brainstorm <company> <board-project-id> | --audit <company> | --backfill <company> --only-active [--yes] | --retry-pending <company>"
|
|
262
330
|
fi
|
|
@@ -199,11 +199,11 @@ existing_state() {
|
|
|
199
199
|
fi
|
|
200
200
|
|
|
201
201
|
local record_thread record_session record_at record_epoch record_generation current_epoch
|
|
202
|
-
record_thread="$(jq -r '.thread_id' "$record_path")"
|
|
203
|
-
record_session="$(jq -r '.session_id' "$record_path")"
|
|
204
|
-
record_at="$(jq -r '.resumed_at' "$record_path")"
|
|
205
|
-
record_epoch="$(jq -r '.resumed_epoch' "$record_path")"
|
|
206
|
-
record_generation="$(jq -r '.generation' "$record_path")"
|
|
202
|
+
record_thread="$(jq -r '.thread_id' "$record_path" | tr -d '\r')"
|
|
203
|
+
record_session="$(jq -r '.session_id' "$record_path" | tr -d '\r')"
|
|
204
|
+
record_at="$(jq -r '.resumed_at' "$record_path" | tr -d '\r')"
|
|
205
|
+
record_epoch="$(jq -r '.resumed_epoch' "$record_path" | tr -d '\r')"
|
|
206
|
+
record_generation="$(jq -r '.generation' "$record_path" | tr -d '\r')"
|
|
207
207
|
|
|
208
208
|
if [[ "$record_thread" != "$thread_id" || ! "$record_epoch" =~ ^[0-9]+$ ]]; then
|
|
209
209
|
emit_stale "invalid-record" "$record_session" "$record_at" "$record_generation"
|
|
@@ -128,16 +128,65 @@ resolve_session_key() {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
# Read-modify-write on the counter must not interleave across concurrent tasks.
|
|
131
|
+
reclaim_stale_counter_lock() {
|
|
132
|
+
local lockdir="$1" owner_pid current_owner claim stale_dir suffix=0
|
|
133
|
+
owner_pid="$(cat "$lockdir/pid" 2>/dev/null || true)"
|
|
134
|
+
case "$owner_pid" in ''|*[!0-9]*) return 1 ;; esac
|
|
135
|
+
[ "$owner_pid" -gt 1 ] 2>/dev/null || return 1
|
|
136
|
+
kill -0 "$owner_pid" 2>/dev/null && return 1
|
|
137
|
+
|
|
138
|
+
claim="${lockdir}.reclaim"
|
|
139
|
+
mkdir "$claim" 2>/dev/null || return 1
|
|
140
|
+
if ! printf '%s\n' "$$" > "$claim/pid" 2>/dev/null; then
|
|
141
|
+
rmdir "$claim" 2>/dev/null || true
|
|
142
|
+
return 1
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
current_owner="$(cat "$lockdir/pid" 2>/dev/null || true)"
|
|
146
|
+
if [ "$current_owner" = "$owner_pid" ] && ! kill -0 "$owner_pid" 2>/dev/null; then
|
|
147
|
+
stale_dir="${lockdir}.stale.$$.$RANDOM"
|
|
148
|
+
while [ -e "$stale_dir" ]; do
|
|
149
|
+
suffix=$((suffix + 1))
|
|
150
|
+
stale_dir="${lockdir}.stale.$$.$RANDOM.$suffix"
|
|
151
|
+
done
|
|
152
|
+
if mv "$lockdir" "$stale_dir" 2>/dev/null; then
|
|
153
|
+
rm -rf "$stale_dir" 2>/dev/null || true
|
|
154
|
+
rm -rf "$claim" 2>/dev/null || true
|
|
155
|
+
return 0
|
|
156
|
+
fi
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
rm -rf "$claim" 2>/dev/null || true
|
|
160
|
+
return 1
|
|
161
|
+
}
|
|
162
|
+
|
|
131
163
|
with_counter_lock() {
|
|
132
164
|
local lockdir="$1"; shift
|
|
133
|
-
local i=0
|
|
134
|
-
|
|
165
|
+
local i=0 acquired=0 rc=0 tries="${HQ_JOURNAL_COUNTER_LOCK_TRIES:-250}"
|
|
166
|
+
case "$tries" in ''|*[!0-9]*) tries=250 ;; esac
|
|
167
|
+
while [ "$i" -lt "$tries" ]; do
|
|
168
|
+
if mkdir "$lockdir" 2>/dev/null; then
|
|
169
|
+
acquired=1
|
|
170
|
+
if ! printf '%s\n' "$$" > "$lockdir/pid" 2>/dev/null; then
|
|
171
|
+
rm -rf "$lockdir" 2>/dev/null || true
|
|
172
|
+
return 0
|
|
173
|
+
fi
|
|
174
|
+
break
|
|
175
|
+
fi
|
|
176
|
+
reclaim_stale_counter_lock "$lockdir" && continue
|
|
135
177
|
i=$((i + 1))
|
|
136
|
-
[ "$i" -ge
|
|
137
|
-
sleep 0.02 2>/dev/null || sleep 1
|
|
178
|
+
[ "$i" -ge "$tries" ] || sleep 0.02 2>/dev/null || sleep 1
|
|
138
179
|
done
|
|
139
|
-
"
|
|
140
|
-
|
|
180
|
+
if [ "$acquired" -ne 1 ]; then
|
|
181
|
+
warn "tool-counter: lock remained busy; skipped update"
|
|
182
|
+
return 0
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
"$@" || rc=$?
|
|
186
|
+
if [ "$(cat "$lockdir/pid" 2>/dev/null || true)" = "$$" ]; then
|
|
187
|
+
rm -rf "$lockdir" 2>/dev/null || true
|
|
188
|
+
fi
|
|
189
|
+
return "$rc"
|
|
141
190
|
}
|
|
142
191
|
|
|
143
192
|
counter_read_raw() {
|
|
@@ -113,7 +113,10 @@ const sortKeys = (v) =>
|
|
|
113
113
|
|
|
114
114
|
function readJson(p, dflt) {
|
|
115
115
|
if (!fs.existsSync(p)) return dflt;
|
|
116
|
-
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch (e) {
|
|
116
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch (e) {
|
|
117
|
+
console.error(`share-suggestion-state: unable to read ${p} (${e.name || "Error"})`);
|
|
118
|
+
process.exit(3);
|
|
119
|
+
}
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
function writeJson(p, payload) {
|
|
@@ -537,5 +540,10 @@ process.exit(0);
|
|
|
537
540
|
JS
|
|
538
541
|
}
|
|
539
542
|
|
|
540
|
-
|
|
543
|
+
# Exit 3 means an existing state file could not be read; callers see that
|
|
544
|
+
# failure. Every other error is logged and the helper exits 0.
|
|
545
|
+
main "$@"
|
|
546
|
+
status=$?
|
|
547
|
+
[ "$status" -eq 3 ] && exit 3
|
|
548
|
+
[ "$status" -eq 0 ] || log_error "internal error"
|
|
541
549
|
exit 0
|
|
@@ -144,9 +144,41 @@ if [ "${HQ_WORK_MESH_RECONCILE_STUB:-}" = "1" ]; then
|
|
|
144
144
|
fi
|
|
145
145
|
|
|
146
146
|
HQ_BIN="$(command -v hq 2>/dev/null || true)"
|
|
147
|
-
if [ -
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
if [ -z "$HQ_BIN" ]; then
|
|
148
|
+
printf '%s\n' 'Work Mesh reconcile skipped: hq CLI not found on PATH.' >&2
|
|
149
|
+
else
|
|
150
|
+
flag_reader="$ROOT/.claude/hooks/work-mesh-daemon-not-loaded-flag.cjs"
|
|
151
|
+
daemon_warning_enabled=false
|
|
152
|
+
if [[ -n "${HQ_FLAGS_API_URL:-}" \
|
|
153
|
+
&& "${HQ_COMPANY_UID:-}" =~ ^cmp_[A-Za-z0-9]{3,128}$ ]] \
|
|
154
|
+
&& command -v node >/dev/null 2>&1 && [[ -f "$flag_reader" ]]; then
|
|
155
|
+
daemon_warning_enabled="$(HQ_CLI_BIN="$HQ_BIN" node "$flag_reader")" || daemon_warning_enabled=false
|
|
156
|
+
fi
|
|
157
|
+
if [ "$daemon_warning_enabled" = "true" ]; then
|
|
158
|
+
# Status only reads the installed service and its live state. Keep this hook
|
|
159
|
+
# bounded, and fail quiet when the CLI or service manager cannot answer.
|
|
160
|
+
daemon_status_file="$(mktemp "${TMPDIR:-/tmp}/hq-work-mesh-daemon-status.XXXXXX" 2>/dev/null)" || daemon_status_file=""
|
|
161
|
+
if [ -n "$daemon_status_file" ]; then
|
|
162
|
+
HQ_NO_UPDATE_CHECK=1 "$HQ_BIN" mesh daemon status --json >"$daemon_status_file" 2>/dev/null &
|
|
163
|
+
daemon_status_pid=$!
|
|
164
|
+
( sleep 2; kill "$daemon_status_pid" 2>/dev/null || true ) &
|
|
165
|
+
daemon_status_watchdog=$!
|
|
166
|
+
daemon_status_rc=0
|
|
167
|
+
wait "$daemon_status_pid" || daemon_status_rc=$?
|
|
168
|
+
kill "$daemon_status_watchdog" 2>/dev/null || true
|
|
169
|
+
wait "$daemon_status_watchdog" 2>/dev/null || true
|
|
170
|
+
if [ "$daemon_status_rc" -eq 0 ] && command -v jq >/dev/null 2>&1 && \
|
|
171
|
+
jq -e '(.ok == true) and (.running == false) and (.message | type == "string" and contains("installed ("))' \
|
|
172
|
+
"$daemon_status_file" >/dev/null 2>&1; then
|
|
173
|
+
printf '%s\n' 'Work Mesh daemon is installed but not loaded; start it with: hq mesh daemon install' >&2
|
|
174
|
+
fi
|
|
175
|
+
rm -f "$daemon_status_file" 2>/dev/null || true
|
|
176
|
+
fi
|
|
177
|
+
fi
|
|
178
|
+
if [ -f "$OBS_FILE" ]; then
|
|
179
|
+
nohup "$HQ_BIN" mesh context reconcile --observation-file "$OBS_FILE" --machine \
|
|
180
|
+
>/dev/null 2>&1 </dev/null &
|
|
181
|
+
disown 2>/dev/null || true
|
|
182
|
+
fi
|
|
151
183
|
fi
|
|
152
184
|
exit 0
|
|
@@ -249,7 +249,7 @@ for wt in "${CANDIDATES[@]:-}"; do
|
|
|
249
249
|
created_epoch="$wt_mtime"
|
|
250
250
|
stamp_file="$GC_META_DIR/$(path_key "$wt").json"
|
|
251
251
|
if [[ -f "$stamp_file" ]]; then
|
|
252
|
-
stamped=$(jq -r '.createdAtEpoch // empty' "$stamp_file" 2>/dev/null || echo "")
|
|
252
|
+
stamped=$(jq -r '.createdAtEpoch // empty' "$stamp_file" 2>/dev/null | tr -d '\r' || echo "")
|
|
253
253
|
[[ -n "$stamped" ]] && created_epoch="$stamped"
|
|
254
254
|
fi
|
|
255
255
|
age_days=$(( (NOW - created_epoch) / 86400 ))
|
|
@@ -799,15 +799,119 @@ export function worklistCommand(args, root) {
|
|
|
799
799
|
if (validation.code !== 0)
|
|
800
800
|
return err(2, `${validation.stderr}${validation.stdout}${validation.stderr}`);
|
|
801
801
|
const ledger = path.join(directory, ".processed");
|
|
802
|
+
const skipLedger = path.join(directory, ".skipped");
|
|
803
|
+
const parserVersion = 2;
|
|
802
804
|
if (doneItem) {
|
|
803
|
-
fs.appendFileSync(ledger, `${path.basename(doneItem)}\n`);
|
|
805
|
+
fs.appendFileSync(ledger, `${path.basename(doneItem)}\t${parserVersion}\n`);
|
|
804
806
|
return ok();
|
|
805
807
|
}
|
|
808
|
+
const hasFrontmatterField = (file, field) => {
|
|
809
|
+
if (!fs.existsSync(file))
|
|
810
|
+
return false;
|
|
811
|
+
let frontmatter = 0;
|
|
812
|
+
for (const rawLine of fs.readFileSync(file, "utf8").split("\n")) {
|
|
813
|
+
const line = rawLine.replace(/\r$/, "");
|
|
814
|
+
if (line === "---") {
|
|
815
|
+
frontmatter += 1;
|
|
816
|
+
if (frontmatter === 2)
|
|
817
|
+
return false;
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
if (frontmatter === 1 && line.startsWith(`${field}:`))
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
return false;
|
|
824
|
+
};
|
|
825
|
+
const rewriteIfChanged = (file, original, output) => {
|
|
826
|
+
if (output === original)
|
|
827
|
+
return;
|
|
828
|
+
const temporary = `${file}.native-${process.pid}`;
|
|
829
|
+
fs.writeFileSync(temporary, output);
|
|
830
|
+
fs.renameSync(temporary, file);
|
|
831
|
+
};
|
|
832
|
+
// Match the shell's parser-version migration. Unstamped legacy rows are
|
|
833
|
+
// stamped unless they carry the old resolved_participants bug signature;
|
|
834
|
+
// older parser versions are dropped so those sources are re-evaluated.
|
|
835
|
+
let processed = "";
|
|
836
|
+
if (fs.existsSync(ledger)) {
|
|
837
|
+
const original = fs.readFileSync(ledger, "utf8");
|
|
838
|
+
const kept = [];
|
|
839
|
+
let changed = false;
|
|
840
|
+
const rows = original.split("\n");
|
|
841
|
+
if (rows.at(-1) === "")
|
|
842
|
+
rows.pop();
|
|
843
|
+
for (const rawLine of rows) {
|
|
844
|
+
const fields = rawLine.split("\t");
|
|
845
|
+
const basename = (fields[0] ?? "").replace(/\r$/, "");
|
|
846
|
+
const version = (fields[1] ?? "").replace(/\r$/, "");
|
|
847
|
+
if (!basename)
|
|
848
|
+
continue;
|
|
849
|
+
if (version) {
|
|
850
|
+
if (/^[+-]?\d+$/.test(version) && Number(version) < parserVersion) {
|
|
851
|
+
changed = true;
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
kept.push(`${basename}\t${version}`);
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
changed = true;
|
|
858
|
+
if (hasFrontmatterField(path.join(directory, basename), "resolved_participants"))
|
|
859
|
+
continue;
|
|
860
|
+
kept.push(`${basename}\t${parserVersion}`);
|
|
861
|
+
}
|
|
862
|
+
processed = kept.join("\n");
|
|
863
|
+
if (kept.length > 0)
|
|
864
|
+
processed += "\n";
|
|
865
|
+
if (changed)
|
|
866
|
+
rewriteIfChanged(ledger, original, processed);
|
|
867
|
+
}
|
|
868
|
+
let skipped = "";
|
|
869
|
+
if (fs.existsSync(skipLedger)) {
|
|
870
|
+
const original = fs.readFileSync(skipLedger, "utf8");
|
|
871
|
+
const kept = [];
|
|
872
|
+
let changed = false;
|
|
873
|
+
const rows = original.split("\n");
|
|
874
|
+
if (rows.at(-1) === "")
|
|
875
|
+
rows.pop();
|
|
876
|
+
for (const rawLine of rows) {
|
|
877
|
+
const fields = rawLine.split("\t");
|
|
878
|
+
const basename = fields[0] ?? "";
|
|
879
|
+
const reason = fields[1] ?? "";
|
|
880
|
+
const version = fields[2] ?? "";
|
|
881
|
+
if (!basename)
|
|
882
|
+
continue;
|
|
883
|
+
if (!version || (/^[+-]?\d+$/.test(version) && Number(version) < parserVersion)) {
|
|
884
|
+
changed = true;
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
kept.push(`${basename}\t${reason}\t${version}`);
|
|
888
|
+
}
|
|
889
|
+
skipped = kept.join("\n");
|
|
890
|
+
if (kept.length > 0)
|
|
891
|
+
skipped += "\n";
|
|
892
|
+
if (changed)
|
|
893
|
+
rewriteIfChanged(skipLedger, original, skipped);
|
|
894
|
+
}
|
|
895
|
+
const isProcessed = (basename) => processed.split("\n").some((line) => {
|
|
896
|
+
if (!line)
|
|
897
|
+
return false;
|
|
898
|
+
const [name, version] = line.split("\t");
|
|
899
|
+
if (name !== basename)
|
|
900
|
+
return false;
|
|
901
|
+
if (version === undefined || version === "")
|
|
902
|
+
return true;
|
|
903
|
+
return Number(version) >= parserVersion;
|
|
904
|
+
});
|
|
905
|
+
const isSkippedAtCurrentVersion = (basename) => skipped.split("\n").some((line) => {
|
|
906
|
+
if (!line)
|
|
907
|
+
return false;
|
|
908
|
+
const fields = line.split("\t");
|
|
909
|
+
return fields[0] === basename && fields[2] === String(parserVersion);
|
|
910
|
+
});
|
|
806
911
|
const run = awkField(sourceFile, "run", false);
|
|
807
912
|
const rule = awkField(sourceFile, "audience_rule", false);
|
|
808
913
|
if (run === "cloud" && !force)
|
|
809
914
|
return err(4, "assigned to cloud agent (run: cloud); pass --force to run locally\n");
|
|
810
|
-
const processed = fs.existsSync(ledger) ? fs.readFileSync(ledger, "utf8").split("\n") : [];
|
|
811
915
|
const entries = fs.existsSync(directory)
|
|
812
916
|
? fs.readdirSync(directory).filter((name) => name.endsWith(".md") && !name.startsWith("."))
|
|
813
917
|
.filter((name) => { try {
|
|
@@ -821,12 +925,12 @@ export function worklistCommand(args, root) {
|
|
|
821
925
|
const output = [];
|
|
822
926
|
let count = 0;
|
|
823
927
|
for (const name of entries) {
|
|
824
|
-
if (
|
|
928
|
+
if (isProcessed(name) || isSkippedAtCurrentVersion(name))
|
|
825
929
|
continue;
|
|
826
930
|
if (/^[+-]?\d+$/.test(limit) && count >= Number(limit))
|
|
827
931
|
break;
|
|
828
932
|
if (!/^[+-]?\d+$/.test(limit)) {
|
|
829
|
-
process.stderr.write(`${resolveBundledAsset(`${ASSET}ontology-source-worklist.sh`)}: line
|
|
933
|
+
process.stderr.write(`${resolveBundledAsset(`${ASSET}ontology-source-worklist.sh`)}: line 186: [: ${limit}: integer expression expected\n`);
|
|
830
934
|
}
|
|
831
935
|
count += 1;
|
|
832
936
|
const file = path.join(directory, name);
|
|
@@ -860,6 +964,8 @@ export function worklistCommand(args, root) {
|
|
|
860
964
|
}
|
|
861
965
|
}
|
|
862
966
|
if (!audience) {
|
|
967
|
+
fs.appendFileSync(skipLedger, `${name}\tno-audience\t${parserVersion}\n`);
|
|
968
|
+
skipped += `${name}\tno-audience\t${parserVersion}\n`;
|
|
863
969
|
output.push(`${JSON.stringify({ file: relative, skip: "no-audience" })}\n`);
|
|
864
970
|
continue;
|
|
865
971
|
}
|
|
@@ -51,11 +51,44 @@ function companyUid(company, deps) {
|
|
|
51
51
|
return direct;
|
|
52
52
|
const file = path.join(deps.root, "companies", company, ".company-uid");
|
|
53
53
|
try {
|
|
54
|
-
|
|
54
|
+
const fromFile = deps.fs.readFileSync(file, "utf8").replace(/[\s\u0085\u00a0]+/g, "");
|
|
55
|
+
if (fromFile)
|
|
56
|
+
return fromFile;
|
|
57
|
+
}
|
|
58
|
+
catch { /* shell falls through to companies/manifest.yaml */ }
|
|
59
|
+
const manifest = path.join(deps.root, "companies", "manifest.yaml");
|
|
60
|
+
let contents;
|
|
61
|
+
try {
|
|
62
|
+
contents = deps.fs.readFileSync(manifest, "utf8");
|
|
55
63
|
}
|
|
56
64
|
catch {
|
|
57
65
|
return null;
|
|
58
66
|
}
|
|
67
|
+
let inCompanies = false;
|
|
68
|
+
let inCompany = false;
|
|
69
|
+
for (const line of contents.split(/\r?\n/u)) {
|
|
70
|
+
if (/^companies:[\t ]*$/u.test(line)) {
|
|
71
|
+
inCompanies = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (inCompanies && /^[^\t ]/u.test(line)) {
|
|
75
|
+
inCompanies = false;
|
|
76
|
+
inCompany = false;
|
|
77
|
+
}
|
|
78
|
+
if (inCompanies && /^ {2}[^ #][^:]*:/u.test(line)) {
|
|
79
|
+
const slug = line.trim().split(/[\t ]+/u, 1)[0]?.replace(/:$/u, "");
|
|
80
|
+
inCompany = slug === company;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (inCompany && /^ {2}[^\t ]/u.test(line))
|
|
84
|
+
break;
|
|
85
|
+
if (inCompany) {
|
|
86
|
+
const fields = line.trim().split(/[\t ]+/u);
|
|
87
|
+
if (fields[0] === "cloud_uid:" && fields[1])
|
|
88
|
+
return fields[1].replace(/["']/gu, "").replace(/[\t ]+$/u, "");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
59
92
|
}
|
|
60
93
|
function boardPath(company, deps) { return path.join(deps.root, "companies", company, "board.json"); }
|
|
61
94
|
function runHelpProbe(deps) {
|
|
@@ -217,7 +250,7 @@ function audit(company, deps) {
|
|
|
217
250
|
return false;
|
|
218
251
|
const uid = companyUid(company, deps);
|
|
219
252
|
if (uid === null) {
|
|
220
|
-
stderr += `no company uid for ${company} (set HQ_COMPANY_UID
|
|
253
|
+
stderr += `no company uid for ${company} (set HQ_COMPANY_UID, companies/${company}/.company-uid, or manifest cloud_uid)\n`;
|
|
221
254
|
return true;
|
|
222
255
|
}
|
|
223
256
|
return !deps.fs.existsSync(path.join(deps.env.HOME || os.homedir(), ".hq/work-mesh/cache/projects", uid, `${project}.json`));
|
|
@@ -243,7 +276,7 @@ function storyNotDone(prd, deps, diagnostics) {
|
|
|
243
276
|
diagnostics.push(jq.stderr);
|
|
244
277
|
else if (jq.error?.code === "ENOENT") {
|
|
245
278
|
const script = resolveBundledAsset(REGISTER_PROJECT_ASSET);
|
|
246
|
-
diagnostics.push(`${script}: line
|
|
279
|
+
diagnostics.push(`${script}: line 65: jq: command not found\n`);
|
|
247
280
|
}
|
|
248
281
|
return false;
|
|
249
282
|
}
|
|
@@ -210,7 +210,7 @@ function runToolCounter(root, action, sessionArg, env, now, writers) {
|
|
|
210
210
|
fs.writeFileSync(counter, `${readCounter(counter) + 1}\n`);
|
|
211
211
|
}
|
|
212
212
|
catch (error) {
|
|
213
|
-
reportDirectoryWriteFailure(error,
|
|
213
|
+
reportDirectoryWriteFailure(error, 395);
|
|
214
214
|
}
|
|
215
215
|
});
|
|
216
216
|
}
|
|
@@ -223,7 +223,7 @@ function runToolCounter(root, action, sessionArg, env, now, writers) {
|
|
|
223
223
|
fs.writeFileSync(counter, "0\n");
|
|
224
224
|
}
|
|
225
225
|
catch (error) {
|
|
226
|
-
reportDirectoryWriteFailure(error,
|
|
226
|
+
reportDirectoryWriteFailure(error, 403);
|
|
227
227
|
}
|
|
228
228
|
});
|
|
229
229
|
}
|
|
@@ -46,6 +46,8 @@ export interface ResolveLatestOptions {
|
|
|
46
46
|
export type Transport = 'npm' | 'git' | 'local' | 'marketplace';
|
|
47
47
|
/** Prefix that routes a source through the HQ marketplace transport (US-006). */
|
|
48
48
|
export declare const MARKETPLACE_PREFIX = "marketplace:";
|
|
49
|
+
/** Pinned source allowed to claim the reserved hq-anywhere pack name via its CLI alias. */
|
|
50
|
+
export declare const PINNED_HQ_ANYWHERE_SOURCE = "github:indigoai-us/hq-packages#packages/hq-pack-hq-anywhere";
|
|
49
51
|
export declare function classify(source: string): Transport;
|
|
50
52
|
/**
|
|
51
53
|
* Parse a git source's '#<...>' fragment into { url, subpath, ref }.
|
|
@@ -411,6 +413,8 @@ export declare function runScanPackages(hqRoot: string, opts?: {
|
|
|
411
413
|
}): void;
|
|
412
414
|
export interface InstallPackOptions {
|
|
413
415
|
allowHooks?: boolean;
|
|
416
|
+
/** Internal grant used only by the @indigoai-us/hq-anywhere CLI alias. */
|
|
417
|
+
allowPinnedHqAnywhereName?: true;
|
|
414
418
|
/** Company context for online authorization of install-time MCP secret refs. */
|
|
415
419
|
company?: string;
|
|
416
420
|
/**
|
|
@@ -112,6 +112,8 @@ async function latestWithDiskCache(cacheKey, opts, refresh) {
|
|
|
112
112
|
}
|
|
113
113
|
/** Prefix that routes a source through the HQ marketplace transport (US-006). */
|
|
114
114
|
export const MARKETPLACE_PREFIX = 'marketplace:';
|
|
115
|
+
/** Pinned source allowed to claim the reserved hq-anywhere pack name via its CLI alias. */
|
|
116
|
+
export const PINNED_HQ_ANYWHERE_SOURCE = 'github:indigoai-us/hq-packages#packages/hq-pack-hq-anywhere';
|
|
115
117
|
export function classify(source) {
|
|
116
118
|
// US-006: marketplace:<slug>[@version] is recognized BEFORE the legacy
|
|
117
119
|
// registry fallback and ahead of every other transport. The slug is resolved
|
|
@@ -1727,10 +1729,16 @@ async function installPackWithoutLease(source, opts) {
|
|
|
1727
1729
|
});
|
|
1728
1730
|
break;
|
|
1729
1731
|
}
|
|
1730
|
-
//
|
|
1731
|
-
//
|
|
1732
|
-
//
|
|
1733
|
-
|
|
1732
|
+
// Local directories retain their existing reserved-name behavior. For
|
|
1733
|
+
// remote sources, publisher is self-declared and cannot prove registry
|
|
1734
|
+
// identity, so accept the reserved name only when the top-level CLI alias
|
|
1735
|
+
// supplied its internal grant for the exact pinned GitHub source.
|
|
1736
|
+
const allowPinnedAliasName = transport === 'git' &&
|
|
1737
|
+
source === PINNED_HQ_ANYWHERE_SOURCE &&
|
|
1738
|
+
opts.allowPinnedHqAnywhereName === true;
|
|
1739
|
+
const pkg = validateManifest(fetched.payloadDir, hqVersion, {
|
|
1740
|
+
allowReservedName: transport === 'local' || allowPinnedAliasName,
|
|
1741
|
+
});
|
|
1734
1742
|
// M0 — pack-to-pack dependency pre-flight. Runs BEFORE any prompts or writes
|
|
1735
1743
|
// so a missing/unsatisfied `requires.packs` aborts with no partial state, and
|
|
1736
1744
|
// before we bother the operator with hook/MCP trust prompts for a pack that
|
package/dist/commands/packs.js
CHANGED
|
@@ -26,7 +26,7 @@ import chalk from 'chalk';
|
|
|
26
26
|
import semverSatisfies from 'semver/functions/satisfies.js';
|
|
27
27
|
import { findHqRoot } from '../utils/manifest.js';
|
|
28
28
|
import { resolveHqRoot as resolveDetectedHqRoot } from './doctor.js';
|
|
29
|
-
import { classify, resolveLatest, resolveLatestMarketplace, runScanPackages, installPack, } from './pack-install.js';
|
|
29
|
+
import { classify, PINNED_HQ_ANYWHERE_SOURCE, resolveLatest, resolveLatestMarketplace, runScanPackages, installPack, } from './pack-install.js';
|
|
30
30
|
import { contributionLinks, linkStatus, listInstalledPacks, findDependentPacks, readPackManifest, unwirePack, unwirePackMcp, readHqVersion, readRecommendedPackages, packagesDir, } from '../utils/pack-contributions.js';
|
|
31
31
|
import { syncCodexPackHooks, syncCodexPackSkills } from '../lib/install/codex-pack-contributions.js';
|
|
32
32
|
function resolveRoot(opts) {
|
|
@@ -267,6 +267,9 @@ async function runUpdate(name, opts) {
|
|
|
267
267
|
allowMcp: opts.yes || opts.allowMcp,
|
|
268
268
|
followBranch: opts.branch,
|
|
269
269
|
quiet: wantsJson(opts),
|
|
270
|
+
...(pname === 'hq-anywhere' && source === PINNED_HQ_ANYWHERE_SOURCE
|
|
271
|
+
? { allowPinnedHqAnywhereName: true }
|
|
272
|
+
: {}),
|
|
270
273
|
});
|
|
271
274
|
}
|
|
272
275
|
finally {
|