@indigoai-us/hq-cli 5.201.4 → 5.202.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,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.202.0] — 2026-09-26
6
+
7
+ ### Added
8
+
9
+ - Behind the off-by-default checkpoint.ontology.company-brief flag, the checkpoint ontology plan writes company-brief.md from the local entities, signals and source index, with no model call; recent signals are dated through their source.
10
+
5
11
  ## [5.201.4] — 2026-09-25
6
12
 
7
13
  ### Fixed
@@ -44,6 +44,11 @@ function requireRunDir(runDir) {
44
44
  export const registryFlagReader = (flagKey, company) => readLanesRegistryFlag(flagKey, {
45
45
  company: company.cloudUid ? { slug: company.slug, uid: company.cloudUid } : null,
46
46
  });
47
+ function companyBriefSuffix(brief) {
48
+ if (!brief)
49
+ return "";
50
+ return `; company brief ${brief.status}${brief.reason ? ` (${brief.reason})` : ""}`;
51
+ }
47
52
  export async function runOntologyPlan(hqRoot, runDirOption, deps = {}) {
48
53
  const runDir = requireRunDir(runDirOption);
49
54
  const outcome = await planOntologyRun({
@@ -53,11 +58,13 @@ export async function runOntologyPlan(hqRoot, runDirOption, deps = {}) {
53
58
  readFlag: deps.readFlag ?? registryFlagReader,
54
59
  now: deps.now?.(),
55
60
  });
56
- if (outcome.status === "skipped")
57
- return `ontology: skipped (${outcome.reason})`;
61
+ if (outcome.status === "skipped") {
62
+ return `ontology: skipped (${outcome.reason})${companyBriefSuffix(outcome.companyBrief)}`;
63
+ }
58
64
  const { plan } = outcome;
59
65
  return (`ontology: planned ${plan.inputs.length} input(s), ~${plan.budget.planned_tokens} tokens, ` +
60
- `${plan.pending_after_run} left for later runs; plan at ${outcome.planPath}`);
66
+ `${plan.pending_after_run} left for later runs; plan at ${outcome.planPath}` +
67
+ companyBriefSuffix(outcome.companyBrief));
61
68
  }
62
69
  export function runOntologyApply(hqRoot, runDirOption, deps = {}) {
63
70
  const runDir = requireRunDir(runDirOption);
@@ -70,7 +77,8 @@ export function runOntologyApply(hqRoot, runDirOption, deps = {}) {
70
77
  return `ontology: ${report.status.replaceAll("_", " ")}; nothing written`;
71
78
  return (`ontology: ${report.created.length} created, ${report.updated.length} updated, ` +
72
79
  `${report.matched} matched, ${report.rejected.length} rejected, ${report.deferred.length} deferred, ` +
73
- `${report.suspect_duplicates.length} suspect duplicate(s), ${report.write_errors.length} write error(s)`);
80
+ `${report.suspect_duplicates.length} suspect duplicate(s), ${report.write_errors.length} write error(s)` +
81
+ companyBriefSuffix(report.company_brief));
74
82
  }
75
83
  function fail(error) {
76
84
  if (error instanceof OntologyUsageError || error instanceof ApplyError || error instanceof WriteBoundsError) {
@@ -5,6 +5,7 @@
5
5
  * sections 4.3, 5 and 14). The model output is untrusted input: a malformed
6
6
  * candidate is rejected and counted, never repaired.
7
7
  */
8
+ import { type CompanyBriefResult } from "./company-brief.js";
8
9
  import { type WritableEntityType } from "./identity.js";
9
10
  import { type EntityIndex } from "./tree.js";
10
11
  export declare const CAPS: {
@@ -73,6 +74,8 @@ export interface ApplyReport {
73
74
  path: string;
74
75
  reason: string;
75
76
  }>;
77
+ /** Present only when the plan ran the company brief and this apply created or updated entities. */
78
+ company_brief?: CompanyBriefResult;
76
79
  duration_ms: number;
77
80
  }
78
81
  export interface ApplyOptions {
@@ -8,6 +8,7 @@
8
8
  import { createHash } from "node:crypto";
9
9
  import * as fs from "node:fs";
10
10
  import * as path from "node:path";
11
+ import { writeCompanyBrief } from "./company-brief.js";
11
12
  import { entitiesRoot, resolveOntologyCompany, safeWriteFile } from "./company.js";
12
13
  import { appendBackRefs, parseEntityFile, patchEntityFile, previewSignalContent, renderNewEntity, validateEntityFrontmatter, } from "./entity-file.js";
13
14
  import { canonicalizeName, entityId, isRejectedName, isWritableEntityType, normalizeEntityName, squash, toSlug, } from "./identity.js";
@@ -438,6 +439,10 @@ export function applyOntologyRun(options) {
438
439
  report.updated.push(rel);
439
440
  }
440
441
  commitLedger(options.hqRoot, company, plan, inputs, blocked, report);
442
+ // The plan rendered the brief before these entity writes; render it again so it matches them.
443
+ if (plan.company_brief && report.created.length + report.updated.length > 0) {
444
+ report.company_brief = writeCompanyBrief(options.hqRoot, company, options.now ?? new Date());
445
+ }
441
446
  return finish();
442
447
  }
443
448
  let signalRefIndex = null;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Deterministic `company-brief.md` for one company.
3
+ *
4
+ * The cloud gardener used to write this file. The checkpoint renders the same
5
+ * shape from the local ontology with no model call. Signal dates come from
6
+ * `sources/_index`, because signal files have no `created_at`.
7
+ */
8
+ import { type OntologyCompany } from "./company.js";
9
+ export declare const COMPANY_BRIEF_FILE = "company-brief.md";
10
+ export interface CompanyBriefResult {
11
+ status: "written" | "unchanged" | "skipped" | "failed";
12
+ /** Set for skipped and failed. */
13
+ reason?: string;
14
+ }
15
+ /** `null` when fewer than three valid person, project, company, or concept entities exist. */
16
+ export declare function renderCompanyBrief(company: OntologyCompany, now: Date): string | null;
17
+ export declare function writeCompanyBrief(hqRoot: string, company: OntologyCompany, now: Date): CompanyBriefResult;
18
+ //# sourceMappingURL=company-brief.d.ts.map
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Deterministic `company-brief.md` for one company.
3
+ *
4
+ * The cloud gardener used to write this file. The checkpoint renders the same
5
+ * shape from the local ontology with no model call. Signal dates come from
6
+ * `sources/_index`, because signal files have no `created_at`.
7
+ */
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ import { safeWriteFile, WriteBoundsError } from "./company.js";
11
+ import { parseEntityFile, validateEntityFrontmatter } from "./entity-file.js";
12
+ export const COMPANY_BRIEF_FILE = "company-brief.md";
13
+ const SIGNAL_TYPES = [
14
+ { dir: "decision", heading: "Decisions" },
15
+ { dir: "risk", heading: "Risks" },
16
+ { dir: "question", heading: "Questions" },
17
+ ];
18
+ const ENTITY_TYPES = [
19
+ { dir: "person", heading: "People" },
20
+ { dir: "project", heading: "Projects" },
21
+ { dir: "company", heading: "Companies" },
22
+ { dir: "concept", heading: "Concepts" },
23
+ ];
24
+ const CHANNELS = [
25
+ { id: "meeting", label: "Meeting" },
26
+ { id: "email", label: "Email" },
27
+ { id: "slack", label: "Slack" },
28
+ { id: "slack-dm", label: "Slack DM" },
29
+ { id: "linear", label: "Linear" },
30
+ { id: "notion", label: "Notion" },
31
+ ];
32
+ const NO_SOURCES = "<!-- source channels: no sources/_index entries in last 7d -->";
33
+ const SIGNAL_PREVIEW_MAX = 160;
34
+ const SIGNAL_TOP = 5;
35
+ const ENTITY_TOP = 8;
36
+ /** Cloud budget: `floor(2000 * 0.4)` reserved tokens, minus the source block. */
37
+ const SIGNAL_TOKEN_RESERVE = Math.floor(2000 * 0.4);
38
+ const MIN_ENTITIES = 3;
39
+ function byCodeUnit(a, b) {
40
+ if (a < b)
41
+ return -1;
42
+ if (a > b)
43
+ return 1;
44
+ return 0;
45
+ }
46
+ function tokenCount(text) {
47
+ return Math.ceil(text.length / 4);
48
+ }
49
+ /** UTC dates `now`, `now − 1 day`, … from the epoch day, not the local calendar. */
50
+ function windowDates(now) {
51
+ const day = Math.floor(now.getTime() / 86_400_000);
52
+ const dates = [];
53
+ for (let i = 0; i < 7; i += 1)
54
+ dates.push(new Date((day - i) * 86_400_000).toISOString().slice(0, 10));
55
+ return dates;
56
+ }
57
+ /**
58
+ * `false` when `dir` does not exist. Throws when it resolves outside the company:
59
+ * a symlinked directory would otherwise copy another company's files into this brief.
60
+ */
61
+ function readableInside(companyDir, dir) {
62
+ let real;
63
+ try {
64
+ real = fs.realpathSync(dir);
65
+ }
66
+ catch (error) {
67
+ if (error.code === "ENOENT")
68
+ return false;
69
+ throw error;
70
+ }
71
+ const relative = path.relative(fs.realpathSync(companyDir), real);
72
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
73
+ throw new WriteBoundsError(`${path.relative(companyDir, dir)} resolves outside the company`);
74
+ }
75
+ return true;
76
+ }
77
+ function listDirectMd(companyDir, dir) {
78
+ if (!readableInside(companyDir, dir))
79
+ return [];
80
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
81
+ const names = [];
82
+ for (const entry of entries) {
83
+ // Files only. A directory whose name starts with `@` or `_` is never entered.
84
+ if (!entry.isFile() || !entry.name.endsWith(".md"))
85
+ continue;
86
+ names.push(entry.name);
87
+ }
88
+ names.sort(byCodeUnit);
89
+ return names;
90
+ }
91
+ /** `null` when the file is missing, unparsable, or not a JSON array. */
92
+ function readIndexArray(file) {
93
+ let raw;
94
+ try {
95
+ raw = fs.readFileSync(file, "utf8");
96
+ }
97
+ catch (error) {
98
+ if (error.code === "ENOENT")
99
+ return null;
100
+ throw error;
101
+ }
102
+ try {
103
+ const parsed = JSON.parse(raw);
104
+ return Array.isArray(parsed) ? parsed : null;
105
+ }
106
+ catch (error) {
107
+ if (error instanceof SyntaxError)
108
+ return null;
109
+ throw error;
110
+ }
111
+ }
112
+ function indexKey(entry) {
113
+ const keys = entry.keys;
114
+ const markdown = keys && typeof keys === "object" && !Array.isArray(keys)
115
+ ? keys.markdown
116
+ : undefined;
117
+ if (typeof markdown === "string" && markdown.length > 0)
118
+ return markdown;
119
+ const sourceRef = entry.source_ref;
120
+ const fallback = sourceRef && typeof sourceRef === "object" && !Array.isArray(sourceRef)
121
+ ? sourceRef.key
122
+ : undefined;
123
+ return typeof fallback === "string" && fallback.length > 0 ? fallback : null;
124
+ }
125
+ function mapChannel(value) {
126
+ if (value === "meeting" || value === "meetings")
127
+ return "meeting";
128
+ if (value === "email" || value === "slack" || value === "slack-dm" || value === "linear" || value === "notion") {
129
+ return value;
130
+ }
131
+ return null;
132
+ }
133
+ function loadSources(companyDir, now) {
134
+ const times = new Map();
135
+ const counts = new Map();
136
+ const dir = path.join(companyDir, "sources", "_index");
137
+ // Regular files only: a symlinked index file could point at another company's index.
138
+ const names = readableInside(companyDir, dir)
139
+ ? fs
140
+ .readdirSync(dir, { withFileTypes: true })
141
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
142
+ .map((entry) => entry.name)
143
+ .sort(byCodeUnit)
144
+ : [];
145
+ const windowFiles = new Set(windowDates(now).map((date) => `${date}.json`));
146
+ for (const name of names) {
147
+ const entries = readIndexArray(path.join(dir, name));
148
+ if (!entries)
149
+ continue;
150
+ const inWindow = windowFiles.has(name);
151
+ for (const entry of entries) {
152
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
153
+ continue;
154
+ const record = entry;
155
+ const key = indexKey(record);
156
+ if (key && typeof record.written_at === "string")
157
+ times.set(key, record.written_at);
158
+ if (!inWindow)
159
+ continue;
160
+ const channel = mapChannel(record.channel);
161
+ if (channel)
162
+ counts.set(channel, (counts.get(channel) ?? 0) + 1);
163
+ }
164
+ }
165
+ return { times, counts };
166
+ }
167
+ function loadSignals(companyDir, times) {
168
+ const items = [];
169
+ for (const type of SIGNAL_TYPES) {
170
+ const dir = path.join(companyDir, "signals", type.dir);
171
+ for (const name of listDirectMd(companyDir, dir)) {
172
+ const parsed = parseEntityFile(fs.readFileSync(path.join(dir, name), "utf8"));
173
+ if (!parsed)
174
+ continue;
175
+ const content = parsed.frontmatter.canonical_content;
176
+ if (typeof content !== "string" || content.length === 0)
177
+ continue;
178
+ const sourceRef = parsed.frontmatter.source_ref;
179
+ const time = typeof sourceRef === "string" ? times.get(sourceRef) ?? null : null;
180
+ items.push({ type: type.dir, rel: `signals/${type.dir}/${name}`, content, time });
181
+ }
182
+ }
183
+ return items;
184
+ }
185
+ function compareSignals(a, b) {
186
+ if (a.time !== null && b.time !== null) {
187
+ if (a.time !== b.time)
188
+ return a.time < b.time ? 1 : -1;
189
+ }
190
+ else if (a.time !== b.time) {
191
+ return a.time === null ? 1 : -1;
192
+ }
193
+ if (a.rel !== b.rel)
194
+ return a.rel < b.rel ? 1 : -1;
195
+ return 0;
196
+ }
197
+ function previewContent(content) {
198
+ const collapsed = content.replace(/\s+/g, " ").trim();
199
+ if (collapsed.length <= SIGNAL_PREVIEW_MAX)
200
+ return collapsed;
201
+ return `${collapsed.slice(0, SIGNAL_PREVIEW_MAX).trimEnd()}…`;
202
+ }
203
+ function renderSignalBlock(items) {
204
+ const sections = [];
205
+ for (const type of SIGNAL_TYPES) {
206
+ const rows = items.filter((item) => item.type === type.dir);
207
+ if (rows.length === 0)
208
+ continue;
209
+ const lines = rows.map((item) => {
210
+ const base = `- [${previewContent(item.content)}](${item.rel})`;
211
+ return item.time ? `${base} _(${item.time.slice(0, 10)})_` : base;
212
+ });
213
+ sections.push(`### ${type.heading}\n${lines.join("\n")}`);
214
+ }
215
+ if (sections.length === 0)
216
+ return null;
217
+ return ["## Recent Signals", ...sections].join("\n\n").trimEnd();
218
+ }
219
+ function confidenceOf(frontmatter) {
220
+ const value = frontmatter.confidence;
221
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : 0;
222
+ }
223
+ function compareEntities(a, b) {
224
+ if (a.score !== b.score)
225
+ return b.score - a.score;
226
+ if (a.name !== b.name)
227
+ return a.name < b.name ? -1 : 1;
228
+ if (a.file !== b.file)
229
+ return a.file < b.file ? -1 : 1;
230
+ return 0;
231
+ }
232
+ function loadEntities(companyDir) {
233
+ let total = 0;
234
+ const groups = [];
235
+ for (const type of ENTITY_TYPES) {
236
+ const dir = path.join(companyDir, "ontology", "entities", type.dir);
237
+ const items = [];
238
+ for (const name of listDirectMd(companyDir, dir)) {
239
+ const parsed = parseEntityFile(fs.readFileSync(path.join(dir, name), "utf8"));
240
+ if (!parsed)
241
+ continue;
242
+ const valid = validateEntityFrontmatter(parsed.frontmatter);
243
+ if (typeof valid === "string")
244
+ continue;
245
+ items.push({
246
+ file: name,
247
+ name: valid.canonical_name,
248
+ signalCount: valid.signal_count,
249
+ score: valid.signal_count * confidenceOf(parsed.frontmatter),
250
+ });
251
+ }
252
+ total += items.length;
253
+ items.sort(compareEntities);
254
+ groups.push({
255
+ heading: type.heading,
256
+ lines: items.slice(0, ENTITY_TOP).map((item) => `- [${item.name}](ontology/entities/${type.dir}/${item.file}) (${item.signalCount} signals)`),
257
+ });
258
+ }
259
+ return { total, groups };
260
+ }
261
+ function renderEntityBlock(slug, groups) {
262
+ const parts = [
263
+ `# ${slug} company brief`,
264
+ "_Rendered by hq core checkpoint from the local ontology._",
265
+ "## Key Entities",
266
+ ];
267
+ for (const group of groups) {
268
+ if (group.lines.length === 0)
269
+ continue;
270
+ parts.push(`### ${group.heading}\n${group.lines.join("\n")}`);
271
+ }
272
+ return parts.join("\n\n");
273
+ }
274
+ function renderSourceBlock(counts) {
275
+ const total = CHANNELS.reduce((sum, channel) => sum + (counts.get(channel.id) ?? 0), 0);
276
+ if (total === 0)
277
+ return NO_SOURCES;
278
+ const rows = CHANNELS.map((channel) => `| ${channel.label} | ${counts.get(channel.id) ?? 0} |`);
279
+ return ["## Source Channels", "", "_Last 7 days_", "", "| Channel | Count |", "| --- | ---: |", ...rows].join("\n");
280
+ }
281
+ function fitSignals(ranked, budget) {
282
+ const kept = ranked.slice();
283
+ while (kept.length > 0) {
284
+ const block = renderSignalBlock(kept);
285
+ if (block === null || tokenCount(block) <= budget)
286
+ break;
287
+ kept.pop();
288
+ }
289
+ return kept;
290
+ }
291
+ /** `null` when fewer than three valid person, project, company, or concept entities exist. */
292
+ export function renderCompanyBrief(company, now) {
293
+ const entities = loadEntities(company.dir);
294
+ if (entities.total < MIN_ENTITIES)
295
+ return null;
296
+ const sources = loadSources(company.dir, now);
297
+ const signals = loadSignals(company.dir, sources.times);
298
+ const ranked = [];
299
+ for (const type of SIGNAL_TYPES) {
300
+ ranked.push(...signals.filter((item) => item.type === type.dir).sort(compareSignals).slice(0, SIGNAL_TOP));
301
+ }
302
+ const sourceBlock = renderSourceBlock(sources.counts);
303
+ const budget = Math.max(0, SIGNAL_TOKEN_RESERVE - tokenCount(sourceBlock));
304
+ const signalBlock = renderSignalBlock(fitSignals(ranked, budget));
305
+ const blocks = [renderEntityBlock(company.slug, entities.groups)];
306
+ if (signalBlock)
307
+ blocks.push(signalBlock);
308
+ blocks.push(sourceBlock);
309
+ return `${blocks.join("\n\n")}\n`;
310
+ }
311
+ export function writeCompanyBrief(hqRoot, company, now) {
312
+ try {
313
+ const content = renderCompanyBrief(company, now);
314
+ if (content === null)
315
+ return { status: "skipped", reason: "fewer than 3 entities" };
316
+ const target = path.join(company.dir, COMPANY_BRIEF_FILE);
317
+ let existing = null;
318
+ try {
319
+ // Never follow a symlinked brief for the equality check; safeWriteFile rejects it below.
320
+ if (!fs.lstatSync(target).isSymbolicLink())
321
+ existing = fs.readFileSync(target);
322
+ }
323
+ catch (error) {
324
+ if (error.code !== "ENOENT")
325
+ throw error;
326
+ }
327
+ if (existing && existing.equals(Buffer.from(content, "utf8")))
328
+ return { status: "unchanged" };
329
+ safeWriteFile(hqRoot, [company.dir], target, content);
330
+ return { status: "written" };
331
+ }
332
+ catch (error) {
333
+ const failed = error;
334
+ return { status: "failed", reason: `${failed.name}: ${failed.message}` };
335
+ }
336
+ }
337
+ //# sourceMappingURL=company-brief.js.map
@@ -7,9 +7,11 @@
7
7
  * backend model to read. The model only fills a candidates file; everything
8
8
  * that affects identity, scope and cost stays in code.
9
9
  */
10
+ import { type CompanyBriefResult } from "./company-brief.js";
10
11
  import { type OntologyCompany } from "./company.js";
11
12
  export declare const ONTOLOGY_FLAG = "checkpoint.ontology";
12
13
  export declare const SHARED_INPUTS_FLAG = "checkpoint.ontology.shared-inputs";
14
+ export declare const COMPANY_BRIEF_FLAG = "checkpoint.ontology.company-brief";
13
15
  export declare const PLAN_FILE = "ontology-plan.json";
14
16
  export declare const CANDIDATES_FILE = "ontology-candidates.json";
15
17
  export declare const REPORT_FILE = "ontology-report.json";
@@ -66,6 +68,11 @@ export interface OntologyPlan {
66
68
  first_run: boolean;
67
69
  boundary: string | null;
68
70
  shared_inputs: boolean;
71
+ /** Present only when `checkpoint.ontology.company-brief` was on for this plan. */
72
+ company_brief?: {
73
+ status: CompanyBriefResult["status"];
74
+ reason?: string;
75
+ };
69
76
  inputs: PlanInput[];
70
77
  /** The session's summary, decisions and learnings, for the `session` input. */
71
78
  session_text: string | null;
@@ -98,10 +105,12 @@ export interface PlanOptions {
98
105
  export type PlanOutcome = {
99
106
  status: "skipped";
100
107
  reason: string;
108
+ companyBrief?: CompanyBriefResult;
101
109
  } | {
102
110
  status: "planned";
103
111
  plan: OntologyPlan;
104
112
  planPath: string;
113
+ companyBrief?: CompanyBriefResult;
105
114
  };
106
115
  export declare function ledgerPath(hqRoot: string, company: OntologyCompany): string;
107
116
  export declare function readLedger(hqRoot: string, company: OntologyCompany): Ledger | null;
@@ -10,10 +10,12 @@
10
10
  import { createHash } from "node:crypto";
11
11
  import * as fs from "node:fs";
12
12
  import * as path from "node:path";
13
+ import { writeCompanyBrief } from "./company-brief.js";
13
14
  import { resolveOntologyCompany, safeWriteFile, stateRoot } from "./company.js";
14
15
  import { buildEntityIndex, listKnowledgeFiles, listSignalFiles, readSignal } from "./tree.js";
15
16
  export const ONTOLOGY_FLAG = "checkpoint.ontology";
16
17
  export const SHARED_INPUTS_FLAG = "checkpoint.ontology.shared-inputs";
18
+ export const COMPANY_BRIEF_FLAG = "checkpoint.ontology.company-brief";
17
19
  export const PLAN_FILE = "ontology-plan.json";
18
20
  export const CANDIDATES_FILE = "ontology-candidates.json";
19
21
  export const REPORT_FILE = "ontology-report.json";
@@ -173,6 +175,10 @@ export async function planOntologyRun(options) {
173
175
  if (!(await options.readFlag(ONTOLOGY_FLAG, company)))
174
176
  return { status: "skipped", reason: "flag off" };
175
177
  const sharedInputs = await options.readFlag(SHARED_INPUTS_FLAG, company);
178
+ // Before the ledger, so a nothing-changed plan still records the brief.
179
+ const companyBrief = (await options.readFlag(COMPANY_BRIEF_FLAG, company))
180
+ ? writeCompanyBrief(hqRoot, company, options.now ?? new Date())
181
+ : undefined;
176
182
  const touched = sessionTouched(hqRoot, company, payload.files);
177
183
  const { ledger, firstRun } = refreshLedger(company, readLedger(hqRoot, company), touched, sharedInputs);
178
184
  const pending = Object.entries(ledger.entries)
@@ -214,7 +220,8 @@ export async function planOntologyRun(options) {
214
220
  }
215
221
  writeLedger(hqRoot, company, ledger);
216
222
  if (inputs.length === 0) {
217
- return { status: "skipped", reason: pending.length > 0 ? "over budget" : "nothing changed" };
223
+ const reason = pending.length > 0 ? "over budget" : "nothing changed";
224
+ return companyBrief ? { status: "skipped", reason, companyBrief } : { status: "skipped", reason };
218
225
  }
219
226
  // Session content is scoped to the session's user (design section 6, D2).
220
227
  // v1 records the existing entities it names as links in the report and
@@ -243,6 +250,13 @@ export async function planOntologyRun(options) {
243
250
  first_run: firstRun,
244
251
  boundary: ledger.boundary,
245
252
  shared_inputs: sharedInputs,
253
+ ...(companyBrief
254
+ ? {
255
+ company_brief: companyBrief.reason
256
+ ? { status: companyBrief.status, reason: companyBrief.reason }
257
+ : { status: companyBrief.status },
258
+ }
259
+ : {}),
246
260
  inputs,
247
261
  session_text: sessionText,
248
262
  pending_after_run: remaining,
@@ -258,7 +272,7 @@ export async function planOntologyRun(options) {
258
272
  fs.mkdirSync(runDir, { recursive: true });
259
273
  const planPath = path.join(runDir, PLAN_FILE);
260
274
  fs.writeFileSync(planPath, `${JSON.stringify(plan, null, 2)}\n`);
261
- return { status: "planned", plan, planPath };
275
+ return companyBrief ? { status: "planned", plan, planPath, companyBrief } : { status: "planned", plan, planPath };
262
276
  }
263
277
  const SESSION_TEXT_MAX_BYTES = 8_000;
264
278
  /** The session summary, decisions and learnings from the payload, capped. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.201.4",
3
+ "version": "5.202.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {