@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +25 -2
  2. package/dist/command-catalog.generated.d.ts +11 -536
  3. package/dist/command-catalog.generated.js +14 -691
  4. package/dist/commands/daemon.d.ts +0 -3
  5. package/dist/commands/daemon.js +1 -173
  6. package/dist/commands/lanes-checks.d.ts +0 -1
  7. package/dist/commands/lanes-checks.js +217 -54
  8. package/dist/commands/lanes-hold.js +40 -2
  9. package/dist/commands/lanes-message.js +98 -14
  10. package/dist/commands/lanes-shared.d.ts +3 -2
  11. package/dist/commands/lanes.d.ts +2 -4
  12. package/dist/commands/lanes.js +3 -23
  13. package/dist/commands/sync-mode.d.ts +0 -11
  14. package/dist/commands/sync-mode.js +0 -30
  15. package/dist/lib/daemon/catalog.d.ts +2 -18
  16. package/dist/lib/daemon/catalog.js +2 -18
  17. package/dist/lib/daemon/config.d.ts +0 -6
  18. package/dist/lib/daemon/config.js +2 -15
  19. package/dist/lib/daemon/requests.d.ts +0 -4
  20. package/dist/lib/daemon/requests.js +0 -2
  21. package/dist/lib/daemon/run.js +19 -78
  22. package/dist/lib/daemon/supervisor.d.ts +0 -8
  23. package/dist/lib/daemon/supervisor.js +1 -101
  24. package/dist/lib/doctor/checks/scaffold-cli-floor.d.ts +7 -0
  25. package/dist/lib/doctor/checks/scaffold-cli-floor.js +73 -0
  26. package/dist/lib/doctor/registry.js +2 -0
  27. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  28. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  29. package/dist/lib/lanes/hire-grant-dispatch.d.ts +6 -1
  30. package/dist/lib/lanes/hire-grant-dispatch.js +78 -3
  31. package/dist/lib/lanes/keep-alive.d.ts +3 -2
  32. package/dist/lib/lanes/keep-alive.js +2 -2
  33. package/dist/lib/lanes/merge-command.d.ts +1 -0
  34. package/dist/lib/lanes/merge-command.js +299 -33
  35. package/dist/lib/lanes/orphans.d.ts +7 -0
  36. package/dist/lib/lanes/orphans.js +39 -4
  37. package/dist/lib/lanes/tracker-store.d.ts +2 -18
  38. package/dist/lib/lanes/tracker-store.js +1 -172
  39. package/dist/lib/scaffold-cli-floor.d.ts +9 -0
  40. package/dist/lib/scaffold-cli-floor.js +50 -0
  41. package/package.json +2 -2
  42. package/dist/commands/lanes-import.d.ts +0 -14
  43. package/dist/commands/lanes-import.js +0 -1210
  44. package/dist/commands/lanes-tracker.d.ts +0 -18
  45. package/dist/commands/lanes-tracker.js +0 -452
  46. package/dist/lib/daemon/sync-runner.d.ts +0 -58
  47. 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
- async function messageLaneExclusion(ctx, lane) {
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 hqRoot = ctx.hqRoot();
76
+ const companyForLane = (candidate) => resolveLaneCompany(ctx.hqRoot(), candidate).company;
24
77
  const lineageAware = await readLanesRegistryFlag(LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, {
25
- company: resolveLaneCompany(hqRoot, lane).company ?? undefined,
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 lanes = loadAllLanes(hqRoot);
33
- const retiredLaneIds = loadRetiredLaneIds(hqRoot);
34
- return {
35
- lane: currentLane,
36
- exclusion: keepAliveExclusion(currentLane, lanes, retiredLaneIds, lineageAware),
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
- await dispatchOutpostLaneCommand(ctx, lane, "message", opts, resolution.remoteOnly
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
- laneTrackerEnabled: (company: LaneCompanyRef) => boolean | Promise<boolean>;
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[];
@@ -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 recording approvals against the active identity. */
98
- trackerActor?: () => string | null;
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;
@@ -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
- trackerActor: deps.trackerActor ?? defaultTrackerActor,
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", "--owner", SYNC_RUNNER_OWNER],
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.syncPaused, "sync is paused (run `hq daemon sync resume`)"],
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, syncPaused: false, enable: [], disable: [] };
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