@indigoai-us/hq-cli 5.315.0 → 5.316.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/assets/scaffold/core/scripts/detect-stale-review-base.sh +199 -0
  3. package/assets/scaffold/core/scripts/repo-run-registry.sh +408 -0
  4. package/assets/scaffold/core/scripts/share-suggestion-state.sh +541 -0
  5. package/dist/command-catalog.generated.d.ts +45 -0
  6. package/dist/command-catalog.generated.js +56 -0
  7. package/dist/commands/bot-jobs.d.ts +5 -1
  8. package/dist/commands/bot-jobs.js +24 -4
  9. package/dist/commands/core-detect-stale-review-base-native.d.ts +23 -0
  10. package/dist/commands/core-detect-stale-review-base-native.js +178 -0
  11. package/dist/commands/core-hook-state-helpers-native.d.ts +11 -0
  12. package/dist/commands/core-hook-state-helpers-native.js +74 -0
  13. package/dist/commands/core-repo-run-registry-native.d.ts +19 -0
  14. package/dist/commands/core-repo-run-registry-native.js +357 -0
  15. package/dist/commands/core-share-suggestion-state-native.d.ts +18 -0
  16. package/dist/commands/core-share-suggestion-state-native.js +445 -0
  17. package/dist/commands/core.js +5 -0
  18. package/dist/commands/lanes.js +35 -5
  19. package/dist/fast-core.d.ts +1 -0
  20. package/dist/fast-core.js +10 -6
  21. package/dist/fast-scaffold.js +17 -0
  22. package/dist/lib/bot/job-quiet-flag.d.ts +4 -0
  23. package/dist/lib/bot/job-quiet-flag.js +14 -0
  24. package/dist/lib/bot/job-run.d.ts +5 -0
  25. package/dist/lib/bot/job-run.js +23 -3
  26. package/dist/lib/bot/run.d.ts +2 -0
  27. package/dist/lib/bot/run.js +15 -3
  28. package/dist/lib/core-utils/common.d.ts +0 -1
  29. package/dist/lib/core-utils/common.js +0 -9
  30. package/dist/lib/lanes/autopilot.d.ts +5 -1
  31. package/dist/lib/lanes/autopilot.js +72 -3
  32. package/dist/lib/lanes/disk-guard.d.ts +3 -4
  33. package/dist/lib/lanes/disk-guard.js +2 -2
  34. package/dist/lib/lanes/flag-keys.d.ts +3 -0
  35. package/dist/lib/lanes/flag-keys.js +3 -0
  36. package/dist/lib/lanes/hooks-install.js +78 -2
  37. package/dist/lib/lanes/keep-alive-audit.d.ts +6 -5
  38. package/dist/lib/lanes/outpost-dispatch.d.ts +9 -0
  39. package/dist/lib/lanes/outpost-dispatch.js +22 -0
  40. package/package.json +1 -1
