@indigoai-us/hq-cli 5.231.0 → 5.232.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 CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.232.0] — 2026-09-27
6
+
7
+ ### Added
8
+
9
+ - `checkpoint.ontology.sync-gated` (off by default) turns the checkpoint
10
+ ontology step on like `checkpoint.ontology`. Older CLI builds do not read it,
11
+ so it can be turned on widely without them. With either flag on, the step runs
12
+ only when the hq-cloud bundled with this CLI and the one the hq daemon runs
13
+ are both 6.18.7 or later; otherwise it skips and says which copy is older.
14
+ It also skips while the daemon's sync still runs from before its hq-cloud
15
+ copy was installed. `hq core ontology apply` checks again before it writes and
16
+ reports `sync_client_old` with the reason. Earlier hq-cloud releases pushed
17
+ stale copies of deleted entities.
18
+
19
+ ## [5.231.1] — 2026-09-27
20
+
21
+ ### Fixed
22
+
23
+ - `hq crm entity upsert` checks CRM settings first and prints the enable hint without sending a write when CRM is disabled.
24
+
5
25
  ## [5.231.0] — 2026-09-27
6
26
 
7
27
  ### Added
@@ -86,6 +86,8 @@ export function runOntologyApply(hqRoot, runDirOption, deps = {}) {
86
86
  throw new OntologyUsageError("ontology: the run payload names no company");
87
87
  }
88
88
  const report = applyOntologyRun({ hqRoot, runDir, company, now: deps.now?.() });
89
+ if (report.status === "sync_client_old")
90
+ return `ontology: skipped (${report.skip_reason}); nothing written`;
89
91
  if (report.status !== "applied")
90
92
  return `ontology: ${report.status.replaceAll("_", " ")}; nothing written`;
91
93
  return (`ontology: ${report.created.length} created, ${report.updated.length} updated, ` +
@@ -19,6 +19,54 @@ const CRM_ENTITY_TYPES = [
19
19
  "invoice",
20
20
  "company",
21
21
  ];
22
+ const CRM_SETTINGS_PRECHECK_TIMEOUT_MS = 3_000;
23
+ const CRM_DISABLED_HINT_PREFIX = "CRM is not enabled for this company. Enable it first: ";
24
+ function printCrmDisabledHint(companySlug) {
25
+ console.error(chalk.red(CRM_DISABLED_HINT_PREFIX +
26
+ `hq company settings set --company ${companySlug} --crm-enabled true`));
27
+ process.exit(1);
28
+ }
29
+ function isRecord(value) {
30
+ return typeof value === "object" && value !== null && !Array.isArray(value);
31
+ }
32
+ /**
33
+ * CRM settings are a best-effort precheck. Only an explicit false prevents the
34
+ * write; unavailable or malformed settings leave hq-pro authoritative.
35
+ */
36
+ async function crmIsExplicitlyDisabled(token, companyUid) {
37
+ try {
38
+ const response = await vaultApiFetch({
39
+ token,
40
+ path: "/company-settings",
41
+ query: { companyUid },
42
+ signal: AbortSignal.timeout(CRM_SETTINGS_PRECHECK_TIMEOUT_MS),
43
+ });
44
+ if (!response.ok) {
45
+ console.error(chalk.red(`CRM settings precheck returned HTTP ${response.status}; continuing to let the server decide.`));
46
+ return false;
47
+ }
48
+ let body;
49
+ try {
50
+ body = await response.json();
51
+ }
52
+ catch (err) {
53
+ const errorName = err instanceof Error ? err.name : "unknown error";
54
+ console.error(chalk.red(`CRM settings precheck returned invalid JSON (${errorName}); continuing to let the server decide.`));
55
+ return false;
56
+ }
57
+ if (!isRecord(body))
58
+ return false;
59
+ const settings = isRecord(body.companySettings)
60
+ ? body.companySettings
61
+ : body;
62
+ return settings.crmEnabled === false;
63
+ }
64
+ catch (err) {
65
+ const errorName = err instanceof Error ? err.name : "unknown error";
66
+ console.error(chalk.red(`CRM settings precheck failed (${errorName}); continuing to let the server decide.`));
67
+ return false;
68
+ }
69
+ }
22
70
  /**
23
71
  * Read the `--json <file|->` payload: a single entity object or an array of
24
72
  * entities. `-` reads from stdin. Returns the entities array (always
@@ -127,6 +175,9 @@ export function registerCrmCommand(program) {
127
175
  }
128
176
  const token = await ensureCognitoToken();
129
177
  const companyUid = await getEntityUid(token, { companySlug });
178
+ if (await crmIsExplicitlyDisabled(token, companyUid)) {
179
+ printCrmDisabledHint(companySlug);
180
+ }
130
181
  const res = await vaultApiFetch({
131
182
  token,
132
183
  path: "/crm/entities",
@@ -137,9 +188,7 @@ export function registerCrmCommand(program) {
137
188
  const body = (await res.json().catch(() => ({})));
138
189
  // CrmDisabledError surfaces as a 403 with a helpful message.
139
190
  if (res.status === 403 && body.code === "CRM_DISABLED") {
140
- console.error(chalk.red("CRM is not enabled for this company. Enable it first: " +
141
- `hq company settings set --company ${companySlug} --crm-enabled true`));
142
- process.exit(1);
191
+ printCrmDisabledHint(companySlug);
143
192
  }
144
193
  console.error(chalk.red(`Failed to upsert entities: ${body.error ?? res.statusText}`));
145
194
  process.exit(1);
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { type DuplicateReport, type MergeBackOutcome } from "./multi-writer.js";
9
9
  import { type CompanyBriefResult } from "./company-brief.js";
10
+ import { type SyncClientVersions } from "./sync-client.js";
10
11
  import { type WritableEntityType } from "./identity.js";
11
12
  import { type EntityIndex } from "./tree.js";
12
13
  export declare const CAPS: {
@@ -30,7 +31,9 @@ export interface ApplyReport {
30
31
  schema: "hq-checkpoint-ontology-report.v1";
31
32
  company: string;
32
33
  run_id: string;
33
- status: "applied" | "no_candidates" | "invalid_candidates";
34
+ status: "applied" | "no_candidates" | "invalid_candidates" | "sync_client_old";
35
+ /** Present only with status `sync_client_old`: which sync client copy is older or unknown. */
36
+ skip_reason?: string;
34
37
  planned_inputs: number;
35
38
  planned_tokens: number;
36
39
  candidates: number;
@@ -94,6 +97,8 @@ export interface ApplyOptions {
94
97
  /** The payload company. Must equal the plan's company. */
95
98
  company: string;
96
99
  now?: Date;
100
+ /** Test seam for the sync client check; defaults to reading this machine's hq-cloud copies. */
101
+ syncClientVersions?: () => SyncClientVersions;
97
102
  }
98
103
  export declare class ApplyError extends Error {
99
104
  constructor(message: string);
@@ -12,6 +12,7 @@ import { entityKey, freshAliases, MAX_NAME_LENGTH, ownNames } from "./aliases.js
12
12
  import { recordOutbox, scanDuplicates } from "./multi-writer.js";
13
13
  import { writeCompanyBrief } from "./company-brief.js";
14
14
  import { entitiesRoot, resolveOntologyCompany, safeWriteFile } from "./company.js";
15
+ import { checkSyncClient, readSyncClientVersions } from "./sync-client.js";
15
16
  import { appendBackRefs, parseEntityFile, patchEntityFile, previewSignalContent, renderNewEntity, validateEntityFrontmatter, } from "./entity-file.js";
16
17
  import { canonicalizeName, entityId, isRejectedName, isWritableEntityType, normalizeEntityName, squash, toSlug, } from "./identity.js";
17
18
  import { CANDIDATES_FILE, CANDIDATES_SCHEMA, PLAN_FILE, PLAN_SCHEMA, REPORT_FILE, readLedger, writeLedger, } from "./plan.js";
@@ -232,6 +233,15 @@ export function applyOntologyRun(options) {
232
233
  fs.writeFileSync(path.join(options.runDir, REPORT_FILE), `${JSON.stringify(report, null, 2)}\n`);
233
234
  return report;
234
235
  };
236
+ // CO-45: the sync client can change between plan and apply, so check it
237
+ // again before anything is written.
238
+ const syncClient = checkSyncClient((options.syncClientVersions ?? readSyncClientVersions)());
239
+ if (!syncClient.ok) {
240
+ report.status = "sync_client_old";
241
+ report.skip_reason = syncClient.reason;
242
+ report.inputs_pending = plan.inputs.filter((i) => i.kind !== "session").map((i) => i.id);
243
+ return finish();
244
+ }
235
245
  const candidatesPath = path.join(options.runDir, CANDIDATES_FILE);
236
246
  if (!fs.existsSync(candidatesPath)) {
237
247
  report.status = "no_candidates";
@@ -11,7 +11,16 @@ import { type CompanyBriefResult } from "./company-brief.js";
11
11
  import { type OntologyCompany } from "./company.js";
12
12
  import { type MultiWriterStart } from "./multi-writer.js";
13
13
  import { type EntityHint } from "./hints.js";
14
+ import { type SyncClientVersions } from "./sync-client.js";
14
15
  export declare const ONTOLOGY_FLAG = "checkpoint.ontology";
16
+ /**
17
+ * Turns the step on like `checkpoint.ontology`, but only builds that check the
18
+ * sync client read it (CO-45, owner decision CO-D-005). Older CLI builds read
19
+ * only `checkpoint.ontology`, so this key can default on without starting the
20
+ * step next to an hq-cloud older than 6.18.7. Either key needs a sync client of
21
+ * 6.18.7 or later; see ./sync-client.ts.
22
+ */
23
+ export declare const ONTOLOGY_SYNC_GATED_FLAG = "checkpoint.ontology.sync-gated";
15
24
  export declare const COMPANY_BRIEF_FLAG = "checkpoint.ontology.company-brief";
16
25
  /**
17
26
  * Applies only when the payload's sibling backend is grok. On gold-v2, Claude
@@ -159,6 +168,8 @@ export interface PlanOptions {
159
168
  payload: CheckpointPayloadLike;
160
169
  readFlag: FlagReader;
161
170
  now?: Date;
171
+ /** This machine's hq-cloud versions; defaults to reading them. */
172
+ syncClientVersions?: () => SyncClientVersions;
162
173
  }
163
174
  export type PlanOutcome = {
164
175
  status: "skipped";
@@ -14,8 +14,17 @@ import { writeCompanyBrief } from "./company-brief.js";
14
14
  import { resolveOntologyCompany, safeWriteFile, stateRoot } from "./company.js";
15
15
  import { startMultiWriterRun } from "./multi-writer.js";
16
16
  import { entityHints, wordWeights } from "./hints.js";
17
+ import { checkSyncClient, readSyncClientVersions } from "./sync-client.js";
17
18
  import { buildEntityIndex, listKnowledgeFiles, listSignalFiles, readSignal } from "./tree.js";
18
19
  export const ONTOLOGY_FLAG = "checkpoint.ontology";
20
+ /**
21
+ * Turns the step on like `checkpoint.ontology`, but only builds that check the
22
+ * sync client read it (CO-45, owner decision CO-D-005). Older CLI builds read
23
+ * only `checkpoint.ontology`, so this key can default on without starting the
24
+ * step next to an hq-cloud older than 6.18.7. Either key needs a sync client of
25
+ * 6.18.7 or later; see ./sync-client.ts.
26
+ */
27
+ export const ONTOLOGY_SYNC_GATED_FLAG = "checkpoint.ontology.sync-gated";
19
28
  export const COMPANY_BRIEF_FLAG = "checkpoint.ontology.company-brief";
20
29
  /**
21
30
  * Applies only when the payload's sibling backend is grok. On gold-v2, Claude
@@ -231,10 +240,12 @@ export async function planOntologyRun(options) {
231
240
  if (!resolution.ok)
232
241
  return { status: "skipped", reason: resolution.reason };
233
242
  const company = resolution.company;
234
- if (!(await options.readFlag(ONTOLOGY_FLAG, company))) {
243
+ const ontologyOn = (await options.readFlag(ONTOLOGY_FLAG, company)) || (await options.readFlag(ONTOLOGY_SYNC_GATED_FLAG, company));
244
+ const syncClient = ontologyOn ? checkSyncClient((options.syncClientVersions ?? readSyncClientVersions)()) : null;
245
+ if (!ontologyOn || !syncClient?.ok) {
235
246
  // A preference recorded while the ontology was on must not outlive it.
236
247
  fs.rmSync(backendPreferencePath(hqRoot, company), { force: true });
237
- return { status: "skipped", reason: "flag off" };
248
+ return { status: "skipped", reason: syncClient && !syncClient.ok ? syncClient.reason : "flag off" };
238
249
  }
239
250
  const briefOn = await options.readFlag(COMPANY_BRIEF_FLAG, company);
240
251
  const ignoreAnnotations = (await options.readFlag(IGNORE_ANNOTATIONS_FLAG, company)) && payload.backend === "grok";
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The sync client check for the checkpoint ontology step (CO-45, owner
3
+ * decision CO-D-005).
4
+ *
5
+ * The step writes entity files locally and a sync client pushes them. hq-cloud
6
+ * before 6.18.7 pushed stale local copies of deleted entities as new objects
7
+ * and overwrote newer entities with older ones (INC-CO-RESURRECT). hq-flags
8
+ * cannot target by client version, so the step checks this machine's hq-cloud
9
+ * copies itself: the one bundled with this CLI (`hq sync`) and the one the hq
10
+ * daemon runs. If either is older than 6.18.7, or the bundled copy cannot be
11
+ * read, the step does not run. The daemon picks up a new hq-cloud only when
12
+ * its sync unit restarts, so a sync unit that started before its copy was
13
+ * installed may still run older code; the step does not run then either.
14
+ *
15
+ * The desktop app's sync runner is not visible from here. The vault's
16
+ * `vault.refuse-stale-entity-recreate` guard refuses those two writes from any
17
+ * client.
18
+ */
19
+ export declare const MIN_SYNC_CLIENT_VERSION = "6.18.7";
20
+ export interface SyncClientVersions {
21
+ /** hq-cloud bundled with this CLI, or null when it cannot be read. */
22
+ bundled: string | null;
23
+ /** hq-cloud the hq daemon runs, or null when it cannot be read. */
24
+ daemon: string | null;
25
+ /** True when the daemon's running sync unit started before that copy was installed. */
26
+ daemonRestartPending?: boolean;
27
+ }
28
+ export type SyncClientCheck = {
29
+ ok: true;
30
+ } | {
31
+ ok: false;
32
+ reason: string;
33
+ };
34
+ /** Read this machine's hq-cloud versions. Never throws. */
35
+ export declare function readSyncClientVersions(): SyncClientVersions;
36
+ /** Every readable copy must be at least 6.18.7, and the bundled copy must be readable. */
37
+ export declare function checkSyncClient(versions: SyncClientVersions): SyncClientCheck;
38
+ //# sourceMappingURL=sync-client.d.ts.map
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The sync client check for the checkpoint ontology step (CO-45, owner
3
+ * decision CO-D-005).
4
+ *
5
+ * The step writes entity files locally and a sync client pushes them. hq-cloud
6
+ * before 6.18.7 pushed stale local copies of deleted entities as new objects
7
+ * and overwrote newer entities with older ones (INC-CO-RESURRECT). hq-flags
8
+ * cannot target by client version, so the step checks this machine's hq-cloud
9
+ * copies itself: the one bundled with this CLI (`hq sync`) and the one the hq
10
+ * daemon runs. If either is older than 6.18.7, or the bundled copy cannot be
11
+ * read, the step does not run. The daemon picks up a new hq-cloud only when
12
+ * its sync unit restarts, so a sync unit that started before its copy was
13
+ * installed may still run older code; the step does not run then either.
14
+ *
15
+ * The desktop app's sync runner is not visible from here. The vault's
16
+ * `vault.refuse-stale-entity-recreate` guard refuses those two writes from any
17
+ * client.
18
+ */
19
+ import * as fs from "node:fs";
20
+ import * as path from "node:path";
21
+ import semver from "semver";
22
+ import { bundledHqCloud, currentHqCloud } from "../daemon/hq-cloud.js";
23
+ import { daemonPaths } from "../daemon/paths.js";
24
+ export const MIN_SYNC_CLIENT_VERSION = "6.18.7";
25
+ /** Read this machine's hq-cloud versions. Never throws. */
26
+ export function readSyncClientVersions() {
27
+ const bundled = bundledHqCloud()?.version ?? null;
28
+ let daemon = null;
29
+ let daemonRestartPending = false;
30
+ try {
31
+ const paths = daemonPaths();
32
+ const picked = currentHqCloud(paths.root);
33
+ daemon = picked?.version ?? null;
34
+ if (picked) {
35
+ try {
36
+ daemonRestartPending = syncStartedBefore(paths.statePath, path.join(picked.root, "package.json"));
37
+ }
38
+ catch (err) {
39
+ // Unknown start time: assume the running sync may be older.
40
+ daemonRestartPending = true;
41
+ console.warn(`ontology: could not read the hq daemon's sync start time: ${err instanceof Error ? err.message : String(err)}`);
42
+ }
43
+ }
44
+ }
45
+ catch (err) {
46
+ console.warn(`ontology: could not read the hq daemon's hq-cloud version: ${err instanceof Error ? err.message : String(err)}`);
47
+ }
48
+ return { bundled, daemon, daemonRestartPending };
49
+ }
50
+ /** Whether the daemon's running sync unit started before `copyPackageJson` was written. No state file: no running unit. */
51
+ function syncStartedBefore(statePath, copyPackageJson) {
52
+ let state;
53
+ try {
54
+ state = JSON.parse(fs.readFileSync(statePath, "utf8"));
55
+ }
56
+ catch (err) {
57
+ if (err.code === "ENOENT")
58
+ return false;
59
+ throw err;
60
+ }
61
+ const sync = state.units?.find((u) => u.id === "sync" && u.status === "running");
62
+ if (!sync || typeof sync.startedAt !== "number")
63
+ return false;
64
+ return fs.statSync(copyPackageJson).mtimeMs > sync.startedAt;
65
+ }
66
+ /** Every readable copy must be at least 6.18.7, and the bundled copy must be readable. */
67
+ export function checkSyncClient(versions) {
68
+ if (versions.bundled === null) {
69
+ return { ok: false, reason: `sync client version unknown; needs hq-cloud ${MIN_SYNC_CLIENT_VERSION} or later` };
70
+ }
71
+ for (const [name, version] of [["bundled", versions.bundled], ["daemon", versions.daemon]]) {
72
+ if (version === null)
73
+ continue;
74
+ const parsed = semver.valid(version);
75
+ if (parsed === null || semver.lt(parsed, MIN_SYNC_CLIENT_VERSION)) {
76
+ return { ok: false, reason: `sync client hq-cloud ${version} (${name}) is older than ${MIN_SYNC_CLIENT_VERSION}` };
77
+ }
78
+ }
79
+ if (versions.daemonRestartPending) {
80
+ return { ok: false, reason: `the hq daemon's sync has not restarted onto hq-cloud ${versions.daemon ?? "(unknown)"} yet` };
81
+ }
82
+ return { ok: true };
83
+ }
84
+ //# sourceMappingURL=sync-client.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.231.0",
3
+ "version": "5.232.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {