@indigoai-us/hq-cli 5.315.0 → 5.317.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 +13 -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/session-journal.sh +301 -0
- package/assets/scaffold/core/scripts/session-project.sh +800 -0
- package/assets/scaffold/core/scripts/session-title-config.sh +76 -0
- package/assets/scaffold/core/scripts/session-title.sh +305 -0
- package/assets/scaffold/core/scripts/share-suggestion-state.sh +541 -0
- package/dist/command-catalog.generated.d.ts +97 -0
- package/dist/command-catalog.generated.js +120 -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-session-helper-runtime.d.ts +10 -0
- package/dist/commands/core-session-helper-runtime.js +7 -0
- package/dist/commands/core-session-journal-native.d.ts +4 -0
- package/dist/commands/core-session-journal-native.js +327 -0
- package/dist/commands/core-session-project-native.d.ts +3 -0
- package/dist/commands/core-session-project-native.js +855 -0
- package/dist/commands/core-session-title-config-native.d.ts +4 -0
- package/dist/commands/core-session-title-config-native.js +77 -0
- package/dist/commands/core-session-title-native.d.ts +3 -0
- package/dist/commands/core-session-title-native.js +306 -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 +66 -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 +33 -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/dist/utils/version-gate.js +8 -7
- package/package.json +1 -1
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/** Native port of share-suggestion-state.sh; kept dependency-light for hooks. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
export const SHARE_SUGGESTION_STATE_ASSET = "core/scripts/share-suggestion-state.sh";
|
|
5
|
+
const isObj = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
const sortKeys = (value) => Array.isArray(value) ? value.map(sortKeys)
|
|
7
|
+
: (value && typeof value === "object")
|
|
8
|
+
? Object.keys(value).sort().reduce((out, key) => { out[key] = sortKeys(value[key]); return out; }, {})
|
|
9
|
+
: value;
|
|
10
|
+
const sanitizeValue = (value) => typeof value === "string" ? value.trim() : value;
|
|
11
|
+
function stripComments(line) {
|
|
12
|
+
let single = false, double = false;
|
|
13
|
+
const chars = [];
|
|
14
|
+
for (const char of line) {
|
|
15
|
+
if (char === "'" && !double)
|
|
16
|
+
single = !single;
|
|
17
|
+
else if (char === '"' && !single)
|
|
18
|
+
double = !double;
|
|
19
|
+
else if (char === "#" && !single && !double)
|
|
20
|
+
break;
|
|
21
|
+
chars.push(char);
|
|
22
|
+
}
|
|
23
|
+
return chars.join("").replace(/\s+$/, "");
|
|
24
|
+
}
|
|
25
|
+
function parseScalar(raw) {
|
|
26
|
+
const value = raw.trim();
|
|
27
|
+
if (!value)
|
|
28
|
+
return "";
|
|
29
|
+
const lower = value.toLowerCase();
|
|
30
|
+
if (lower === "true")
|
|
31
|
+
return true;
|
|
32
|
+
if (lower === "false")
|
|
33
|
+
return false;
|
|
34
|
+
if (lower === "null" || lower === "~")
|
|
35
|
+
return null;
|
|
36
|
+
if (value === "{}")
|
|
37
|
+
return {};
|
|
38
|
+
if (value === "[]")
|
|
39
|
+
return [];
|
|
40
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
41
|
+
const inner = value.slice(1, -1).trim();
|
|
42
|
+
return inner ? inner.split(",").map((part) => parseScalar(part.trim())) : [];
|
|
43
|
+
}
|
|
44
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))
|
|
45
|
+
return value.slice(1, -1);
|
|
46
|
+
if (/^-?\d+$/.test(value))
|
|
47
|
+
return Number.parseInt(value, 10);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function parseYamlTokens(tokens, index = 0, indent = null) {
|
|
51
|
+
if (index >= tokens.length)
|
|
52
|
+
return [{}, index];
|
|
53
|
+
indent ??= tokens[index][0];
|
|
54
|
+
if (tokens[index][1].startsWith("- ")) {
|
|
55
|
+
const items = [];
|
|
56
|
+
while (index < tokens.length) {
|
|
57
|
+
const [current, content] = tokens[index];
|
|
58
|
+
if (current !== indent || !content.startsWith("- "))
|
|
59
|
+
break;
|
|
60
|
+
const value = content.slice(2).trim();
|
|
61
|
+
if (!value) {
|
|
62
|
+
const [child, next] = parseYamlTokens(tokens, index + 1);
|
|
63
|
+
items.push(child);
|
|
64
|
+
index = next;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
items.push(parseScalar(value));
|
|
68
|
+
index += 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return [items, index];
|
|
72
|
+
}
|
|
73
|
+
const result = {};
|
|
74
|
+
while (index < tokens.length) {
|
|
75
|
+
const [current, content] = tokens[index];
|
|
76
|
+
if (current !== indent || content.startsWith("- "))
|
|
77
|
+
break;
|
|
78
|
+
const sep = content.indexOf(":");
|
|
79
|
+
const key = (sep < 0 ? content : content.slice(0, sep)).trim();
|
|
80
|
+
const remainder = (sep < 0 ? "" : content.slice(sep + 1)).trim();
|
|
81
|
+
if (!remainder) {
|
|
82
|
+
if (index + 1 < tokens.length && tokens[index + 1][0] > current) {
|
|
83
|
+
const [child, next] = parseYamlTokens(tokens, index + 1, tokens[index + 1][0]);
|
|
84
|
+
result[key] = child;
|
|
85
|
+
index = next;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
result[key] = {};
|
|
89
|
+
index += 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
result[key] = parseScalar(remainder);
|
|
94
|
+
index += 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return [result, index];
|
|
98
|
+
}
|
|
99
|
+
function loadYaml(file, fileSystem) {
|
|
100
|
+
if (!file || !fileSystem.existsSync(file))
|
|
101
|
+
return {};
|
|
102
|
+
try {
|
|
103
|
+
const tokens = [];
|
|
104
|
+
for (const raw of fileSystem.readFileSync(file, "utf8").split(/\r?\n/)) {
|
|
105
|
+
const line = stripComments(raw);
|
|
106
|
+
if (!line.trim())
|
|
107
|
+
continue;
|
|
108
|
+
const content = line.replace(/^ +/, "");
|
|
109
|
+
tokens.push([line.length - content.length, content]);
|
|
110
|
+
}
|
|
111
|
+
if (!tokens.length)
|
|
112
|
+
return {};
|
|
113
|
+
const [value] = parseYamlTokens(tokens);
|
|
114
|
+
return isObj(value) ? value : {};
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function lookupNested(data, dotted) {
|
|
121
|
+
let current = data;
|
|
122
|
+
for (const part of dotted.split(".")) {
|
|
123
|
+
if (!isObj(current) || !(part in current))
|
|
124
|
+
return null;
|
|
125
|
+
current = current[part];
|
|
126
|
+
}
|
|
127
|
+
return current;
|
|
128
|
+
}
|
|
129
|
+
function makeContext(deps) {
|
|
130
|
+
const fileSystem = deps.fs ?? fs;
|
|
131
|
+
const env = deps.env ?? process.env;
|
|
132
|
+
const now = deps.now ?? (() => new Date());
|
|
133
|
+
const isoNow = () => {
|
|
134
|
+
const d = now();
|
|
135
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
136
|
+
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`;
|
|
137
|
+
};
|
|
138
|
+
return { fileSystem, env, isoNow };
|
|
139
|
+
}
|
|
140
|
+
export function runShareSuggestionState(args, stdin, deps) {
|
|
141
|
+
const cmd = args[0] ?? "";
|
|
142
|
+
const rawSession = args[1] ?? "";
|
|
143
|
+
const argDecision = args[2] ?? "";
|
|
144
|
+
const failFromShellMain = (message) => ({ code: 0, stdout: "", stderr: `${message}share-suggestion-state: internal error\n` });
|
|
145
|
+
if (!cmd)
|
|
146
|
+
return failFromShellMain("share-suggestion-state: missing subcommand\n");
|
|
147
|
+
const root = deps.root;
|
|
148
|
+
if (!root)
|
|
149
|
+
return failFromShellMain("share-suggestion-state: unable to resolve HQ root\n");
|
|
150
|
+
const { fileSystem: f, env, isoNow } = makeContext(deps);
|
|
151
|
+
const stateHome = path.join(root, "workspace/orchestrator/share-suggestions");
|
|
152
|
+
const historyPath = path.join(stateHome, "history.jsonl");
|
|
153
|
+
const suppressionsPath = path.join(stateHome, "suppressions.jsonl");
|
|
154
|
+
try {
|
|
155
|
+
f.mkdirSync(stateHome, { recursive: true });
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
const code = error.code;
|
|
159
|
+
let blockedPath = stateHome;
|
|
160
|
+
let current = root;
|
|
161
|
+
for (const piece of path.relative(root, stateHome).split(path.sep)) {
|
|
162
|
+
current = path.join(current, piece);
|
|
163
|
+
try {
|
|
164
|
+
if (!f.statSync(current).isDirectory()) {
|
|
165
|
+
blockedPath = current;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch { /* locate the first existing non-directory parent when present */ }
|
|
170
|
+
}
|
|
171
|
+
const reason = code === "ENOTDIR" ? "Not a directory" : code === "EACCES" || code === "EPERM" ? "Permission denied" : code === "EEXIST" ? "File exists" : (error instanceof Error ? error.message : String(error));
|
|
172
|
+
return failFromShellMain(`mkdir: cannot create directory ‘${blockedPath}’: ${reason}\nshare-suggestion-state: unable to create state directory\n`);
|
|
173
|
+
}
|
|
174
|
+
const sessionBytes = Buffer.from(rawSession, "utf8");
|
|
175
|
+
const sessionId = Array.from(sessionBytes, (byte) => byte === 45 || byte === 95 || (byte >= 48 && byte <= 57) || (byte >= 65 && byte <= 90) || (byte >= 97 && byte <= 122) ? String.fromCharCode(byte) : "_").join("") || "unknown";
|
|
176
|
+
const pendingPath = path.join(stateHome, `${sessionId}.json`);
|
|
177
|
+
// Bash command substitution removes every trailing LF before exporting the payload.
|
|
178
|
+
const payloadRaw = deps.stdinIsTty ? "" : stdin.replace(/\n+$/, "");
|
|
179
|
+
let payload = {};
|
|
180
|
+
if (payloadRaw) {
|
|
181
|
+
try {
|
|
182
|
+
const parsed = JSON.parse(payloadRaw);
|
|
183
|
+
if (isObj(parsed))
|
|
184
|
+
payload = parsed;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
payload = {};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const readJson = (file, fallback) => {
|
|
191
|
+
if (!f.existsSync(file))
|
|
192
|
+
return fallback;
|
|
193
|
+
try {
|
|
194
|
+
return JSON.parse(f.readFileSync(file, "utf8"));
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return fallback;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const writeJson = (file, value) => {
|
|
201
|
+
f.writeFileSync(`${file}.tmp`, `${JSON.stringify(sortKeys(value), null, 2)}\n`);
|
|
202
|
+
f.renameSync(`${file}.tmp`, file);
|
|
203
|
+
};
|
|
204
|
+
const appendJsonl = (file, value) => f.appendFileSync(file, `${JSON.stringify(sortKeys(value))}\n`);
|
|
205
|
+
const uniquePeople = (items) => {
|
|
206
|
+
const seen = new Set(), result = [];
|
|
207
|
+
for (const item of items || []) {
|
|
208
|
+
if (!isObj(item))
|
|
209
|
+
continue;
|
|
210
|
+
const personId = sanitizeValue(item.id || item.slug || ""), name = sanitizeValue(item.name || ""), role = sanitizeValue(item.role || "");
|
|
211
|
+
const cleaned = {};
|
|
212
|
+
if (personId)
|
|
213
|
+
cleaned.id = personId;
|
|
214
|
+
if (name)
|
|
215
|
+
cleaned.name = name;
|
|
216
|
+
if (role)
|
|
217
|
+
cleaned.role = role;
|
|
218
|
+
if (!Object.keys(cleaned).length)
|
|
219
|
+
continue;
|
|
220
|
+
const key = `${cleaned.id || ""}|${cleaned.name || ""}`;
|
|
221
|
+
if (!seen.has(key)) {
|
|
222
|
+
seen.add(key);
|
|
223
|
+
result.push(cleaned);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return result;
|
|
227
|
+
};
|
|
228
|
+
const sanitizeSources = (items) => {
|
|
229
|
+
const seen = new Set(), result = [];
|
|
230
|
+
for (const item of items || []) {
|
|
231
|
+
if (typeof item !== "string")
|
|
232
|
+
continue;
|
|
233
|
+
const value = item.trim();
|
|
234
|
+
if (value && !seen.has(value)) {
|
|
235
|
+
seen.add(value);
|
|
236
|
+
result.push(value);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return result;
|
|
240
|
+
};
|
|
241
|
+
const sanitizeArtifact = (body) => {
|
|
242
|
+
if (!isObj(body))
|
|
243
|
+
body = {};
|
|
244
|
+
let artifact = body.artifact;
|
|
245
|
+
if (!isObj(artifact))
|
|
246
|
+
artifact = body;
|
|
247
|
+
const cleaned = {};
|
|
248
|
+
for (const key of ["path", "fingerprint", "class", "surface", "permission", "app_id", "label"]) {
|
|
249
|
+
const value = sanitizeValue(artifact[key]);
|
|
250
|
+
if (typeof value === "string" && value)
|
|
251
|
+
cleaned[key] = value;
|
|
252
|
+
}
|
|
253
|
+
if (!("permission" in cleaned))
|
|
254
|
+
cleaned.permission = "read";
|
|
255
|
+
return cleaned;
|
|
256
|
+
};
|
|
257
|
+
const sanitizeCandidateHints = (body) => {
|
|
258
|
+
let hints = body.candidate_hints;
|
|
259
|
+
if (!isObj(hints))
|
|
260
|
+
hints = {};
|
|
261
|
+
const cleaned = {
|
|
262
|
+
sources: sanitizeSources(hints.sources || body.candidate_sources || []),
|
|
263
|
+
local_people: uniquePeople(hints.local_people || body.local_people || []),
|
|
264
|
+
needs_assistant_resolution: Boolean("needs_assistant_resolution" in hints ? hints.needs_assistant_resolution : body.needs_assistant_resolution),
|
|
265
|
+
};
|
|
266
|
+
const projects = sanitizeSources(hints.project_recipient_hints || body.project_recipient_hints || []);
|
|
267
|
+
if (projects.length)
|
|
268
|
+
cleaned.project_recipient_hints = projects;
|
|
269
|
+
return cleaned;
|
|
270
|
+
};
|
|
271
|
+
const sanitizeSuggestion = (body) => {
|
|
272
|
+
const artifact = sanitizeArtifact(body), fingerprint = artifact.fingerprint || "";
|
|
273
|
+
const company = sanitizeValue(body.company || ""), project = sanitizeValue(body.project || "");
|
|
274
|
+
const suggestion = {
|
|
275
|
+
session_id: sessionId, company, project, artifact,
|
|
276
|
+
trigger: sanitizeValue(body.trigger || ""), action_kind: sanitizeValue(body.action_kind || "share-suggestion"),
|
|
277
|
+
suggested_permission: sanitizeValue(body.suggested_permission || artifact.permission || "read"),
|
|
278
|
+
recommended_surface: sanitizeValue(body.recommended_surface || artifact.surface || ""),
|
|
279
|
+
recipients: uniquePeople(body.recipients || []), candidate_hints: sanitizeCandidateHints(body),
|
|
280
|
+
created_at: body.created_at || isoNow(), updated_at: isoNow(), shown_at: body.shown_at === undefined ? null : body.shown_at,
|
|
281
|
+
};
|
|
282
|
+
return !company || !fingerprint ? null : suggestion;
|
|
283
|
+
};
|
|
284
|
+
const sanitizeHistory = (body) => {
|
|
285
|
+
const artifact = sanitizeArtifact(body);
|
|
286
|
+
const entry = {
|
|
287
|
+
session_id: sessionId, company: sanitizeValue(body.company || ""), project: sanitizeValue(body.project || ""),
|
|
288
|
+
trigger: sanitizeValue(body.trigger || ""), action_kind: sanitizeValue(body.action_kind || "share-suggestion"),
|
|
289
|
+
decision: sanitizeValue(body.decision || ""), event: sanitizeValue(body.event || ""), artifact,
|
|
290
|
+
recipients: uniquePeople(body.recipients || []), recorded_at: isoNow(),
|
|
291
|
+
};
|
|
292
|
+
return artifact.fingerprint ? entry : null;
|
|
293
|
+
};
|
|
294
|
+
const sanitizeSuppression = (body) => {
|
|
295
|
+
const artifact = sanitizeArtifact(body), scope = sanitizeValue(body.scope || "");
|
|
296
|
+
const record = {
|
|
297
|
+
kind: scope ? "scope" : "artifact", scope,
|
|
298
|
+
company: sanitizeValue(body.company || ""), project: sanitizeValue(body.project || ""),
|
|
299
|
+
artifact_class: sanitizeValue(body.artifact_class || artifact.class || ""), artifact: {},
|
|
300
|
+
reason: sanitizeValue(body.reason || "suppressed"), created_at: isoNow(),
|
|
301
|
+
};
|
|
302
|
+
if (artifact.fingerprint)
|
|
303
|
+
record.artifact.fingerprint = artifact.fingerprint;
|
|
304
|
+
if (artifact.path)
|
|
305
|
+
record.artifact.path = artifact.path;
|
|
306
|
+
if (scope) {
|
|
307
|
+
if (!["global", "company", "project"].includes(scope))
|
|
308
|
+
return null;
|
|
309
|
+
return record;
|
|
310
|
+
}
|
|
311
|
+
return record.artifact.fingerprint ? record : null;
|
|
312
|
+
};
|
|
313
|
+
const configPaths = (company, project) => ({
|
|
314
|
+
global: path.join(root, "personal/settings/auto-share-preferences.yaml"),
|
|
315
|
+
company: company ? path.join(root, "companies", company, "settings/auto-share.yaml") : null,
|
|
316
|
+
project: company && project ? path.join(root, "companies", company, "projects", project, "share-policy.yaml") : null,
|
|
317
|
+
});
|
|
318
|
+
const isSuppressedByConfig = (company, project, artifactClass) => {
|
|
319
|
+
const paths = configPaths(company, project), projectCfg = loadYaml(paths.project, f), companyCfg = loadYaml(paths.company, f), globalCfg = loadYaml(paths.global, f);
|
|
320
|
+
const has = (v) => Object.keys(v).length > 0;
|
|
321
|
+
let enabled = has(projectCfg) ? (typeof lookupNested(projectCfg, "enabled") === "boolean" ? lookupNested(projectCfg, "enabled") : null) : null;
|
|
322
|
+
if (enabled === null)
|
|
323
|
+
enabled = has(companyCfg) && typeof lookupNested(companyCfg, "defaults.enabled") === "boolean" ? lookupNested(companyCfg, "defaults.enabled") : null;
|
|
324
|
+
if (enabled === null)
|
|
325
|
+
enabled = typeof lookupNested(globalCfg, "defaults.enabled") === "boolean" ? lookupNested(globalCfg, "defaults.enabled") : null;
|
|
326
|
+
if (enabled === false)
|
|
327
|
+
return true;
|
|
328
|
+
const key = `artifact_classes.${artifactClass}`;
|
|
329
|
+
let cls = has(projectCfg) && typeof lookupNested(projectCfg, key) === "boolean" ? lookupNested(projectCfg, key) : null;
|
|
330
|
+
if (cls === null)
|
|
331
|
+
cls = has(companyCfg) && typeof lookupNested(companyCfg, key) === "boolean" ? lookupNested(companyCfg, key) : null;
|
|
332
|
+
if (cls === null)
|
|
333
|
+
cls = typeof lookupNested(globalCfg, key) === "boolean" ? lookupNested(globalCfg, key) : null;
|
|
334
|
+
return cls === false;
|
|
335
|
+
};
|
|
336
|
+
const suppressionMatch = (record, company, project, cls, fingerprint) => {
|
|
337
|
+
if (!isObj(record))
|
|
338
|
+
return false;
|
|
339
|
+
const artifact = isObj(record.artifact) ? record.artifact : {};
|
|
340
|
+
if (fingerprint && artifact.fingerprint === fingerprint)
|
|
341
|
+
return true;
|
|
342
|
+
if (record.kind !== "scope")
|
|
343
|
+
return false;
|
|
344
|
+
if (record.artifact_class && cls && record.artifact_class !== cls)
|
|
345
|
+
return false;
|
|
346
|
+
if (record.scope === "global")
|
|
347
|
+
return true;
|
|
348
|
+
if (record.scope === "company")
|
|
349
|
+
return Boolean(company) && record.company === company;
|
|
350
|
+
if (record.scope === "project")
|
|
351
|
+
return Boolean(company && project) && record.company === company && record.project === project;
|
|
352
|
+
return false;
|
|
353
|
+
};
|
|
354
|
+
try {
|
|
355
|
+
if (cmd === "enqueue") {
|
|
356
|
+
const suggestion = sanitizeSuggestion(payload);
|
|
357
|
+
if (!suggestion)
|
|
358
|
+
return { code: 0, stdout: "", stderr: "" };
|
|
359
|
+
const existing = readJson(pendingPath, {});
|
|
360
|
+
if (isObj(existing) && Object.keys(existing).length && !existing.resolved_at)
|
|
361
|
+
return { code: 0, stdout: "", stderr: "" };
|
|
362
|
+
writeJson(pendingPath, suggestion);
|
|
363
|
+
const entry = sanitizeHistory(Object.assign({}, suggestion, { event: "enqueued" }));
|
|
364
|
+
if (entry)
|
|
365
|
+
appendJsonl(historyPath, entry);
|
|
366
|
+
}
|
|
367
|
+
else if (cmd === "peek") {
|
|
368
|
+
const pending = readJson(pendingPath, {});
|
|
369
|
+
if (isObj(pending) && Object.keys(pending).length && !pending.resolved_at)
|
|
370
|
+
return { code: 0, stdout: JSON.stringify(sortKeys(pending)), stderr: "" };
|
|
371
|
+
}
|
|
372
|
+
else if (cmd === "mark-shown") {
|
|
373
|
+
const pending = readJson(pendingPath, {});
|
|
374
|
+
if (isObj(pending) && Object.keys(pending).length && !pending.resolved_at && !pending.shown_at) {
|
|
375
|
+
pending.shown_at = isoNow();
|
|
376
|
+
pending.updated_at = isoNow();
|
|
377
|
+
writeJson(pendingPath, pending);
|
|
378
|
+
const entry = sanitizeHistory(Object.assign({}, pending, { event: "shown" }));
|
|
379
|
+
if (entry)
|
|
380
|
+
appendJsonl(historyPath, entry);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
else if (cmd === "record-decision") {
|
|
384
|
+
const pending = readJson(pendingPath, {}), decision = argDecision || env.SHARE_SUGGESTION_DECISION || sanitizeValue(payload.decision || "");
|
|
385
|
+
if (!isObj(pending) || !Object.keys(pending).length || pending.resolved_at || !decision)
|
|
386
|
+
return { code: 0, stdout: "", stderr: "" };
|
|
387
|
+
pending.decision = decision;
|
|
388
|
+
pending.decision_at = isoNow();
|
|
389
|
+
pending.resolved_at = isoNow();
|
|
390
|
+
pending.updated_at = isoNow();
|
|
391
|
+
if (payload.recipients)
|
|
392
|
+
pending.recipients = uniquePeople(payload.recipients);
|
|
393
|
+
const entry = sanitizeHistory(Object.assign({}, pending, { event: "decision", decision }));
|
|
394
|
+
if (entry)
|
|
395
|
+
appendJsonl(historyPath, entry);
|
|
396
|
+
if (["never", "never-again", "never_again"].includes(decision)) {
|
|
397
|
+
const suppression = sanitizeSuppression({ company: pending.company, project: pending.project, artifact_class: (pending.artifact || {}).class, artifact: pending.artifact, reason: "never-again" });
|
|
398
|
+
if (suppression)
|
|
399
|
+
appendJsonl(suppressionsPath, suppression);
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
f.unlinkSync(pendingPath);
|
|
403
|
+
}
|
|
404
|
+
catch { /* shell ignores a missing pending file */ }
|
|
405
|
+
}
|
|
406
|
+
else if (cmd === "append-history") {
|
|
407
|
+
const entry = sanitizeHistory(payload);
|
|
408
|
+
if (entry)
|
|
409
|
+
appendJsonl(historyPath, entry);
|
|
410
|
+
}
|
|
411
|
+
else if (cmd === "suppress") {
|
|
412
|
+
const record = sanitizeSuppression(payload);
|
|
413
|
+
if (record)
|
|
414
|
+
appendJsonl(suppressionsPath, record);
|
|
415
|
+
}
|
|
416
|
+
else if (cmd === "is-suppressed") {
|
|
417
|
+
const artifact = sanitizeArtifact(payload), company = sanitizeValue(payload.company || ""), project = sanitizeValue(payload.project || "");
|
|
418
|
+
const cls = sanitizeValue(payload.artifact_class || artifact.class || ""), fingerprint = sanitizeValue(artifact.fingerprint || "");
|
|
419
|
+
if (isSuppressedByConfig(company, project, cls))
|
|
420
|
+
return { code: 0, stdout: "true", stderr: "" };
|
|
421
|
+
if (f.existsSync(suppressionsPath)) {
|
|
422
|
+
for (let raw of f.readFileSync(suppressionsPath, "utf8").split(/\r?\n/)) {
|
|
423
|
+
raw = raw.trim();
|
|
424
|
+
if (!raw)
|
|
425
|
+
continue;
|
|
426
|
+
let record;
|
|
427
|
+
try {
|
|
428
|
+
record = JSON.parse(raw);
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (suppressionMatch(record, company, project, cls, fingerprint))
|
|
434
|
+
return { code: 0, stdout: "true", stderr: "" };
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return { code: 0, stdout: "", stderr: "" };
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
442
|
+
return { code: 0, stdout: "", stderr: `share-suggestion-state: internal error\n${detail}\n` };
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
//# sourceMappingURL=core-share-suggestion-state-native.js.map
|
package/dist/commands/core.js
CHANGED
|
@@ -46,6 +46,7 @@ import { registerCorePinNative } from "./core-pin-native.js";
|
|
|
46
46
|
import { registerCoreRefreshVaultAccessNative } from "./core-refresh-vault-access-native.js";
|
|
47
47
|
import { registerCoreWorkMeshBindTrustedNative } from "./core-work-mesh-bind-trusted-native.js";
|
|
48
48
|
import { registerCoreHookHelperNativeCommands } from "./core-native-hook-helper-commands.js";
|
|
49
|
+
import { registerCoreHookStateHelpers } from "./core-hook-state-helpers-native.js";
|
|
49
50
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
50
51
|
import { interpreterFor, runBundledScript } from "../utils/run-bundled-script.js";
|
|
51
52
|
import { CompanyIndexUserError, renderIndexTarget } from "../lib/index-render/index.js";
|
|
@@ -65,6 +66,7 @@ import { timeoutGuardCommand } from "../lib/core-utils/timeout-guard.js";
|
|
|
65
66
|
import { readSentryReportStdin, reportSentryEvent } from "../lib/core-utils/sentry-report.js";
|
|
66
67
|
import { CLEAN_HANDOFF_ASSET, RESUME_LOCK_ASSET, VERIFY_DELIVERABLES_ASSET, WORKTREE_GC_ASSET, runCleanWorktreeReconcileHandoff, runResumeThreadLock, runVerifyStoryDeliverables, runWorktreeGc, } from "../lib/core-utils/worktree-resume.js";
|
|
67
68
|
import { coreNativeHandoffEnabled } from "./core-native-handoff-flag.js";
|
|
69
|
+
import { coreNativeHookHelpersEnabled } from "./core-native-hook-helpers-flag.js";
|
|
68
70
|
import { coreCompanyIndexScopeEnabled } from "./core-company-index-scope-flag.js";
|
|
69
71
|
import { runWorkerSkillCommand } from "./core-worker-skills-command.js";
|
|
70
72
|
import { describeCoreCommand, registeredCoreCommandListing, } from "./core-command-catalog.js";
|
|
@@ -223,6 +225,10 @@ export const SCAFFOLD_COMMANDS = [
|
|
|
223
225
|
* direct `hq core` command.
|
|
224
226
|
*/
|
|
225
227
|
export const SCAFFOLD_ONLY_ASSETS = [
|
|
228
|
+
{ asset: "core/scripts/session-journal.sh", root: "live", summary: "Rollback companion for native session journal" },
|
|
229
|
+
{ asset: "core/scripts/session-project.sh", root: "live", summary: "Rollback companion for native session project" },
|
|
230
|
+
{ asset: "core/scripts/session-title.sh", root: "live", summary: "Rollback companion for native session title" },
|
|
231
|
+
{ asset: "core/scripts/session-title-config.sh", root: "live", summary: "Rollback companion for native session-title settings" },
|
|
226
232
|
{ asset: "core/scripts/resolve-company.sh", root: "live", summary: "Rollback companion for native resolve-company" },
|
|
227
233
|
{ asset: "core/scripts/jobs-schedule-parse.sh", root: "live", summary: "Rollback companion for native jobs schedule-parse" },
|
|
228
234
|
{ asset: "core/scripts/jobs-validate.sh", root: "live", summary: "Rollback companion for native jobs validate" },
|
|
@@ -233,6 +239,9 @@ export const SCAFFOLD_ONLY_ASSETS = [
|
|
|
233
239
|
{ asset: "core/scripts/hq-job-remediate.sh", root: "live", summary: "Scheduled jobs bounded remediation helper" },
|
|
234
240
|
{ asset: "core/scripts/hq-dm-bind.sh", root: "live", summary: "Rollback shell for native dm-bind" },
|
|
235
241
|
{ asset: "core/scripts/work-mesh-live-bind-trusted.sh", root: "live", summary: "Rollback shell for native work-mesh-bind-trusted" },
|
|
242
|
+
{ asset: "core/scripts/share-suggestion-state.sh", root: "live", summary: "Rollback shell for native share-suggestion-state" },
|
|
243
|
+
{ asset: "core/scripts/repo-run-registry.sh", root: "live", summary: "Rollback shell for native repo-run-registry" },
|
|
244
|
+
{ asset: "core/scripts/detect-stale-review-base.sh", root: "live", summary: "Rollback shell for native detect-stale-review-base" },
|
|
236
245
|
// Shared shell libs sourced by the two relocated scripts (and hooks). Never
|
|
237
246
|
// dispatched on their own (so no `hq core` command), but they MUST ship in the
|
|
238
247
|
// tarball, or the bundled hq-session / checkpoint-stop-gate die at `source`
|
|
@@ -387,6 +396,63 @@ export function registerCoreCommands(program) {
|
|
|
387
396
|
registerCoreDmBindNative(core);
|
|
388
397
|
registerCoreWorkMeshBindTrustedNative(core);
|
|
389
398
|
registerCoreHookHelperNativeCommands(core);
|
|
399
|
+
registerCoreHookStateHelpers(core);
|
|
400
|
+
const sessionHelperCommands = [
|
|
401
|
+
{
|
|
402
|
+
name: "session-journal", asset: "core/scripts/session-journal.sh", root: "live",
|
|
403
|
+
summary: "Write and read session journal entries",
|
|
404
|
+
run: async (args, _stdin, root) => (await import("./core-session-journal-native.js"))
|
|
405
|
+
.runSessionJournalFast(args, "", process.cwd(), root),
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
name: "session-project", asset: "core/scripts/session-project.sh", root: "live",
|
|
409
|
+
summary: "Find and update native session projects", readsStdin: true,
|
|
410
|
+
run: async (args, stdin, root) => (await import("./core-session-project-native.js"))
|
|
411
|
+
.runSessionProjectFast(args, stdin, process.cwd(), root),
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
name: "session-title", asset: "core/scripts/session-title.sh", root: "live",
|
|
415
|
+
summary: "Compute the current session title",
|
|
416
|
+
run: async (args, _stdin, root) => (await import("./core-session-title-native.js"))
|
|
417
|
+
.runSessionTitleFast(args, "", process.cwd(), root),
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
name: "session-title-config", asset: "core/scripts/session-title-config.sh", root: "live",
|
|
421
|
+
summary: "Resolve session title settings",
|
|
422
|
+
run: async (args, _stdin, root) => (await import("./core-session-title-config-native.js"))
|
|
423
|
+
.runSessionTitleConfigFast(args, "", process.cwd(), root),
|
|
424
|
+
},
|
|
425
|
+
];
|
|
426
|
+
for (const entry of sessionHelperCommands) {
|
|
427
|
+
const command = core
|
|
428
|
+
.command(entry.name)
|
|
429
|
+
.description(entry.summary)
|
|
430
|
+
.allowUnknownOption()
|
|
431
|
+
.allowExcessArguments()
|
|
432
|
+
.helpOption(false)
|
|
433
|
+
.argument("[args...]")
|
|
434
|
+
.action(async (args = [], _opts, cmd) => {
|
|
435
|
+
const scope = core.opts();
|
|
436
|
+
const operands = cmd.args.length > 0 ? cmd.args : args;
|
|
437
|
+
if (!await coreNativeHookHelpersEnabled()) {
|
|
438
|
+
runEntry(entry, scope, operands);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const { cwd, hqRoot } = resolveEntryRoot(entry, scope);
|
|
442
|
+
const shouldReadStdin = entry.readsStdin && operands[0] === "ingest-plan";
|
|
443
|
+
let stdin = "";
|
|
444
|
+
if (shouldReadStdin) {
|
|
445
|
+
const chunks = [];
|
|
446
|
+
for await (const chunk of process.stdin)
|
|
447
|
+
chunks.push(chunk);
|
|
448
|
+
stdin = Buffer.concat(chunks).toString("utf8");
|
|
449
|
+
}
|
|
450
|
+
const code = await entry.run(operands, stdin, hqRoot ?? cwd);
|
|
451
|
+
if (code !== 0)
|
|
452
|
+
process.exit(code);
|
|
453
|
+
});
|
|
454
|
+
describeCoreCommand(command, { execution: "native", asset: entry.asset, root: entry.root });
|
|
455
|
+
}
|
|
390
456
|
// Wave 2 handoff/worktree commands retain their byte-identical shell assets
|
|
391
457
|
// as a default-off rollback path for one release. Keep the gate helper small
|
|
392
458
|
// so sibling Wave 2 work can reuse it without editing this command table.
|