@@ -0,0 +1,541 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -uo pipefail
4
+
5
+ log_error() {
6
+ printf 'share-suggestion-state: %s\n' "$*" >&2
7
+ }
8
+
9
+ is_hq_root() {
10
+ # Strip trailing slash before appending to prevent "//core" on Windows (MSYS2 SMB stall).
11
+ [ -n "${1:-}" ] && [ -d "${1%/}/core" ] && [ -d "${1%/}/.claude" ]
12
+ }
13
+
14
+ walk_up_to_hq_root() {
15
+ local dir="${1:-}"
16
+ while [ -n "$dir" ]; do
17
+ # Stop before testing "/" so we never form "//core" on Windows.
18
+ [ "$dir" = "/" ] && break
19
+ if is_hq_root "$dir"; then
20
+ printf '%s\n' "$dir"
21
+ return 0
22
+ fi
23
+ dir="$(dirname "$dir")"
24
+ done
25
+ return 1
26
+ }
27
+
28
+ resolve_hq_root() {
29
+ local script_dir root
30
+ if is_hq_root "${CLAUDE_PROJECT_DIR:-}"; then
31
+ printf '%s\n' "$CLAUDE_PROJECT_DIR"
32
+ return 0
33
+ fi
34
+ if root="$(walk_up_to_hq_root "$PWD")"; then
35
+ printf '%s\n' "$root"
36
+ return 0
37
+ fi
38
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
39
+ if root="$(walk_up_to_hq_root "$script_dir")"; then
40
+ printf '%s\n' "$root"
41
+ return 0
42
+ fi
43
+ return 1
44
+ }
45
+
46
+ sanitize_session_id() {
47
+ local raw="${1:-}" cleaned
48
+ cleaned="$(printf '%s' "$raw" | tr -c 'A-Za-z0-9_-' '_')"
49
+ if [ -z "$cleaned" ]; then
50
+ cleaned="unknown"
51
+ fi
52
+ printf '%s\n' "$cleaned"
53
+ }
54
+
55
+ main() {
56
+ local cmd="${1:-}"
57
+ local raw_session_id="${2:-}"
58
+ local decision="${3:-}"
59
+ local session_id payload hq_root state_home pending_path history_path suppressions_path
60
+
61
+ [ -n "$cmd" ] || {
62
+ log_error "missing subcommand"
63
+ return 1
64
+ }
65
+
66
+ hq_root="$(resolve_hq_root)" || {
67
+ log_error "unable to resolve HQ root"
68
+ return 1
69
+ }
70
+
71
+ state_home="$hq_root/workspace/orchestrator/share-suggestions"
72
+ history_path="$state_home/history.jsonl"
73
+ suppressions_path="$state_home/suppressions.jsonl"
74
+ mkdir -p "$state_home" || {
75
+ log_error "unable to create state directory"
76
+ return 1
77
+ }
78
+
79
+ session_id="$(sanitize_session_id "$raw_session_id")"
80
+ pending_path="$state_home/$session_id.json"
81
+
82
+ if [ ! -t 0 ]; then
83
+ payload="$(cat 2>/dev/null || true)"
84
+ else
85
+ payload=""
86
+ fi
87
+
88
+ SHARE_SUGGESTION_CMD="$cmd" \
89
+ SHARE_SUGGESTION_HQ_ROOT="$hq_root" \
90
+ SHARE_SUGGESTION_STATE_HOME="$state_home" \
91
+ SHARE_SUGGESTION_PENDING_PATH="$pending_path" \
92
+ SHARE_SUGGESTION_HISTORY_PATH="$history_path" \
93
+ SHARE_SUGGESTION_SUPPRESSIONS_PATH="$suppressions_path" \
94
+ SHARE_SUGGESTION_SESSION_ID="$session_id" \
95
+ SHARE_SUGGESTION_DECISION="$decision" \
96
+ SHARE_SUGGESTION_PAYLOAD="$payload" \
97
+ node - <<'JS'
98
+ const fs = require("fs");
99
+ const path = require("path");
100
+
101
+ const pad = (x) => String(x).padStart(2, "0");
102
+ function nowIso() {
103
+ const d = new Date();
104
+ return d.getUTCFullYear() + "-" + pad(d.getUTCMonth() + 1) + "-" + pad(d.getUTCDate()) +
105
+ "T" + pad(d.getUTCHours()) + ":" + pad(d.getUTCMinutes()) + ":" + pad(d.getUTCSeconds()) + "Z";
106
+ }
107
+
108
+ const sortKeys = (v) =>
109
+ Array.isArray(v) ? v.map(sortKeys)
110
+ : (v && typeof v === "object")
111
+ ? Object.keys(v).sort().reduce((o, k) => { o[k] = sortKeys(v[k]); return o; }, {})
112
+ : v;
113
+
114
+ function readJson(p, dflt) {
115
+ if (!fs.existsSync(p)) return dflt;
116
+ try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch (e) { return dflt; }
117
+ }
118
+
119
+ function writeJson(p, payload) {
120
+ const tmp = p + ".tmp";
121
+ fs.writeFileSync(tmp, JSON.stringify(sortKeys(payload), null, 2) + "\n");
122
+ fs.renameSync(tmp, p);
123
+ }
124
+
125
+ function appendJsonl(p, payload) {
126
+ fs.appendFileSync(p, JSON.stringify(sortKeys(payload)) + "\n");
127
+ }
128
+
129
+ function loadPayload() {
130
+ const raw = process.env.SHARE_SUGGESTION_PAYLOAD || "";
131
+ if (!raw) return {};
132
+ try { return JSON.parse(raw); } catch (e) { return {}; }
133
+ }
134
+
135
+ const isObj = (v) => v && typeof v === "object" && !Array.isArray(v);
136
+ const sanitizeValue = (v) => (typeof v === "string" ? v.trim() : v);
137
+
138
+ function sanitizePerson(person) {
139
+ if (!isObj(person)) return null;
140
+ const personId = sanitizeValue(person.id || person.slug || "");
141
+ const name = sanitizeValue(person.name || "");
142
+ const role = sanitizeValue(person.role || "");
143
+ const cleaned = {};
144
+ if (personId) cleaned.id = personId;
145
+ if (name) cleaned.name = name;
146
+ if (role) cleaned.role = role;
147
+ return Object.keys(cleaned).length ? cleaned : null;
148
+ }
149
+
150
+ function uniquePeople(items) {
151
+ const seen = new Set();
152
+ const result = [];
153
+ for (const item of items || []) {
154
+ const cleaned = sanitizePerson(item);
155
+ if (!cleaned) continue;
156
+ const key = (cleaned.id || "") + "|" + (cleaned.name || "");
157
+ if (seen.has(key)) continue;
158
+ seen.add(key);
159
+ result.push(cleaned);
160
+ }
161
+ return result;
162
+ }
163
+
164
+ function sanitizeSources(items) {
165
+ const allowed = [];
166
+ const seen = new Set();
167
+ for (const item of items || []) {
168
+ if (typeof item !== "string") continue;
169
+ const value = item.trim();
170
+ if (!value || seen.has(value)) continue;
171
+ seen.add(value);
172
+ allowed.push(value);
173
+ }
174
+ return allowed;
175
+ }
176
+
177
+ function sanitizeArtifact(payload) {
178
+ if (!isObj(payload)) payload = {};
179
+ let artifact = payload.artifact;
180
+ if (!isObj(artifact)) artifact = payload;
181
+ const cleaned = {};
182
+ for (const key of ["path", "fingerprint", "class", "surface", "permission", "app_id", "label"]) {
183
+ const value = sanitizeValue(artifact[key]);
184
+ if (typeof value === "string" && value) cleaned[key] = value;
185
+ }
186
+ if (!("permission" in cleaned)) cleaned.permission = "read";
187
+ return cleaned;
188
+ }
189
+
190
+ function sanitizeCandidateHints(payload) {
191
+ let hints = payload.candidate_hints;
192
+ if (!isObj(hints)) hints = {};
193
+ const cleaned = {
194
+ sources: sanitizeSources(hints.sources || payload.candidate_sources || []),
195
+ local_people: uniquePeople(hints.local_people || payload.local_people || []),
196
+ needs_assistant_resolution: Boolean(
197
+ "needs_assistant_resolution" in hints
198
+ ? hints.needs_assistant_resolution
199
+ : payload.needs_assistant_resolution
200
+ ),
201
+ };
202
+ const projectHints = hints.project_recipient_hints || payload.project_recipient_hints || [];
203
+ const projectCleaned = sanitizeSources(projectHints);
204
+ if (projectCleaned.length) cleaned.project_recipient_hints = projectCleaned;
205
+ return cleaned;
206
+ }
207
+
208
+ function sanitizeSuggestion(payload, sessionId) {
209
+ const artifact = sanitizeArtifact(payload);
210
+ const fingerprint = artifact.fingerprint || "";
211
+ const company = sanitizeValue(payload.company || "");
212
+ const project = sanitizeValue(payload.project || "");
213
+ const suggestion = {
214
+ session_id: sessionId,
215
+ company: company,
216
+ project: project,
217
+ artifact: artifact,
218
+ trigger: sanitizeValue(payload.trigger || ""),
219
+ action_kind: sanitizeValue(payload.action_kind || "share-suggestion"),
220
+ suggested_permission: sanitizeValue(payload.suggested_permission || artifact.permission || "read"),
221
+ recommended_surface: sanitizeValue(payload.recommended_surface || artifact.surface || ""),
222
+ recipients: uniquePeople(payload.recipients || []),
223
+ candidate_hints: sanitizeCandidateHints(payload),
224
+ created_at: payload.created_at || nowIso(),
225
+ updated_at: nowIso(),
226
+ shown_at: payload.shown_at === undefined ? null : payload.shown_at,
227
+ };
228
+ if (!company || !fingerprint) return null;
229
+ return suggestion;
230
+ }
231
+
232
+ function sanitizeHistoryEntry(payload, sessionId) {
233
+ const artifact = sanitizeArtifact(payload);
234
+ const entry = {
235
+ session_id: sessionId,
236
+ company: sanitizeValue(payload.company || ""),
237
+ project: sanitizeValue(payload.project || ""),
238
+ trigger: sanitizeValue(payload.trigger || ""),
239
+ action_kind: sanitizeValue(payload.action_kind || "share-suggestion"),
240
+ decision: sanitizeValue(payload.decision || ""),
241
+ event: sanitizeValue(payload.event || ""),
242
+ artifact: artifact,
243
+ recipients: uniquePeople(payload.recipients || []),
244
+ recorded_at: nowIso(),
245
+ };
246
+ if (!entry.artifact.fingerprint) return null;
247
+ return entry;
248
+ }
249
+
250
+ function sanitizeSuppression(payload) {
251
+ const artifact = sanitizeArtifact(payload);
252
+ const scope = sanitizeValue(payload.scope || "");
253
+ const record = {
254
+ kind: scope ? "scope" : "artifact",
255
+ scope: scope,
256
+ company: sanitizeValue(payload.company || ""),
257
+ project: sanitizeValue(payload.project || ""),
258
+ artifact_class: sanitizeValue(payload.artifact_class || artifact.class || ""),
259
+ artifact: {},
260
+ reason: sanitizeValue(payload.reason || "suppressed"),
261
+ created_at: nowIso(),
262
+ };
263
+ if (artifact.fingerprint) record.artifact.fingerprint = artifact.fingerprint;
264
+ if (artifact.path) record.artifact.path = artifact.path;
265
+ if (scope) {
266
+ if (!["global", "company", "project"].includes(scope)) return null;
267
+ return record;
268
+ }
269
+ if (!record.artifact.fingerprint) return null;
270
+ return record;
271
+ }
272
+
273
+ // --- tiny YAML subset parser (ported 1:1 from the python original) ---
274
+ function stripComments(line) {
275
+ let inSingle = false, inDouble = false;
276
+ const chars = [];
277
+ for (const ch of line) {
278
+ if (ch === "'" && !inDouble) inSingle = !inSingle;
279
+ else if (ch === '"' && !inSingle) inDouble = !inDouble;
280
+ else if (ch === "#" && !inSingle && !inDouble) break;
281
+ chars.push(ch);
282
+ }
283
+ return chars.join("").replace(/\s+$/, "");
284
+ }
285
+
286
+ function parseScalar(value) {
287
+ value = value.trim();
288
+ if (value === "") return "";
289
+ const lowered = value.toLowerCase();
290
+ if (lowered === "true") return true;
291
+ if (lowered === "false") return false;
292
+ if (lowered === "null" || lowered === "~") return null;
293
+ if (value === "{}") return {};
294
+ if (value === "[]") return [];
295
+ if (value.startsWith("[") && value.endsWith("]")) {
296
+ const inner = value.slice(1, -1).trim();
297
+ if (!inner) return [];
298
+ return inner.split(",").map((part) => parseScalar(part.trim()));
299
+ }
300
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
301
+ return value.slice(1, -1);
302
+ }
303
+ if (/^-?\d+$/.test(value)) {
304
+ const n = parseInt(value, 10);
305
+ return Number.isNaN(n) ? value : n;
306
+ }
307
+ return value;
308
+ }
309
+
310
+ function tokenizeYaml(text) {
311
+ const tokens = [];
312
+ for (const raw of text.split(/\r?\n/)) {
313
+ const line = stripComments(raw);
314
+ if (!line.trim()) continue;
315
+ const content = line.replace(/^ +/, "");
316
+ const indent = line.length - content.length;
317
+ tokens.push([indent, content]);
318
+ }
319
+ return tokens;
320
+ }
321
+
322
+ function parseYamlBlock(tokens, index = 0, indent = null) {
323
+ if (index >= tokens.length) return [{}, index];
324
+ if (indent === null) indent = tokens[index][0];
325
+ const firstContent = tokens[index][1];
326
+ if (firstContent.startsWith("- ")) {
327
+ const result = [];
328
+ while (index < tokens.length) {
329
+ const [currentIndent, content] = tokens[index];
330
+ if (currentIndent !== indent || !content.startsWith("- ")) break;
331
+ const value = content.slice(2).trim();
332
+ if (value === "") {
333
+ const [child, next] = parseYamlBlock(tokens, index + 1);
334
+ result.push(child);
335
+ index = next;
336
+ } else {
337
+ result.push(parseScalar(value));
338
+ index += 1;
339
+ }
340
+ }
341
+ return [result, index];
342
+ }
343
+
344
+ const result = {};
345
+ while (index < tokens.length) {
346
+ const [currentIndent, content] = tokens[index];
347
+ if (currentIndent !== indent || content.startsWith("- ")) break;
348
+ const sep = content.indexOf(":");
349
+ const key = (sep === -1 ? content : content.slice(0, sep)).trim();
350
+ const remainder = (sep === -1 ? "" : content.slice(sep + 1)).trim();
351
+ if (remainder === "") {
352
+ if (index + 1 < tokens.length && tokens[index + 1][0] > currentIndent) {
353
+ const [child, next] = parseYamlBlock(tokens, index + 1, tokens[index + 1][0]);
354
+ result[key] = child;
355
+ index = next;
356
+ } else {
357
+ result[key] = {};
358
+ index += 1;
359
+ }
360
+ } else {
361
+ result[key] = parseScalar(remainder);
362
+ index += 1;
363
+ }
364
+ }
365
+ return [result, index];
366
+ }
367
+
368
+ function loadYaml(p) {
369
+ if (!p || !fs.existsSync(p)) return {};
370
+ try {
371
+ const tokens = tokenizeYaml(fs.readFileSync(p, "utf8"));
372
+ if (!tokens.length) return {};
373
+ const [parsed] = parseYamlBlock(tokens);
374
+ return isObj(parsed) ? parsed : {};
375
+ } catch (e) {
376
+ return {};
377
+ }
378
+ }
379
+
380
+ function lookupNested(data, dottedKey) {
381
+ let current = data;
382
+ for (const part of dottedKey.split(".")) {
383
+ if (!isObj(current) || !(part in current)) return null;
384
+ current = current[part];
385
+ }
386
+ return current;
387
+ }
388
+
389
+ const boolSetting = (value) => (typeof value === "boolean" ? value : null);
390
+
391
+ function configPaths(hqRoot, company, project) {
392
+ return {
393
+ global: path.join(hqRoot, "personal", "settings", "auto-share-preferences.yaml"),
394
+ company: company ? path.join(hqRoot, "companies", company, "settings", "auto-share.yaml") : null,
395
+ project: company && project ? path.join(hqRoot, "companies", company, "projects", project, "share-policy.yaml") : null,
396
+ };
397
+ }
398
+
399
+ function classDisabledFromConfig(hqRoot, company, project, artifactClass) {
400
+ const paths = configPaths(hqRoot, company, project);
401
+ const projectCfg = paths.project ? loadYaml(paths.project) : {};
402
+ const companyCfg = paths.company ? loadYaml(paths.company) : {};
403
+ const globalCfg = loadYaml(paths.global);
404
+ const hasKeys = (o) => Object.keys(o).length > 0;
405
+
406
+ let enabled = hasKeys(projectCfg) ? boolSetting(lookupNested(projectCfg, "enabled")) : null;
407
+ if (enabled === null) enabled = hasKeys(companyCfg) ? boolSetting(lookupNested(companyCfg, "defaults.enabled")) : null;
408
+ if (enabled === null) enabled = boolSetting(lookupNested(globalCfg, "defaults.enabled"));
409
+ if (enabled === false) return true;
410
+
411
+ const classKey = "artifact_classes." + artifactClass;
412
+ let classEnabled = hasKeys(projectCfg) ? boolSetting(lookupNested(projectCfg, classKey)) : null;
413
+ if (classEnabled === null) classEnabled = hasKeys(companyCfg) ? boolSetting(lookupNested(companyCfg, classKey)) : null;
414
+ if (classEnabled === null) classEnabled = boolSetting(lookupNested(globalCfg, classKey));
415
+ return classEnabled === false;
416
+ }
417
+
418
+ function suppressionMatch(record, company, project, artifactClass, fingerprint) {
419
+ if (!isObj(record)) return false;
420
+ const artifact = isObj(record.artifact) ? record.artifact : {};
421
+ if (fingerprint && artifact.fingerprint === fingerprint) return true;
422
+ if (record.kind !== "scope") return false;
423
+ const scope = record.scope;
424
+ const scopeClass = record.artifact_class;
425
+ if (scopeClass && artifactClass && scopeClass !== artifactClass) return false;
426
+ if (scope === "global") return true;
427
+ if (scope === "company") return Boolean(company) && record.company === company;
428
+ if (scope === "project") return Boolean(company && project) && record.company === company && record.project === project;
429
+ return false;
430
+ }
431
+
432
+ // --- dispatch ---
433
+ const cmd = process.env.SHARE_SUGGESTION_CMD;
434
+ const hqRoot = process.env.SHARE_SUGGESTION_HQ_ROOT;
435
+ const pendingPath = process.env.SHARE_SUGGESTION_PENDING_PATH;
436
+ const historyPath = process.env.SHARE_SUGGESTION_HISTORY_PATH;
437
+ const suppressionsPath = process.env.SHARE_SUGGESTION_SUPPRESSIONS_PATH;
438
+ const sessionId = process.env.SHARE_SUGGESTION_SESSION_ID;
439
+ const payload = loadPayload();
440
+
441
+ if (cmd === "enqueue") {
442
+ const suggestion = sanitizeSuggestion(payload, sessionId);
443
+ if (suggestion === null) process.exit(0);
444
+ const existing = readJson(pendingPath, {});
445
+ if (isObj(existing) && Object.keys(existing).length && !existing.resolved_at) {
446
+ process.exit(0); // one pending suggestion at a time (same as the original)
447
+ }
448
+ writeJson(pendingPath, suggestion);
449
+ const entry = sanitizeHistoryEntry(Object.assign({}, suggestion, { event: "enqueued" }), sessionId);
450
+ if (entry) appendJsonl(historyPath, entry);
451
+ process.exit(0);
452
+ }
453
+
454
+ if (cmd === "peek") {
455
+ const pending = readJson(pendingPath, {});
456
+ if (isObj(pending) && Object.keys(pending).length && !pending.resolved_at) {
457
+ process.stdout.write(JSON.stringify(sortKeys(pending)));
458
+ }
459
+ process.exit(0);
460
+ }
461
+
462
+ if (cmd === "mark-shown") {
463
+ const pending = readJson(pendingPath, {});
464
+ if (isObj(pending) && Object.keys(pending).length && !pending.resolved_at && !pending.shown_at) {
465
+ pending.shown_at = nowIso();
466
+ pending.updated_at = nowIso();
467
+ writeJson(pendingPath, pending);
468
+ const entry = sanitizeHistoryEntry(Object.assign({}, pending, { event: "shown" }), sessionId);
469
+ if (entry) appendJsonl(historyPath, entry);
470
+ }
471
+ process.exit(0);
472
+ }
473
+
474
+ if (cmd === "record-decision") {
475
+ const pending = readJson(pendingPath, {});
476
+ const decision = process.env.SHARE_SUGGESTION_DECISION || sanitizeValue(payload.decision || "");
477
+ if (!isObj(pending) || !Object.keys(pending).length || pending.resolved_at || !decision) process.exit(0);
478
+ pending.decision = decision;
479
+ pending.decision_at = nowIso();
480
+ pending.resolved_at = nowIso();
481
+ pending.updated_at = nowIso();
482
+ if (payload.recipients) pending.recipients = uniquePeople(payload.recipients);
483
+ const entry = sanitizeHistoryEntry(Object.assign({}, pending, { event: "decision", decision: decision }), sessionId);
484
+ if (entry) appendJsonl(historyPath, entry);
485
+ if (["never", "never-again", "never_again"].includes(decision)) {
486
+ const suppression = sanitizeSuppression({
487
+ company: pending.company,
488
+ project: pending.project,
489
+ artifact_class: (pending.artifact || {}).class,
490
+ artifact: pending.artifact,
491
+ reason: "never-again",
492
+ });
493
+ if (suppression) appendJsonl(suppressionsPath, suppression);
494
+ }
495
+ try { fs.unlinkSync(pendingPath); } catch (e) {}
496
+ process.exit(0);
497
+ }
498
+
499
+ if (cmd === "append-history") {
500
+ const entry = sanitizeHistoryEntry(payload, sessionId);
501
+ if (entry) appendJsonl(historyPath, entry);
502
+ process.exit(0);
503
+ }
504
+
505
+ if (cmd === "suppress") {
506
+ const record = sanitizeSuppression(payload);
507
+ if (record) appendJsonl(suppressionsPath, record);
508
+ process.exit(0);
509
+ }
510
+
511
+ if (cmd === "is-suppressed") {
512
+ const artifact = sanitizeArtifact(payload);
513
+ const company = sanitizeValue(payload.company || "");
514
+ const project = sanitizeValue(payload.project || "");
515
+ const artifactClass = sanitizeValue(payload.artifact_class || artifact.class || "");
516
+ const fingerprint = sanitizeValue(artifact.fingerprint || "");
517
+ if (classDisabledFromConfig(hqRoot, company, project, artifactClass)) {
518
+ process.stdout.write("true");
519
+ process.exit(0);
520
+ }
521
+ if (fs.existsSync(suppressionsPath)) {
522
+ for (let raw of fs.readFileSync(suppressionsPath, "utf8").split(/\r?\n/)) {
523
+ raw = raw.trim();
524
+ if (!raw) continue;
525
+ let record;
526
+ try { record = JSON.parse(raw); } catch (e) { continue; }
527
+ if (suppressionMatch(record, company, project, artifactClass, fingerprint)) {
528
+ process.stdout.write("true");
529
+ process.exit(0);
530
+ }
531
+ }
532
+ }
533
+ process.exit(0);
534
+ }
535
+
536
+ process.exit(0);
537
+ JS
538
+ }
539
+
540
+ main "$@" || log_error "internal error"
541
+ exit 0
@@ -5932,6 +5932,45 @@ export declare const COMMAND_CATALOG: readonly [{
5932
5932
  }];
