@indigoai-us/hq-cli 5.264.0 → 5.265.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 CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.265.0] — 2026-09-28
6
+
7
+ ### Added
8
+
9
+ - The `lanes.kr-freshness` flag tracks each manager KPI in a runtime-owned
10
+ ledger and queues one corrective resume for each stalled KPI set. The
11
+ freshness window defaults to six hours and can be set with
12
+ `kr_freshness_hours` in the lanes config.
13
+
14
+ ### Fixed
15
+
16
+ - Reconcile resolves a missing lane company for keep-alive and supersession
17
+ flag reads, and watch applies the same resolution to keep-alive checks.
18
+ Unusable project ownership remains unresolved; reconcile reports
19
+ `keep_alive_company_unresolved` or `keep_alive_flag_off` instead of silently
20
+ skipping those candidates.
21
+
5
22
  ## [5.264.0] — 2026-09-28
6
23
 
7
24
  ### Removed
@@ -7537,6 +7537,22 @@ export declare const COMMAND_CATALOG: readonly [{
7537
7537
  }];
7538
7538
  readonly subcommands: readonly [];
7539
7539
  }];
7540
+ }, {
7541
+ readonly name: "kpis";
7542
+ readonly description: "Show runtime freshness for a manager lane's key results";
7543
+ readonly aliases: readonly [];
7544
+ readonly hidden: false;
7545
+ readonly usage: "[options] <lane>";
7546
+ readonly arguments: readonly [{
7547
+ readonly name: "lane";
7548
+ readonly required: true;
7549
+ readonly variadic: false;
7550
+ }];
7551
+ readonly options: readonly [{
7552
+ readonly flags: "--json";
7553
+ readonly description: "Print the KR freshness report as JSON";
7554
+ }];
7555
+ readonly subcommands: readonly [];
7540
7556
  }, {
7541
7557
  readonly name: "create";
7542
7558
  readonly description: "Register a lane, run admission, and return immediately";
@@ -9742,6 +9742,27 @@ export const COMMAND_CATALOG = [
9742
9742
  }
9743
9743
  ]
9744
9744
  },
9745
+ {
9746
+ "name": "kpis",
9747
+ "description": "Show runtime freshness for a manager lane's key results",
9748
+ "aliases": [],
9749
+ "hidden": false,
9750
+ "usage": "[options] <lane>",
9751
+ "arguments": [
9752
+ {
9753
+ "name": "lane",
9754
+ "required": true,
9755
+ "variadic": false
9756
+ }
9757
+ ],
9758
+ "options": [
9759
+ {
9760
+ "flags": "--json",
9761
+ "description": "Print the KR freshness report as JSON"
9762
+ }
9763
+ ],
9764
+ "subcommands": []
9765
+ },
9745
9766
  {
9746
9767
  "name": "create",
9747
9768
  "description": "Register a lane, run admission, and return immediately",
@@ -0,0 +1,11 @@
1
+ import { Command } from "commander";
2
+ export interface LaneKpisCommandDeps {
3
+ hqRoot: () => string;
4
+ now: () => Date;
5
+ wrap: WrapHandler;
6
+ }
7
+ type WrapHandler = <Args extends unknown[]>(action: (...args: Args) => Promise<void>) => (...args: Args) => Promise<void>;
8
+ /** Register the operator report command without depending on the large lanes registrar. */
9
+ export declare function registerLaneKpisCommand(parent: Command, deps: LaneKpisCommandDeps): void;
10
+ export {};
11
+ //# sourceMappingURL=lanes-kpis.d.ts.map
@@ -0,0 +1,34 @@
1
+ import { readKrFreshnessReport } from "../lib/lanes/kr-freshness.js";
2
+ /** Register the operator report command without depending on the large lanes registrar. */
3
+ export function registerLaneKpisCommand(parent, deps) {
4
+ parent
5
+ .command("kpis")
6
+ .description("Show runtime freshness for a manager lane's key results")
7
+ .argument("<lane>")
8
+ .option("--json", "Print the KR freshness report as JSON")
9
+ .action(deps.wrap(async (laneId, options) => {
10
+ const report = readKrFreshnessReport(deps.hqRoot(), laneId, deps.now().getTime());
11
+ const result = {
12
+ ok: true,
13
+ action: "kpis",
14
+ lane_id: laneId,
15
+ ledger_available: report.available,
16
+ ledger_path: report.ledger_path,
17
+ kr_freshness_hours: report.kr_freshness_hours,
18
+ kpis: report.kpis,
19
+ };
20
+ if (options.json) {
21
+ console.log(JSON.stringify(result));
22
+ return;
23
+ }
24
+ if (!report.available) {
25
+ console.log(`No KR freshness ledger exists for ${laneId}. Expected ${report.ledger_path}.`);
26
+ return;
27
+ }
28
+ console.log(`KR freshness for ${laneId} (window ${report.kr_freshness_hours}h):`);
29
+ for (const kpi of report.kpis) {
30
+ console.log(`${kpi.name}: ${JSON.stringify(kpi.value)} — ${kpi.verdict}; measured ${kpi.measured_at}; value changed ${kpi.value_changed_at}`);
31
+ }
32
+ }));
33
+ }
34
+ //# sourceMappingURL=lanes-kpis.js.map
@@ -1,5 +1,6 @@
1
1
  import { accountStatus, coveredLaneIds, loadLanesConfig, MAX_WATCHED_LANES, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, monitorReminderText, runLaneWatch, } from "../lib/lanes/index.js";
2
2
  import { LEAD_LANE_KEEP_ALIVE_FLAG } from "../lib/lanes/keep-alive.js";
3
+ import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
3
4
  import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
4
5
  import { isLanesError, LanesError } from "../lib/lanes/errors.js";
5
6
  import { LANE_WAIT_INVALID_INPUT_EXIT_CODE, LANE_WAIT_TIMEOUT_EXIT_CODE, runLaneWait, } from "../lib/lanes/wait.js";
@@ -46,7 +47,7 @@ export function createLaneWatchHandlers(deps) {
46
47
  intervalMs: rawInterval,
47
48
  sleep: ctx.sleep,
48
49
  keepAliveEnabled: (lane) => readLanesRegistryFlag(LEAD_LANE_KEEP_ALIVE_FLAG, {
49
- company: lane.company,
50
+ company: resolveLaneCompany(hqRoot, lane).company ?? undefined,
50
51
  env: ctx.env,
51
52
  }),
52
53
  readAccountStatuses: async (lanes) => {
@@ -18,6 +18,7 @@ import { createLaneResumeHandlers } from "./lanes-resume.js";
18
18
  import { createLaneWatchHandlers } from "./lanes-watch.js";
19
19
  export { resolveAnswerChoice } from "./lanes-questions.js";
20
20
  import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
21
+ import { registerLaneKpisCommand } from "./lanes-kpis.js";
21
22
  import { registerLaneChecksCommand } from "./lanes-checks.js";
22
23
  import { registerLaneReportCommands } from "./lanes-report.js";
23
24
  import { registerLaneTrackerImportCommand } from "./lanes-import.js";
@@ -50,6 +51,7 @@ import { effectiveAccountHome } from "../lib/lanes/account-home.js";
50
51
  import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
51
52
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
52
53
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
54
+ import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
53
55
  import { doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveExclusion, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
54
56
  import { questionBodyAbsPath, resolveQuestionCompanyWithFallback, resolveProjectQuestionCompany, } from "../lib/lanes/questions.js";
55
57
  import { assertQuestionBodySecretSafe } from "../lib/lanes/questions.js";
@@ -60,6 +62,7 @@ import { loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
60
62
  import { SESSION_MOVE_REASON } from "../lib/lanes/move-account.js";
61
63
  import { managerStatusGateEnabledForLane, resolveManagerStatusGateForLane, } from "../lib/lanes/manager-status-gate.js";
62
64
  import { blockerFreshnessFingerprint, resolveBlockerFreshnessForLane, } from "../lib/lanes/blocker-freshness.js";
65
+ import { resolveKrFreshnessForShow } from "../lib/lanes/kr-freshness.js";
63
66
  import { ensureSessionHome } from "../lib/lanes/session-relink.js";
64
67
  import { defaultProbe, validateProfileLaneModel, validateLaneModel, } from "../lib/lanes/models.js";
65
68
  import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, spawnLaneWorker, spawnEnvelopeRepairRequest, laneRunDir, readSpawnManifest, stopLaneProcesses, } from "../lib/lanes/spawn.js";
@@ -4524,6 +4527,7 @@ async function runShow(ctx, laneId) {
4524
4527
  : storedLane;
4525
4528
  const askDeliveryFailed = readJsonFile(askDeliveryFailedPath(hqRoot, laneId));
4526
4529
  const keepAliveState = resolveLaneKeepAlive(hqRoot, storedLane);
4530
+ const keepAliveCompany = resolveLaneCompany(hqRoot, storedLane);
4527
4531
  const doneStateVerdict = storedLane.state === "done" && storedLane.keep_alive === true
4528
4532
  ? doneKeepAliveVerdict(storedLane, { decision: storedLane.last_envelope?.decision, story_complete: storedLane.story_complete }, storedLane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, storedLane))
4529
4533
  : null;
@@ -4533,6 +4537,10 @@ async function runShow(ctx, laneId) {
4533
4537
  env: ctx.env,
4534
4538
  now: ctx.now().getTime(),
4535
4539
  });
4540
+ const krFreshness = await resolveKrFreshnessForShow(hqRoot, storedLane, {
4541
+ env: ctx.env,
4542
+ now: ctx.now().getTime(),
4543
+ });
4536
4544
  printJson({
4537
4545
  ok: true,
4538
4546
  action: "show",
@@ -4562,6 +4570,8 @@ async function runShow(ctx, laneId) {
4562
4570
  keep_alive_state: {
4563
4571
  enabled: keepAliveState.enabled,
4564
4572
  source: keepAliveState.source,
4573
+ company_source: keepAliveCompany.source,
4574
+ company_slug: keepAliveCompany.company?.slug ?? null,
4565
4575
  standing: storedLane.keep_alive_standing === true,
4566
4576
  done_state_verdict: doneStateVerdict,
4567
4577
  opted_in_at: storedLane.keep_alive_opted_in_at ?? null,
@@ -4597,6 +4607,7 @@ async function runShow(ctx, laneId) {
4597
4607
  blockerFreshnessFingerprint(blockerFreshness.stale),
4598
4608
  }
4599
4609
  : null,
4610
+ kr_freshness: krFreshness,
4600
4611
  last_message_at: lane.last_message_at ?? null,
4601
4612
  last_compaction: lane.last_compaction
4602
4613
  ? { ...lane.last_compaction, after: compactionAfterUsage(storedLane) }
@@ -6918,6 +6929,7 @@ export function registerLanesCommand(parent, deps = {}) {
6918
6929
  env: ctx.env,
6919
6930
  }, wrap);
6920
6931
  registerOutpostPrunerCommand(lanes);
6932
+ registerLaneKpisCommand(lanes, { hqRoot: ctx.hqRoot, now: ctx.now, wrap });
6921
6933
  const noStoryOption = new Option("--no-story <reason>", "Create a lane without a story and record why");
6922
6934
  noStoryOption.attributeName = () => "noStory";
6923
6935
  lanes
@@ -41,6 +41,8 @@ export declare const DEFAULT_SILENT_LOST_MS: number;
41
41
  export declare const DEFAULT_SESSION_QUESTION_TIMEOUT_SECS: number;
42
42
  /** Open manager blockers are re-verified every four hours by default. */
43
43
  export declare const DEFAULT_BLOCKER_FRESHNESS_HOURS = 4;
44
+ /** Manager key results must be re-measured within six hours by default. */
45
+ export declare const DEFAULT_KR_FRESHNESS_HOURS = 6;
44
46
  export declare const DEFAULT_TRACKER_CHECKS: TrackerChecksConfig;
45
47
  export declare const DEFAULT_LIVENESS_WINDOWS: LivenessWindows;
46
48
  export declare const DEFAULT_PROVIDER_MAX_RUNNING: Record<LaneProvider, number>;
@@ -45,6 +45,8 @@ export const DEFAULT_SILENT_LOST_MS = 12 * 60 * 60 * 1000;
45
45
  export const DEFAULT_SESSION_QUESTION_TIMEOUT_SECS = 10 * 60;
46
46
  /** Open manager blockers are re-verified every four hours by default. */
47
47
  export const DEFAULT_BLOCKER_FRESHNESS_HOURS = 4;
48
+ /** Manager key results must be re-measured within six hours by default. */
49
+ export const DEFAULT_KR_FRESHNESS_HOURS = 6;
48
50
  export const DEFAULT_TRACKER_CHECKS = {
49
51
  owner_question_surface_ms: 10 * 60 * 1000,
50
52
  open_question_age_ms: 2 * 60 * 60 * 1000,
@@ -425,6 +427,7 @@ export function documentedDefaultConfig(deps = defaultDeps()) {
425
427
  tracker: { checks: { ...DEFAULT_TRACKER_CHECKS } },
426
428
  session_question_timeout_secs: DEFAULT_SESSION_QUESTION_TIMEOUT_SECS,
427
429
  blocker_freshness_hours: DEFAULT_BLOCKER_FRESHNESS_HOURS,
430
+ kr_freshness_hours: DEFAULT_KR_FRESHNESS_HOURS,
428
431
  };
429
432
  }
430
433
  export function parseLanesConfig(raw, deps = defaultDeps()) {
@@ -468,6 +471,7 @@ export function parseLanesConfig(raw, deps = defaultDeps()) {
468
471
  tracker: { checks: parseTrackerChecks(raw.tracker) },
469
472
  session_question_timeout_secs: positiveInt(raw.session_question_timeout_secs, "session_question_timeout_secs", DEFAULT_SESSION_QUESTION_TIMEOUT_SECS),
470
473
  blocker_freshness_hours: positiveNumber(raw.blocker_freshness_hours, "blocker_freshness_hours", DEFAULT_BLOCKER_FRESHNESS_HOURS),
474
+ kr_freshness_hours: positiveNumber(raw.kr_freshness_hours, "kr_freshness_hours", DEFAULT_KR_FRESHNESS_HOURS),
471
475
  };
472
476
  }
473
477
  export function lanesConfigPath(hqRoot) {
@@ -40,7 +40,7 @@ export declare function retiredLaneRegistryPath(hqRoot: string): string;
40
40
  export declare function loadRetiredLaneIds(hqRoot: string): ReadonlySet<string>;
41
41
  /** IDs may be stored bare or with an owner-host suffix after an underscore. */
42
42
  export declare function isRetiredLaneId(laneId: string, retiredLaneIds: ReadonlySet<string>): boolean;
43
- export declare function keepAliveSupersededBy(lane: LaneRecord, lanes: readonly LaneRecord[], lineageAware?: boolean): LaneRecord | undefined;
44
- export declare function keepAliveExclusion(lane: LaneRecord, lanes: readonly LaneRecord[], retiredLaneIds: ReadonlySet<string>, lineageAware?: boolean): "retired" | "superseded" | undefined;
43
+ export declare function keepAliveSupersededBy(lane: LaneRecord, lanes: readonly LaneRecord[], lineageAware?: boolean, companyForLane?: (lane: LaneRecord) => LaneRecord["company"]): LaneRecord | undefined;
44
+ export declare function keepAliveExclusion(lane: LaneRecord, lanes: readonly LaneRecord[], retiredLaneIds: ReadonlySet<string>, lineageAware?: boolean, companyForLane?: (lane: LaneRecord) => LaneRecord["company"]): "retired" | "superseded" | undefined;
45
45
  export declare function leadLaneKeepAliveBackoffMs(attempt: number): number;
46
46
  //# sourceMappingURL=keep-alive.d.ts.map
@@ -199,10 +199,12 @@ export function isRetiredLaneId(laneId, retiredLaneIds) {
199
199
  }
200
200
  return false;
201
201
  }
202
- export function keepAliveSupersededBy(lane, lanes, lineageAware = LANE_SUPERSESSION_LINEAGE_AWARE_DEFAULT_ENABLED) {
202
+ export function keepAliveSupersededBy(lane, lanes, lineageAware = LANE_SUPERSESSION_LINEAGE_AWARE_DEFAULT_ENABLED, companyForLane) {
203
203
  const createdAt = Date.parse(lane.created_at);
204
204
  if (!Number.isFinite(createdAt))
205
205
  return undefined;
206
+ const companyOf = companyForLane ?? ((candidate) => candidate.company);
207
+ const laneCompany = companyOf(lane);
206
208
  const lanesById = new Map(lanes.map((candidate) => [candidate.lane_id, candidate]));
207
209
  const targetAncestorIds = new Set();
208
210
  if (lineageAware) {
@@ -218,9 +220,10 @@ export function keepAliveSupersededBy(lane, lanes, lineageAware = LANE_SUPERSESS
218
220
  return lanes.find((candidate) => {
219
221
  if (candidate.lane_id === lane.lane_id || Date.parse(candidate.created_at) <= createdAt)
220
222
  return false;
221
- const sameCompany = lane.company === null
222
- ? candidate.company === null
223
- : candidate.company?.uid === lane.company.uid;
223
+ const candidateCompany = companyOf(candidate);
224
+ const sameCompany = laneCompany === null
225
+ ? candidateCompany === null
226
+ : candidateCompany?.uid === laneCompany.uid;
224
227
  if (!sameCompany)
225
228
  return false;
226
229
  if (lineageAware && targetAncestorIds.has(candidate.lane_id))
@@ -249,10 +252,10 @@ export function keepAliveSupersededBy(lane, lanes, lineageAware = LANE_SUPERSESS
249
252
  return sameProfile || projectLead;
250
253
  });
251
254
  }
252
- export function keepAliveExclusion(lane, lanes, retiredLaneIds, lineageAware = LANE_SUPERSESSION_LINEAGE_AWARE_DEFAULT_ENABLED) {
255
+ export function keepAliveExclusion(lane, lanes, retiredLaneIds, lineageAware = LANE_SUPERSESSION_LINEAGE_AWARE_DEFAULT_ENABLED, companyForLane) {
253
256
  if (isRetiredLaneId(lane.lane_id, retiredLaneIds))
254
257
  return "retired";
255
- return keepAliveSupersededBy(lane, lanes, lineageAware) === undefined ? undefined : "superseded";
258
+ return keepAliveSupersededBy(lane, lanes, lineageAware, companyForLane) === undefined ? undefined : "superseded";
256
259
  }
257
260
  export function leadLaneKeepAliveBackoffMs(attempt) {
258
261
  if (!Number.isSafeInteger(attempt) || attempt < 1) {
@@ -0,0 +1,76 @@
1
+ import { type ManagerStatusTarget } from "./manager-status-gate.js";
2
+ import { readLanesRegistryFlag } from "./registry-flag.js";
3
+ import type { LaneRecord } from "./types.js";
4
+ export declare const KR_FRESHNESS_FLAG = "lanes.kr-freshness";
5
+ export declare const KR_FRESHNESS_DEFAULT_HOURS = 6;
6
+ export declare const KR_FRESHNESS_LEDGER_REL: string;
7
+ export declare const KR_FRESHNESS_LEDGER_SCHEMA = "kr-freshness-ledger.v1";
8
+ export declare const KR_FRESHNESS_HISTORY_LIMIT = 50;
9
+ export type KrFreshnessVerdict = "moving" | "flat" | "stalled";
10
+ export interface KrFreshnessKpi {
11
+ name: string;
12
+ value: number | string;
13
+ entry_measured_at: string | null;
14
+ first_seen_at: string;
15
+ measured_at: string;
16
+ value_changed_at: string;
17
+ removed_at?: string;
18
+ }
19
+ export interface KrFreshnessLedger {
20
+ schema_version: typeof KR_FRESHNESS_LEDGER_SCHEMA;
21
+ lane_id: string;
22
+ updated_at: string;
23
+ kpis: Record<string, KrFreshnessKpi>;
24
+ }
25
+ export interface KrFreshnessView {
26
+ name: string;
27
+ value: number | string;
28
+ verdict: KrFreshnessVerdict;
29
+ measured_at: string;
30
+ value_changed_at: string;
31
+ }
32
+ export interface KrFreshnessResolution {
33
+ flagEnabled: boolean;
34
+ enabled: boolean;
35
+ target?: ManagerStatusTarget;
36
+ statusPath?: string;
37
+ windowHours: number;
38
+ kpis: KrFreshnessView[];
39
+ stalledKeys: string[];
40
+ error?: string;
41
+ }
42
+ export interface KrFreshnessResolutionOptions {
43
+ env?: NodeJS.ProcessEnv;
44
+ now?: number;
45
+ readFlag?: typeof readLanesRegistryFlag;
46
+ onError?: (error: unknown) => void;
47
+ }
48
+ export interface KrFreshnessStatusKpi {
49
+ key: string;
50
+ fullName: string;
51
+ name: string;
52
+ value: number | string;
53
+ entryMeasuredAt: string | null;
54
+ }
55
+ export declare function readKrFreshnessStatus(statusPath: string): KrFreshnessStatusKpi[];
56
+ export declare function krFreshnessLedgerPath(root: string, laneId: string): string;
57
+ export declare function readKrFreshnessLedger(root: string, laneId: string): KrFreshnessLedger | null;
58
+ export declare function writeKrFreshnessLedger(root: string, ledger: KrFreshnessLedger): void;
59
+ export declare function updateKrFreshnessLedger(laneId: string, prior: KrFreshnessLedger | null, statusKpis: readonly KrFreshnessStatusKpi[], now: number): KrFreshnessLedger;
60
+ export declare function krFreshnessVerdict(kpi: Pick<KrFreshnessKpi, "measured_at" | "value_changed_at">, now: number, windowHours: number): KrFreshnessVerdict;
61
+ export declare function krFreshnessFingerprint(keys: readonly string[]): string;
62
+ /** Read the flag before manager mapping, status, configuration or ledger files. */
63
+ export declare function resolveKrFreshnessForLane(root: string, lane: LaneRecord, options?: KrFreshnessResolutionOptions): Promise<KrFreshnessResolution>;
64
+ /** Read a previously written ledger for `lanes show`; this never reads status. */
65
+ export declare function resolveKrFreshnessForShow(root: string, lane: LaneRecord, options?: KrFreshnessResolutionOptions): Promise<{
66
+ enabled: true;
67
+ kpis: KrFreshnessView[];
68
+ } | null>;
69
+ /** Operator report path: the ledger remains readable while runtime enforcement is off. */
70
+ export declare function readKrFreshnessReport(root: string, laneId: string, now: number): {
71
+ ledger_path: string;
72
+ available: boolean;
73
+ kr_freshness_hours: number;
74
+ kpis: KrFreshnessView[];
75
+ };
76
+ //# sourceMappingURL=kr-freshness.d.ts.map
@@ -0,0 +1,412 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { DEFAULT_KR_FRESHNESS_HOURS, loadLanesConfig } from "./config.js";
5
+ import { LanesError } from "./errors.js";
6
+ import { resolveManagerStatusTarget } from "./manager-status-gate.js";
7
+ import { readLanesRegistryFlag } from "./registry-flag.js";
8
+ import { assertNoSecretText } from "./text-scan.js";
9
+ export const KR_FRESHNESS_FLAG = "lanes.kr-freshness";
10
+ export const KR_FRESHNESS_DEFAULT_HOURS = DEFAULT_KR_FRESHNESS_HOURS;
11
+ export const KR_FRESHNESS_LEDGER_REL = path.join("workspace", "lanes", "kpi-ledger");
12
+ export const KR_FRESHNESS_LEDGER_SCHEMA = "kr-freshness-ledger.v1";
13
+ export const KR_FRESHNESS_HISTORY_LIMIT = 50;
14
+ const MAX_STATUS_BYTES = 1024 * 1024;
15
+ const MAX_LEDGER_BYTES = 1024 * 1024;
16
+ const MAX_KPIS = 1000;
17
+ const REMOVED_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
18
+ function isObject(value) {
19
+ return value !== null && typeof value === "object" && !Array.isArray(value);
20
+ }
21
+ function fail(code, message, details = {}) {
22
+ throw new LanesError(code, message, details);
23
+ }
24
+ function errorCode(error) {
25
+ return error instanceof LanesError ? error.error : "kr_freshness_unavailable";
26
+ }
27
+ function sha256(value) {
28
+ return createHash("sha256").update(value, "utf8").digest("hex");
29
+ }
30
+ function displayName(name) {
31
+ const chars = Array.from(name);
32
+ return chars.length <= 120 ? name : `${chars.slice(0, 119).join("")}…`;
33
+ }
34
+ function safeLaneId(laneId) {
35
+ return /^[A-Za-z0-9._-]{1,200}$/.test(laneId) && laneId !== "." && laneId !== "..";
36
+ }
37
+ function ensureLedgerDirectory(root, create) {
38
+ let current = path.resolve(root);
39
+ const parts = [current];
40
+ for (const segment of ["workspace", "lanes", "kpi-ledger"]) {
41
+ current = path.join(current, segment);
42
+ parts.push(current);
43
+ }
44
+ for (const [index, directory] of parts.entries()) {
45
+ if (create && index > 0) {
46
+ try {
47
+ fs.mkdirSync(directory, { mode: 0o700 });
48
+ }
49
+ catch (error) {
50
+ if (error.code !== "EEXIST") {
51
+ fail("kr_freshness_ledger_write_failed", `Cannot create KR freshness ledger directory: ${directory}`, { path: directory });
52
+ }
53
+ }
54
+ }
55
+ let stat;
56
+ try {
57
+ stat = fs.lstatSync(directory);
58
+ }
59
+ catch (error) {
60
+ if (!create && error.code === "ENOENT")
61
+ return false;
62
+ fail("kr_freshness_ledger_unavailable", `Cannot inspect KR freshness ledger directory: ${directory}`, { path: directory });
63
+ }
64
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
65
+ fail("kr_freshness_ledger_invalid", `KR freshness ledger path must not be a symlink: ${directory}`, { path: directory });
66
+ }
67
+ }
68
+ return true;
69
+ }
70
+ function ledgerFilePath(root, laneId) {
71
+ if (!safeLaneId(laneId))
72
+ fail("kr_freshness_lane_id_invalid", "Lane id cannot be used as a KR freshness ledger filename");
73
+ return path.join(root, KR_FRESHNESS_LEDGER_REL, `${laneId}.json`);
74
+ }
75
+ function validDate(value) {
76
+ return typeof value === "string" && Number.isFinite(Date.parse(value));
77
+ }
78
+ function validateLedger(value, laneId, filePath) {
79
+ if (!isObject(value) || value.schema_version !== KR_FRESHNESS_LEDGER_SCHEMA || value.lane_id !== laneId ||
80
+ !validDate(value.updated_at) || !isObject(value.kpis)) {
81
+ fail("kr_freshness_ledger_invalid", `KR freshness ledger has an invalid shape: ${filePath}`, { path: filePath });
82
+ }
83
+ const keys = Object.keys(value.kpis);
84
+ if (keys.length > MAX_KPIS)
85
+ fail("kr_freshness_ledger_invalid", `KR freshness ledger exceeds ${MAX_KPIS} entries`, { path: filePath });
86
+ const kpis = {};
87
+ for (const key of keys) {
88
+ const row = value.kpis[key];
89
+ if (!/^[a-f0-9]{64}$/.test(key) || !isObject(row) || typeof row.name !== "string" || row.name.trim() === "" ||
90
+ !(typeof row.value === "string" || (typeof row.value === "number" && Number.isFinite(row.value))) ||
91
+ !(row.entry_measured_at === null || typeof row.entry_measured_at === "string") ||
92
+ !validDate(row.first_seen_at) || !validDate(row.measured_at) || !validDate(row.value_changed_at) ||
93
+ (row.removed_at !== undefined && !validDate(row.removed_at))) {
94
+ fail("kr_freshness_ledger_invalid", `KR freshness ledger contains an invalid KPI record: ${filePath}`, { path: filePath });
95
+ }
96
+ assertNoSecretText("KR name", row.name);
97
+ if (typeof row.value === "string")
98
+ assertNoSecretText("KR value", row.value);
99
+ if (typeof row.entry_measured_at === "string")
100
+ assertNoSecretText("KR measured_at", row.entry_measured_at);
101
+ kpis[key] = {
102
+ name: row.name,
103
+ value: row.value,
104
+ entry_measured_at: row.entry_measured_at,
105
+ first_seen_at: row.first_seen_at,
106
+ measured_at: row.measured_at,
107
+ value_changed_at: row.value_changed_at,
108
+ ...(typeof row.removed_at === "string" ? { removed_at: row.removed_at } : {}),
109
+ };
110
+ }
111
+ return {
112
+ schema_version: KR_FRESHNESS_LEDGER_SCHEMA,
113
+ lane_id: laneId,
114
+ updated_at: value.updated_at,
115
+ kpis,
116
+ };
117
+ }
118
+ export function readKrFreshnessStatus(statusPath) {
119
+ let stat;
120
+ try {
121
+ stat = fs.lstatSync(statusPath);
122
+ }
123
+ catch (error) {
124
+ fail("kr_freshness_status_unreadable", `Cannot inspect manager status for KR freshness: ${statusPath}`, {
125
+ path: statusPath,
126
+ cause: error.code ?? "unknown",
127
+ });
128
+ }
129
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_STATUS_BYTES) {
130
+ fail("kr_freshness_status_invalid", `Manager status is not a bounded regular file: ${statusPath}`, { path: statusPath });
131
+ }
132
+ let text;
133
+ try {
134
+ text = fs.readFileSync(statusPath, "utf8");
135
+ }
136
+ catch (error) {
137
+ fail("kr_freshness_status_unreadable", `Cannot read manager status for KR freshness: ${statusPath}`, {
138
+ path: statusPath,
139
+ cause: error.code ?? "unknown",
140
+ });
141
+ }
142
+ if (Buffer.byteLength(text, "utf8") > MAX_STATUS_BYTES) {
143
+ fail("kr_freshness_status_invalid", `Manager status exceeds the KR freshness size limit: ${statusPath}`, { path: statusPath });
144
+ }
145
+ let parsed;
146
+ try {
147
+ parsed = JSON.parse(text);
148
+ }
149
+ catch {
150
+ fail("kr_freshness_status_invalid", `Manager status contains invalid JSON: ${statusPath}`, { path: statusPath });
151
+ }
152
+ if (!isObject(parsed) || !Array.isArray(parsed.kpis)) {
153
+ fail("kr_freshness_status_invalid", `Manager status must contain a top-level kpis array: ${statusPath}`, { path: statusPath });
154
+ }
155
+ if (parsed.kpis.length > MAX_KPIS) {
156
+ fail("kr_freshness_status_invalid", `Manager status exceeds ${MAX_KPIS} KPIs: ${statusPath}`, { path: statusPath });
157
+ }
158
+ const found = new Map();
159
+ for (const [index, candidate] of parsed.kpis.entries()) {
160
+ if (!isObject(candidate) || typeof candidate.name !== "string" || candidate.name.trim() === "")
161
+ continue;
162
+ const fullName = candidate.name.trim();
163
+ if (Array.from(fullName).some((char) => {
164
+ const codePoint = char.codePointAt(0) ?? 0;
165
+ return codePoint <= 0x1f || codePoint === 0x7f;
166
+ })) {
167
+ fail("kr_freshness_status_invalid", `Manager KPI ${index + 1} name contains control characters: ${statusPath}`, { path: statusPath, index });
168
+ }
169
+ if (!(typeof candidate.value === "string" || (typeof candidate.value === "number" && Number.isFinite(candidate.value)))) {
170
+ fail("kr_freshness_status_invalid", `Manager KPI ${index + 1} needs a string or finite number value: ${statusPath}`, { path: statusPath, index });
171
+ }
172
+ if (candidate.measured_at !== undefined && typeof candidate.measured_at !== "string") {
173
+ fail("kr_freshness_status_invalid", `Manager KPI ${index + 1} measured_at must be a string when present: ${statusPath}`, { path: statusPath, index });
174
+ }
175
+ assertNoSecretText("KR name", fullName);
176
+ if (typeof candidate.value === "string")
177
+ assertNoSecretText("KR value", candidate.value);
178
+ if (typeof candidate.measured_at === "string")
179
+ assertNoSecretText("KR measured_at", candidate.measured_at);
180
+ const key = sha256(fullName);
181
+ if (found.has(key))
182
+ fail("kr_freshness_status_invalid", `Manager status contains duplicate KPI names: ${statusPath}`, { path: statusPath, index });
183
+ found.set(key, {
184
+ key,
185
+ fullName,
186
+ name: displayName(fullName),
187
+ value: candidate.value,
188
+ entryMeasuredAt: typeof candidate.measured_at === "string" ? candidate.measured_at : null,
189
+ });
190
+ }
191
+ return [...found.values()];
192
+ }
193
+ export function krFreshnessLedgerPath(root, laneId) {
194
+ return ledgerFilePath(root, laneId);
195
+ }
196
+ export function readKrFreshnessLedger(root, laneId) {
197
+ const filePath = ledgerFilePath(root, laneId);
198
+ if (!ensureLedgerDirectory(root, false))
199
+ return null;
200
+ let stat;
201
+ try {
202
+ stat = fs.lstatSync(filePath);
203
+ }
204
+ catch (error) {
205
+ if (error.code === "ENOENT")
206
+ return null;
207
+ fail("kr_freshness_ledger_unreadable", `Cannot inspect KR freshness ledger: ${filePath}`, { path: filePath });
208
+ }
209
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_LEDGER_BYTES) {
210
+ fail("kr_freshness_ledger_invalid", `KR freshness ledger is not a bounded regular file: ${filePath}`, { path: filePath });
211
+ }
212
+ let parsed;
213
+ try {
214
+ parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
215
+ }
216
+ catch {
217
+ fail("kr_freshness_ledger_invalid", `KR freshness ledger contains invalid JSON: ${filePath}`, { path: filePath });
218
+ }
219
+ return validateLedger(parsed, laneId, filePath);
220
+ }
221
+ export function writeKrFreshnessLedger(root, ledger) {
222
+ const filePath = ledgerFilePath(root, ledger.lane_id);
223
+ const directory = path.dirname(filePath);
224
+ let tempPath;
225
+ try {
226
+ ensureLedgerDirectory(root, true);
227
+ const data = `${JSON.stringify(ledger, null, 2)}\n`;
228
+ if (Buffer.byteLength(data, "utf8") > MAX_LEDGER_BYTES) {
229
+ fail("kr_freshness_ledger_invalid", `KR freshness ledger exceeds ${MAX_LEDGER_BYTES} bytes`, { path: filePath });
230
+ }
231
+ tempPath = path.join(directory, `.${ledger.lane_id}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
232
+ fs.writeFileSync(tempPath, data, { encoding: "utf8", flag: "wx", mode: 0o600 });
233
+ fs.renameSync(tempPath, filePath);
234
+ }
235
+ catch (error) {
236
+ if (tempPath !== undefined) {
237
+ try {
238
+ fs.unlinkSync(tempPath);
239
+ }
240
+ catch { /* preserve the original write failure */ }
241
+ }
242
+ if (error instanceof LanesError)
243
+ throw error;
244
+ fail("kr_freshness_ledger_write_failed", `Cannot write KR freshness ledger: ${filePath}`, { path: filePath });
245
+ }
246
+ }
247
+ export function updateKrFreshnessLedger(laneId, prior, statusKpis, now) {
248
+ const at = new Date(now).toISOString();
249
+ const next = {};
250
+ const seen = new Set();
251
+ for (const item of statusKpis) {
252
+ const { key, fullName } = item;
253
+ seen.add(key);
254
+ const old = prior?.kpis[key];
255
+ const changed = old === undefined || JSON.stringify(old.value) !== JSON.stringify(item.value);
256
+ const remeasured = old === undefined || old.removed_at !== undefined || old.entry_measured_at !== item.entryMeasuredAt;
257
+ next[key] = {
258
+ name: item.name,
259
+ value: item.value,
260
+ entry_measured_at: item.entryMeasuredAt,
261
+ first_seen_at: old?.first_seen_at ?? at,
262
+ measured_at: changed || remeasured ? at : old.measured_at,
263
+ value_changed_at: changed ? at : old.value_changed_at,
264
+ };
265
+ assertNoSecretText("KR name", fullName);
266
+ }
267
+ for (const [key, old] of Object.entries(prior?.kpis ?? {})) {
268
+ if (seen.has(key))
269
+ continue;
270
+ const removedAt = old.removed_at ?? at;
271
+ if (now - Date.parse(removedAt) >= REMOVED_RETENTION_MS)
272
+ continue;
273
+ next[key] = { ...old, removed_at: removedAt };
274
+ }
275
+ if (Object.keys(next).length > MAX_KPIS) {
276
+ fail("kr_freshness_ledger_invalid", `KR freshness ledger exceeds ${MAX_KPIS} retained entries`);
277
+ }
278
+ return {
279
+ schema_version: KR_FRESHNESS_LEDGER_SCHEMA,
280
+ lane_id: laneId,
281
+ updated_at: at,
282
+ kpis: next,
283
+ };
284
+ }
285
+ export function krFreshnessVerdict(kpi, now, windowHours) {
286
+ const cutoff = now - windowHours * 60 * 60 * 1000;
287
+ if (Date.parse(kpi.value_changed_at) >= cutoff)
288
+ return "moving";
289
+ if (Date.parse(kpi.measured_at) >= cutoff)
290
+ return "flat";
291
+ return "stalled";
292
+ }
293
+ export function krFreshnessFingerprint(keys) {
294
+ return sha256(JSON.stringify([...keys].sort()));
295
+ }
296
+ function viewsFromLedger(ledger, now, windowHours) {
297
+ if (!ledger)
298
+ return [];
299
+ return Object.entries(ledger.kpis)
300
+ .filter(([, kpi]) => kpi.removed_at === undefined)
301
+ .sort(([left], [right]) => left.localeCompare(right))
302
+ .map(([, kpi]) => ({
303
+ name: kpi.name,
304
+ value: kpi.value,
305
+ verdict: krFreshnessVerdict(kpi, now, windowHours),
306
+ measured_at: kpi.measured_at,
307
+ value_changed_at: kpi.value_changed_at,
308
+ }));
309
+ }
310
+ function reportFailure(lane, error) {
311
+ process.stderr.write(`hq lanes: KR freshness disabled for ${lane.lane_id}: ${errorCode(error)}\n`);
312
+ }
313
+ function failedResolution(error, onError) {
314
+ onError(error);
315
+ return {
316
+ flagEnabled: false,
317
+ enabled: false,
318
+ windowHours: KR_FRESHNESS_DEFAULT_HOURS,
319
+ kpis: [],
320
+ stalledKeys: [],
321
+ error: errorCode(error),
322
+ };
323
+ }
324
+ /** Read the flag before manager mapping, status, configuration or ledger files. */
325
+ export async function resolveKrFreshnessForLane(root, lane, options = {}) {
326
+ const onError = options.onError ?? ((error) => reportFailure(lane, error));
327
+ let flagEnabled;
328
+ try {
329
+ flagEnabled = await (options.readFlag ?? readLanesRegistryFlag)(KR_FRESHNESS_FLAG, {
330
+ company: lane.company ?? lane.mesh_company ?? undefined,
331
+ env: options.env,
332
+ });
333
+ }
334
+ catch (error) {
335
+ return failedResolution(error, onError);
336
+ }
337
+ if (!flagEnabled) {
338
+ return { flagEnabled: false, enabled: false, windowHours: KR_FRESHNESS_DEFAULT_HOURS, kpis: [], stalledKeys: [] };
339
+ }
340
+ try {
341
+ const target = resolveManagerStatusTarget(root, lane);
342
+ if (!target.manager || target.statusPath === undefined) {
343
+ return {
344
+ flagEnabled: true,
345
+ enabled: true,
346
+ target,
347
+ windowHours: KR_FRESHNESS_DEFAULT_HOURS,
348
+ kpis: [],
349
+ stalledKeys: [],
350
+ };
351
+ }
352
+ const windowHours = loadLanesConfig(root).kr_freshness_hours;
353
+ const statusKpis = readKrFreshnessStatus(target.statusPath);
354
+ const prior = readKrFreshnessLedger(root, lane.lane_id);
355
+ const ledger = updateKrFreshnessLedger(lane.lane_id, prior, statusKpis, options.now ?? Date.now());
356
+ writeKrFreshnessLedger(root, ledger);
357
+ const effectiveNow = options.now ?? Date.now();
358
+ return {
359
+ flagEnabled: true,
360
+ enabled: true,
361
+ target,
362
+ statusPath: target.statusPath,
363
+ windowHours,
364
+ kpis: viewsFromLedger(ledger, effectiveNow, windowHours),
365
+ stalledKeys: Object.entries(ledger.kpis)
366
+ .filter(([, kpi]) => kpi.removed_at === undefined && krFreshnessVerdict(kpi, effectiveNow, windowHours) === "stalled")
367
+ .map(([key]) => key)
368
+ .sort(),
369
+ };
370
+ }
371
+ catch (error) {
372
+ return failedResolution(error, onError);
373
+ }
374
+ }
375
+ /** Read a previously written ledger for `lanes show`; this never reads status. */
376
+ export async function resolveKrFreshnessForShow(root, lane, options = {}) {
377
+ const onError = options.onError ?? ((error) => reportFailure(lane, error));
378
+ let enabled;
379
+ try {
380
+ enabled = await (options.readFlag ?? readLanesRegistryFlag)(KR_FRESHNESS_FLAG, {
381
+ company: lane.company ?? lane.mesh_company ?? undefined,
382
+ env: options.env,
383
+ });
384
+ }
385
+ catch (error) {
386
+ onError(error);
387
+ return null;
388
+ }
389
+ if (!enabled)
390
+ return null;
391
+ try {
392
+ const windowHours = loadLanesConfig(root).kr_freshness_hours;
393
+ const ledger = readKrFreshnessLedger(root, lane.lane_id);
394
+ return { enabled: true, kpis: viewsFromLedger(ledger, options.now ?? Date.now(), windowHours) };
395
+ }
396
+ catch (error) {
397
+ onError(error);
398
+ return null;
399
+ }
400
+ }
401
+ /** Operator report path: the ledger remains readable while runtime enforcement is off. */
402
+ export function readKrFreshnessReport(root, laneId, now) {
403
+ const ledger = readKrFreshnessLedger(root, laneId);
404
+ const windowHours = loadLanesConfig(root).kr_freshness_hours;
405
+ return {
406
+ ledger_path: krFreshnessLedgerPath(root, laneId),
407
+ available: ledger !== null,
408
+ kr_freshness_hours: windowHours,
409
+ kpis: viewsFromLedger(ledger, now, windowHours),
410
+ };
411
+ }
412
+ //# sourceMappingURL=kr-freshness.js.map
@@ -0,0 +1,9 @@
1
+ import type { LaneCompanyRef, LaneRecord } from "./types.js";
2
+ export type LaneCompanySource = "lane" | "project" | "unresolved";
3
+ export interface LaneCompanyResolution {
4
+ company: LaneCompanyRef | null;
5
+ source: LaneCompanySource;
6
+ }
7
+ /** Resolve a lane's company without inferring ownership from its worker or caller. */
8
+ export declare function resolveLaneCompany(hqRoot: string, lane: Pick<LaneRecord, "lane_id" | "company" | "project_id">): LaneCompanyResolution;
9
+ //# sourceMappingURL=lane-company.d.ts.map
@@ -0,0 +1,24 @@
1
+ import { resolveProjectQuestionCompany } from "./questions.js";
2
+ import { LanesError } from "./errors.js";
3
+ /** Resolve a lane's company without inferring ownership from its worker or caller. */
4
+ export function resolveLaneCompany(hqRoot, lane) {
5
+ const slug = typeof lane.company?.slug === "string" ? lane.company.slug.trim() : "";
6
+ const uid = typeof lane.company?.uid === "string" ? lane.company.uid.trim() : "";
7
+ if (slug && uid) {
8
+ return { company: { slug, uid }, source: "lane" };
9
+ }
10
+ let projectCompany;
11
+ try {
12
+ projectCompany = resolveProjectQuestionCompany(hqRoot, lane);
13
+ }
14
+ catch (err) {
15
+ if (err instanceof LanesError && err.error === "question_company_unavailable") {
16
+ return { company: null, source: "unresolved" };
17
+ }
18
+ throw err;
19
+ }
20
+ return projectCompany
21
+ ? { company: projectCompany, source: "project" }
22
+ : { company: null, source: "unresolved" };
23
+ }
24
+ //# sourceMappingURL=lane-company.js.map
@@ -22,6 +22,7 @@ import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecor
22
22
  import { appendCappedHistory, lanesStoreRoot, parseLaneRecord, parseQuestionRecord, readLane, readQuestion, updateLane, updateQuestion, } from "./store.js";
23
23
  import { applyThrottleDecision, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, throttleStatePath, writeThrottleState, } from "./throttle.js";
24
24
  import { readLanesRegistryFlag } from "./registry-flag.js";
25
+ import { resolveLaneCompany } from "./lane-company.js";
25
26
  import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, LEAD_LANE_KEEP_ALIVE_RESUME_TEXT, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, undeliveredAnsweredQuestions, } from "./keep-alive.js";
26
27
  import { providerSupportsSessionResume } from "./delivery.js";
27
28
  import { applyDefaultQuotaHold, readQuotaSignal, recordAccountQuota } from "./quota.js";
@@ -851,8 +852,11 @@ export async function runPassBody(hqRoot, deps) {
851
852
  if (doneKeepAliveVerdict(lane, envelope, hasQueuedWork) !== "eligible")
852
853
  continue;
853
854
  }
854
- const key = keepAliveCompanyKey(lane.company);
855
- keepAliveFlagCompanies.set(key, lane.company);
855
+ const company = resolveLaneCompany(hqRoot, lane).company;
856
+ if (company === null)
857
+ continue;
858
+ const key = keepAliveCompanyKey(company);
859
+ keepAliveFlagCompanies.set(key, company);
856
860
  }
857
861
  const keepAliveFlagByCompany = new Map();
858
862
  const supersessionLineageAwareByCompany = new Map();
@@ -1598,6 +1602,15 @@ function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCom
1598
1602
  const nowMs = now.getTime();
1599
1603
  const nowIso = now.toISOString();
1600
1604
  const config = loadLanesConfig(hqRoot);
1605
+ const resolvedCompanyByLaneVersion = new Map();
1606
+ const companyForLane = (candidate) => {
1607
+ const key = `${candidate.lane_id}\u0000${candidate.version}`;
1608
+ if (resolvedCompanyByLaneVersion.has(key))
1609
+ return resolvedCompanyByLaneVersion.get(key) ?? null;
1610
+ const company = resolveLaneCompany(hqRoot, candidate).company;
1611
+ resolvedCompanyByLaneVersion.set(key, company);
1612
+ return company;
1613
+ };
1601
1614
  const keepAliveEnabled = [...flagByCompany.values()].some((enabled) => enabled);
1602
1615
  let retiredLaneIds = null;
1603
1616
  if (keepAliveEnabled) {
@@ -1615,7 +1628,7 @@ function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCom
1615
1628
  originalLane.state === "cancelled" ||
1616
1629
  originalLane.state === "failed") {
1617
1630
  if (originalLane.keep_alive === true &&
1618
- flagByCompany.get(keepAliveCompanyKey(originalLane.company)) === true)
1631
+ flagByCompany.get(keepAliveCompanyKey(companyForLane(originalLane))) === true)
1619
1632
  recordKeepAliveSkip(hqRoot, originalLane, "terminal_state", summary, deps);
1620
1633
  continue;
1621
1634
  }
@@ -1641,8 +1654,28 @@ function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCom
1641
1654
  continue;
1642
1655
  }
1643
1656
  }
1644
- if (flagByCompany.get(keepAliveCompanyKey(lane.company)) !== true)
1657
+ const company = companyForLane(lane);
1658
+ if (company === null) {
1659
+ if (lane.keep_alive === true) {
1660
+ summary.skipped.push({
1661
+ kind: "lane",
1662
+ id: lane.lane_id,
1663
+ why: "keep_alive_company_unresolved",
1664
+ });
1665
+ }
1645
1666
  continue;
1667
+ }
1668
+ const companyKey = keepAliveCompanyKey(company);
1669
+ if (flagByCompany.get(companyKey) !== true) {
1670
+ if (lane.keep_alive === true) {
1671
+ summary.skipped.push({
1672
+ kind: "lane",
1673
+ id: lane.lane_id,
1674
+ why: "keep_alive_flag_off",
1675
+ });
1676
+ }
1677
+ continue;
1678
+ }
1646
1679
  // Persist a stable timestamp for legacy interrupted rows before a later
1647
1680
  // keep-alive status write can advance their general updated_at clock. The
1648
1681
  // watcher classification is the interruption time; updated_at is only an
@@ -1760,7 +1793,7 @@ function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCom
1760
1793
  recordKeepAliveSkip(hqRoot, lane, "retired_list_invalid", summary, deps);
1761
1794
  continue;
1762
1795
  }
1763
- const exclusion = keepAliveExclusion(lane, lanes, retiredLaneIds, supersessionLineageAwareByCompany.get(keepAliveCompanyKey(lane.company)) === true);
1796
+ const exclusion = keepAliveExclusion(lane, lanes, retiredLaneIds, supersessionLineageAwareByCompany.get(companyKey) === true, companyForLane);
1764
1797
  if (exclusion !== undefined) {
1765
1798
  recordKeepAliveSkip(hqRoot, lane, exclusion, summary, deps);
1766
1799
  continue;
@@ -1851,7 +1884,25 @@ function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCom
1851
1884
  continue;
1852
1885
  }
1853
1886
  const currentLanes = loadLaneRecords(hqRoot, summary);
1854
- const currentExclusion = keepAliveExclusion(current, currentLanes, retiredLaneIds, supersessionLineageAwareByCompany.get(keepAliveCompanyKey(current.company)) === true);
1887
+ const currentCompany = companyForLane(current);
1888
+ if (currentCompany === null) {
1889
+ summary.skipped.push({
1890
+ kind: "lane",
1891
+ id: current.lane_id,
1892
+ why: "keep_alive_company_unresolved",
1893
+ });
1894
+ continue;
1895
+ }
1896
+ const currentCompanyKey = keepAliveCompanyKey(currentCompany);
1897
+ if (flagByCompany.get(currentCompanyKey) !== true) {
1898
+ summary.skipped.push({
1899
+ kind: "lane",
1900
+ id: current.lane_id,
1901
+ why: "keep_alive_flag_off",
1902
+ });
1903
+ continue;
1904
+ }
1905
+ const currentExclusion = keepAliveExclusion(current, currentLanes, retiredLaneIds, supersessionLineageAwareByCompany.get(currentCompanyKey) === true, companyForLane);
1855
1906
  if (currentExclusion !== undefined) {
1856
1907
  recordKeepAliveSkip(hqRoot, current, currentExclusion, summary, deps);
1857
1908
  continue;
@@ -370,7 +370,22 @@ export interface BlockerFreshnessHistoryEntry {
370
370
  resume_skipped_reason?: "open_question" | "blocked" | "miss_limit" | "no_provider_session";
371
371
  delivery_failed?: true;
372
372
  }
373
- export type LaneGateHistoryEntry = ManagerStatusGateHistoryEntry | BlockerFreshnessHistoryEntry;
373
+ export interface KrFreshnessState {
374
+ /** Stable fingerprint of the last stalled KPI set, which is resumed once. */
375
+ stalled_fingerprint?: string;
376
+ }
377
+ export interface KrFreshnessHistoryEntry {
378
+ event: "kr_stalled";
379
+ turn_id: string;
380
+ path: string;
381
+ at: string;
382
+ names: string[];
383
+ count: number;
384
+ fingerprint: string;
385
+ resume_queued?: boolean;
386
+ resume_skipped_reason?: "open_question" | "blocked" | "miss_limit" | "no_provider_session";
387
+ }
388
+ export type LaneGateHistoryEntry = ManagerStatusGateHistoryEntry | BlockerFreshnessHistoryEntry | KrFreshnessHistoryEntry;
374
389
  export interface LaneStory {
375
390
  project_id: string;
376
391
  story_id: string;
@@ -516,6 +531,8 @@ export interface LaneRecord {
516
531
  status_gate_history?: LaneGateHistoryEntry[];
517
532
  /** Last blocker-freshness state used to detect consecutive stale turns. */
518
533
  blocker_freshness_state?: BlockerFreshnessState;
534
+ /** Last stalled KR set already offered one automatic resume. */
535
+ kr_freshness_state?: KrFreshnessState;
519
536
  /** Last automatic compaction of a resumed session (compaction.ts). */
520
537
  last_compaction?: LaneCompactionRecord;
521
538
  /**
@@ -857,6 +874,8 @@ export interface LanesConfig {
857
874
  session_question_timeout_secs: number;
858
875
  /** Age in hours after which an open manager blocker must be re-verified. */
859
876
  blocker_freshness_hours: number;
877
+ /** Age in hours after which a manager KPI is considered stalled. */
878
+ kr_freshness_hours: number;
860
879
  }
861
880
  export declare function parseMajorSchema(schemaVersion: string, expectedPrefix: string, expectedMajor: number): void;
862
881
  export declare function parseLaneEnvelope(raw: unknown): LaneEnvelope;
@@ -228,7 +228,7 @@ export const LANE_FIELD_WRITERS = {
228
228
  provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_selection: ["creator", "command"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], failed_resume_history: ["command"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
229
229
  version: ["creator"], created_at: ["creator"], updated_at: ["creator"], attribution_history: ["command"],
230
230
  last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn", "command"], effort_source: ["creator", "spawn", "command"], fast_mode: ["spawn"],
231
- last_message_at: ["command"], status_gate: ["spawn", "watcher"], status_gate_history: ["watcher"], blocker_freshness_state: ["watcher"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
231
+ last_message_at: ["command"], status_gate: ["spawn", "watcher"], status_gate_history: ["watcher"], blocker_freshness_state: ["watcher"], kr_freshness_state: ["watcher"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
232
232
  overdue: ["reconcile"], overdue_at: ["reconcile"], last_host_report_at: ["watcher", "spawn", "reconcile"], stalled: ["watcher"], stalled_at: ["watcher"], stalled_reason: ["watcher"],
233
233
  admitted_reason: ["command", "reconcile"], terminal_reason: ["command", "watcher", "spawn", "reconcile"], stop_history: ["command"], interrupted_at: ["watcher", "reconcile"], terminal_details: ["command", "spawn", "watcher", "reconcile"],
234
234
  quota_reset_at: ["watcher", "reconcile"], model_quota_marker: ["watcher", "reconcile"], outpost_command_id: ["command"],
@@ -233,6 +233,8 @@ export interface WatcherLoopDeps {
233
233
  readManagerStatusGateFlag?: typeof readLanesRegistryFlag;
234
234
  /** Test seam; production reads the company-scoped hq-flags row. */
235
235
  readBlockerFreshnessFlag?: typeof readLanesRegistryFlag;
236
+ /** Test seam; production reads the company-scoped hq-flags row. */
237
+ readKrFreshnessFlag?: typeof readLanesRegistryFlag;
236
238
  }
237
239
  /**
238
240
  * Wrap one mesh call. A failed emit is logged as `mesh_event_failed` with
@@ -49,6 +49,7 @@ import { createLaneMesh, emitLaneDecisionEvents, emitLaneProgressEvent, emitLane
49
49
  import { envelopeQuestionId } from "./questions.js";
50
50
  import { readLanesRegistryFlag } from "./registry-flag.js";
51
51
  import { blockerFreshnessFingerprint, resolveBlockerFreshnessForLane, } from "./blocker-freshness.js";
52
+ import { krFreshnessFingerprint, resolveKrFreshnessForLane, } from "./kr-freshness.js";
52
53
  import { MANAGER_STATUS_GATE_FLAG, managerStatusWasWritten, readStatusSnapshot, resolveManagerStatusTarget, managerStatusGateEnabledForLane, } from "./manager-status-gate.js";
53
54
  import { queueLaneTerminalResume, queueManagerStatusGateResume } from "./keep-alive.js";
54
55
  import { monitorSessionDir } from "../monitor.js";
@@ -863,6 +864,72 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
863
864
  }
864
865
  return updated;
865
866
  }
867
+ async function settleKrFreshness(hqRoot, lane, manifestTurnId, at, deps, logDecision) {
868
+ if (manifestTurnId !== undefined && lane.status_gate !== undefined && lane.status_gate.turn_id !== manifestTurnId) {
869
+ logDecision({ event: "kr_freshness_baseline_mismatch", at, turn_id: lane.status_gate.turn_id });
870
+ return lane;
871
+ }
872
+ let errorLogged = false;
873
+ const resolution = await resolveKrFreshnessForLane(hqRoot, lane, {
874
+ readFlag: deps.readKrFreshnessFlag ?? readLanesRegistryFlag,
875
+ now: Date.parse(at),
876
+ onError: (error) => {
877
+ if (errorLogged)
878
+ return;
879
+ errorLogged = true;
880
+ const code = error instanceof LanesError ? error.error : "kr_freshness_unavailable";
881
+ process.stderr.write(`hq lanes watcher: KR freshness disabled for ${lane.lane_id}: ${code}\n`);
882
+ logDecision({ event: "kr_freshness_disabled", at, error: code });
883
+ },
884
+ });
885
+ if (!resolution.enabled || resolution.statusPath === undefined || resolution.error !== undefined)
886
+ return lane;
887
+ const stalled = resolution.kpis.filter((kpi) => kpi.verdict === "stalled");
888
+ if (stalled.length === 0) {
889
+ if (lane.kr_freshness_state === undefined)
890
+ return lane;
891
+ return retryUpdateLane(hqRoot, lane.lane_id, "watcher", (current) => current.kr_freshness_state === undefined
892
+ ? current
893
+ : { ...current, kr_freshness_state: undefined });
894
+ }
895
+ const fingerprint = krFreshnessFingerprint(resolution.stalledKeys);
896
+ const names = stalled.map((kpi) => kpi.name);
897
+ const turnId = manifestTurnId ?? lane.status_gate?.turn_id ?? `unknown-${at}`;
898
+ const resumeText = `These KPIs in ${resolution.statusPath} were not re-measured in the last ${resolution.windowHours}h: ${names.join(", ")}. Measure each one now, update value and measured_at, then end the turn.`;
899
+ return retryUpdateLane(hqRoot, lane.lane_id, "watcher", (current) => {
900
+ const history = current.status_gate_history ?? [];
901
+ if (history.some((entry) => entry.event === "kr_stalled" && entry.turn_id === turnId))
902
+ return current;
903
+ const sameStalledSet = current.kr_freshness_state?.stalled_fingerprint === fingerprint;
904
+ let nextLane = current;
905
+ let resumeQueued = false;
906
+ let resumeSkippedReason;
907
+ if (!sameStalledSet) {
908
+ const mergeExisting = current.pending_resume !== undefined &&
909
+ current.state !== "blocked" && current.state !== "awaiting_input";
910
+ const resume = queueLaneTerminalResume(current, at, resumeText, { mergeExisting });
911
+ nextLane = resume.lane;
912
+ resumeQueued = resume.resumeQueued;
913
+ resumeSkippedReason = resume.resumeSkippedReason;
914
+ }
915
+ const entry = {
916
+ event: "kr_stalled",
917
+ turn_id: turnId,
918
+ path: resolution.statusPath,
919
+ at,
920
+ names,
921
+ count: stalled.length,
922
+ fingerprint,
923
+ ...(resumeQueued ? { resume_queued: true } : {}),
924
+ ...(resumeSkippedReason !== undefined ? { resume_skipped_reason: resumeSkippedReason } : {}),
925
+ };
926
+ return {
927
+ ...nextLane,
928
+ kr_freshness_state: { stalled_fingerprint: fingerprint },
929
+ status_gate_history: appendCappedHistory(history, entry),
930
+ };
931
+ });
932
+ }
866
933
  function resolveBlockerFreshnessSessionInbox(hqRoot, sessionId) {
867
934
  let candidates;
868
935
  try {
@@ -1452,11 +1519,13 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
1452
1519
  if (terminalWrite.changed) {
1453
1520
  updated = await settleManagerStatusGate(hqRoot, updated, manifest.turn_id, tick.emit.at, deps, logDecision);
1454
1521
  updated = await settleBlockerFreshness(hqRoot, updated, manifest.turn_id, tick.emit.at, deps, mesh, logDecision);
1522
+ updated = await settleKrFreshness(hqRoot, updated, manifest.turn_id, tick.emit.at, deps, logDecision);
1455
1523
  }
1456
1524
  const managerStatusFollowupQueued = manifest.turn_id !== undefined &&
1457
1525
  updated.status_gate_history?.some((entry) => entry.turn_id === manifest.turn_id &&
1458
1526
  ((entry.event === "status_missed" && entry.resume_queued === true) ||
1459
- (entry.event === "blocker_stale" && entry.resume_queued === true))) === true;
1527
+ (entry.event === "blocker_stale" && entry.resume_queued === true) ||
1528
+ (entry.event === "kr_stalled" && entry.resume_queued === true))) === true;
1460
1529
  if (terminalWrite.changed && deps.recoverQueuedMessages && !managerStatusFollowupQueued) {
1461
1530
  try {
1462
1531
  if (dropboxInboxHasMessages(hqRoot, laneId)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.264.0",
3
+ "version": "5.265.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {