@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.
- package/CHANGELOG.md +11 -0
- package/assets/scaffold/core/scripts/migrate-policy-triggers.sh +12 -7
- package/dist/command-catalog.generated.d.ts +18 -0
- package/dist/command-catalog.generated.js +23 -0
- package/dist/commands/core-migrate-policy-triggers-native.js +4 -8
- package/dist/commands/lanes-keep-alive-audit.d.ts +10 -0
- package/dist/commands/lanes-keep-alive-audit.js +19 -0
- package/dist/commands/lanes-message.js +37 -28
- package/dist/commands/lanes-shared.d.ts +1 -0
- package/dist/commands/lanes.js +33 -23
- package/dist/lib/core-utils/sentry-report.d.ts +3 -3
- package/dist/lib/core-utils/sentry-report.js +23 -2
- package/dist/lib/first-sync-next-step.d.ts +0 -3
- package/dist/lib/first-sync-next-step.js +6 -16
- package/dist/lib/lanes/autopilot.d.ts +13 -0
- package/dist/lib/lanes/autopilot.js +198 -37
- package/dist/lib/lanes/flag-keys.d.ts +7 -0
- package/dist/lib/lanes/flag-keys.js +7 -0
- package/dist/lib/lanes/keep-alive-audit.d.ts +46 -0
- package/dist/lib/lanes/keep-alive-audit.js +370 -0
- package/dist/lib/lanes/reconcile.js +20 -1
- package/dist/sentry.d.ts +8 -2
- package/dist/sentry.js +63 -6
- package/dist/utils/cli-telemetry.d.ts +4 -3
- package/dist/utils/cli-telemetry.js +5 -6
- package/package.json +1 -1
|
@@ -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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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:
|
|
440
|
-
fingerprint
|
|
441
|
-
prompt: `Re-measure
|
|
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.
|
|
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
|
-
|
|
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}:`)
|
|
712
|
-
|
|
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
|
-
|
|
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
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
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
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
|
-
|
|
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
|