@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.
- package/CHANGELOG.md +9 -0
- package/assets/scaffold/core/scripts/detect-stale-review-base.sh +199 -0
- package/assets/scaffold/core/scripts/repo-run-registry.sh +408 -0
- package/assets/scaffold/core/scripts/share-suggestion-state.sh +541 -0
- package/dist/command-catalog.generated.d.ts +45 -0
- package/dist/command-catalog.generated.js +56 -0
- package/dist/commands/bot-jobs.d.ts +5 -1
- package/dist/commands/bot-jobs.js +24 -4
- package/dist/commands/core-detect-stale-review-base-native.d.ts +23 -0
- package/dist/commands/core-detect-stale-review-base-native.js +178 -0
- package/dist/commands/core-hook-state-helpers-native.d.ts +11 -0
- package/dist/commands/core-hook-state-helpers-native.js +74 -0
- package/dist/commands/core-repo-run-registry-native.d.ts +19 -0
- package/dist/commands/core-repo-run-registry-native.js +357 -0
- package/dist/commands/core-share-suggestion-state-native.d.ts +18 -0
- package/dist/commands/core-share-suggestion-state-native.js +445 -0
- package/dist/commands/core.js +5 -0
- package/dist/commands/lanes.js +35 -5
- package/dist/fast-core.d.ts +1 -0
- package/dist/fast-core.js +10 -6
- package/dist/fast-scaffold.js +17 -0
- package/dist/lib/bot/job-quiet-flag.d.ts +4 -0
- package/dist/lib/bot/job-quiet-flag.js +14 -0
- package/dist/lib/bot/job-run.d.ts +5 -0
- package/dist/lib/bot/job-run.js +23 -3
- package/dist/lib/bot/run.d.ts +2 -0
- package/dist/lib/bot/run.js +15 -3
- package/dist/lib/core-utils/common.d.ts +0 -1
- package/dist/lib/core-utils/common.js +0 -9
- package/dist/lib/lanes/autopilot.d.ts +5 -1
- package/dist/lib/lanes/autopilot.js +72 -3
- package/dist/lib/lanes/disk-guard.d.ts +3 -4
- package/dist/lib/lanes/disk-guard.js +2 -2
- package/dist/lib/lanes/flag-keys.d.ts +3 -0
- package/dist/lib/lanes/flag-keys.js +3 -0
- package/dist/lib/lanes/hooks-install.js +78 -2
- package/dist/lib/lanes/keep-alive-audit.d.ts +6 -5
- package/dist/lib/lanes/outpost-dispatch.d.ts +9 -0
- package/dist/lib/lanes/outpost-dispatch.js +22 -0
- 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"
|