@indigoai-us/hq-cli 5.309.1 → 5.311.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/assets/scaffold/core/scripts/hq-dm-bind.sh +585 -0
  3. package/assets/scaffold/core/scripts/jobs-validate.sh +1 -1
  4. package/assets/scaffold/core/scripts/work-mesh-live-bind-trusted.sh +152 -0
  5. package/dist/command-catalog.generated.d.ts +28 -2
  6. package/dist/command-catalog.generated.js +34 -2
  7. package/dist/commands/channels.d.ts +1 -9
  8. package/dist/commands/channels.js +2 -2
  9. package/dist/commands/company-transfer.d.ts +4 -28
  10. package/dist/commands/company-transfer.js +5 -49
  11. package/dist/commands/core-checkpoint-stop-gate-native.d.ts +34 -0
  12. package/dist/commands/core-checkpoint-stop-gate-native.js +713 -0
  13. package/dist/commands/core-checkpoint.d.ts +0 -24
  14. package/dist/commands/core-checkpoint.js +4 -4
  15. package/dist/commands/core-command-catalog.js +1 -1
  16. package/dist/commands/core-dm-bind-native.d.ts +32 -0
  17. package/dist/commands/core-dm-bind-native.js +668 -0
  18. package/dist/commands/core-hq-session-native.d.ts +39 -0
  19. package/dist/commands/core-hq-session-native.js +579 -0
  20. package/dist/commands/core-jobs-native.js +1 -4
  21. package/dist/commands/core-native-session-flag.d.ts +4 -0
  22. package/dist/commands/core-native-session-flag.js +7 -0
  23. package/dist/commands/core-native-utilities.d.ts +2 -1
  24. package/dist/commands/core-native-utilities.js +4 -2
  25. package/dist/commands/core-native-worker-skills-flag.d.ts +4 -0
  26. package/dist/commands/core-native-worker-skills-flag.js +7 -0
  27. package/dist/commands/core-work-mesh-bind-trusted-native.d.ts +32 -0
  28. package/dist/commands/core-work-mesh-bind-trusted-native.js +190 -0
  29. package/dist/commands/core-worker-lint-native.d.ts +4 -0
  30. package/dist/commands/core-worker-lint-native.js +160 -0
  31. package/dist/commands/core-worker-share-native.d.ts +5 -0
  32. package/dist/commands/core-worker-share-native.js +260 -0
  33. package/dist/commands/core-worker-skills-command.d.ts +8 -0
  34. package/dist/commands/core-worker-skills-command.js +33 -0
  35. package/dist/commands/core-worker-skills-runtime.d.ts +33 -0
  36. package/dist/commands/core-worker-skills-runtime.js +86 -0
  37. package/dist/commands/core.js +10 -2
  38. package/dist/commands/dm.d.ts +3 -7
  39. package/dist/commands/dm.js +3 -3
  40. package/dist/commands/integrations-import.d.ts +1 -1
  41. package/dist/commands/integrations-import.js +34 -19
  42. package/dist/commands/lanes-accounts.js +3 -1
  43. package/dist/commands/lanes-message.js +84 -10
  44. package/dist/commands/lanes-shared.d.ts +4 -1
  45. package/dist/commands/lanes.d.ts +4 -1
  46. package/dist/commands/lanes.js +48 -7
  47. package/dist/commands/personal-connector-settings-flag.d.ts +4 -0
  48. package/dist/commands/personal-connector-settings-flag.js +7 -0
  49. package/dist/commands/report.d.ts +1 -26
  50. package/dist/commands/report.js +6 -6
  51. package/dist/fast-core.js +15 -2
  52. package/dist/fast-scaffold.js +25 -2
  53. package/dist/index.js +8 -0
  54. package/dist/lib/bot/continuity.d.ts +2 -1
  55. package/dist/lib/bot/job-completions.d.ts +0 -2
  56. package/dist/lib/bot/job-completions.js +2 -2
  57. package/dist/lib/bot/local-config.d.ts +0 -10
  58. package/dist/lib/bot/local-config.js +3 -9
  59. package/dist/lib/bot/owner-context.d.ts +2 -6
  60. package/dist/lib/bot/owner-context.js +3 -3
  61. package/dist/lib/bot/runtime/windows.d.ts +0 -2
  62. package/dist/lib/bot/runtime/windows.js +1 -1
  63. package/dist/lib/bot/setup-explain.d.ts +0 -4
  64. package/dist/lib/bot/setup-explain.js +2 -2
  65. package/dist/lib/daemon/catalog.d.ts +3 -2
  66. package/dist/lib/daemon/hq-cloud.d.ts +2 -2
  67. package/dist/lib/daemon/hq-cloud.js +1 -1
  68. package/dist/lib/daemon/machine-type.d.ts +3 -4
  69. package/dist/lib/daemon/machine-type.js +2 -2
  70. package/dist/lib/daemon/remote-control.d.ts +0 -1
  71. package/dist/lib/daemon/remote-control.js +1 -1
  72. package/dist/lib/daemon/supervisor.d.ts +4 -9
  73. package/dist/lib/daemon/supervisor.js +5 -5
  74. package/dist/lib/lanes/admission-host.d.ts +3 -0
  75. package/dist/lib/lanes/admission-host.js +4 -0
  76. package/dist/lib/lanes/autopilot-registry.d.ts +62 -0
  77. package/dist/lib/lanes/autopilot-registry.js +192 -0
  78. package/dist/lib/lanes/autopilot.d.ts +2 -0
  79. package/dist/lib/lanes/autopilot.js +18 -5
  80. package/dist/lib/lanes/claude-balance.d.ts +6 -0
  81. package/dist/lib/lanes/claude-balance.js +17 -2
  82. package/dist/lib/lanes/keep-alive.d.ts +3 -0
  83. package/dist/lib/lanes/keep-alive.js +3 -0
  84. package/dist/lib/lanes/reconcile.js +14 -6
  85. package/dist/lib/lanes/spawn.js +2 -0
  86. package/dist/lib/mesh/live/daemon/credentials.d.ts +2 -17
  87. package/dist/lib/mesh/live/daemon/credentials.js +9 -9
  88. package/dist/lib/mesh/live/daemon/index.d.ts +5 -5
  89. package/dist/lib/mesh/live/daemon/index.js +2 -2
  90. package/dist/lib/mesh/live/daemon/state.d.ts +1 -1
  91. package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +4 -11
  92. package/dist/lib/mesh/live/daemon/transcript-watch.js +5 -5
  93. package/dist/lib/work-context/company.d.ts +2 -25
  94. package/dist/lib/work-context/company.js +3 -3
  95. package/dist/lib/work-context/config.d.ts +3 -8
  96. package/dist/lib/work-context/config.js +2 -13
  97. package/dist/lib/work-context/state.d.ts +4 -13
  98. package/dist/lib/work-context/state.js +2 -30
  99. package/package.json +1 -1
