@indigoai-us/hq-cli 5.300.0 → 5.302.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 +42 -0
  2. package/assets/scaffold/core/scripts/rebuild-projects-index.sh +99 -12
  3. package/assets/scaffold/core/scripts/token-usage-report.sh +9 -0
  4. package/dist/command-catalog.generated.d.ts +32 -3
  5. package/dist/command-catalog.generated.js +39 -3
  6. package/dist/commands/__fixtures__/access-vault.d.ts +2 -0
  7. package/dist/commands/__fixtures__/access-vault.js +1 -1
  8. package/dist/commands/access.d.ts +2 -1
  9. package/dist/commands/access.js +58 -49
  10. package/dist/commands/bot.js +1 -0
  11. package/dist/commands/cloud-provision.js +51 -2
  12. package/dist/commands/core-checkpoint.js +1 -0
  13. package/dist/commands/core-company-index-scope-flag.d.ts +3 -0
  14. package/dist/commands/core-company-index-scope-flag.js +6 -0
  15. package/dist/commands/core-handoff-fast.js +1 -0
  16. package/dist/commands/core-handoff.js +2 -0
  17. package/dist/commands/core-job-native-utils.d.ts +6 -0
  18. package/dist/commands/core-job-native-utils.js +80 -0
  19. package/dist/commands/core-job-native.d.ts +2 -4
  20. package/dist/commands/core-job-native.js +6 -10
  21. package/dist/commands/core-job-notify-native.d.ts +36 -0
  22. package/dist/commands/core-job-notify-native.js +359 -0
  23. package/dist/commands/core-job-remediate-native.d.ts +26 -0
  24. package/dist/commands/core-job-remediate-native.js +374 -0
  25. package/dist/commands/core.js +26 -8
  26. package/dist/commands/deploy-comments.js +5 -2
  27. package/dist/commands/files.js +31 -14
  28. package/dist/commands/integrations-api.d.ts +6 -6
  29. package/dist/commands/integrations-api.js +4 -13
  30. package/dist/commands/integrations-connect.d.ts +3 -0
  31. package/dist/commands/integrations-connect.js +21 -2
  32. package/dist/commands/integrations-manage.js +1 -4
  33. package/dist/commands/lanes-message.js +43 -5
  34. package/dist/commands/lanes-questions.d.ts +4 -2
  35. package/dist/commands/lanes-questions.js +123 -2
  36. package/dist/commands/lanes-resume.js +25 -4
  37. package/dist/commands/lanes-shared.d.ts +15 -1
  38. package/dist/commands/lanes-watch.js +5 -1
  39. package/dist/commands/lanes.d.ts +2 -0
  40. package/dist/commands/lanes.js +92 -9
  41. package/dist/commands/meetings.js +35 -16
  42. package/dist/commands/monitor.js +17 -2
  43. package/dist/commands/report.js +5 -2
  44. package/dist/commands/rescue.d.ts +4 -0
  45. package/dist/commands/rescue.js +27 -17
  46. package/dist/commands/safe-extract.d.ts +4 -4
  47. package/dist/commands/safe-extract.js +6 -7
  48. package/dist/lib/agent-kit/fallback.d.ts +1 -0
  49. package/dist/lib/agent-kit/fallback.js +1 -0
  50. package/dist/lib/bot/daemon.d.ts +5 -1
  51. package/dist/lib/bot/daemon.js +4 -4
  52. package/dist/lib/core-utils/soft-timeout.js +1 -1
  53. package/dist/lib/core-utils/token-usage-report.js +17 -3
  54. package/dist/lib/deploy-apps.d.ts +13 -0
  55. package/dist/lib/deploy-apps.js +24 -0
  56. package/dist/lib/index-render/companies.d.ts +4 -1
  57. package/dist/lib/index-render/companies.js +66 -3
  58. package/dist/lib/index-render/index.d.ts +1 -0
  59. package/dist/lib/index-render/index.js +2 -1
  60. package/dist/lib/index-render/projects.js +82 -5
  61. package/dist/lib/index-render/shared.d.ts +1 -1
  62. package/dist/lib/index-render/shared.js +4 -2
  63. package/dist/lib/lanes/delivery.d.ts +7 -3
  64. package/dist/lib/lanes/delivery.js +18 -8
  65. package/dist/lib/lanes/index.d.ts +1 -1
  66. package/dist/lib/lanes/keep-alive.d.ts +13 -3
  67. package/dist/lib/lanes/keep-alive.js +29 -5
  68. package/dist/lib/lanes/outpost-dispatch.d.ts +5 -0
  69. package/dist/lib/lanes/outpost-dispatch.js +12 -0
  70. package/dist/lib/lanes/reconcile.d.ts +1 -0
  71. package/dist/lib/lanes/reconcile.js +358 -38
  72. package/dist/lib/lanes/spawn.d.ts +1 -0
  73. package/dist/lib/lanes/spawn.js +20 -10
  74. package/dist/lib/lanes/store.js +24 -0
  75. package/dist/lib/lanes/story-ledger.d.ts +2 -0
  76. package/dist/lib/lanes/story-ledger.js +8 -0
  77. package/dist/lib/lanes/types.d.ts +26 -4
  78. package/dist/lib/lanes/types.js +33 -1
  79. package/dist/lib/lanes/watch.d.ts +1 -0
  80. package/dist/lib/lanes/watch.js +19 -6
  81. package/dist/lib/lanes/watcher.js +139 -9
  82. package/dist/lib/mesh/live/daemon/mqtt-events.d.ts +7 -0
  83. package/dist/lib/mesh/live/daemon/mqtt-events.js +2 -0
  84. package/dist/lib/mesh/live/daemon/mqtt-events.test-d.d.ts +2 -0
  85. package/dist/lib/mesh/live/daemon/mqtt-events.test-d.js +10 -0
  86. package/dist/lib/mesh/live/daemon/presence.d.ts +2 -2
  87. package/dist/lib/mesh/live/daemon/run.js +36 -2
  88. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +2 -2
  89. package/dist/lib/search-index/background.d.ts +1 -0
  90. package/dist/lib/search-index/background.js +1 -0
  91. package/dist/utils/self-update.js +51 -4
  92. package/dist/utils/staged-npm-install.d.ts +16 -0
  93. package/dist/utils/staged-npm-install.js +264 -0
  94. package/dist/utils/update-command-supervisor.cjs +54 -1
  95. package/dist/utils/version-gate.d.ts +8 -0
  96. package/dist/utils/version-gate.js +83 -16
  97. package/package.json +2 -2
  98. package/dist/lib/install-path-compatibility-flag.d.ts +0 -9
  99. package/dist/lib/install-path-compatibility-flag.js +0 -12
