@indigoai-us/hq-cli 5.208.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,13 @@
|
|
|
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
|
+
|
|
5
12
|
## [5.208.0] — 2026-09-26
|
|
6
13
|
|
|
7
14
|
### Fixed
|
|
@@ -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
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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`);
|
|
@@ -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
|
|
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:
|
|
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) {
|