@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.
- package/CHANGELOG.md +36 -0
- package/dist/command-catalog.generated.d.ts +77 -4
- package/dist/command-catalog.generated.js +98 -4
- package/dist/commands/files.js +6 -3
- package/dist/commands/lanes-hold.d.ts +2 -0
- package/dist/commands/lanes-hold.js +122 -310
- package/dist/commands/lanes-message.js +6 -4
- package/dist/commands/lanes-review.d.ts +10 -0
- package/dist/commands/lanes-review.js +56 -0
- package/dist/commands/lanes-shared.d.ts +1 -0
- package/dist/commands/lanes.d.ts +2 -0
- package/dist/commands/lanes.js +29 -4
- package/dist/lib/billing/plan-limit-upgrade-url.d.ts +1 -4
- package/dist/lib/billing/plan-limit-upgrade-url.js +7 -18
- package/dist/lib/doctor/checks/sync-runtime.d.ts +1 -1
- package/dist/lib/doctor/checks/sync-runtime.js +69 -19
- package/dist/lib/lanes/blocker-freshness.js +2 -1
- package/dist/lib/lanes/keep-alive.js +49 -20
- package/dist/lib/lanes/kr-freshness.js +3 -2
- package/dist/lib/lanes/lane-company.d.ts +3 -2
- package/dist/lib/lanes/lane-company.js +21 -4
- package/dist/lib/lanes/manager-status-gate.js +5 -3
- package/dist/lib/lanes/merge-command.d.ts +14 -0
- package/dist/lib/lanes/merge-command.js +363 -0
- package/dist/lib/lanes/reviews.d.ts +38 -0
- package/dist/lib/lanes/reviews.js +395 -0
- package/dist/lib/lanes/watcher.js +4 -2
- package/dist/lib/plan-limit-nag.js +2 -4
- package/dist/utils/feedback-logs.d.ts +14 -9
- package/dist/utils/feedback-logs.js +43 -14
- package/dist/utils/local-files-overview.d.ts +4 -2
- package/dist/utils/local-files-overview.js +47 -6
- package/dist/utils/sync-log-sources.d.ts +15 -0
- package/dist/utils/sync-log-sources.js +36 -0
- package/dist/utils/version-gate.d.ts +1 -0
- package/dist/utils/version-gate.js +6 -1
- package/package.json +1 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -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
|
|
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
|
|
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", "
|
|
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
|
-
/**
|
|
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 (
|
|
24
|
-
selected = choices.legacyUrl;
|
|
20
|
+
if (companySlug?.trim()) {
|
|
21
|
+
selected = generatedUrl ?? topLevelUrl ?? nestedUrl ?? choices.legacyUrl;
|
|
25
22
|
}
|
|
26
23
|
else {
|
|
27
|
-
|
|
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
|
-
/**
|
|
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
|
|
44
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
108
|
-
if (
|
|
109
|
-
return
|
|
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 <
|
|
120
|
-
const file =
|
|
146
|
+
for (let n = 0; n < SYNC_LOG_GENERATIONS; n += 1) {
|
|
147
|
+
const file = syncLogGenerationPath(logFile, n);
|
|
121
148
|
if (!fs.existsSync(file))
|
|
122
|
-
|
|
149
|
+
continue;
|
|
123
150
|
const lines = readLogTail(file);
|
|
124
|
-
|
|
125
|
-
const
|
|
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
|
|
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 ??
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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 ??
|
|
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 ??
|
|
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
|
|
6
|
-
const
|
|
7
|
-
if (
|
|
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 ??
|
|
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
|
|
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:
|
|
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
|