@@ -0,0 +1,713 @@
1
+ /** Native fast-route implementation of checkpoint-stop-gate.sh. */
2
+ import * as childProcess from "node:child_process";
3
+ import * as fs from "node:fs";
4
+ import * as os from "node:os";
5
+ import * as path from "node:path";
6
+ export const CHECKPOINT_STOP_GATE_ASSET = "core/scripts/checkpoint-stop-gate.sh";
7
+ const expectedFsMiss = (error) => {
8
+ const code = error?.code;
9
+ return code === "ENOENT" || code === "ENOTDIR" || code === "EACCES" || code === "ELOOP";
10
+ };
11
+ const defaults = () => ({
12
+ env: process.env,
13
+ now: () => new Date(),
14
+ readFile: (filePath) => fs.readFileSync(filePath, "utf8"),
15
+ exists: (filePath) => fs.existsSync(filePath),
16
+ isFile: (filePath) => { try {
17
+ return fs.statSync(filePath).isFile();
18
+ }
19
+ catch (e) {
20
+ if (expectedFsMiss(e))
21
+ return false;
22
+ throw e;
23
+ } },
24
+ isDirectory: (filePath) => { try {
25
+ return fs.statSync(filePath).isDirectory();
26
+ }
27
+ catch (e) {
28
+ if (expectedFsMiss(e))
29
+ return false;
30
+ throw e;
31
+ } },
32
+ canRead: (filePath) => { try {
33
+ fs.accessSync(filePath, fs.constants.R_OK);
34
+ return true;
35
+ }
36
+ catch (e) {
37
+ if (expectedFsMiss(e))
38
+ return false;
39
+ throw e;
40
+ } },
41
+ canWrite: (filePath) => { try {
42
+ fs.accessSync(filePath, fs.constants.W_OK);
43
+ return true;
44
+ }
45
+ catch (e) {
46
+ if (expectedFsMiss(e))
47
+ return false;
48
+ throw e;
49
+ } },
50
+ mtimeSeconds: (filePath) => Math.floor(fs.statSync(filePath).mtimeMs / 1000),
51
+ mkdir: (directory) => fs.mkdirSync(directory, { recursive: true }),
52
+ writeFile: (filePath, contents, mode) => fs.writeFileSync(filePath, contents, mode === undefined ? undefined : { mode }),
53
+ appendFile: (filePath, contents) => fs.appendFileSync(filePath, contents),
54
+ rename: (from, to) => fs.renameSync(from, to),
55
+ remove: (filePath) => { try {
56
+ fs.rmSync(filePath, { recursive: true, force: true });
57
+ }
58
+ catch (e) {
59
+ if (!expectedFsMiss(e))
60
+ throw e;
61
+ } },
62
+ touch: (filePath) => {
63
+ const now = new Date();
64
+ try {
65
+ fs.utimesSync(filePath, now, now);
66
+ }
67
+ catch (error) {
68
+ if (error.code !== "ENOENT")
69
+ throw error;
70
+ fs.writeFileSync(filePath, "", { mode: 0o666 });
71
+ }
72
+ },
73
+ makeTempPath: () => path.join(os.tmpdir(), `tmp.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`),
74
+ commandExists: (name, env) => {
75
+ for (const entry of (env?.PATH ?? process.env.PATH ?? "").split(path.delimiter)) {
76
+ if (!entry)
77
+ continue;
78
+ const candidate = path.join(entry, name);
79
+ try {
80
+ fs.accessSync(candidate, fs.constants.X_OK);
81
+ return true;
82
+ }
83
+ catch { /* try the next PATH entry */ }
84
+ }
85
+ return false;
86
+ },
87
+ spawnHqDetached: (args, env) => {
88
+ const child = childProcess.spawn("hq", args, { detached: true, stdio: "ignore", env: env ?? process.env });
89
+ child.unref();
90
+ },
91
+ });
92
+ function toShellString(value) {
93
+ if (value === undefined || value === null)
94
+ return "";
95
+ if (typeof value === "object")
96
+ return JSON.stringify(value);
97
+ return String(value);
98
+ }
99
+ function parseJson(text) {
100
+ return JSON.parse(text);
101
+ }
102
+ function record(value) {
103
+ return value !== null && typeof value === "object" && !Array.isArray(value)
104
+ ? value
105
+ : {};
106
+ }
107
+ function trimShell(value) {
108
+ return value.replace(/^[\t\n\v\f\r ]+|[\t\n\v\f\r ]+$/g, "");
109
+ }
110
+ function jsonString(value) { return JSON.stringify(value); }
111
+ function readIfReadable(filePath, deps) {
112
+ if (!deps.canRead(filePath))
113
+ return undefined;
114
+ try {
115
+ return deps.readFile(filePath, "utf8");
116
+ }
117
+ catch (error) {
118
+ if (expectedFsMiss(error))
119
+ return undefined;
120
+ throw error;
121
+ }
122
+ }
123
+ function readJsonFile(filePath, deps) {
124
+ const text = readIfReadable(filePath, deps);
125
+ if (text === undefined)
126
+ return undefined;
127
+ try {
128
+ return parseJson(text);
129
+ }
130
+ catch {
131
+ return undefined;
132
+ }
133
+ }
134
+ function timestamp(deps) {
135
+ return deps.now().toISOString().slice(0, 19) + "Z";
136
+ }
137
+ function safeSessionKey(raw, fallbackPid) {
138
+ // `tr -c` operates on bytes, so each byte of a multibyte character becomes `_`.
139
+ let value = [...Buffer.from(raw, "utf8")].map((byte) => byte >= 48 && byte <= 57 || byte >= 65 && byte <= 90 || byte >= 97 && byte <= 122 ||
140
+ byte === 46 || byte === 95 || byte === 45 ? String.fromCharCode(byte) : "_").join("");
141
+ value = value.replace(/^_/, "").replace(/_$/, "");
142
+ if (!value)
143
+ value = `session-${fallbackPid}`;
144
+ return value.slice(0, 96);
145
+ }
146
+ function boundCompany(root, sessionId, deps) {
147
+ if (!sessionId)
148
+ return "";
149
+ const sessionDir = path.join(root, "workspace", "sessions", sessionId);
150
+ const capability = readJsonFile(path.join(sessionDir, "scope-capability.json"), deps);
151
+ const capValue = toShellString(record(capability).company_slug);
152
+ if (capValue)
153
+ return capValue;
154
+ const meta = readIfReadable(path.join(sessionDir, "meta.yaml"), deps);
155
+ if (meta === undefined)
156
+ return "";
157
+ for (const line of meta.split("\n")) {
158
+ const first = line.match(/^\s*(\S+)/)?.[1] ?? "";
159
+ if (first !== "company_slug:")
160
+ continue;
161
+ return line.replace(/^[^:]+:[\t ]*/, "").replace(/^"|"$/g, "");
162
+ }
163
+ return "";
164
+ }
165
+ function gateCallerEmail(deps, home) {
166
+ if (deps.env.HQ_CHECKPOINT_CALLER_EMAIL)
167
+ return deps.env.HQ_CHECKPOINT_CALLER_EMAIL;
168
+ const tokenFile = deps.env.HQ_COGNITO_TOKENS_FILE ?? (home ? path.join(home, ".hq", "cognito-tokens.json") : "");
169
+ if (!tokenFile)
170
+ return "";
171
+ const tokens = record(readJsonFile(tokenFile, deps));
172
+ for (const key of ["idToken", "accessToken"]) {
173
+ const token = typeof tokens[key] === "string" ? tokens[key] : "";
174
+ const segment = token.split(".")[1] ?? "";
175
+ if (!segment)
176
+ continue;
177
+ try {
178
+ const payload = record(parseJson(Buffer.from(segment.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8")));
179
+ const email = toShellString(payload["custom:delegatedEmail"] ?? payload.email);
180
+ if (/^.+@.+$/.test(email))
181
+ return email;
182
+ }
183
+ catch { /* malformed or opaque local token cache is fail-open */ }
184
+ }
185
+ return "";
186
+ }
187
+ function checkpointGateSwitch(key, root, deps) {
188
+ let raw = trimShell(deps.env[key] ?? "");
189
+ if (!raw) {
190
+ for (const file of [
191
+ path.join(root, ".claude", "settings.local.json"),
192
+ path.join(root, ".claude", "settings.json"),
193
+ ]) {
194
+ const docText = readIfReadable(file, deps);
195
+ if (docText === undefined)
196
+ continue;
197
+ let parsed;
198
+ try {
199
+ parsed = parseJson(docText);
200
+ }
201
+ catch {
202
+ continue;
203
+ }
204
+ const env = record(record(parsed).env);
205
+ if (!Object.prototype.hasOwnProperty.call(env, key))
206
+ continue;
207
+ const value = env[key];
208
+ if (typeof value !== "string" && typeof value !== "boolean" && typeof value !== "number") {
209
+ return { value: "", diagnostic: `checkpoint-stop-gate: ignoring ${key} in ${file} (not a value it can read; expected a string such as "0" or "1")\n` };
210
+ }
211
+ raw = trimShell(String(value));
212
+ if (raw)
213
+ break;
214
+ return { value: "", diagnostic: `checkpoint-stop-gate: ignoring ${key} in ${file} (not a value it can read; expected a string such as "0" or "1")\n` };
215
+ }
216
+ }
217
+ if (!raw)
218
+ return { value: "", diagnostic: "" };
219
+ const normalized = raw.toLowerCase();
220
+ if (["0", "false", "off", "no"].includes(normalized))
221
+ return { value: "0", diagnostic: "" };
222
+ if (["1", "true", "on", "yes"].includes(normalized))
223
+ return { value: "1", diagnostic: "" };
224
+ return {
225
+ value: "",
226
+ diagnostic: `checkpoint-stop-gate: ignoring ${key}=${raw} (expected 0/1, false/true, off/on or no/yes)\n`,
227
+ };
228
+ }
229
+ function companyGateOff(root, sessionId, home, deps) {
230
+ const company = boundCompany(root, sessionId, deps);
231
+ if (!company || company === "personal" || /[^a-z0-9-]/.test(company) || !home)
232
+ return false;
233
+ const directory = path.join(home, ".hq", "cache", "checkpoint-gate");
234
+ const cache = path.join(directory, `${company}.json`);
235
+ const refreshStamp = path.join(directory, `${company}.refresh`);
236
+ const parsed = record(readJsonFile(cache, deps));
237
+ const enabled = parsed.checkpointGateEnabled;
238
+ const value = enabled === false ? "off" : enabled === true ? "on" : "";
239
+ const fetchedRaw = parsed.fetched_at;
240
+ const fetched = typeof fetchedRaw === "number" && Number.isFinite(fetchedRaw) ? Math.floor(fetchedRaw) : 0;
241
+ const now = Math.floor(deps.now().getTime() / 1000);
242
+ if (now - fetched > 600 && deps.commandExists("hq", deps.env)) {
243
+ let stampMtime = 0;
244
+ try {
245
+ stampMtime = deps.mtimeSeconds(refreshStamp);
246
+ }
247
+ catch {
248
+ stampMtime = 0;
249
+ }
250
+ if (now - stampMtime > 600) {
251
+ try {
252
+ deps.mkdir(directory);
253
+ deps.touch(refreshStamp);
254
+ deps.spawnHqDetached(["core", "checkpoint-gate-setting", "--refresh", "--company", company]);
255
+ }
256
+ catch { /* cache refresh is best-effort and never gates this Stop */ }
257
+ }
258
+ }
259
+ return value === "off";
260
+ }
261
+ function payloadReplyEvidence(value) {
262
+ const obj = record(value);
263
+ if (!Object.prototype.hasOwnProperty.call(obj, "last_assistant_message"))
264
+ return { state: "absent", chars: 0 };
265
+ const message = obj.last_assistant_message;
266
+ if (typeof message === "string")
267
+ return { state: "present", chars: Array.from(message.replace(/\s+/g, "")).length };
268
+ if (Array.isArray(message)) {
269
+ const joined = message.map((item) => toShellString(record(item).text)).join("");
270
+ return { state: "present", chars: Array.from(joined.replace(/\s+/g, "")).length };
271
+ }
272
+ return { state: "invalid", chars: 0 };
273
+ }
274
+ function isRecord(value) {
275
+ return value !== null && typeof value === "object" && !Array.isArray(value);
276
+ }
277
+ function nonWhitespaceLength(value) { return Array.from(value.replace(/\s+/g, "")).length; }
278
+ function codexCommand(payload) {
279
+ const name = toShellString(payload.name);
280
+ const input = payload.input;
281
+ if (name === "exec" && typeof input === "string") {
282
+ const match = input.match(/tools\.exec_command\((\{.*\})\);/s);
283
+ if (match) {
284
+ try {
285
+ const args = JSON.parse(match[1]);
286
+ const command = toShellString(record(args).cmd);
287
+ if (command)
288
+ return command;
289
+ }
290
+ catch { /* shell filter falls back to a JS object literal probe */ }
291
+ const cmdMatch = input.match(/(?:^|[,{}])\s*cmd\s*:\s*("(?:\\.|[^"\\])*")/s);
292
+ if (cmdMatch) {
293
+ try {
294
+ return JSON.parse(cmdMatch[1]);
295
+ }
296
+ catch {
297
+ return "";
298
+ }
299
+ }
300
+ }
301
+ return "";
302
+ }
303
+ if (name === "exec_command" || name === "Bash") {
304
+ if (isRecord(input))
305
+ return toShellString(input.cmd ?? input.command);
306
+ if (typeof input === "string") {
307
+ try {
308
+ const parsed = JSON.parse(input);
309
+ return toShellString(record(parsed).cmd ?? record(parsed).command);
310
+ }
311
+ catch {
312
+ return input;
313
+ }
314
+ }
315
+ if (typeof payload.arguments === "string") {
316
+ try {
317
+ const parsed = JSON.parse(payload.arguments);
318
+ return toShellString(record(parsed).cmd ?? record(parsed).command);
319
+ }
320
+ catch {
321
+ return "";
322
+ }
323
+ }
324
+ }
325
+ return "";
326
+ }
327
+ const CHECKPOINT_COMMAND = /(^\s*|[;&|()]\s*|\n\s*)(?:(?:command|exec)\s+)?(?:env\s+)?(?:[A-Za-z_][A-Za-z0-9_]*=(?:"(?:\\.|[^"\\])*"|'[^']*'|[^\s;&|()]*)\s+)*(?:(?:command|exec|env)\s+)?("[^"]*[/\\]hq"|[^\s;&|()]*[/\\]hq|hq)\s+core\s+checkpoint(\s|$)/;
328
+ const PURE_CHECKPOINT = /^\s*(?:(?:command|exec)\s+)?(?:env\s+)?(?:[A-Za-z_][A-Za-z0-9_]*=(?:"(?:\\.|[^"\\])*"|'[^']*'|[^\s;&|()]*)\s+)*(?:(?:command|exec|env)\s+)?(?:"[^"]*[/\\]hq"|[^\s;&|()]*[/\\]hq|hq)(?<tail>\s+core\s+checkpoint(?:\s+[^;&|\n]*)?)$/;
329
+ function stripQuoted(command) {
330
+ return command
331
+ .replace(/"(?:\\.|[^"\\])*"/g, "")
332
+ .replace(/'[^']*'/g, "");
333
+ }
334
+ function isCheckpointCommand(command) { return CHECKPOINT_COMMAND.test(command); }
335
+ function isPureCheckpoint(command) {
336
+ const match = command.match(PURE_CHECKPOINT);
337
+ if (!match?.groups?.tail)
338
+ return false;
339
+ const normalized = command.replace(/(?:"[^"]*[/\\]hq"|[^\s;&|()]*[/\\]hq|hq)(?=\s+core\s+checkpoint)/, "hq");
340
+ const stripped = stripQuoted(normalized);
341
+ return /^\s*(?:(?:command|exec)\s+)?(?:env\s+)?(?:[A-Za-z_][A-Za-z0-9_]*=(?:"(?:\\.|[^"\\])*"|'[^']*'|[^\s;&|()]*)\s+)*(?:(?:command|exec|env)\s+)?hq\s+core\s+checkpoint([\s][^;&|\n]*)?$/.test(stripped);
342
+ }
343
+ function checkpointFlag(command, flag) {
344
+ return stripQuoted(command).split(/\s+/).includes(flag);
345
+ }
346
+ function parseTranscript(contents) {
347
+ const allLines = contents.split("\n");
348
+ const lines = allLines.slice(Math.max(0, allLines.length - 400));
349
+ const rows = [];
350
+ for (const raw of lines) {
351
+ if (!raw)
352
+ continue;
353
+ const value = JSON.parse(raw);
354
+ if (!isRecord(value))
355
+ throw new Error("invalid transcript row");
356
+ rows.push({ raw, entry: value });
357
+ }
358
+ const isUser = (entry) => {
359
+ if (entry.type === "user") {
360
+ const content = record(entry.message).content;
361
+ return typeof content === "string" || Array.isArray(content) && content.some((item) => record(item).type === "text");
362
+ }
363
+ const payload = record(entry.payload);
364
+ return entry.type === "response_item" && payload.type === "message" && payload.role === "user" &&
365
+ Array.isArray(payload.content) && payload.content.some((item) => record(item).type === "input_text");
366
+ };
367
+ const userIndexes = rows.flatMap((row, index) => isUser(row.entry) ? [index] : []);
368
+ if (!userIndexes.length)
369
+ throw new Error("no real user entry");
370
+ const userIndex = userIndexes[userIndexes.length - 1];
371
+ const genuine = userIndexes.filter((index) => {
372
+ const entry = rows[index].entry;
373
+ return !(entry.type === "user" && typeof record(entry.message).content === "string" &&
374
+ record(entry.message).content.startsWith("Stop hook feedback:"));
375
+ });
376
+ const genuineIndex = genuine.length ? genuine[genuine.length - 1] : userIndexes[0];
377
+ const events = [];
378
+ rows.forEach(({ entry }, row) => {
379
+ if (row <= genuineIndex)
380
+ return;
381
+ if (entry.type === "assistant") {
382
+ const content = record(entry.message).content;
383
+ if (!Array.isArray(content))
384
+ return;
385
+ for (const itemValue of content) {
386
+ const item = record(itemValue);
387
+ if (item.type === "tool_use") {
388
+ events.push({ row, kind: "tool", runtime: "claude", id: toShellString(item.id), name: toShellString(item.name), command: toShellString(record(item.input).command) });
389
+ }
390
+ else if (item.type === "text") {
391
+ events.push({ row, kind: "text", chars: nonWhitespaceLength(toShellString(item.text)) });
392
+ }
393
+ }
394
+ }
395
+ else if (entry.type === "response_item") {
396
+ const payload = record(entry.payload);
397
+ if (payload.type === "custom_tool_call" || payload.type === "function_call") {
398
+ events.push({ row, kind: "tool", runtime: "codex", id: toShellString(payload.call_id ?? payload.id), name: toShellString(payload.name), command: codexCommand(payload) });
399
+ }
400
+ else if (payload.type === "message" && payload.role === "assistant" && Array.isArray(payload.content)) {
401
+ const text = payload.content.map((item) => toShellString(record(item).text)).join("");
402
+ events.push({ row, kind: "text", chars: nonWhitespaceLength(text) });
403
+ }
404
+ }
405
+ });
406
+ const segment = events.filter((event) => event.row > userIndex);
407
+ const tools = events.filter((event) => event.kind === "tool");
408
+ const lastTool = tools.at(-1);
409
+ const segmentToolIndex = segment.reduce((found, event, index) => event.kind === "tool" ? index : found, -1);
410
+ const segmentReplyChars = segment.slice(segmentToolIndex + 1).reduce((sum, event) => sum + (event.kind === "text" ? event.chars : 0), 0);
411
+ const lastWorkIndex = events.reduce((found, event, index) => event.kind === "tool" && !isCheckpointCommand(event.command) ? index : found, -1);
412
+ const replyChars = events.slice(lastWorkIndex + 1).reduce((sum, event) => sum + (event.kind === "text" ? event.chars : 0), 0);
413
+ const workTools = tools.filter((tool) => !isCheckpointCommand(tool.command));
414
+ const lastResultError = rows.some(({ entry }) => {
415
+ if (entry.type !== "user" || !Array.isArray(record(entry.message).content))
416
+ return false;
417
+ return record(entry.message).content.some((itemValue) => {
418
+ const item = record(itemValue);
419
+ return item.type === "tool_result" && item.tool_use_id === lastTool?.id && item.is_error === true;
420
+ });
421
+ });
422
+ if (tools.length > 0 && lastTool && isCheckpointCommand(lastTool.command)) {
423
+ if (lastTool.runtime === "claude" && !lastResultError) {
424
+ const pure = isPureCheckpoint(lastTool.command);
425
+ const replyRequired = !pure || (!checkpointFlag(lastTool.command, "--gate-probe") &&
426
+ (!checkpointFlag(lastTool.command, "--idle") || workTools.length > 0));
427
+ return { kind: "checkpoint", toolId: lastTool.id, replyChars, replyRequired };
428
+ }
429
+ if (lastTool.runtime === "codex")
430
+ return { kind: "stamp" };
431
+ return segmentReplyChars > 0 ? { kind: "replied" } : { kind: "unreplied" };
432
+ }
433
+ if (workTools.length === 0)
434
+ return { kind: "idle" };
435
+ return segmentReplyChars > 0 ? { kind: "replied" } : { kind: "unreplied" };
436
+ }
437
+ function countFile(filePath, deps) {
438
+ const text = readIfReadable(filePath, deps) ?? "";
439
+ const value = text.replace(/[\r\n]/g, "");
440
+ return /^\d+$/.test(value) ? Number(value) : 0;
441
+ }
442
+ function atomicStateWrite(filePath, value, deps) {
443
+ const temp = `${filePath}.${process.pid}`;
444
+ try {
445
+ deps.writeFile(temp, value, 0o600);
446
+ deps.rename(temp, filePath);
447
+ }
448
+ catch (error) {
449
+ try {
450
+ deps.remove(temp);
451
+ }
452
+ catch { /* shell ignores cleanup failure */ }
453
+ throw error;
454
+ }
455
+ }
456
+ function incrementBlockCount(filePath, count, deps) {
457
+ atomicStateWrite(filePath, String(count + 1), deps);
458
+ }
459
+ function block(reason) {
460
+ return { code: 0, stdout: `${JSON.stringify({ decision: "block", reason })}\n`, stderr: "" };
461
+ }
462
+ const COMPANY_SCOPE_REASON = (root, sessionId) => `This session has not declared its scope, and a scope is required before the turn can end. Decide where this work belongs, then bind the session and finish the turn with your user-facing reply as the final text after the command output:\n\n bash ${root}/core/scripts/hq-session.sh --session-id ${sessionId} set company_slug <company-slug>\n\nUse a real tenant slug from companies/manifest.yaml — the company this session is actually working in, never an invented one. If this session does no company-scoped work, bind it to the reserved personal scope instead:\n\n bash ${root}/core/scripts/hq-session.sh --session-id ${sessionId} set company_slug personal\n\nAn operator can also disable this requirement for the run by exporting HQ_CHECKPOINT_GATE=0, or for every runtime by putting "env": {"HQ_CHECKPOINT_GATE": "0"} in .claude/settings.json.`;
463
+ const REPLY_REASON = "Checkpoint recorded — but the turn cannot end yet: the user has not received a reply this turn. The checkpoint payload went to a background maintenance agent; the user sees NONE of it, and nothing you have written since your last real action reads as a reply to them. Deliver your complete reply to the user now, as ordinary text: every result, finding, link, decision, and anything awaiting their input, in plain language. Do not run another checkpoint and do not call any other tool — end the turn with text only.";
464
+ const MISSING_CHECKPOINT_REASON = (sessionId) => {
465
+ const flags = ` hq core checkpoint --session-id ${sessionId} --trigger stop-gate --summary "<what changed, in one line>" [--file <path>] [--decision "<choice and why>"] [--learning "<reusable rule>"] [--next "<outstanding step>"]\n\nOnly --summary is required, and the repeatable flags are what the sibling uses to enrich the record, distil policies and update the indexes — a bare summary gives it almost nothing to work with. Write them as machine record, not prose for the user, and pass each one that genuinely applies:\n --file every path you created or modified this turn\n --decision a choice you made that a reader would otherwise have to reverse-engineer\n --learning a rule that changes how someone acts next time, not a restatement of what just happened\n --next work that is genuinely still outstanding\nOmit a flag rather than padding it: an empty or invented learning is worse than none.\n\nIf this turn only read or inspected things and changed no state, the correct call instead is:\n\n hq core checkpoint --session-id ${sessionId} --idle`;
466
+ return `This turn changed something, so it needs an end-of-turn checkpoint. Two different audiences are involved — do not conflate them:\n\n1. THE USER reads your normal chat reply. You have not sent one yet this turn — everything you owe them (results, links, answers, status, decisions) must go into it. The checkpoint is invisible to them and does NOT count as having replied.\n2. THE SIBLING (a background maintenance agent) reads the checkpoint payload. It never sees your chat reply, so anything it needs must go into the flags.\n\nORDER: run the checkpoint FIRST, then deliver your complete user-facing reply as the FINAL text of the turn — final-position text is the one placement every host renders in full. Every link, URL, instruction, command, and decision the user needs must appear in that final message. The gate enforces this: a turn that ends without a user-facing reply is blocked until the reply is delivered.\n\n${flags}`;
467
+ };
468
+ /** In-process command body with injectable filesystem, clock, PATH, and hq process. */
469
+ export function executeCheckpointStopGate(input, context, overrides = {}) {
470
+ const deps = { ...defaults(), ...overrides };
471
+ const env = deps.env;
472
+ const home = env.HOME ?? "";
473
+ const selfRoot = context.hqRoot || context.cwd;
474
+ const root = env.CLAUDE_PROJECT_DIR || env.HQ_ROOT || selfRoot || (home ? path.join(home, "Documents", "HQ") : "");
475
+ const companyGateRoot = env.HQ_ROOT || env.CLAUDE_PROJECT_DIR || selfRoot;
476
+ let payload;
477
+ try {
478
+ payload = record(parseJson(input || "{}"));
479
+ }
480
+ catch {
481
+ return { code: 0, stdout: "", stderr: "" };
482
+ }
483
+ const evidence = payloadReplyEvidence(payload);
484
+ const transcriptPath = toShellString(payload.transcript_path);
485
+ const sessionId = toShellString(payload.session_id);
486
+ if (env.HQ_CHECKPOINT_SIBLING === "1")
487
+ return { code: 0, stdout: "", stderr: "" };
488
+ const setting = checkpointGateSwitch("HQ_CHECKPOINT_GATE", root, deps);
489
+ let stderr = setting.diagnostic;
490
+ const gateSwitch = setting.value;
491
+ const domains = env.HQ_CHECKPOINT_SCOPE_GATE_DOMAINS ?? "";
492
+ if (gateSwitch !== "0" && domains && sessionId) {
493
+ const email = gateCallerEmail(deps, home).toLowerCase();
494
+ const matched = email && domains.split(",").some((domain) => {
495
+ const normalized = domain.replace(/\s/g, "").toLowerCase();
496
+ return !!normalized && email.endsWith(`@${normalized}`);
497
+ });
498
+ if (matched) {
499
+ const company = boundCompany(companyGateRoot, sessionId, deps);
500
+ const bound = company === "personal" || !!company && deps.isDirectory(path.join(companyGateRoot, "companies", company));
501
+ if (!bound)
502
+ return { code: 0, stdout: block(COMPANY_SCOPE_REASON(companyGateRoot, sessionId)).stdout, stderr };
503
+ }
504
+ }
505
+ if (!gateSwitch && companyGateOff(companyGateRoot, sessionId, home, deps))
506
+ return { code: 0, stdout: "", stderr };
507
+ const runtime = env.HQ_CHECKPOINT_RUNTIME || "claude";
508
+ let enforce;
509
+ if (gateSwitch === "0")
510
+ return { code: 0, stdout: "", stderr };
511
+ if (gateSwitch === "1")
512
+ enforce = true;
513
+ else if (runtime === "claude")
514
+ enforce = true;
515
+ else if (runtime === "codex")
516
+ enforce = false;
517
+ else
518
+ return { code: 0, stdout: "", stderr };
519
+ const stateDir = path.join(root, "workspace", "orchestrator", "hook-state");
520
+ if (!deps.isDirectory(stateDir) || !deps.canWrite(stateDir))
521
+ return { code: 0, stdout: "", stderr };
522
+ const eligibilityFile = path.join(stateDir, `checkpoint-gate-eligible-${runtime}`);
523
+ const refreshEligibility = () => {
524
+ if (deps.commandExists("hq", env))
525
+ deps.spawnHqDetached(["core", "checkpoint", "--gate-probe"], { ...env, HQ_CHECKPOINT_RUNTIME: runtime });
526
+ };
527
+ if (runtime === "codex" && !enforce) {
528
+ if (!deps.exists(eligibilityFile)) {
529
+ try {
530
+ refreshEligibility();
531
+ }
532
+ catch { /* fail open */ }
533
+ return { code: 0, stdout: "", stderr };
534
+ }
535
+ if (!deps.canRead(eligibilityFile))
536
+ return { code: 0, stdout: "", stderr };
537
+ const eligibility = (readIfReadable(eligibilityFile, deps) ?? "").replace(/[\r\n]/g, "");
538
+ if (eligibility === "0")
539
+ return { code: 0, stdout: "", stderr };
540
+ if (eligibility !== "1")
541
+ return { code: 0, stdout: "", stderr };
542
+ let mtime;
543
+ try {
544
+ mtime = deps.mtimeSeconds(eligibilityFile);
545
+ }
546
+ catch {
547
+ return { code: 0, stdout: "", stderr };
548
+ }
549
+ const now = Math.floor(deps.now().getTime() / 1000);
550
+ if (now - mtime > 86400) {
551
+ try {
552
+ refreshEligibility();
553
+ }
554
+ catch { /* fail open */ }
555
+ }
556
+ enforce = true;
557
+ }
558
+ if (!deps.commandExists("hq", env) || !transcriptPath || !deps.isFile(transcriptPath) || !deps.canRead(transcriptPath) || !sessionId) {
559
+ return { code: 0, stdout: "", stderr };
560
+ }
561
+ const safeKey = safeSessionKey(sessionId, process.ppid);
562
+ if (!safeKey)
563
+ return { code: 0, stdout: "", stderr };
564
+ const ambiguityLog = path.join(stateDir, "checkpoint-stop-gate-ambiguity.log");
565
+ const logAmbiguity = (reason) => {
566
+ try {
567
+ deps.appendFile(ambiguityLog, `${timestamp(deps)}\t${safeKey}\treply-evidence-ambiguous\t${reason}\n`);
568
+ }
569
+ catch { /* shell ignores failed diagnostics */ }
570
+ };
571
+ const transcript = readIfReadable(transcriptPath, deps);
572
+ if (transcript === undefined)
573
+ return { code: 0, stdout: "", stderr };
574
+ let verdict;
575
+ try {
576
+ verdict = parseTranscript(transcript);
577
+ }
578
+ catch {
579
+ logAmbiguity("transcript-parse-failed");
580
+ return { code: 0, stdout: "", stderr };
581
+ }
582
+ let satisfied = "0";
583
+ let replied = verdict.kind === "replied" ? 1 : 0;
584
+ let toolId = "";
585
+ let replyChars = 0;
586
+ let replyRequired = "";
587
+ if (verdict.kind === "checkpoint") {
588
+ satisfied = "1";
589
+ toolId = verdict.toolId;
590
+ replyChars = verdict.replyChars;
591
+ replyRequired = verdict.replyRequired ? "1" : "0";
592
+ }
593
+ else if (verdict.kind === "stamp")
594
+ satisfied = "stamp";
595
+ else if (verdict.kind === "idle")
596
+ satisfied = "idle";
597
+ if (evidence.state === "present" && runtime === "claude") {
598
+ if (satisfied === "1")
599
+ replyChars = Math.max(replyChars, evidence.chars);
600
+ else if (satisfied === "0" && evidence.chars > 0)
601
+ replied = 1;
602
+ }
603
+ let ambiguous = false;
604
+ if ((evidence.state === "absent" || evidence.state === "invalid") && runtime === "claude" && replyChars === 0) {
605
+ if (satisfied === "1" && replyRequired === "1")
606
+ ambiguous = true;
607
+ else if (satisfied === "0") {
608
+ ambiguous = true;
609
+ replied = 1;
610
+ }
611
+ }
612
+ if (ambiguous)
613
+ logAmbiguity(`payload=${evidence.state};transcript-reply-chars=${replyChars}`);
614
+ if (satisfied === "stamp") {
615
+ satisfied = "0";
616
+ const stampFile = path.join(stateDir, `checkpoint-cli-last-${safeKey}`);
617
+ const stampText = readIfReadable(stampFile, deps);
618
+ if (stampText !== undefined) {
619
+ const stampValue = stampText.replace(/[\r\n]/g, "");
620
+ const now = Math.floor(deps.now().getTime() / 1000);
621
+ if (/^\d+$/.test(stampValue)) {
622
+ const age = now - Number(stampValue);
623
+ if (age >= 0 && age <= 120)
624
+ satisfied = "1";
625
+ }
626
+ }
627
+ }
628
+ const blockCountFile = path.join(stateDir, `checkpoint-block-count-${safeKey}`);
629
+ const blockCount = countFile(blockCountFile, deps);
630
+ const bump = () => incrementBlockCount(blockCountFile, blockCount, deps);
631
+ if (satisfied === "1" || satisfied === "idle") {
632
+ if (runtime === "codex") {
633
+ try {
634
+ deps.remove(path.join(stateDir, `codex-checkpoint-reprompt-${safeKey}`));
635
+ }
636
+ catch { /* best effort */ }
637
+ }
638
+ if (satisfied === "1" && runtime === "claude" && replyRequired === "1" && toolId && !ambiguous) {
639
+ const minimumRaw = env.HQ_CHECKPOINT_REPLY_MIN || "1";
640
+ const minimum = /^\d+$/.test(minimumRaw) ? Number.parseInt(minimumRaw, 10) : 1;
641
+ if (minimum > 0 && replyChars < minimum && blockCount < 3) {
642
+ const nudgeFile = path.join(stateDir, `checkpoint-reply-nudge-${safeKey}`);
643
+ const previous = readIfReadable(nudgeFile, deps) ?? "";
644
+ if (previous !== toolId) {
645
+ try {
646
+ atomicStateWrite(nudgeFile, toolId, deps);
647
+ bump();
648
+ }
649
+ catch {
650
+ try {
651
+ deps.remove(`${nudgeFile}.${process.pid}`);
652
+ }
653
+ catch { /* ignored by the shell */ }
654
+ }
655
+ if (readIfReadable(nudgeFile, deps) === toolId)
656
+ return { code: 0, stdout: block(REPLY_REASON).stdout, stderr };
657
+ }
658
+ }
659
+ }
660
+ try {
661
+ deps.remove(blockCountFile);
662
+ }
663
+ catch { /* allow path is fail-open */ }
664
+ return { code: 0, stdout: "", stderr };
665
+ }
666
+ if (replied === 1) {
667
+ try {
668
+ deps.spawnHqDetached([
669
+ "core", "checkpoint", "--session-id", sessionId, "--trigger", "stop-gate-auto",
670
+ "--transcript", transcriptPath, "--summary", "turn ended with its user-facing reply already delivered",
671
+ ]);
672
+ }
673
+ catch { /* an automatic checkpoint is best-effort */ }
674
+ try {
675
+ deps.remove(blockCountFile);
676
+ }
677
+ catch { /* fail-open */ }
678
+ return { code: 0, stdout: "", stderr };
679
+ }
680
+ if (blockCount >= 3)
681
+ return { code: 0, stdout: "", stderr };
682
+ try {
683
+ bump();
684
+ }
685
+ catch {
686
+ return { code: 0, stdout: "", stderr };
687
+ }
688
+ let reason = MISSING_CHECKPOINT_REASON(sessionId);
689
+ if (runtime === "codex") {
690
+ const repromptFile = path.join(stateDir, `codex-checkpoint-reprompt-${safeKey}`);
691
+ try {
692
+ atomicStateWrite(repromptFile, reason, deps);
693
+ reason = "Hook re-prompted Codex";
694
+ }
695
+ catch {
696
+ try {
697
+ deps.remove(`${repromptFile}.${process.pid}`);
698
+ }
699
+ catch { /* shell ignores cleanup */ }
700
+ }
701
+ }
702
+ return { code: 0, stdout: block(reason).stdout, stderr };
703
+ }
704
+ /** Signature consumed by the lightweight fast-core dispatcher. */
705
+ export async function runCheckpointStopGateFast(_argv, stdin, cwd, hqRoot) {
706
+ const result = executeCheckpointStopGate(stdin, { cwd, hqRoot });
707
+ if (result.stdout)
708
+ process.stdout.write(result.stdout);
709
+ if (result.stderr)
710
+ process.stderr.write(result.stderr);
711
+ return result.code;
712
+ }
713
+ //# sourceMappingURL=core-checkpoint-stop-gate-native.js.map