@@ -18,14 +18,14 @@ import { AccountResolutionError, LanesError, isLaneVersionConflict } from "./err
18
18
  import { loadWorkerProfile } from "./profile.js";
19
19
  import { terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./grants.js";
20
20
  import { classifySessionLiveness, defaultLivenessDeps, loadLivenessIndex, readDaemonPresenceConnected, unindexedClaudeSessionLiveness, } from "./liveness.js";
21
- import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecordAlive, laneRunDir, readPidRecord, readProcStarttime, readSpawnManifest, markSpawnFailure, spawnEnvelopeRepair, spawnLaneWorker, retryLaneOnAlternateAccount, } from "./spawn.js";
21
+ import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecordAlive, laneRunDir, readPidRecord, readProcStarttime, readSpawnManifest, markSpawnFailure, stopLaneProcesses, spawnEnvelopeRepair, spawnLaneWorker, retryLaneOnAlternateAccount, } from "./spawn.js";
22
22
  import { appendCappedHistory, lanesStoreRoot, parseLaneRecord, parseQuestionRecord, readLane, readQuestion, listQuestionIds, updateLane, updateQuestion, } from "./store.js";
23
23
  import { applyThrottleDecision, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyObservation, recordCpuBusyObservation, throttleStatePath, writeThrottleState, } from "./throttle.js";
24
24
  import { readLanesRegistryFlag } from "./registry-flag.js";
25
25
  import { resolveLaneCompany } from "./lane-company.js";
26
26
  import { appendDropboxMessage } from "./dropbox.js";
27
27
  import { DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG, listHostProcesses, runRunawayToolGuard, RUNAWAY_TOOL_GUARD_FLAG, } from "./runaway-tool-guard.js";
28
- import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG, LEAD_LANE_KEEP_ALIVE_BLOCKED_GRACE_MS, LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT, queueLaneTerminalResume, LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, LEAD_LANE_KEEP_ALIVE_RESUME_TEXT, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, undeliveredAnsweredQuestions, } from "./keep-alive.js";
28
+ import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG, LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG, LEAD_LANE_KEEP_ALIVE_BLOCKED_GRACE_MS, LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT, queueLaneTerminalResume, LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, LEAD_LANE_KEEP_ALIVE_RESUME_TEXT, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, undeliveredAnsweredQuestions, } from "./keep-alive.js";
29
29
  import { providerSupportsSessionResume } from "./delivery.js";
30
30
  import { applyDefaultQuotaHold, clearAccountQuotaAfterSuccessfulTurnBestEffort, readQuotaSignal, recordAccountQuota, } from "./quota.js";
31
31
  import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
@@ -33,6 +33,7 @@ import { SILENCE_MS, appendWatcherEvent, classifyLaneExit, isPidAlive, missingEx
33
33
  import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshCompany, } from "./mesh-events.js";
34
34
  import { activeLaneStories, laneStories, terminalEnvelopeStoryStatus } from "./stories.js";
35
35
  import { applyStorySwitchEnvelope } from "./story-switch.js";
36
+ import { readProjectStoryStates } from "./story-ledger.js";
36
37
  import { decideEnvelopeRepair, envelopeQuestionIds, routeProviderFailure } from "./watcher.js";
37
38
  import { PROVIDER_REJECTED_REASON, providerRejectionDetails, readProviderRejection, } from "./provider-rejection.js";
38
39
  /** Spec §9: unanswered at the current hop for 60 minutes → one hop. */
@@ -46,6 +47,8 @@ export const RECONCILE_LOCK_TIMEOUT_MS = 5_000;
46
47
  /** A pickup probe that has not cleared its marker within this window is abandoned. */
47
48
  export const PICKUP_VALIDATION_MAX_AGE_MS = 15 * 60 * 1000;
48
49
  export const PICKUP_VALIDATION_TIMEOUT_REASON = "pickup_validation_timeout";
50
+ export const CLOSE_INTERRUPTED_DONE_STORY_WORKERS_FLAG = "lanes.close-interrupted-done-story-workers";
51
+ const INTERRUPTED_DONE_STORY_MIN_AGE_MS = 24 * 60 * 60 * 1000;
49
52
  export const RECONCILE_UNIT_NAME = "hq-lanes-reconcile.service";
50
53
  export const RECONCILE_TIMER_NAME = "hq-lanes-reconcile.timer";
51
54
  function spawnDepsForHost(deps) {
@@ -324,6 +327,129 @@ function casLaneIf(hqRoot, laneId, shouldWrite, updater, dryRun) {
324
327
  }
325
328
  throw new LanesError("version_conflict", `Exhausted CAS retries for lane ${laneId}`, { lane_id: laneId });
326
329
  }
