@indigoai-us/hq-cli 5.345.54 → 5.345.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/assets/scaffold/core/scripts/derive-trigger-facts.sh +10 -1
  3. package/assets/scaffold/core/scripts/jobs-validate.sh +2 -1
  4. package/assets/scaffold/core/scripts/session-title.sh +18 -15
  5. package/dist/command-catalog.generated.d.ts +143 -2
  6. package/dist/command-catalog.generated.js +175 -2
  7. package/dist/command-registration-plan.d.ts +6 -1
  8. package/dist/command-registration-plan.js +2 -1
  9. package/dist/commands/cloud.d.ts +7 -0
  10. package/dist/commands/cloud.js +92 -16
  11. package/dist/commands/core-delegate-w1b.js +18 -5
  12. package/dist/commands/core-derive-trigger-facts-command.js +14 -0
  13. package/dist/commands/core-derive-trigger-facts-native.d.ts +1 -0
  14. package/dist/commands/core-derive-trigger-facts-native.js +20 -1
  15. package/dist/commands/daemon.js +16 -0
  16. package/dist/commands/doctor.d.ts +1 -0
  17. package/dist/commands/doctor.js +15 -2
  18. package/dist/commands/import-scan.d.ts +24 -0
  19. package/dist/commands/import-scan.js +75 -0
  20. package/dist/commands/lanes-checks.d.ts +3 -0
  21. package/dist/commands/lanes-checks.js +34 -0
  22. package/dist/commands/lanes-session-start.d.ts +17 -0
  23. package/dist/commands/lanes-session-start.js +195 -0
  24. package/dist/commands/lanes-shared.d.ts +2 -1
  25. package/dist/commands/lanes-watch.d.ts +2 -0
  26. package/dist/commands/lanes-watch.js +2 -1
  27. package/dist/commands/lanes.d.ts +3 -1
  28. package/dist/commands/lanes.js +121 -18
  29. package/dist/commands/members.d.ts +4 -0
  30. package/dist/commands/members.js +23 -2
  31. package/dist/lanes-drain-hook.js +6 -2
  32. package/dist/lib/daemon/conflict-notices.d.ts +4 -0
  33. package/dist/lib/daemon/conflict-notices.js +80 -0
  34. package/dist/lib/doctor/checks/sync-health.d.ts +2 -0
  35. package/dist/lib/doctor/checks/sync-health.js +47 -23
  36. package/dist/lib/import-scan/events.d.ts +135 -0
  37. package/dist/lib/import-scan/events.js +71 -0
  38. package/dist/lib/import-scan/run.d.ts +84 -0
  39. package/dist/lib/import-scan/run.js +308 -0
  40. package/dist/lib/import-scan/scan-event.schema.json +127 -0
  41. package/dist/lib/import-scan/validate.d.ts +31 -0
  42. package/dist/lib/import-scan/validate.js +111 -0
  43. package/dist/lib/lanes/dropbox.d.ts +4 -2
  44. package/dist/lib/lanes/dropbox.js +54 -9
  45. package/dist/lib/lanes/flag-keys.d.ts +10 -0
  46. package/dist/lib/lanes/flag-keys.js +10 -0
  47. package/dist/lib/lanes/hooks-install.d.ts +14 -11
  48. package/dist/lib/lanes/hooks-install.js +70 -50
  49. package/dist/lib/lanes/monitor.js +1 -1
  50. package/dist/lib/lanes/reaper.d.ts +58 -0
  51. package/dist/lib/lanes/reaper.js +403 -0
  52. package/dist/lib/lanes/registry-flag.d.ts +2 -0
  53. package/dist/lib/lanes/registry-flag.js +1 -1
  54. package/dist/lib/lanes/spawn.d.ts +2 -0
  55. package/dist/lib/lanes/spawn.js +32 -2
  56. package/dist/lib/lanes/status-fields.d.ts +12 -0
  57. package/dist/lib/lanes/status-fields.js +189 -0
  58. package/dist/lib/lanes/types.js +1 -1
  59. package/dist/lib/lanes/worker-continuity.d.ts +18 -0
  60. package/dist/lib/lanes/worker-continuity.js +81 -0
  61. package/dist/lib/lanes/worker-process.d.ts +4 -0
  62. package/dist/lib/lanes/worker-process.js +77 -0
  63. package/dist/main.js +9 -3
  64. package/dist/utils/cli-telemetry.d.ts +4 -1
  65. package/dist/utils/cli-telemetry.js +8 -0
  66. package/dist/utils/jq-windows-compat.d.ts +13 -0
  67. package/dist/utils/jq-windows-compat.js +48 -0
  68. package/dist/utils/run-bundled-script.d.ts +3 -0
  69. package/dist/utils/run-bundled-script.js +9 -80
  70. package/dist/utils/windows-bash.d.ts +5 -0
  71. package/dist/utils/windows-bash.js +75 -0
  72. package/package.json +1 -1
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Runs the HQ import-context scanner with `--progress-json` and relays its
3
+ * JSON Lines event stream (contract: ./events.ts).
4
+ *
5
+ * The scanner is the HQ tree's own copy at
6
+ * `<hq root>/.claude/skills/import-context/scan.sh`, the same place the
7
+ * /import-context skill runs it from. The scan is local only: the scanner
8
+ * reads the filesystem and writes one report.json; nothing here touches the
9
+ * network.
10
+ *
11
+ * HQ root: `--hq-root` when given, else the root the HQ installer recorded in
12
+ * `~/.hq/menubar.json` (`hqPath`). Nothing else is trusted: no walk up from
13
+ * the current folder and no `$HQ_ROOT`, because this command runs a script
14
+ * from that tree.
15
+ *
16
+ * Output modes:
17
+ * stream every valid event, rebuilt from its v1 fields (invalid lines are
18
+ * dropped with a warning on stderr that never echoes the line)
19
+ * final only the final `done` object
20
+ * human short progress lines and a summary
21
+ *
22
+ * The `done` event is held until the scanner exits. When the scan cannot run
23
+ * or does not finish cleanly (HQ not found, scanner missing or too old,
24
+ * scanner exited non-zero or without `done`, cancelled), an `error` event
25
+ * (source "scanner") comes first, then `done` (zeroed with `report: null`
26
+ * unless the scanner produced one), and the command exits non-zero.
27
+ *
28
+ * Process handling: the scanner runs in its own process group. SIGINT/SIGTERM
29
+ * to hq-cli are forwarded to the whole group and escalated to SIGKILL after
30
+ * `killTimeoutMs`. When the scanner exits, anything still left in its group
31
+ * is terminated so an orphan holding stdout open cannot stall the command.
32
+ *
33
+ * Scanner stderr is not forwarded verbatim: each line is passed through with
34
+ * paths and token-like strings replaced, at most MAX_STDERR_LINES lines, then
35
+ * a count of the rest.
36
+ */
37
+ import { spawn } from "node:child_process";
38
+ import * as fs from "node:fs";
39
+ import * as os from "node:os";
40
+ import * as path from "node:path";
41
+ import * as readline from "node:readline";
42
+ import { SCAN_CLI_ERROR_SOURCE, SCAN_EVENT_VERSION, } from "./events.js";
43
+ import { parseScanEventLine } from "./validate.js";
44
+ export const SCANNER_RELATIVE_PATH = path.join(".claude", "skills", "import-context", "scan.sh");
45
+ const MAX_STDERR_LINES = 20;
46
+ const DEFAULT_KILL_TIMEOUT_MS = 5000;
47
+ const DRAIN_TIMEOUT_MS = 2000;
48
+ /** UTC scan id in the /import-context skill's format: YYYYMMDDTHHMMSSZ. */
49
+ export function scanId(date) {
50
+ return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
51
+ }
52
+ export class HqRootError extends Error {
53
+ }
54
+ /**
55
+ * --hq-root, else the installer-recorded root in ~/.hq/menubar.json. Throws
56
+ * HqRootError with a plain-words message otherwise.
57
+ */
58
+ export function resolveScanHqRoot(opts) {
59
+ if (opts.hqRoot) {
60
+ const root = path.resolve(opts.hqRoot);
61
+ if (!isDirectory(root))
62
+ throw new HqRootError("The folder given with --hq-root does not exist.");
63
+ return root;
64
+ }
65
+ const menubar = path.join(opts.home ?? os.homedir(), ".hq", "menubar.json");
66
+ let recorded;
67
+ try {
68
+ recorded = JSON.parse(fs.readFileSync(menubar, "utf8")).hqPath;
69
+ }
70
+ catch {
71
+ recorded = undefined;
72
+ }
73
+ if (typeof recorded !== "string" || !path.isAbsolute(recorded) || !isDirectory(recorded)) {
74
+ throw new HqRootError("Could not find your HQ folder. Pass --hq-root <path>, or open the HQ app once so it records where HQ is installed.");
75
+ }
76
+ return path.resolve(recorded);
77
+ }
78
+ function isDirectory(p) {
79
+ try {
80
+ return fs.statSync(p).isDirectory();
81
+ }
82
+ catch {
83
+ return false;
84
+ }
85
+ }
86
+ /**
87
+ * Replace paths and token-like strings in one scanner stderr line. A path
88
+ * runs from its first "/" (or a leading ~ / $HOME) to the next quote or the
89
+ * end of the line, because folder names may contain spaces: stopping at
90
+ * whitespace would leak the rest of "/Users/x/My Project/secret".
91
+ */
92
+ export function redactScannerLine(line) {
93
+ return line
94
+ .replace(/(?<![^\s'"`=:([])(?:\$HOME|~)?\/[^'"`]*/g, "<path>")
95
+ .replace(/\b[A-Za-z0-9_+=-]{24,}\b/g, "<redacted>")
96
+ .replace(/\S+@\S+/g, "<redacted>")
97
+ .slice(0, 300);
98
+ }
99
+ function zeroDone() {
100
+ return {
101
+ v: SCAN_EVENT_VERSION,
102
+ type: "done",
103
+ report: null,
104
+ summary: { companies: 0, projects: 0, sessions: 0 },
105
+ };
106
+ }
107
+ function cliError(message, code) {
108
+ return { v: SCAN_EVENT_VERSION, type: "error", source: SCAN_CLI_ERROR_SOURCE, message, code };
109
+ }
110
+ const failed = (message) => ({ message, code: "scan_failed" });
111
+ const outdated = (message) => ({ message, code: "scanner_outdated" });
112
+ function humanLine(event, labels) {
113
+ switch (event.type) {
114
+ case "source": {
115
+ const label = labels.get(event.id) ?? event.id;
116
+ if (event.status === "scanning")
117
+ return `Scanning ${label}...`;
118
+ if (event.status === "done") {
119
+ const parts = Object.entries(event.counts ?? {})
120
+ .filter(([, n]) => n > 0)
121
+ .map(([k, n]) => `${n} ${k.replace(/_/g, " ")}`);
122
+ return ` ${label}: ${parts.length ? parts.join(", ") : "nothing found"}`;
123
+ }
124
+ return ` ${label}: ${event.status}${event.message ? ` - ${event.message}` : ""}`;
125
+ }
126
+ case "company":
127
+ return ` company: ${event.name}`;
128
+ case "error":
129
+ return ` warning: ${event.message}`;
130
+ case "done":
131
+ return (`Found ${event.summary.companies} companies, ${event.summary.projects} projects, ` +
132
+ `${event.summary.sessions} sessions.` + (event.report ? `\nReport: ${event.report}` : ""));
133
+ default:
134
+ return undefined;
135
+ }
136
+ }
137
+ function signalGroup(child, signal) {
138
+ if (child.pid === undefined)
139
+ return;
140
+ try {
141
+ process.kill(-child.pid, signal);
142
+ }
143
+ catch {
144
+ try {
145
+ child.kill(signal);
146
+ }
147
+ catch {
148
+ // already gone
149
+ }
150
+ }
151
+ }
152
+ /** Run the scan. Resolves to the process exit code. */
153
+ export async function runImportScan(opts, io) {
154
+ const labels = new Map();
155
+ const write = (event) => {
156
+ if (event.type === "start")
157
+ for (const s of event.sources)
158
+ labels.set(s.id, s.label);
159
+ if (opts.mode === "stream") {
160
+ io.stdout.write(`${JSON.stringify(event)}\n`);
161
+ }
162
+ else if (opts.mode === "human") {
163
+ const line = humanLine(event, labels);
164
+ if (line)
165
+ io.stdout.write(`${line}\n`);
166
+ }
167
+ };
168
+ const finish = (done, error, code = 1) => {
169
+ if (error)
170
+ write(cliError(error.message, error.code));
171
+ write(done);
172
+ if (opts.mode === "final")
173
+ io.stdout.write(`${JSON.stringify(done)}\n`);
174
+ return error ? code : 0;
175
+ };
176
+ let hqRoot;
177
+ try {
178
+ hqRoot = resolveScanHqRoot({ hqRoot: opts.hqRoot, home: opts.home });
179
+ }
180
+ catch (err) {
181
+ return finish(zeroDone(), failed(err instanceof HqRootError ? err.message : "Could not find your HQ folder."));
182
+ }
183
+ const scanner = path.join(hqRoot, SCANNER_RELATIVE_PATH);
184
+ if (!fs.existsSync(scanner)) {
185
+ return finish(zeroDone(), outdated("The import-context scanner is not installed in this HQ. Update HQ and try again."));
186
+ }
187
+ const output = path.resolve(opts.output ??
188
+ path.join(hqRoot, "workspace", "imports", scanId((opts.now ?? (() => new Date()))()), "report.json"));
189
+ const args = [scanner, `--hq-root=${hqRoot}`, `--output=${output}`, "--progress-json"];
190
+ for (const scope of opts.scopes ?? [])
191
+ args.push(`--scope=${scope}`);
192
+ if (opts.claudeExport)
193
+ args.push(`--claude-export=${opts.claudeExport}`);
194
+ if (opts.noDefaultScopes)
195
+ args.push("--no-default-scopes");
196
+ const child = spawn(opts.bash ?? "bash", args, {
197
+ cwd: hqRoot,
198
+ env: opts.env ?? process.env,
199
+ stdio: ["ignore", "pipe", "pipe"],
200
+ detached: true,
201
+ });
202
+ // Signal forwarding to the whole scanner process group, with escalation.
203
+ let cancelledBy;
204
+ let killTimer;
205
+ const onSignal = (signal) => {
206
+ if (!cancelledBy)
207
+ cancelledBy = signal;
208
+ signalGroup(child, signal);
209
+ if (!killTimer) {
210
+ killTimer = setTimeout(() => signalGroup(child, "SIGKILL"), opts.killTimeoutMs ?? DEFAULT_KILL_TIMEOUT_MS);
211
+ killTimer.unref();
212
+ }
213
+ };
214
+ const onInt = () => onSignal("SIGINT");
215
+ const onTerm = () => onSignal("SIGTERM");
216
+ process.on("SIGINT", onInt);
217
+ process.on("SIGTERM", onTerm);
218
+ opts.abortSignal?.addEventListener("abort", onTerm, { once: true });
219
+ // Scanner stderr: redacted, capped.
220
+ let stderrTail = "";
221
+ let stderrShown = 0;
222
+ let stderrHidden = 0;
223
+ const errLines = readline.createInterface({ input: child.stderr, crlfDelay: Infinity });
224
+ errLines.on("line", (line) => {
225
+ stderrTail = (stderrTail + line + "\n").slice(-4096);
226
+ if (!line.trim())
227
+ return;
228
+ if (stderrShown < MAX_STDERR_LINES) {
229
+ stderrShown += 1;
230
+ io.stderr.write(`hq import scan: scanner: ${redactScannerLine(line)}\n`);
231
+ }
232
+ else {
233
+ stderrHidden += 1;
234
+ }
235
+ });
236
+ let lineNo = 0;
237
+ let dropped = 0;
238
+ let heldDone;
239
+ const lines = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
240
+ const consumed = (async () => {
241
+ for await (const raw of lines) {
242
+ lineNo += 1;
243
+ const line = raw.trim();
244
+ if (!line)
245
+ continue;
246
+ if (heldDone) {
247
+ dropped += 1;
248
+ io.stderr.write(`hq import scan: dropped scan event on line ${lineNo}: received after done\n`);
249
+ continue;
250
+ }
251
+ const parsed = parseScanEventLine(line);
252
+ if (!parsed.ok) {
253
+ dropped += 1;
254
+ io.stderr.write(`hq import scan: dropped scan event on line ${lineNo}: ${parsed.reason}\n`);
255
+ continue;
256
+ }
257
+ if (parsed.event.type === "done")
258
+ heldDone = parsed.event;
259
+ else
260
+ write(parsed.event);
261
+ }
262
+ })();
263
+ const exit = await new Promise((resolve) => {
264
+ child.once("error", (error) => resolve({ code: null, signal: null, error }));
265
+ child.once("exit", (code, signal) => resolve({ code, signal }));
266
+ });
267
+ // The scanner is gone; stop anything it left behind so stdout can close.
268
+ signalGroup(child, "SIGTERM");
269
+ const drained = await Promise.race([
270
+ consumed.then(() => true),
271
+ new Promise((r) => setTimeout(() => r(false), DRAIN_TIMEOUT_MS).unref()),
272
+ ]);
273
+ if (!drained) {
274
+ signalGroup(child, "SIGKILL");
275
+ child.stdout?.destroy();
276
+ child.stderr?.destroy();
277
+ lines.close();
278
+ }
279
+ errLines.close();
280
+ if (killTimer)
281
+ clearTimeout(killTimer);
282
+ process.removeListener("SIGINT", onInt);
283
+ process.removeListener("SIGTERM", onTerm);
284
+ opts.abortSignal?.removeEventListener("abort", onTerm);
285
+ if (stderrHidden > 0) {
286
+ io.stderr.write(`hq import scan: ${stderrHidden} more scanner diagnostic line(s) not shown\n`);
287
+ }
288
+ if (dropped > 0 && opts.mode !== "stream") {
289
+ io.stderr.write(`hq import scan: ${dropped} invalid scan event(s) were dropped\n`);
290
+ }
291
+ if (exit.error) {
292
+ return finish(zeroDone(), failed("The scanner could not be started."));
293
+ }
294
+ if (cancelledBy) {
295
+ return finish(heldDone ?? zeroDone(), failed("The scan was cancelled."), cancelledBy === "SIGINT" ? 130 : 143);
296
+ }
297
+ if (!heldDone) {
298
+ if (exit.code === 2 && /unknown arg: --progress-json/.test(stderrTail)) {
299
+ return finish(zeroDone(), outdated("The installed import-context scanner does not support streaming. Update HQ and try again."));
300
+ }
301
+ return finish(zeroDone(), failed(`The scan stopped before it finished (scanner exit ${exit.code ?? exit.signal ?? "unknown"}).`));
302
+ }
303
+ if (exit.code !== 0) {
304
+ return finish(heldDone, failed(`The scanner reported a problem after finishing (exit ${exit.code ?? exit.signal ?? "unknown"}).`));
305
+ }
306
+ return finish(heldDone);
307
+ }
308
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1,127 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://hq.computer/schemas/import-scan/scan-event.v1.schema.json",
4
+ "title": "HQ import scan event (v1)",
5
+ "description": "One line of `hq import scan --json --stream` / `scan.sh --progress-json`. Each type is also addressable as #/$defs/<type>. Unknown extra fields are allowed within v1; hq-cli forwards only the fields listed here.",
6
+ "type": "object",
7
+ "required": ["v", "type"],
8
+ "properties": {
9
+ "v": { "const": 1 },
10
+ "type": { "enum": ["start", "source", "count", "company", "project", "error", "done"] }
11
+ },
12
+ "oneOf": [
13
+ { "$ref": "#/$defs/start" },
14
+ { "$ref": "#/$defs/source" },
15
+ { "$ref": "#/$defs/nonNegative" },
16
+ { "$ref": "#/$defs/company" },
17
+ { "$ref": "#/$defs/project" },
18
+ { "$ref": "#/$defs/error" },
19
+ { "$ref": "#/$defs/done" }
20
+ ],
21
+ "$defs": {
22
+ "name": { "type": "string", "minLength": 1, "maxLength": 512 },
23
+ "nonNegative": { "type": "integer", "minimum": 0 },
24
+ "baseName": {
25
+ "description": "A folder or repository basename: a name without \"/\".",
26
+ "allOf": [{ "$ref": "#/$defs/name" }, { "pattern": "^[^/]+$" }]
27
+ },
28
+ "start": {
29
+ "type": "object",
30
+ "required": ["v", "type", "sources"],
31
+ "properties": {
32
+ "v": { "const": 1 },
33
+ "type": { "const": "start" },
34
+ "sources": {
35
+ "type": "array",
36
+ "items": {
37
+ "type": "object",
38
+ "required": ["id", "label"],
39
+ "properties": {
40
+ "id": { "$ref": "#/$defs/name" },
41
+ "label": { "$ref": "#/$defs/name" }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ },
47
+ "source": {
48
+ "type": "object",
49
+ "required": ["v", "type", "id", "status"],
50
+ "properties": {
51
+ "v": { "const": 1 },
52
+ "type": { "const": "source" },
53
+ "id": { "$ref": "#/$defs/name" },
54
+ "status": { "enum": ["scanning", "done", "skipped", "error"] },
55
+ "counts": {
56
+ "type": "object",
57
+ "propertyNames": { "pattern": "^[a-z0-9_-]{1,64}$" },
58
+ "additionalProperties": { "$ref": "#/$defs/nonNegative" }
59
+ },
60
+ "message": { "type": "string", "maxLength": 2000 }
61
+ }
62
+ },
63
+ "count": {
64
+ "type": "object",
65
+ "required": ["v", "type", "source", "key", "value"],
66
+ "properties": {
67
+ "v": { "const": 1 },
68
+ "type": { "const": "count" },
69
+ "source": { "$ref": "#/$defs/name" },
70
+ "key": { "type": "string", "pattern": "^[a-z0-9_-]{1,64}$" },
71
+ "value": { "$ref": "#/$defs/nonNegative" }
72
+ }
73
+ },
74
+ "company": {
75
+ "type": "object",
76
+ "required": ["v", "type", "id", "name", "basis"],
77
+ "properties": {
78
+ "v": { "const": 1 },
79
+ "type": { "const": "company" },
80
+ "id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$", "maxLength": 128 },
81
+ "name": { "$ref": "#/$defs/name" },
82
+ "basis": { "enum": ["hq-company", "repo-org", "folder"] }
83
+ }
84
+ },
85
+ "project": {
86
+ "type": "object",
87
+ "required": ["v", "type", "id", "name", "company", "basis"],
88
+ "properties": {
89
+ "v": { "const": 1 },
90
+ "type": { "const": "project" },
91
+ "id": { "type": "string", "pattern": "^p_[0-9a-f]{12}$" },
92
+ "name": { "$ref": "#/$defs/baseName" },
93
+ "company": { "type": ["string", "null"] },
94
+ "basis": { "enum": ["repo", "claude-code-cwd", "codex-cwd"] }
95
+ }
96
+ },
97
+ "error": {
98
+ "type": "object",
99
+ "required": ["v", "type", "source", "message"],
100
+ "properties": {
101
+ "v": { "const": 1 },
102
+ "type": { "const": "error" },
103
+ "source": { "$ref": "#/$defs/name" },
104
+ "message": { "type": "string", "minLength": 1, "maxLength": 2000 },
105
+ "code": { "enum": ["scanner_outdated", "scan_failed"] }
106
+ }
107
+ },
108
+ "done": {
109
+ "type": "object",
110
+ "required": ["v", "type", "report", "summary"],
111
+ "properties": {
112
+ "v": { "const": 1 },
113
+ "type": { "const": "done" },
114
+ "report": { "type": ["string", "null"] },
115
+ "summary": {
116
+ "type": "object",
117
+ "required": ["companies", "projects", "sessions"],
118
+ "properties": {
119
+ "companies": { "$ref": "#/$defs/nonNegative" },
120
+ "projects": { "$ref": "#/$defs/nonNegative" },
121
+ "sessions": { "$ref": "#/$defs/nonNegative" }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Validates one line of the import-scan event stream against
3
+ * `scan-event.schema.json` (Ajv, draft 2020-12) and rebuilds the event from
4
+ * the v1 fields only.
5
+ *
6
+ * Validation dispatches on `type`: the common envelope is checked first, then
7
+ * the line is validated against `#/$defs/<type>` alone, so a drop reason names
8
+ * the real problem instead of the first failing `oneOf` branch.
9
+ *
10
+ * Failure reasons name the type, the schema path and Ajv's generic message
11
+ * only. They never echo values from the line, so a malformed event that
12
+ * happens to carry content cannot leak through the warning hq-cli prints.
13
+ */
14
+ import { type ScanEvent } from "./events.js";
15
+ export declare const SCAN_EVENT_SCHEMA_PATH: string;
16
+ export type ParsedScanLine = {
17
+ ok: true;
18
+ event: ScanEvent;
19
+ } | {
20
+ ok: false;
21
+ reason: string;
22
+ };
23
+ /** Parse and validate one JSON Lines record. */
24
+ export declare function parseScanEventLine(line: string): ParsedScanLine;
25
+ export declare function validateScanEvent(value: unknown): ParsedScanLine;
26
+ /**
27
+ * Rebuild an already-validated event from the v1 fields only, so fields a
28
+ * scanner adds (or smuggles) never reach stdout.
29
+ */
30
+ export declare function pickScanEvent(event: ScanEvent): ScanEvent;
31
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Validates one line of the import-scan event stream against
3
+ * `scan-event.schema.json` (Ajv, draft 2020-12) and rebuilds the event from
4
+ * the v1 fields only.
5
+ *
6
+ * Validation dispatches on `type`: the common envelope is checked first, then
7
+ * the line is validated against `#/$defs/<type>` alone, so a drop reason names
8
+ * the real problem instead of the first failing `oneOf` branch.
9
+ *
10
+ * Failure reasons name the type, the schema path and Ajv's generic message
11
+ * only. They never echo values from the line, so a malformed event that
12
+ * happens to carry content cannot leak through the warning hq-cli prints.
13
+ */
14
+ import { readFileSync } from "node:fs";
15
+ import { dirname, join } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import Ajv2020 from "ajv/dist/2020.js";
18
+ import { SCAN_EVENT_TYPES, SCAN_EVENT_VERSION } from "./events.js";
19
+ const HERE = dirname(fileURLToPath(import.meta.url));
20
+ export const SCAN_EVENT_SCHEMA_PATH = join(HERE, "scan-event.schema.json");
21
+ let validators;
22
+ function getValidators() {
23
+ if (validators)
24
+ return validators;
25
+ const schema = JSON.parse(readFileSync(SCAN_EVENT_SCHEMA_PATH, "utf8"));
26
+ const ajv = new Ajv2020({ allErrors: false, strict: false, validateFormats: false });
27
+ ajv.addSchema(schema);
28
+ const map = new Map();
29
+ for (const type of SCAN_EVENT_TYPES) {
30
+ const fn = ajv.getSchema(`${schema.$id}#/$defs/${type}`);
31
+ if (!fn)
32
+ throw new Error(`scan-event schema has no $defs/${type}`);
33
+ map.set(type, fn);
34
+ }
35
+ validators = map;
36
+ return map;
37
+ }
38
+ /** Parse and validate one JSON Lines record. */
39
+ export function parseScanEventLine(line) {
40
+ let value;
41
+ try {
42
+ value = JSON.parse(line);
43
+ }
44
+ catch {
45
+ return { ok: false, reason: "not valid JSON" };
46
+ }
47
+ return validateScanEvent(value);
48
+ }
49
+ function isRecord(value) {
50
+ return typeof value === "object" && value !== null && !Array.isArray(value);
51
+ }
52
+ export function validateScanEvent(value) {
53
+ if (!isRecord(value))
54
+ return { ok: false, reason: "not a JSON object" };
55
+ if (value.v !== SCAN_EVENT_VERSION)
56
+ return { ok: false, reason: "missing or unsupported version (v)" };
57
+ const type = value.type;
58
+ if (typeof type !== "string" || !SCAN_EVENT_TYPES.includes(type)) {
59
+ return { ok: false, reason: "missing or unknown event type" };
60
+ }
61
+ const validate = getValidators().get(type);
62
+ if (!validate(value)) {
63
+ const first = validate.errors?.[0];
64
+ const where = first ? `${first.instancePath || "/"} ${first.message ?? first.keyword}` : "schema";
65
+ return { ok: false, reason: `event "${type}" does not match the v1 schema (${where})` };
66
+ }
67
+ return { ok: true, event: pickScanEvent(value) };
68
+ }
69
+ /**
70
+ * Rebuild an already-validated event from the v1 fields only, so fields a
71
+ * scanner adds (or smuggles) never reach stdout.
72
+ */
73
+ export function pickScanEvent(event) {
74
+ const v = SCAN_EVENT_VERSION;
75
+ switch (event.type) {
76
+ case "start":
77
+ return { v, type: "start", sources: event.sources.map((s) => ({ id: s.id, label: s.label })) };
78
+ case "source": {
79
+ const out = { v, type: "source", id: event.id, status: event.status };
80
+ if (event.counts)
81
+ out.counts = Object.fromEntries(Object.entries(event.counts));
82
+ if (event.message !== undefined)
83
+ out.message = event.message;
84
+ return out;
85
+ }
86
+ case "count":
87
+ return { v, type: "count", source: event.source, key: event.key, value: event.value };
88
+ case "company":
89
+ return { v, type: "company", id: event.id, name: event.name, basis: event.basis };
90
+ case "project":
91
+ return { v, type: "project", id: event.id, name: event.name, company: event.company, basis: event.basis };
92
+ case "error": {
93
+ const out = { v, type: "error", source: event.source, message: event.message };
94
+ if (event.code !== undefined)
95
+ out.code = event.code;
96
+ return out;
97
+ }
98
+ case "done":
99
+ return {
100
+ v,
101
+ type: "done",
102
+ report: event.report,
103
+ summary: {
104
+ companies: event.summary.companies,
105
+ projects: event.summary.projects,
106
+ sessions: event.summary.sessions,
107
+ },
108
+ };
109
+ }
110
+ }
111
+ //# sourceMappingURL=validate.js.map
@@ -53,12 +53,14 @@ export interface DrainDeps {
53
53
  }
54
54
  export interface LaneDrainHookInput {
55
55
  transcript_path?: string | null;
56
- session_id?: string;
57
56
  hook_event_name?: string;
57
+ hookEventName?: string;
58
+ session_id?: string;
58
59
  }
59
60
  export interface RunDrainHookDeps {
60
61
  readLane?: typeof tryReadLane;
61
62
  probeCompactionsSince?: typeof import("./compaction.js").probeProviderCompactionsSince;
63
+ readLifecycleDeliveryFlag?: () => Promise<boolean>;
62
64
  }
63
65
  export declare function mintClaimId(): string;
64
66
  export declare function assertSafeLaneId(laneId: string): void;
@@ -102,7 +104,7 @@ export declare function formatOperatorInstruction(messages: DropboxMessage[]): s
102
104
  export declare function formatMonitorEvent(message: DropboxMessage): string;
103
105
  /** Keep event order while framing monitor output separately from operator messages. */
104
106
  export declare function formatDropboxMessages(messages: DropboxMessage[]): string;
105
- export declare function hookAdditionalContextJson(messages: DropboxMessage[], hookEventName?: "PreToolUse" | "UserPromptSubmit", leadingNote?: string): string;
107
+ export declare function hookAdditionalContextJson(messages: DropboxMessage[], hookEventName?: "PreToolUse" | "PostToolUse" | "UserPromptSubmit", leadingNote?: string): string;
106
108
  export interface DrainHookResult {
107
109
  exitCode: number;
108
110
  stdout: string;