@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.
@@ -21,13 +21,44 @@ resolve_uid() {
21
21
  tr -d '[:space:]' < "$file"
22
22
  return
23
23
  fi
24
- die "no company uid for $company (set HQ_COMPANY_UID or companies/$company/.company-uid)"
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
- [ -f "${HOME}/.hq/work-mesh/cache/projects/${uid}/${project}.json" ]
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 "$project" --company "$company" --stories-file "$prd" --json)"; then
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 "$project" \
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 == $id) and .threadId == $thread and .channelId == $channel)' \
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:-}" = "--audit" ]; then
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
- while ! mkdir "$lockdir" 2>/dev/null; do
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 50 ] && break
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
- rmdir "$lockdir" 2>/dev/null || true
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) { return dflt; }
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
- main "$@" || log_error "internal error"
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 [ -n "$HQ_BIN" ] && [ -f "$OBS_FILE" ]; then
148
- nohup "$HQ_BIN" mesh context reconcile --observation-file "$OBS_FILE" --machine \
149
- >/dev/null 2>&1 </dev/null &
150
- disown 2>/dev/null || true
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 (processed.includes(name))
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 61: [: ${limit}: integer expression expected\n`);
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
- return deps.fs.readFileSync(file, "utf8").replace(/[\s\u0085\u00a0]+/g, "");
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 or companies/${company}/.company-uid)\n`;
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 34: jq: command not found\n`);
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, 346);
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, 354);
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
- // Only an explicit local directory source may use the reserved bundled
1731
- // pack name. Publisher is self-declared package metadata and is not proof
1732
- // of registry identity, so remote transports never receive this exception.
1733
- const pkg = validateManifest(fetched.payloadDir, hqVersion, { allowReservedName: transport === 'local' });
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
@@ -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 {