@indigoai-us/hq-cli 5.344.0 → 5.345.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 +8 -0
- package/assets/bot-workers/setup/worker.yaml +21 -0
- package/dist/commands/lanes.js +1 -25
- package/dist/commands/mcp-serve.js +4 -3
- package/dist/lib/bot/coding-tool-activity.d.ts +142 -0
- package/dist/lib/bot/coding-tool-activity.js +372 -0
- package/dist/lib/bot/run.d.ts +7 -0
- package/dist/lib/bot/run.js +123 -11
- package/dist/lib/bot/setup-explain.d.ts +12 -0
- package/dist/lib/bot/setup-tool-offer.d.ts +54 -0
- package/dist/lib/bot/setup-tool-offer.js +91 -0
- package/dist/lib/lanes/autopilot.d.ts +0 -5
- package/dist/lib/lanes/autopilot.js +13 -30
- package/dist/lib/lanes/disk-guard.d.ts +0 -1
- package/dist/lib/lanes/disk-guard.js +0 -1
- package/dist/lib/lanes/flag-keys.d.ts +0 -7
- package/dist/lib/lanes/flag-keys.js +0 -7
- package/dist/lib/lanes/reconcile.js +1 -14
- package/dist/utils/package-use-lease.d.ts +7 -2
- package/dist/utils/package-use-lease.js +39 -3
- package/package.json +1 -1
package/dist/lib/bot/run.js
CHANGED
|
@@ -62,6 +62,8 @@ import { splitBody } from "./split.js";
|
|
|
62
62
|
import { createBotLogger } from "./log.js";
|
|
63
63
|
import { buildSystemPrompt, introDmText, readPromptSources } from "./prompt.js";
|
|
64
64
|
import { loadSetupExplainer, patchSetupIntroState, readSetupIntroState, readSetupProgressIntro, replyOffersExplain, SETUP_EXPLAINED_NOTE, setupExplainDue, } from "./setup-explain.js";
|
|
65
|
+
import { detectCodingToolActivity, SIGNIFICANT_CODING_TOOL_ACTIVITY, } from "./coding-tool-activity.js";
|
|
66
|
+
import { setupKeptHereNote, setupOfferAnsweredInWordsNote, setupToolOfferAnswer, setupToolOfferText, setupWelcomeOpening, } from "./setup-tool-offer.js";
|
|
65
67
|
import { FALLBACK_CLAUDE_PERMISSION_MODE, isClaudeBypassDisabled } from "./runtime/claude.js";
|
|
66
68
|
import { runRuntimeTurn, RuntimeError } from "./runtime/index.js";
|
|
67
69
|
import { botAttachmentsDir, dmSessionScope } from "./paths.js";
|
|
@@ -319,6 +321,57 @@ export async function runBot(deps) {
|
|
|
319
321
|
log("warn", `setup intro state not updated: ${err instanceof Error ? err.message : String(err)}`);
|
|
320
322
|
}
|
|
321
323
|
};
|
|
324
|
+
/** The tool the pending "continue setup there?" offer named, while the kickoff waits on it. */
|
|
325
|
+
const pendingToolOffer = () => {
|
|
326
|
+
try {
|
|
327
|
+
const current = readSetupIntroState(dir);
|
|
328
|
+
return current.kickoffDeferred && current.toolOffer ? current.toolOffer.tool : null;
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
/**
|
|
335
|
+
* Setup bot, first message: when the person has the Claude app or the Codex
|
|
336
|
+
* app and uses it a lot (coding-tool-activity.ts), the first message is the
|
|
337
|
+
* fixed "continue setup there?" offer instead of the kickoff turn, and the
|
|
338
|
+
* kickoff waits for their answer (setup-tool-offer.ts). Made once, and only
|
|
339
|
+
* before setup has started. Null (run the kickoff as usual) when nothing
|
|
340
|
+
* qualifies or anything goes wrong.
|
|
341
|
+
*/
|
|
342
|
+
const codingToolToOffer = () => {
|
|
343
|
+
if (!isSetupBot || !(config.kickoff ?? "").trim())
|
|
344
|
+
return null;
|
|
345
|
+
try {
|
|
346
|
+
if (readSetupIntroState(dir).toolOffer)
|
|
347
|
+
return null;
|
|
348
|
+
if (readSetupProgressIntro(resolveBotMemoryDir(config)) !== null)
|
|
349
|
+
return null;
|
|
350
|
+
const detect = deps.detectCodingToolActivity ?? detectCodingToolActivity;
|
|
351
|
+
const activity = detect({ now: now(), extraHqRoots: [config.hqRoot] });
|
|
352
|
+
const describe = (a) => a.installed ? `${a.sessions} sessions on ${a.days} days` : "not installed";
|
|
353
|
+
log("info", `desktop app use in the last ${SIGNIFICANT_CODING_TOOL_ACTIVITY.windowDays} days: ` +
|
|
354
|
+
`Claude ${describe(activity.claude)}, Codex ${describe(activity.codex)}`);
|
|
355
|
+
return activity.pick;
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
358
|
+
log("warn", `coding tool check failed (${err instanceof Error ? err.message : String(err)}); running the kickoff`);
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
/** The offer went out: the kickoff now waits for the answer. */
|
|
363
|
+
const recordToolOffer = (tool) => {
|
|
364
|
+
try {
|
|
365
|
+
patchSetupIntroState(dir, { toolOffer: { tool, offeredAt: now().toISOString() }, kickoffDeferred: true });
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
log("warn", `setup intro state not updated: ${err instanceof Error ? err.message : String(err)}`);
|
|
369
|
+
}
|
|
370
|
+
if (readBotConfig(dir)?.kickoffPendingSignIn)
|
|
371
|
+
patchBotConfig(dir, { kickoffPendingSignIn: false });
|
|
372
|
+
setStatus({ repliesSent: (readStatus()?.repliesSent ?? 0) + 1, lastActivityAt: now().toISOString() });
|
|
373
|
+
log("info", `offered to continue setup in the ${tool} app (no model turn); the kickoff waits for the answer`);
|
|
374
|
+
};
|
|
322
375
|
// Scheduled runs are reported in the owner's own zone: "every weekday at 9am"
|
|
323
376
|
// was set up here, so "due 13:00 UTC" would read as the wrong time.
|
|
324
377
|
const timeZone = machineTimeZone();
|
|
@@ -455,16 +508,33 @@ export async function runBot(deps) {
|
|
|
455
508
|
kickoffAfterIntro = {};
|
|
456
509
|
}
|
|
457
510
|
else if (!config.introSentAt) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
511
|
+
// Setup bot with an app to offer: the welcome leads straight into the
|
|
512
|
+
// offer, without its "checking your computer" and Launch-button lines.
|
|
513
|
+
const offerTool = codingToolToOffer();
|
|
514
|
+
let offered = false;
|
|
515
|
+
if (offerTool) {
|
|
516
|
+
try {
|
|
517
|
+
await api.sendDm({ toPersonUid: config.ownerUid, body: setupToolOfferText(offerTool, setupWelcomeOpening(config.intro)) });
|
|
518
|
+
patchBotConfig(dir, { introSentAt: now().toISOString() });
|
|
519
|
+
log("info", "intro DM sent to owner, leading into the app offer");
|
|
520
|
+
recordToolOffer(offerTool);
|
|
521
|
+
offered = true;
|
|
522
|
+
}
|
|
523
|
+
catch (err) {
|
|
524
|
+
log("warn", `coding tool offer could not be sent as the welcome (${err instanceof Error ? err.message : String(err)}); sending the usual welcome`);
|
|
525
|
+
}
|
|
467
526
|
}
|
|
527
|
+
if (!offered)
|
|
528
|
+
try {
|
|
529
|
+
const sent = await api.sendDm({ toPersonUid: config.ownerUid, body: introDmText(config.name, config.runtime, config.intro, { kind: effectiveBotKind(config), companies: effectiveBotCompanies(config) }) });
|
|
530
|
+
patchBotConfig(dir, { introSentAt: now().toISOString() });
|
|
531
|
+
log("info", "intro DM sent to owner");
|
|
532
|
+
if (config.kickoff?.trim())
|
|
533
|
+
kickoffAfterIntro = { ...(sent?.eventId ? { introEventId: sent.eventId } : {}) };
|
|
534
|
+
}
|
|
535
|
+
catch (err) {
|
|
536
|
+
log("warn", `intro DM failed (will retry next start): ${err instanceof Error ? err.message : String(err)}`);
|
|
537
|
+
}
|
|
468
538
|
}
|
|
469
539
|
// ── Heartbeat loop ─────────────────────────────────────────────────────────
|
|
470
540
|
const failures = new FailureWindow(FAILURE_WINDOW_MS, FAILURE_LIMIT);
|
|
@@ -1097,10 +1167,31 @@ export async function runBot(deps) {
|
|
|
1097
1167
|
* in-between messages). The thinking reaction goes on the intro message
|
|
1098
1168
|
* (there is no owner message to mark). A failure never crashes the loop.
|
|
1099
1169
|
*/
|
|
1170
|
+
/**
|
|
1171
|
+
* Setup bot whose welcome already went out (a kickoff held for a sign-in):
|
|
1172
|
+
* the offer as its own message instead of the kickoff turn. False (run the
|
|
1173
|
+
* kickoff) when there is nothing to offer or it cannot be sent.
|
|
1174
|
+
*/
|
|
1175
|
+
const offerCodingTool = async () => {
|
|
1176
|
+
const tool = codingToolToOffer();
|
|
1177
|
+
if (!tool)
|
|
1178
|
+
return false;
|
|
1179
|
+
try {
|
|
1180
|
+
await sendToTarget({ kind: "dm", peerUid: config.ownerUid }, setupToolOfferText(tool), undefined, "human");
|
|
1181
|
+
}
|
|
1182
|
+
catch (err) {
|
|
1183
|
+
log("warn", `coding tool offer could not be sent (${err instanceof Error ? err.message : String(err)}); running the kickoff`);
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
recordToolOffer(tool);
|
|
1187
|
+
return true;
|
|
1188
|
+
};
|
|
1100
1189
|
const runKickoff = async (introEventId) => {
|
|
1101
1190
|
const prompt = (config.kickoff ?? "").trim();
|
|
1102
1191
|
if (!prompt || stopping || state !== "running")
|
|
1103
1192
|
return;
|
|
1193
|
+
if (isSetupBot && (await offerCodingTool()))
|
|
1194
|
+
return;
|
|
1104
1195
|
const peerUid = config.ownerUid;
|
|
1105
1196
|
const tReceived = now().getTime();
|
|
1106
1197
|
log("info", `kickoff turn starting (${prompt.length} chars)`);
|
|
@@ -1530,7 +1621,18 @@ export async function runBot(deps) {
|
|
|
1530
1621
|
const sessionScope = target.kind === "room" ? target.sessionScope : dmSessionScope(target.peerUid, config.ownerUid);
|
|
1531
1622
|
// The turn after the app sent the explanation: tell the model it went out.
|
|
1532
1623
|
const setupNote = isSetupBot && ownerDm && readSetupIntroState(dir).notePending === true;
|
|
1533
|
-
|
|
1624
|
+
// The first answer to the coding tool offer: "Keep going here" runs the
|
|
1625
|
+
// waiting kickoff; anything else runs with a note that setup has not started.
|
|
1626
|
+
const toolOffer = isSetupBot && ownerDm ? pendingToolOffer() : null;
|
|
1627
|
+
const toolAnswer = toolOffer ? setupToolOfferAnswer(messageText, attachmentRefs.length) : null;
|
|
1628
|
+
const kickoffPrompt = (config.kickoff ?? "").trim();
|
|
1629
|
+
const ownerText = toolOffer && toolAnswer === "kept_here" && kickoffPrompt
|
|
1630
|
+
? `${setupKeptHereNote(toolOffer)}\n\n${kickoffPrompt}`
|
|
1631
|
+
: toolOffer
|
|
1632
|
+
? `${setupOfferAnsweredInWordsNote(toolOffer)}\n\n${text}`
|
|
1633
|
+
: setupNote
|
|
1634
|
+
? `${SETUP_EXPLAINED_NOTE}\n\n${text}`
|
|
1635
|
+
: text;
|
|
1534
1636
|
const prompt = target.kind === "room" ? await buildRoomTurn(item, target, text) : target.peerUid === config.ownerUid ? ownerText :
|
|
1535
1637
|
`[HQ DM sender: ${target.peerUid}; owner: ${config.ownerUid}; sender is not the owner.]\n\n${text}`;
|
|
1536
1638
|
const markerTarget = target.kind === "dm" ? { kind: "dm", peerUid: target.peerUid } : { kind: "room", channelId: target.channelId };
|
|
@@ -1555,7 +1657,7 @@ export async function runBot(deps) {
|
|
|
1555
1657
|
});
|
|
1556
1658
|
// The model saw the note once its turn ran. A turn that never started, or
|
|
1557
1659
|
// one held for a coding-tool sign-in, runs again and keeps the note for it.
|
|
1558
|
-
if (setupNote && (outcome.ok || (outcome.started && !isRuntimeSignInFailure(outcome.excerpt)))) {
|
|
1660
|
+
if (setupNote && !toolOffer && (outcome.ok || (outcome.started && !isRuntimeSignInFailure(outcome.excerpt)))) {
|
|
1559
1661
|
try {
|
|
1560
1662
|
patchSetupIntroState(dir, { notePending: false });
|
|
1561
1663
|
}
|
|
@@ -1563,6 +1665,16 @@ export async function runBot(deps) {
|
|
|
1563
1665
|
log("warn", `setup intro state not updated: ${err instanceof Error ? err.message : String(err)}`);
|
|
1564
1666
|
}
|
|
1565
1667
|
}
|
|
1668
|
+
// Same rule for the offer: answered once the model saw the answer.
|
|
1669
|
+
if (toolOffer && toolAnswer && (outcome.ok || (outcome.started && !isRuntimeSignInFailure(outcome.excerpt)))) {
|
|
1670
|
+
try {
|
|
1671
|
+
patchSetupIntroState(dir, { kickoffDeferred: false, toolOfferAnswer: toolAnswer });
|
|
1672
|
+
log("info", `coding tool offer answered (${toolAnswer === "kept_here" ? "kept setup here" : "wrote a message"}); setup runs here`);
|
|
1673
|
+
}
|
|
1674
|
+
catch (err) {
|
|
1675
|
+
log("warn", `setup intro state not updated: ${err instanceof Error ? err.message : String(err)}`);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1566
1678
|
if (outcome.ok && ownerDm)
|
|
1567
1679
|
noteSetupReply(outcome.reply);
|
|
1568
1680
|
if (outcome.ok) {
|
|
@@ -44,6 +44,18 @@ export interface SetupIntroState {
|
|
|
44
44
|
explainedAt?: string;
|
|
45
45
|
/** The model has not yet been told that the explanation went out. */
|
|
46
46
|
notePending?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* The first message offered to continue setup in this coding tool
|
|
49
|
+
* (setup-tool-offer.ts). Set once; the offer is never made again.
|
|
50
|
+
*/
|
|
51
|
+
toolOffer?: {
|
|
52
|
+
tool: "claude" | "codex";
|
|
53
|
+
offeredAt: string;
|
|
54
|
+
};
|
|
55
|
+
/** The kickoff turn is waiting for the person's answer to that offer. */
|
|
56
|
+
kickoffDeferred?: boolean;
|
|
57
|
+
/** How the person answered the offer: the "Keep going here" button, or a message of their own. */
|
|
58
|
+
toolOfferAnswer?: "kept_here" | "wrote";
|
|
47
59
|
}
|
|
48
60
|
export declare function readSetupIntroState(dir: string): SetupIntroState;
|
|
49
61
|
export declare function patchSetupIntroState(dir: string, patch: Partial<Omit<SetupIntroState, "v">>): SetupIntroState;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The setup bot's "continue in your coding tool" offer.
|
|
3
|
+
*
|
|
4
|
+
* Before the setup bot's first message, the runner checks whether the person
|
|
5
|
+
* has the Claude app or the Codex app installed and has used it a lot lately
|
|
6
|
+
* (coding-tool-activity.ts). When one qualifies, the first message is not a
|
|
7
|
+
* model turn: in place of the usual welcome, the runner sends the welcome's
|
|
8
|
+
* opening sentence and one fixed line,
|
|
9
|
+
*
|
|
10
|
+
* "Looks like you use the Claude app. Want to continue setup there?"
|
|
11
|
+
*
|
|
12
|
+
* with an hq-block the HQ desktop app draws as two buttons: "Continue in
|
|
13
|
+
* Claude" (the app opens that desktop app in the HQ folder with the setup
|
|
14
|
+
* request ready) and "Keep going here". The kickoff turn waits:
|
|
15
|
+
*
|
|
16
|
+
* - "Keep going here" (exactly that, nothing attached) runs the waiting
|
|
17
|
+
* kickoff as the reply to it, so setup carries on as it always did.
|
|
18
|
+
* - Any other message from the person runs as a normal turn, with a note
|
|
19
|
+
* saying the offer was made and setup has not started yet.
|
|
20
|
+
*
|
|
21
|
+
* The offer is made once per bot, only when setup has not started (no
|
|
22
|
+
* progress note). Anything unexpected falls back to the normal kickoff turn.
|
|
23
|
+
*/
|
|
24
|
+
import type { CodingTool } from "./coding-tool-activity.js";
|
|
25
|
+
/** The reply that keeps setup in the HQ app. Exact match only. */
|
|
26
|
+
export declare const KEEP_GOING_HERE_REPLY = "Keep going here";
|
|
27
|
+
/** The hq-block kind the desktop app draws as the two-button card. */
|
|
28
|
+
export declare const CONTINUE_IN_TOOL_BLOCK_KIND = "continueInTool";
|
|
29
|
+
/** The desktop apps' product names. */
|
|
30
|
+
export declare const CODING_TOOL_NAMES: Readonly<Record<CodingTool, string>>;
|
|
31
|
+
/** The offer's one line. */
|
|
32
|
+
export declare function setupToolOfferLine(tool: CodingTool): string;
|
|
33
|
+
/**
|
|
34
|
+
* The opening of the app's welcome, kept when the offer replaces it: its
|
|
35
|
+
* first sentence (who the bot is and what setup covers). The rest of the
|
|
36
|
+
* welcome says the bot is checking the computer and will post its first
|
|
37
|
+
* question, and points at the Launch button; with the offer showing, none of
|
|
38
|
+
* that is true yet, so it is dropped. A first sentence that itself talks about
|
|
39
|
+
* checking or the Launch button is dropped too.
|
|
40
|
+
*/
|
|
41
|
+
export declare function setupWelcomeOpening(intro: string | undefined): string | null;
|
|
42
|
+
/**
|
|
43
|
+
* The whole first message. The block also carries "Keep going here" as a
|
|
44
|
+
* suggested reply, so an app that predates the card still shows that button.
|
|
45
|
+
*/
|
|
46
|
+
export declare function setupToolOfferText(tool: CodingTool, welcome?: string | null): string;
|
|
47
|
+
/** Put in front of the waiting kickoff when the person clicked "Keep going here". */
|
|
48
|
+
export declare function setupKeptHereNote(tool: CodingTool): string;
|
|
49
|
+
/** Put in front of the person's message when they wrote something instead of clicking. */
|
|
50
|
+
export declare function setupOfferAnsweredInWordsNote(tool: CodingTool): string;
|
|
51
|
+
export type SetupToolOfferAnswer = "kept_here" | "wrote";
|
|
52
|
+
/** How the person answered a pending offer: the button, or anything else. */
|
|
53
|
+
export declare function setupToolOfferAnswer(text: string, attachments: number): SetupToolOfferAnswer;
|
|
54
|
+
//# sourceMappingURL=setup-tool-offer.d.ts.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The setup bot's "continue in your coding tool" offer.
|
|
3
|
+
*
|
|
4
|
+
* Before the setup bot's first message, the runner checks whether the person
|
|
5
|
+
* has the Claude app or the Codex app installed and has used it a lot lately
|
|
6
|
+
* (coding-tool-activity.ts). When one qualifies, the first message is not a
|
|
7
|
+
* model turn: in place of the usual welcome, the runner sends the welcome's
|
|
8
|
+
* opening sentence and one fixed line,
|
|
9
|
+
*
|
|
10
|
+
* "Looks like you use the Claude app. Want to continue setup there?"
|
|
11
|
+
*
|
|
12
|
+
* with an hq-block the HQ desktop app draws as two buttons: "Continue in
|
|
13
|
+
* Claude" (the app opens that desktop app in the HQ folder with the setup
|
|
14
|
+
* request ready) and "Keep going here". The kickoff turn waits:
|
|
15
|
+
*
|
|
16
|
+
* - "Keep going here" (exactly that, nothing attached) runs the waiting
|
|
17
|
+
* kickoff as the reply to it, so setup carries on as it always did.
|
|
18
|
+
* - Any other message from the person runs as a normal turn, with a note
|
|
19
|
+
* saying the offer was made and setup has not started yet.
|
|
20
|
+
*
|
|
21
|
+
* The offer is made once per bot, only when setup has not started (no
|
|
22
|
+
* progress note). Anything unexpected falls back to the normal kickoff turn.
|
|
23
|
+
*/
|
|
24
|
+
/** The reply that keeps setup in the HQ app. Exact match only. */
|
|
25
|
+
export const KEEP_GOING_HERE_REPLY = "Keep going here";
|
|
26
|
+
/** The hq-block kind the desktop app draws as the two-button card. */
|
|
27
|
+
export const CONTINUE_IN_TOOL_BLOCK_KIND = "continueInTool";
|
|
28
|
+
/** The desktop apps' product names. */
|
|
29
|
+
export const CODING_TOOL_NAMES = Object.freeze({
|
|
30
|
+
claude: "Claude",
|
|
31
|
+
codex: "Codex",
|
|
32
|
+
});
|
|
33
|
+
/** The offer's one line. */
|
|
34
|
+
export function setupToolOfferLine(tool) {
|
|
35
|
+
return `Looks like you use the ${CODING_TOOL_NAMES[tool]} app. Want to continue setup there?`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The opening of the app's welcome, kept when the offer replaces it: its
|
|
39
|
+
* first sentence (who the bot is and what setup covers). The rest of the
|
|
40
|
+
* welcome says the bot is checking the computer and will post its first
|
|
41
|
+
* question, and points at the Launch button; with the offer showing, none of
|
|
42
|
+
* that is true yet, so it is dropped. A first sentence that itself talks about
|
|
43
|
+
* checking or the Launch button is dropped too.
|
|
44
|
+
*/
|
|
45
|
+
export function setupWelcomeOpening(intro) {
|
|
46
|
+
const text = (intro ?? "").trim();
|
|
47
|
+
if (!text)
|
|
48
|
+
return null;
|
|
49
|
+
const m = /^[\s\S]*?[.!?](?=\s|$)/.exec(text);
|
|
50
|
+
const first = (m ? m[0] : text).trim();
|
|
51
|
+
if (!first || /\bcheck(ing)?\b|\blaunch\b|first question/i.test(first))
|
|
52
|
+
return null;
|
|
53
|
+
return first;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The whole first message. The block also carries "Keep going here" as a
|
|
57
|
+
* suggested reply, so an app that predates the card still shows that button.
|
|
58
|
+
*/
|
|
59
|
+
export function setupToolOfferText(tool, welcome) {
|
|
60
|
+
const envelope = {
|
|
61
|
+
v: 1,
|
|
62
|
+
blocks: [
|
|
63
|
+
{ kind: CONTINUE_IN_TOOL_BLOCK_KIND, tool },
|
|
64
|
+
{ kind: "suggestions", items: [KEEP_GOING_HERE_REPLY] },
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
return ((welcome ? `${welcome}\n\n` : "") +
|
|
68
|
+
`${setupToolOfferLine(tool)}\n\n` +
|
|
69
|
+
"```hq-block\n" +
|
|
70
|
+
`${JSON.stringify(envelope)}\n` +
|
|
71
|
+
"```");
|
|
72
|
+
}
|
|
73
|
+
/** Put in front of the waiting kickoff when the person clicked "Keep going here". */
|
|
74
|
+
export function setupKeptHereNote(tool) {
|
|
75
|
+
return (`[Note from the HQ app: before setup started, the app offered to continue setup in the ${CODING_TOOL_NAMES[tool]} app, ` +
|
|
76
|
+
`because the person uses that app a lot on this computer. They clicked "${KEEP_GOING_HERE_REPLY}", so setup happens here. ` +
|
|
77
|
+
"Do not mention the offer. The kickoff follows.]");
|
|
78
|
+
}
|
|
79
|
+
/** Put in front of the person's message when they wrote something instead of clicking. */
|
|
80
|
+
export function setupOfferAnsweredInWordsNote(tool) {
|
|
81
|
+
return (`[Note from the HQ app: before setup started, the app said the person seems to use the ${CODING_TOOL_NAMES[tool]} app a lot ` +
|
|
82
|
+
`and offered to continue setup there, with a button that opens the ${CODING_TOOL_NAMES[tool]} app in their HQ folder and a "${KEEP_GOING_HERE_REPLY}" button. ` +
|
|
83
|
+
"They wrote the message below instead. Setup has not started and there was no kickoff. " +
|
|
84
|
+
`If they want to continue in the ${CODING_TOOL_NAMES[tool]} app, tell them in plain words to click the button above to open it. ` +
|
|
85
|
+
"Otherwise begin setup here as your instructions say for a first message.]");
|
|
86
|
+
}
|
|
87
|
+
/** How the person answered a pending offer: the button, or anything else. */
|
|
88
|
+
export function setupToolOfferAnswer(text, attachments) {
|
|
89
|
+
return text === KEEP_GOING_HERE_REPLY && attachments === 0 ? "kept_here" : "wrote";
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=setup-tool-offer.js.map
|
|
@@ -3,7 +3,6 @@ import type { LaneRecord } from "./types.js";
|
|
|
3
3
|
export declare const LEAD_AUTOPILOT_FLAG = "lanes.lead-autopilot";
|
|
4
4
|
export declare const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = "lanes.autopilot-missed-deadline-escalation";
|
|
5
5
|
export declare const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_DELAY_MS: number;
|
|
6
|
-
export declare const LEAD_AUTOPILOT_NUDGE_DEDUPE_FLAG = "lanes.autopilot-nudge-dedupe";
|
|
7
6
|
export declare const LEAD_AUTOPILOT_NUDGE_REDELIVERY_DELAY_MS: number;
|
|
8
7
|
export declare const LEAD_AUTOPILOT_NUDGE_STATE_RETENTION_MS: number;
|
|
9
8
|
export declare const LEAD_AUTOPILOT_REVIEW_DELAY_MS: number;
|
|
@@ -11,8 +10,6 @@ export declare const LEAD_AUTOPILOT_FILL_DELAY_MS: number;
|
|
|
11
10
|
export declare const LEAD_AUTOPILOT_PROMPT_LOOP = "personal/knowledge/team-lead-operating-loop.md";
|
|
12
11
|
export declare const LEAD_AUTOPILOT_KPI_STALE_AFTER_MINUTES = 55;
|
|
13
12
|
export declare const LEAD_AUTOPILOT_HEADLINE_STALE_AFTER_MINUTES = 55;
|
|
14
|
-
export declare const LEAD_AUTOPILOT_PER_LEAD_ERRORS_FLAG = "lanes.autopilot-per-lead-errors";
|
|
15
|
-
export declare const LEAD_AUTOPILOT_PER_LEAD_ERRORS_DEFAULT_ENABLED = false;
|
|
16
13
|
export interface RegisteredLead {
|
|
17
14
|
team: string;
|
|
18
15
|
lane: string;
|
|
@@ -115,7 +112,6 @@ export declare function planLeadAutopilotWakes(input: {
|
|
|
115
112
|
managerStatus?: Record<string, unknown>;
|
|
116
113
|
keepAliveResumed?: boolean;
|
|
117
114
|
kpiStalenessEnabled?: boolean;
|
|
118
|
-
kpiStalenessBatchEnabled?: boolean;
|
|
119
115
|
headlineStalenessEnabled?: boolean;
|
|
120
116
|
}): AutopilotWake[];
|
|
121
117
|
export declare function runLeadAutopilotPass(input: {
|
|
@@ -126,7 +122,6 @@ export declare function runLeadAutopilotPass(input: {
|
|
|
126
122
|
now: Date;
|
|
127
123
|
dryRun?: boolean;
|
|
128
124
|
readFlag?: typeof readLanesRegistryFlag;
|
|
129
|
-
readNudgeDedupeFlag?: typeof readLanesRegistryFlag;
|
|
130
125
|
readDeadlineFlag?: typeof readLanesRegistryFlag;
|
|
131
126
|
keepAliveResumedLeads?: ReadonlySet<string>;
|
|
132
127
|
missedDeadlineEscalationDelayMs?: number;
|
|
@@ -6,14 +6,13 @@ 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,
|
|
9
|
+
import { LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG, LEAD_AUTOPILOT_KPI_STALENESS_FLAG, LEAD_AUTOPILOT_HEADLINE_STALENESS_FLAG, LEAD_AUTOPILOT_OUTPOST_NATIVE_LEADS_FLAG, } from "./flag-keys.js";
|
|
10
10
|
import { resolveLaneCompany } from "./lane-company.js";
|
|
11
11
|
import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
12
12
|
import { readAllLanes } from "./store.js";
|
|
13
13
|
export const LEAD_AUTOPILOT_FLAG = "lanes.lead-autopilot";
|
|
14
14
|
export const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG;
|
|
15
15
|
export const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_DELAY_MS = 60 * 60_000;
|
|
16
|
-
export const LEAD_AUTOPILOT_NUDGE_DEDUPE_FLAG = LANE_AUTOPILOT_NUDGE_DEDUPE_FLAG;
|
|
17
16
|
export const LEAD_AUTOPILOT_NUDGE_REDELIVERY_DELAY_MS = 30 * 60_000;
|
|
18
17
|
export const LEAD_AUTOPILOT_NUDGE_STATE_RETENTION_MS = 24 * 60 * 60_000;
|
|
19
18
|
const LEAD_AUTOPILOT_DELIVERED_ID_RETENTION_MS = 7 * 24 * 60 * 60_000;
|
|
@@ -22,8 +21,6 @@ export const LEAD_AUTOPILOT_FILL_DELAY_MS = 15 * 60_000;
|
|
|
22
21
|
export const LEAD_AUTOPILOT_PROMPT_LOOP = "personal/knowledge/team-lead-operating-loop.md";
|
|
23
22
|
export const LEAD_AUTOPILOT_KPI_STALE_AFTER_MINUTES = 55;
|
|
24
23
|
export const LEAD_AUTOPILOT_HEADLINE_STALE_AFTER_MINUTES = 55;
|
|
25
|
-
export const LEAD_AUTOPILOT_PER_LEAD_ERRORS_FLAG = PER_LEAD_ERRORS_FLAG;
|
|
26
|
-
export const LEAD_AUTOPILOT_PER_LEAD_ERRORS_DEFAULT_ENABLED = false;
|
|
27
24
|
const LIVE_STATES = new Set(["running", "queued", "awaiting_input", "blocked"]);
|
|
28
25
|
const OPEN_STORY_STATES = new Set(["todo", "queued", "in_progress", "review", "blocked"]);
|
|
29
26
|
function isOpenProjectStory(story) {
|
|
@@ -478,7 +475,7 @@ export function planLeadAutopilotWakes(input) {
|
|
|
478
475
|
}
|
|
479
476
|
if (input.kpiStalenessEnabled) {
|
|
480
477
|
const staleKpis = managerKpiFreshness(status, now).filter((kpi) => kpi.stale);
|
|
481
|
-
if (
|
|
478
|
+
if (staleKpis.length > 0) {
|
|
482
479
|
const byOldest = [...staleKpis].sort((a, b) => (b.ageMinutes ?? Number.MAX_SAFE_INTEGER) - (a.ageMinutes ?? Number.MAX_SAFE_INTEGER) ||
|
|
483
480
|
a.name.localeCompare(b.name));
|
|
484
481
|
const identity = staleKpis
|
|
@@ -688,13 +685,11 @@ function readState(file) {
|
|
|
688
685
|
};
|
|
689
686
|
}
|
|
690
687
|
function appendAutopilotMessage(input) {
|
|
691
|
-
if (input.
|
|
688
|
+
if (input.state.delivered_ids?.[input.message.message_id])
|
|
692
689
|
return false;
|
|
693
690
|
appendDropboxMessage(input.hqRoot, input.leadLane, input.message);
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
input.state.delivered_ids[input.message.message_id] = input.message.at;
|
|
697
|
-
}
|
|
691
|
+
input.state.delivered_ids ??= {};
|
|
692
|
+
input.state.delivered_ids[input.message.message_id] = input.message.at;
|
|
698
693
|
return true;
|
|
699
694
|
}
|
|
700
695
|
function removeDeliveredIdsForFingerprint(state, fingerprint) {
|
|
@@ -799,7 +794,6 @@ async function runLeadAutopilotPassUnlocked(input) {
|
|
|
799
794
|
continue;
|
|
800
795
|
}
|
|
801
796
|
let stateChanged = false;
|
|
802
|
-
let perLeadErrorsEnabled = false;
|
|
803
797
|
let currentInputPath = registered.status;
|
|
804
798
|
try {
|
|
805
799
|
const readFlag = input.readFlag ?? readLanesRegistryFlag;
|
|
@@ -808,11 +802,9 @@ async function runLeadAutopilotPassUnlocked(input) {
|
|
|
808
802
|
result.skipped.push({ lead_lane: lead.lane_id, reason: "flag_off" });
|
|
809
803
|
continue;
|
|
810
804
|
}
|
|
811
|
-
perLeadErrorsEnabled = await readFlag(PER_LEAD_ERRORS_FLAG, { company });
|
|
812
805
|
currentInputPath = registered.status;
|
|
813
806
|
const status = readStatus(hqRoot, registered.status, company.slug);
|
|
814
807
|
const kpiStalenessEnabled = await (input.readFlag ?? readLanesRegistryFlag)(LEAD_AUTOPILOT_KPI_STALENESS_FLAG, { company });
|
|
815
|
-
const kpiStalenessBatchEnabled = kpiStalenessEnabled && await (input.readFlag ?? readLanesRegistryFlag)(LEAD_AUTOPILOT_KPI_STALENESS_BATCH_FLAG, { company });
|
|
816
808
|
const headlineStalenessEnabled = await (input.readFlag ?? readLanesRegistryFlag)(LEAD_AUTOPILOT_HEADLINE_STALENESS_FLAG, { company });
|
|
817
809
|
if (kpiStalenessEnabled) {
|
|
818
810
|
result.kpi_status ??= [];
|
|
@@ -834,8 +826,7 @@ async function runLeadAutopilotPassUnlocked(input) {
|
|
|
834
826
|
const stories = projectStories(hqRoot, registered.prd, company.slug, lead.project_id);
|
|
835
827
|
const children = lanes.filter((lane) => lane.senior.kind === "lane" && lane.senior.id === lead.lane_id);
|
|
836
828
|
const deadlineEscalationEnabled = await (input.readDeadlineFlag ?? readFlag)(LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG, { company });
|
|
837
|
-
|
|
838
|
-
if (nudgeDedupeEnabled && pruneDeliveredIds(state, now))
|
|
829
|
+
if (pruneDeliveredIds(state, now))
|
|
839
830
|
stateChanged = true;
|
|
840
831
|
currentInputPath = registered.status;
|
|
841
832
|
let planned = planLeadAutopilotWakes({
|
|
@@ -845,11 +836,10 @@ async function runLeadAutopilotPassUnlocked(input) {
|
|
|
845
836
|
now,
|
|
846
837
|
managerStatus: status,
|
|
847
838
|
kpiStalenessEnabled,
|
|
848
|
-
kpiStalenessBatchEnabled,
|
|
849
839
|
headlineStalenessEnabled,
|
|
850
840
|
keepAliveResumed: input.keepAliveResumedLeads?.has(lead.lane_id) === true,
|
|
851
841
|
});
|
|
852
|
-
if (
|
|
842
|
+
if (kpiStalenessEnabled) {
|
|
853
843
|
const batch = planned.find((wake) => wake.condition === "stale_kpi" && wake.kpi_id === "batch");
|
|
854
844
|
const headline = planned.find((wake) => wake.condition === "stale_headline");
|
|
855
845
|
if (batch && headline) {
|
|
@@ -872,13 +862,6 @@ async function runLeadAutopilotPassUnlocked(input) {
|
|
|
872
862
|
continue;
|
|
873
863
|
const entry = state.sent[key];
|
|
874
864
|
const isActive = activeKeys.has(key);
|
|
875
|
-
if (!nudgeDedupeEnabled) {
|
|
876
|
-
if (!isActive) {
|
|
877
|
-
delete state.sent[key];
|
|
878
|
-
stateChanged = true;
|
|
879
|
-
}
|
|
880
|
-
continue;
|
|
881
|
-
}
|
|
882
865
|
if (!isActive && entry.inactive_since === undefined) {
|
|
883
866
|
state.sent[key] = { ...entry, inactive_since: now.toISOString() };
|
|
884
867
|
stateChanged = true;
|
|
@@ -915,7 +898,7 @@ async function runLeadAutopilotPassUnlocked(input) {
|
|
|
915
898
|
if (suppressedKeys.has(key))
|
|
916
899
|
continue;
|
|
917
900
|
const prior = state.sent[key];
|
|
918
|
-
if (
|
|
901
|
+
if (prior && prior.fingerprint !== wake.fingerprint && !prior.acted) {
|
|
919
902
|
const leadActedSincePrior = statusUpdatedAt !== undefined && prior.status_updated_at !== undefined &&
|
|
920
903
|
Date.parse(statusUpdatedAt) > Date.parse(prior.status_updated_at);
|
|
921
904
|
if (!leadActedSincePrior) {
|
|
@@ -974,11 +957,11 @@ async function runLeadAutopilotPassUnlocked(input) {
|
|
|
974
957
|
continue;
|
|
975
958
|
if (wake.condition === "stale_headline")
|
|
976
959
|
continue;
|
|
977
|
-
const redeliveryDelay =
|
|
960
|
+
const redeliveryDelay = LEAD_AUTOPILOT_NUDGE_REDELIVERY_DELAY_MS;
|
|
978
961
|
if (!current.acted && !current.auto_started && current.escalated_at === undefined && now.getTime() - lastWakeAt >= redeliveryDelay) {
|
|
979
962
|
const wakeCount = current.wake_count ?? 1;
|
|
980
963
|
if (wakeCount < 2) {
|
|
981
|
-
const delivered = appendAutopilotMessage({ hqRoot, leadLane: lead.lane_id, state,
|
|
964
|
+
const delivered = appendAutopilotMessage({ hqRoot, leadLane: lead.lane_id, state, message: {
|
|
982
965
|
at: now.toISOString(),
|
|
983
966
|
message_id: `lead-autopilot:${wake.fingerprint}:wake-2`,
|
|
984
967
|
kind: "system_notice",
|
|
@@ -1002,7 +985,7 @@ async function runLeadAutopilotPassUnlocked(input) {
|
|
|
1002
985
|
continue;
|
|
1003
986
|
}
|
|
1004
987
|
if (!input.dryRun) {
|
|
1005
|
-
const delivered = appendAutopilotMessage({ hqRoot, leadLane: lead.lane_id, state,
|
|
988
|
+
const delivered = appendAutopilotMessage({ hqRoot, leadLane: lead.lane_id, state, message: {
|
|
1006
989
|
at: now.toISOString(),
|
|
1007
990
|
message_id: `lead-autopilot:${wake.fingerprint}`,
|
|
1008
991
|
kind: "system_notice",
|
|
@@ -1093,7 +1076,7 @@ async function runLeadAutopilotPassUnlocked(input) {
|
|
|
1093
1076
|
fingerprint,
|
|
1094
1077
|
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}.`,
|
|
1095
1078
|
};
|
|
1096
|
-
const delivered = appendAutopilotMessage({ hqRoot, leadLane: lead.lane_id, state,
|
|
1079
|
+
const delivered = appendAutopilotMessage({ hqRoot, leadLane: lead.lane_id, state, message: {
|
|
1097
1080
|
at: now.toISOString(),
|
|
1098
1081
|
message_id: `lead-autopilot:${fingerprint}`,
|
|
1099
1082
|
kind: "system_notice",
|
|
@@ -1160,7 +1143,7 @@ async function runLeadAutopilotPassUnlocked(input) {
|
|
|
1160
1143
|
catch (error) {
|
|
1161
1144
|
if (!input.dryRun && stateChanged)
|
|
1162
1145
|
writeState(stateFile, state);
|
|
1163
|
-
if (
|
|
1146
|
+
if (error instanceof LanesError && LEAD_AUTOPILOT_PER_LEAD_ERROR_CODES.has(error.error)) {
|
|
1164
1147
|
const errorPath = typeof error.details.path === "string" ? error.details.path : currentInputPath;
|
|
1165
1148
|
const messageId = `lead-autopilot-error:${error.error}:${createHash("sha256").update(`${error.error}\0${errorPath}`).digest("hex")}`;
|
|
1166
1149
|
result.skipped.push({ lane_id: lead.lane_id, error: error.error, path: errorPath, message: error.message });
|
|
@@ -2,7 +2,6 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import { type ReadLanesFlagOptions } from "./registry-flag.js";
|
|
3
3
|
import type { LaneRecord, LanesConfig } from "./types.js";
|
|
4
4
|
export declare const LOW_DISK_GUARD_FLAG = "lanes.low-disk-guard";
|
|
5
|
-
export declare const CODEX_ROLLOUT_RETENTION_FLAG = "lanes.codex-rollout-retention";
|
|
6
5
|
export declare function lowDiskGuardEnabledForLane(hqRoot: string, lane: LaneRecord, readFlag?: (key: string, options: ReadLanesFlagOptions) => Promise<boolean>): Promise<boolean>;
|
|
7
6
|
interface DiskStats {
|
|
8
7
|
bsize: number;
|
|
@@ -8,7 +8,6 @@ import { resolveLaneCompany } from "./lane-company.js";
|
|
|
8
8
|
import { effectiveAccountHome } from "./account-home.js";
|
|
9
9
|
import { lanesConfigPath } from "./config.js";
|
|
10
10
|
export const LOW_DISK_GUARD_FLAG = "lanes.low-disk-guard";
|
|
11
|
-
export const CODEX_ROLLOUT_RETENTION_FLAG = "lanes.codex-rollout-retention";
|
|
12
11
|
const DEFAULT_LOW_DISK_FREE_BYTES = 5 * 1024 ** 3;
|
|
13
12
|
const DEFAULT_LOW_DISK_FRACTION = 0.02;
|
|
14
13
|
const EXCLUDED_SESSION_FILES = new Set(["auth.json", ".credentials.json", "thread_history.sqlite"]);
|
|
@@ -12,17 +12,10 @@ export declare const LANE_HIRE_GRANT_FINISHED_LANES_FLAG = "lanes.hire-grant-cou
|
|
|
12
12
|
export declare const LANE_LIST_SKIP_REMOTE_REFRESH_FLAG = "lanes.list-skip-remote-refresh";
|
|
13
13
|
/** Enables missed-deadline lead nudges and second-miss operator escalation. */
|
|
14
14
|
export declare const LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = "lanes.autopilot-missed-deadline-escalation";
|
|
15
|
-
export declare const LANE_AUTOPILOT_NUDGE_DEDUPE_FLAG = "lanes.autopilot-nudge-dedupe";
|
|
16
15
|
/** Enables company-scoped lookup of a hostless lane ULID across Outpost indexes. */
|
|
17
16
|
export declare const LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG = "lanes.message-route-owner-host";
|
|
18
17
|
/** Enables stale manager-status KPI reporting and one-time lead nudges. */
|
|
19
18
|
export declare const LEAD_AUTOPILOT_KPI_STALENESS_FLAG = "lanes.autopilot-kpi-staleness";
|
|
20
|
-
/** Skips malformed lead inputs individually instead of aborting the pass. */
|
|
21
|
-
export declare const LEAD_AUTOPILOT_PER_LEAD_ERRORS_FLAG = "lanes.autopilot-per-lead-errors";
|
|
22
|
-
export declare const LEAD_AUTOPILOT_PER_LEAD_ERRORS_DEFAULT_ENABLED = false;
|
|
23
|
-
/** Batches stale manager-status KPI notices into one lead message per pass. */
|
|
24
|
-
export declare const LEAD_AUTOPILOT_KPI_STALENESS_BATCH_FLAG = "lanes.autopilot-kpi-staleness-batch";
|
|
25
|
-
export declare const LEAD_AUTOPILOT_KPI_STALENESS_BATCH_DEFAULT_ENABLED = false;
|
|
26
19
|
/** Enables staleness notices for a registered lead's dashboard headline. */
|
|
27
20
|
export declare const LEAD_AUTOPILOT_HEADLINE_STALENESS_FLAG = "lanes.autopilot-headline-staleness";
|
|
28
21
|
/** Lets an Outpost's autopilot wake leads created on that Outpost, which record a local host. */
|
|
@@ -12,17 +12,10 @@ export const LANE_HIRE_GRANT_FINISHED_LANES_FLAG = "lanes.hire-grant-count-live-
|
|
|
12
12
|
export const LANE_LIST_SKIP_REMOTE_REFRESH_FLAG = "lanes.list-skip-remote-refresh";
|
|
13
13
|
/** Enables missed-deadline lead nudges and second-miss operator escalation. */
|
|
14
14
|
export const LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = "lanes.autopilot-missed-deadline-escalation";
|
|
15
|
-
export const LANE_AUTOPILOT_NUDGE_DEDUPE_FLAG = "lanes.autopilot-nudge-dedupe";
|
|
16
15
|
/** Enables company-scoped lookup of a hostless lane ULID across Outpost indexes. */
|
|
17
16
|
export const LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG = "lanes.message-route-owner-host";
|
|
18
17
|
/** Enables stale manager-status KPI reporting and one-time lead nudges. */
|
|
19
18
|
export const LEAD_AUTOPILOT_KPI_STALENESS_FLAG = "lanes.autopilot-kpi-staleness";
|
|
20
|
-
/** Skips malformed lead inputs individually instead of aborting the pass. */
|
|
21
|
-
export const LEAD_AUTOPILOT_PER_LEAD_ERRORS_FLAG = "lanes.autopilot-per-lead-errors";
|
|
22
|
-
export const LEAD_AUTOPILOT_PER_LEAD_ERRORS_DEFAULT_ENABLED = false;
|
|
23
|
-
/** Batches stale manager-status KPI notices into one lead message per pass. */
|
|
24
|
-
export const LEAD_AUTOPILOT_KPI_STALENESS_BATCH_FLAG = "lanes.autopilot-kpi-staleness-batch";
|
|
25
|
-
export const LEAD_AUTOPILOT_KPI_STALENESS_BATCH_DEFAULT_ENABLED = false;
|
|
26
19
|
/** Enables staleness notices for a registered lead's dashboard headline. */
|
|
27
20
|
export const LEAD_AUTOPILOT_HEADLINE_STALENESS_FLAG = "lanes.autopilot-headline-staleness";
|
|
28
21
|
/** Lets an Outpost's autopilot wake leads created on that Outpost, which record a local host. */
|
|
@@ -955,21 +955,8 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
955
955
|
}
|
|
956
956
|
const config = loadLanesConfig(hqRoot);
|
|
957
957
|
try {
|
|
958
|
-
const companyRefs = new Map();
|
|
959
|
-
for (const lane of lanes) {
|
|
960
|
-
const company = resolveLaneCompany(hqRoot, lane).company;
|
|
961
|
-
if (company)
|
|
962
|
-
companyRefs.set(company.uid ?? company.slug, company);
|
|
963
|
-
}
|
|
964
|
-
const readFlag = deps.readRegistryFlag ?? readLanesRegistryFlag;
|
|
965
958
|
const configuredHomes = configuredCodexRolloutHomes(hqRoot, config, deps.host.hostname);
|
|
966
|
-
|
|
967
|
-
for (const company of configuredHomes.length > 0 ? companyRefs.values() : []) {
|
|
968
|
-
retentionEnabled ||= await readFlag("lanes.codex-rollout-retention", { company });
|
|
969
|
-
if (retentionEnabled)
|
|
970
|
-
break;
|
|
971
|
-
}
|
|
972
|
-
if (retentionEnabled) {
|
|
959
|
+
if (configuredHomes.length > 0) {
|
|
973
960
|
const presentHomes = [];
|
|
974
961
|
for (const home of configuredHomes) {
|
|
975
962
|
try {
|