@indigoai-us/hq-cli 5.311.4 → 5.312.1
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 +20 -0
- package/assets/scaffold/core/scripts/lib/work-mesh-live-hook.sh +378 -0
- package/assets/scaffold/core/scripts/lib/work-mesh-live-rebind.sh +141 -0
- package/assets/scaffold/core/scripts/migrate-policy-triggers.sh +373 -0
- package/assets/scaffold/core/scripts/register-project.sh +262 -0
- package/assets/scaffold/core/scripts/work-mesh-live-rebind.sh +59 -0
- package/dist/command-catalog.generated.d.ts +160 -0
- package/dist/command-catalog.generated.js +200 -0
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/company-rename-flag.d.ts +4 -0
- package/dist/commands/company-rename-flag.js +7 -0
- package/dist/commands/company-rename.d.ts +4 -0
- package/dist/commands/company-rename.js +90 -0
- package/dist/commands/company.js +2 -0
- package/dist/commands/core-checkpoint-stop-gate-native.js +12 -0
- package/dist/commands/core-eval-trigger-native.d.ts +4 -0
- package/dist/commands/core-eval-trigger-native.js +12 -0
- package/dist/commands/core-migrate-policy-triggers-native.d.ts +21 -0
- package/dist/commands/core-migrate-policy-triggers-native.js +378 -0
- package/dist/commands/core-native-hook-helper-commands.d.ts +4 -0
- package/dist/commands/core-native-hook-helper-commands.js +56 -0
- package/dist/commands/core-native-hook-helpers-flag.d.ts +4 -0
- package/dist/commands/core-native-hook-helpers-flag.js +7 -0
- package/dist/commands/core-register-project-native.d.ts +30 -0
- package/dist/commands/core-register-project-native.js +347 -0
- package/dist/commands/core-work-mesh-live-rebind-native.d.ts +18 -0
- package/dist/commands/core-work-mesh-live-rebind-native.js +262 -0
- package/dist/commands/core.js +7 -0
- package/dist/commands/daemon.js +22 -0
- package/dist/commands/dm.js +22 -6
- package/dist/commands/files-browse.js +16 -4
- package/dist/commands/lanes-message.js +17 -2
- package/dist/commands/lanes-shared.d.ts +1 -0
- package/dist/commands/lanes.js +96 -20
- package/dist/commands/vault-retention-flag.d.ts +3 -0
- package/dist/commands/vault-retention-flag.js +9 -0
- package/dist/commands/vault-retention.d.ts +11 -0
- package/dist/commands/vault-retention.js +69 -0
- package/dist/fast-core.js +10 -3
- package/dist/fast-scaffold.d.ts +2 -0
- package/dist/fast-scaffold.js +11 -0
- package/dist/lib/agents/model-catalog.d.ts +3 -2
- package/dist/lib/company-skill-wrapper.d.ts +0 -8
- package/dist/lib/core-utils/codex-skill-bridge.d.ts +2 -1
- package/dist/lib/core-utils/sentry-report.js +21 -4
- package/dist/lib/daemon/catalog.d.ts +2 -0
- package/dist/lib/daemon/catalog.js +13 -1
- package/dist/lib/daemon/config.d.ts +2 -0
- package/dist/lib/daemon/config.js +4 -0
- package/dist/lib/daemon/run.d.ts +2 -0
- package/dist/lib/daemon/run.js +5 -0
- package/dist/lib/deploy-api.d.ts +0 -1
- package/dist/lib/deploy-api.js +1 -1
- package/dist/lib/doctor/checks/work-context.js +9 -1
- package/dist/lib/lanes/autopilot.d.ts +38 -1
- package/dist/lib/lanes/autopilot.js +353 -5
- package/dist/lib/lanes/flag-keys.d.ts +9 -1
- package/dist/lib/lanes/flag-keys.js +9 -1
- package/dist/lib/lanes/keep-alive.d.ts +2 -0
- package/dist/lib/lanes/keep-alive.js +2 -0
- package/dist/lib/lanes/reconcile.d.ts +5 -0
- package/dist/lib/lanes/reconcile.js +57 -4
- package/dist/lib/lanes/watcher-main.js +20 -1
- package/dist/lib/lanes/watcher.d.ts +9 -0
- package/dist/lib/lanes/watcher.js +47 -1
- package/dist/lib/mesh/client.js +23 -5
- package/dist/lib/mesh/denial-codes.d.ts +5 -0
- package/dist/lib/mesh/denial-codes.js +14 -0
- package/dist/lib/mesh/live/daemon/doctor.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/doctor.js +11 -1
- package/dist/lib/report-run.d.ts +3 -2
- package/dist/lib/report-templates.d.ts +0 -1
- package/dist/lib/report-templates.js +0 -3
- package/dist/lib/scaffold-cli-floor.d.ts +0 -1
- package/dist/lib/scaffold-cli-floor.js +1 -1
- package/dist/lib/search-index/embed-lock.d.ts +2 -0
- package/dist/lib/search-index/embed-lock.js +10 -1
- package/dist/lib/work-context/contract.d.ts +3 -0
- package/dist/lib/work-context/contract.schema.json +3 -1
- package/dist/lib/work-context/outbox.d.ts +14 -5
- package/dist/lib/work-context/outbox.js +48 -5
- package/dist/lib/work-context/reconcile.js +26 -5
- package/dist/main.d.ts +20 -0
- package/dist/main.js +8 -1
- package/dist/run/discover-schemas.d.ts +2 -1
- package/dist/utils/core-eval-trigger.d.ts +4 -0
- package/dist/utils/core-eval-trigger.js +88 -0
- package/dist/utils/hq-roots.d.ts +0 -2
- package/dist/utils/hq-roots.js +1 -8
- package/dist/utils/incomplete-install-error.d.ts +7 -6
- package/dist/utils/large-file-guard.d.ts +0 -10
- package/dist/utils/large-file-guard.js +1 -1
- package/dist/utils/package-root-diagnostics.d.ts +1 -1
- package/dist/utils/self-update.d.ts +2 -1
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { laneRunsOnHost } from "./admission.js";
|
|
@@ -5,14 +6,20 @@ import { isControllerAliasOutpostId } from "./admission-host.js";
|
|
|
5
6
|
import { LEAD_REGISTRY_REL, validateOutpostLeadRegistry } from "./autopilot-registry.js";
|
|
6
7
|
import { appendDropboxMessage } from "./dropbox.js";
|
|
7
8
|
import { LanesError } from "./errors.js";
|
|
9
|
+
import { LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG } from "./flag-keys.js";
|
|
8
10
|
import { resolveLaneCompany } from "./lane-company.js";
|
|
9
11
|
import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
12
|
+
import { LEAD_AUTOPILOT_KPI_STALENESS_FLAG } from "./flag-keys.js";
|
|
10
13
|
import { readAllLanes } from "./store.js";
|
|
11
14
|
export const LEAD_AUTOPILOT_FLAG = "lanes.lead-autopilot";
|
|
12
15
|
export const LEAD_AUTOPILOT_DEFAULT_ENABLED = false;
|
|
16
|
+
export const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG;
|
|
17
|
+
export const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_DEFAULT_ENABLED = false;
|
|
18
|
+
export const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_DELAY_MS = 60 * 60_000;
|
|
13
19
|
export const LEAD_AUTOPILOT_REVIEW_DELAY_MS = 15 * 60_000;
|
|
14
20
|
export const LEAD_AUTOPILOT_FILL_DELAY_MS = 15 * 60_000;
|
|
15
21
|
export const LEAD_AUTOPILOT_PROMPT_LOOP = "personal/knowledge/team-lead-operating-loop.md";
|
|
22
|
+
export const LEAD_AUTOPILOT_KPI_STALE_AFTER_MINUTES = 55;
|
|
16
23
|
const LIVE_STATES = new Set(["running", "queued", "awaiting_input", "blocked"]);
|
|
17
24
|
const OPEN_STORY_STATES = new Set(["todo", "queued", "in_progress", "review", "blocked"]);
|
|
18
25
|
function isOpenProjectStory(story) {
|
|
@@ -157,9 +164,112 @@ function projectStories(hqRoot, relativePath, companySlug, projectId) {
|
|
|
157
164
|
...storyLaneIds(story),
|
|
158
165
|
...(typeof story.updated_at === "string" ? { updatedAt: story.updated_at } : {}),
|
|
159
166
|
...(typeof story.external_blocker === "string" ? { externalBlocker: story.external_blocker } : {}),
|
|
167
|
+
deadlineFields: story,
|
|
160
168
|
};
|
|
161
169
|
});
|
|
162
170
|
}
|
|
171
|
+
const DEADLINE_FIELDS = ["deadline", "due_at", "unblock_by", "unblock_at", "eta"];
|
|
172
|
+
const CLOSED_COMMITMENT_STATES = new Set(["done", "resolved", "closed", "cancelled", "canceled", "dropped", "complete", "completed"]);
|
|
173
|
+
const COMMITMENT_SECTIONS = ["open_blockers", "waiting", "open_decisions", "decisions", "slot_requests", "blockers"];
|
|
174
|
+
function hasDeadlineField(value) {
|
|
175
|
+
return DEADLINE_FIELDS.some((field) => value[field] !== undefined && value[field] !== null && value[field] !== "");
|
|
176
|
+
}
|
|
177
|
+
function commitmentDue(value, context) {
|
|
178
|
+
const dates = [];
|
|
179
|
+
const errors = [];
|
|
180
|
+
for (const field of DEADLINE_FIELDS) {
|
|
181
|
+
const raw = value[field];
|
|
182
|
+
if (raw === undefined || raw === null || raw === "")
|
|
183
|
+
continue;
|
|
184
|
+
if (typeof raw !== "string" || !/^\d{4}-\d\d-\d\dT\d\d:\d\d(?::\d\d(?:\.\d{1,9})?)?(?:Z|[+-]\d\d:\d\d)$/.test(raw) || !Number.isFinite(Date.parse(raw))) {
|
|
185
|
+
// `eta` is also used as free-text planning context in manager and story
|
|
186
|
+
// records. It is a deadline only when it is already a valid ISO instant.
|
|
187
|
+
if (field === "eta")
|
|
188
|
+
continue;
|
|
189
|
+
errors.push({ field, reason: `${context}.${field} must be a valid date-time` });
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
dates.push({ field, iso: new Date(Date.parse(raw)).toISOString() });
|
|
193
|
+
}
|
|
194
|
+
if (errors.length > 0)
|
|
195
|
+
return { due: null, errors };
|
|
196
|
+
if (dates.length === 0)
|
|
197
|
+
return { due: null, errors };
|
|
198
|
+
if (new Set(dates.map((date) => date.iso)).size > 1) {
|
|
199
|
+
return { due: null, errors: [{ field: "deadline_fields", reason: `${context} has conflicting deadline fields` }] };
|
|
200
|
+
}
|
|
201
|
+
return { due: dates[0].iso, errors };
|
|
202
|
+
}
|
|
203
|
+
function isResolvedCommitment(value, status) {
|
|
204
|
+
return value.passes === true || value.resolved === true || value.open === false ||
|
|
205
|
+
value.resolved_at !== undefined && value.resolved_at !== null ||
|
|
206
|
+
value.completed_at !== undefined && value.completed_at !== null ||
|
|
207
|
+
(typeof status === "string" && CLOSED_COMMITMENT_STATES.has(status.toLowerCase()));
|
|
208
|
+
}
|
|
209
|
+
function optionalLabel(value, fields, fallback) {
|
|
210
|
+
for (const field of fields) {
|
|
211
|
+
const candidate = value[field];
|
|
212
|
+
if (typeof candidate === "string" && candidate.trim())
|
|
213
|
+
return candidate.trim();
|
|
214
|
+
}
|
|
215
|
+
return fallback;
|
|
216
|
+
}
|
|
217
|
+
function deadlineCommitments(status, stories, leadLane) {
|
|
218
|
+
const commitments = [];
|
|
219
|
+
const errors = [];
|
|
220
|
+
for (const section of COMMITMENT_SECTIONS) {
|
|
221
|
+
const rows = status[section];
|
|
222
|
+
if (rows === undefined)
|
|
223
|
+
continue;
|
|
224
|
+
if (!Array.isArray(rows)) {
|
|
225
|
+
errors.push({ lead_lane: leadLane, item_ref: `manager:${section}`, field: section, reason: `Manager status ${section} must be an array` });
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
rows.forEach((row, index) => {
|
|
229
|
+
if (!isRecord(row) || !hasDeadlineField(row))
|
|
230
|
+
return;
|
|
231
|
+
const itemRef = `manager:${section}[${index}]`;
|
|
232
|
+
const dueResult = commitmentDue(row, `Manager status ${section}[${index}]`);
|
|
233
|
+
errors.push(...dueResult.errors.map((error) => ({ lead_lane: leadLane, item_ref: itemRef, ...error })));
|
|
234
|
+
if (dueResult.errors.length > 0 || !dueResult.due)
|
|
235
|
+
return;
|
|
236
|
+
const id = row.id;
|
|
237
|
+
if (typeof id !== "string" || !id.trim()) {
|
|
238
|
+
errors.push({ lead_lane: leadLane, item_ref: itemRef, field: "id", reason: `Manager status ${section}[${index}] with a deadline needs a non-empty id` });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const owner = optionalLabel(row, ["owner", "unblock_owner", "waiting_on", "assigned_to"], "unassigned");
|
|
242
|
+
const state = typeof row.status === "string" ? row.status : typeof row.state === "string" ? row.state : undefined;
|
|
243
|
+
commitments.push({
|
|
244
|
+
key: `manager:${section}:${id}`,
|
|
245
|
+
item: id,
|
|
246
|
+
owner,
|
|
247
|
+
due: dueResult.due,
|
|
248
|
+
description: optionalLabel(row, ["what", "question", "request", "title", "line"], id),
|
|
249
|
+
resolved: isResolvedCommitment(row, state),
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
for (const story of stories) {
|
|
254
|
+
const raw = story.deadlineFields;
|
|
255
|
+
if (!raw || !hasDeadlineField(raw))
|
|
256
|
+
continue;
|
|
257
|
+
const dueResult = commitmentDue(raw, `Project story ${story.id}`);
|
|
258
|
+
errors.push(...dueResult.errors.map((error) => ({ lead_lane: leadLane, item_ref: `story:${story.id}`, ...error })));
|
|
259
|
+
if (dueResult.errors.length > 0 || !dueResult.due)
|
|
260
|
+
continue;
|
|
261
|
+
const state = typeof raw.status === "string" ? raw.status : typeof raw.state === "string" ? raw.state : story.status;
|
|
262
|
+
commitments.push({
|
|
263
|
+
key: `story:${story.id}`,
|
|
264
|
+
item: story.id,
|
|
265
|
+
owner: optionalLabel(raw, ["owner", "assigned_to"], "lead"),
|
|
266
|
+
due: dueResult.due,
|
|
267
|
+
description: optionalLabel(raw, ["title", "name", "what"], story.id),
|
|
268
|
+
resolved: isResolvedCommitment(raw, state),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return { commitments: commitments.sort((a, b) => a.key.localeCompare(b.key)), errors };
|
|
272
|
+
}
|
|
163
273
|
function readStatus(hqRoot, relativePath, companySlug) {
|
|
164
274
|
if (path.isAbsolute(relativePath)) {
|
|
165
275
|
throw new LanesError("lead_autopilot_status_path_invalid", `Registered status path must be relative to the HQ root: ${relativePath}`, { path: relativePath });
|
|
@@ -232,6 +342,72 @@ function statusBlockers(status) {
|
|
|
232
342
|
return value;
|
|
233
343
|
});
|
|
234
344
|
}
|
|
345
|
+
function managerKpiFreshness(status, now) {
|
|
346
|
+
const rawKpis = status.kpis;
|
|
347
|
+
if (rawKpis === undefined)
|
|
348
|
+
return [];
|
|
349
|
+
if (!Array.isArray(rawKpis)) {
|
|
350
|
+
return [{
|
|
351
|
+
name: "kpis",
|
|
352
|
+
measuredAt: null,
|
|
353
|
+
ageMinutes: null,
|
|
354
|
+
stale: false,
|
|
355
|
+
staleAfterMinutes: LEAD_AUTOPILOT_KPI_STALE_AFTER_MINUTES,
|
|
356
|
+
fingerprint: "",
|
|
357
|
+
error: "invalid_kpi",
|
|
358
|
+
}];
|
|
359
|
+
}
|
|
360
|
+
return rawKpis.map((value, index) => {
|
|
361
|
+
const invalid = () => {
|
|
362
|
+
let name = `KPI ${index + 1}`;
|
|
363
|
+
if (isRecord(value)) {
|
|
364
|
+
const rawName = typeof value.name === "string" ? value.name : value.id;
|
|
365
|
+
if (typeof rawName === "string" && rawName.trim())
|
|
366
|
+
name = rawName.trim();
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
name,
|
|
370
|
+
measuredAt: null,
|
|
371
|
+
ageMinutes: null,
|
|
372
|
+
stale: false,
|
|
373
|
+
staleAfterMinutes: LEAD_AUTOPILOT_KPI_STALE_AFTER_MINUTES,
|
|
374
|
+
fingerprint: "",
|
|
375
|
+
...(isRecord(value) && typeof value.status === "string" ? { status: value.status } : {}),
|
|
376
|
+
error: "invalid_kpi",
|
|
377
|
+
};
|
|
378
|
+
};
|
|
379
|
+
if (!isRecord(value)) {
|
|
380
|
+
return invalid();
|
|
381
|
+
}
|
|
382
|
+
const rawName = typeof value.name === "string" ? value.name : value.id;
|
|
383
|
+
if (typeof rawName !== "string" || !rawName.trim()) {
|
|
384
|
+
return invalid();
|
|
385
|
+
}
|
|
386
|
+
const staleAfterMinutes = value.stale_after_minutes === undefined
|
|
387
|
+
? LEAD_AUTOPILOT_KPI_STALE_AFTER_MINUTES
|
|
388
|
+
: value.stale_after_minutes;
|
|
389
|
+
if (typeof staleAfterMinutes !== "number" || !Number.isSafeInteger(staleAfterMinutes) || staleAfterMinutes < 1 || staleAfterMinutes > 525_600) {
|
|
390
|
+
return invalid();
|
|
391
|
+
}
|
|
392
|
+
const measuredAt = typeof value.measured_at === "string" && Number.isFinite(Date.parse(value.measured_at))
|
|
393
|
+
? new Date(Date.parse(value.measured_at)).toISOString()
|
|
394
|
+
: null;
|
|
395
|
+
const elapsedMs = measuredAt === null ? null : Math.max(0, now.getTime() - Date.parse(measuredAt));
|
|
396
|
+
const ageMinutes = elapsedMs === null ? null : Math.floor(elapsedMs / 60_000);
|
|
397
|
+
const terminalStatus = typeof value.status === "string" && ["superseded", "retired", "dropped"].includes(value.status.toLowerCase());
|
|
398
|
+
const stale = !terminalStatus && (elapsedMs === null || elapsedMs > staleAfterMinutes * 60_000);
|
|
399
|
+
const fingerprint = `stale-kpi:${createHash("sha256").update(JSON.stringify([rawName.trim(), measuredAt, value.value ?? null])).digest("hex")}`;
|
|
400
|
+
return {
|
|
401
|
+
name: rawName.trim(),
|
|
402
|
+
measuredAt,
|
|
403
|
+
ageMinutes,
|
|
404
|
+
stale,
|
|
405
|
+
staleAfterMinutes,
|
|
406
|
+
fingerprint,
|
|
407
|
+
...(typeof value.status === "string" ? { status: value.status } : {}),
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
}
|
|
235
411
|
function leastMoved(lines, priorLines = []) {
|
|
236
412
|
const priorById = new Map(priorLines.map((line) => [line.id, line]));
|
|
237
413
|
return [...lines].sort((a, b) => {
|
|
@@ -249,6 +425,23 @@ export function planLeadAutopilotWakes(input) {
|
|
|
249
425
|
const blockers = statusBlockers(status);
|
|
250
426
|
const byStory = new Map(stories.map((story) => [story.id, story]));
|
|
251
427
|
const wakes = [];
|
|
428
|
+
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
|
+
wakes.push({
|
|
436
|
+
condition: "stale_kpi",
|
|
437
|
+
lead_lane: lead.lane_id,
|
|
438
|
+
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}.`,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
252
445
|
const finishedReviews = children.flatMap((child) => {
|
|
253
446
|
if (!["done", "blocked", "failed", "interrupted"].includes(child.state))
|
|
254
447
|
return [];
|
|
@@ -384,9 +577,10 @@ export function planLeadAutopilotWakes(input) {
|
|
|
384
577
|
}
|
|
385
578
|
function readState(file) {
|
|
386
579
|
if (!fs.existsSync(file))
|
|
387
|
-
return { schema_version: "hq-lanes-autopilot-state.v1", sent: {} };
|
|
580
|
+
return { schema_version: "hq-lanes-autopilot-state.v1", sent: {}, deadline_misses: {} };
|
|
388
581
|
const value = readJsonFile(file, "lead_autopilot_state_invalid");
|
|
389
|
-
if (!isRecord(value) || value.schema_version !== "hq-lanes-autopilot-state.v1" || !isRecord(value.sent)
|
|
582
|
+
if (!isRecord(value) || value.schema_version !== "hq-lanes-autopilot-state.v1" || !isRecord(value.sent) ||
|
|
583
|
+
(value.deadline_misses !== undefined && !isRecord(value.deadline_misses))) {
|
|
390
584
|
throw new LanesError("lead_autopilot_state_invalid", `Invalid autopilot state ${file}`, { path: file });
|
|
391
585
|
}
|
|
392
586
|
for (const [key, row] of Object.entries(value.sent)) {
|
|
@@ -401,6 +595,16 @@ function readState(file) {
|
|
|
401
595
|
throw new LanesError("lead_autopilot_state_invalid", `Invalid autopilot state entry ${key}`, { path: file, key });
|
|
402
596
|
}
|
|
403
597
|
}
|
|
598
|
+
for (const [key, row] of Object.entries(value.deadline_misses ?? {})) {
|
|
599
|
+
if (!isRecord(row) || typeof row.item !== "string" || typeof row.owner !== "string" ||
|
|
600
|
+
typeof row.due_at !== "string" || !Number.isFinite(Date.parse(row.due_at)) ||
|
|
601
|
+
!Number.isSafeInteger(row.miss_count) || row.miss_count < 1 ||
|
|
602
|
+
typeof row.first_missed_at !== "string" || !Number.isFinite(Date.parse(row.first_missed_at)) ||
|
|
603
|
+
typeof row.currently_missed !== "boolean" ||
|
|
604
|
+
(row.escalated_at !== undefined && (typeof row.escalated_at !== "string" || !Number.isFinite(Date.parse(row.escalated_at))))) {
|
|
605
|
+
throw new LanesError("lead_autopilot_state_invalid", `Invalid missed-deadline state entry ${key}`, { path: file, key });
|
|
606
|
+
}
|
|
607
|
+
}
|
|
404
608
|
return value;
|
|
405
609
|
}
|
|
406
610
|
function writeState(file, state) {
|
|
@@ -465,26 +669,44 @@ async function runLeadAutopilotPassUnlocked(input) {
|
|
|
465
669
|
}
|
|
466
670
|
let stateChanged = false;
|
|
467
671
|
try {
|
|
468
|
-
const
|
|
672
|
+
const readFlag = input.readFlag ?? readLanesRegistryFlag;
|
|
673
|
+
const enabled = await readFlag(LEAD_AUTOPILOT_FLAG, { company });
|
|
469
674
|
if (!enabled) {
|
|
470
675
|
result.skipped.push({ lead_lane: lead.lane_id, reason: "flag_off" });
|
|
471
676
|
continue;
|
|
472
677
|
}
|
|
473
678
|
const status = readStatus(hqRoot, registered.status, company.slug);
|
|
679
|
+
const kpiStalenessEnabled = await (input.readFlag ?? readLanesRegistryFlag)(LEAD_AUTOPILOT_KPI_STALENESS_FLAG, { company });
|
|
680
|
+
if (kpiStalenessEnabled) {
|
|
681
|
+
result.kpi_status ??= [];
|
|
682
|
+
result.kpi_status.push(...managerKpiFreshness(status, now).map((kpi) => ({
|
|
683
|
+
lead_lane: lead.lane_id,
|
|
684
|
+
team: registered.team,
|
|
685
|
+
kpi: kpi.name,
|
|
686
|
+
age_minutes: kpi.ageMinutes,
|
|
687
|
+
stale: kpi.stale,
|
|
688
|
+
stale_after_minutes: kpi.staleAfterMinutes,
|
|
689
|
+
...(kpi.status === undefined ? {} : { status: kpi.status }),
|
|
690
|
+
...(kpi.error === undefined ? {} : { error: kpi.error }),
|
|
691
|
+
})));
|
|
692
|
+
}
|
|
474
693
|
const statusUpdatedAt = typeof status.updated_at === "string" && Number.isFinite(Date.parse(status.updated_at))
|
|
475
694
|
? new Date(Date.parse(status.updated_at)).toISOString()
|
|
476
695
|
: undefined;
|
|
477
696
|
const stories = projectStories(hqRoot, registered.prd, company.slug, lead.project_id);
|
|
478
697
|
const children = lanes.filter((lane) => lane.senior.kind === "lane" && lane.senior.id === lead.lane_id);
|
|
698
|
+
const deadlineEscalationEnabled = await (input.readDeadlineFlag ?? readFlag)(LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG, { company });
|
|
479
699
|
const planned = planLeadAutopilotWakes({
|
|
480
700
|
lead,
|
|
481
701
|
children,
|
|
482
702
|
stories,
|
|
483
703
|
now,
|
|
484
704
|
managerStatus: status,
|
|
705
|
+
kpiStalenessEnabled,
|
|
485
706
|
keepAliveResumed: input.keepAliveResumedLeads?.has(lead.lane_id) === true,
|
|
486
707
|
});
|
|
487
|
-
const
|
|
708
|
+
const stateKey = (wake) => `${lead.lane_id}:${wake.condition}${wake.condition === "stale_kpi" ? `:${wake.kpi_id}` : ""}`;
|
|
709
|
+
const activeKeys = new Set(planned.map(stateKey));
|
|
488
710
|
for (const key of Object.keys(state.sent)) {
|
|
489
711
|
if (key.startsWith(`${lead.lane_id}:`) && !activeKeys.has(key)) {
|
|
490
712
|
delete state.sent[key];
|
|
@@ -492,7 +714,7 @@ async function runLeadAutopilotPassUnlocked(input) {
|
|
|
492
714
|
}
|
|
493
715
|
}
|
|
494
716
|
for (const wake of planned) {
|
|
495
|
-
const key =
|
|
717
|
+
const key = stateKey(wake);
|
|
496
718
|
const prior = state.sent[key];
|
|
497
719
|
if (prior?.fingerprint === wake.fingerprint) {
|
|
498
720
|
if (input.dryRun)
|
|
@@ -535,6 +757,10 @@ async function runLeadAutopilotPassUnlocked(input) {
|
|
|
535
757
|
}
|
|
536
758
|
}
|
|
537
759
|
}
|
|
760
|
+
// Stale KPIs get one notice per measurement fingerprint, not the
|
|
761
|
+
// repeated wake/escalation cadence used by other conditions.
|
|
762
|
+
if (wake.condition === "stale_kpi")
|
|
763
|
+
continue;
|
|
538
764
|
if (!current.acted && !current.auto_started && current.escalated_at === undefined && now.getTime() - lastWakeAt >= LEAD_AUTOPILOT_FILL_DELAY_MS) {
|
|
539
765
|
const wakeCount = current.wake_count ?? 1;
|
|
540
766
|
if (wakeCount < 2) {
|
|
@@ -578,6 +804,128 @@ async function runLeadAutopilotPassUnlocked(input) {
|
|
|
578
804
|
}
|
|
579
805
|
result.sent.push(wake);
|
|
580
806
|
}
|
|
807
|
+
// Planned wakes have already been delivered. Persist their dedupe state
|
|
808
|
+
// before deadline parsing/recording so an unexpected deadline failure
|
|
809
|
+
// cannot make those wakes repeat on the next pass.
|
|
810
|
+
if (!input.dryRun && stateChanged) {
|
|
811
|
+
writeState(stateFile, state);
|
|
812
|
+
stateChanged = false;
|
|
813
|
+
}
|
|
814
|
+
if (deadlineEscalationEnabled) {
|
|
815
|
+
result.missed_deadlines ??= [];
|
|
816
|
+
const parsedCommitments = deadlineCommitments(status, stories, lead.lane_id);
|
|
817
|
+
if (parsedCommitments.errors.length > 0) {
|
|
818
|
+
result.deadline_errors ??= [];
|
|
819
|
+
result.deadline_errors.push(...parsedCommitments.errors);
|
|
820
|
+
}
|
|
821
|
+
const commitments = parsedCommitments.commitments;
|
|
822
|
+
const currentCommitmentKeys = new Set(commitments.map((item) => `${lead.lane_id}:${item.key}`));
|
|
823
|
+
const deadlineState = state.deadline_misses ?? (state.deadline_misses = {});
|
|
824
|
+
for (const key of Object.keys(deadlineState)) {
|
|
825
|
+
if (key.startsWith(`${lead.lane_id}:`) && !currentCommitmentKeys.has(key)) {
|
|
826
|
+
delete deadlineState[key];
|
|
827
|
+
stateChanged = true;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
const escalationDelay = input.missedDeadlineEscalationDelayMs ?? LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_DELAY_MS;
|
|
831
|
+
if (!Number.isFinite(escalationDelay) || escalationDelay < 0) {
|
|
832
|
+
throw new LanesError("lead_autopilot_deadline_config_invalid", "Missed-deadline escalation delay must be a non-negative number");
|
|
833
|
+
}
|
|
834
|
+
for (const commitment of commitments) {
|
|
835
|
+
const key = `${lead.lane_id}:${commitment.key}`;
|
|
836
|
+
const prior = deadlineState[key];
|
|
837
|
+
if (commitment.resolved) {
|
|
838
|
+
if (prior) {
|
|
839
|
+
delete deadlineState[key];
|
|
840
|
+
stateChanged = true;
|
|
841
|
+
}
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
const isMissed = now.getTime() > Date.parse(commitment.due);
|
|
845
|
+
if (!isMissed) {
|
|
846
|
+
if (prior && (prior.due_at !== commitment.due || prior.owner !== commitment.owner || prior.currently_missed)) {
|
|
847
|
+
deadlineState[key] = { ...prior, due_at: commitment.due, owner: commitment.owner, currently_missed: false };
|
|
848
|
+
stateChanged = true;
|
|
849
|
+
}
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
if (!prior || prior.miss_count === 0) {
|
|
853
|
+
const firstMiss = {
|
|
854
|
+
item: commitment.item,
|
|
855
|
+
owner: commitment.owner,
|
|
856
|
+
due_at: commitment.due,
|
|
857
|
+
miss_count: 1,
|
|
858
|
+
first_missed_at: now.toISOString(),
|
|
859
|
+
currently_missed: true,
|
|
860
|
+
};
|
|
861
|
+
if (!input.dryRun) {
|
|
862
|
+
const fingerprint = `missed-deadline:${commitment.key}:${commitment.owner}:${commitment.due}`;
|
|
863
|
+
const wake = {
|
|
864
|
+
condition: "missed_deadline",
|
|
865
|
+
lead_lane: lead.lane_id,
|
|
866
|
+
story_id: commitment.item,
|
|
867
|
+
fingerprint,
|
|
868
|
+
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
|
+
};
|
|
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
|
+
});
|
|
876
|
+
stateChanged = true;
|
|
877
|
+
result.sent.push(wake);
|
|
878
|
+
deadlineState[key] = firstMiss;
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
deadlineState[key] = firstMiss;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
const dueChanged = prior.due_at !== commitment.due || prior.owner !== commitment.owner;
|
|
886
|
+
const reMissed = !prior.currently_missed;
|
|
887
|
+
const firstMissAt = Date.parse(prior.first_missed_at);
|
|
888
|
+
if (!Number.isFinite(firstMissAt)) {
|
|
889
|
+
throw new LanesError("lead_autopilot_state_invalid", `Invalid first missed-deadline time for ${commitment.item}`, { lead_lane: lead.lane_id, item: commitment.item });
|
|
890
|
+
}
|
|
891
|
+
const stillLateAgain = now.getTime() - firstMissAt >= escalationDelay;
|
|
892
|
+
const secondMiss = !prior.escalated_at && (dueChanged || reMissed || stillLateAgain);
|
|
893
|
+
const next = {
|
|
894
|
+
...prior,
|
|
895
|
+
item: commitment.item,
|
|
896
|
+
owner: commitment.owner,
|
|
897
|
+
due_at: commitment.due,
|
|
898
|
+
miss_count: secondMiss ? prior.miss_count + 1 : prior.miss_count,
|
|
899
|
+
currently_missed: true,
|
|
900
|
+
...(secondMiss ? { escalated_at: now.toISOString() } : {}),
|
|
901
|
+
};
|
|
902
|
+
if (secondMiss) {
|
|
903
|
+
const fingerprint = `missed-deadline:${commitment.key}:${commitment.owner}:${commitment.due}:miss-${next.miss_count}`;
|
|
904
|
+
const escalation = {
|
|
905
|
+
team: registered.team,
|
|
906
|
+
lead_lane: lead.lane_id,
|
|
907
|
+
condition: "missed_deadline",
|
|
908
|
+
story_id: commitment.item,
|
|
909
|
+
at: now.toISOString(),
|
|
910
|
+
fingerprint,
|
|
911
|
+
owner: commitment.owner,
|
|
912
|
+
due: commitment.due,
|
|
913
|
+
miss_count: next.miss_count,
|
|
914
|
+
};
|
|
915
|
+
if (!input.dryRun)
|
|
916
|
+
recordOperatorEscalation(hqRoot, escalation);
|
|
917
|
+
if (!input.dryRun)
|
|
918
|
+
stateChanged = true;
|
|
919
|
+
result.escalated.push(escalation);
|
|
920
|
+
}
|
|
921
|
+
if (!input.dryRun && JSON.stringify(prior) !== JSON.stringify(next))
|
|
922
|
+
stateChanged = true;
|
|
923
|
+
deadlineState[key] = next;
|
|
924
|
+
}
|
|
925
|
+
const current = deadlineState[key] ?? prior;
|
|
926
|
+
result.missed_deadlines.push({ lead_lane: lead.lane_id, item: commitment.item, owner: commitment.owner, due: commitment.due, miss_count: current?.miss_count ?? 1 });
|
|
927
|
+
}
|
|
928
|
+
}
|
|
581
929
|
if (!input.dryRun && stateChanged)
|
|
582
930
|
writeState(stateFile, state);
|
|
583
931
|
result.evaluated += 1;
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
/** hq-flags keys used by lane
|
|
1
|
+
/** hq-flags keys used by lane command behavior. */
|
|
2
2
|
export declare const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
3
3
|
/** Enables one automatic continuation after a missing-envelope repair. */
|
|
4
4
|
export declare const LANE_ENVELOPE_REPAIR_CONTINUE_FLAG = "lanes.envelope-repair-continue";
|
|
5
|
+
/** Enables missed-deadline lead nudges and second-miss operator escalation. */
|
|
6
|
+
export declare const LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = "lanes.autopilot-missed-deadline-escalation";
|
|
7
|
+
/** Enables company-scoped lookup of a hostless lane ULID across Outpost indexes. */
|
|
8
|
+
export declare const LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG = "lanes.message-route-owner-host";
|
|
9
|
+
export declare const LANE_MESSAGE_ROUTE_OWNER_HOST_DEFAULT_ENABLED = false;
|
|
10
|
+
/** Enables stale manager-status KPI reporting and one-time lead nudges. */
|
|
11
|
+
export declare const LEAD_AUTOPILOT_KPI_STALENESS_FLAG = "lanes.autopilot-kpi-staleness";
|
|
12
|
+
export declare const LEAD_AUTOPILOT_KPI_STALENESS_DEFAULT_ENABLED = false;
|
|
5
13
|
//# sourceMappingURL=flag-keys.d.ts.map
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
/** hq-flags keys used by lane
|
|
1
|
+
/** hq-flags keys used by lane command behavior. */
|
|
2
2
|
export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
3
3
|
/** Enables one automatic continuation after a missing-envelope repair. */
|
|
4
4
|
export const LANE_ENVELOPE_REPAIR_CONTINUE_FLAG = "lanes.envelope-repair-continue";
|
|
5
|
+
/** Enables missed-deadline lead nudges and second-miss operator escalation. */
|
|
6
|
+
export const LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = "lanes.autopilot-missed-deadline-escalation";
|
|
7
|
+
/** Enables company-scoped lookup of a hostless lane ULID across Outpost indexes. */
|
|
8
|
+
export const LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG = "lanes.message-route-owner-host";
|
|
9
|
+
export const LANE_MESSAGE_ROUTE_OWNER_HOST_DEFAULT_ENABLED = false;
|
|
10
|
+
/** Enables stale manager-status KPI reporting and one-time lead nudges. */
|
|
11
|
+
export const LEAD_AUTOPILOT_KPI_STALENESS_FLAG = "lanes.autopilot-kpi-staleness";
|
|
12
|
+
export const LEAD_AUTOPILOT_KPI_STALENESS_DEFAULT_ENABLED = false;
|
|
5
13
|
//# sourceMappingURL=flag-keys.js.map
|
|
@@ -7,6 +7,8 @@ export declare const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_DEFAULT_ENABLED = false
|
|
|
7
7
|
export declare const LEAD_LANE_KEEP_ALIVE_IMMEDIATE_FIRST_RESUME_FLAG = "lanes.keep-alive-immediate-first-resume";
|
|
8
8
|
export declare const LEAD_LANE_KEEP_ALIVE_IMMEDIATE_FIRST_RESUME_DEFAULT_ENABLED = false;
|
|
9
9
|
export declare const LEAD_LANE_KEEP_ALIVE_FIRST_RESUME_SETTLE_MS = 30000;
|
|
10
|
+
export declare const LEAD_LANE_KEEP_ALIVE_TERMINAL_TRIGGER_FLAG = "lanes.keep-alive-terminal-trigger";
|
|
11
|
+
export declare const LEAD_LANE_KEEP_ALIVE_TERMINAL_TRIGGER_DEFAULT_ENABLED = false;
|
|
10
12
|
export declare const LEAD_LANE_KEEP_ALIVE_BLOCKED_GRACE_MS: number;
|
|
11
13
|
/** Missing or unreadable registry values retain the legacy supersession behavior. */
|
|
12
14
|
export declare const LANE_SUPERSESSION_LINEAGE_AWARE_FLAG = "lanes.supersession-lineage-aware";
|
|
@@ -12,6 +12,8 @@ export const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_DEFAULT_ENABLED = false;
|
|
|
12
12
|
export const LEAD_LANE_KEEP_ALIVE_IMMEDIATE_FIRST_RESUME_FLAG = "lanes.keep-alive-immediate-first-resume";
|
|
13
13
|
export const LEAD_LANE_KEEP_ALIVE_IMMEDIATE_FIRST_RESUME_DEFAULT_ENABLED = false;
|
|
14
14
|
export const LEAD_LANE_KEEP_ALIVE_FIRST_RESUME_SETTLE_MS = 30_000;
|
|
15
|
+
export const LEAD_LANE_KEEP_ALIVE_TERMINAL_TRIGGER_FLAG = "lanes.keep-alive-terminal-trigger";
|
|
16
|
+
export const LEAD_LANE_KEEP_ALIVE_TERMINAL_TRIGGER_DEFAULT_ENABLED = false;
|
|
15
17
|
export const LEAD_LANE_KEEP_ALIVE_BLOCKED_GRACE_MS = 15 * 60 * 1000;
|
|
16
18
|
/** Missing or unreadable registry values retain the legacy supersession behavior. */
|
|
17
19
|
export const LANE_SUPERSESSION_LINEAGE_AWARE_FLAG = "lanes.supersession-lineage-aware";
|
|
@@ -95,6 +95,11 @@ export interface ReconcileDeps {
|
|
|
95
95
|
mesh?: LaneMesh | null;
|
|
96
96
|
/** Bound on one terminal mesh emit during dead-host recovery. Default 10 s. */
|
|
97
97
|
meshEmitTimeoutMs?: number;
|
|
98
|
+
/** Restrict keep-alive execution to the terminal-triggered lane version. */
|
|
99
|
+
terminalKeepAliveCandidate?: {
|
|
100
|
+
laneId: string;
|
|
101
|
+
version: number;
|
|
102
|
+
};
|
|
98
103
|
/** Fresh keep-alive eligibility read; injected to exercise opt-in changes before queueing. */
|
|
99
104
|
readKeepAliveRecheckLane?: (hqRoot: string, laneId: string) => LaneRecord;
|
|
100
105
|
/** Reuse the command's Claude resume selection and session-move path for keep-alive. */
|
|
@@ -895,6 +895,9 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
895
895
|
markError(summary, err);
|
|
896
896
|
return summary;
|
|
897
897
|
}
|
|
898
|
+
if (deps.terminalKeepAliveCandidate) {
|
|
899
|
+
return runTerminalKeepAliveCandidatePass(hqRoot, lanes, summary, deps, now, deps.terminalKeepAliveCandidate);
|
|
900
|
+
}
|
|
898
901
|
let presenceConnected;
|
|
899
902
|
try {
|
|
900
903
|
presenceConnected =
|
|
@@ -1159,7 +1162,7 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
1159
1162
|
const freshLanes = loadLaneRecords(hqRoot, summary);
|
|
1160
1163
|
const freshQuestions = loadQuestionRecords(hqRoot, summary);
|
|
1161
1164
|
await closeInterruptedDoneStoryWorkers(hqRoot, freshLanes, freshQuestions, summary, admissionDeps, now, storyCloseEnabledByCompany);
|
|
1162
|
-
await resumeKeepAlive(hqRoot, freshLanes, freshQuestions, summary, admissionDeps, now, keepAliveFlagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany, immediateFirstResumeFlagByCompany);
|
|
1165
|
+
await resumeKeepAlive(hqRoot, freshLanes, freshQuestions, summary, admissionDeps, now, keepAliveFlagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany, immediateFirstResumeFlagByCompany, deps.terminalKeepAliveCandidate);
|
|
1163
1166
|
const drainLanes = loadLaneRecords(hqRoot, summary);
|
|
1164
1167
|
await admitQueued(hqRoot, drainLanes, summary, admissionDeps, now);
|
|
1165
1168
|
await resumeAwaiting(hqRoot, drainLanes, freshQuestions, summary, admissionDeps);
|
|
@@ -1635,13 +1638,15 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
|
|
|
1635
1638
|
}
|
|
1636
1639
|
}
|
|
1637
1640
|
}
|
|
1638
|
-
async function admitQueued(hqRoot, lanes, summary, deps, now) {
|
|
1639
|
-
|
|
1641
|
+
async function admitQueued(hqRoot, lanes, summary, deps, now, onlyLaneIds) {
|
|
1642
|
+
if (onlyLaneIds === undefined)
|
|
1643
|
+
expireAbandonedPickupValidation(hqRoot, lanes, summary, deps, now);
|
|
1640
1644
|
// A lane whose pickup probe is still out is not admitted: its record is
|
|
1641
1645
|
// about to be bound or failed by the pickup, and a worker spawned now
|
|
1642
1646
|
// would keep running behind that write.
|
|
1643
1647
|
const localQueued = lanes
|
|
1644
1648
|
.filter((lane) => lane.state === "queued" &&
|
|
1649
|
+
(onlyLaneIds === undefined || onlyLaneIds.has(lane.lane_id)) &&
|
|
1645
1650
|
lane.pickup_validation_pending !== true &&
|
|
1646
1651
|
laneRunsHere(lane, deps.host))
|
|
1647
1652
|
.filter((lane) => !hasFreshAdmissionReservation(lane, now.getTime()))
|
|
@@ -2238,7 +2243,7 @@ function appendBlockedKeepAliveHistory(hqRoot, lane, terminalAt, at, reason, dep
|
|
|
2238
2243
|
}),
|
|
2239
2244
|
}), false);
|
|
2240
2245
|
}
|
|
2241
|
-
async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany, immediateFirstResumeFlagByCompany) {
|
|
2246
|
+
async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany, immediateFirstResumeFlagByCompany, terminalKeepAliveCandidate) {
|
|
2242
2247
|
const nowMs = now.getTime();
|
|
2243
2248
|
const nowIso = now.toISOString();
|
|
2244
2249
|
const config = loadLanesConfig(hqRoot);
|
|
@@ -2262,6 +2267,10 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2262
2267
|
}
|
|
2263
2268
|
}
|
|
2264
2269
|
for (const originalLane of lanes) {
|
|
2270
|
+
if (terminalKeepAliveCandidate && (originalLane.lane_id !== terminalKeepAliveCandidate.laneId ||
|
|
2271
|
+
originalLane.version !== terminalKeepAliveCandidate.version ||
|
|
2272
|
+
originalLane.state !== "done"))
|
|
2273
|
+
continue;
|
|
2265
2274
|
if (!laneRunsHere(originalLane, deps.host))
|
|
2266
2275
|
continue;
|
|
2267
2276
|
if (originalLane.state === "killed" ||
|
|
@@ -3082,6 +3091,50 @@ async function resumeAwaiting(hqRoot, lanes, questions, summary, deps, onlyLaneI
|
|
|
3082
3091
|
export async function runReconcilePass(hqRoot, deps) {
|
|
3083
3092
|
return withReconcileLock(hqRoot, async () => runPassBody(hqRoot, deps));
|
|
3084
3093
|
}
|
|
3094
|
+
async function runTerminalKeepAliveCandidatePass(hqRoot, lanes, summary, deps, now, candidate) {
|
|
3095
|
+
const lane = lanes.find((row) => row.lane_id === candidate.laneId);
|
|
3096
|
+
if (!lane || lane.version !== candidate.version || lane.state !== "done" ||
|
|
3097
|
+
lane.keep_alive !== true || !laneRunsHere(lane, deps.host))
|
|
3098
|
+
return summary;
|
|
3099
|
+
let company;
|
|
3100
|
+
try {
|
|
3101
|
+
company = resolveLaneCompany(hqRoot, lane).company;
|
|
3102
|
+
}
|
|
3103
|
+
catch (err) {
|
|
3104
|
+
markDirty(summary, { kind: "lane", id: lane.lane_id, why: `keep-alive company lookup failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
3105
|
+
return summary;
|
|
3106
|
+
}
|
|
3107
|
+
if (company === null)
|
|
3108
|
+
return summary;
|
|
3109
|
+
const readFlag = deps.readRegistryFlag ?? readLanesRegistryFlag;
|
|
3110
|
+
const options = { company, env: deps.env ?? process.env };
|
|
3111
|
+
let keepAliveEnabled;
|
|
3112
|
+
let lineageAware;
|
|
3113
|
+
let immediateFirstResumeEnabled;
|
|
3114
|
+
try {
|
|
3115
|
+
[keepAliveEnabled, lineageAware, immediateFirstResumeEnabled] = await Promise.all([
|
|
3116
|
+
readFlag(LEAD_LANE_KEEP_ALIVE_FLAG, options),
|
|
3117
|
+
readFlag(LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, options),
|
|
3118
|
+
readFlag(LEAD_LANE_KEEP_ALIVE_IMMEDIATE_FIRST_RESUME_FLAG, options),
|
|
3119
|
+
]);
|
|
3120
|
+
}
|
|
3121
|
+
catch (err) {
|
|
3122
|
+
markDirty(summary, { kind: "lane", id: lane.lane_id, why: `keep-alive flag read failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
3123
|
+
return summary;
|
|
3124
|
+
}
|
|
3125
|
+
if (!keepAliveEnabled)
|
|
3126
|
+
return summary;
|
|
3127
|
+
await withAllLaneAdmissionLocks(hqRoot, async () => {
|
|
3128
|
+
const freshLanes = loadLaneRecords(hqRoot, summary);
|
|
3129
|
+
const freshQuestions = loadQuestionRecords(hqRoot, summary);
|
|
3130
|
+
const current = freshLanes.find((row) => row.lane_id === candidate.laneId);
|
|
3131
|
+
if (!current || current.version !== candidate.version || current.state !== "done")
|
|
3132
|
+
return;
|
|
3133
|
+
await resumeKeepAlive(hqRoot, freshLanes, freshQuestions, summary, deps, now, new Map([[keepAliveCompanyKey(company), true]]), new Map([[keepAliveCompanyKey(company), lineageAware]]), new Map(), new Map([[keepAliveCompanyKey(company), immediateFirstResumeEnabled]]), candidate);
|
|
3134
|
+
await admitQueued(hqRoot, loadLaneRecords(hqRoot, summary), summary, deps, now, new Set([candidate.laneId]));
|
|
3135
|
+
});
|
|
3136
|
+
return summary;
|
|
3137
|
+
}
|
|
3085
3138
|
/** Run the reconcile answer-resume path for one lane before questions answer returns. */
|
|
3086
3139
|
export async function wakeAnsweredLaneNow(hqRoot, laneId, deps) {
|
|
3087
3140
|
const summary = emptySummary(deps.dryRun);
|