330
+ function laneStoryLedgerIsClosed(hqRoot, lane) {
331
+ const stories = laneStories(lane);
332
+ if (stories.length === 0)
333
+ return { closed: false, states: [] };
334
+ const states = [];
335
+ for (const projectId of new Set(stories.map((story) => story.project_id))) {
336
+ const projectStories = stories.filter((story) => story.project_id === projectId);
337
+ const ledgerStates = readProjectStoryStates(hqRoot, projectId, projectStories.map((story) => story.story_id), lane.company?.slug);
338
+ for (const story of projectStories) {
339
+ const state = ledgerStates.get(story.story_id);
340
+ if (state === undefined)
341
+ return { closed: false, states: [...states, "missing"] };
342
+ states.push(state);
343
+ }
344
+ }
345
+ return { closed: states.length > 0 && states.every((state) => state === "done" || state === "dropped"), states };
346
+ }
347
+ async function closeInterruptedDoneStoryWorkers(hqRoot, lanes, questions, summary, deps, now, enabledByCompany) {
348
+ if (deps.dryRun)
349
+ return;
350
+ const nowMs = now.getTime();
351
+ const at = now.toISOString();
352
+ for (const lane of lanes) {
353
+ if (lane.depth < 1 || lane.state !== "interrupted" || lane.keep_alive === true ||
354
+ !laneRunsHere(lane, deps.host) || !lane.interrupted_at ||
355
+ !Number.isFinite(Date.parse(lane.interrupted_at)) ||
356
+ nowMs - Date.parse(lane.interrupted_at) < INTERRUPTED_DONE_STORY_MIN_AGE_MS ||
357
+ hasPendingKeepAliveWork(hqRoot, lane, questions))
358
+ continue;
359
+ let company;
360
+ try {
361
+ company = resolveLaneCompany(hqRoot, lane).company;
362
+ }
363
+ catch (err) {
364
+ markDirty(summary, { kind: "lane", id: lane.lane_id, why: `story close company lookup failed: ${err instanceof Error ? err.message : String(err)}` });
365
+ continue;
366
+ }
367
+ if (!company || enabledByCompany.get(keepAliveCompanyKey(company)) !== true)
368
+ continue;
369
+ let ledger;
370
+ try {
371
+ ledger = laneStoryLedgerIsClosed(hqRoot, lane);
372
+ }
373
+ catch (err) {
374
+ markDirty(summary, { kind: "lane", id: lane.lane_id, why: `story close ledger read failed: ${err instanceof Error ? err.message : String(err)}` });
375
+ continue;
376
+ }
377
+ if (!ledger.closed)
378
+ continue;
379
+ try {
380
+ const result = casLaneIf(hqRoot, lane.lane_id, (current) => {
381
+ const currentCompany = resolveLaneCompany(hqRoot, current).company;
382
+ if (current.state !== "interrupted" || current.depth < 1 || current.keep_alive === true ||
383
+ !currentCompany || enabledByCompany.get(keepAliveCompanyKey(currentCompany)) !== true ||
384
+ !laneRunsHere(current, deps.host) || !current.interrupted_at ||
385
+ !Number.isFinite(Date.parse(current.interrupted_at)) ||
386
+ nowMs - Date.parse(current.interrupted_at) < INTERRUPTED_DONE_STORY_MIN_AGE_MS ||
387
+ hasPendingKeepAliveWork(hqRoot, current, questions))
388
+ return false;
389
+ return laneStoryLedgerIsClosed(hqRoot, current).closed;
390
+ }, (current) => ({
391
+ ...current,
392
+ state: "killed",
393
+ queued_reason: undefined,
394
+ queued_details: undefined,
395
+ updated_at: at,
396
+ terminal_reason: "story_closed",
397
+ terminal_details: { ...(current.terminal_details ?? {}), operator_reason: "story_closed" },
398
+ stop_history: [...(current.stop_history ?? []), { at, reason: "story_closed" }],
399
+ }), false);
400
+ if (!result.changed)
401
+ continue;
402
+ stopLaneProcesses(hqRoot, lane.lane_id, {
403
+ killProcess: deps.spawnLaneDeps?.killProcess,
404
+ isAlive: deps.isAlive,
405
+ });
406
+ for (const question of questions) {
407
+ if (question.origin.id !== lane.lane_id || question.status !== "pending")
408
+ continue;
409
+ casQuestionIf(hqRoot, question.question_id, (current) => current.status === "pending", (current) => ({
410
+ ...current,
411
+ status: "cancelled",
412
+ routing_history: appendCappedHistory(current.routing_history, {
413
+ hop: current.hop,
414
+ audience: { kind: "user" },
415
+ at,
416
+ reason: "cancelled",
417
+ }),
418
+ }), false);
419
+ }
420
+ appendWatcherEvent(watcherEventsPath(hqRoot, lane.lane_id), {
421
+ event: "story_closed",
422
+ at,
423
+ lane_id: lane.lane_id,
424
+ story_state: [...new Set(ledger.states)].join(","),
425
+ terminal_reason: "story_closed",
426
+ });
427
+ const mesh = deps.mesh === undefined ? createLaneMesh({ interactive: false }) : deps.mesh;
428
+ if (mesh) {
429
+ const timeoutMs = deps.meshEmitTimeoutMs ?? 10_000;
430
+ let timer;
431
+ try {
432
+ await Promise.race([
433
+ emitLaneTerminalStateEvent(mesh, result.record, "killed", "story_closed"),
434
+ new Promise((_resolve, reject) => {
435
+ timer = setTimeout(() => reject(new Error(`terminal mesh emit timed out after ${timeoutMs}ms`)), timeoutMs);
436
+ }),
437
+ ]);
438
+ }
439
+ catch (err) {
440
+ markDirty(summary, { kind: "lane", id: lane.lane_id, why: `story close mesh emit failed: ${err instanceof Error ? err.message : String(err)}` });
441
+ }
442
+ finally {
443
+ if (timer)
444
+ clearTimeout(timer);
445
+ }
446
+ }
447
+ }
448
+ catch (err) {
449
+ markDirty(summary, { kind: "lane", id: lane.lane_id, why: `story close failed: ${err instanceof Error ? err.message : String(err)}` });
450
+ }
451
+ }
452
+ }
327
453
  function writeQueueReason(hqRoot, lane, reason, dryRun) {
328
454
  if (lane.queued_reason === reason)
329
455
  return;
@@ -929,7 +1055,39 @@ export async function runPassBody(hqRoot, deps) {
929
1055
  }
930
1056
  }
