@indigoai-us/hq-cli 5.207.0 → 5.209.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,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.209.0] — 2026-09-26
6
+
7
+ ### Added
8
+
9
+ - Behind the off-by-default `checkpoint.ontology.grok-backend` flag, `hq core checkpoint --backend auto` tries grok first for the payload company and falls back to the usual order when grok is missing or crashed last time. The detached ontology plan reads the flag and records it in the company state directory, so the foreground checkpoint makes no network call; a flag change applies from the checkpoint after the next plan.
10
+ - Behind the off-by-default `checkpoint.ontology.ignore-annotations` flag, the checkpoint ontology plan gives the model a copy of each knowledge input without the old cloud annotator's `entities:` front-matter list and entity links, which were mostly wrong and which the model copied.
11
+
12
+ ## [5.208.0] — 2026-09-26
13
+
14
+ ### Fixed
15
+
16
+ - `hq lanes accounts set-home` and `login` accept `--company <slug|uid>` and
17
+ scope the host-account-home flag read to that company.
18
+ - The `hq daemon` updater now installs new hq-cloud patch releases. It looks up the newest release in the bundled major version and installs that exact version. Before, `npm install` with a version range kept an installed copy that already matched the range, so the updater stayed on 6.18.1 after 6.18.2 was published. A failed lookup is reported as a failed update and installs nothing.
19
+
5
20
  ## [5.207.0] — 2026-09-26
6
21
 
7
22
  ### Added
@@ -8172,6 +8172,9 @@ export declare const COMMAND_CATALOG: readonly [{
8172
8172
  readonly options: readonly [{
8173
8173
  readonly flags: "--host <hostname>";
8174
8174
  readonly description: "Host key (defaults to this host's hostname)";
8175
+ }, {
8176
+ readonly flags: "--company <slug|uid>";
8177
+ readonly description: "Company scope for the host-account-home flag";
8175
8178
  }, {
8176
8179
  readonly flags: "--json";
8177
8180
  readonly description: "JSON on stdout (always)";
@@ -8195,6 +8198,9 @@ export declare const COMMAND_CATALOG: readonly [{
8195
8198
  readonly options: readonly [{
8196
8199
  readonly flags: "--home <path>";
8197
8200
  readonly description: "Account home directory";
8201
+ }, {
8202
+ readonly flags: "--company <slug|uid>";
8203
+ readonly description: "Company scope for the host-account-home flag";
8198
8204
  }, {
8199
8205
  readonly flags: "--device-auth";
8200
8206
  readonly description: "Use Codex device authentication instead of localhost callback";
@@ -10571,6 +10571,10 @@ export const COMMAND_CATALOG = [
10571
10571
  "flags": "--host <hostname>",
10572
10572
  "description": "Host key (defaults to this host's hostname)"
10573
10573
  },
10574
+ {
10575
+ "flags": "--company <slug|uid>",
10576
+ "description": "Company scope for the host-account-home flag"
10577
+ },
10574
10578
  {
10575
10579
  "flags": "--json",
10576
10580
  "description": "JSON on stdout (always)"
@@ -10601,6 +10605,10 @@ export const COMMAND_CATALOG = [
10601
10605
  "flags": "--home <path>",
10602
10606
  "description": "Account home directory"
10603
10607
  },
10608
+ {
10609
+ "flags": "--company <slug|uid>",
10610
+ "description": "Company scope for the host-account-home flag"
10611
+ },
10604
10612
  {
10605
10613
  "flags": "--device-auth",
10606
10614
  "description": "Use Codex device authentication instead of localhost callback"
@@ -71,9 +71,17 @@ export declare function writeStamps(liveRoot: string, sessionId: string | undefi
71
71
  * claude session keeps using claude and a codex session keeps using codex
72
72
  * (same-engine continuity), then the fallback priority claude -> codex -> grok
73
73
  * for whatever the caller is not. Deterministic and env-only, so it is unit
74
- * tested directly.
74
+ * tested directly. `preferGrok` (the company's grok-backend flag) puts grok
75
+ * ahead of that order and leaves the rest as the fallback.
75
76
  */
76
- export declare function autoBackendPreference(): SpawnableBackend[];
77
+ export declare function autoBackendPreference(preferGrok?: boolean): SpawnableBackend[];
78
+ /**
79
+ * Whether the payload company's last ontology plan recorded
80
+ * checkpoint.ontology.grok-backend as on. Reads only the local state file the
81
+ * detached sibling writes, so a foreground checkpoint never waits on the flag
82
+ * registry; a flag change takes effect from the checkpoint after the next plan.
83
+ */
84
+ export declare function companyPrefersGrok(liveRoot: string, companySlug: string | null | undefined): boolean;
77
85
  /**
78
86
  * Whether the maintenance sibling may run at all. The process environment wins
79
87
  * over the settings file, so a one-off `HQ_CHECKPOINT_AGENT=0 hq core
@@ -15,6 +15,8 @@ import { resolveLiveRoot } from "../utils/hq-roots.js";
15
15
  import { peekIdToken } from "../utils/id-token.js";
16
16
  import { toHqStateWriteError } from "../utils/hq-state-write-error.js";
17
17
  import { checkpointStampWriteErrorFor, isCheckpointStampWriteError, } from "../utils/checkpoint-stamp-write-error.js";
18
+ import { readBackendPreference } from "../lib/checkpoint-ontology/plan.js";
19
+ import { resolveOntologyCompany } from "../lib/checkpoint-ontology/company.js";
18
20
  const DEFAULT_TRIGGER = "stop-gate";
19
21
  const BACKENDS = new Set(["auto", "claude", "codex", "grok", "none"]);
20
22
  // Pinned by operator directive 2026-09-15; change defaults here deliberately.
@@ -672,13 +674,34 @@ function callerBackend() {
672
674
  * claude session keeps using claude and a codex session keeps using codex
673
675
  * (same-engine continuity), then the fallback priority claude -> codex -> grok
674
676
  * for whatever the caller is not. Deterministic and env-only, so it is unit
675
- * tested directly.
677
+ * tested directly. `preferGrok` (the company's grok-backend flag) puts grok
678
+ * ahead of that order and leaves the rest as the fallback.
676
679
  */
677
- export function autoBackendPreference() {
680
+ export function autoBackendPreference(preferGrok = false) {
678
681
  const caller = callerBackend();
679
- if (!caller)
680
- return [...FALLBACK_BACKEND_ORDER];
681
- return [caller, ...FALLBACK_BACKEND_ORDER.filter((name) => name !== caller)];
682
+ const order = caller
683
+ ? [caller, ...FALLBACK_BACKEND_ORDER.filter((name) => name !== caller)]
684
+ : [...FALLBACK_BACKEND_ORDER];
685
+ return preferGrok ? ["grok", ...order.filter((name) => name !== "grok")] : order;
686
+ }
687
+ /**
688
+ * Whether the payload company's last ontology plan recorded
689
+ * checkpoint.ontology.grok-backend as on. Reads only the local state file the
690
+ * detached sibling writes, so a foreground checkpoint never waits on the flag
691
+ * registry; a flag change takes effect from the checkpoint after the next plan.
692
+ */
693
+ export function companyPrefersGrok(liveRoot, companySlug) {
694
+ const resolved = resolveOntologyCompany(liveRoot, companySlug);
695
+ if (!resolved.ok)
696
+ return false;
697
+ try {
698
+ return readBackendPreference(liveRoot, resolved.company);
699
+ }
700
+ catch (error) {
701
+ // A damaged state file must not stop the checkpoint; the usual order applies.
702
+ printError(`checkpoint: ignoring unreadable backend preference for ${resolved.company.slug}: ${error.message}`);
703
+ return false;
704
+ }
682
705
  }
683
706
  function settingsEnvValue(liveRoot, key) {
684
707
  // settings.local.json wins, matching Claude Code's own merge order.
@@ -767,12 +790,12 @@ function assertKnownBackend(requested) {
767
790
  usage(`checkpoint: unknown backend: ${value}`);
768
791
  return value;
769
792
  }
770
- function resolveBackend(requested, liveRoot) {
793
+ function resolveBackend(requested, liveRoot, company) {
771
794
  const value = assertKnownBackend(requested);
772
795
  // An explicitly named backend is honoured as given; only `auto` shops around.
773
796
  if (value !== "auto")
774
797
  return value;
775
- const available = autoBackendPreference().filter((name) => backendOnPath(name) && backendResponds(name));
798
+ const available = autoBackendPreference(companyPrefersGrok(liveRoot, company)).filter((name) => backendOnPath(name) && backendResponds(name));
776
799
  if (available.length === 0)
777
800
  return "none";
778
801
  const crashed = crashedBackends(path.join(liveRoot, "workspace", "checkpoints", "sibling"));
@@ -1073,7 +1096,7 @@ function runCheckpoint(options, command, group) {
1073
1096
  const siblingOptOut = options.agent === false || !siblingEnabledBySetting(liveRoot);
1074
1097
  // Validated even when nothing will spawn, so `--backend bogus` is still an error.
1075
1098
  const requestedBackend = assertKnownBackend(options.backend);
1076
- const backend = siblingOptOut ? "none" : resolveBackend(requestedBackend, liveRoot);
1099
+ const backend = siblingOptOut ? "none" : resolveBackend(requestedBackend, liveRoot, input.company);
1077
1100
  const now = new Date();
1078
1101
  const threadId = `T-${formatTimestamp(now)}-auto-${summarySlug(input.summary)}`;
1079
1102
  const threadPath = path.join(liveRoot, "workspace", "threads", `${threadId}.json`);
@@ -35,6 +35,8 @@ export interface LanesCommandDeps {
35
35
  now?: () => Date;
36
36
  hostname?: () => string;
37
37
  resolveCompany?: (company?: string) => Promise<LaneCompanyRef>;
38
+ /** Membership-backed explicit company selection for account-home writers. */
39
+ resolveActiveCompany?: (company: string) => Promise<LaneCompanyRef | null>;
38
40
  listOutpostIds?: () => Promise<string[]>;
39
41
  readOutpostIdentity?: () => OutpostIdentity;
40
42
  spawnLogin?: (opts: {
@@ -30,7 +30,9 @@ import { BALANCE_MOVABLE_STATES, CLAUDE_ACCOUNT_BALANCE_FLAG, accountLoadsForRep
30
30
  import { parseUserYaml } from "../utils/user-yaml-error.js";
31
31
  import { backfillOutpostIdentityMarker as backfillOutpostIdentityMarkerImpl, createOutpostIdentityMarkerIfMissing, validateOutpostIdentityId, } from "../lib/lanes/outpost-identity.js";
32
32
  import { vaultApiFetch } from "../utils/vault-api.js";
33
- import { openMeshTransport } from "../lib/mesh/client.js";
33
+ import { resolveActiveMembershipCompany } from "../lib/mesh/api.js";
34
+ import { openMeshTransport, requireToken } from "../lib/mesh/client.js";
35
+ import { CompanySelectionError } from "../utils/company-selection-error.js";
34
36
  import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
35
37
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
36
38
  import { effectiveAccountHome } from "../lib/lanes/account-home.js";
@@ -88,11 +90,18 @@ function defaultLaneOutpostQuestionVisibilityEnabled(company, env) {
88
90
  function defaultLaneHostScopedAccountHomesEnabled(company, env) {
89
91
  return readLanesRegistryFlag(LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG, { company, env });
90
92
  }
91
- async function hostScopedAccountHomesEnabled(ctx) {
93
+ async function hostScopedAccountHomesEnabled(ctx, company) {
92
94
  const localOverride = readLocalOverride(LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG, ctx.env);
93
95
  if (localOverride !== null)
94
96
  return localOverride;
95
- return ctx.laneHostScopedAccountHomesEnabled(await ctx.resolveCompany());
97
+ return ctx.laneHostScopedAccountHomesEnabled(company ?? await ctx.resolveCompany());
98
+ }
99
+ async function resolveSelectedAccountHomeCompany(ctx, selector) {
100
+ const company = await ctx.resolveActiveCompany(selector);
101
+ if (!company) {
102
+ throw new CompanySelectionError(`No unique active company membership for ${selector}. Pass an active company UID with --company.`);
103
+ }
104
+ return company;
96
105
  }
97
106
  function withoutProviderHome(value) {
98
107
  if (Array.isArray(value))
@@ -158,6 +167,12 @@ async function defaultResolveCompany(company) {
158
167
  const { company: mesh } = await openMeshTransport({ company });
159
168
  return { slug: mesh.companySlug, uid: mesh.companyUid };
160
169
  }
170
+ async function defaultResolveActiveCompany(company) {
171
+ const membership = await resolveActiveMembershipCompany(await requireToken(), company);
172
+ return membership
173
+ ? { slug: membership.companySlug, uid: membership.companyUid }
174
+ : null;
175
+ }
161
176
  function defaultOutpostsClient() {
162
177
  return createOutpostsClient({
163
178
  transport: vaultApiFetch,
@@ -243,6 +258,7 @@ function resolveDeps(deps = {}) {
243
258
  now: deps.now ?? (() => new Date()),
244
259
  hostname: deps.hostname ?? (() => os.hostname()),
245
260
  resolveCompany: deps.resolveCompany ?? defaultResolveCompany,
261
+ resolveActiveCompany: deps.resolveActiveCompany ?? defaultResolveActiveCompany,
246
262
  listOutpostIds: deps.listOutpostIds ?? defaultListOutpostIds,
247
263
  readOutpostIdentity: deps.readOutpostIdentity ?? (() => readOutpostIdentity()),
248
264
  spawnLogin: deps.spawnLogin ?? defaultSpawnLogin,
@@ -7922,8 +7938,11 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
7922
7938
  if (opts.home !== undefined && !requestedHome) {
7923
7939
  throw new LanesError("invalid_account_home", "--home must be a non-empty path", { provider, account: id });
7924
7940
  }
7941
+ const company = opts.company !== undefined
7942
+ ? await resolveSelectedAccountHomeCompany(ctx, opts.company)
7943
+ : undefined;
7925
7944
  const hostScopedWriterEnabled = registeredAccount !== undefined && requestedHome !== undefined
7926
- ? await hostScopedAccountHomesEnabled(ctx)
7945
+ ? await hostScopedAccountHomesEnabled(ctx, company)
7927
7946
  : false;
7928
7947
  const homeRaw = requestedHome ??
7929
7948
  (registeredAccount !== undefined
@@ -8017,7 +8036,10 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
8017
8036
  printJson({ ok: true, action: "accounts.login", account: status });
8018
8037
  }
8019
8038
  async function runAccountsSetHome(ctx, providerRaw, accountIdRaw, homeRaw, opts) {
8020
- if (!(await hostScopedAccountHomesEnabled(ctx))) {
8039
+ const company = opts.company !== undefined
8040
+ ? await resolveSelectedAccountHomeCompany(ctx, opts.company)
8041
+ : undefined;
8042
+ if (!(await hostScopedAccountHomesEnabled(ctx, company))) {
8021
8043
  throw new LanesError("host_scoped_account_homes_disabled", "Setting a host-specific account home requires the lanes.host-scoped-account-homes flag", { flag: LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG });
8022
8044
  }
8023
8045
  const callerCwd = ctx.callerCwd();
@@ -8689,6 +8711,7 @@ export function registerLanesCommand(parent, deps = {}) {
8689
8711
  .argument("<account-id>", "Existing account id")
8690
8712
  .argument("<path>", "Home directory for this host")
8691
8713
  .option("--host <hostname>", "Host key (defaults to this host's hostname)")
8714
+ .option("--company <slug|uid>", "Company scope for the host-account-home flag")
8692
8715
  .option("--json", "JSON on stdout (always)")
8693
8716
  .action(wrap(async (provider, accountId, home, opts) => {
8694
8717
  await runAccountsSetHome(ctx, provider, accountId, home, opts);
@@ -8699,6 +8722,7 @@ export function registerLanesCommand(parent, deps = {}) {
8699
8722
  .argument("<provider>", "claude | codex | grok")
8700
8723
  .argument("<account-id>", "Open-ended account id")
8701
8724
  .option("--home <path>", "Account home directory")
8725
+ .option("--company <slug|uid>", "Company scope for the host-account-home flag")
8702
8726
  .option("--device-auth", "Use Codex device authentication instead of localhost callback")
8703
8727
  .action(wrap(async (provider, accountId, opts) => {
8704
8728
  await runAccountsLogin(ctx, provider, accountId, opts);
@@ -12,6 +12,13 @@ import { type OntologyCompany } from "./company.js";
12
12
  export declare const ONTOLOGY_FLAG = "checkpoint.ontology";
13
13
  export declare const SHARED_INPUTS_FLAG = "checkpoint.ontology.shared-inputs";
14
14
  export declare const COMPANY_BRIEF_FLAG = "checkpoint.ontology.company-brief";
15
+ export declare const IGNORE_ANNOTATIONS_FLAG = "checkpoint.ontology.ignore-annotations";
16
+ /**
17
+ * With `--backend auto`, `hq core checkpoint` tries grok first for this
18
+ * company. Read here, in the detached sibling, and recorded in the company's
19
+ * state directory, so the foreground checkpoint makes no network call.
20
+ */
21
+ export declare const GROK_BACKEND_FLAG = "checkpoint.ontology.grok-backend";
15
22
  export declare const PLAN_FILE = "ontology-plan.json";
16
23
  export declare const CANDIDATES_FILE = "ontology-candidates.json";
17
24
  export declare const REPORT_FILE = "ontology-report.json";
@@ -59,6 +66,8 @@ export interface PlanInput {
59
66
  } | null;
60
67
  /** For signals: `entity_refs` items with no entity file yet. */
61
68
  missing_refs?: string[];
69
+ /** Run-directory-relative copy of `path` with annotator entity lists removed; same line numbers. */
70
+ read_path?: string;
62
71
  }
63
72
  export interface OntologyPlan {
64
73
  schema: typeof PLAN_SCHEMA;
@@ -112,9 +121,13 @@ export type PlanOutcome = {
112
121
  planPath: string;
113
122
  companyBrief?: CompanyBriefResult;
114
123
  };
124
+ export declare const READ_PATH_INSTRUCTION = "When an input has read_path, read that file (relative to the run directory) instead of path, with the same line range. It is the same text with an older annotator's entity lists and entity links removed.";
115
125
  export declare function ledgerPath(hqRoot: string, company: OntologyCompany): string;
116
126
  export declare function readLedger(hqRoot: string, company: OntologyCompany): Ledger | null;
117
127
  export declare function writeLedger(hqRoot: string, company: OntologyCompany, ledger: Ledger): void;
128
+ export declare function backendPreferencePath(hqRoot: string, company: OntologyCompany): string;
129
+ /** The grok-backend flag value the last plan recorded; false when no plan has recorded one. */
130
+ export declare function readBackendPreference(hqRoot: string, company: OntologyCompany): boolean;
118
131
  /** Company-relative paths of the payload's touched files that are inside this company. */
119
132
  export declare function sessionTouched(hqRoot: string, company: OntologyCompany, files: unknown): Set<string>;
120
133
  /** 1-based line ranges, each at most `chunkTokens` tokens (bytes / 4). */
@@ -130,4 +143,13 @@ export declare function refreshLedger(company: OntologyCompany, existing: Ledger
130
143
  };
131
144
  /** Plan this run. Never throws for a skip; returns the reason instead. */
132
145
  export declare function planOntologyRun(options: PlanOptions): Promise<PlanOutcome>;
146
+ /**
147
+ * The cloud annotator wrote an `entities:` list into knowledge front matter and
148
+ * `[[ontology/entities/...]]` links into the text. Most are wrong, and a model
149
+ * told to ignore them still copies them (design section 18), so the model reads
150
+ * a copy without them. The list's lines are blanked rather than removed so the
151
+ * plan's line ranges still hold; a link becomes its label, or its last path
152
+ * segment. Returns null when the text has neither.
153
+ */
154
+ export declare function stripAnnotations(text: string): string | null;
133
155
  //# sourceMappingURL=plan.d.ts.map
@@ -16,6 +16,13 @@ import { buildEntityIndex, listKnowledgeFiles, listSignalFiles, readSignal } fro
16
16
  export const ONTOLOGY_FLAG = "checkpoint.ontology";
17
17
  export const SHARED_INPUTS_FLAG = "checkpoint.ontology.shared-inputs";
18
18
  export const COMPANY_BRIEF_FLAG = "checkpoint.ontology.company-brief";
19
+ export const IGNORE_ANNOTATIONS_FLAG = "checkpoint.ontology.ignore-annotations";
20
+ /**
21
+ * With `--backend auto`, `hq core checkpoint` tries grok first for this
22
+ * company. Read here, in the detached sibling, and recorded in the company's
23
+ * state directory, so the foreground checkpoint makes no network call.
24
+ */
25
+ export const GROK_BACKEND_FLAG = "checkpoint.ontology.grok-backend";
19
26
  export const PLAN_FILE = "ontology-plan.json";
20
27
  export const CANDIDATES_FILE = "ontology-candidates.json";
21
28
  export const REPORT_FILE = "ontology-report.json";
@@ -27,6 +34,7 @@ export const BUDGET = {
27
34
  maxTokens: 60_000,
28
35
  chunkTokens: 12_000,
29
36
  };
37
+ export const READ_PATH_INSTRUCTION = "When an input has read_path, read that file (relative to the run directory) instead of path, with the same line range. It is the same text with an older annotator's entity lists and entity links removed.";
30
38
  const CANDIDATE_INSTRUCTIONS = [
31
39
  `Write ${CANDIDATES_FILE} in the run directory as {"schema":"${CANDIDATES_SCHEMA}","candidates":[...]} and nothing else.`,
32
40
  "Each candidate: {input, type, name, aliases, confidence, evidence}. input is one of this plan's input ids; type is person, project, company or concept; name is the entity's full proper name; aliases are other names the input uses for it (at most 5); confidence is 0 to 1; evidence is a short quote from the input.",
@@ -61,6 +69,26 @@ export function writeLedger(hqRoot, company, ledger) {
61
69
  const target = ledgerPath(hqRoot, company);
62
70
  safeWriteFile(hqRoot, [stateRoot(hqRoot, company)], target, `${JSON.stringify(ledger, null, 2)}\n`);
63
71
  }
72
+ export function backendPreferencePath(hqRoot, company) {
73
+ return path.join(stateRoot(hqRoot, company), "backend.json");
74
+ }
75
+ /** The grok-backend flag value the last plan recorded; false when no plan has recorded one. */
76
+ export function readBackendPreference(hqRoot, company) {
77
+ let raw;
78
+ try {
79
+ raw = fs.readFileSync(backendPreferencePath(hqRoot, company), "utf8");
80
+ }
81
+ catch (error) {
82
+ if (error.code === "ENOENT")
83
+ return false;
84
+ throw error;
85
+ }
86
+ return JSON.parse(raw)?.prefer_grok === true;
87
+ }
88
+ function recordBackendPreference(hqRoot, company, preferGrok) {
89
+ const target = backendPreferencePath(hqRoot, company);
90
+ safeWriteFile(hqRoot, [stateRoot(hqRoot, company)], target, `${JSON.stringify({ prefer_grok: preferGrok })}\n`);
91
+ }
64
92
  /** Company-relative paths of the payload's touched files that are inside this company. */
65
93
  export function sessionTouched(hqRoot, company, files) {
66
94
  const out = new Set();
@@ -172,13 +200,18 @@ export async function planOntologyRun(options) {
172
200
  if (!resolution.ok)
173
201
  return { status: "skipped", reason: resolution.reason };
174
202
  const company = resolution.company;
175
- if (!(await options.readFlag(ONTOLOGY_FLAG, company)))
203
+ if (!(await options.readFlag(ONTOLOGY_FLAG, company))) {
204
+ // A preference recorded while the ontology was on must not outlive it.
205
+ fs.rmSync(backendPreferencePath(hqRoot, company), { force: true });
176
206
  return { status: "skipped", reason: "flag off" };
207
+ }
177
208
  const sharedInputs = await options.readFlag(SHARED_INPUTS_FLAG, company);
178
209
  // Before the ledger, so a nothing-changed plan still records the brief.
179
210
  const companyBrief = (await options.readFlag(COMPANY_BRIEF_FLAG, company))
180
211
  ? writeCompanyBrief(hqRoot, company, options.now ?? new Date())
181
212
  : undefined;
213
+ const ignoreAnnotations = await options.readFlag(IGNORE_ANNOTATIONS_FLAG, company);
214
+ recordBackendPreference(hqRoot, company, await options.readFlag(GROK_BACKEND_FLAG, company));
182
215
  const touched = sessionTouched(hqRoot, company, payload.files);
183
216
  const { ledger, firstRun } = refreshLedger(company, readLedger(hqRoot, company), touched, sharedInputs);
184
217
  const pending = Object.entries(ledger.entries)
@@ -192,9 +225,16 @@ export async function planOntologyRun(options) {
192
225
  remaining += 1;
193
226
  continue;
194
227
  }
195
- const lines = fs.readFileSync(path.join(company.dir, rel), "utf8").split("\n");
228
+ const text = fs.readFileSync(path.join(company.dir, rel), "utf8");
229
+ const lines = text.split("\n");
196
230
  const chunks = chunkLines(lines.join("\n"), BUDGET.chunkTokens);
197
231
  const kind = rel.startsWith("signals/") ? "signal" : "knowledge";
232
+ const cleaned = ignoreAnnotations && kind === "knowledge" ? stripAnnotations(text) : null;
233
+ const readPath = cleaned === null ? null : path.join(READ_COPY_DIR, rel);
234
+ if (readPath !== null) {
235
+ fs.mkdirSync(path.dirname(path.join(runDir, readPath)), { recursive: true });
236
+ fs.writeFileSync(path.join(runDir, readPath), cleaned);
237
+ }
198
238
  let index = entry.next_chunk ?? 0;
199
239
  for (; index < chunks.length; index += 1) {
200
240
  const [start, end] = chunks[index];
@@ -212,6 +252,7 @@ export async function planOntologyRun(options) {
212
252
  lines: chunks.length > 1 ? [start, end] : null,
213
253
  chunk: chunks.length > 1 ? { index, count: chunks.length } : null,
214
254
  ...(kind === "signal" ? { missing_refs: missingRefs(company.dir, rel) } : {}),
255
+ ...(readPath !== null ? { read_path: readPath } : {}),
215
256
  });
216
257
  tokens += cost;
217
258
  }
@@ -267,13 +308,47 @@ export async function planOntologyRun(options) {
267
308
  planned_tokens: tokens,
268
309
  },
269
310
  candidates_file: CANDIDATES_FILE,
270
- instructions: CANDIDATE_INSTRUCTIONS,
311
+ instructions: inputs.some((input) => input.read_path)
312
+ ? [...CANDIDATE_INSTRUCTIONS.slice(0, 3), READ_PATH_INSTRUCTION, ...CANDIDATE_INSTRUCTIONS.slice(3)]
313
+ : CANDIDATE_INSTRUCTIONS,
271
314
  };
272
315
  fs.mkdirSync(runDir, { recursive: true });
273
316
  const planPath = path.join(runDir, PLAN_FILE);
274
317
  fs.writeFileSync(planPath, `${JSON.stringify(plan, null, 2)}\n`);
275
318
  return companyBrief ? { status: "planned", plan, planPath, companyBrief } : { status: "planned", plan, planPath };
276
319
  }
320
+ const READ_COPY_DIR = "ontology-inputs";
321
+ /**
322
+ * The cloud annotator wrote an `entities:` list into knowledge front matter and
323
+ * `[[ontology/entities/...]]` links into the text. Most are wrong, and a model
324
+ * told to ignore them still copies them (design section 18), so the model reads
325
+ * a copy without them. The list's lines are blanked rather than removed so the
326
+ * plan's line ranges still hold; a link becomes its label, or its last path
327
+ * segment. Returns null when the text has neither.
328
+ */
329
+ export function stripAnnotations(text) {
330
+ let out = text;
331
+ const front = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
332
+ if (front && /^entities:/m.test(front[1])) {
333
+ let inList = false;
334
+ const body = front[1]
335
+ .split("\n")
336
+ .map((line) => {
337
+ if (/^entities:/.test(line))
338
+ inList = true;
339
+ else if (inList && /^\S/.test(line))
340
+ inList = false;
341
+ return inList ? "" : line;
342
+ })
343
+ .join("\n");
344
+ const start = text.indexOf("\n") + 1;
345
+ out = text.slice(0, start) + body + text.slice(start + front[1].length);
346
+ }
347
+ out = out
348
+ .replace(/\[\[ontology\/entities\/[^\]|\n]*\|([^\]\n]*)\]\]/g, "$1")
349
+ .replace(/\[\[ontology\/entities\/(?:[^\]\n]*\/)?([^\]/\n]*)\]\]/g, "$1");
350
+ return out === text ? null : out;
351
+ }
277
352
  const SESSION_TEXT_MAX_BYTES = 8_000;
278
353
  /** The session summary, decisions and learnings from the payload, capped. */
279
354
  function sessionContent(payload) {
@@ -31,8 +31,21 @@ export interface HqCloudRunners {
31
31
  sessionHost: string | null;
32
32
  }
33
33
  export declare function hqCloudRunners(root: string, exists?: (p: string) => boolean): HqCloudRunners;
34
- /** Install the newest release of the bundled major version, never older than the bundled copy. */
35
- export declare function hqCloudInstallCommand(prefix: string, bundledVersion: string, platform?: NodeJS.Platform, pinnedVersion?: string): {
34
+ /**
35
+ * Ask npm for every release of the bundled major version, no older than the
36
+ * bundled copy. The updater installs the newest one exactly: `npm install` with
37
+ * a range keeps an installed copy that already satisfies it, so a range alone
38
+ * never moves 6.18.1 to 6.18.2.
39
+ */
40
+ export declare function hqCloudLatestCommand(bundledVersion: string, platform?: NodeJS.Platform): {
41
+ file: string;
42
+ args: string[];
43
+ shell: boolean;
44
+ };
45
+ /** The newest release in `npm view --json` output with the bundled major version, never older than it. */
46
+ export declare function latestSameMajor(npmViewJson: string, bundledVersion: string): string | null;
47
+ /** Install one exact hq-cloud version into the updater's folder. */
48
+ export declare function hqCloudInstallCommand(prefix: string, version: string, platform?: NodeJS.Platform): {
36
49
  file: string;
37
50
  args: string[];
38
51
  shell: boolean;
@@ -48,6 +61,17 @@ export interface HqCloudUpdateOutcome {
48
61
  to: string | null;
49
62
  detail?: string;
50
63
  }
64
+ export interface NpmRunResult {
65
+ status: number | null;
66
+ stdout?: string | null;
67
+ stderr?: string | null;
68
+ error?: Error;
69
+ }
70
+ export interface UpdateHqCloudDeps {
71
+ run?: (file: string, args: string[], shell: boolean, env: NodeJS.ProcessEnv) => NpmRunResult;
72
+ bundled?: HqCloudCopy | null;
73
+ platform?: NodeJS.Platform;
74
+ }
51
75
  /** Install or update the updater's copy, as this (the login) user. */
52
- export declare function updateManagedHqCloud(daemonRoot: string, env: NodeJS.ProcessEnv): HqCloudUpdateOutcome;
76
+ export declare function updateManagedHqCloud(daemonRoot: string, env: NodeJS.ProcessEnv, deps?: UpdateHqCloudDeps): HqCloudUpdateOutcome;
53
77
  //# sourceMappingURL=hq-cloud.d.ts.map
@@ -61,18 +61,45 @@ export function hqCloudRunners(root, exists = fs.existsSync) {
61
61
  sessionHost: bin("outpost-session-host.js"),
62
62
  };
63
63
  }
64
- /** Install the newest release of the bundled major version, never older than the bundled copy. */
65
- export function hqCloudInstallCommand(prefix, bundledVersion, platform = process.platform, pinnedVersion) {
64
+ /**
65
+ * Ask npm for every release of the bundled major version, no older than the
66
+ * bundled copy. The updater installs the newest one exactly: `npm install` with
67
+ * a range keeps an installed copy that already satisfies it, so a range alone
68
+ * never moves 6.18.1 to 6.18.2.
69
+ */
70
+ export function hqCloudLatestCommand(bundledVersion, platform = process.platform) {
71
+ const plan = buildSpawnPlan("npm", ["view", `${HQ_CLOUD_PACKAGE}@^${bundledVersion}`, "version", "--json"], platform);
72
+ return { file: plan.cmd, args: plan.args, shell: plan.shell };
73
+ }
74
+ /** The newest release in `npm view --json` output with the bundled major version, never older than it. */
75
+ export function latestSameMajor(npmViewJson, bundledVersion) {
76
+ const b = parseVersion(bundledVersion);
77
+ if (!b)
78
+ return null;
79
+ let parsed;
80
+ try {
81
+ parsed = JSON.parse(npmViewJson);
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ const versions = typeof parsed === "string" ? [parsed] : Array.isArray(parsed) ? parsed : [];
87
+ let best = null;
88
+ for (const text of versions) {
89
+ if (typeof text !== "string" || !/^\d+\.\d+\.\d+$/.test(text))
90
+ continue;
91
+ const v = parseVersion(text);
92
+ if (v[0] !== b[0] || newer(b, v))
93
+ continue;
94
+ if (!best || newer(v, best.v))
95
+ best = { text, v };
96
+ }
97
+ return best?.text ?? null;
98
+ }
99
+ /** Install one exact hq-cloud version into the updater's folder. */
100
+ export function hqCloudInstallCommand(prefix, version, platform = process.platform) {
66
101
  // npm is a .cmd on Windows and needs the shell, so every argument is quoted for it.
67
- const plan = buildSpawnPlan("npm", [
68
- "install",
69
- "--prefix",
70
- prefix,
71
- "--no-audit",
72
- "--no-fund",
73
- "--omit=dev",
74
- `${HQ_CLOUD_PACKAGE}@${pinnedVersion ?? `^${bundledVersion}`}`,
75
- ], platform);
102
+ const plan = buildSpawnPlan("npm", ["install", "--prefix", prefix, "--no-audit", "--no-fund", "--omit=dev", `${HQ_CLOUD_PACKAGE}@${version}`], platform);
76
103
  return { file: plan.cmd, args: plan.args, shell: plan.shell };
77
104
  }
78
105
  export function readHqCloudCopy(root) {
@@ -99,10 +126,26 @@ export function currentHqCloud(daemonRoot) {
99
126
  const pinnedVersion = readDaemonConfig(path.join(daemonRoot, "config.json")).hqCloudVersion;
100
127
  return pickHqCloud(bundledHqCloud(), readHqCloudCopy(managedHqCloudRoot(daemonRoot)), (copy) => hqCloudRunners(copy.root).syncRunner !== null, pinnedVersion);
101
128
  }
129
+ function runNpm(file, args, shell, env) {
130
+ return spawnSync(file, args, {
131
+ encoding: "utf8",
132
+ env,
133
+ timeout: 10 * 60_000,
134
+ stdio: ["ignore", "pipe", "pipe"],
135
+ windowsHide: true,
136
+ shell,
137
+ });
138
+ }
139
+ function npmFailure(r) {
140
+ const tail = `${r.stderr ?? ""}\n${r.stdout ?? ""}`.trim().split("\n").slice(-3).join(" ").slice(0, 400);
141
+ return tail || (r.error ? r.error.message : `npm exited ${r.status ?? "by signal"}`);
142
+ }
102
143
  /** Install or update the updater's copy, as this (the login) user. */
103
- export function updateManagedHqCloud(daemonRoot, env) {
144
+ export function updateManagedHqCloud(daemonRoot, env, deps = {}) {
145
+ const run = deps.run ?? runNpm;
146
+ const platform = deps.platform ?? process.platform;
104
147
  const pinnedVersion = readDaemonConfig(path.join(daemonRoot, "config.json")).hqCloudVersion;
105
- const bundled = bundledHqCloud();
148
+ const bundled = deps.bundled !== undefined ? deps.bundled : bundledHqCloud();
106
149
  const from = currentHqCloud(daemonRoot)?.version ?? null;
107
150
  if (!bundled)
108
151
  return { ok: false, from, to: null, detail: "this CLI install has no bundled hq-cloud to match" };
@@ -113,19 +156,22 @@ export function updateManagedHqCloud(daemonRoot, env) {
113
156
  catch (err) {
114
157
  return { ok: false, from, to: null, detail: `could not create ${prefix}: ${err instanceof Error ? err.message : String(err)}` };
115
158
  }
116
- const cmd = hqCloudInstallCommand(prefix, bundled.version, process.platform, pinnedVersion);
117
- const r = spawnSync(cmd.file, cmd.args, {
118
- encoding: "utf8",
119
- env,
120
- timeout: 10 * 60_000,
121
- stdio: ["ignore", "pipe", "pipe"],
122
- windowsHide: true,
123
- shell: cmd.shell,
124
- });
125
- if (r.status !== 0) {
126
- const tail = `${r.stderr ?? ""}\n${r.stdout ?? ""}`.trim().split("\n").slice(-3).join(" ").slice(0, 400);
127
- return { ok: false, from, to: null, detail: tail || (r.error ? r.error.message : `npm exited ${r.status ?? "by signal"}`) };
159
+ let version = pinnedVersion;
160
+ if (!version) {
161
+ const view = hqCloudLatestCommand(bundled.version, platform);
162
+ const r = run(view.file, view.args, view.shell, env);
163
+ if (r.status !== 0)
164
+ return { ok: false, from, to: null, detail: `could not look up the newest hq-cloud: ${npmFailure(r)}` };
165
+ const latest = latestSameMajor(r.stdout ?? "", bundled.version);
166
+ if (!latest) {
167
+ return { ok: false, from, to: null, detail: `npm listed no hq-cloud release at or above ${bundled.version} in the same major version` };
168
+ }
169
+ version = latest;
128
170
  }
171
+ const cmd = hqCloudInstallCommand(prefix, version, platform);
172
+ const r = run(cmd.file, cmd.args, cmd.shell, env);
173
+ if (r.status !== 0)
174
+ return { ok: false, from, to: null, detail: npmFailure(r) };
129
175
  return { ok: true, from, to: currentHqCloud(daemonRoot)?.version ?? null };
130
176
  }
131
177
  //# sourceMappingURL=hq-cloud.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.207.0",
3
+ "version": "5.209.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {