@indigoai-us/hq-cli 5.313.0 → 5.315.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.
@@ -6,20 +6,26 @@ import { isControllerAliasOutpostId } from "./admission-host.js";
6
6
  import { LEAD_REGISTRY_REL, validateOutpostLeadRegistry } from "./autopilot-registry.js";
7
7
  import { appendDropboxMessage } from "./dropbox.js";
8
8
  import { LanesError } from "./errors.js";
9
- import { LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG } from "./flag-keys.js";
9
+ import { LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG, LANE_AUTOPILOT_NUDGE_DEDUPE_FLAG, LEAD_AUTOPILOT_KPI_STALENESS_BATCH_FLAG, LEAD_AUTOPILOT_KPI_STALENESS_FLAG, LEAD_AUTOPILOT_PER_LEAD_ERRORS_FLAG as PER_LEAD_ERRORS_FLAG, } from "./flag-keys.js";
10
10
  import { resolveLaneCompany } from "./lane-company.js";
11
11
  import { readLanesRegistryFlag } from "./registry-flag.js";
12
- import { LEAD_AUTOPILOT_KPI_STALENESS_FLAG } from "./flag-keys.js";
13
12
  import { readAllLanes } from "./store.js";
14
13
  export const LEAD_AUTOPILOT_FLAG = "lanes.lead-autopilot";
15
14
  export const LEAD_AUTOPILOT_DEFAULT_ENABLED = false;
16
15
  export const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG;
17
16
  export const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_DEFAULT_ENABLED = false;
18
17
  export const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_DELAY_MS = 60 * 60_000;
18
+ export const LEAD_AUTOPILOT_NUDGE_DEDUPE_FLAG = LANE_AUTOPILOT_NUDGE_DEDUPE_FLAG;
19
+ export const LEAD_AUTOPILOT_NUDGE_DEDUPE_DEFAULT_ENABLED = false;
20
+ export const LEAD_AUTOPILOT_NUDGE_REDELIVERY_DELAY_MS = 30 * 60_000;
21
+ export const LEAD_AUTOPILOT_NUDGE_STATE_RETENTION_MS = 24 * 60 * 60_000;
22
+ const LEAD_AUTOPILOT_DELIVERED_ID_RETENTION_MS = 7 * 24 * 60 * 60_000;
19
23
  export const LEAD_AUTOPILOT_REVIEW_DELAY_MS = 15 * 60_000;
20
24
  export const LEAD_AUTOPILOT_FILL_DELAY_MS = 15 * 60_000;
21
25
  export const LEAD_AUTOPILOT_PROMPT_LOOP = "personal/knowledge/team-lead-operating-loop.md";
22
26
  export const LEAD_AUTOPILOT_KPI_STALE_AFTER_MINUTES = 55;
27
+ export const LEAD_AUTOPILOT_PER_LEAD_ERRORS_FLAG = PER_LEAD_ERRORS_FLAG;
28
+ export const LEAD_AUTOPILOT_PER_LEAD_ERRORS_DEFAULT_ENABLED = false;
23
29
  const LIVE_STATES = new Set(["running", "queued", "awaiting_input", "blocked"]);
24
30
  const OPEN_STORY_STATES = new Set(["todo", "queued", "in_progress", "review", "blocked"]);
25
31
  function isOpenProjectStory(story) {
@@ -426,21 +432,39 @@ export function planLeadAutopilotWakes(input) {
426
432
  const byStory = new Map(stories.map((story) => [story.id, story]));
427
433
  const wakes = [];
428
434
  if (input.kpiStalenessEnabled) {
429
- for (const kpi of managerKpiFreshness(status, now)) {
430
- if (!kpi.stale)
431
- continue;
432
- const age = kpi.ageMinutes === null
433
- ? "its measurement age is unknown"
434
- : `its latest measurement is ${kpi.ageMinutes} minutes old`;
435
+ const staleKpis = managerKpiFreshness(status, now).filter((kpi) => kpi.stale);
436
+ if (input.kpiStalenessBatchEnabled && staleKpis.length > 0) {
437
+ const byOldest = [...staleKpis].sort((a, b) => (b.ageMinutes ?? Number.MAX_SAFE_INTEGER) - (a.ageMinutes ?? Number.MAX_SAFE_INTEGER) ||
438
+ a.name.localeCompare(b.name));
439
+ const identity = staleKpis
440
+ .map(({ name, measuredAt }) => [name, measuredAt])
441
+ .sort(([nameA, measuredAtA], [nameB, measuredAtB]) => nameA.localeCompare(nameB) || String(measuredAtA).localeCompare(String(measuredAtB)));
442
+ const fingerprint = `stale-kpi-batch:${createHash("sha256").update(JSON.stringify(identity)).digest("hex")}`;
443
+ const details = byOldest.map((kpi) => `- ${kpi.name}: age ${kpi.ageMinutes === null ? "unknown" : kpi.ageMinutes} minutes (stale after ${kpi.staleAfterMinutes} minutes)`).join("\n");
435
444
  wakes.push({
436
445
  condition: "stale_kpi",
437
446
  lead_lane: lead.lane_id,
438
447
  story_id: "kpi-staleness",
439
- kpi_id: kpi.name,
440
- fingerprint: kpi.fingerprint,
441
- prompt: `Re-measure KPI ${kpi.name}: ${age} (stale after ${kpi.staleAfterMinutes} minutes). Update measured_at in manager-status.json. Follow ${LEAD_AUTOPILOT_PROMPT_LOOP}.`,
448
+ kpi_id: "batch",
449
+ fingerprint,
450
+ prompt: `Re-measure these stale KPIs, oldest first:\n${details}\nUpdate measured_at in manager-status.json. Follow ${LEAD_AUTOPILOT_PROMPT_LOOP}.`,
442
451
  });
443
452
  }
453
+ else {
454
+ for (const kpi of staleKpis) {
455
+ const age = kpi.ageMinutes === null
456
+ ? "its measurement age is unknown"
457
+ : `its latest measurement is ${kpi.ageMinutes} minutes old`;
458
+ wakes.push({
459
+ condition: "stale_kpi",
460
+ lead_lane: lead.lane_id,
461
+ story_id: "kpi-staleness",
462
+ kpi_id: kpi.name,
463
+ fingerprint: kpi.fingerprint,
464
+ prompt: `Re-measure KPI ${kpi.name}: ${age} (stale after ${kpi.staleAfterMinutes} minutes). Update measured_at in manager-status.json. Follow ${LEAD_AUTOPILOT_PROMPT_LOOP}.`,
465
+ });
466
+ }
467
+ }
444
468
  }
445
469
  const finishedReviews = children.flatMap((child) => {
446
470
  if (!["done", "blocked", "failed", "interrupted"].includes(child.state))
@@ -580,7 +604,9 @@ function readState(file) {
580
604
  return { schema_version: "hq-lanes-autopilot-state.v1", sent: {}, deadline_misses: {} };
581
605
  const value = readJsonFile(file, "lead_autopilot_state_invalid");
582
606
  if (!isRecord(value) || value.schema_version !== "hq-lanes-autopilot-state.v1" || !isRecord(value.sent) ||
583
- (value.deadline_misses !== undefined && !isRecord(value.deadline_misses))) {
607
+ (value.delivered_ids !== undefined && !isRecord(value.delivered_ids)) ||
608
+ (value.deadline_misses !== undefined && !isRecord(value.deadline_misses)) ||
609
+ (value.per_lead_error_notices !== undefined && (!isRecord(value.per_lead_error_notices) || Object.values(value.per_lead_error_notices).some((id) => typeof id !== "string")))) {
584
610
  throw new LanesError("lead_autopilot_state_invalid", `Invalid autopilot state ${file}`, { path: file });
585
611
  }
586
612
  for (const [key, row] of Object.entries(value.sent)) {
@@ -591,7 +617,8 @@ function readState(file) {
591
617
  (row.worker_lane !== undefined && typeof row.worker_lane !== "string") ||
592
618
  (row.wake_count !== undefined && (!Number.isSafeInteger(row.wake_count) || row.wake_count < 1)) ||
593
619
  (row.last_wake_at !== undefined && typeof row.last_wake_at !== "string") ||
594
- (row.escalated_at !== undefined && typeof row.escalated_at !== "string")) {
620
+ (row.escalated_at !== undefined && typeof row.escalated_at !== "string") ||
621
+ (row.inactive_since !== undefined && (typeof row.inactive_since !== "string" || !Number.isFinite(Date.parse(row.inactive_since))))) {
595
622
  throw new LanesError("lead_autopilot_state_invalid", `Invalid autopilot state entry ${key}`, { path: file, key });
596
623
  }
597
624
  }
@@ -605,7 +632,46 @@ function readState(file) {
605
632
  throw new LanesError("lead_autopilot_state_invalid", `Invalid missed-deadline state entry ${key}`, { path: file, key });
606
633
  }
607
634
  }
608
- return value;
635
+ for (const [messageId, deliveredAt] of Object.entries(value.delivered_ids ?? {})) {
636
+ if (!messageId.trim() || typeof deliveredAt !== "string" || !Number.isFinite(Date.parse(deliveredAt))) {
637
+ throw new LanesError("lead_autopilot_state_invalid", `Invalid delivered message id ${messageId}`, { path: file, message_id: messageId });
638
+ }
639
+ }
640
+ return {
641
+ ...value,
642
+ per_lead_error_notices: (value.per_lead_error_notices ?? {}),
643
+ };
644
+ }
645
+ function appendAutopilotMessage(input) {
646
+ if (input.dedupeEnabled && input.state.delivered_ids?.[input.message.message_id])
647
+ return false;
648
+ appendDropboxMessage(input.hqRoot, input.leadLane, input.message);
649
+ if (input.dedupeEnabled) {
650
+ input.state.delivered_ids ??= {};
651
+ input.state.delivered_ids[input.message.message_id] = input.message.at;
652
+ }
653
+ return true;
654
+ }
655
+ function removeDeliveredIdsForFingerprint(state, fingerprint) {
656
+ const prefix = `lead-autopilot:${fingerprint}`;
657
+ let removed = false;
658
+ for (const messageId of Object.keys(state.delivered_ids ?? {})) {
659
+ if (messageId.startsWith(prefix)) {
660
+ delete state.delivered_ids[messageId];
661
+ removed = true;
662
+ }
663
+ }
664
+ return removed;
665
+ }
666
+ function pruneDeliveredIds(state, now) {
667
+ let pruned = false;
668
+ for (const [messageId, deliveredAt] of Object.entries(state.delivered_ids ?? {})) {
669
+ if (now.getTime() - Date.parse(deliveredAt) > LEAD_AUTOPILOT_DELIVERED_ID_RETENTION_MS) {
670
+ delete state.delivered_ids[messageId];
671
+ pruned = true;
672
+ }
673
+ }
674
+ return pruned;
609
675
  }
610
676
  function writeState(file, state) {
611
677
  fs.mkdirSync(path.dirname(file), { recursive: true });
@@ -668,6 +734,8 @@ async function runLeadAutopilotPassUnlocked(input) {
668
734
  continue;
669
735
  }
670
736
  let stateChanged = false;
737
+ let perLeadErrorsEnabled = false;
738
+ let currentInputPath = registered.status;
671
739
  try {
672
740
  const readFlag = input.readFlag ?? readLanesRegistryFlag;
673
741
  const enabled = await readFlag(LEAD_AUTOPILOT_FLAG, { company });
@@ -675,8 +743,11 @@ async function runLeadAutopilotPassUnlocked(input) {
675
743
  result.skipped.push({ lead_lane: lead.lane_id, reason: "flag_off" });
676
744
  continue;
677
745
  }
746
+ perLeadErrorsEnabled = await readFlag(PER_LEAD_ERRORS_FLAG, { company });
747
+ currentInputPath = registered.status;
678
748
  const status = readStatus(hqRoot, registered.status, company.slug);
679
749
  const kpiStalenessEnabled = await (input.readFlag ?? readLanesRegistryFlag)(LEAD_AUTOPILOT_KPI_STALENESS_FLAG, { company });
750
+ const kpiStalenessBatchEnabled = kpiStalenessEnabled && await (input.readFlag ?? readLanesRegistryFlag)(LEAD_AUTOPILOT_KPI_STALENESS_BATCH_FLAG, { company });
680
751
  if (kpiStalenessEnabled) {
681
752
  result.kpi_status ??= [];
682
753
  result.kpi_status.push(...managerKpiFreshness(status, now).map((kpi) => ({
@@ -693,9 +764,14 @@ async function runLeadAutopilotPassUnlocked(input) {
693
764
  const statusUpdatedAt = typeof status.updated_at === "string" && Number.isFinite(Date.parse(status.updated_at))
694
765
  ? new Date(Date.parse(status.updated_at)).toISOString()
695
766
  : undefined;
767
+ currentInputPath = registered.prd;
696
768
  const stories = projectStories(hqRoot, registered.prd, company.slug, lead.project_id);
697
769
  const children = lanes.filter((lane) => lane.senior.kind === "lane" && lane.senior.id === lead.lane_id);
698
770
  const deadlineEscalationEnabled = await (input.readDeadlineFlag ?? readFlag)(LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG, { company });
771
+ const nudgeDedupeEnabled = await (input.readNudgeDedupeFlag ?? readFlag)(LEAD_AUTOPILOT_NUDGE_DEDUPE_FLAG, { company });
772
+ if (nudgeDedupeEnabled && pruneDeliveredIds(state, now))
773
+ stateChanged = true;
774
+ currentInputPath = registered.status;
699
775
  const planned = planLeadAutopilotWakes({
700
776
  lead,
701
777
  children,
@@ -703,19 +779,72 @@ async function runLeadAutopilotPassUnlocked(input) {
703
779
  now,
704
780
  managerStatus: status,
705
781
  kpiStalenessEnabled,
782
+ kpiStalenessBatchEnabled,
706
783
  keepAliveResumed: input.keepAliveResumedLeads?.has(lead.lane_id) === true,
707
784
  });
708
785
  const stateKey = (wake) => `${lead.lane_id}:${wake.condition}${wake.condition === "stale_kpi" ? `:${wake.kpi_id}` : ""}`;
709
786
  const activeKeys = new Set(planned.map(stateKey));
787
+ const suppressedKeys = new Set();
710
788
  for (const key of Object.keys(state.sent)) {
711
- if (key.startsWith(`${lead.lane_id}:`) && !activeKeys.has(key)) {
712
- delete state.sent[key];
789
+ if (!key.startsWith(`${lead.lane_id}:`))
790
+ continue;
791
+ const entry = state.sent[key];
792
+ const isActive = activeKeys.has(key);
793
+ if (!nudgeDedupeEnabled) {
794
+ if (!isActive) {
795
+ delete state.sent[key];
796
+ stateChanged = true;
797
+ }
798
+ continue;
799
+ }
800
+ if (!isActive && entry.inactive_since === undefined) {
801
+ state.sent[key] = { ...entry, inactive_since: now.toISOString() };
713
802
  stateChanged = true;
803
+ continue;
804
+ }
805
+ if (isActive && entry.inactive_since !== undefined) {
806
+ const inactiveAt = Date.parse(entry.inactive_since);
807
+ if (now.getTime() - inactiveAt < LEAD_AUTOPILOT_NUDGE_STATE_RETENTION_MS) {
808
+ suppressedKeys.add(key);
809
+ }
810
+ else {
811
+ delete state.sent[key];
812
+ if (removeDeliveredIdsForFingerprint(state, entry.fingerprint))
813
+ stateChanged = true;
814
+ stateChanged = true;
815
+ }
816
+ }
817
+ else if (!isActive) {
818
+ const inactiveSince = entry.inactive_since ?? now.toISOString();
819
+ if (now.getTime() - Date.parse(inactiveSince) >= LEAD_AUTOPILOT_NUDGE_STATE_RETENTION_MS) {
820
+ delete state.sent[key];
821
+ if (removeDeliveredIdsForFingerprint(state, entry.fingerprint))
822
+ stateChanged = true;
823
+ stateChanged = true;
824
+ }
825
+ else if (entry.inactive_since === undefined) {
826
+ state.sent[key] = { ...entry, inactive_since: inactiveSince };
827
+ stateChanged = true;
828
+ }
714
829
  }
715
830
  }
716
831
  for (const wake of planned) {
717
832
  const key = stateKey(wake);
833
+ if (suppressedKeys.has(key))
834
+ continue;
718
835
  const prior = state.sent[key];
836
+ if (nudgeDedupeEnabled && prior && prior.fingerprint !== wake.fingerprint && !prior.acted) {
837
+ const leadActedSincePrior = statusUpdatedAt !== undefined && prior.status_updated_at !== undefined &&
838
+ Date.parse(statusUpdatedAt) > Date.parse(prior.status_updated_at);
839
+ if (!leadActedSincePrior) {
840
+ const lastWakeAt = Date.parse(prior.last_wake_at ?? prior.at);
841
+ if (!Number.isFinite(lastWakeAt)) {
842
+ throw new LanesError("lead_autopilot_state_invalid", `Invalid wake time for ${lead.lane_id}`, { lead_lane: lead.lane_id, story_id: wake.story_id });
843
+ }
844
+ if (now.getTime() - lastWakeAt < LEAD_AUTOPILOT_NUDGE_REDELIVERY_DELAY_MS)
845
+ continue;
846
+ }
847
+ }
719
848
  if (prior?.fingerprint === wake.fingerprint) {
720
849
  if (input.dryRun)
721
850
  continue;
@@ -761,19 +890,21 @@ async function runLeadAutopilotPassUnlocked(input) {
761
890
  // repeated wake/escalation cadence used by other conditions.
762
891
  if (wake.condition === "stale_kpi")
763
892
  continue;
764
- if (!current.acted && !current.auto_started && current.escalated_at === undefined && now.getTime() - lastWakeAt >= LEAD_AUTOPILOT_FILL_DELAY_MS) {
893
+ const redeliveryDelay = nudgeDedupeEnabled ? LEAD_AUTOPILOT_NUDGE_REDELIVERY_DELAY_MS : LEAD_AUTOPILOT_FILL_DELAY_MS;
894
+ if (!current.acted && !current.auto_started && current.escalated_at === undefined && now.getTime() - lastWakeAt >= redeliveryDelay) {
765
895
  const wakeCount = current.wake_count ?? 1;
766
896
  if (wakeCount < 2) {
767
- appendDropboxMessage(hqRoot, lead.lane_id, {
768
- at: now.toISOString(),
769
- message_id: `lead-autopilot:${wake.fingerprint}:wake-2`,
770
- kind: "system_notice",
771
- text: wake.prompt,
772
- });
897
+ const delivered = appendAutopilotMessage({ hqRoot, leadLane: lead.lane_id, state, dedupeEnabled: nudgeDedupeEnabled, message: {
898
+ at: now.toISOString(),
899
+ message_id: `lead-autopilot:${wake.fingerprint}:wake-2`,
900
+ kind: "system_notice",
901
+ text: wake.prompt,
902
+ } });
773
903
  current = { ...current, wake_count: 2, last_wake_at: now.toISOString() };
774
904
  state.sent[key] = current;
775
905
  stateChanged = true;
776
- result.sent.push(wake);
906
+ if (delivered)
907
+ result.sent.push(wake);
777
908
  }
778
909
  else {
779
910
  const escalation = { team: registered.team, lead_lane: lead.lane_id, condition: wake.condition, story_id: wake.story_id, at: now.toISOString(), fingerprint: wake.fingerprint };
@@ -787,12 +918,12 @@ async function runLeadAutopilotPassUnlocked(input) {
787
918
  continue;
788
919
  }
789
920
  if (!input.dryRun) {
790
- appendDropboxMessage(hqRoot, lead.lane_id, {
791
- at: now.toISOString(),
792
- message_id: `lead-autopilot:${wake.fingerprint}`,
793
- kind: "system_notice",
794
- text: wake.prompt,
795
- });
921
+ const delivered = appendAutopilotMessage({ hqRoot, leadLane: lead.lane_id, state, dedupeEnabled: nudgeDedupeEnabled, message: {
922
+ at: now.toISOString(),
923
+ message_id: `lead-autopilot:${wake.fingerprint}`,
924
+ kind: "system_notice",
925
+ text: wake.prompt,
926
+ } });
796
927
  state.sent[key] = {
797
928
  fingerprint: wake.fingerprint,
798
929
  at: now.toISOString(),
@@ -801,6 +932,8 @@ async function runLeadAutopilotPassUnlocked(input) {
801
932
  last_wake_at: now.toISOString(),
802
933
  };
803
934
  stateChanged = true;
935
+ if (!delivered)
936
+ continue;
804
937
  }
805
938
  result.sent.push(wake);
806
939
  }
@@ -867,14 +1000,15 @@ async function runLeadAutopilotPassUnlocked(input) {
867
1000
  fingerprint,
868
1001
  prompt: `Deadline missed for ${commitment.item} (${commitment.description}), owner ${commitment.owner}, due ${commitment.due}. Deliver it or re-date with a reason. Follow ${LEAD_AUTOPILOT_PROMPT_LOOP}.`,
869
1002
  };
870
- appendDropboxMessage(hqRoot, lead.lane_id, {
871
- at: now.toISOString(),
872
- message_id: `lead-autopilot:${fingerprint}`,
873
- kind: "system_notice",
874
- text: wake.prompt,
875
- });
1003
+ const delivered = appendAutopilotMessage({ hqRoot, leadLane: lead.lane_id, state, dedupeEnabled: nudgeDedupeEnabled, message: {
1004
+ at: now.toISOString(),
1005
+ message_id: `lead-autopilot:${fingerprint}`,
1006
+ kind: "system_notice",
1007
+ text: wake.prompt,
1008
+ } });
876
1009
  stateChanged = true;
877
- result.sent.push(wake);
1010
+ if (delivered)
1011
+ result.sent.push(wake);
878
1012
  deadlineState[key] = firstMiss;
879
1013
  }
880
1014
  else {
@@ -933,6 +1067,24 @@ async function runLeadAutopilotPassUnlocked(input) {
933
1067
  catch (error) {
934
1068
  if (!input.dryRun && stateChanged)
935
1069
  writeState(stateFile, state);
1070
+ if (perLeadErrorsEnabled && error instanceof LanesError && LEAD_AUTOPILOT_PER_LEAD_ERROR_CODES.has(error.error)) {
1071
+ const errorPath = typeof error.details.path === "string" ? error.details.path : currentInputPath;
1072
+ const messageId = `lead-autopilot-error:${error.error}:${createHash("sha256").update(`${error.error}\0${errorPath}`).digest("hex")}`;
1073
+ result.skipped.push({ lane_id: lead.lane_id, error: error.error, path: errorPath, message: error.message });
1074
+ const noticeKey = `${lead.lane_id}:${error.error}:${errorPath}`;
1075
+ if (!input.dryRun && state.per_lead_error_notices?.[noticeKey] !== messageId) {
1076
+ appendDropboxMessage(hqRoot, lead.lane_id, {
1077
+ at: now.toISOString(),
1078
+ message_id: messageId,
1079
+ kind: "system_notice",
1080
+ text: `Lead autopilot skipped this lead because ${error.error}: ${error.message}. Fix the registry row for lane ${registered.lane} (team ${registered.team}) in ${LEAD_REGISTRY_REL} (registered path: ${errorPath}).`,
1081
+ });
1082
+ state.per_lead_error_notices ??= {};
1083
+ state.per_lead_error_notices[noticeKey] = messageId;
1084
+ writeState(stateFile, state);
1085
+ }
1086
+ continue;
1087
+ }
936
1088
  if (!(error instanceof LanesError) || LEAD_AUTOPILOT_PASS_FATAL_ERRORS.has(error.error))
937
1089
  throw error;
938
1090
  result.skipped.push({ lead_lane: lead.lane_id, reason: error.error });
@@ -947,6 +1099,15 @@ const LEAD_AUTOPILOT_PASS_FATAL_ERRORS = new Set([
947
1099
  "lead_autopilot_status_path_invalid",
948
1100
  "lead_autopilot_escalation_record_invalid",
949
1101
  ]);
1102
+ const LEAD_AUTOPILOT_PER_LEAD_ERROR_CODES = new Set([
1103
+ "lead_autopilot_tenant_mismatch",
1104
+ "lead_autopilot_project_path_invalid",
1105
+ "lead_autopilot_project_invalid",
1106
+ "lead_autopilot_story_brief_path_invalid",
1107
+ "lead_autopilot_story_brief_invalid",
1108
+ "lead_autopilot_status_path_invalid",
1109
+ "lead_autopilot_status_invalid",
1110
+ ]);
950
1111
  const LEAD_AUTOPILOT_STALE_LOCK_MAX_AGE_MS = 60 * 60_000;
951
1112
  function processStartIdentity(pid) {
952
1113
  try {
@@ -6,10 +6,17 @@ export declare const LANE_ENVELOPE_REPAIR_CONTINUE_FLAG = "lanes.envelope-repair
6
6
  export declare const LANE_HIRE_GRANT_FINISHED_LANES_FLAG = "lanes.hire-grant-count-live-only";
7
7
  /** Enables missed-deadline lead nudges and second-miss operator escalation. */
8
8
  export declare const LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = "lanes.autopilot-missed-deadline-escalation";
9
+ export declare const LANE_AUTOPILOT_NUDGE_DEDUPE_FLAG = "lanes.autopilot-nudge-dedupe";
9
10
  /** Enables company-scoped lookup of a hostless lane ULID across Outpost indexes. */
10
11
  export declare const LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG = "lanes.message-route-owner-host";
11
12
  export declare const LANE_MESSAGE_ROUTE_OWNER_HOST_DEFAULT_ENABLED = false;
12
13
  /** Enables stale manager-status KPI reporting and one-time lead nudges. */
13
14
  export declare const LEAD_AUTOPILOT_KPI_STALENESS_FLAG = "lanes.autopilot-kpi-staleness";
14
15
  export declare const LEAD_AUTOPILOT_KPI_STALENESS_DEFAULT_ENABLED = false;
16
+ /** Skips malformed lead inputs individually instead of aborting the pass. */
17
+ export declare const LEAD_AUTOPILOT_PER_LEAD_ERRORS_FLAG = "lanes.autopilot-per-lead-errors";
18
+ export declare const LEAD_AUTOPILOT_PER_LEAD_ERRORS_DEFAULT_ENABLED = false;
19
+ /** Batches stale manager-status KPI notices into one lead message per pass. */
20
+ export declare const LEAD_AUTOPILOT_KPI_STALENESS_BATCH_FLAG = "lanes.autopilot-kpi-staleness-batch";
21
+ export declare const LEAD_AUTOPILOT_KPI_STALENESS_BATCH_DEFAULT_ENABLED = false;
15
22
  //# sourceMappingURL=flag-keys.d.ts.map
@@ -6,10 +6,17 @@ export const LANE_ENVELOPE_REPAIR_CONTINUE_FLAG = "lanes.envelope-repair-continu
6
6
  export const LANE_HIRE_GRANT_FINISHED_LANES_FLAG = "lanes.hire-grant-count-live-only";
7
7
  /** Enables missed-deadline lead nudges and second-miss operator escalation. */
8
8
  export const LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = "lanes.autopilot-missed-deadline-escalation";
9
+ export const LANE_AUTOPILOT_NUDGE_DEDUPE_FLAG = "lanes.autopilot-nudge-dedupe";
9
10
  /** Enables company-scoped lookup of a hostless lane ULID across Outpost indexes. */
10
11
  export const LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG = "lanes.message-route-owner-host";
11
12
  export const LANE_MESSAGE_ROUTE_OWNER_HOST_DEFAULT_ENABLED = false;
12
13
  /** Enables stale manager-status KPI reporting and one-time lead nudges. */
13
14
  export const LEAD_AUTOPILOT_KPI_STALENESS_FLAG = "lanes.autopilot-kpi-staleness";
14
15
  export const LEAD_AUTOPILOT_KPI_STALENESS_DEFAULT_ENABLED = false;
16
+ /** Skips malformed lead inputs individually instead of aborting the pass. */
17
+ export const LEAD_AUTOPILOT_PER_LEAD_ERRORS_FLAG = "lanes.autopilot-per-lead-errors";
18
+ export const LEAD_AUTOPILOT_PER_LEAD_ERRORS_DEFAULT_ENABLED = false;
19
+ /** Batches stale manager-status KPI notices into one lead message per pass. */
20
+ export const LEAD_AUTOPILOT_KPI_STALENESS_BATCH_FLAG = "lanes.autopilot-kpi-staleness-batch";
21
+ export const LEAD_AUTOPILOT_KPI_STALENESS_BATCH_DEFAULT_ENABLED = false;
15
22
  //# sourceMappingURL=flag-keys.js.map
@@ -0,0 +1,46 @@
1
+ export type KeepAliveAuditVerdict = "on_time" | "late" | "early_wake" | "message_wake" | "self_wake" | "early_by_keep_alive" | "not_restarted" | "waiting";
2
+ export interface KeepAliveAuditRestart {
3
+ at: string;
4
+ trigger: string;
5
+ caller: string | null;
6
+ }
7
+ export interface KeepAliveAuditRow {
8
+ lane: string;
9
+ project: string;
10
+ profile: string | null;
11
+ stopped_at: string;
12
+ next_check_at: string | null;
13
+ due_at: string;
14
+ due_reason: "next_check_at" | "backoff";
15
+ restart: KeepAliveAuditRestart | null;
16
+ verdict: KeepAliveAuditVerdict;
17
+ }
18
+ export interface KeepAliveAuditSummary {
19
+ by_verdict: Record<KeepAliveAuditVerdict, number>;
20
+ not_restarted: number;
21
+ late_over_10m: number;
22
+ early_wake_by_others: number;
23
+ self_wake: number;
24
+ stopped_and_not_restarted_on_their_own: number;
25
+ }
26
+ export interface KeepAliveAuditError {
27
+ lane: string;
28
+ error: string;
29
+ path: string;
30
+ message: string;
31
+ }
32
+ export interface KeepAliveAuditResult {
33
+ ok: true;
34
+ since: string;
35
+ rows: KeepAliveAuditRow[];
36
+ errors: KeepAliveAuditError[];
37
+ summary: KeepAliveAuditSummary;
38
+ }
39
+ export declare function buildKeepAliveAudit(input: {
40
+ hqRoot: string;
41
+ now: Date;
42
+ since?: string;
43
+ laneId?: string;
44
+ }): KeepAliveAuditResult;
45
+ export declare function formatKeepAliveAuditText(result: KeepAliveAuditResult): string;
46
+ //# sourceMappingURL=keep-alive-audit.d.ts.map