931
1057
  const readRegistryFlag = deps.readRegistryFlag ?? readLanesRegistryFlag;
1058
+ const storyCloseCompanies = new Map();
1059
+ for (const lane of lanes) {
1060
+ if (lane.depth < 1 || lane.state !== "interrupted" || lane.keep_alive === true ||
1061
+ !laneRunsHere(lane, deps.host) || !lane.interrupted_at ||
1062
+ !Number.isFinite(Date.parse(lane.interrupted_at)) ||
1063
+ nowMs - Date.parse(lane.interrupted_at) < INTERRUPTED_DONE_STORY_MIN_AGE_MS ||
1064
+ hasPendingKeepAliveWork(hqRoot, lane, questions) || laneStories(lane).length === 0)
1065
+ continue;
1066
+ try {
1067
+ const company = resolveLaneCompany(hqRoot, lane).company;
1068
+ if (company)
1069
+ storyCloseCompanies.set(keepAliveCompanyKey(company), company);
1070
+ }
1071
+ catch (err) {
1072
+ markDirty(summary, { kind: "lane", id: lane.lane_id, why: `story close company lookup failed: ${err instanceof Error ? err.message : String(err)}` });
1073
+ }
1074
+ }
1075
+ const storyCloseEnabledByCompany = new Map();
1076
+ const env = deps.env ?? process.env;
1077
+ await Promise.all([...storyCloseCompanies.entries()].map(async ([key, company]) => {
1078
+ try {
1079
+ storyCloseEnabledByCompany.set(key, await readRegistryFlag(CLOSE_INTERRUPTED_DONE_STORY_WORKERS_FLAG, {
1080
+ company: company ?? undefined,
1081
+ env,
1082
+ }));
1083
+ }
1084
+ catch (err) {
1085
+ storyCloseEnabledByCompany.set(key, false);
1086
+ markDirty(summary, { kind: "lane", why: `story close flag read failed for ${company?.slug ?? "unresolved company"}: ${err instanceof Error ? err.message : String(err)}` });
1087
+ }
1088
+ }));
932
1089
  const keepAliveFlagCompanies = new Map();
1090
+ const nextCheckAtFlagCompanyKeys = new Set();
933
1091
  for (const lane of lanes) {
934
1092
  if (!(lane.state === "done" || lane.state === "interrupted" || lane.state === "blocked" || lane.state === "killed" || lane.state === "cancelled" || lane.state === "failed") ||
935
1093
  (lane.keep_alive !== true && lane.worker_profile === undefined) ||
@@ -940,6 +1098,7 @@ export async function runPassBody(hqRoot, deps) {
940
1098
  !providerSupportsSessionResume(lane.provider) ||
941
1099
  !(lane.provider_session_id ?? lane.last_resume?.session_id))))
942
1100
  continue;
1101
+ let hasNextCheckAt = false;
943
1102
  if (lane.state === "done") {
944
1103
  const envelope = readLatestEnvelopeRecord(hqRoot, lane.lane_id)?.envelope;
945
1104
  if (!envelope || envelope.decision !== "done")
@@ -947,17 +1106,20 @@ export async function runPassBody(hqRoot, deps) {
947
1106
  const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane, questions);
948
1107
  if (doneKeepAliveVerdict(lane, envelope, hasQueuedWork) !== "eligible")
949
1108
  continue;
1109
+ hasNextCheckAt = envelope.next_check_at !== undefined;
950
1110
  }
951
1111
  const company = resolveLaneCompany(hqRoot, lane).company;
952
1112
  if (company === null)
953
1113
  continue;
954
1114
  const key = keepAliveCompanyKey(company);
955
1115
  keepAliveFlagCompanies.set(key, company);
1116
+ if (hasNextCheckAt)
1117
+ nextCheckAtFlagCompanyKeys.add(key);
956
1118
  }
957
1119
  const keepAliveFlagByCompany = new Map();
1120
+ const nextCheckAtFlagByCompany = new Map();
958
1121
  const blockedResumeFlagByCompany = new Map();
959
1122
  const supersessionLineageAwareByCompany = new Map();
