@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
@@ -14,6 +14,8 @@ import { LanesError } from "./errors.js";
14
14
  import { MESSAGE_COMPACTION_REPLAY_NOTE, MESSAGE_REPLAY_STATE_NAME, readMessageReplayState, updateMessageReplayTranscript, writeMessageReplayState, } from "./message-replay.js";
15
15
  import { LANES_RUNS_REL, readProcStarttime } from "./spawn.js";
16
16
  import { tryReadLane } from "./store.js";
17
+ import { readLanesRegistryFlagFailOpen } from "./registry-flag.js";
18
+ import { LANE_DROPBOX_LIFECYCLE_DELIVERY_FLAG } from "./flag-keys.js";
17
19
  export const DROPBOX_DIR = "dropbox";
18
20
  export const INBOX_NAME = "inbox.jsonl";
19
21
  export const DRAINING_DIR = "draining";
@@ -426,6 +428,9 @@ export function hookAdditionalContextJson(messages, hookEventName = "PreToolUse"
426
428
  },
427
429
  })}\n`;
428
430
  }
431
+ function hookStopBlockJson(reason) {
432
+ return `${JSON.stringify({ decision: "block", reason })}\n`;
433
+ }
429
434
  function replayStatePath(hqRoot, laneId) {
430
435
  return path.join(dropboxDir(hqRoot, laneId), MESSAGE_REPLAY_STATE_NAME);
431
436
  }
@@ -465,14 +470,47 @@ export async function runDrainHook(env, input = {}, deps = {}) {
465
470
  stderr: "hq-lanes-drain: HQ_ROOT is unset while HQ_LANE_ID is set\n",
466
471
  };
467
472
  }
468
- // Liveness first: a failed drain must still leave proof the worker called
469
- // a tool. A failure here is reported, never fatal to delivery.
470
- let stderr = "";
471
- try {
472
- recordToolActivity(hqRoot, laneId);
473
+ const hookEvent = input.hook_event_name ?? input.hookEventName ?? "PreToolUse";
474
+ if (hookEvent !== "PreToolUse" && hookEvent !== "PostToolUse" && hookEvent !== "Stop") {
475
+ return { exitCode: 0, stdout: "", stderr: "" };
473
476
  }
474
- catch (err) {
475
- stderr += `hq-lanes-drain: failed to record tool activity: ${err instanceof Error ? err.message : String(err)}\n`;
477
+ // Grok invokes lifecycle hooks, but their output cannot reach the model.
478
+ // Preserve queued messages for its next deliverable PreToolUse event.
479
+ if (env.HQ_LANE_PROVIDER === "grok" && hookEvent !== "PreToolUse") {
480
+ return { exitCode: 0, stdout: "", stderr: "" };
481
+ }
482
+ const hasQueuedMessages = dropboxInboxHasMessages(hqRoot, laneId);
483
+ if (hookEvent === "Stop" && !hasQueuedMessages) {
484
+ return { exitCode: 0, stdout: "", stderr: "" };
485
+ }
486
+ if ((hookEvent === "PostToolUse" || hookEvent === "Stop") && hasQueuedMessages) {
487
+ const lane = (deps.readLane ?? tryReadLane)(hqRoot, laneId);
488
+ const company = lane?.mesh_company ?? lane?.company ?? undefined;
489
+ let enabled = true;
490
+ try {
491
+ enabled = deps.readLifecycleDeliveryFlag
492
+ ? await deps.readLifecycleDeliveryFlag()
493
+ : await readLanesRegistryFlagFailOpen(LANE_DROPBOX_LIFECYCLE_DELIVERY_FLAG, {
494
+ ...(company ? { company } : {}),
495
+ env,
496
+ });
497
+ }
498
+ catch (err) {
499
+ process.stderr.write(`hq-lanes-drain: lifecycle delivery flag read failed open: ${err instanceof Error ? err.message : String(err)}\n`);
500
+ }
501
+ if (!enabled)
502
+ return { exitCode: 0, stdout: "", stderr: "" };
503
+ }
504
+ // PreToolUse remains the tool-activity liveness touch used by watcher.ts.
505
+ // PostToolUse and Stop add delivery opportunities without extra liveness writes.
506
+ let stderr = "";
507
+ if (hookEvent === "PreToolUse") {
508
+ try {
509
+ recordToolActivity(hqRoot, laneId);
510
+ }
511
+ catch (err) {
512
+ stderr += `hq-lanes-drain: failed to record tool activity: ${err instanceof Error ? err.message : String(err)}\n`;
513
+ }
476
514
  }
477
515
  try {
478
516
  const turnId = env.HQ_LANE_TURN_ID ?? "";
@@ -515,15 +553,22 @@ export async function runDrainHook(env, input = {}, deps = {}) {
515
553
  }
516
554
  }
517
555
  if (replayNote && replayed && replayed.length > 0) {
556
+ const formatted = `${replayNote}\n\n${formatDropboxMessages(replayed)}`;
518
557
  return {
519
558
  exitCode: 0,
520
- stdout: hookAdditionalContextJson(replayed, "PreToolUse", replayNote),
559
+ stdout: hookEvent === "Stop"
560
+ ? hookStopBlockJson(formatted)
561
+ : hookAdditionalContextJson(replayed, hookEvent, replayNote),
521
562
  stderr,
522
563
  };
523
564
  }
524
565
  if (messages.length === 0)
525
566
  return { exitCode: 0, stdout: "", stderr };
526
- return { exitCode: 0, stdout: hookAdditionalContextJson(messages), stderr };
567
+ const formatted = formatDropboxMessages(messages);
568
+ if (hookEvent === "Stop") {
569
+ return { exitCode: 0, stdout: hookStopBlockJson(formatted), stderr };
570
+ }
571
+ return { exitCode: 0, stdout: hookAdditionalContextJson(messages, hookEvent), stderr };
527
572
  }
528
573
  catch (err) {
529
574
  const message = err instanceof Error ? err.message : String(err);
@@ -30,4 +30,14 @@ export declare const LEAD_AUTOPILOT_OUTPOST_NATIVE_LEADS_FLAG = "lanes.autopilot
30
30
  export declare const LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG = "lanes.orphaned-bare-id-dropbox-repair";
31
31
  /** Allows a Claude lane to leave an exhausted profile account pin for an eligible account. */
32
32
  export declare const LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG = "lanes.profile-pin-exhausted-fallback";
33
+ /** Kill switch for PostToolUse and Stop drop-box delivery. Default on. */
34
+ export declare const LANE_DROPBOX_LIFECYCLE_DELIVERY_FLAG = "lanes.dropbox-lifecycle-delivery";
35
+ /** Kill switch for bounded local status fields in lane list/show JSON. Default on. */
36
+ export declare const LANE_STATUS_ROW_FIELDS_FLAG = "lanes.status-row-fields";
37
+ /** Enables conduct-mode SessionStart bootstrapping (defaults on; operator kill switch). */
38
+ export declare const LANE_CONDUCT_SESSION_START_FLAG = "lanes.conduct-session-start";
39
+ /** Kill switch for stopping local orphan lane process trees; off means report-only. */
40
+ export declare const LANE_REAP_STOP_FLAG = "lanes.reap-stop";
41
+ /** Enables same-senior worker lane continuity on create; keep ON as the kill switch default. */
42
+ export declare const LANE_WORKER_CONTINUITY_FLAG = "lanes.worker-continuity";
33
43
  //# sourceMappingURL=flag-keys.d.ts.map
@@ -30,4 +30,14 @@ export const LEAD_AUTOPILOT_OUTPOST_NATIVE_LEADS_FLAG = "lanes.autopilot-outpost
30
30
  export const LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG = "lanes.orphaned-bare-id-dropbox-repair";
31
31
  /** Allows a Claude lane to leave an exhausted profile account pin for an eligible account. */
32
32
  export const LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG = "lanes.profile-pin-exhausted-fallback";
33
+ /** Kill switch for PostToolUse and Stop drop-box delivery. Default on. */
34
+ export const LANE_DROPBOX_LIFECYCLE_DELIVERY_FLAG = "lanes.dropbox-lifecycle-delivery";
35
+ /** Kill switch for bounded local status fields in lane list/show JSON. Default on. */
36
+ export const LANE_STATUS_ROW_FIELDS_FLAG = "lanes.status-row-fields";
37
+ /** Enables conduct-mode SessionStart bootstrapping (defaults on; operator kill switch). */
38
+ export const LANE_CONDUCT_SESSION_START_FLAG = "lanes.conduct-session-start";
39
+ /** Kill switch for stopping local orphan lane process trees; off means report-only. */
40
+ export const LANE_REAP_STOP_FLAG = "lanes.reap-stop";
41
+ /** Enables same-senior worker lane continuity on create; keep ON as the kill switch default. */
42
+ export const LANE_WORKER_CONTINUITY_FLAG = "lanes.worker-continuity";
33
43
  //# sourceMappingURL=flag-keys.js.map
@@ -6,15 +6,18 @@
6
6
  * registers nothing:
7
7
  *
8
8
  * - grok reads every `*.json` under `$GROK_HOME/hooks/`, so the registration
9
- * file in that directory is the registration.
9
+ * file in that directory is the registration. Grok passive events cannot
10
+ * inject model-facing context or block, so its drain stays on PreToolUse.
10
11
  * - claude reads `$CLAUDE_CONFIG_DIR/settings.json` and nothing under
11
- * `hooks/`. The registration is merged into the `hooks.PreToolUse` array of
12
- * that file.
13
- * - codex reads `$CODEX_HOME/config.toml` (`[[hooks.PreToolUse]]`, gated by
14
- * `[features] hooks = true`). The registration is appended to that file.
12
+ * `hooks/`. The registration is merged into PreToolUse, PostToolUse, and
13
+ * Stop arrays in that file.
14
+ * - codex reads `$CODEX_HOME/config.toml` (`[[hooks.<Event>]]`, gated by
15
+ * `[features] hooks = true`). The registration is appended for all three
16
+ * delivery events.
15
17
  *
16
- * Every registration merge keeps what the operator already has and is a
17
- * no-op when the entry is present, because this runs on every spawn.
18
+ * Claude and Codex register all three delivery events. Grok registers only
19
+ * PreToolUse because its PostToolUse and Stop output cannot reach the model.
20
+ * Registration merges keep existing operator hooks and are idempotent.
18
21
  * Never merges into hq-hq-bridge.json / hq-hq-bridge.sh.
19
22
  */
20
23
  import type { LaneProvider } from "./types.js";
@@ -40,9 +43,9 @@ export interface DrainHookInstallResult {
40
43
  files: string[];
41
44
  registration: DrainHookRegistration;
42
45
  }
43
- export declare function drainHookCommand(_shPath?: string): string;
46
+ export declare function drainHookCommand(_shPath?: string, provider?: LaneProvider): string;
44
47
  export declare function drainHookShPath(providerHome: string): string;
45
- export declare function renderDrainHookJson(shPath: string): string;
48
+ export declare function renderDrainHookJson(shPath: string, provider?: LaneProvider): string;
46
49
  export interface MergeResult {
47
50
  text: string;
48
51
  changed: boolean;
@@ -56,9 +59,9 @@ export interface MergeResult {
56
59
  export declare function mergeClaudeSettingsHook(existingText: string | null, command: string, settingsPath: string, legacyCommand?: string): MergeResult;
57
60
  /**
58
61
  * Codex: hooks come from `config.toml` in the codex home. The drain is
59
- * appended as one `[[hooks.PreToolUse]]` table and `[features] hooks = true`
62
+ * appended to PreToolUse, PostToolUse, and Stop, and `[features] hooks = true`
60
63
  * is ensured. Edits are textual so comments and unrelated tables are kept
61
- * byte for byte; the result is re-parsed and must contain the registration,
64
+ * byte for byte; the result is re-parsed and must contain every registration,
62
65
  * otherwise nothing is written.
63
66
  */
64
67
  export declare function mergeCodexConfigHook(existingText: string | null, command: string, configPath: string, legacyCommand?: string): MergeResult;
@@ -6,15 +6,18 @@
6
6
  * registers nothing:
7
7
  *
8
8
  * - grok reads every `*.json` under `$GROK_HOME/hooks/`, so the registration
9
- * file in that directory is the registration.
9
+ * file in that directory is the registration. Grok passive events cannot
10
+ * inject model-facing context or block, so its drain stays on PreToolUse.
10
11
  * - claude reads `$CLAUDE_CONFIG_DIR/settings.json` and nothing under
11
- * `hooks/`. The registration is merged into the `hooks.PreToolUse` array of
12
- * that file.
13
- * - codex reads `$CODEX_HOME/config.toml` (`[[hooks.PreToolUse]]`, gated by
14
- * `[features] hooks = true`). The registration is appended to that file.
12
+ * `hooks/`. The registration is merged into PreToolUse, PostToolUse, and
13
+ * Stop arrays in that file.
14
+ * - codex reads `$CODEX_HOME/config.toml` (`[[hooks.<Event>]]`, gated by
15
+ * `[features] hooks = true`). The registration is appended for all three
16
+ * delivery events.
15
17
  *
16
- * Every registration merge keeps what the operator already has and is a
17
- * no-op when the entry is present, because this runs on every spawn.
18
+ * Claude and Codex register all three delivery events. Grok registers only
19
+ * PreToolUse because its PostToolUse and Stop output cannot reach the model.
20
+ * Registration merges keep existing operator hooks and are idempotent.
18
21
  * Never merges into hq-hq-bridge.json / hq-hq-bridge.sh.
19
22
  */
20
23
  import * as fs from "node:fs";
@@ -44,8 +47,9 @@ export const PROVIDER_REGISTRATION_KIND = {
44
47
  codex: "config_toml",
45
48
  grok: "hooks_dir",
46
49
  };
47
- export function drainHookCommand(_shPath) {
48
- return `${shellQuote(process.execPath)} ${shellQuote(drainHookEntryPath())}`;
50
+ export function drainHookCommand(_shPath, provider) {
51
+ const providerEnv = provider === "grok" ? "HQ_LANE_PROVIDER=grok " : "";
52
+ return `${providerEnv}${shellQuote(process.execPath)} ${shellQuote(drainHookEntryPath())}`;
49
53
  }
50
54
  function legacyDrainHookCommand(shPath) {
51
55
  return `bash ${shPath}`;
@@ -148,7 +152,10 @@ function isInstalledDrainEntry(entryPath) {
148
152
  function drainCommandsMatch(candidate, expected) {
149
153
  if (candidate === expected)
150
154
  return true;
151
- const argv = parseShellQuotedArguments(candidate);
155
+ const withoutProviderEnv = candidate.startsWith("HQ_LANE_PROVIDER=grok ")
156
+ ? candidate.slice("HQ_LANE_PROVIDER=grok ".length)
157
+ : candidate;
158
+ const argv = parseShellQuotedArguments(withoutProviderEnv);
152
159
  if (argv === null || argv.length !== 2)
153
160
  return false;
154
161
  if (path.parse(argv[0]).name.toLowerCase() !== "node")
@@ -170,7 +177,9 @@ function groupsContainDrainCommand(groups, expected) {
170
177
  });
171
178
  }
172
179
  function tomlHasDrainCommand(parsed, expected) {
173
- return isPlainObject(parsed.hooks) && groupsContainDrainCommand(parsed.hooks.PreToolUse, expected);
180
+ const hooks = isPlainObject(parsed.hooks) ? parsed.hooks : null;
181
+ return hooks !== null &&
182
+ ["PreToolUse", "PostToolUse", "Stop"].every((event) => groupsContainDrainCommand(hooks[event], expected));
174
183
  }
175
184
  function removeCommandFromGroups(groups, command) {
176
185
  let changed = false;
@@ -187,22 +196,14 @@ function removeCommandFromGroups(groups, command) {
187
196
  });
188
197
  return { groups: next, changed };
189
198
  }
190
- export function renderDrainHookJson(shPath) {
191
- return `${JSON.stringify({
192
- hooks: {
193
- PreToolUse: [
194
- {
195
- hooks: [
196
- {
197
- type: "command",
198
- command: drainHookCommand(shPath),
199
- timeout: DRAIN_HOOK_TIMEOUT_SECONDS,
200
- },
201
- ],
202
- },
203
- ],
204
- },
205
- }, null, 2)}\n`;
199
+ export function renderDrainHookJson(shPath, provider = "grok") {
200
+ const command = drainHookCommand(shPath, provider);
201
+ const registration = {
202
+ hooks: Object.fromEntries(["PreToolUse"].map((event) => [event, [
203
+ { hooks: [{ type: "command", command, timeout: DRAIN_HOOK_TIMEOUT_SECONDS }] },
204
+ ]])),
205
+ };
206
+ return `${JSON.stringify(registration, null, 2)}\n`;
206
207
  }
207
208
  /**
208
209
  * Claude Code: `settings.json` is the one file it reads hooks from. Merge the
@@ -227,27 +228,35 @@ export function mergeClaudeSettingsHook(existingText, command, settingsPath, leg
227
228
  if (!isPlainObject(hooks)) {
228
229
  throw new LanesError("settings_unparseable", `${settingsPath} has a non-object "hooks" key; refusing to rewrite it`, { path: settingsPath });
229
230
  }
230
- const pre = hooks.PreToolUse === undefined ? [] : hooks.PreToolUse;
231
+ const events = ["PreToolUse", "PostToolUse", "Stop"];
232
+ const eventGroups = new Map();
233
+ for (const event of events) {
234
+ const groups = hooks[event] === undefined ? [] : hooks[event];
235
+ if (!Array.isArray(groups)) {
236
+ throw new LanesError("settings_unparseable", `${settingsPath} has a non-array "hooks.${event}"; refusing to rewrite it`, { path: settingsPath });
237
+ }
238
+ eventGroups.set(event, groups);
239
+ }
240
+ const pre = eventGroups.get("PreToolUse");
231
241
  if (!Array.isArray(pre)) {
232
242
  throw new LanesError("settings_unparseable", `${settingsPath} has a non-array "hooks.PreToolUse"; refusing to rewrite it`, { path: settingsPath });
233
243
  }
234
244
  const migration = legacyCommand && legacyCommand !== command
235
245
  ? removeCommandFromGroups(pre, legacyCommand)
236
246
  : { groups: pre, changed: false };
237
- if (groupsContainCommand(migration.groups, command) && !migration.changed) {
247
+ const allRegistered = events.every((event) => groupsContainCommand(eventGroups.get(event), command));
248
+ if (allRegistered && !migration.changed) {
238
249
  return { text: existingText ?? "", changed: false };
239
250
  }
240
- const nextPre = groupsContainCommand(migration.groups, command)
241
- ? migration.groups
242
- : [
243
- ...migration.groups,
244
- {
245
- hooks: [
246
- { type: "command", command, timeout: DRAIN_HOOK_TIMEOUT_SECONDS },
247
- ],
248
- },
251
+ const nextHooks = { ...hooks };
252
+ for (const event of events) {
253
+ const groups = event === "PreToolUse" ? migration.groups : eventGroups.get(event);
254
+ nextHooks[event] = groupsContainCommand(groups, command) ? groups : [
255
+ ...groups,
256
+ { hooks: [{ type: "command", command, timeout: DRAIN_HOOK_TIMEOUT_SECONDS }] },
249
257
  ];
250
- const nextRoot = { ...root, hooks: { ...hooks, PreToolUse: nextPre } };
258
+ }
259
+ const nextRoot = { ...root, hooks: nextHooks };
251
260
  return { text: `${JSON.stringify(nextRoot, null, 2)}\n`, changed: true };
252
261
  }
253
262
  function tomlBasicString(value) {
@@ -424,9 +433,9 @@ function enableTomlHooksFeature(text) {
424
433
  }
425
434
  /**
426
435
  * Codex: hooks come from `config.toml` in the codex home. The drain is
427
- * appended as one `[[hooks.PreToolUse]]` table and `[features] hooks = true`
436
+ * appended to PreToolUse, PostToolUse, and Stop, and `[features] hooks = true`
428
437
  * is ensured. Edits are textual so comments and unrelated tables are kept
429
- * byte for byte; the result is re-parsed and must contain the registration,
438
+ * byte for byte; the result is re-parsed and must contain every registration,
430
439
  * otherwise nothing is written.
431
440
  */
432
441
  export function mergeCodexConfigHook(existingText, command, configPath, legacyCommand) {
@@ -435,7 +444,8 @@ export function mergeCodexConfigHook(existingText, command, configPath, legacyCo
435
444
  const migration = removeLegacyCodexHook(original, parsed, legacyCommand, configPath);
436
445
  const textAfterMigration = migration.text;
437
446
  const current = parseTomlOrThrow(textAfterMigration, configPath, "after legacy hook removal");
438
- const hasCommand = tomlHasCommand(current, command);
447
+ const hasPreToolCommand = tomlHasCommand(current, command);
448
+ const hasCommand = tomlHasDrainCommand(current, command);
439
449
  const featureOn = tomlHooksFeatureEnabled(current);
440
450
  if (hasCommand && featureOn && !migration.changed) {
441
451
  return { text: original, changed: false };
@@ -443,7 +453,7 @@ export function mergeCodexConfigHook(existingText, command, configPath, legacyCo
443
453
  let text = textAfterMigration;
444
454
  if (!featureOn)
445
455
  text = enableTomlHooksFeature(text);
446
- if (!hasCommand) {
456
+ if (!hasPreToolCommand) {
447
457
  const sep = text === "" || text.endsWith("\n") ? "" : "\n";
448
458
  text =
449
459
  `${text}${sep}\n` +
@@ -455,8 +465,18 @@ export function mergeCodexConfigHook(existingText, command, configPath, legacyCo
455
465
  `command = ${tomlBasicString(command)}\n` +
456
466
  `timeout = ${DRAIN_HOOK_TIMEOUT_SECONDS}\n`;
457
467
  }
468
+ for (const event of ["PostToolUse", "Stop"]) {
469
+ if (isPlainObject(current.hooks) && groupsContainCommand(current.hooks[event], command))
470
+ continue;
471
+ const sep = text === "" || text.endsWith("\n") ? "" : "\n";
472
+ text += `${sep}\n# hq lanes: drop-box delivery\n[[hooks.${event}]]\nmatcher = ".*"\n\n[[hooks.${event}.hooks]]\ntype = "command"\ncommand = ${tomlBasicString(command)}\ntimeout = ${DRAIN_HOOK_TIMEOUT_SECONDS}\n`;
473
+ }
458
474
  const check = parseTomlOrThrow(text, configPath, "after merge");
459
- if (!tomlHasCommand(check, command) || !tomlHooksFeatureEnabled(check)) {
475
+ if (!tomlHasCommand(check, command) ||
476
+ !isPlainObject(check.hooks) ||
477
+ !groupsContainCommand(check.hooks.PostToolUse, command) ||
478
+ !groupsContainCommand(check.hooks.Stop, command) ||
479
+ !tomlHooksFeatureEnabled(check)) {
460
480
  throw new LanesError("config_toml_merge_failed", `Merged ${configPath} does not carry the drain registration; refusing to write it`, { path: configPath, command });
461
481
  }
462
482
  return { text, changed: true };
@@ -479,7 +499,7 @@ export function probeDrainHookRegistration(provider, providerHome) {
479
499
  const parsed = parseLaneProvider(provider);
480
500
  const kind = PROVIDER_REGISTRATION_KIND[parsed];
481
501
  const regPath = drainHookRegistrationPath(parsed, providerHome);
482
- const command = drainHookCommand(drainHookShPath(providerHome));
502
+ const command = drainHookCommand(drainHookShPath(providerHome), parsed);
483
503
  const text = readTextIfExists(regPath);
484
504
  const base = { kind, path: regPath, command, file_present: text !== null };
485
505
  if (text === null)
@@ -493,11 +513,11 @@ export function probeDrainHookRegistration(provider, providerHome) {
493
513
  };
494
514
  }
495
515
  const json = JSON.parse(text);
516
+ const hooks = isPlainObject(json) && isPlainObject(json.hooks) ? json.hooks : null;
496
517
  return {
497
518
  ...base,
498
- registered: isPlainObject(json) &&
499
- isPlainObject(json.hooks) &&
500
- groupsContainDrainCommand(json.hooks.PreToolUse, command),
519
+ registered: hooks !== null &&
520
+ (parsed === "grok" ? ["PreToolUse"] : ["PreToolUse", "PostToolUse", "Stop"]).every((event) => groupsContainDrainCommand(hooks[event], command)),
501
521
  };
502
522
  }
503
523
  catch (err) {
@@ -508,9 +528,9 @@ export function probeDrainHookRegistration(provider, providerHome) {
508
528
  function registerDrainHook(provider, providerHome, shPath) {
509
529
  const kind = PROVIDER_REGISTRATION_KIND[provider];
510
530
  const regPath = drainHookRegistrationPath(provider, providerHome);
511
- const command = drainHookCommand(shPath);
531
+ const command = drainHookCommand(shPath, provider);
512
532
  if (kind === "hooks_dir") {
513
- const changed = writeIfChanged(regPath, renderDrainHookJson(shPath), 0o644);
533
+ const changed = writeIfChanged(regPath, renderDrainHookJson(shPath, provider), 0o644);
514
534
  return { kind, path: regPath, command, changed };
515
535
  }
516
536
  const existing = readTextIfExists(regPath);
@@ -422,7 +422,7 @@ export function monitorReminderText() {
422
422
  "For active Claude senior lanes, arm one live Claude Code Monitor task per uncovered lane:",
423
423
  'Monitor(command="hq lanes watch <lane-id>...", timeout_ms=1800000, persistent=true)',
424
424
  "Use `hq lanes monitor-check --session <id>` to verify coverage. `persistent=true` is requested where the installed Monitor tool honors it; Claude Code caps Monitor tasks at 30 minutes, so re-arm after expiry.",
425
- "The caller must pass `--engine codex` or `--engine grok` (or set HQ_CONDUCT_ENGINE) for sessions whose engine has no Claude Monitor tool; those engines are never required to provide one.",
425
+ "The caller must pass `--engine codex` or `--engine grok` (or set HQ_LANE_ENGINE; deprecated alias HQ_CONDUCT_ENGINE) for sessions whose engine has no Claude Monitor tool; those engines are never required to provide one.",
426
426
  ].join("\n");
427
427
  }
428
428
  //# sourceMappingURL=monitor.js.map
@@ -0,0 +1,58 @@
1
+ import type { LaneRecord } from "./types.js";
2
+ export declare const DEFAULT_REAP_MIN_AGE_MINUTES = 120;
3
+ export declare const REAP_SINGLE_FLIGHT_LOCK_STALE_MS: number;
4
+ export declare const REAP_RECENT_SWEEP_MS: number;
5
+ export interface ReapProcess {
6
+ pid: number;
7
+ ppid: number;
8
+ args: string;
9
+ }
10
+ export interface ReapFinding {
11
+ lane_id: string;
12
+ orphaned: boolean;
13
+ reason: string;
14
+ age_minutes: number;
15
+ session_id?: string;
16
+ pid?: number;
17
+ process_starttime?: number | null;
18
+ would_stop?: boolean;
19
+ stopped?: boolean;
20
+ stop_complete?: boolean;
21
+ stop_error?: string;
22
+ stale_state?: string[];
23
+ }
24
+ export interface ReapDeps {
25
+ now?: () => Date;
26
+ hostname?: () => string;
27
+ processes?: () => ReapProcess[];
28
+ isAlive?: (pid: number) => boolean;
29
+ readStarttime?: (pid: number) => number | null;
30
+ sessionState?: (root: string, sessionId: string, now: Date) => string | null;
31
+ stopEnabled?: (lane: LaneRecord) => Promise<boolean> | boolean;
32
+ stopLane?: (laneId: string, reason: string) => Promise<void>;
33
+ sleep?: (ms: number) => void;
34
+ }
35
+ export declare function parseReapMinAge(value: string | undefined): number;
36
+ export declare function isProcessRunning(pid: number): boolean;
37
+ export declare function classifyLocalLanes(root: string, now: Date, minAgeMinutes: number, deps?: ReapDeps): Promise<ReapFinding[]>;
38
+ export interface ReapOptions {
39
+ minAgeMinutes: number;
40
+ stop: boolean;
41
+ applyStaleState: boolean;
42
+ reason: string;
43
+ json: boolean;
44
+ }
45
+ export interface ReapSummary {
46
+ ok: true;
47
+ action: "lanes.reap";
48
+ generated_at: string;
49
+ min_age_minutes: number;
50
+ mode: "report" | "apply_stale_state" | "stop";
51
+ stop_enabled: boolean;
52
+ skipped?: "already_running" | "recent_sweep";
53
+ lanes: ReapFinding[];
54
+ }
55
+ export declare function reapLocalLanes(root: string, options: ReapOptions, deps?: ReapDeps): Promise<ReapSummary>;
56
+ /** Internal alias used by session-start hooks; it has no shell or OS-specific dependency. */
57
+ export declare const runSessionStartReap: typeof reapLocalLanes;
58
+ //# sourceMappingURL=reaper.d.ts.map