@indigoai-us/hq-cli 5.268.0 → 5.270.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/command-catalog.generated.d.ts +77 -4
  3. package/dist/command-catalog.generated.js +98 -4
  4. package/dist/commands/files.js +6 -3
  5. package/dist/commands/lanes-hold.d.ts +2 -0
  6. package/dist/commands/lanes-hold.js +122 -310
  7. package/dist/commands/lanes-message.js +6 -4
  8. package/dist/commands/lanes-review.d.ts +10 -0
  9. package/dist/commands/lanes-review.js +56 -0
  10. package/dist/commands/lanes-shared.d.ts +1 -0
  11. package/dist/commands/lanes.d.ts +2 -0
  12. package/dist/commands/lanes.js +29 -4
  13. package/dist/lib/billing/plan-limit-upgrade-url.d.ts +1 -4
  14. package/dist/lib/billing/plan-limit-upgrade-url.js +7 -18
  15. package/dist/lib/doctor/checks/sync-runtime.d.ts +1 -1
  16. package/dist/lib/doctor/checks/sync-runtime.js +69 -19
  17. package/dist/lib/lanes/blocker-freshness.js +2 -1
  18. package/dist/lib/lanes/keep-alive.js +49 -20
  19. package/dist/lib/lanes/kr-freshness.js +3 -2
  20. package/dist/lib/lanes/lane-company.d.ts +3 -2
  21. package/dist/lib/lanes/lane-company.js +21 -4
  22. package/dist/lib/lanes/manager-status-gate.js +5 -3
  23. package/dist/lib/lanes/merge-command.d.ts +14 -0
  24. package/dist/lib/lanes/merge-command.js +363 -0
  25. package/dist/lib/lanes/reviews.d.ts +38 -0
  26. package/dist/lib/lanes/reviews.js +395 -0
  27. package/dist/lib/lanes/watcher.js +4 -2
  28. package/dist/lib/plan-limit-nag.js +2 -4
  29. package/dist/utils/feedback-logs.d.ts +14 -9
  30. package/dist/utils/feedback-logs.js +43 -14
  31. package/dist/utils/local-files-overview.d.ts +4 -2
  32. package/dist/utils/local-files-overview.js +47 -6
  33. package/dist/utils/sync-log-sources.d.ts +15 -0
  34. package/dist/utils/sync-log-sources.js +36 -0
  35. package/dist/utils/version-gate.d.ts +1 -0
  36. package/dist/utils/version-gate.js +6 -1
  37. package/package.json +1 -1
@@ -23,6 +23,7 @@ import { registerLaneChecksCommand } from "./lanes-checks.js";
23
23
  import { registerLaneReportCommands } from "./lanes-report.js";
24
24
  import { registerLaneTrackerImportCommand } from "./lanes-import.js";
25
25
  import { registerLaneRepoMergeHoldCommand } from "./lanes-hold.js";
26
+ import { registerLaneReviewCommand } from "./lanes-review.js";
26
27
  import { ensureCognitoToken } from "../utils/cognito-session.js";
27
28
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
28
29
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
@@ -47,6 +48,7 @@ import { CompanySelectionError } from "../utils/company-selection-error.js";
47
48
  import { assertNoSecretText } from "../lib/lanes/text-scan.js";
48
49
  import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
49
50
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
51
+ import { listLaneReviews } from "../lib/lanes/reviews.js";
50
52
  import { effectiveAccountHome } from "../lib/lanes/account-home.js";
51
53
  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";
52
54
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
@@ -291,6 +293,7 @@ function resolveDeps(deps = {}) {
291
293
  isSsmOwnerDispatch: deps.isSsmOwnerDispatch ?? (() => isSsmRunCommandProcess()),
292
294
  withGrantHireLocks: deps.withGrantHireLocks ?? withLaneHireLocks,
293
295
  laneTrackerEnabled: deps.laneTrackerEnabled ?? defaultLaneTrackerEnabled,
296
+ mergeReviewEnabled: deps.mergeReviewEnabled,
294
297
  trackerActor: deps.trackerActor ?? defaultTrackerActor,
295
298
  };
296
299
  }
@@ -4541,10 +4544,26 @@ async function runShow(ctx, laneId) {
4541
4544
  env: ctx.env,
4542
4545
  now: ctx.now().getTime(),
4543
4546
  });
4547
+ const laneIsSenior = allLanes.some((item) => item.senior.kind === "lane" && item.senior.id === laneId);
4548
+ const now = ctx.now().getTime();
4549
+ const reviewListing = laneIsSenior
4550
+ ? listLaneReviews(hqRoot, laneId)
4551
+ : { reviews: [], skipped: 0 };
4552
+ if (reviewListing.skipped > 0) {
4553
+ console.error(`hq lanes show: skipped ${reviewListing.skipped} invalid or unreadable review item(s)`);
4554
+ }
4555
+ const recentReviews = reviewListing.reviews
4556
+ .filter((item) => {
4557
+ const recordedAt = Date.parse(item.recorded_at);
4558
+ return recordedAt >= now - 24 * 60 * 60 * 1000 && recordedAt <= now;
4559
+ })
4560
+ .slice(0, 20)
4561
+ .map(({ repo, pr, head_sha, recorded_at }) => ({ repo, pr, head_sha, recorded_at }));
4544
4562
  printJson({
4545
4563
  ok: true,
4546
4564
  action: "show",
4547
4565
  lane,
4566
+ ...(laneIsSenior ? { reviews: recentReviews, reviews_skipped: reviewListing.skipped } : {}),
4548
4567
  ...hire,
4549
4568
  overdue: Boolean(lane.overdue) || isOverdue(hqRoot, lane.lane_id, ctx.now()),
4550
4569
  stalled: Boolean(lane.stalled),
@@ -4741,7 +4760,7 @@ async function runKeepAlive(ctx, laneId, opts) {
4741
4760
  const initialLane = readLane(hqRoot, laneId);
4742
4761
  assertKeepAliveChangeAllowed(ctx, initialLane, laneId);
4743
4762
  lineageAware = await readLanesRegistryFlag(LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, {
4744
- company: initialLane.company,
4763
+ company: resolveLaneCompany(hqRoot, initialLane).company ?? undefined,
4745
4764
  env: ctx.env,
4746
4765
  });
4747
4766
  }
@@ -6927,6 +6946,12 @@ export function registerLanesCommand(parent, deps = {}) {
6927
6946
  resolveCompany: ctx.resolveCompany,
6928
6947
  now: ctx.now,
6929
6948
  env: ctx.env,
6949
+ mergeReviewEnabled: ctx.mergeReviewEnabled,
6950
+ }, wrap);
6951
+ registerLaneReviewCommand(lanes, {
6952
+ hqRoot: ctx.hqRoot,
6953
+ env: ctx.env,
6954
+ now: ctx.now,
6930
6955
  }, wrap);
6931
6956
  registerOutpostPrunerCommand(lanes);
6932
6957
  registerLaneKpisCommand(lanes, { hqRoot: ctx.hqRoot, now: ctx.now, wrap });
@@ -6977,7 +7002,7 @@ export function registerLanesCommand(parent, deps = {}) {
6977
7002
  .command("grant-target", { hidden: true })
6978
7003
  .argument("<lane-id>", "Lane id")
6979
7004
  .option("--hire <n>", "Non-negative safe integer hire grant")
6980
- .option("--grant-context <base64url>", "Primary-authorized hire hierarchy and gate decision")
7005
+ .option("--grant-context <base64url>", "Primary-authorized hire hierarchy context")
6981
7006
  .action(wrap(async (laneId, opts) => {
6982
7007
  await runGrantOwnerTarget(ctx, laneId, opts);
6983
7008
  }));
@@ -7002,9 +7027,9 @@ export function registerLanesCommand(parent, deps = {}) {
7002
7027
  }));
7003
7028
  lanes
7004
7029
  .command("backfill-grants")
7005
- .description("Preview or apply missing hire grants for alive manager lanes")
7030
+ .description("Preview missing hire grants for alive manager lanes; writes are disabled")
7006
7031
  .option("--dry-run", "Print proposed values without writing (default)")
7007
- .option("--apply", "Write missing hire grants; only run before enforcement is enabled")
7032
+ .option("--apply", "Retained for compatibility; writes are disabled")
7008
7033
  .action(wrap(async (opts) => {
7009
7034
  await runBackfillGrants(ctx, opts);
7010
7035
  }));
@@ -1,14 +1,11 @@
1
- export declare const PLAN_LIMIT_UPGRADE_FALLBACK_FLAG = "cli.plan-limit-upgrade-fallback";
2
- export declare const PLAN_LIMIT_UPGRADE_FALLBACK_DEFAULT = false;
3
1
  export interface PlanLimitUpgradeUrlChoices {
4
2
  topLevelUrl?: string | null;
5
3
  nestedUrl?: string | null;
6
4
  legacyUrl: string;
7
5
  }
8
- export declare function isPlanLimitUpgradeFallbackEnabled(): boolean;
9
6
  /** Resolve and attribute the final selected company billing destination. */
10
7
  export declare function selectPlanLimitUpgradeUrl(companySlug: string | undefined, choices: PlanLimitUpgradeUrlChoices): string;
11
8
  export declare function withCliLimitEntrySurface(value: string): string;
12
- /** Keep the existing destination and flag behavior while labeling company limit links. */
9
+ /** Select the standard upgrade destination and label company limit links. */
13
10
  export declare function planLimitUpgradeFallbackUrl(companySlug: string | undefined, previousFallbackUrl: string): string;
14
11
  //# sourceMappingURL=plan-limit-upgrade-url.d.ts.map
@@ -1,11 +1,5 @@
1
- import { resolveProcessFlagGate } from "../flag-registry.js";
2
1
  import { DEFAULT_VAULT_API_URL } from "../../utils/cognito-session.js";
3
2
  import { consoleOrigin } from "../../utils/console-origin.js";
4
- export const PLAN_LIMIT_UPGRADE_FALLBACK_FLAG = "cli.plan-limit-upgrade-fallback";
5
- export const PLAN_LIMIT_UPGRADE_FALLBACK_DEFAULT = false;
6
- export function isPlanLimitUpgradeFallbackEnabled() {
7
- return resolveProcessFlagGate(PLAN_LIMIT_UPGRADE_FALLBACK_FLAG, { fallback: PLAN_LIMIT_UPGRADE_FALLBACK_DEFAULT }, () => PLAN_LIMIT_UPGRADE_FALLBACK_DEFAULT);
8
- }
9
3
  function companyBillingUrl(companySlug) {
10
4
  const controlPlaneUrl = process.env.HQ_VAULT_API_URL?.trim() || DEFAULT_VAULT_API_URL;
11
5
  const origin = consoleOrigin(controlPlaneUrl);
@@ -19,20 +13,15 @@ function companyBillingUrl(companySlug) {
19
13
  }
20
14
  /** Resolve and attribute the final selected company billing destination. */
21
15
  export function selectPlanLimitUpgradeUrl(companySlug, choices) {
16
+ const topLevelUrl = choices.topLevelUrl?.trim() || undefined;
17
+ const nestedUrl = choices.nestedUrl?.trim() || undefined;
18
+ const generatedUrl = companyBillingUrl(companySlug);
22
19
  let selected;
23
- if (!isPlanLimitUpgradeFallbackEnabled()) {
24
- selected = choices.legacyUrl;
20
+ if (companySlug?.trim()) {
21
+ selected = generatedUrl ?? topLevelUrl ?? nestedUrl ?? choices.legacyUrl;
25
22
  }
26
23
  else {
27
- const topLevelUrl = choices.topLevelUrl?.trim() || undefined;
28
- const nestedUrl = choices.nestedUrl?.trim() || undefined;
29
- const generatedUrl = companyBillingUrl(companySlug);
30
- if (companySlug?.trim()) {
31
- selected = generatedUrl ?? topLevelUrl ?? nestedUrl ?? choices.legacyUrl;
32
- }
33
- else {
34
- selected = topLevelUrl ?? nestedUrl ?? generatedUrl ?? choices.legacyUrl;
35
- }
24
+ selected = topLevelUrl ?? nestedUrl ?? generatedUrl ?? choices.legacyUrl;
36
25
  }
37
26
  return withCliLimitEntrySurface(selected);
38
27
  }
@@ -68,7 +57,7 @@ export function withCliLimitEntrySurface(value) {
68
57
  }
69
58
  return value;
70
59
  }
71
- /** Keep the existing destination and flag behavior while labeling company limit links. */
60
+ /** Select the standard upgrade destination and label company limit links. */
72
61
  export function planLimitUpgradeFallbackUrl(companySlug, previousFallbackUrl) {
73
62
  return selectPlanLimitUpgradeUrl(companySlug, {
74
63
  legacyUrl: previousFallbackUrl,
@@ -7,5 +7,5 @@ export interface SyncRuntimeOptions {
7
7
  /** Classify the last hour of sync log lines into privacy-safe results. */
8
8
  export declare function analyzeSyncRuntimeLog(lines: readonly string[], options: SyncRuntimeOptions): CheckResult[];
9
9
  /** Read the machine's sync logs and client health file. */
10
- export declare function defaultSyncRuntimeResults(now: Date, hqHome?: string): CheckResult[];
10
+ export declare function defaultSyncRuntimeResults(now: Date, hqHome?: string, env?: NodeJS.ProcessEnv): CheckResult[];
11
11
  //# sourceMappingURL=sync-runtime.d.ts.map
@@ -9,6 +9,7 @@
9
9
  import * as fs from "node:fs";
10
10
  import * as os from "node:os";
11
11
  import * as path from "node:path";
12
+ import { currentSyncLogFamily, SYNC_LOG_GENERATIONS, syncLogBasePaths, syncLogGenerationPath, } from "../../../utils/sync-log-sources.js";
12
13
  export const SYNC_RUNTIME_WINDOW_MS = 60 * 60_000;
13
14
  const LAG_SECONDS = 15 * 60;
14
15
  const LAG_MIN_RECEIPTS = 3;
@@ -32,6 +33,32 @@ function parseRecord(rest) {
32
33
  return null;
33
34
  }
34
35
  }
36
+ function timestampFromRecord(record) {
37
+ if (!record)
38
+ return null;
39
+ for (const key of ["time", "timestamp", "ts"]) {
40
+ const value = record[key];
41
+ if (typeof value === "number" && Number.isFinite(value)) {
42
+ const millis = value < 100_000_000_000 ? value * 1000 : value;
43
+ if (Number.isFinite(millis))
44
+ return millis;
45
+ }
46
+ if (typeof value === "string") {
47
+ const parsed = Date.parse(value);
48
+ if (Number.isFinite(parsed))
49
+ return parsed;
50
+ }
51
+ }
52
+ return null;
53
+ }
54
+ function lineTimestamp(line) {
55
+ const stamp = TIMESTAMP.exec(line);
56
+ if (stamp) {
57
+ const parsed = Date.parse(stamp[1]);
58
+ return Number.isFinite(parsed) ? parsed : null;
59
+ }
60
+ return timestampFromRecord(parseRecord(line));
61
+ }
35
62
  /** Classify the last hour of sync log lines into privacy-safe results. */
36
63
  export function analyzeSyncRuntimeLog(lines, options) {
37
64
  const since = options.now - SYNC_RUNTIME_WINDOW_MS;
@@ -40,13 +67,13 @@ export function analyzeSyncRuntimeLog(lines, options) {
40
67
  let starts = 0;
41
68
  let deviceLimited = 0;
42
69
  for (const line of lines) {
43
- const stamp = TIMESTAMP.exec(line);
44
- if (!stamp)
70
+ const time = lineTimestamp(line);
71
+ if (time === null)
45
72
  continue;
46
- const time = Date.parse(stamp[1]);
47
73
  if (!Number.isFinite(time) || time < since || time > options.now)
48
74
  continue;
49
- const rest = line.slice(stamp[0].length);
75
+ const stamp = TIMESTAMP.exec(line);
76
+ const rest = stamp ? line.slice(stamp[0].length) : line;
50
77
  if (DEVICE_LIMIT.test(rest))
51
78
  deviceLimited += 1;
52
79
  const record = parseRecord(rest);
@@ -61,7 +88,8 @@ export function analyzeSyncRuntimeLog(lines, options) {
61
88
  else if (event === "receiver.start") {
62
89
  starts += 1;
63
90
  }
64
- if ((record.type === "receiver" || record.type === "error") &&
91
+ if ((record.type === "receiver" || record.type === "error" ||
92
+ (typeof event === "string" && event.startsWith("receiver."))) &&
65
93
  PERMISSION.test(JSON.stringify(record.err ?? record.message ?? ""))) {
66
94
  denied += 1;
67
95
  }
@@ -80,7 +108,7 @@ export function analyzeSyncRuntimeLog(lines, options) {
80
108
  checkId: `sync.runtime.${check.id}`,
81
109
  reasonCode: check.reasonCode,
82
110
  message: `${check.count} ${check.label} in the last hour.`,
83
- remediation: "Run `hq sync status` and check ~/.hq/logs/hq-sync.log.",
111
+ remediation: "Run `hq sync status` and inspect the selected sync log under ~/.hq/logs/, ~/.hq/daemon/logs/, or the configured HQ_DAEMON_DIR/logs/.",
84
112
  }
85
113
  : {
86
114
  status: "PASS",
@@ -88,7 +116,6 @@ export function analyzeSyncRuntimeLog(lines, options) {
88
116
  message: `${check.count} ${check.label} in the last hour.`,
89
117
  });
90
118
  }
91
- const LOG_GENERATIONS = 4;
92
119
  function readLogTail(file) {
93
120
  const fd = fs.openSync(file, "r");
94
121
  try {
@@ -104,9 +131,9 @@ function readLogTail(file) {
104
131
  }
105
132
  function firstTimestamp(lines) {
106
133
  for (const line of lines) {
107
- const stamp = TIMESTAMP.exec(line);
108
- if (stamp)
109
- return Date.parse(stamp[1]);
134
+ const timestamp = lineTimestamp(line);
135
+ if (timestamp !== null)
136
+ return timestamp;
110
137
  }
111
138
  return null;
112
139
  }
@@ -114,15 +141,21 @@ function firstTimestamp(lines) {
114
141
  * Lines from the active log and as many rotated generations as needed to
115
142
  * cover the window. Throws when a present log cannot be read.
116
143
  */
117
- function readWindowLines(logFile, since) {
144
+ function readWindowLines(logFile, since, daemonLog) {
118
145
  const generations = [];
119
- for (let n = 0; n < LOG_GENERATIONS; n += 1) {
120
- const file = n === 0 ? logFile : `${logFile}.${n}`;
146
+ for (let n = 0; n < SYNC_LOG_GENERATIONS; n += 1) {
147
+ const file = syncLogGenerationPath(logFile, n);
121
148
  if (!fs.existsSync(file))
122
- break;
149
+ continue;
123
150
  const lines = readLogTail(file);
124
- generations.unshift(lines);
125
- const first = firstTimestamp(lines);
151
+ const fallbackTime = daemonLog ? fs.statSync(file).mtimeMs : null;
152
+ const timestampedLines = fallbackTime === null
153
+ ? lines
154
+ : lines.map((line) => lineTimestamp(line) === null
155
+ ? `${new Date(fallbackTime).toISOString()} ${line}`
156
+ : line);
157
+ generations.unshift(timestampedLines);
158
+ const first = firstTimestamp(timestampedLines);
126
159
  if (first !== null && first <= since)
127
160
  break;
128
161
  }
@@ -139,15 +172,32 @@ function readConflictCount(file) {
139
172
  }
140
173
  }
141
174
  /** Read the machine's sync logs and client health file. */
142
- export function defaultSyncRuntimeResults(now, hqHome = path.join(os.homedir(), ".hq")) {
175
+ export function defaultSyncRuntimeResults(now, hqHome = path.join(os.homedir(), ".hq"), env = process.env) {
143
176
  const options = {
144
177
  now: now.getTime(),
145
178
  conflictCount: readConflictCount(path.join(hqHome, "client-health.json")),
146
179
  };
147
- const logFile = path.join(hqHome, "logs", "hq-sync.log");
180
+ const paths = syncLogBasePaths(hqHome, env);
181
+ const familyModifiedMs = (family) => {
182
+ let newest = null;
183
+ for (let generation = 0; generation < SYNC_LOG_GENERATIONS; generation += 1) {
184
+ try {
185
+ const stat = fs.lstatSync(syncLogGenerationPath(paths[family], generation));
186
+ if (!stat.isFile() && !stat.isDirectory())
187
+ continue;
188
+ newest = newest === null ? stat.mtimeMs : Math.max(newest, stat.mtimeMs);
189
+ }
190
+ catch {
191
+ // Missing or inaccessible generations do not make a source family newer.
192
+ }
193
+ }
194
+ return newest;
195
+ };
196
+ const family = currentSyncLogFamily(familyModifiedMs("desktop"), familyModifiedMs("daemon"));
197
+ const logFile = family === null ? paths.desktop : paths[family];
148
198
  let lines;
149
199
  try {
150
- lines = readWindowLines(logFile, options.now - SYNC_RUNTIME_WINDOW_MS);
200
+ lines = readWindowLines(logFile, options.now - SYNC_RUNTIME_WINDOW_MS, family === "daemon");
151
201
  }
152
202
  catch {
153
203
  return [
@@ -4,6 +4,7 @@ import { LanesError } from "./errors.js";
4
4
  import { DEFAULT_BLOCKER_FRESHNESS_HOURS, loadLanesConfig } from "./config.js";
5
5
  import { resolveManagerStatusTarget } from "./manager-status-gate.js";
6
6
  import { readLanesRegistryFlag } from "./registry-flag.js";
7
+ import { resolveLaneCompany } from "./lane-company.js";
7
8
  import { assertNoSecretText } from "./text-scan.js";
8
9
  export const BLOCKER_FRESHNESS_FLAG = "lanes.blocker-freshness";
9
10
  export const BLOCKER_FRESHNESS_DEFAULT_HOURS = DEFAULT_BLOCKER_FRESHNESS_HOURS;
@@ -100,7 +101,7 @@ export async function resolveBlockerFreshnessForLane(root, lane, options = {}) {
100
101
  let flagEnabled;
101
102
  try {
102
103
  flagEnabled = await (options.readFlag ?? readLanesRegistryFlag)(BLOCKER_FRESHNESS_FLAG, {
103
- company: lane.company ?? lane.mesh_company ?? undefined,
104
+ company: resolveLaneCompany(root, lane).company ?? undefined,
104
105
  env: options.env,
105
106
  });
106
107
  }
@@ -206,20 +206,23 @@ export function keepAliveSupersededBy(lane, lanes, lineageAware = LANE_SUPERSESS
206
206
  const companyOf = companyForLane ?? ((candidate) => candidate.company);
207
207
  const laneCompany = companyOf(lane);
208
208
  const lanesById = new Map(lanes.map((candidate) => [candidate.lane_id, candidate]));
209
- const targetAncestorIds = new Set();
210
- if (lineageAware) {
211
- let parentLaneId = lane.senior.kind === "lane" ? lane.senior.id : undefined;
212
- while (parentLaneId) {
213
- if (targetAncestorIds.has(parentLaneId))
214
- break;
215
- targetAncestorIds.add(parentLaneId);
216
- const parent = lanesById.get(parentLaneId);
217
- parentLaneId = parent?.senior.kind === "lane" ? parent.senior.id : undefined;
209
+ lanesById.set(lane.lane_id, lane);
210
+ const sessionOwners = new Map();
211
+ for (const owner of lanesById.values()) {
212
+ for (const sessionId of recordedLaneSessionIds(owner)) {
213
+ const owners = sessionOwners.get(sessionId) ?? new Set();
214
+ owners.add(owner.lane_id);
215
+ sessionOwners.set(sessionId, owners);
218
216
  }
219
217
  }
218
+ const targetAncestorIds = lineageAware
219
+ ? laneAncestorsFrom(lane.senior, lanesById, sessionOwners)
220
+ : new Set();
220
221
  return lanes.find((candidate) => {
221
222
  if (candidate.lane_id === lane.lane_id || Date.parse(candidate.created_at) <= createdAt)
222
223
  return false;
224
+ if (candidate.state === "cancelled" || candidate.state === "killed" || candidate.state === "failed")
225
+ return false;
223
226
  const candidateCompany = companyOf(candidate);
224
227
  const sameCompany = laneCompany === null
225
228
  ? candidateCompany === null
@@ -229,17 +232,8 @@ export function keepAliveSupersededBy(lane, lanes, lineageAware = LANE_SUPERSESS
229
232
  if (lineageAware && targetAncestorIds.has(candidate.lane_id))
230
233
  return false;
231
234
  // Any descendant is part of this lead's work tree, not a replacement lead.
232
- let parentLaneId = candidate.senior.kind === "lane" ? candidate.senior.id : undefined;
233
- const visitedLaneIds = new Set();
234
- while (parentLaneId) {
235
- if (parentLaneId === lane.lane_id)
236
- return false;
237
- if (visitedLaneIds.has(parentLaneId))
238
- break;
239
- visitedLaneIds.add(parentLaneId);
240
- const parent = lanesById.get(parentLaneId);
241
- parentLaneId = parent?.senior.kind === "lane" ? parent.senior.id : undefined;
242
- }
235
+ if (laneAncestorsFrom(candidate.senior, lanesById, sessionOwners).has(lane.lane_id))
236
+ return false;
243
237
  const sameLaneSenior = lane.senior.kind === "lane" &&
244
238
  candidate.senior.kind === "lane" &&
245
239
  candidate.senior.id === lane.senior.id;
@@ -252,6 +246,41 @@ export function keepAliveSupersededBy(lane, lanes, lineageAware = LANE_SUPERSESS
252
246
  return sameProfile || projectLead;
253
247
  });
254
248
  }
249
+ /** Session ids durable lane records still identify as spawn/resume sessions. */
250
+ function recordedLaneSessionIds(lane) {
251
+ return [lane.provider_session_id, lane.last_resume?.session_id].filter((sessionId) => typeof sessionId === "string" && sessionId.trim().length > 0);
252
+ }
253
+ /** Follow both lane seniors and session seniors back to the lane that owns each session. */
254
+ function laneAncestorsFrom(senior, lanesById, sessionOwners) {
255
+ const laneIds = new Set();
256
+ const sessionIds = new Set();
257
+ const pending = [senior];
258
+ while (pending.length > 0) {
259
+ const current = pending.pop();
260
+ if (current === undefined)
261
+ break;
262
+ if (current.kind === "lane") {
263
+ const laneId = current.id;
264
+ if (typeof laneId !== "string" || laneId.trim().length === 0 || laneIds.has(laneId))
265
+ continue;
266
+ laneIds.add(laneId);
267
+ const parent = lanesById.get(laneId);
268
+ if (parent !== undefined)
269
+ pending.push(parent.senior);
270
+ continue;
271
+ }
272
+ if (current.kind !== "session")
273
+ continue;
274
+ const sessionId = current.id;
275
+ if (typeof sessionId !== "string" || sessionId.trim().length === 0 || sessionIds.has(sessionId))
276
+ continue;
277
+ sessionIds.add(sessionId);
278
+ for (const ownerLaneId of sessionOwners.get(sessionId) ?? []) {
279
+ pending.push({ kind: "lane", id: ownerLaneId });
280
+ }
281
+ }
282
+ return laneIds;
283
+ }
255
284
  export function keepAliveExclusion(lane, lanes, retiredLaneIds, lineageAware = LANE_SUPERSESSION_LINEAGE_AWARE_DEFAULT_ENABLED, companyForLane) {
256
285
  if (isRetiredLaneId(lane.lane_id, retiredLaneIds))
257
286
  return "retired";
@@ -5,6 +5,7 @@ import { DEFAULT_KR_FRESHNESS_HOURS, loadLanesConfig } from "./config.js";
5
5
  import { LanesError } from "./errors.js";
6
6
  import { resolveManagerStatusTarget } from "./manager-status-gate.js";
7
7
  import { readLanesRegistryFlag } from "./registry-flag.js";
8
+ import { resolveLaneCompany } from "./lane-company.js";
8
9
  import { assertNoSecretText } from "./text-scan.js";
9
10
  export const KR_FRESHNESS_FLAG = "lanes.kr-freshness";
10
11
  export const KR_FRESHNESS_DEFAULT_HOURS = DEFAULT_KR_FRESHNESS_HOURS;
@@ -327,7 +328,7 @@ export async function resolveKrFreshnessForLane(root, lane, options = {}) {
327
328
  let flagEnabled;
328
329
  try {
329
330
  flagEnabled = await (options.readFlag ?? readLanesRegistryFlag)(KR_FRESHNESS_FLAG, {
330
- company: lane.company ?? lane.mesh_company ?? undefined,
331
+ company: resolveLaneCompany(root, lane).company ?? undefined,
331
332
  env: options.env,
332
333
  });
333
334
  }
@@ -378,7 +379,7 @@ export async function resolveKrFreshnessForShow(root, lane, options = {}) {
378
379
  let enabled;
379
380
  try {
380
381
  enabled = await (options.readFlag ?? readLanesRegistryFlag)(KR_FRESHNESS_FLAG, {
381
- company: lane.company ?? lane.mesh_company ?? undefined,
382
+ company: resolveLaneCompany(root, lane).company ?? undefined,
382
383
  env: options.env,
383
384
  });
384
385
  }
@@ -1,9 +1,10 @@
1
1
  import type { LaneCompanyRef, LaneRecord } from "./types.js";
2
- export type LaneCompanySource = "lane" | "project" | "unresolved";
2
+ export type LaneCompanySource = "lane" | "mesh_company" | "project" | "unresolved";
3
3
  export interface LaneCompanyResolution {
4
4
  company: LaneCompanyRef | null;
5
5
  source: LaneCompanySource;
6
6
  }
7
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;
8
+ export declare function resolveLaneCompany(hqRoot: string, lane: Pick<LaneRecord, "lane_id" | "company" | "mesh_company" | "project_id">): LaneCompanyResolution;
9
+ export declare function laneCompanyUidSource(source: LaneCompanySource): string | undefined;
9
10
  //# sourceMappingURL=lane-company.d.ts.map
@@ -2,10 +2,15 @@ import { resolveProjectQuestionCompany } from "./questions.js";
2
2
  import { LanesError } from "./errors.js";
3
3
  /** Resolve a lane's company without inferring ownership from its worker or caller. */
4
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" };
5
+ const laneCompanySlug = typeof lane.company?.slug === "string" ? lane.company.slug.trim() : "";
6
+ const laneCompanyUid = typeof lane.company?.uid === "string" ? lane.company.uid.trim() : "";
7
+ if (laneCompanySlug && laneCompanyUid) {
8
+ return { company: { slug: laneCompanySlug, uid: laneCompanyUid }, source: "lane" };
9
+ }
10
+ const meshCompanySlug = typeof lane.mesh_company?.slug === "string" ? lane.mesh_company.slug.trim() : "";
11
+ const meshCompanyUid = typeof lane.mesh_company?.uid === "string" ? lane.mesh_company.uid.trim() : "";
12
+ if (meshCompanySlug && meshCompanyUid) {
13
+ return { company: { slug: meshCompanySlug, uid: meshCompanyUid }, source: "mesh_company" };
9
14
  }
10
15
  let projectCompany;
11
16
  try {
@@ -21,4 +26,16 @@ export function resolveLaneCompany(hqRoot, lane) {
21
26
  ? { company: projectCompany, source: "project" }
22
27
  : { company: null, source: "unresolved" };
23
28
  }
29
+ export function laneCompanyUidSource(source) {
30
+ switch (source) {
31
+ case "lane":
32
+ return "lane.company.uid";
33
+ case "mesh_company":
34
+ return "lane.mesh_company.uid";
35
+ case "project":
36
+ return "project.cloud_uid";
37
+ case "unresolved":
38
+ return undefined;
39
+ }
40
+ }
24
41
  //# sourceMappingURL=lane-company.js.map
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { checkCompanyPath } from "./checked-company-path.js";
4
4
  import { LanesError } from "./errors.js";
5
+ import { laneCompanyUidSource, resolveLaneCompany } from "./lane-company.js";
5
6
  import { readLanesRegistryFlag } from "./registry-flag.js";
6
7
  export const MANAGER_STATUS_GATE_FLAG = "lanes.manager-status-gate";
7
8
  function isObject(value) {
@@ -84,7 +85,7 @@ export function resolveManagerStatusTarget(root, lane) {
84
85
  invalidMapping(`Matched juniors row needs a non-empty ${field}`, registry.filePath, { lane_id: lane.lane_id, field });
85
86
  }
86
87
  }
87
- const company = lane.company?.slug ?? lane.mesh_company?.slug ?? "";
88
+ const company = resolveLaneCompany(root, lane).company?.slug ?? "";
88
89
  const statusPath = checkCompanyPath(root, company, row.status, registry.filePath);
89
90
  return {
90
91
  manager: true,
@@ -96,11 +97,12 @@ export async function resolveManagerStatusGateForLane(root, lane, options = {})
96
97
  const reportError = options.onError ?? ((err) => logManagerStatusGateFailure(lane, err));
97
98
  let enabled;
98
99
  try {
99
- const company = lane.company ?? lane.mesh_company ?? undefined;
100
+ const resolution = resolveLaneCompany(root, lane);
101
+ const company = resolution.company ?? undefined;
100
102
  enabled = await (options.readFlag ?? readLanesRegistryFlag)(MANAGER_STATUS_GATE_FLAG, {
101
103
  company,
102
104
  ...(company ? {
103
- companyUidSource: lane.company ? "lane.company.uid" : "lane.mesh_company.uid",
105
+ companyUidSource: laneCompanyUidSource(resolution.source),
104
106
  } : {}),
105
107
  env: options.env,
106
108
  });
@@ -0,0 +1,14 @@
1
+ export interface MergeCommandTarget {
2
+ kind: "pr" | "api" | "graphql" | "git";
3
+ repo: string | null;
4
+ pr: number | null;
5
+ pinnedHead: string | null;
6
+ unresolvedReason: string | null;
7
+ }
8
+ export interface MergeCommandParseResult {
9
+ targets: MergeCommandTarget[];
10
+ unresolved: boolean;
11
+ }
12
+ export declare function canonicalRepo(raw: string): string;
13
+ export declare function parseMergeCommands(command: string): MergeCommandParseResult;
14
+ //# sourceMappingURL=merge-command.d.ts.map