960
- const env = deps.env ?? process.env;
961
1123
  await Promise.all([...keepAliveFlagCompanies.entries()].map(async ([key, company]) => {
962
1124
  const options = {
963
1125
  company: company ?? undefined,
@@ -967,14 +1129,18 @@ export async function runPassBody(hqRoot, deps) {
967
1129
  lane.keep_alive === true &&
968
1130
  laneRunsHere(lane, startDeps.host) &&
969
1131
  keepAliveCompanyKey(resolveLaneCompany(hqRoot, lane).company) === key);
970
- const [keepAliveEnabled, lineageAware, blockedResumeEnabled] = await Promise.all([
1132
+ const [keepAliveEnabled, nextCheckAtEnabled, lineageAware, blockedResumeEnabled] = await Promise.all([
971
1133
  readRegistryFlag(LEAD_LANE_KEEP_ALIVE_FLAG, options),
1134
+ nextCheckAtFlagCompanyKeys.has(key)
1135
+ ? readRegistryFlag(LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG, options)
1136
+ : Promise.resolve(false),
972
1137
  readRegistryFlag(LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, options),
973
1138
  hasBlockedKeepAliveLane
974
1139
  ? readRegistryFlag(LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG, options)
975
1140
  : Promise.resolve(false),
976
1141
  ]);
977
1142
  keepAliveFlagByCompany.set(key, keepAliveEnabled);
1143
+ nextCheckAtFlagByCompany.set(key, nextCheckAtEnabled);
978
1144
  blockedResumeFlagByCompany.set(key, blockedResumeEnabled);
979
1145
  supersessionLineageAwareByCompany.set(key, lineageAware);
980
1146
  }));
@@ -995,7 +1161,8 @@ export async function runPassBody(hqRoot, deps) {
995
1161
  await withAllLaneAdmissionLocks(hqRoot, async () => {
996
1162
  const freshLanes = loadLaneRecords(hqRoot, summary);
997
1163
  const freshQuestions = loadQuestionRecords(hqRoot, summary);
998
- await resumeKeepAlive(hqRoot, freshLanes, freshQuestions, summary, admissionDeps, now, keepAliveFlagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany);
1164
+ await closeInterruptedDoneStoryWorkers(hqRoot, freshLanes, freshQuestions, summary, admissionDeps, now, storyCloseEnabledByCompany);
1165
+ await resumeKeepAlive(hqRoot, freshLanes, freshQuestions, summary, admissionDeps, now, keepAliveFlagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany, nextCheckAtFlagByCompany);
999
1166
  const drainLanes = loadLaneRecords(hqRoot, summary);
1000
1167
  await admitQueued(hqRoot, drainLanes, summary, admissionDeps, now);
1001
1168
  await resumeAwaiting(hqRoot, drainLanes, freshQuestions, summary, admissionDeps);
@@ -1741,25 +1908,119 @@ function recordKeepAliveSkip(hqRoot, lane, reason, summary, deps, detail) {
1741
1908
  summary.keep_alive_blocked.push({ id: lane.lane_id, reason });
1742
1909
  process.stderr.write(`hq lanes reconcile: keep-alive blocked for lane ${lane.lane_id} (${reason}); ${keepAliveSkipGuidance(lane.lane_id, reason)}\n`);
1743
1910
  }
1744
- if (deps.dryRun || lane.keep_alive_blocked_reason === reason)
1911
+ if (deps.dryRun)
1745
1912
  return;
1746
- try {
1747
- casLaneIf(hqRoot, lane.lane_id, (current) => current.version === lane.version && current.state === lane.state, (current) => ({ ...current, keep_alive_blocked_reason: reason }), false);
1913
+ let laneVersion = lane.version;
1914
+ if (lane.keep_alive_blocked_reason !== reason) {
1915
+ try {
1916
+ const update = casLaneIf(hqRoot, lane.lane_id, (current) => current.version === lane.version && current.state === lane.state, (current) => ({ ...current, keep_alive_blocked_reason: reason }), false);
1917
+ laneVersion = update.record.version;
1918
+ }
1919
+ catch (err) {
1920
+ markDirty(summary, {
1921
+ kind: "lane",
1922
+ id: lane.lane_id,
1923
+ why: `keep-alive skip state write failed: ${err instanceof Error ? err.message : String(err)}`,
1924
+ });
1925
+ }
1748
1926
  }
1749
- catch (err) {
1750
- markDirty(summary, {
1751
- kind: "lane",
1752
- id: lane.lane_id,
1753
- why: `keep-alive skip state write failed: ${err instanceof Error ? err.message : String(err)}`,
1754
- });
1927
+ if (lane.keep_alive === true) {
1928
+ recordKeepAliveSkipEvent(hqRoot, lane, reason, laneVersion, summary, deps, detail);
1755
1929
  }
1756
1930
  }
1757
- function recordDoneKeepAlivePolicySkip(summary, lane, verdict) {
1931
+ function recordDoneKeepAlivePolicySkip(hqRoot, summary, lane, verdict, deps) {
1758
1932
  summary.skipped.push({
1759
1933
  kind: "lane",
1760
1934
  id: lane.lane_id,
1761
1935
  why: `keep_alive_${verdict}`,
1762
1936
  });
1937
+ if (lane.keep_alive === true && !deps.dryRun) {
1938
+ recordKeepAliveSkipEvent(hqRoot, lane, verdict, lane.version, summary, deps);
1939
+ }
1940
+ }
1941
+ function recordKeepAliveSkipEvent(hqRoot, lane, reason, laneVersion, summary, deps, detail) {
1942
+ if (lane.keep_alive !== true || deps.dryRun)
1943
+ return;
1944
+ const eventsPath = watcherEventsPath(hqRoot, lane.lane_id);
1945
+ try {
1946
+ let lastSkip;
1947
+ try {
1948
+ const scanBackwards = (body, targetReason, targetVersion) => {
1949
+ const lines = body.split(/\r?\n/);
1950
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
1951
+ if (!lines[index])
1952
+ continue;
1953
+ try {
1954
+ const value = JSON.parse(lines[index]);
1955
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
1956
+ const event = value;
1957
+ if (event.event === "keep_alive_skip" &&
1958
+ (targetReason === undefined || (event.reason === targetReason && event.lane_version === targetVersion))) {
1959
+ return event;
1960
+ }
1961
+ }
1962
+ }
1963
+ catch {
1964
+ // A partial concurrent append must not prevent future skip events.
1965
+ }
1966
+ }
1967
+ return undefined;
1968
+ };
1969
+ const maxTailBytes = 64 * 1024;
1970
+ const fd = fs.openSync(eventsPath, "r");
1971
+ let fileSize;
1972
+ let tail;
1973
+ let tailOffset;
1974
+ try {
1975
+ fileSize = fs.fstatSync(fd).size;
1976
+ tailOffset = Math.max(0, fileSize - maxTailBytes);
1977
+ const buffer = Buffer.alloc(fileSize - tailOffset);
1978
+ let bytesRead = 0;
1979
+ while (bytesRead < buffer.length) {
1980
+ const count = fs.readSync(fd, buffer, bytesRead, buffer.length - bytesRead, tailOffset + bytesRead);
1981
+ if (count === 0)
1982
+ break;
1983
+ bytesRead += count;
1984
+ }
1985
+ tail = buffer.subarray(0, bytesRead).toString("utf8");
1986
+ }
1987
+ finally {
1988
+ fs.closeSync(fd);
1989
+ }
1990
+ // When the bounded read starts mid-line, ignore that incomplete prefix.
1991
+ if (tailOffset > 0) {
1992
+ const firstNewline = tail.indexOf("\n");
1993
+ tail = firstNewline < 0 ? "" : tail.slice(firstNewline + 1);
1994
+ }
1995
+ lastSkip = scanBackwards(tail, reason === "next_check_at" ? reason : undefined, laneVersion);
1996
+ // For small logs the tail is not a complete record boundary if a single
1997
+ // record exceeded the bound, so allow one full-file scan as a fallback.
1998
+ if (!lastSkip && fileSize < maxTailBytes) {
1999
+ lastSkip = scanBackwards(fs.readFileSync(eventsPath, "utf8"), reason === "next_check_at" ? reason : undefined, laneVersion);
2000
+ }
2001
+ }
2002
+ catch (err) {
2003
+ if (err.code !== "ENOENT")
2004
+ throw err;
2005
+ }
2006
+ if (lastSkip?.reason === reason && lastSkip.lane_version === laneVersion)
2007
+ return;
2008
+ recordKeepAliveEvent(hqRoot, lane.lane_id, summary, {
2009
+ event: "keep_alive_skip",
2010
+ at: deps.now().toISOString(),
2011
+ reason,
2012
+ ...(detail === undefined ? {} : { detail }),
2013
+ lane_version: laneVersion,
2014
+ lane_state: lane.state,
2015
+ });
2016
+ }
2017
+ catch (err) {
2018
+ markDirty(summary, {
2019
+ kind: "lane",
2020
+ id: lane.lane_id,
2021
+ why: `keep-alive skip event dedupe failed: ${err instanceof Error ? err.message : String(err)}`,
2022
+ });
2023
+ }
1763
2024
  }
1764
2025
  function readLatestInterruptedClassificationAt(hqRoot, lane) {
1765
2026
  const logPath = watcherOutLogPath(hqRoot, lane.lane_id);
@@ -1774,10 +2035,15 @@ function readLatestInterruptedClassificationAt(hqRoot, lane) {
1774
2035
  }
1775
2036
  let latestClassification;
1776
2037
  for (const line of body.split(/\r?\n/).filter(Boolean)) {
1777
- const parsed = JSON.parse(line);
1778
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1779
- throw new Error("watcher log contains a non-object decision record");
2038
+ let parsed;
2039
+ try {
2040
+ parsed = JSON.parse(line);
2041
+ }
2042
+ catch {
2043
+ continue;
1780
2044
  }
2045
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
2046
+ continue;
1781
2047
  const record = parsed;
1782
2048
  if (record.event === "classification")
1783
2049
  latestClassification = record;
@@ -1975,7 +2241,7 @@ function appendBlockedKeepAliveHistory(hqRoot, lane, terminalAt, at, reason, dep
1975
2241
  }),
1976
2242
  }), false);
1977
2243
  }
1978
- async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany) {
2244
+ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany, nextCheckAtFlagByCompany) {
1979
2245
  const nowMs = now.getTime();
1980
2246
  const nowIso = now.toISOString();
1981
2247
  const config = loadLanesConfig(hqRoot);
@@ -2022,16 +2288,26 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
2022
2288
  const envelope = readLatestEnvelopeRecord(hqRoot, lane.lane_id)?.envelope;
2023
2289
  if (!envelope || envelope.decision !== "done")
2024
2290
  continue;
2025
- const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane, questions);
2026
- const verdict = doneKeepAliveVerdict(lane, envelope, hasQueuedWork);
2291
+ const nextCheckAtEnabled = nextCheckAtFlagByCompany.get(keepAliveCompanyKey(companyForLane(lane))) === true;
2292
+ const hasPendingWork = lane.keep_alive_standing === true || nextCheckAtEnabled
2293
+ ? hasPendingKeepAliveWork(hqRoot, lane, questions)
2294
+ : false;
2295
+ const hasQueuedWork = lane.keep_alive_standing === true && hasPendingWork;
2296
+ const verdict = doneKeepAliveVerdict(lane, envelope, hasQueuedWork, {
2297
+ nextCheckAtEnabled,
2298
+ hasPendingWork,
2299
+ nowMs,
2300
+ nextCheckAnchorMs: Date.parse(lane.last_envelope?.at ?? lane.updated_at),
2301
+ });
2027
2302
  if (verdict !== "eligible") {
2028
- if (verdict !== null)
2029
- recordDoneKeepAlivePolicySkip(summary, lane, verdict);
2303
+ let skipLane = lane;
2030
2304
  if (envelope.story_complete === true && lane.keep_alive_standing !== true && hasKeepAliveProgress(lane)) {
2031
- casLaneIf(hqRoot, lane.lane_id, (current) => current.version === lane.version &&
2305
+ skipLane = casLaneIf(hqRoot, lane.lane_id, (current) => current.version === lane.version &&
2032
2306
  current.state === "done" &&
2033
- current.keep_alive === lane.keep_alive, (current) => ({ ...clearedKeepAliveProgress(current), keep_alive_blocked_reason: undefined }), deps.dryRun);
2307
+ current.keep_alive === lane.keep_alive, (current) => ({ ...clearedKeepAliveProgress(current), keep_alive_blocked_reason: undefined }), deps.dryRun).record;
2034
2308
  }
2309
+ if (verdict !== null)
2310
+ recordDoneKeepAlivePolicySkip(hqRoot, summary, skipLane, verdict, deps);
2035
2311
  continue;
2036
2312
  }
2037
2313
  }
@@ -2043,6 +2319,7 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
2043
2319
  id: lane.lane_id,
2044
2320
  why: "keep_alive_company_unresolved",
2045
2321
  });
