@indigoai-us/hq-cli 5.321.0 → 5.323.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 +10 -0
- package/assets/scaffold/core/scripts/derive-trigger-facts.sh +263 -0
- package/assets/scaffold/core/scripts/lib/transcript-tail.sh +98 -0
- package/assets/scaffold/core/scripts/lib/trigger-fact-text.awk +86 -0
- package/dist/command-catalog.generated.d.ts +13 -0
- package/dist/command-catalog.generated.js +16 -0
- package/dist/commands/cloud.js +23 -4
- package/dist/commands/core-derive-trigger-facts-command.d.ts +3 -0
- package/dist/commands/core-derive-trigger-facts-command.js +47 -0
- package/dist/commands/core-derive-trigger-facts-native.d.ts +21 -0
- package/dist/commands/core-derive-trigger-facts-native.js +492 -0
- package/dist/commands/core.js +6 -0
- package/dist/commands/daemon.js +1 -1
- package/dist/fast-scaffold.js +13 -0
- package/dist/lib/cli-invite-nudge.d.ts +25 -0
- package/dist/lib/cli-invite-nudge.js +152 -0
- package/dist/lib/daemon/requests.d.ts +1 -0
- package/dist/lib/daemon/requests.js +1 -1
- package/dist/lib/daemon/run.js +5 -4
- package/dist/lib/daemon/succession.js +1 -1
- package/dist/lib/daemon/supervisor.d.ts +2 -1
- package/dist/lib/daemon/supervisor.js +134 -7
- package/dist/lib/first-sync-next-step.d.ts +10 -1
- package/dist/lib/mesh/live/daemon/run.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/run.js +36 -15
- package/dist/main.js +3 -1
- package/dist/utils/cli-telemetry.d.ts +11 -0
- package/dist/utils/cli-telemetry.js +76 -0
- package/dist/utils/run-bundled-script.d.ts +7 -4
- package/dist/utils/run-bundled-script.js +47 -12
- package/dist/utils/vault-api.js +87 -37
- package/package.json +1 -1
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
5
|
+
export const DERIVE_TRIGGER_FACTS_ASSET = "core/scripts/derive-trigger-facts.sh";
|
|
6
|
+
const TEXT_FACT_AWK = "core/scripts/lib/trigger-fact-text.awk";
|
|
7
|
+
const TRANSCRIPT_TAIL_MAX_BYTES = 1_048_576;
|
|
8
|
+
function effectiveEnvironment(options) {
|
|
9
|
+
return {
|
|
10
|
+
...process.env,
|
|
11
|
+
...(options.env ?? {}),
|
|
12
|
+
HQ_ROOT: options.hqRoot,
|
|
13
|
+
CLAUDE_PROJECT_DIR: options.hqRoot,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
class ShellFallbackRequired extends Error {
|
|
17
|
+
}
|
|
18
|
+
function asciiLower(value) {
|
|
19
|
+
return value.replace(/[A-Z]/g, (letter) => String.fromCharCode(letter.charCodeAt(0) + 32));
|
|
20
|
+
}
|
|
21
|
+
function localeLower(value, env) {
|
|
22
|
+
const locale = env.LC_ALL || env.LC_CTYPE || env.LANG || "C";
|
|
23
|
+
if (/^(?:C|POSIX)(?:$|\.(?!utf-?8$))/i.test(locale))
|
|
24
|
+
return asciiLower(value);
|
|
25
|
+
// glibc applies towlower independently to each character. Lowercase code
|
|
26
|
+
// points separately so JavaScript does not apply context-sensitive sigma.
|
|
27
|
+
// glibc maps U+0130 to simple `i`; JavaScript otherwise expands it.
|
|
28
|
+
return Array.from(value, (character) => character === "\u0130" ? "i" : character.toLowerCase()).join("");
|
|
29
|
+
}
|
|
30
|
+
function hasGlobPattern(value) {
|
|
31
|
+
// The bracket is a literal glob metacharacter in this predicate.
|
|
32
|
+
// eslint-disable-next-line no-useless-escape -- Keep the set as `*`, `?`, or `[`.
|
|
33
|
+
return /[*?\[]/.test(value);
|
|
34
|
+
}
|
|
35
|
+
function globSegmentMatches(pattern, value) {
|
|
36
|
+
let expression = "^";
|
|
37
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
38
|
+
const character = pattern[index] ?? "";
|
|
39
|
+
if (character === "*")
|
|
40
|
+
expression += ".*";
|
|
41
|
+
else if (character === "?")
|
|
42
|
+
expression += ".";
|
|
43
|
+
else if (character === "[") {
|
|
44
|
+
const close = pattern.indexOf("]", index + 1);
|
|
45
|
+
if (close > index + 1) {
|
|
46
|
+
let characterClass = pattern.slice(index + 1, close);
|
|
47
|
+
if (characterClass.startsWith("!"))
|
|
48
|
+
characterClass = `^${characterClass.slice(1)}`;
|
|
49
|
+
expression += `[${characterClass}]`;
|
|
50
|
+
index = close;
|
|
51
|
+
}
|
|
52
|
+
else
|
|
53
|
+
expression += "\\[";
|
|
54
|
+
}
|
|
55
|
+
else
|
|
56
|
+
expression += character.replace(/[\\^$+?.()|{}]/g, "\\$&");
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
return new RegExp(`${expression}$`).test(value);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function expandBashGlob(token, cwd) {
|
|
66
|
+
if (!hasGlobPattern(token))
|
|
67
|
+
return [token];
|
|
68
|
+
const absolute = token.startsWith(path.sep);
|
|
69
|
+
const parts = token.split(path.sep).filter((part, index) => !(absolute && index === 0));
|
|
70
|
+
let candidates = [absolute ? path.sep : ""];
|
|
71
|
+
let matchedPattern = false;
|
|
72
|
+
for (const part of parts) {
|
|
73
|
+
if (!hasGlobPattern(part)) {
|
|
74
|
+
candidates = candidates.map((parent) => !parent ? part : parent === path.sep ? `${path.sep}${part}` : `${parent}${path.sep}${part}`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
matchedPattern = true;
|
|
78
|
+
const expanded = [];
|
|
79
|
+
for (const parent of candidates) {
|
|
80
|
+
const directory = absolute ? (parent || path.sep) : path.resolve(cwd, parent || ".");
|
|
81
|
+
let names;
|
|
82
|
+
try {
|
|
83
|
+
names = fs.readdirSync(directory).sort();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
for (const name of names) {
|
|
89
|
+
if (name.startsWith(".") && !part.startsWith("."))
|
|
90
|
+
continue;
|
|
91
|
+
if (!globSegmentMatches(part, name))
|
|
92
|
+
continue;
|
|
93
|
+
expanded.push(!parent ? name : parent === path.sep ? `${path.sep}${name}` : `${parent}${path.sep}${name}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (expanded.length === 0)
|
|
97
|
+
return [token];
|
|
98
|
+
candidates = expanded;
|
|
99
|
+
}
|
|
100
|
+
return matchedPattern ? candidates : [token];
|
|
101
|
+
}
|
|
102
|
+
function bashUnquotedWords(facts, cwd) {
|
|
103
|
+
return facts.join(" ").split(/[\t\n ]+/).filter(Boolean).flatMap((word) => expandBashGlob(word, cwd));
|
|
104
|
+
}
|
|
105
|
+
function commandOnPath(command, env) {
|
|
106
|
+
const pathValue = env.PATH ?? "";
|
|
107
|
+
for (const directory of pathValue.split(path.delimiter)) {
|
|
108
|
+
const candidate = path.join(directory || process.cwd(), command);
|
|
109
|
+
try {
|
|
110
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// PATH probes intentionally ignore missing and inaccessible entries.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
function jqField(value) {
|
|
120
|
+
// jq's `// ""` falls through for null and false only.
|
|
121
|
+
return value === null || value === undefined || value === false ? "" : value;
|
|
122
|
+
}
|
|
123
|
+
function jqText(value) {
|
|
124
|
+
const selected = jqField(value);
|
|
125
|
+
if (typeof selected === "string")
|
|
126
|
+
return selected;
|
|
127
|
+
if (typeof selected === "boolean")
|
|
128
|
+
return selected ? "true" : "";
|
|
129
|
+
if (typeof selected === "number" && Number.isFinite(selected)) {
|
|
130
|
+
throw new ShellFallbackRequired("numeric jq tostring requires shell serialization");
|
|
131
|
+
}
|
|
132
|
+
if (typeof selected === "object") {
|
|
133
|
+
if (selected === null)
|
|
134
|
+
return "";
|
|
135
|
+
throw new ShellFallbackRequired("compound jq tostring requires shell serialization");
|
|
136
|
+
}
|
|
137
|
+
return "";
|
|
138
|
+
}
|
|
139
|
+
function nestedField(record, first, second) {
|
|
140
|
+
const parent = jqField(record[first]);
|
|
141
|
+
if (parent === "")
|
|
142
|
+
return "";
|
|
143
|
+
if (typeof parent !== "object" || parent === null || Array.isArray(parent)) {
|
|
144
|
+
throw new ShellFallbackRequired(`unexpected ${first} payload shape`);
|
|
145
|
+
}
|
|
146
|
+
return jqField(parent[second]);
|
|
147
|
+
}
|
|
148
|
+
function parsePayload(raw) {
|
|
149
|
+
let parsed;
|
|
150
|
+
try {
|
|
151
|
+
parsed = JSON.parse(raw);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
throw new ShellFallbackRequired("malformed hook JSON");
|
|
155
|
+
}
|
|
156
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
157
|
+
throw new ShellFallbackRequired("unexpected hook payload shape");
|
|
158
|
+
}
|
|
159
|
+
const record = parsed;
|
|
160
|
+
const toolResponse = jqField(record.tool_response);
|
|
161
|
+
const fields = {
|
|
162
|
+
tool: jqText(record.tool_name),
|
|
163
|
+
command: jqText(nestedField(record, "tool_input", "command")),
|
|
164
|
+
runInBackground: jqText(nestedField(record, "tool_input", "run_in_background")),
|
|
165
|
+
toolResponse: typeof toolResponse === "string" ? toolResponse : jqText(toolResponse),
|
|
166
|
+
prompt: jqText(record.prompt),
|
|
167
|
+
cwd: jqText(record.cwd),
|
|
168
|
+
sessionId: jqText(record.session_id),
|
|
169
|
+
transcriptPath: jqText(record.transcript_path),
|
|
170
|
+
};
|
|
171
|
+
if (Object.values(fields).some((field) => field.includes("\0"))) {
|
|
172
|
+
throw new ShellFallbackRequired("NUL-delimited jq fields require shell semantics");
|
|
173
|
+
}
|
|
174
|
+
return fields;
|
|
175
|
+
}
|
|
176
|
+
function matchTextFacts(rawText, env) {
|
|
177
|
+
const text = localeLower(rawText, env);
|
|
178
|
+
const facts = [];
|
|
179
|
+
const words = /[a-z][a-z0-9_-]+/g;
|
|
180
|
+
for (const match of text.matchAll(words))
|
|
181
|
+
facts.push(match[0]);
|
|
182
|
+
if (/(^|[^a-z0-9_])aws_profile/.test(text) || /op:\/\/|\.env([^a-z0-9]|$)/.test(text)) {
|
|
183
|
+
facts.push("secret");
|
|
184
|
+
}
|
|
185
|
+
if (/(^|[^a-z0-9_])(main|master|staging|production)([^a-z0-9_]|$)/.test(text) || /release\//.test(text)) {
|
|
186
|
+
facts.push("shared_branch");
|
|
187
|
+
}
|
|
188
|
+
if (/(^|[^a-z0-9])sk-[a-z0-9]/.test(text) ||
|
|
189
|
+
/(^|[^a-z0-9])(gh[opsur]_|github_pat_)[a-z0-9_]/.test(text) ||
|
|
190
|
+
/(^|[^a-z0-9])akia[a-z0-9][a-z0-9]/.test(text) ||
|
|
191
|
+
/(^|[^a-z0-9])xox[bpsa]-[a-z0-9]/.test(text) ||
|
|
192
|
+
/(^|[^a-z0-9])glpat-[a-z0-9]/.test(text) ||
|
|
193
|
+
/-----begin[a-z -]*private key/.test(text) ||
|
|
194
|
+
/(^|[^a-z0-9])bearer [a-z0-9._-][a-z0-9._-][a-z0-9._-]/.test(text)) {
|
|
195
|
+
facts.push("apikey", "secret");
|
|
196
|
+
}
|
|
197
|
+
if (/successfully (merged|deployed|pushed|published|created)/.test(text) ||
|
|
198
|
+
/(deployment|deploy|build|release) (complete|completed|succeeded|ready)/.test(text) ||
|
|
199
|
+
/merged pull request/.test(text) ||
|
|
200
|
+
/pull request #?[0-9]+ [\s\S]* merged/.test(text)) {
|
|
201
|
+
facts.push("completed");
|
|
202
|
+
}
|
|
203
|
+
const filenames = /\.?[a-z0-9_][a-z0-9_./-]*\.[a-z][a-z0-9]+/g;
|
|
204
|
+
for (const match of text.matchAll(filenames)) {
|
|
205
|
+
const filename = match[0].slice(match[0].lastIndexOf("/") + 1);
|
|
206
|
+
const dot = filename.lastIndexOf(".");
|
|
207
|
+
facts.push(`.${filename.slice(dot + 1)}`, filename);
|
|
208
|
+
}
|
|
209
|
+
const slashCommands = / \/[a-z][a-z0-9-]*/g;
|
|
210
|
+
for (const match of `${" "}${text}`.matchAll(slashCommands)) {
|
|
211
|
+
facts.push(match[0].slice(1));
|
|
212
|
+
}
|
|
213
|
+
return facts;
|
|
214
|
+
}
|
|
215
|
+
function isSharedBranch(branch, env) {
|
|
216
|
+
const value = localeLower(branch, env);
|
|
217
|
+
return /(^|[^a-z0-9_])(main|master|staging|production)([^a-z0-9_]|$)/.test(value) || /release\//.test(value);
|
|
218
|
+
}
|
|
219
|
+
function normalizeFacts(facts, branch, cwd, env) {
|
|
220
|
+
const seen = new Set();
|
|
221
|
+
const output = [];
|
|
222
|
+
const expandedText = bashUnquotedWords(facts, cwd).join("\n");
|
|
223
|
+
const lower = localeLower(expandedText, env);
|
|
224
|
+
// AWK's [[:space:]] splitter recognizes these bytes after Bash has split and
|
|
225
|
+
// pathname-expanded the unquoted FACTS parameter expansion.
|
|
226
|
+
for (const token of lower.split(/[\t\n\v\f\r ]+/).filter(Boolean)) {
|
|
227
|
+
if (seen.has(token))
|
|
228
|
+
continue;
|
|
229
|
+
seen.add(token);
|
|
230
|
+
output.push(token);
|
|
231
|
+
}
|
|
232
|
+
if (isSharedBranch(branch, env) && !seen.has("shared_branch"))
|
|
233
|
+
output.push("shared_branch");
|
|
234
|
+
return output.join(" ");
|
|
235
|
+
}
|
|
236
|
+
function companyFromMeta(file) {
|
|
237
|
+
let contents;
|
|
238
|
+
try {
|
|
239
|
+
if (!fs.statSync(file).isFile())
|
|
240
|
+
return "";
|
|
241
|
+
contents = fs.readFileSync(file, "utf8");
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return "";
|
|
245
|
+
}
|
|
246
|
+
for (const line of contents.split(/\n/)) {
|
|
247
|
+
const fields = line.trim().split(/[\t\v\f\r ]+/);
|
|
248
|
+
if (fields[0] !== "company_slug:")
|
|
249
|
+
continue;
|
|
250
|
+
let value = line.replace(/^[^:]+:[\t\n\v\f\r ]*/, "");
|
|
251
|
+
value = value.replace(/^"|"$/g, "");
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
return "";
|
|
255
|
+
}
|
|
256
|
+
function resolveCompanyFact(payloadCwd, sessionId, hqRoot, fallbackCwd, env) {
|
|
257
|
+
let scope = env.HQ_POLICY_COMPANY ?? "";
|
|
258
|
+
const cwd = payloadCwd || env.CLAUDE_PROJECT_DIR || fallbackCwd;
|
|
259
|
+
if (!scope) {
|
|
260
|
+
const marker = cwd.lastIndexOf("companies/");
|
|
261
|
+
if (marker >= 0)
|
|
262
|
+
scope = cwd.slice(marker + "companies/".length).split("/")[0] ?? "";
|
|
263
|
+
}
|
|
264
|
+
if (!scope && sessionId) {
|
|
265
|
+
const metaPath = path.join(hqRoot, "workspace", "sessions", sessionId, "meta.yaml");
|
|
266
|
+
scope = companyFromMeta(metaPath);
|
|
267
|
+
}
|
|
268
|
+
return Boolean(scope);
|
|
269
|
+
}
|
|
270
|
+
function readBranch(cwd, env) {
|
|
271
|
+
const result = spawnSync("git", ["-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"], {
|
|
272
|
+
env: { ...process.env, ...env },
|
|
273
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
274
|
+
});
|
|
275
|
+
if (result.error || result.status !== 0)
|
|
276
|
+
return "";
|
|
277
|
+
return Buffer.from(result.stdout ?? "").toString("utf8").replace(/\n+$/, "");
|
|
278
|
+
}
|
|
279
|
+
function transcriptAssistantText(transcriptPath) {
|
|
280
|
+
let stat;
|
|
281
|
+
let raw;
|
|
282
|
+
try {
|
|
283
|
+
stat = fs.statSync(transcriptPath);
|
|
284
|
+
if (!stat.isFile())
|
|
285
|
+
return "";
|
|
286
|
+
if (stat.size > TRANSCRIPT_TAIL_MAX_BYTES + 1) {
|
|
287
|
+
throw new ShellFallbackRequired("bounded transcript tail requires shell semantics");
|
|
288
|
+
}
|
|
289
|
+
raw = fs.readFileSync(transcriptPath);
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
if (error instanceof ShellFallbackRequired)
|
|
293
|
+
throw error;
|
|
294
|
+
throw new ShellFallbackRequired("transcript could not be read exactly");
|
|
295
|
+
}
|
|
296
|
+
if (raw.includes(0))
|
|
297
|
+
throw new ShellFallbackRequired("NUL transcript needs jq semantics");
|
|
298
|
+
const records = [];
|
|
299
|
+
for (const line of raw.toString("utf8").split(/\n/)) {
|
|
300
|
+
if (!line.trim())
|
|
301
|
+
continue;
|
|
302
|
+
let parsed;
|
|
303
|
+
try {
|
|
304
|
+
parsed = JSON.parse(line);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
throw new ShellFallbackRequired("malformed transcript JSON");
|
|
308
|
+
}
|
|
309
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
310
|
+
throw new ShellFallbackRequired("transcript line requires jq object indexing");
|
|
311
|
+
}
|
|
312
|
+
const event = parsed;
|
|
313
|
+
const typeValue = jqField(event.type);
|
|
314
|
+
const eventType = typeof typeValue === "string" ? typeValue : typeValue === true ? "true" : "";
|
|
315
|
+
const message = event.message;
|
|
316
|
+
if (message !== null && message !== undefined && (typeof message !== "object" || Array.isArray(message))) {
|
|
317
|
+
throw new ShellFallbackRequired("transcript message requires jq object indexing");
|
|
318
|
+
}
|
|
319
|
+
let content = message?.content;
|
|
320
|
+
if (content === null || content === undefined || content === false)
|
|
321
|
+
content = event.content;
|
|
322
|
+
let text = "";
|
|
323
|
+
if (Array.isArray(content)) {
|
|
324
|
+
const selected = [];
|
|
325
|
+
for (const item of content) {
|
|
326
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
327
|
+
throw new ShellFallbackRequired("transcript content item requires jq object indexing");
|
|
328
|
+
}
|
|
329
|
+
if (item.type !== "text")
|
|
330
|
+
continue;
|
|
331
|
+
const itemText = item.text;
|
|
332
|
+
if (itemText === null || itemText === undefined)
|
|
333
|
+
selected.push("");
|
|
334
|
+
else if (typeof itemText === "string")
|
|
335
|
+
selected.push(itemText);
|
|
336
|
+
else
|
|
337
|
+
throw new ShellFallbackRequired("transcript content requires jq join semantics");
|
|
338
|
+
}
|
|
339
|
+
text = selected.join(" ");
|
|
340
|
+
}
|
|
341
|
+
else if (typeof content === "string") {
|
|
342
|
+
text = content;
|
|
343
|
+
}
|
|
344
|
+
if (eventType === "user")
|
|
345
|
+
records.length = 0;
|
|
346
|
+
else if (eventType === "assistant") {
|
|
347
|
+
records.push(...(records.length ? [" "] : []), text);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return records.join("").replace(/\n+$/, "");
|
|
351
|
+
}
|
|
352
|
+
function computeOutput(options) {
|
|
353
|
+
const args = [...options.argv];
|
|
354
|
+
if (args.length === 0 || args.length > 2)
|
|
355
|
+
throw new ShellFallbackRequired("unsupported argv");
|
|
356
|
+
const event = args[0] ?? "";
|
|
357
|
+
const withAssistantIntent = args[1] === "--with-assistant-intent";
|
|
358
|
+
if (args.length === 2 && !withAssistantIntent)
|
|
359
|
+
throw new ShellFallbackRequired("unsupported argv");
|
|
360
|
+
if (!["PreToolUse", "PostToolUse", "UserPromptSubmit", "AssistantIntent"].includes(event)) {
|
|
361
|
+
throw new ShellFallbackRequired("unsupported event");
|
|
362
|
+
}
|
|
363
|
+
const paired = withAssistantIntent && (event === "PreToolUse" || event === "UserPromptSubmit");
|
|
364
|
+
const env = effectiveEnvironment(options);
|
|
365
|
+
const jqAvailable = options.jqAvailable ?? commandOnPath("jq", env);
|
|
366
|
+
let payload = {
|
|
367
|
+
tool: "", command: "", runInBackground: "", toolResponse: "", prompt: "",
|
|
368
|
+
cwd: "", sessionId: "", transcriptPath: "",
|
|
369
|
+
};
|
|
370
|
+
if (jqAvailable)
|
|
371
|
+
payload = parsePayload(options.stdin || "{}");
|
|
372
|
+
const payloadCwd = payload.cwd || env.CLAUDE_PROJECT_DIR || options.hqRoot;
|
|
373
|
+
const company = resolveCompanyFact(payload.cwd, payload.sessionId, options.hqRoot, options.hqRoot, env);
|
|
374
|
+
const derive = (kind) => {
|
|
375
|
+
const facts = ["always"];
|
|
376
|
+
let branch = "";
|
|
377
|
+
if (kind === "PreToolUse" || kind === "PostToolUse") {
|
|
378
|
+
if (payload.tool === "Bash") {
|
|
379
|
+
if (kind === "PreToolUse") {
|
|
380
|
+
facts.push(...matchTextFacts(payload.command, env));
|
|
381
|
+
if (payload.runInBackground === "true")
|
|
382
|
+
facts.push("run_in_background");
|
|
383
|
+
}
|
|
384
|
+
else
|
|
385
|
+
facts.push(...matchTextFacts(payload.toolResponse, env));
|
|
386
|
+
}
|
|
387
|
+
else if (payload.tool !== "") {
|
|
388
|
+
if (kind === "PreToolUse")
|
|
389
|
+
facts.push(localeLower(payload.tool, env));
|
|
390
|
+
else
|
|
391
|
+
facts.push(...matchTextFacts(payload.toolResponse, env));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
else if (kind === "UserPromptSubmit") {
|
|
395
|
+
facts.push(...matchTextFacts(payload.prompt, env));
|
|
396
|
+
}
|
|
397
|
+
if (company)
|
|
398
|
+
facts.push("company");
|
|
399
|
+
if (kind === "AssistantIntent") {
|
|
400
|
+
if (payload.transcriptPath && jqAvailable) {
|
|
401
|
+
try {
|
|
402
|
+
if (fs.statSync(payload.transcriptPath).isFile()) {
|
|
403
|
+
const lookback = transcriptAssistantText(payload.transcriptPath);
|
|
404
|
+
if (lookback)
|
|
405
|
+
facts.push(...matchTextFacts(lookback, env));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
if (error instanceof ShellFallbackRequired)
|
|
410
|
+
throw error;
|
|
411
|
+
// The shell checks -f before invoking jq; missing files add no facts.
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
if (payloadCwd.includes("repos/public/") || payloadCwd.includes("repos/private/"))
|
|
417
|
+
facts.push("repo");
|
|
418
|
+
branch = options.gitBranch ?? readBranch(payloadCwd, env);
|
|
419
|
+
}
|
|
420
|
+
return normalizeFacts(facts, branch, options.cwd, env);
|
|
421
|
+
};
|
|
422
|
+
const primary = derive(event);
|
|
423
|
+
return paired ? `${primary}\n${derive("AssistantIntent")}\n` : `${primary}\n`;
|
|
424
|
+
}
|
|
425
|
+
function shellFallback(options) {
|
|
426
|
+
const env = { ...process.env, ...(options.env ?? {}) };
|
|
427
|
+
delete env.HQ_ROOT;
|
|
428
|
+
delete env.CLAUDE_PROJECT_DIR;
|
|
429
|
+
const result = runBundledScript({
|
|
430
|
+
asset: DERIVE_TRIGGER_FACTS_ASSET,
|
|
431
|
+
args: [...options.argv],
|
|
432
|
+
cwd: options.hqRoot,
|
|
433
|
+
hqRoot: options.hqRoot,
|
|
434
|
+
stdin: options.stdin,
|
|
435
|
+
captureOutput: true,
|
|
436
|
+
env,
|
|
437
|
+
});
|
|
438
|
+
return {
|
|
439
|
+
code: result.code,
|
|
440
|
+
stdout: result.stdout ?? Buffer.alloc(0),
|
|
441
|
+
stderr: result.stderr ?? Buffer.alloc(0),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function missingSupportLibrary(hqRoot) {
|
|
445
|
+
const required = [
|
|
446
|
+
[
|
|
447
|
+
"core/scripts/lib/trigger-fact-text.awk",
|
|
448
|
+
"ERROR: derive-trigger-facts: missing core/scripts/lib/trigger-fact-text.awk",
|
|
449
|
+
],
|
|
450
|
+
[
|
|
451
|
+
"core/scripts/lib/transcript-tail.sh",
|
|
452
|
+
"ERROR: derive-trigger-facts: missing core/scripts/lib/transcript-tail.sh",
|
|
453
|
+
],
|
|
454
|
+
];
|
|
455
|
+
for (const [relativePath, message] of required) {
|
|
456
|
+
try {
|
|
457
|
+
fs.accessSync(path.join(hqRoot, relativePath), fs.constants.R_OK);
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
return message;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return undefined;
|
|
464
|
+
}
|
|
465
|
+
/** Run the native handler contract, retaining exact bytes for differential tests. */
|
|
466
|
+
export function runDeriveTriggerFacts(options) {
|
|
467
|
+
const missingLibrary = missingSupportLibrary(options.hqRoot);
|
|
468
|
+
if (missingLibrary) {
|
|
469
|
+
return {
|
|
470
|
+
code: 1,
|
|
471
|
+
stdout: Buffer.alloc(0),
|
|
472
|
+
stderr: Buffer.from(`${missingLibrary}\n`),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
return { code: 0, stdout: Buffer.from(computeOutput(options)), stderr: Buffer.alloc(0) };
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
if (!(error instanceof ShellFallbackRequired))
|
|
480
|
+
throw error;
|
|
481
|
+
return shellFallback(options);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
export function runDeriveTriggerFactsFast(argv, stdin, cwd, hqRoot) {
|
|
485
|
+
const result = runDeriveTriggerFacts({ argv, stdin, cwd, hqRoot: hqRoot ?? cwd });
|
|
486
|
+
if (result.stdout.length)
|
|
487
|
+
process.stdout.write(result.stdout);
|
|
488
|
+
if (result.stderr.length)
|
|
489
|
+
process.stderr.write(result.stderr);
|
|
490
|
+
return result.code;
|
|
491
|
+
}
|
|
492
|
+
//# sourceMappingURL=core-derive-trigger-facts-native.js.map
|
package/dist/commands/core.js
CHANGED
|
@@ -46,6 +46,8 @@ 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 { registerCoreDeriveTriggerFactsCommand } from "./core-derive-trigger-facts-command.js";
|
|
50
|
+
import { DERIVE_TRIGGER_FACTS_ASSET } from "./core-derive-trigger-facts-native.js";
|
|
49
51
|
import { registerCoreHookStateHelpers } from "./core-hook-state-helpers-native.js";
|
|
50
52
|
import { registerCoreHookBatchCommand } from "./core-hook-batch-command.js";
|
|
51
53
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
@@ -253,6 +255,9 @@ export const SCAFFOLD_ONLY_ASSETS = [
|
|
|
253
255
|
{ asset: "core/scripts/lib/work-mesh-enqueue.sh", root: "live", summary: "Work Mesh Live spool enqueue lib sourced by hooks" },
|
|
254
256
|
{ asset: "core/scripts/lib/work-mesh-live-hook.sh", root: "live", summary: "Work Mesh Live hook helpers sourced by rebind" },
|
|
255
257
|
{ asset: "core/scripts/lib/work-mesh-live-rebind.sh", root: "live", summary: "Work Mesh Live session rebind library sourced by hooks and the native rollback command" },
|
|
258
|
+
{ asset: DERIVE_TRIGGER_FACTS_ASSET, root: "live", summary: "Rollback companion for native derive-trigger-facts" },
|
|
259
|
+
{ asset: "core/scripts/lib/trigger-fact-text.awk", root: "live", summary: "Shared trigger fact tokenizer used by hooks" },
|
|
260
|
+
{ asset: "core/scripts/lib/transcript-tail.sh", root: "live", summary: "Bounded transcript reader used by trigger helper rollback" },
|
|
256
261
|
// Wave 2 handoff/worktree commands retain their old scripts for one release,
|
|
257
262
|
// behind the shared default-off rollback flag.
|
|
258
263
|
{ asset: WORKTREE_GC_ASSET, root: "live", summary: "Rollback companion for native worktree-gc" },
|
|
@@ -397,6 +402,7 @@ export function registerCoreCommands(program) {
|
|
|
397
402
|
registerCoreDmBindNative(core);
|
|
398
403
|
registerCoreWorkMeshBindTrustedNative(core);
|
|
399
404
|
registerCoreHookHelperNativeCommands(core);
|
|
405
|
+
registerCoreDeriveTriggerFactsCommand(core);
|
|
400
406
|
registerCoreHookStateHelpers(core);
|
|
401
407
|
registerCoreHookBatchCommand(core);
|
|
402
408
|
const sessionHelperCommands = [
|
package/dist/commands/daemon.js
CHANGED
|
@@ -632,7 +632,7 @@ async function runServiceSelfUpdate(d) {
|
|
|
632
632
|
tools: TOOL_NAMES.map((name) => hostToolUpdater(name, env, d.home)),
|
|
633
633
|
updateHqCloud: async () => updateManagedHqCloud(paths.root, env),
|
|
634
634
|
hqCloudUnits: hqCloudUnits(detected.type, enabledServices(detected.type, readDaemonConfig(paths.configPath))),
|
|
635
|
-
requestDaemonRestart: () => writeControlRequest(paths.requestsDir, { action: "restart-daemon" }),
|
|
635
|
+
requestDaemonRestart: () => writeControlRequest(paths.requestsDir, { action: "restart-daemon", deferUntilSyncIdle: true }),
|
|
636
636
|
requestRestart: (unit) => writeControlRequest(paths.requestsDir, { action: "restart", unit }),
|
|
637
637
|
log: d.out,
|
|
638
638
|
});
|
package/dist/fast-scaffold.js
CHANGED
|
@@ -238,6 +238,19 @@ export const FAST_SCAFFOLD_SCRIPTS = [
|
|
|
238
238
|
flagKey: "hq-core-native-fast-route",
|
|
239
239
|
familyFlagKey: "core-native-hook-helpers",
|
|
240
240
|
})),
|
|
241
|
+
{
|
|
242
|
+
name: "derive-trigger-facts",
|
|
243
|
+
kind: "native",
|
|
244
|
+
modulePath: "./commands/core-derive-trigger-facts-native.js",
|
|
245
|
+
exportName: "runDeriveTriggerFactsFast",
|
|
246
|
+
flagKey: "hq-core-native-fast-route",
|
|
247
|
+
familyFlagKey: "core-native-hook-helpers",
|
|
248
|
+
asset: "core/scripts/derive-trigger-facts.sh",
|
|
249
|
+
root: "live",
|
|
250
|
+
shellCwd: "live",
|
|
251
|
+
shellHqRoot: "live",
|
|
252
|
+
readsStdin: true,
|
|
253
|
+
},
|
|
241
254
|
...["resolve", "pickup", "repo", "secrets", "transfer"].map((name) => ({
|
|
242
255
|
name: `delegate ${name}`,
|
|
243
256
|
kind: "native",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const CLI_INVITE_NUDGE_FLAG = "cli.first-sync-invite-teammate-v1";
|
|
2
|
+
export type CliInviteNudgeOutcome = "shown" | "clicked" | "dismissed";
|
|
3
|
+
export interface CliInviteNudgeInput {
|
|
4
|
+
companyUid: string;
|
|
5
|
+
companyRef: string;
|
|
6
|
+
interactive: boolean;
|
|
7
|
+
canPrompt: boolean;
|
|
8
|
+
alreadyShown: boolean;
|
|
9
|
+
flagEnabled: () => Promise<boolean>;
|
|
10
|
+
readHumanMemberCount: () => Promise<number>;
|
|
11
|
+
claimShown: () => boolean;
|
|
12
|
+
ask: (prompt: string) => Promise<boolean>;
|
|
13
|
+
write: (line: string) => void;
|
|
14
|
+
record: (companyUid: string, outcome: CliInviteNudgeOutcome) => void;
|
|
15
|
+
reportFailure?: (operation: string, error: unknown) => void;
|
|
16
|
+
}
|
|
17
|
+
/** One eligible, successful work-bearing company sync can show one prompt per company. */
|
|
18
|
+
export declare function runCliInviteNudge(input: CliInviteNudgeInput): Promise<boolean>;
|
|
19
|
+
/** Called after a successful work-bearing company sync; every dependency remains fail-closed. */
|
|
20
|
+
export declare function maybeShowCliInviteNudge(input: {
|
|
21
|
+
companyUid?: string;
|
|
22
|
+
companyRef?: string;
|
|
23
|
+
accessToken: string;
|
|
24
|
+
}): Promise<boolean>;
|
|
25
|
+
//# sourceMappingURL=cli-invite-nudge.d.ts.map
|