5933
5933
  readonly options: readonly [];
5934
5934
  readonly subcommands: readonly [];
5935
+ }, {
5936
+ readonly name: "share-suggestion-state";
5937
+ readonly description: "Manage hook share suggestion state";
5938
+ readonly aliases: readonly [];
5939
+ readonly hidden: false;
5940
+ readonly usage: "[args...]";
5941
+ readonly arguments: readonly [{
5942
+ readonly name: "args";
5943
+ readonly required: false;
5944
+ readonly variadic: true;
5945
+ }];
5946
+ readonly options: readonly [];
5947
+ readonly subcommands: readonly [];
5948
+ }, {
5949
+ readonly name: "repo-run-registry";
5950
+ readonly description: "Coordinate active runs across repositories";
5951
+ readonly aliases: readonly [];
5952
+ readonly hidden: false;
5953
+ readonly usage: "[args...]";
5954
+ readonly arguments: readonly [{
5955
+ readonly name: "args";
5956
+ readonly required: false;
5957
+ readonly variadic: true;
5958
+ }];
5959
+ readonly options: readonly [];
5960
+ readonly subcommands: readonly [];
5961
+ }, {
5962
+ readonly name: "detect-stale-review-base";
5963
+ readonly description: "Find stale remote review bases in an HQ root";
5964
+ readonly aliases: readonly [];
5965
+ readonly hidden: false;
5966
+ readonly usage: "[args...]";
5967
+ readonly arguments: readonly [{
5968
+ readonly name: "args";
5969
+ readonly required: false;
5970
+ readonly variadic: true;
5971
+ }];
5972
+ readonly options: readonly [];
5973
+ readonly subcommands: readonly [];
5935
5974
  }, {
5936
5975
  readonly name: "worktree-gc";
5937
5976
  readonly description: "Safely collect old, preserved HQ worktrees";
@@ -8348,6 +8387,9 @@ export declare const COMMAND_CATALOG: readonly [{
8348
8387
  }, {
8349
8388
  readonly flags: "--lane <id>";
8350
8389
  readonly description: "Filter by lane id (repeatable)";
8390
+ }, {
8391
+ readonly flags: "--lane-ulid <ulid>";
8392
+ readonly description: "Filter by lane ULID for internal owner lookup";
8351
8393
  }, {
8352
8394
  readonly flags: "--active";
8353
8395
  readonly description: "Include only lanes that are not terminal";
@@ -10048,6 +10090,9 @@ export declare const COMMAND_CATALOG: readonly [{
10048
10090
  }, {
10049
10091
  readonly flags: "--tz <zone>";
10050
10092
  readonly description: "IANA timezone for the schedule (default: this computer's)";
10093
+ }, {
10094
+ readonly flags: "--quiet";
10095
+ readonly description: "Skip owner DMs after successful runs (requires rollout flag)";
10051
10096
  }, {
10052
10097
  readonly flags: "--rm <jobId>";
10053
10098
  readonly description: "Remove a scheduled job";
@@ -7633,6 +7633,54 @@ export const COMMAND_CATALOG = [
7633
7633
  "options": [],
7634
7634
  "subcommands": []
7635
7635
  },
7636
+ {
7637
+ "name": "share-suggestion-state",
7638
+ "description": "Manage hook share suggestion state",
7639
+ "aliases": [],
7640
+ "hidden": false,
7641
+ "usage": "[args...]",
7642
+ "arguments": [
7643
+ {
7644
+ "name": "args",
7645
+ "required": false,
7646
+ "variadic": true
7647
+ }
7648
+ ],
7649
+ "options": [],
7650
+ "subcommands": []
7651
+ },
7652
+ {
7653
+ "name": "repo-run-registry",
7654
+ "description": "Coordinate active runs across repositories",
7655
+ "aliases": [],
7656
+ "hidden": false,
7657
+ "usage": "[args...]",
7658
+ "arguments": [
7659
+ {
7660
+ "name": "args",
7661
+ "required": false,
7662
+ "variadic": true
7663
+ }
7664
+ ],
7665
+ "options": [],
7666
+ "subcommands": []
7667
+ },
7668
+ {
7669
+ "name": "detect-stale-review-base",
7670
+ "description": "Find stale remote review bases in an HQ root",
7671
+ "aliases": [],
7672
+ "hidden": false,
7673
+ "usage": "[args...]",
7674
+ "arguments": [
7675
+ {
7676
+ "name": "args",
7677
+ "required": false,
7678
+ "variadic": true
7679
+ }
7680
+ ],
7681
+ "options": [],
7682
+ "subcommands": []
7683
+ },
7636
7684
  {
7637
7685
  "name": "worktree-gc",
7638
7686
  "description": "Safely collect old, preserved HQ worktrees",
@@ -10750,6 +10798,10 @@ export const COMMAND_CATALOG = [
10750
10798
  "flags": "--lane <id>",
10751
10799
  "description": "Filter by lane id (repeatable)"
10752
10800
  },
10801
+ {
10802
+ "flags": "--lane-ulid <ulid>",
10803
+ "description": "Filter by lane ULID for internal owner lookup"
10804
+ },
10753
10805
  {
10754
10806
  "flags": "--active",
10755
10807
  "description": "Include only lanes that are not terminal"
@@ -12943,6 +12995,10 @@ export const COMMAND_CATALOG = [
12943
12995
  "flags": "--tz <zone>",
12944
12996
  "description": "IANA timezone for the schedule (default: this computer's)"
12945
12997
  },
12998
+ {
12999
+ "flags": "--quiet",
13000
+ "description": "Skip owner DMs after successful runs (requires rollout flag)"
13001
+ },
12946
13002
  {
12947
13003
  "flags": "--rm <jobId>",
12948
13004
  "description": "Remove a scheduled job"