@indigoai-us/hq-cli 5.273.0-alpha.0 → 5.274.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 (91) hide show
  1. package/CHANGELOG.md +65 -2
  2. package/README.md +32 -1
  3. package/dist/command-catalog.generated.d.ts +83 -536
  4. package/dist/command-catalog.generated.js +129 -714
  5. package/dist/command-registration-plan.d.ts +12 -0
  6. package/dist/command-registration-plan.js +2 -0
  7. package/dist/commands/cloud.js +40 -0
  8. package/dist/commands/core-checkpoint.js +3 -1
  9. package/dist/commands/core-command-catalog.d.ts +32 -0
  10. package/dist/commands/core-command-catalog.js +58 -0
  11. package/dist/commands/core-ontology.js +9 -4
  12. package/dist/commands/core.d.ts +4 -0
  13. package/dist/commands/core.js +79 -75
  14. package/dist/commands/daemon.d.ts +0 -3
  15. package/dist/commands/daemon.js +1 -173
  16. package/dist/commands/deploy-comments.d.ts +3 -0
  17. package/dist/commands/deploy-comments.js +159 -0
  18. package/dist/commands/lanes-checks.d.ts +0 -1
  19. package/dist/commands/lanes-checks.js +217 -54
  20. package/dist/commands/lanes-hold.js +40 -2
  21. package/dist/commands/lanes-message.js +98 -14
  22. package/dist/commands/lanes-move.d.ts +12 -0
  23. package/dist/commands/lanes-move.js +342 -0
  24. package/dist/commands/lanes-resume.js +3 -1
  25. package/dist/commands/lanes-shared.d.ts +39 -3
  26. package/dist/commands/lanes-shared.js +59 -0
  27. package/dist/commands/lanes.d.ts +3 -5
  28. package/dist/commands/lanes.js +206 -478
  29. package/dist/commands/report.js +2 -14
  30. package/dist/commands/sync-mode.d.ts +0 -11
  31. package/dist/commands/sync-mode.js +0 -30
  32. package/dist/fast-core.js +1 -1
  33. package/dist/{commands/scaffold-fast.d.ts → fast-scaffold.d.ts} +1 -1
  34. package/dist/{commands/scaffold-fast.js → fast-scaffold.js} +1 -1
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.js +2 -2
  37. package/dist/lib/checkpoint-ontology/apply.d.ts +8 -0
  38. package/dist/lib/checkpoint-ontology/apply.js +33 -0
  39. package/dist/lib/checkpoint-ontology/plan.d.ts +15 -0
  40. package/dist/lib/checkpoint-ontology/plan.js +15 -0
  41. package/dist/lib/daemon/catalog.d.ts +2 -18
  42. package/dist/lib/daemon/catalog.js +2 -18
  43. package/dist/lib/daemon/config.d.ts +0 -6
  44. package/dist/lib/daemon/config.js +2 -15
  45. package/dist/lib/daemon/requests.d.ts +0 -4
  46. package/dist/lib/daemon/requests.js +0 -2
  47. package/dist/lib/daemon/run.js +19 -78
  48. package/dist/lib/daemon/supervisor.d.ts +0 -8
  49. package/dist/lib/daemon/supervisor.js +1 -101
  50. package/dist/lib/deploy-api.d.ts +6 -0
  51. package/dist/lib/deploy-api.js +21 -0
  52. package/dist/lib/doctor/checks/scaffold-cli-floor.d.ts +7 -0
  53. package/dist/lib/doctor/checks/scaffold-cli-floor.js +73 -0
  54. package/dist/lib/doctor/fix/apply.d.ts +17 -2
  55. package/dist/lib/doctor/fix/apply.js +111 -57
  56. package/dist/lib/doctor/fix/json-output.d.ts +9 -1
  57. package/dist/lib/doctor/fix/json-output.js +11 -0
  58. package/dist/lib/doctor/registry.js +2 -0
  59. package/dist/lib/first-sync-next-step.d.ts +37 -0
  60. package/dist/lib/first-sync-next-step.js +189 -0
  61. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  62. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  63. package/dist/lib/lanes/hire-grant-dispatch.d.ts +6 -1
  64. package/dist/lib/lanes/hire-grant-dispatch.js +78 -3
  65. package/dist/lib/lanes/keep-alive.d.ts +3 -2
  66. package/dist/lib/lanes/keep-alive.js +2 -2
  67. package/dist/lib/lanes/merge-command.d.ts +1 -0
  68. package/dist/lib/lanes/merge-command.js +299 -33
  69. package/dist/lib/lanes/orphans.d.ts +7 -0
  70. package/dist/lib/lanes/orphans.js +39 -4
  71. package/dist/lib/lanes/tracker-store.d.ts +2 -18
  72. package/dist/lib/lanes/tracker-store.js +1 -172
  73. package/dist/lib/scaffold-cli-floor.d.ts +9 -0
  74. package/dist/lib/scaffold-cli-floor.js +50 -0
  75. package/dist/main.js +4 -1
  76. package/dist/utils/cli-telemetry.d.ts +7 -1
  77. package/dist/utils/cli-telemetry.js +84 -8
  78. package/dist/utils/contribution-table.d.ts +1 -1
  79. package/dist/utils/scaffold-fast-import-guard.d.ts +3 -0
  80. package/dist/utils/scaffold-fast-import-guard.js +28 -0
  81. package/dist/utils/scaffold-packlist-guard.d.ts +7 -0
  82. package/dist/utils/scaffold-packlist-guard.js +23 -0
  83. package/dist/utils/scaffold-sibling-references.d.ts +21 -0
  84. package/dist/utils/scaffold-sibling-references.js +156 -0
  85. package/package.json +2 -2
  86. package/dist/commands/lanes-import.d.ts +0 -14
  87. package/dist/commands/lanes-import.js +0 -1210
  88. package/dist/commands/lanes-tracker.d.ts +0 -18
  89. package/dist/commands/lanes-tracker.js +0 -452
  90. package/dist/lib/daemon/sync-runner.d.ts +0 -58
  91. package/dist/lib/daemon/sync-runner.js +0 -277
@@ -5,9 +5,9 @@ import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { ensureCognitoToken, } from "../utils/cognito-session.js";
7
7
  import { vaultApiFetch } from "../utils/vault-api.js";
8
+ import { DEPLOY_API_URL, deployApiRequest, deployHeaders } from "../lib/deploy-api.js";
8
9
  import { listReportTemplates } from "../lib/report-templates.js";
9
10
  import { adoptReport, buildReportTarball, conventionReportDir, deployOrgContext, ReportPublishError, plainFailureReason, publishReport, } from "../lib/report-publish.js";
10
- const DEPLOY_API_URL = process.env.HQ_DEPLOY_API_URL ?? "https://api.indigo-hq.com";
11
11
  const REPORT_CHANNEL_STATE_FILE = "report-channels.json";
12
12
  const MAX_REPORT_DISCOVERY_CHANNELS = 50;
13
13
  const JOB_EXECUTION_DISABLED_MESSAGE = "Scheduled jobs are turned off for this company. A company owner can turn them back on.";
@@ -214,22 +214,10 @@ export async function patchReport(token, report, patch) {
214
214
  throw new ReportPublishError("Report update response was incomplete.");
215
215
  return body.report;
216
216
  }
217
- function deployHeaders(token, json = true, extra = {}) {
218
- return {
219
- Authorization: `Bearer ${token}`,
220
- Accept: "application/json",
221
- ...(json ? { "Content-Type": "application/json" } : {}),
222
- ...extra,
223
- };
224
- }
225
217
  async function deployJson(token, route, method, body, extraHeaders = {}) {
226
218
  let res;
227
219
  try {
228
- res = await fetch(`${DEPLOY_API_URL.replace(/\/+$/, "")}${route}`, {
229
- method,
230
- headers: deployHeaders(token, body !== undefined, extraHeaders),
231
- ...(body === undefined ? {} : { body: JSON.stringify(body) }),
232
- });
220
+ res = await deployApiRequest(token, route, method, body, extraHeaders);
233
221
  }
234
222
  catch {
235
223
  throw new ReportPublishError("Could not reach hq-deploy.");
@@ -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
package/dist/fast-core.js CHANGED
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { resolveLiveRoot } from "./utils/hq-roots.js";
11
11
  import { runBundledScript } from "./utils/run-bundled-script.js";
12
- import { parseFastCore } from "./commands/scaffold-fast.js";
12
+ import { parseFastCore } from "./fast-scaffold.js";
13
13
  /**
14
14
  * Run the fast-path scaffold script named in `argv` and exit with its code.
15
15
  * Only call when {@link isFastCoreRequest} is true; a non-match returns without
@@ -38,4 +38,4 @@ export type ParsedFastCore = {
38
38
  export declare function parseFastCore(argv: readonly string[]): ParsedFastCore | null;
39
39
  /** Whether this invocation is a fast-path scaffold dispatch. */
40
40
  export declare function isFastCoreRequest(argv: readonly string[]): boolean;
41
- //# sourceMappingURL=scaffold-fast.d.ts.map
41
+ //# sourceMappingURL=fast-scaffold.d.ts.map
@@ -54,4 +54,4 @@ export function parseFastCore(argv) {
54
54
  export function isFastCoreRequest(argv) {
55
55
  return parseFastCore(argv) !== null;
56
56
  }
57
- //# sourceMappingURL=scaffold-fast.js.map
57
+ //# sourceMappingURL=fast-scaffold.js.map
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import "./node-preflight.js";
3
3
  import "./node-network-compat.js";
4
4
  import { isVersionRequest } from "./version-request.js";
5
- import { isFastCoreRequest } from "./commands/scaffold-fast.js";
5
+ import { isFastCoreRequest } from "./fast-scaffold.js";
6
6
  export declare const __test__: {
7
7
  isVersionRequest: typeof isVersionRequest;
8
8
  isFastCoreRequest: typeof isFastCoreRequest;
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import "./node-network-compat.js";
6
6
  import { isVersionRequest, wantsLabelledVersionReport } from "./version-request.js";
7
7
  // Dependency-light: a pure parser + table, no command graph. Safe to load on
8
8
  // every path (including --version) without reintroducing the heavy startup.
9
- import { isFastCoreRequest } from "./commands/scaffold-fast.js";
9
+ import { isFastCoreRequest } from "./fast-scaffold.js";
10
10
  // Also dependency-light: registers a process-level rejection boundary; its only
11
11
  // heavy import (main.js) is lazy and reached only when a rejection fires.
12
12
  import { installProcessRejectionBoundary } from "./unhandled-rejection-boundary.js";
@@ -33,7 +33,7 @@ if (isVersionRequest(process.argv)) {
33
33
  else if (isFastCoreRequest(process.argv)) {
34
34
  // Hot relocated plumbing (`hq core hq-session`, `hq core checkpoint-stop-gate`)
35
35
  // dispatches straight to the bundled-script runner, so it never pays for the
36
- // ~60-module command graph main.ts imports. See src/commands/scaffold-fast.ts.
36
+ // ~60-module command graph main.ts imports. See src/fast-scaffold.ts.
37
37
  void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv));
38
38
  }
39
39
  else {
@@ -105,6 +105,14 @@ export declare class ApplyError extends Error {
105
105
  }
106
106
  /** Validate one raw candidate. Returns the candidate or a rejection reason. */
107
107
  export declare function parseCandidate(raw: unknown): Candidate | string;
108
+ /**
109
+ * checkpoint.ontology.named-evidence: true when `evidence` holds one of
110
+ * `names` as a word, matching case. A name inside a path, domain, handle, tag
111
+ * or identifier does not count: `indigoai-us/hq`, `getindigo.ai`,
112
+ * `C:\\Indigo\\hq`, `scope: company:indigo`, `Indigo:internal`, `@indigo`,
113
+ * `` `indigo` ``.
114
+ */
115
+ export declare function evidenceNames(evidence: string, names: string[]): boolean;
108
116
  type Resolution = {
109
117
  tier: 1;
110
118
  target: string;
@@ -63,6 +63,25 @@ export function parseCandidate(raw) {
63
63
  evidence: typeof c.evidence === "string" ? c.evidence : "",
64
64
  };
65
65
  }
66
+ /**
67
+ * checkpoint.ontology.named-evidence: true when `evidence` holds one of
68
+ * `names` as a word, matching case. A name inside a path, domain, handle, tag
69
+ * or identifier does not count: `indigoai-us/hq`, `getindigo.ai`,
70
+ * `C:\\Indigo\\hq`, `scope: company:indigo`, `Indigo:internal`, `@indigo`,
71
+ * `` `indigo` ``.
72
+ */
73
+ export function evidenceNames(evidence, names) {
74
+ for (const raw of names) {
75
+ const name = raw.trim();
76
+ if (!name || !evidence)
77
+ continue;
78
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
79
+ const word = new RegExp(`(?<![\\p{L}\\p{N}_\\-./\\\\@:\`#])${escaped}(?![\\p{L}\\p{N}_\\-/\\\\@\`]|[.:#][\\p{L}\\p{N}])`, "u");
80
+ if (word.test(evidence))
81
+ return true;
82
+ }
83
+ return false;
84
+ }
66
85
  /** Design section 5.3, tiers 1 to 4. */
67
86
  export function resolveCandidate(index, type, canonical) {
68
87
  const normalized = normalizeEntityName(canonical, type);
@@ -281,6 +300,9 @@ export function applyOntologyRun(options) {
281
300
  if (types.size > 1)
282
301
  crossTypeNames.add(name);
283
302
  }
303
+ // checkpoint.ontology.named-evidence: the company's own company entity.
304
+ const ownCompanyRel = `ontology/entities/company/${company.slug}.md`;
305
+ const isOwnCompany = (entity, rel) => entity.type === "company" && (rel === ownCompanyRel || crossTypeKey(entity.canonical_name) === crossTypeKey(company.slug));
284
306
  const inputs = liveInputs(options.hqRoot, company, plan);
285
307
  const blocked = new Set();
286
308
  const updates = new Map();
@@ -339,6 +361,13 @@ export function applyOntologyRun(options) {
339
361
  note(report.rejected, "ambiguous_type");
340
362
  continue;
341
363
  }
364
+ if (plan.named_evidence === true &&
365
+ input.kind === "knowledge" &&
366
+ isOwnCompany(linked, resolved.target) &&
367
+ !evidenceNames(candidate.evidence, [candidate.name, ...candidate.aliases])) {
368
+ note(report.rejected, "unnamed_in_evidence");
369
+ continue;
370
+ }
342
371
  report.matched += 1;
343
372
  report.links.push({ input: candidate.input, action: "link", type: linked.type, canonical_name: linked.canonical_name, path: resolved.target });
344
373
  if (resolved.tier === 1 && resolved.duplicates.length > 0) {
@@ -379,6 +408,10 @@ export function applyOntologyRun(options) {
379
408
  ? [...(index.canonical.get(entityKey("project", normalizeEntityName(canonical, "project"))) ?? [])]
380
409
  : [];
381
410
  if (projects.length === 1) {
411
+ if (plan.named_evidence === true && !evidenceNames(candidate.evidence, [candidate.name, ...candidate.aliases])) {
412
+ note(report.rejected, "unnamed_in_evidence");
413
+ continue;
414
+ }
382
415
  const linked = index.byRel.get(projects[0]);
383
416
  report.matched += 1;
384
417
  report.links.push({ input: candidate.input, action: "link", type: linked.type, canonical_name: linked.canonical_name, path: projects[0] });
@@ -81,6 +81,19 @@ export declare const SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG = "checkpoint.ontology.sk
81
81
  * to 0.709 at precision 0.747 to 0.731 (61 of the 63 added links are HQ).
82
82
  */
83
83
  export declare const CONCEPT_PROJECT_LINK_FLAG = "checkpoint.ontology.concept-project-link";
84
+ /**
85
+ * A knowledge link to the company's own company entity, or a
86
+ * concept-project-link redirect, needs evidence that names the entity as a
87
+ * word: the candidate name or one of its aliases, not inside a path, domain,
88
+ * handle, tag or identifier (`indigoai-us/hq`, `getindigo.ai`,
89
+ * `scope: company:indigo`). The plan carries `named_evidence: true`, and apply
90
+ * rejects such a link as `unnamed_in_evidence`. CO-57: on the six CO-52 runs,
91
+ * 67 of 143 false positives were links to company/indigo and project/HQ, most
92
+ * on evidence that only held the name inside an identifier; a replay of this
93
+ * rule raised precision on the concept-project-link arm from 0.705 to 0.750
94
+ * and lost no gold link.
95
+ */
96
+ export declare const NAMED_EVIDENCE_FLAG = "checkpoint.ontology.named-evidence";
84
97
  export declare const PLAN_FILE = "ontology-plan.json";
85
98
  export declare const CANDIDATES_FILE = "ontology-candidates.json";
86
99
  export declare const REPORT_FILE = "ontology-report.json";
@@ -153,6 +166,8 @@ export interface OntologyPlan {
153
166
  skip_ambiguous_concept_links?: true;
154
167
  /** Set by checkpoint.ontology.concept-project-link: apply links a knowledge concept with an ambiguous alias to the one project of that canonical name. */
155
168
  concept_project_link?: true;
169
+ /** Set by checkpoint.ontology.named-evidence: apply rejects a knowledge link to the company's own entity, or a concept-project-link redirect, whose evidence does not name it as a word. */
170
+ named_evidence?: true;
156
171
  inputs: PlanInput[];
157
172
  /** The session's summary, decisions and learnings, for the `session` input. */
158
173
  session_text: string | null;
@@ -85,6 +85,19 @@ export const SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG = "checkpoint.ontology.skip-ambig
85
85
  * to 0.709 at precision 0.747 to 0.731 (61 of the 63 added links are HQ).
86
86
  */
87
87
  export const CONCEPT_PROJECT_LINK_FLAG = "checkpoint.ontology.concept-project-link";
88
+ /**
89
+ * A knowledge link to the company's own company entity, or a
90
+ * concept-project-link redirect, needs evidence that names the entity as a
91
+ * word: the candidate name or one of its aliases, not inside a path, domain,
92
+ * handle, tag or identifier (`indigoai-us/hq`, `getindigo.ai`,
93
+ * `scope: company:indigo`). The plan carries `named_evidence: true`, and apply
94
+ * rejects such a link as `unnamed_in_evidence`. CO-57: on the six CO-52 runs,
95
+ * 67 of 143 false positives were links to company/indigo and project/HQ, most
96
+ * on evidence that only held the name inside an identifier; a replay of this
97
+ * rule raised precision on the concept-project-link arm from 0.705 to 0.750
98
+ * and lost no gold link.
99
+ */
100
+ export const NAMED_EVIDENCE_FLAG = "checkpoint.ontology.named-evidence";
88
101
  export const PLAN_FILE = "ontology-plan.json";
89
102
  export const CANDIDATES_FILE = "ontology-candidates.json";
90
103
  export const REPORT_FILE = "ontology-report.json";
@@ -264,6 +277,7 @@ export async function planOntologyRun(options) {
264
277
  const firstNamePeople = await options.readFlag(FIRST_NAME_PEOPLE_FLAG, company);
265
278
  const skipAmbiguousConceptLinks = await options.readFlag(SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG, company);
266
279
  const conceptProjectLink = await options.readFlag(CONCEPT_PROJECT_LINK_FLAG, company);
280
+ const namedEvidence = await options.readFlag(NAMED_EVIDENCE_FLAG, company);
267
281
  const touched = sessionTouched(hqRoot, company, payload.files);
268
282
  // A first run takes its boundary from the entity tree, so the ledger is
269
283
  // refreshed before merge-back moves any `last_updated` forward.
@@ -366,6 +380,7 @@ export async function planOntologyRun(options) {
366
380
  ...(firstNamePeople ? { first_name_people: true } : {}),
367
381
  ...(skipAmbiguousConceptLinks ? { skip_ambiguous_concept_links: true } : {}),
368
382
  ...(conceptProjectLink ? { concept_project_link: true } : {}),
383
+ ...(namedEvidence ? { named_evidence: true } : {}),
369
384
  inputs,
370
385
  session_text: sessionText,
371
386
  pending_after_run: remaining,
@@ -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
@@ -16,7 +16,7 @@ import { agentKitPaths } from "../agent-kit/paths.js";
16
16
  import { readBotConfig } from "../bot/config.js";
17
17
  import { botsRoot } from "../bot/paths.js";
18
18
  import { CLAUDE_RUNNER_SCRIPT, CODEX_RUNNER_SCRIPT, enabledServices, planUnits } from "./catalog.js";
19
- import { effectiveSyncOwner, readDaemonConfig } from "./config.js";
19
+ import { readDaemonConfig } from "./config.js";
20
20
  import { findLegacyUnits, legacyOwnerLookup } from "./legacy.js";
21
21
  import { readLegacySyncEnv } from "./legacy-sync-env.js";
22
22
  import { acquireDaemonLock, releaseDaemonLock } from "./lock.js";
@@ -26,7 +26,6 @@ import { appendPrivate, daemonPaths, ensurePrivateDir, openPrivateAppend } from
26
26
  import { takeControlRequests } from "./requests.js";
27
27
  import { currentHqCloud, hqCloudRunners } from "./hq-cloud.js";
28
28
  import { runnerSessionName } from "./remote-control.js";
29
- import { listSyncRunnerProcesses, readWatchRunnerOwnerStatus, syncHeartbeatTimeoutMs, syncRunnerNodeOptions, watchRunnerOwnerStatusPath, } from "./sync-runner.js";
30
29
  import { STOP_ESCALATION_MS, createSupervisor } from "./supervisor.js";
31
30
  import { userToolDirs } from "./tools.js";
32
31
  export const LOG_ROTATE_BYTES = 5 * 1024 * 1024;
@@ -214,10 +213,7 @@ function makeLogger(paths, foreground) {
214
213
  * an on-failure unit down. The daemon's updater handles CLI updates.
215
214
  */
216
215
  export function unitProcessEnv(base, unit) {
217
- const env = { ...base, ...unit.env, HQ_DAEMON_UNIT: unit.id, HQ_NO_UPDATE_CHECK: "1" };
218
- if (unit.service === "sync")
219
- env.NODE_OPTIONS = syncRunnerNodeOptions(env);
220
- return env;
216
+ return { ...base, ...unit.env, HQ_DAEMON_UNIT: unit.id, HQ_NO_UPDATE_CHECK: "1" };
221
217
  }
222
218
  export async function runDaemon(opts) {
223
219
  const env = opts.env ?? process.env;
@@ -262,11 +258,10 @@ export async function runDaemon(opts) {
262
258
  return legacyCache.units;
263
259
  };
264
260
  let warnedMissingPin = null;
265
- const daemonSyncRunnerPids = new Set();
266
261
  const plan = () => {
267
262
  const config = readDaemonConfig(paths.configPath);
268
263
  const legacy = legacyUnits();
269
- const configured = config.hqRoot ?? (env.HQ_ROOT?.trim() || undefined);
264
+ const configured = config.hqRoot ?? env.HQ_ROOT?.trim() ?? undefined;
270
265
  const hqRoot = configured && fs.existsSync(configured) ? configured : undefined;
271
266
  // Re-picked every reconcile, so a newer hq-cloud from the updater is used as soon as it lands.
272
267
  const hqCloud = currentHqCloud(paths.root);
@@ -278,33 +273,7 @@ export async function runDaemon(opts) {
278
273
  }
279
274
  }
280
275
  const runners = hqCloud ? hqCloudRunners(hqCloud.root) : { syncRunner: null, heartbeat: null, sessionHost: null };
281
- const services = enabledServices(machineType, config);
282
- const isSignedIn = signedIn(env, home, log);
283
- const syncOwner = effectiveSyncOwner(config, machineType);
284
- let syncRunnerProcesses = [];
285
- let syncRunnerOwnerPid = null;
286
- if (machineType === "desktop" &&
287
- syncOwner === "daemon" &&
288
- services.includes("sync") &&
289
- !config.syncPaused &&
290
- hqRoot &&
291
- isSignedIn) {
292
- const processList = listSyncRunnerProcesses(platform);
293
- if (!processList.ok) {
294
- syncRunnerProcesses = null;
295
- log("warn", `${processList.error}; sync will wait before taking ownership`);
296
- }
297
- else {
298
- syncRunnerProcesses = processList.processes;
299
- try {
300
- syncRunnerOwnerPid = readWatchRunnerOwnerStatus(watchRunnerOwnerStatusPath(hqRoot, home, env))?.pid ?? null;
301
- }
302
- catch (error) {
303
- log("warn", `could not verify the sync watch-owner status: ${error instanceof Error ? error.message : String(error)}`);
304
- }
305
- }
306
- }
307
- return planUnits(services, {
276
+ return planUnits(enabledServices(machineType, config), {
308
277
  machineType,
309
278
  platform,
310
279
  home,
@@ -313,20 +282,14 @@ export async function runDaemon(opts) {
313
282
  heartbeatRunnerPath: runners.heartbeat,
314
283
  sessionHostRunnerPath: runners.sessionHost,
315
284
  legacySyncEnv: readLegacySyncEnv(platform),
316
- signedIn: isSignedIn,
285
+ signedIn: signedIn(env, home, log),
317
286
  kitConfigured: fs.existsSync(agentKitPaths(home, env).kitConfigPath),
318
- syncPaused: config.syncPaused === true,
319
- syncOwner,
320
287
  desktopAppManagesSync: desktopAppManagesSync({
321
288
  machineType,
322
289
  host: opts.host,
323
290
  legacy,
324
291
  appRunning: () => desktopAppRunning(platform),
325
292
  }),
326
- syncRunnerProcesses,
327
- syncRunnerOwnerPid,
328
- daemonSyncRunnerPids,
329
- syncHeartbeatTimeoutMs: syncHeartbeatTimeoutMs(env),
330
293
  bots: listLocalBots(),
331
294
  legacyOwner: legacyOwnerLookup(legacy),
332
295
  cliSelfUpdatable: updatable,
@@ -348,27 +311,20 @@ export async function runDaemon(opts) {
348
311
  : command.type === "node"
349
312
  ? [opts.nodeBinary, [command.script, ...command.args]]
350
313
  : [command.file, command.args];
351
- const logPath = paths.unitLogPath(unit.id);
352
- const logFd = openPrivateAppend(logPath);
353
- const logStream = fs.createWriteStream(logPath, { fd: logFd, autoClose: true });
354
- logStream.on("error", (err) => log("error", `could not write ${unit.id} log ${logPath}: ${err.message}`));
355
- const cwd = unit.env.HQ_ROOT && fs.existsSync(unit.env.HQ_ROOT) ? unit.env.HQ_ROOT : home;
356
- const child = spawn(file, args, {
357
- cwd,
358
- env: unitProcessEnv(env, unit),
359
- stdio: ["ignore", "pipe", "pipe"],
360
- windowsHide: true,
361
- });
362
- if (unit.service === "sync" && child.pid !== undefined)
363
- daemonSyncRunnerPids.add(child.pid);
364
- child.stdout?.pipe(logStream, { end: false });
365
- child.stderr?.pipe(logStream, { end: false });
366
- child.on("close", () => {
367
- if (child.pid !== undefined)
368
- daemonSyncRunnerPids.delete(child.pid);
369
- logStream.end();
370
- });
371
- return child;
314
+ const fd = openPrivateAppend(paths.unitLogPath(unit.id));
315
+ try {
316
+ const cwd = unit.env.HQ_ROOT && fs.existsSync(unit.env.HQ_ROOT) ? unit.env.HQ_ROOT : home;
317
+ const child = spawn(file, args, {
318
+ cwd,
319
+ env: unitProcessEnv(env, unit),
320
+ stdio: ["ignore", fd, fd],
321
+ windowsHide: true,
322
+ });
323
+ return child;
324
+ }
325
+ finally {
326
+ fs.closeSync(fd);
327
+ }
372
328
  };
373
329
  // Set once the shutdown path exists below; a restart request before then is dropped.
374
330
  let restartDaemon = () => { };
@@ -390,21 +346,6 @@ export async function runDaemon(opts) {
390
346
  restartDaemon: () => restartDaemon(),
391
347
  writeState: (state) => writeJsonAtomic(paths.statePath, state),
392
348
  log,
393
- syncOwnerBlockReason: () => {
394
- const config = readDaemonConfig(paths.configPath);
395
- const hqRoot = config.hqRoot ?? env.HQ_ROOT?.trim();
396
- if (!hqRoot)
397
- return undefined;
398
- const statusPath = watchRunnerOwnerStatusPath(hqRoot, home, env);
399
- try {
400
- const owner = readWatchRunnerOwnerStatus(statusPath);
401
- return owner ? `sync owned by ${owner.owner} pid ${owner.pid}` : undefined;
402
- }
403
- catch (err) {
404
- log("error", `could not read sync owner status at ${statusPath}: ${err instanceof Error ? err.message : String(err)}`);
405
- return undefined;
406
- }
407
- },
408
349
  });
409
350
  return await new Promise((resolve) => {
410
351
  let stopping = false;