@indigoai-us/hq-cli 5.273.0-alpha.0 → 5.273.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 +25 -2
- package/dist/command-catalog.generated.d.ts +11 -536
- package/dist/command-catalog.generated.js +14 -691
- package/dist/commands/daemon.d.ts +0 -3
- package/dist/commands/daemon.js +1 -173
- package/dist/commands/lanes-checks.d.ts +0 -1
- package/dist/commands/lanes-checks.js +217 -54
- package/dist/commands/lanes-hold.js +40 -2
- package/dist/commands/lanes-message.js +98 -14
- package/dist/commands/lanes-shared.d.ts +3 -2
- package/dist/commands/lanes.d.ts +2 -4
- package/dist/commands/lanes.js +3 -23
- package/dist/commands/sync-mode.d.ts +0 -11
- package/dist/commands/sync-mode.js +0 -30
- package/dist/lib/daemon/catalog.d.ts +2 -18
- package/dist/lib/daemon/catalog.js +2 -18
- package/dist/lib/daemon/config.d.ts +0 -6
- package/dist/lib/daemon/config.js +2 -15
- package/dist/lib/daemon/requests.d.ts +0 -4
- package/dist/lib/daemon/requests.js +0 -2
- package/dist/lib/daemon/run.js +19 -78
- package/dist/lib/daemon/supervisor.d.ts +0 -8
- package/dist/lib/daemon/supervisor.js +1 -101
- package/dist/lib/doctor/checks/scaffold-cli-floor.d.ts +7 -0
- package/dist/lib/doctor/checks/scaffold-cli-floor.js +73 -0
- package/dist/lib/doctor/registry.js +2 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/hire-grant-dispatch.d.ts +6 -1
- package/dist/lib/lanes/hire-grant-dispatch.js +78 -3
- package/dist/lib/lanes/keep-alive.d.ts +3 -2
- package/dist/lib/lanes/keep-alive.js +2 -2
- package/dist/lib/lanes/merge-command.d.ts +1 -0
- package/dist/lib/lanes/merge-command.js +299 -33
- package/dist/lib/lanes/orphans.d.ts +7 -0
- package/dist/lib/lanes/orphans.js +39 -4
- package/dist/lib/lanes/tracker-store.d.ts +2 -18
- package/dist/lib/lanes/tracker-store.js +1 -172
- package/dist/lib/scaffold-cli-floor.d.ts +9 -0
- package/dist/lib/scaffold-cli-floor.js +50 -0
- package/package.json +2 -2
- package/dist/commands/lanes-import.d.ts +0 -14
- package/dist/commands/lanes-import.js +0 -1210
- package/dist/commands/lanes-tracker.d.ts +0 -18
- package/dist/commands/lanes-tracker.js +0 -452
- package/dist/lib/daemon/sync-runner.d.ts +0 -58
- package/dist/lib/daemon/sync-runner.js +0 -277
|
@@ -3,38 +3,115 @@ import * as path from "node:path";
|
|
|
3
3
|
import { appendCappedHistory, appendDropboxMessage, deliveryRefusalReason, dropboxInboxPath, laneAcceptsOperatorMessage, laneDeliveryMechanism, operatorMessageRefusalReason, providerSessionPortable, providerSupportsSessionResume, readLane, sessionNotPortableMessage, validateProjectStories, withLaneAdmissionLock } from "../lib/lanes/index.js";
|
|
4
4
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
5
5
|
import { parseCliEffort } from "../lib/lanes/effort.js";
|
|
6
|
-
import { parseOutpostHireGrantContext } from "../lib/lanes/hire-grant-dispatch.js";
|
|
6
|
+
import { parseOutpostHireGrantContext, sameHireGrantSenior } from "../lib/lanes/hire-grant-dispatch.js";
|
|
7
7
|
import { dispatchLaneDelivery } from "../lib/lanes/delivery-core.js";
|
|
8
8
|
import { dropboxInboxHasMessages, drainDropbox, formatDropboxMessages, formatMonitorEvent, requeueDropboxMessages } from "../lib/lanes/dropbox.js";
|
|
9
9
|
import { isPidAlive, runWatcherLoop, WATCHER_OUT_MAX_BYTES, watcherOutLogPath, writeWatcherDecision } from "../lib/lanes/watcher.js";
|
|
10
10
|
import { laneStories, laneStory, storyWarning } from "../lib/lanes/stories.js";
|
|
11
11
|
import { laneRunDir, probePidRecord, readPidRecord, readProcStarttime, spawnLaneWorker } from "../lib/lanes/spawn.js";
|
|
12
|
-
import { keepAliveExclusion, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds } from "../lib/lanes/keep-alive.js";
|
|
12
|
+
import { keepAliveExclusion, keepAliveSupersededBy, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds } from "../lib/lanes/keep-alive.js";
|
|
13
13
|
import { managerStatusGateEnabledForLane } from "../lib/lanes/manager-status-gate.js";
|
|
14
14
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
15
15
|
import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
|
|
16
16
|
import { accountMoveSummary, accountRequestKeepsCurrentLane, assertCurrentResumeAccountAvailable, recordedResumeEffort, resetKeepAliveProgress, resolveResumeEffort } from "./lanes-shared.js";
|
|
17
17
|
export function createLaneMessageHandlers(deps) {
|
|
18
18
|
const { archiveResumeMessage, assertLaneRunsHereForAdmission, assertLaneRunsHereForOperatorMutation, assessLaneAdmission, casUpdateLane, dispatchOutpostLaneCommand, drainHookStatusFor, laneIsHostedHere, laneWithRecoveredProviderSession, loadAllLanes, mintMessageId, persistClaudeAccountSelection, prepareAccountMove, printJson, recordResumeSetup, resolveActiveQuestionCompany, resolveResumeProfile, selectAutomaticClaudeResumeAccount, validateResumeProfileModel, withResumeHireAdmission } = deps;
|
|
19
|
-
|
|
19
|
+
function currentSuccessorOf(lane, lanes, lineageAware, companyForLane) {
|
|
20
|
+
const validLanes = lanes.filter((candidate) => Number.isFinite(Date.parse(candidate.created_at)));
|
|
21
|
+
const newestFirst = [...validLanes].sort((left, right) => Date.parse(right.created_at) - Date.parse(left.created_at));
|
|
22
|
+
const visited = new Set([lane.lane_id]);
|
|
23
|
+
let current = lane;
|
|
24
|
+
for (let depth = 0; depth < validLanes.length; depth += 1) {
|
|
25
|
+
const next = keepAliveSupersededBy(current, newestFirst, lineageAware, companyForLane);
|
|
26
|
+
if (!next || visited.has(next.lane_id))
|
|
27
|
+
break;
|
|
28
|
+
current = next;
|
|
29
|
+
visited.add(next.lane_id);
|
|
30
|
+
}
|
|
31
|
+
return current.lane_id === lane.lane_id ? undefined : current;
|
|
32
|
+
}
|
|
33
|
+
function callerMayMessageSupersededLane(ctx, lane, lanes, lineageAware, companyForLane, callerLaneIdOverride) {
|
|
34
|
+
if (lane.senior.kind !== "lane")
|
|
35
|
+
return false;
|
|
36
|
+
const callerLaneId = callerLaneIdOverride ?? ctx.env.HQ_LANE_ID?.trim();
|
|
37
|
+
if (!callerLaneId)
|
|
38
|
+
return false;
|
|
39
|
+
const senior = lanes.find((candidate) => candidate.lane_id === lane.senior.id);
|
|
40
|
+
const caller = lanes.find((candidate) => candidate.lane_id === callerLaneId);
|
|
41
|
+
if (!senior || !caller)
|
|
42
|
+
return false;
|
|
43
|
+
const workerCompany = companyForLane(lane);
|
|
44
|
+
const seniorCompany = companyForLane(senior);
|
|
45
|
+
const callerCompany = companyForLane(caller);
|
|
46
|
+
const projectId = lane.project_id;
|
|
47
|
+
if (!workerCompany ||
|
|
48
|
+
seniorCompany?.uid !== workerCompany.uid ||
|
|
49
|
+
callerCompany?.uid !== workerCompany.uid ||
|
|
50
|
+
!projectId ||
|
|
51
|
+
senior.project_id !== projectId ||
|
|
52
|
+
caller.project_id !== projectId)
|
|
53
|
+
return false;
|
|
54
|
+
if (caller.lane_id === senior.lane_id)
|
|
55
|
+
return true;
|
|
56
|
+
const projectLanes = lanes.filter((candidate) => companyForLane(candidate)?.uid === workerCompany.uid && candidate.project_id === projectId);
|
|
57
|
+
return currentSuccessorOf(lane, projectLanes, lineageAware, companyForLane)?.lane_id === caller.lane_id;
|
|
58
|
+
}
|
|
59
|
+
function keepAliveLaneFromSnapshot(snapshot) {
|
|
60
|
+
if (snapshot.created_at === undefined || !Object.hasOwn(snapshot, "company"))
|
|
61
|
+
return undefined;
|
|
62
|
+
return {
|
|
63
|
+
lane_id: snapshot.lane_id,
|
|
64
|
+
created_at: snapshot.created_at,
|
|
65
|
+
senior: snapshot.senior,
|
|
66
|
+
company: snapshot.company ?? null,
|
|
67
|
+
project_id: snapshot.project_id ?? "",
|
|
68
|
+
...(snapshot.worker_profile === undefined ? {} : { worker_profile: snapshot.worker_profile }),
|
|
69
|
+
...(snapshot.hire_grant === undefined ? {} : { hire_grant: snapshot.hire_grant }),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async function messageLaneExclusion(ctx, lane, grantContext) {
|
|
20
73
|
if (lane.state === "killed" || lane.state === "cancelled") {
|
|
21
74
|
return { lane, exclusion: lane.state };
|
|
22
75
|
}
|
|
23
|
-
const
|
|
76
|
+
const companyForLane = (candidate) => resolveLaneCompany(ctx.hqRoot(), candidate).company;
|
|
24
77
|
const lineageAware = await readLanesRegistryFlag(LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, {
|
|
25
|
-
company:
|
|
78
|
+
company: companyForLane(lane),
|
|
26
79
|
env: ctx.env,
|
|
27
80
|
});
|
|
28
|
-
const currentLane = readLane(hqRoot, lane.lane_id);
|
|
81
|
+
const currentLane = readLane(ctx.hqRoot(), lane.lane_id);
|
|
29
82
|
if (currentLane.state === "killed" || currentLane.state === "cancelled") {
|
|
30
83
|
return { lane: currentLane, exclusion: currentLane.state };
|
|
31
84
|
}
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
85
|
+
const localLanes = loadAllLanes(ctx.hqRoot());
|
|
86
|
+
let lanes = localLanes;
|
|
87
|
+
const retiredLaneIds = loadRetiredLaneIds(ctx.hqRoot());
|
|
88
|
+
const dispatchedCallerId = grantContext?.superseded_message_caller_id;
|
|
89
|
+
const dispatchedContextAuthorized = grantContext !== undefined &&
|
|
90
|
+
dispatchedCallerId !== undefined &&
|
|
91
|
+
ctx.isSsmOwnerDispatch() &&
|
|
92
|
+
sameHireGrantSenior(grantContext.expected_senior, currentLane.senior);
|
|
93
|
+
if (dispatchedContextAuthorized && grantContext) {
|
|
94
|
+
const merged = new Map(localLanes.map((record) => [record.lane_id, record]));
|
|
95
|
+
for (const snapshot of grantContext.hierarchy) {
|
|
96
|
+
const contextLane = keepAliveLaneFromSnapshot(snapshot);
|
|
97
|
+
if (contextLane)
|
|
98
|
+
merged.set(contextLane.lane_id, contextLane);
|
|
99
|
+
}
|
|
100
|
+
merged.set(currentLane.lane_id, currentLane);
|
|
101
|
+
lanes = [...merged.values()];
|
|
102
|
+
}
|
|
103
|
+
const exclusion = keepAliveExclusion(currentLane, lanes, retiredLaneIds, lineageAware, companyForLane);
|
|
104
|
+
const callerLaneId = dispatchedContextAuthorized ? dispatchedCallerId : undefined;
|
|
105
|
+
if (exclusion === "superseded" && callerMayMessageSupersededLane(ctx, currentLane, lanes, lineageAware, companyForLane, callerLaneId)) {
|
|
106
|
+
return {
|
|
107
|
+
lane: currentLane,
|
|
108
|
+
exclusion: undefined,
|
|
109
|
+
...(!dispatchedContextAuthorized && ctx.env.HQ_LANE_ID?.trim()
|
|
110
|
+
? { supersededMessageCallerId: ctx.env.HQ_LANE_ID.trim() }
|
|
111
|
+
: {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { lane: currentLane, exclusion };
|
|
38
115
|
}
|
|
39
116
|
function recordMessageUndeliverable(ctx, lane, reason, messageCount) {
|
|
40
117
|
writeWatcherDecision(watcherOutLogPath(ctx.hqRoot(), lane.lane_id), {
|
|
@@ -68,7 +145,14 @@ export function createLaneMessageHandlers(deps) {
|
|
|
68
145
|
const resolution = await deps.resolveCommandLane(ctx, laneId);
|
|
69
146
|
let lane = resolution.lane;
|
|
70
147
|
if (resolution.remoteOnly || (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane))) {
|
|
71
|
-
|
|
148
|
+
const callerLaneId = ctx.env.HQ_LANE_ID?.trim();
|
|
149
|
+
const supersededMessageCallerId = callerLaneId === undefined || resolution.remoteOnly
|
|
150
|
+
? undefined
|
|
151
|
+
: (await messageLaneExclusion(ctx, lane)).supersededMessageCallerId;
|
|
152
|
+
await dispatchOutpostLaneCommand(ctx, lane, "message", {
|
|
153
|
+
...opts,
|
|
154
|
+
...(supersededMessageCallerId === undefined ? {} : { supersededMessageCallerId }),
|
|
155
|
+
}, resolution.remoteOnly
|
|
72
156
|
? { syncPrimary: false, expectedOwnerHost: resolution.ownerHost }
|
|
73
157
|
: undefined);
|
|
74
158
|
return;
|
|
@@ -115,7 +199,7 @@ export function createLaneMessageHandlers(deps) {
|
|
|
115
199
|
}
|
|
116
200
|
}
|
|
117
201
|
try {
|
|
118
|
-
const eligibility = await messageLaneExclusion(ctx, lane);
|
|
202
|
+
const eligibility = await messageLaneExclusion(ctx, lane, hireGrantContext);
|
|
119
203
|
lane = eligibility.lane;
|
|
120
204
|
const exclusion = eligibility.exclusion;
|
|
121
205
|
if (exclusion !== undefined) {
|
|
@@ -41,6 +41,8 @@ export interface LaneMessageOptions {
|
|
|
41
41
|
effort?: string;
|
|
42
42
|
worker?: string;
|
|
43
43
|
grantContext?: string;
|
|
44
|
+
/** Primary-verified caller identity for a superseded Outpost message. */
|
|
45
|
+
supersededMessageCallerId?: string;
|
|
44
46
|
monitorEvent?: {
|
|
45
47
|
monitor_id: string;
|
|
46
48
|
description: string;
|
|
@@ -237,9 +239,8 @@ export interface Ctx {
|
|
|
237
239
|
storyLedgerEnabled: () => boolean;
|
|
238
240
|
isSsmOwnerDispatch: () => boolean;
|
|
239
241
|
withGrantHireLocks: typeof withLaneHireLocks;
|
|
240
|
-
|
|
242
|
+
mergeHoldEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
241
243
|
mergeReviewEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
242
|
-
trackerActor: () => string | null;
|
|
243
244
|
}
|
|
244
245
|
export interface LaneWaitCommandOptions {
|
|
245
246
|
any?: string[];
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -90,12 +90,10 @@ export interface LanesCommandDeps {
|
|
|
90
90
|
isSsmOwnerDispatch?: () => boolean;
|
|
91
91
|
/** Test seam for changing a lane between pre-lock and locked grant reads. */
|
|
92
92
|
withGrantHireLocks?: typeof withLaneHireLocks;
|
|
93
|
-
/** Test seam for the hq-flags gated tracker commands. */
|
|
94
|
-
laneTrackerEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
95
93
|
/** Test seam for the review-before-merge registry flag. */
|
|
96
94
|
mergeReviewEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
97
|
-
/** Test seam for
|
|
98
|
-
|
|
95
|
+
/** Test seam for the repository-merge-hold registry flag. */
|
|
96
|
+
mergeHoldEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
99
97
|
}
|
|
100
98
|
export declare const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
101
99
|
export declare function parseSeniorRef(raw: string): SeniorRef;
|
package/dist/commands/lanes.js
CHANGED
|
@@ -21,7 +21,6 @@ import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
|
|
|
21
21
|
import { registerLaneKpisCommand } from "./lanes-kpis.js";
|
|
22
22
|
import { registerLaneChecksCommand } from "./lanes-checks.js";
|
|
23
23
|
import { registerLaneReportCommands } from "./lanes-report.js";
|
|
24
|
-
import { registerLaneTrackerImportCommand } from "./lanes-import.js";
|
|
25
24
|
import { registerLaneRepoMergeHoldCommand } from "./lanes-hold.js";
|
|
26
25
|
import { registerLaneReviewCommand } from "./lanes-review.js";
|
|
27
26
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
@@ -29,7 +28,6 @@ import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
|
29
28
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
30
29
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
31
30
|
import { resolveProcessFlagCompanyContext } from "../lib/flag-registry.js";
|
|
32
|
-
import { defaultTrackerActor, LANES_TRACKER_FLAG, registerLaneTrackerCommands, } from "./lanes-tracker.js";
|
|
33
31
|
import { createOutpostHireGrantContext, parseOutpostHireGrantContext, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
|
|
34
32
|
import { createOutpostProfileRepinContext, parseOutpostProfileRepinContext, } from "../lib/lanes/outpost-profile-repin.js";
|
|
35
33
|
import { createOutpostRegistryWorkerApprovalContext, parseOutpostRegistryWorkerApprovalContext, } from "../lib/lanes/outpost-registry-worker-approval.js";
|
|
@@ -109,9 +107,6 @@ async function readLanePlacementEnabled(ctx, company) {
|
|
|
109
107
|
return false;
|
|
110
108
|
}
|
|
111
109
|
}
|
|
112
|
-
function defaultLaneTrackerEnabled(company) {
|
|
113
|
-
return readLanesRegistryFlag(LANES_TRACKER_FLAG, { company });
|
|
114
|
-
}
|
|
115
110
|
function processLanePlacementCompany(ctx) {
|
|
116
111
|
const context = resolveProcessFlagCompanyContext(ctx.env);
|
|
117
112
|
if (!context.companySlug)
|
|
@@ -328,9 +323,8 @@ function resolveDeps(deps = {}) {
|
|
|
328
323
|
storyLedgerEnabled: deps.storyLedgerEnabled ?? laneStoryLedgerEnabled,
|
|
329
324
|
isSsmOwnerDispatch: deps.isSsmOwnerDispatch ?? (() => isSsmRunCommandProcess()),
|
|
330
325
|
withGrantHireLocks: deps.withGrantHireLocks ?? withLaneHireLocks,
|
|
331
|
-
laneTrackerEnabled: deps.laneTrackerEnabled ?? defaultLaneTrackerEnabled,
|
|
332
326
|
mergeReviewEnabled: deps.mergeReviewEnabled,
|
|
333
|
-
|
|
327
|
+
mergeHoldEnabled: deps.mergeHoldEnabled,
|
|
334
328
|
};
|
|
335
329
|
}
|
|
336
330
|
function livenessDepsFromCtx(ctx) {
|
|
@@ -3497,7 +3491,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
|
|
|
3497
3491
|
}
|
|
3498
3492
|
else if ((action === "resume" || action === "message") &&
|
|
3499
3493
|
lane.senior.kind === "lane") {
|
|
3500
|
-
grantContext = createOutpostHireGrantContext(lane, loadAllLanes(ctx.hqRoot()));
|
|
3494
|
+
grantContext = createOutpostHireGrantContext(lane, loadAllLanes(ctx.hqRoot()), action === "message" ? options.supersededMessageCallerId : undefined);
|
|
3501
3495
|
}
|
|
3502
3496
|
let dispatched;
|
|
3503
3497
|
let grantContextRejected = false;
|
|
@@ -7083,23 +7077,9 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
7083
7077
|
const lanes = parent
|
|
7084
7078
|
.command("lanes", { hidden: true })
|
|
7085
7079
|
.description("Create, supervise, and steer worker lanes; routed decision queue");
|
|
7086
|
-
registerLaneTrackerCommands(lanes, {
|
|
7087
|
-
hqRoot: ctx.hqRoot,
|
|
7088
|
-
resolveCompany: ctx.resolveCompany,
|
|
7089
|
-
trackerEnabled: ctx.laneTrackerEnabled,
|
|
7090
|
-
now: ctx.now,
|
|
7091
|
-
actor: ctx.trackerActor,
|
|
7092
|
-
}, wrap);
|
|
7093
|
-
registerLaneTrackerImportCommand(lanes, {
|
|
7094
|
-
hqRoot: ctx.hqRoot,
|
|
7095
|
-
resolveCompany: ctx.resolveCompany,
|
|
7096
|
-
trackerEnabled: ctx.laneTrackerEnabled,
|
|
7097
|
-
now: ctx.now,
|
|
7098
|
-
}, wrap);
|
|
7099
7080
|
registerLaneChecksCommand(lanes, {
|
|
7100
7081
|
hqRoot: ctx.hqRoot,
|
|
7101
7082
|
resolveCompany: ctx.resolveCompany,
|
|
7102
|
-
trackerEnabled: ctx.laneTrackerEnabled,
|
|
7103
7083
|
now: ctx.now,
|
|
7104
7084
|
stopLane: (laneId, reason) => runStop(ctx, laneId, reason, true),
|
|
7105
7085
|
sessionState: (root, sessionId, now) => sessionLiveness(root, sessionId, {
|
|
@@ -7111,7 +7091,6 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
7111
7091
|
registerLaneReportCommands(lanes, {
|
|
7112
7092
|
hqRoot: ctx.hqRoot,
|
|
7113
7093
|
resolveCompany: ctx.resolveCompany,
|
|
7114
|
-
trackerEnabled: ctx.laneTrackerEnabled,
|
|
7115
7094
|
now: ctx.now,
|
|
7116
7095
|
readQuestionBody: (root, question) => {
|
|
7117
7096
|
const body = questionHandlers.requireQuestionBody(root, question);
|
|
@@ -7124,6 +7103,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
7124
7103
|
resolveCompany: ctx.resolveCompany,
|
|
7125
7104
|
now: ctx.now,
|
|
7126
7105
|
env: ctx.env,
|
|
7106
|
+
mergeHoldEnabled: ctx.mergeHoldEnabled,
|
|
7127
7107
|
mergeReviewEnabled: ctx.mergeReviewEnabled,
|
|
7128
7108
|
}, wrap);
|
|
7129
7109
|
registerLaneReviewCommand(lanes, {
|
|
@@ -70,15 +70,6 @@ export interface SetSyncModeResult {
|
|
|
70
70
|
export interface ShowSyncModesOptions {
|
|
71
71
|
vaultClient: SyncModeVaultClient;
|
|
72
72
|
}
|
|
73
|
-
export interface GetSyncModeOptions {
|
|
74
|
-
companySlug: string;
|
|
75
|
-
vaultClient: SyncModeVaultClient;
|
|
76
|
-
}
|
|
77
|
-
export interface GetSyncModeResult {
|
|
78
|
-
membershipId: string;
|
|
79
|
-
companySlug: string;
|
|
80
|
-
config: MembershipSyncConfig;
|
|
81
|
-
}
|
|
82
73
|
export interface ShowSyncModesRow {
|
|
83
74
|
companySlug: string;
|
|
84
75
|
companyName?: string;
|
|
@@ -109,8 +100,6 @@ export declare function setSyncMode(options: SetSyncModeOptions): Promise<SetSyn
|
|
|
109
100
|
* stable table output.
|
|
110
101
|
*/
|
|
111
102
|
export declare function showSyncModes(options: ShowSyncModesOptions): Promise<ShowSyncModesRow[]>;
|
|
112
|
-
/** Read one company's effective sync mode without hiding API read failures. */
|
|
113
|
-
export declare function getSyncModeForCompany(options: GetSyncModeOptions): Promise<GetSyncModeResult>;
|
|
114
103
|
/**
|
|
115
104
|
* Render the `--show` table as plain text. No external dep — hq-cli doesn't
|
|
116
105
|
* use cli-table3, so we hand-format columns to match the existing chalk +
|
|
@@ -163,36 +163,6 @@ export async function showSyncModes(options) {
|
|
|
163
163
|
rows.sort((a, b) => a.companySlug.localeCompare(b.companySlug));
|
|
164
164
|
return rows;
|
|
165
165
|
}
|
|
166
|
-
/** Read one company's effective sync mode without hiding API read failures. */
|
|
167
|
-
export async function getSyncModeForCompany(options) {
|
|
168
|
-
const { companySlug, vaultClient } = options;
|
|
169
|
-
const memberships = await vaultClient.listMyMemberships();
|
|
170
|
-
const enriched = await Promise.all(memberships.map(async (membership) => {
|
|
171
|
-
try {
|
|
172
|
-
const entity = await vaultClient.entity.get(membership.companyUid);
|
|
173
|
-
return { ok: true, membership, slug: entity.slug };
|
|
174
|
-
}
|
|
175
|
-
catch (error) {
|
|
176
|
-
return { ok: false, membership, error };
|
|
177
|
-
}
|
|
178
|
-
}));
|
|
179
|
-
const match = enriched.find((row) => row.ok && row.slug === companySlug);
|
|
180
|
-
if (!match) {
|
|
181
|
-
const failedLookup = enriched.find((row) => !row.ok);
|
|
182
|
-
if (failedLookup && !failedLookup.ok)
|
|
183
|
-
throw failedLookup.error;
|
|
184
|
-
throw new Error(`No membership found for company '${companySlug}'.`);
|
|
185
|
-
}
|
|
186
|
-
for (const failedLookup of enriched) {
|
|
187
|
-
if (!failedLookup.ok) {
|
|
188
|
-
const detail = failedLookup.error instanceof Error ? failedLookup.error.message : String(failedLookup.error);
|
|
189
|
-
console.error(`getSyncModeForCompany: entity lookup failed for membership ${failedLookup.membership.membershipKey} while resolving '${companySlug}': ${detail}`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
const membershipId = match.membership.membershipKey;
|
|
193
|
-
const config = await vaultClient.getMembershipSyncConfig(membershipId);
|
|
194
|
-
return { membershipId, companySlug, config };
|
|
195
|
-
}
|
|
196
166
|
// ── Table rendering (used by CLI action, separable for tests) ──────────────
|
|
197
167
|
/**
|
|
198
168
|
* Render the `--show` table as plain text. No external dep — hq-cli doesn't
|
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
* them instead of silently dropping them.
|
|
11
11
|
*/
|
|
12
12
|
import type { MachineType } from "./machine-type.js";
|
|
13
|
-
import type { SyncOwner } from "./config.js";
|
|
14
|
-
import { type SyncRunnerProcess } from "./sync-runner.js";
|
|
15
13
|
export declare const SERVICE_NAMES: readonly ["sync", "mesh", "heartbeat", "inbox", "bots", "remote-control", "codex-remote-control", "session-host", "lanes-reconcile", "search-index", "jobs-reconcile", "updater", "housekeeping"];
|
|
16
14
|
export type ServiceName = (typeof SERVICE_NAMES)[number];
|
|
17
15
|
export declare function isServiceName(value: unknown): value is ServiceName;
|
|
@@ -53,8 +51,6 @@ export interface UnitPlan {
|
|
|
53
51
|
restart: "always" | "on-failure";
|
|
54
52
|
intervalSec?: number;
|
|
55
53
|
initialDelaySec?: number;
|
|
56
|
-
/** Kill and restart this child after silence on its liveness stream. */
|
|
57
|
-
heartbeatTimeoutMs?: number;
|
|
58
54
|
command: UnitCommand | null;
|
|
59
55
|
env: Record<string, string>;
|
|
60
56
|
blocked?: UnitBlock;
|
|
@@ -72,23 +68,11 @@ export interface PlanContext {
|
|
|
72
68
|
sessionHostRunnerPath: string | null;
|
|
73
69
|
/** Environment retained from legacy outpost sync systemd drop-ins. */
|
|
74
70
|
legacySyncEnv: Record<string, string>;
|
|
75
|
-
/** Keep the local sync runner stopped until the operator resumes it. */
|
|
76
|
-
syncPaused: boolean;
|
|
77
|
-
/** Persisted/effective owner selected for this machine. */
|
|
78
|
-
syncOwner: SyncOwner;
|
|
79
|
-
/** A legacy desktop app or sync unit is still managing sync. */
|
|
80
|
-
desktopAppManagesSync: boolean;
|
|
81
|
-
/** Full process list, or null when the daemon could not read it. */
|
|
82
|
-
syncRunnerProcesses: readonly SyncRunnerProcess[] | null;
|
|
83
|
-
/** PID recorded in the watch-owner status, when it is readable. */
|
|
84
|
-
syncRunnerOwnerPid: number | null;
|
|
85
|
-
/** Sync runner children already started by this daemon before their lease is written. */
|
|
86
|
-
daemonSyncRunnerPids?: ReadonlySet<number>;
|
|
87
|
-
/** The protocol-event heartbeat watchdog timeout for the sync runner. */
|
|
88
|
-
syncHeartbeatTimeoutMs: number;
|
|
89
71
|
signedIn: boolean;
|
|
90
72
|
/** ~/.hq-agent/kit.json exists. */
|
|
91
73
|
kitConfigured: boolean;
|
|
74
|
+
/** The HQ desktop app runs its own sync on this machine. */
|
|
75
|
+
desktopAppManagesSync: boolean;
|
|
92
76
|
bots: readonly string[];
|
|
93
77
|
/** Name of a pre-daemon unit that still runs this unit id, or null. */
|
|
94
78
|
legacyOwner: (unitId: string) => string | null;
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* with no entrypoint yet stay in the catalog as `unavailable` so status shows
|
|
10
10
|
* them instead of silently dropping them.
|
|
11
11
|
*/
|
|
12
|
-
import { SYNC_RUNNER_OWNER, syncRunnerUnleasedBlockReason } from "./sync-runner.js";
|
|
13
12
|
export const SERVICE_NAMES = [
|
|
14
13
|
"sync",
|
|
15
14
|
"mesh",
|
|
@@ -128,29 +127,14 @@ function planService(service, ctx) {
|
|
|
128
127
|
command: {
|
|
129
128
|
type: "node",
|
|
130
129
|
script: ctx.syncRunnerPath,
|
|
131
|
-
args: ["--companies", "--direction", "both", "--on-conflict", "keep", "--hq-root", ctx.hqRoot ?? "", "--watch", "--event-push"
|
|
130
|
+
args: ["--companies", "--direction", "both", "--on-conflict", "keep", "--hq-root", ctx.hqRoot ?? "", "--watch", "--event-push"],
|
|
132
131
|
},
|
|
133
132
|
env: { ...ctx.legacySyncEnv },
|
|
134
|
-
heartbeatTimeoutMs: ctx.syncHeartbeatTimeoutMs,
|
|
135
133
|
});
|
|
136
|
-
let ownerBlock;
|
|
137
|
-
if (ctx.machineType === "desktop" && ctx.syncOwner === "desktop-app" && ctx.desktopAppManagesSync) {
|
|
138
|
-
ownerBlock = "the desktop app is still managing sync (run `hq daemon sync take-ownership` after it stops its runner)";
|
|
139
|
-
}
|
|
140
|
-
else if (ctx.machineType === "desktop" &&
|
|
141
|
-
ctx.syncOwner === "daemon" &&
|
|
142
|
-
!ctx.syncPaused &&
|
|
143
|
-
ctx.hqRoot &&
|
|
144
|
-
ctx.signedIn) {
|
|
145
|
-
ownerBlock = ctx.syncRunnerProcesses === null
|
|
146
|
-
? "could not check for an existing sync runner; waiting to avoid starting a duplicate"
|
|
147
|
-
: syncRunnerUnleasedBlockReason(ctx.hqRoot, ctx.syncRunnerProcesses, ctx.syncRunnerOwnerPid, ctx.platform, ctx.daemonSyncRunnerPids);
|
|
148
|
-
}
|
|
149
134
|
unit.blocked = firstBlock([
|
|
150
|
-
[ctx.
|
|
135
|
+
[ctx.desktopAppManagesSync, "the HQ desktop app is handling sync on this machine"],
|
|
151
136
|
[!ctx.hqRoot, NO_HQ_ROOT],
|
|
152
137
|
[!ctx.signedIn, NOT_SIGNED_IN],
|
|
153
|
-
[!!ownerBlock, ownerBlock ?? ""],
|
|
154
138
|
]);
|
|
155
139
|
return [unit];
|
|
156
140
|
}
|
|
@@ -12,17 +12,11 @@ export interface DaemonConfig {
|
|
|
12
12
|
hqRoot?: string;
|
|
13
13
|
/** Exact hq-cloud package version preferred by this daemon, including prereleases. */
|
|
14
14
|
hqCloudVersion?: string;
|
|
15
|
-
/** Persisted machine-local sync pause. */
|
|
16
|
-
syncPaused?: boolean;
|
|
17
|
-
/** Explicit desktop handoff; absent values preserve the legacy per-machine owner. */
|
|
18
|
-
syncOwner?: SyncOwner;
|
|
19
15
|
/** Services turned on here beyond the machine type's defaults. */
|
|
20
16
|
enable: string[];
|
|
21
17
|
/** Default services turned off here. */
|
|
22
18
|
disable: string[];
|
|
23
19
|
}
|
|
24
|
-
export type SyncOwner = "daemon" | "desktop-app";
|
|
25
|
-
export declare function effectiveSyncOwner(config: Pick<DaemonConfig, "syncOwner">, machineType: MachineType): SyncOwner;
|
|
26
20
|
export declare function emptyDaemonConfig(): DaemonConfig;
|
|
27
21
|
export declare function readDaemonConfig(file: string): DaemonConfig;
|
|
28
22
|
export declare function writeDaemonConfig(file: string, config: DaemonConfig): void;
|
|
@@ -10,11 +10,8 @@ import { valid as validSemver } from "semver";
|
|
|
10
10
|
import { isServiceName } from "./catalog.js";
|
|
11
11
|
import { isMachineType } from "./machine-type.js";
|
|
12
12
|
import { ensurePrivateDir } from "./paths.js";
|
|
13
|
-
export function effectiveSyncOwner(config, machineType) {
|
|
14
|
-
return config.syncOwner ?? (machineType === "desktop" ? "desktop-app" : "daemon");
|
|
15
|
-
}
|
|
16
13
|
export function emptyDaemonConfig() {
|
|
17
|
-
return { version: 1,
|
|
14
|
+
return { version: 1, enable: [], disable: [] };
|
|
18
15
|
}
|
|
19
16
|
function stringList(value) {
|
|
20
17
|
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
@@ -39,16 +36,9 @@ export function readDaemonConfig(file) {
|
|
|
39
36
|
if (!raw || typeof raw !== "object")
|
|
40
37
|
throw new Error(`hq daemon config at ${file} is not a JSON object`);
|
|
41
38
|
const obj = raw;
|
|
42
|
-
const config = {
|
|
43
|
-
version: 1,
|
|
44
|
-
syncPaused: obj.syncPaused === true,
|
|
45
|
-
enable: stringList(obj.enable),
|
|
46
|
-
disable: stringList(obj.disable),
|
|
47
|
-
};
|
|
39
|
+
const config = { version: 1, enable: stringList(obj.enable), disable: stringList(obj.disable) };
|
|
48
40
|
if (isMachineType(obj.machineType))
|
|
49
41
|
config.machineType = obj.machineType;
|
|
50
|
-
if (obj.syncOwner === "daemon" || obj.syncOwner === "desktop-app")
|
|
51
|
-
config.syncOwner = obj.syncOwner;
|
|
52
42
|
if (typeof obj.hqRoot === "string" && obj.hqRoot.trim())
|
|
53
43
|
config.hqRoot = obj.hqRoot;
|
|
54
44
|
if (typeof obj.hqCloudVersion === "string" && validSemver(obj.hqCloudVersion) === obj.hqCloudVersion) {
|
|
@@ -66,9 +56,6 @@ export function writeDaemonConfig(file, config) {
|
|
|
66
56
|
ordered.hqRoot = config.hqRoot;
|
|
67
57
|
if (config.hqCloudVersion)
|
|
68
58
|
ordered.hqCloudVersion = config.hqCloudVersion;
|
|
69
|
-
ordered.syncPaused = config.syncPaused === true;
|
|
70
|
-
if (config.syncOwner)
|
|
71
|
-
ordered.syncOwner = config.syncOwner;
|
|
72
59
|
ordered.enable = config.enable;
|
|
73
60
|
ordered.disable = config.disable;
|
|
74
61
|
fs.writeFileSync(tmp, `${JSON.stringify(ordered, null, 2)}\n`, { mode: 0o600 });
|
|
@@ -15,10 +15,6 @@ export type ControlRequest = {
|
|
|
15
15
|
action: "stop";
|
|
16
16
|
unit: string;
|
|
17
17
|
}
|
|
18
|
-
/** Re-read persisted configuration and apply any changed blocks immediately. */
|
|
19
|
-
| {
|
|
20
|
-
action: "reconcile";
|
|
21
|
-
}
|
|
22
18
|
/** Restart the whole daemon, for example after the updater installed a new CLI. */
|
|
23
19
|
| {
|
|
24
20
|
action: "restart-daemon";
|
|
@@ -19,8 +19,6 @@ function isControlRequest(value) {
|
|
|
19
19
|
return typeof v.unit === "string";
|
|
20
20
|
if (v.action === "restart-daemon")
|
|
21
21
|
return true;
|
|
22
|
-
if (v.action === "reconcile")
|
|
23
|
-
return true;
|
|
24
22
|
return false;
|
|
25
23
|
}
|
|
26
24
|
// Orders requests written by one process within the same millisecond; the
|