2322
+ recordKeepAliveSkipEvent(hqRoot, lane, "company_unresolved", lane.version, summary, deps);
2046
2323
  }
2047
2324
  continue;
2048
2325
  }
@@ -2054,6 +2331,7 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
2054
2331
  id: lane.lane_id,
2055
2332
  why: "keep_alive_flag_off",
2056
2333
  });
2334
+ recordKeepAliveSkipEvent(hqRoot, lane, "keep_alive_flag_off", lane.version, summary, deps);
2057
2335
  }
2058
2336
  continue;
2059
2337
  }
@@ -2134,14 +2412,24 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
2134
2412
  const latest = readLatestEnvelopeRecord(hqRoot, lane.lane_id);
2135
2413
  if (!latest || latest.envelope.decision !== "done")
2136
2414
  continue;
2137
- const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane, questions);
2138
- const verdict = doneKeepAliveVerdict(lane, latest.envelope, hasQueuedWork);
2415
+ const nextCheckAtEnabled = nextCheckAtFlagByCompany.get(keepAliveCompanyKey(companyForLane(lane))) === true;
2416
+ const hasPendingWork = lane.keep_alive_standing === true || nextCheckAtEnabled
2417
+ ? hasPendingKeepAliveWork(hqRoot, lane, questions)
2418
+ : false;
2419
+ const hasQueuedWork = lane.keep_alive_standing === true && hasPendingWork;
2420
+ const verdict = doneKeepAliveVerdict(lane, latest.envelope, hasQueuedWork, {
2421
+ nextCheckAtEnabled,
2422
+ hasPendingWork,
2423
+ nowMs,
2424
+ nextCheckAnchorMs: Date.parse(lane.last_envelope?.at ?? lane.updated_at),
2425
+ });
2139
2426
  if (verdict !== "eligible") {
2140
- if (verdict !== null)
2141
- recordDoneKeepAlivePolicySkip(summary, lane, verdict);
2427
+ let skipLane = lane;
2142
2428
  if (latest.envelope.story_complete === true && lane.keep_alive_standing !== true && hasKeepAliveProgress(lane)) {
2143
- casLaneIf(hqRoot, lane.lane_id, (current) => current.version === lane.version && current.state === "done", (current) => ({ ...clearedKeepAliveProgress(current), keep_alive_blocked_reason: undefined }), deps.dryRun);
2429
+ skipLane = casLaneIf(hqRoot, lane.lane_id, (current) => current.version === lane.version && current.state === "done", (current) => ({ ...clearedKeepAliveProgress(current), keep_alive_blocked_reason: undefined }), deps.dryRun).record;
2144
2430
  }
2431
+ if (verdict !== null)
2432
+ recordDoneKeepAlivePolicySkip(hqRoot, summary, skipLane, verdict, deps);
2145
2433
  continue;
2146
2434
  }
2147
2435
  try {
@@ -2216,17 +2504,27 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
2216
2504
  const currentEnvelope = readLatestEnvelopeRecord(hqRoot, current.lane_id);
2217
2505
  if (!currentEnvelope || currentEnvelope.envelope.decision !== "done")
2218
2506
  continue;
2219
- const hasQueuedWork = current.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, current, questions);
2220
- const verdict = doneKeepAliveVerdict(current, currentEnvelope.envelope, hasQueuedWork);
2507
+ const nextCheckAtEnabled = nextCheckAtFlagByCompany.get(keepAliveCompanyKey(companyForLane(current))) === true;
2508
+ const hasPendingWork = current.keep_alive_standing === true || nextCheckAtEnabled
2509
+ ? hasPendingKeepAliveWork(hqRoot, current, questions)
2510
+ : false;
2511
+ const hasQueuedWork = current.keep_alive_standing === true && hasPendingWork;
2512
+ const verdict = doneKeepAliveVerdict(current, currentEnvelope.envelope, hasQueuedWork, {
2513
+ nextCheckAtEnabled,
2514
+ hasPendingWork,
2515
+ nowMs,
2516
+ nextCheckAnchorMs: Date.parse(current.last_envelope?.at ?? current.updated_at),
2517
+ });
2221
2518
  if (verdict !== "eligible") {
2222
- if (verdict !== null)
2223
- recordDoneKeepAlivePolicySkip(summary, current, verdict);
2519
+ let skipLane = current;
2224
2520
  if (currentEnvelope.envelope.story_complete === true && current.keep_alive_standing !== true && hasKeepAliveProgress(current)) {
2225
- casLaneIf(hqRoot, current.lane_id, (latestLane) => latestLane.version === current.version &&
2521
+ skipLane = casLaneIf(hqRoot, current.lane_id, (latestLane) => latestLane.version === current.version &&
2226
2522
  latestLane.state === "done" &&
2227
2523
  latestLane.keep_alive === true &&
2228
- latestLane.keep_alive_opted_in_at === current.keep_alive_opted_in_at, (latestLane) => ({ ...clearedKeepAliveProgress(latestLane), keep_alive_blocked_reason: undefined }), deps.dryRun);
2524
+ latestLane.keep_alive_opted_in_at === current.keep_alive_opted_in_at, (latestLane) => ({ ...clearedKeepAliveProgress(latestLane), keep_alive_blocked_reason: undefined }), deps.dryRun).record;
2229
2525
  }
2526
+ if (verdict !== null)
2527
+ recordDoneKeepAlivePolicySkip(hqRoot, summary, skipLane, verdict, deps);
2230
2528
  continue;
2231
2529
  }
2232
2530
  try {
@@ -2272,6 +2570,7 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
2272
2570
  id: current.lane_id,
2273
2571
  why: "keep_alive_company_unresolved",
2274
2572
  });
2573
+ recordKeepAliveSkipEvent(hqRoot, current, "company_unresolved", current.version, summary, deps);
2275
2574
  continue;
2276
2575
  }
2277
2576
  const currentCompanyKey = keepAliveCompanyKey(currentCompany);
@@ -2281,6 +2580,7 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
2281
2580
  id: current.lane_id,
2282
2581
  why: "keep_alive_flag_off",
2283
2582
  });
2583
+ recordKeepAliveSkipEvent(hqRoot, current, "keep_alive_flag_off", current.version, summary, deps);
2284
2584
  continue;
2285
2585
  }
2286
2586
  const currentExclusion = keepAliveExclusion(current, currentLanes, retiredLaneIds, supersessionLineageAwareByCompany.get(currentCompanyKey) === true, companyForLane);
@@ -2342,12 +2642,15 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
2342
2642
  ? Math.max(attemptEligibleMs, storedNextMs)
2343
2643
  : attemptEligibleMs;
2344
2644
  if (nowMs < nextEligibleMs) {
2645
+ let skipLane = current;
2345
2646
  if (resetByHuman && hasKeepAliveProgress(current)) {
2346
- casLaneIf(hqRoot, current.lane_id, (latestLane) => latestLane.version === current.version &&
2647
+ const clearedProgress = casLaneIf(hqRoot, current.lane_id, (latestLane) => latestLane.version === current.version &&
2347
2648
  latestLane.state === current.state &&
2348
2649
  latestLane.keep_alive === true &&
2349
2650
  latestLane.keep_alive_opted_in_at === current.keep_alive_opted_in_at, (latestLane) => clearedKeepAliveProgress(latestLane), deps.dryRun);
2651
+ skipLane = clearedProgress.record;
2350
2652
  }
2653
+ recordKeepAliveSkipEvent(hqRoot, skipLane, "backoff", skipLane.version, summary, deps, new Date(nextEligibleMs).toISOString());
2351
2654
  continue;
2352
2655
  }
2353
2656
  const nextEligibleAt = new Date(nowMs + leadLaneKeepAliveBackoffMs(attempt + 1)).toISOString();
@@ -2362,6 +2665,7 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
2362
2665
  id: current.lane_id,
2363
2666
  why: `lane_queue_full: ${current.provider} queue has ${queuedCount}/${maxQueued} lanes`,
2364
2667
  });
2668
+ recordKeepAliveSkipEvent(hqRoot, current, "lane_queue_full", current.version, summary, deps, `${current.provider} queue has ${queuedCount}/${maxQueued} lanes`);
2365
2669
  continue;
2366
2670
  }
2367
2671
  const seniorId = current.senior.kind === "lane" ? current.senior.id : undefined;
@@ -2399,6 +2703,7 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
2399
2703
  id: current.lane_id,
2400
2704
  why: `keep_alive_hire_admission: ${grantPreflight.error}: ${grantPreflight.message}`,
2401
2705
  });
2706
+ recordKeepAliveSkipEvent(hqRoot, current, "hire_admission", current.version, summary, deps, `${grantPreflight.error}: ${grantPreflight.message}`);
2402
2707
  continue;
2403
2708
  }
2404
2709
  }
@@ -2481,8 +2786,22 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
2481
2786
  const envelope = readLatestEnvelopeRecord(hqRoot, latest.lane_id);
2482
2787
  if (!envelope || envelope.envelope.decision !== "done")
2483
2788
  continue;
2484
- if (doneKeepAliveVerdict(latest, envelope.envelope, latest.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, latest, questions)) !== "eligible")
2789
+ const nextCheckAtEnabled = nextCheckAtFlagByCompany.get(keepAliveCompanyKey(companyForLane(latest))) === true;
2790
+ const hasPendingWork = latest.keep_alive_standing === true || nextCheckAtEnabled
2791
+ ? hasPendingKeepAliveWork(hqRoot, latest, questions)
2792
+ : false;
2793
+ const finalDoneVerdict = doneKeepAliveVerdict(latest, envelope.envelope, latest.keep_alive_standing === true && hasPendingWork, {
2794
+ nextCheckAtEnabled,
2795
+ hasPendingWork,
2796
+ nowMs,
2797
+ nextCheckAnchorMs: Date.parse(latest.last_envelope?.at ?? latest.updated_at),
2798
+ });
2799
+ if (finalDoneVerdict !== "eligible") {
2800
+ if (finalDoneVerdict === "next_check_at") {
2801
+ recordDoneKeepAlivePolicySkip(hqRoot, summary, latest, finalDoneVerdict, deps);
2802
+ }
2485
2803
  continue;
2804
+ }
2486
2805
  try {
2487
2806
  latestEligibilityAtMs = latestEnvelopeMtimeMs(hqRoot, latest, envelope.ref);
2488
2807
  }
@@ -2514,6 +2833,7 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
2514
2833
  why: `keep_alive_account_selection_failed: ${accountSelectionFailure}`,
2515
2834
  });
2516
2835
  if (!deps.dryRun) {
2836
+ recordKeepAliveSkipEvent(hqRoot, current, "account_selection_failed", current.version, summary, deps, accountSelectionFailure);
2517
2837
  recordKeepAliveEvent(hqRoot, current.lane_id, summary, {
2518
2838
  event: "keep_alive_account_selection_failed",
2519
2839
  at: nowIso,
@@ -48,6 +48,7 @@ export interface ProviderProfileLaunch {
48
48
  export type DetachedStdio = ["ignore", number, number];
49
49
  export interface DetachedSpawnOptions {
50
50
  detached: true;
51
+ windowsHide: true;
51
52
  stdio: DetachedStdio;
52
53
  env: NodeJS.ProcessEnv;
53
54
  cwd: string;