@indigoai-us/hq-cli 5.230.1 → 5.231.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,28 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.231.0] — 2026-09-27
6
+
7
+ ### Added
8
+
9
+ - Behind the off-by-default `checkpoint.ontology.first-name-people` flag, the
10
+ checkpoint ontology instructions tell the model to name a person by their
11
+ first name when neither the input nor the company's entities give more,
12
+ instead of leaving them out. With the flag on, apply also defers a one-word
13
+ person whose word starts an existing person's fuller name or alias (`John`
14
+ when `John Smith` exists), since the model cannot see the entity tree. With
15
+ the flag off the instructions and apply are unchanged.
16
+
17
+ ## [5.230.2] — 2026-09-27
18
+
19
+ ### Fixed
20
+
21
+ - Resuming a terminal Outpost lane whose senior lives on the primary now checks
22
+ hire-grant capacity on both hosts using the primary hierarchy, then syncs the
23
+ running owner record back to the primary. An owner CLI that rejects the
24
+ hierarchy context now fails with an upgrade-required error instead of retrying
25
+ without the grant check.
26
+
5
27
  ## [5.230.1] — 2026-09-27
6
28
 
7
29
  ### Fixed
@@ -8056,6 +8056,9 @@ export declare const COMMAND_CATALOG: readonly [{
8056
8056
  }, {
8057
8057
  readonly flags: "--worker <profile>";
8058
8058
  readonly description: "Attach a personal worker profile for the resumed turn";
8059
+ }, {
8060
+ readonly flags: "--grant-context <base64url>";
8061
+ readonly description: "";
8059
8062
  }, {
8060
8063
  readonly flags: "--keep-alive";
8061
8064
  readonly description: "Automatically resume done lead lanes while their story remains open";
@@ -10421,6 +10421,10 @@ export const COMMAND_CATALOG = [
10421
10421
  "flags": "--worker <profile>",
10422
10422
  "description": "Attach a personal worker profile for the resumed turn"
10423
10423
  },
10424
+ {
10425
+ "flags": "--grant-context <base64url>",
10426
+ "description": ""
10427
+ },
10424
10428
  {
10425
10429
  "flags": "--keep-alive",
10426
10430
  "description": "Automatically resume done lead lanes while their story remains open"
@@ -1039,15 +1039,36 @@ async function mapWithConcurrency(values, concurrency, map) {
1039
1039
  await Promise.all(workers);
1040
1040
  return results;
1041
1041
  }
1042
- async function withResumeHireAdmission(ctx, lane, fn) {
1042
+ async function withResumeHireAdmission(ctx, lane, fn, grantContext) {
1043
1043
  const hqRoot = ctx.hqRoot();
1044
+ if (grantContext !== undefined && (!ctx.isSsmOwnerDispatch() ||
1045
+ lane.host.kind !== "outpost" ||
1046
+ !laneIsHostedHere(ctx, lane) ||
1047
+ lane.senior.kind !== "lane" ||
1048
+ !sameHireGrantSenior(lane.senior, grantContext.expected_senior))) {
1049
+ throw new LanesError("hire_grant_senior_changed", `Lane ${lane.lane_id}'s senior changed after the primary authorized its resume`, { lane_id: lane.lane_id, expected_senior: grantContext.expected_senior, current_senior: lane.senior });
1050
+ }
1044
1051
  if (!isTerminalLaneState(lane.state) ||
1045
1052
  lane.senior.kind !== "lane" ||
1046
1053
  !lane.senior.id) {
1047
1054
  return fn();
1048
1055
  }
1056
+ if (grantContext === undefined &&
1057
+ lane.host.kind === "outpost" &&
1058
+ laneIsHostedHere(ctx, lane) &&
1059
+ !listLaneIds(hqRoot).includes(lane.senior.id)) {
1060
+ throw new LanesError("hire_grant_context_required", `Resume of Outpost lane ${lane.lane_id} requires the primary senior's hire-grant context`, { lane_id: lane.lane_id, senior_lane_id: lane.senior.id });
1061
+ }
1049
1062
  return withHireGrantAdmission(hqRoot, lane.senior.id, (snapshot) => {
1050
1063
  const current = readLane(hqRoot, lane.lane_id);
1064
+ if (grantContext !== undefined &&
1065
+ !sameHireGrantSenior(current.senior, grantContext.expected_senior)) {
1066
+ throw new LanesError("hire_grant_senior_changed", `Lane ${current.lane_id}'s senior changed before the Outpost resumed it`, {
1067
+ lane_id: current.lane_id,
1068
+ expected_senior: grantContext.expected_senior,
1069
+ current_senior: current.senior,
1070
+ });
1071
+ }
1051
1072
  // A different resume already reactivated this lane; it occupies the slot
1052
1073
  // in the shared snapshot and is not a second hire.
1053
1074
  if (!isTerminalLaneState(current.state))
@@ -1056,7 +1077,7 @@ async function withResumeHireAdmission(ctx, lane, fn) {
1056
1077
  if (refusal !== null)
1057
1078
  throw refusal;
1058
1079
  return fn();
1059
- });
1080
+ }, grantContext?.hierarchy);
1060
1081
  }
1061
1082
  const CAPACITY_PROBE_TIMEOUT_MS = 15_000;
1062
1083
  const MAX_PLACEMENT_OUTPOSTS = 16;
@@ -3569,6 +3590,10 @@ function outpostRejectedProfileRepinOption(result) {
3569
3590
  return result.exitCode !== 0 &&
3570
3591
  /unknown option\s+(?:['"])?--profile-repin-context(?:['"])?(?:\s|$)/i.test(`${result.stderr}\n${result.stdout}`);
3571
3592
  }
3593
+ function outpostRejectedGrantContextOption(result) {
3594
+ return result.exitCode !== 0 &&
3595
+ /unknown option\s+(?:['"])?--grant-context(?:['"])?(?:\s|$)/i.test(`${result.stderr}\n${result.stdout}`);
3596
+ }
3572
3597
  function verifyOutpostProfileContent(hqRoot, lane) {
3573
3598
  if (lane.host.kind !== "outpost" || lane.worker_profile === undefined)
3574
3599
  return;
@@ -3713,9 +3738,14 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3713
3738
  grantHierarchy = hierarchy;
3714
3739
  grantContext = createOutpostHireGrantContext(lane, hierarchy);
3715
3740
  }
3741
+ else if (action === "resume" &&
3742
+ lane.senior.kind === "lane") {
3743
+ grantContext = createOutpostHireGrantContext(lane, loadAllLanes(ctx.hqRoot()));
3744
+ }
3716
3745
  let dispatched;
3746
+ let grantContextRejected = false;
3717
3747
  try {
3718
- const dispatchCommand = (repinContext) => ctx.dispatchOutpostExec({
3748
+ const dispatchCommand = (contexts) => ctx.dispatchOutpostExec({
3719
3749
  outpostId,
3720
3750
  command: buildOutpostLaneCommand({
3721
3751
  action: action === "grant"
@@ -3738,20 +3768,38 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3738
3768
  hire: options.hire,
3739
3769
  senior: options.senior,
3740
3770
  reason: options.reason,
3741
- grantContext,
3742
- ...(repinContext !== undefined ? { profileRepinContext: repinContext } : {}),
3771
+ grantContext: contexts.grantContext,
3772
+ profileRepinContext: contexts.profileRepinContext,
3743
3773
  compact: options.compact,
3744
3774
  }),
3745
3775
  });
3746
- dispatched = await dispatchCommand(profileRepinContext);
3747
- if (profileRepinContext !== undefined &&
3748
- outpostRejectedProfileRepinOption(dispatched)) {
3749
- dispatched = await dispatchCommand();
3776
+ let contexts = { grantContext, profileRepinContext };
3777
+ dispatched = await dispatchCommand(contexts);
3778
+ for (let retry = 0; action === "resume" && retry < 2; retry += 1) {
3779
+ if (contexts.grantContext !== undefined && outpostRejectedGrantContextOption(dispatched)) {
3780
+ grantContextRejected = true;
3781
+ break;
3782
+ }
3783
+ else if (contexts.profileRepinContext !== undefined &&
3784
+ outpostRejectedProfileRepinOption(dispatched)) {
3785
+ contexts = { ...contexts, profileRepinContext: undefined };
3786
+ }
3787
+ else {
3788
+ break;
3789
+ }
3790
+ dispatched = await dispatchCommand(contexts);
3750
3791
  }
3751
3792
  }
3752
3793
  catch (err) {
3753
3794
  throw new LanesError("outpost_dispatch_failed", `Could not dispatch ${action} for lane ${lane.lane_id} to Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { lane_id: lane.lane_id, outpost_id: outpostId });
3754
3795
  }
3796
+ if (grantContextRejected) {
3797
+ throw new LanesError("outpost_cli_upgrade_required", `Outpost ${outpostId} needs an hq-cli release containing this fix before it can resume lane ${lane.lane_id} under its primary-hosted senior`, {
3798
+ lane_id: lane.lane_id,
3799
+ outpost_id: outpostId,
3800
+ required_option: "--grant-context",
3801
+ });
3802
+ }
3755
3803
  const ownerError = ownerErrorFromOutput(dispatched.stdout);
3756
3804
  if (options.effort !== undefined && outpostRejectedEffortOption(dispatched)) {
3757
3805
  throw new LanesError("outpost_effort_unsupported", `Outpost ${outpostId} rejected --effort; update hq-cli on that Outpost before changing a lane's effort remotely`, {
@@ -3766,7 +3814,9 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3766
3814
  });
3767
3815
  }
3768
3816
  if (ownerError) {
3769
- if (action === "resume" && ownerError.error === "worker_profile_hash_mismatch") {
3817
+ if (action === "resume" &&
3818
+ (ownerError.error === "worker_profile_hash_mismatch" ||
3819
+ ownerError.error.startsWith("hire_grant_"))) {
3770
3820
  throw new LanesError(ownerError.error, ownerError.message, {
3771
3821
  ...ownerError.details,
3772
3822
  lane_id: lane.lane_id,
@@ -3821,7 +3871,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3821
3871
  Array.isArray(remote.changed_descendant_ids)
3822
3872
  : (remote.delivery_status === "worker_spawned" ||
3823
3873
  remote.delivery_status === "queued_on_target_account") &&
3824
- (options.worker === undefined || remote.lane !== undefined));
3874
+ remote.lane !== undefined);
3825
3875
  if (!resultConfirmed) {
3826
3876
  throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} completed exec without a confirmed ${action} result for lane ${lane.lane_id}`, {
3827
3877
  lane_id: lane.lane_id,
@@ -3842,7 +3892,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3842
3892
  });
3843
3893
  return;
3844
3894
  }
3845
- if (action === "reparent" || (action === "resume" && options.worker !== undefined)) {
3895
+ if (action === "reparent" || action === "resume") {
3846
3896
  const ownerLane = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, remote.lane, undefined, { allowNonTerminal: true });
3847
3897
  const descendants = [];
3848
3898
  if (action === "reparent") {
@@ -6549,16 +6599,25 @@ function laneWithRecoveredProviderSession(hqRoot, lane) {
6549
6599
  async function runResume(ctx, laneId, opts) {
6550
6600
  const hqRoot = ctx.hqRoot();
6551
6601
  let lane = readLane(hqRoot, laneId);
6602
+ const hireGrantContext = opts.grantContext === undefined
6603
+ ? undefined
6604
+ : (() => {
6605
+ if (!ctx.isSsmOwnerDispatch()) {
6606
+ throw new LanesError("hire_grant_dispatch_unauthorized", "Resume grant context is only valid on an Outpost owner dispatch", { lane_id: laneId });
6607
+ }
6608
+ return parseOutpostHireGrantContext(opts.grantContext, laneId);
6609
+ })();
6552
6610
  if (opts.profileRepinContext !== undefined && !ctx.isSsmOwnerDispatch()) {
6553
6611
  throw new LanesError("worker_profile_hash_mismatch", "Profile re-pin context is only valid on an Outpost owner dispatch", { lane_id: laneId });
6554
6612
  }
6555
6613
  if (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane)) {
6614
+ await withResumeHireAdmission(ctx, lane, () => undefined);
6556
6615
  await dispatchOutpostLaneCommand(ctx, lane, "resume", opts);
6557
6616
  return;
6558
6617
  }
6559
6618
  // Fail before provider/session work if a terminal child would consume a
6560
6619
  // slot that its senior has already assigned elsewhere.
6561
- await withResumeHireAdmission(ctx, lane, () => undefined);
6620
+ await withResumeHireAdmission(ctx, lane, () => undefined, hireGrantContext);
6562
6621
  assertLaneRunsHereForOperatorMutation(ctx, lane, "resume");
6563
6622
  const cliEffort = parseCliEffort(lane.provider, opts.effort);
6564
6623
  if (cliEffort !== undefined && !providerSupportsSessionResume(lane.provider)) {
@@ -6768,7 +6827,7 @@ async function runResume(ctx, laneId, opts) {
6768
6827
  compact: opts.compact !== false,
6769
6828
  effort: resumeEffort ?? recordedResumeEffort(lane),
6770
6829
  });
6771
- });
6830
+ }, hireGrantContext);
6772
6831
  }, ctx.sleep);
6773
6832
  }
6774
6833
  if (text !== undefined && messageId !== undefined) {
@@ -9567,6 +9626,7 @@ export function registerLanesCommand(parent, deps = {}) {
9567
9626
  .option("--effort <level>", "Reasoning effort for the resumed provider turn")
9568
9627
  .option("--worker <profile>", "Attach a personal worker profile for the resumed turn")
9569
9628
  .addOption(new Option("--profile-repin-context <base64url>").hideHelp())
9629
+ .addOption(new Option("--grant-context <base64url>").hideHelp())
9570
9630
  .option("--keep-alive", "Automatically resume done lead lanes while their story remains open")
9571
9631
  .option("--mesh-company <slug>", "Attach Work Mesh attribution after verifying company membership")
9572
9632
  .option("--no-compact", "Skip the resume-time compaction check for this resume")
@@ -128,6 +128,13 @@ export declare function resolveCandidate(index: EntityIndex, type: string, canon
128
128
  * (`Chaudhry Hassaan Saleem` and `Hassaan Saleem`).
129
129
  */
130
130
  export declare function overlappingEntities(index: EntityIndex, type: string, canonical: string, aliases: string[]): string[];
131
+ /**
132
+ * Existing people a one-word person candidate may be: the word is the first
133
+ * word of their name or of one of their aliases, and that name has two words
134
+ * or more (`John` and `John Smith`). The model never sees the entity tree, so
135
+ * it cannot tell a first name that the company already holds in full.
136
+ */
137
+ export declare function firstNameOverlap(index: EntityIndex, canonical: string): string[];
131
138
  export declare function applyOntologyRun(options: ApplyOptions): ApplyReport;
132
139
  export {};
133
140
  //# sourceMappingURL=apply.d.ts.map
@@ -113,6 +113,30 @@ export function overlappingEntities(index, type, canonical, aliases) {
113
113
  }
114
114
  return [...paths].sort();
115
115
  }
116
+ /**
117
+ * Existing people a one-word person candidate may be: the word is the first
118
+ * word of their name or of one of their aliases, and that name has two words
119
+ * or more (`John` and `John Smith`). The model never sees the entity tree, so
120
+ * it cannot tell a first name that the company already holds in full.
121
+ */
122
+ export function firstNameOverlap(index, canonical) {
123
+ const words = normalizeEntityName(canonical, "person").split(" ");
124
+ if (words.length !== 1 || !words[0])
125
+ return [];
126
+ const prefix = entityKey("person", "");
127
+ const paths = new Set();
128
+ for (const map of [index.canonical, index.alias]) {
129
+ for (const [name, rels] of map) {
130
+ if (!name.startsWith(prefix))
131
+ continue;
132
+ const other = name.slice(prefix.length).split(" ");
133
+ if (other.length >= 2 && other[0] === words[0])
134
+ for (const rel of rels)
135
+ paths.add(rel);
136
+ }
137
+ }
138
+ return [...paths].sort();
139
+ }
116
140
  function readJson(file) {
117
141
  return JSON.parse(fs.readFileSync(file, "utf8"));
118
142
  }
@@ -335,6 +359,12 @@ export function applyOntologyRun(options) {
335
359
  report.suspect_duplicates.push({ name: canonical, type: candidate.type, paths: overlap });
336
360
  continue;
337
361
  }
362
+ const firstName = plan.first_name_people === true && candidate.type === "person" ? firstNameOverlap(index, canonical) : [];
363
+ if (firstName.length > 0) {
364
+ note(report.deferred, "suspect_duplicate");
365
+ report.suspect_duplicates.push({ name: canonical, type: candidate.type, paths: firstName });
366
+ continue;
367
+ }
338
368
  // Tier 5: create.
339
369
  if (candidate.confidence < MIN_CREATE_CONFIDENCE) {
340
370
  note(report.rejected, "low_confidence");
@@ -48,6 +48,17 @@ export declare const ENTITY_HINTS_FLAG = "checkpoint.ontology.entity-hints";
48
48
  * hinted runs).
49
49
  */
50
50
  export declare const NO_LINK_ALIASES_FLAG = "checkpoint.ontology.no-link-aliases";
51
+ /**
52
+ * A person the input and the company's entities give only a first name for is
53
+ * named by that first name instead of being left out. CO-44: on the CO-41
54
+ * grok-4.6 runs the model read "not a first name alone" as "skip them", and
55
+ * dropped people such as Amir and Sebastian, whose own entities carry only a
56
+ * first name. People were 24 of the 138 missed gold entities over three runs.
57
+ * The plan carries `first_name_people: true`, and apply then defers a one-word
58
+ * person whose word starts an existing person's fuller name or alias, since
59
+ * the model cannot see that `John Smith` already exists.
60
+ */
61
+ export declare const FIRST_NAME_PEOPLE_FLAG = "checkpoint.ontology.first-name-people";
51
62
  export declare const PLAN_FILE = "ontology-plan.json";
52
63
  export declare const CANDIDATES_FILE = "ontology-candidates.json";
53
64
  export declare const REPORT_FILE = "ontology-report.json";
@@ -116,6 +127,8 @@ export interface OntologyPlan {
116
127
  multi_writer?: MultiWriterStart;
117
128
  /** Present, as false, only when `checkpoint.ontology.no-link-aliases` was on for this plan. */
118
129
  link_aliases?: false;
130
+ /** Set by checkpoint.ontology.first-name-people: apply defers a one-word person who may be an existing fuller name. */
131
+ first_name_people?: true;
119
132
  inputs: PlanInput[];
120
133
  /** The session's summary, decisions and learnings, for the `session` input. */
121
134
  session_text: string | null;
@@ -160,6 +173,8 @@ export type PlanOutcome = {
160
173
  };
161
174
  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.";
162
175
  export declare const ENTITY_HINTS_INSTRUCTIONS: string[];
176
+ export declare const PERSON_NAME_INSTRUCTION = "Name a person by the full name the input or the company's entities give, not a first name alone.";
177
+ export declare const FIRST_NAME_PEOPLE_INSTRUCTION = "Name a person by the full name the input or the company's entities give. When neither gives more than a first name, name the person by that first name; do not leave them out.";
163
178
  export declare function ledgerPath(hqRoot: string, company: OntologyCompany): string;
164
179
  export declare function readLedger(hqRoot: string, company: OntologyCompany): Ledger | null;
165
180
  export declare function writeLedger(hqRoot: string, company: OntologyCompany, ledger: Ledger): void;
@@ -52,6 +52,17 @@ export const ENTITY_HINTS_FLAG = "checkpoint.ontology.entity-hints";
52
52
  * hinted runs).
53
53
  */
54
54
  export const NO_LINK_ALIASES_FLAG = "checkpoint.ontology.no-link-aliases";
55
+ /**
56
+ * A person the input and the company's entities give only a first name for is
57
+ * named by that first name instead of being left out. CO-44: on the CO-41
58
+ * grok-4.6 runs the model read "not a first name alone" as "skip them", and
59
+ * dropped people such as Amir and Sebastian, whose own entities carry only a
60
+ * first name. People were 24 of the 138 missed gold entities over three runs.
61
+ * The plan carries `first_name_people: true`, and apply then defers a one-word
62
+ * person whose word starts an existing person's fuller name or alias, since
63
+ * the model cannot see that `John Smith` already exists.
64
+ */
65
+ export const FIRST_NAME_PEOPLE_FLAG = "checkpoint.ontology.first-name-people";
55
66
  export const PLAN_FILE = "ontology-plan.json";
56
67
  export const CANDIDATES_FILE = "ontology-candidates.json";
57
68
  export const REPORT_FILE = "ontology-report.json";
@@ -68,11 +79,13 @@ export const ENTITY_HINTS_INSTRUCTIONS = [
68
79
  "A knowledge input's known_entities lists entities that already exist in the company and whose names appear in its text. When the input refers to one of them, under that name or another, use that entity's name and type exactly and do not name a new entity for it.",
69
80
  "Skip rows of a glossary or translation table, file and directory paths, and names built from tags or slugs.",
70
81
  ];
82
+ export const PERSON_NAME_INSTRUCTION = "Name a person by the full name the input or the company's entities give, not a first name alone.";
83
+ export const FIRST_NAME_PEOPLE_INSTRUCTION = "Name a person by the full name the input or the company's entities give. When neither gives more than a first name, name the person by that first name; do not leave them out.";
71
84
  const CANDIDATE_INSTRUCTIONS = [
72
85
  `Write ${CANDIDATES_FILE} in the run directory as {"schema":"${CANDIDATES_SCHEMA}","candidates":[...]} and nothing else.`,
73
86
  "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.",
74
87
  "Read only the files and line ranges this plan lists. For a signal, read its body and name the entities in missing_refs if the signal supports them.",
75
- "Name the specific people, projects, products, companies and named concepts that the input says something about. That includes the company that owns this knowledge and its own products when the input describes them. Name a third-party tool, vendor or service only when a section of the input is about it; skip ones that appear in a list, a stack summary or a passing mention. Skip generic words, file names and headings. Name a person by the full name the input or the company's entities give, not a first name alone.",
88
+ "Name the specific people, projects, products, companies and named concepts that the input says something about. That includes the company that owns this knowledge and its own products when the input describes them. Name a third-party tool, vendor or service only when a section of the input is about it; skip ones that appear in a list, a stack summary or a passing mention. Skip generic words, file names and headings.",
76
89
  "For the session input (its text is session_text in this plan), name only entities that already exist in the company and that the session did work on. Session candidates are recorded as links, never created.",
77
90
  "Do not create, edit, move or delete anything under ontology/, signals/ or sources/ yourself.",
78
91
  ];
@@ -229,6 +242,7 @@ export async function planOntologyRun(options) {
229
242
  const multiWriterOn = await options.readFlag(MULTI_WRITER_FLAG, company);
230
243
  const hintsOn = await options.readFlag(ENTITY_HINTS_FLAG, company);
231
244
  const noLinkAliases = await options.readFlag(NO_LINK_ALIASES_FLAG, company);
245
+ const firstNamePeople = await options.readFlag(FIRST_NAME_PEOPLE_FLAG, company);
232
246
  const touched = sessionTouched(hqRoot, company, payload.files);
233
247
  // A first run takes its boundary from the entity tree, so the ledger is
234
248
  // refreshed before merge-back moves any `last_updated` forward.
@@ -342,6 +356,7 @@ export async function planOntologyRun(options) {
342
356
  : {}),
343
357
  ...(multiWriter ? { multi_writer: multiWriter } : {}),
344
358
  ...(noLinkAliases ? { link_aliases: false } : {}),
359
+ ...(firstNamePeople ? { first_name_people: true } : {}),
345
360
  inputs,
346
361
  session_text: sessionText,
347
362
  pending_after_run: remaining,
@@ -355,7 +370,7 @@ export async function planOntologyRun(options) {
355
370
  instructions: [
356
371
  ...CANDIDATE_INSTRUCTIONS.slice(0, 3),
357
372
  ...(inputs.some((input) => input.read_path) ? [READ_PATH_INSTRUCTION] : []),
358
- CANDIDATE_INSTRUCTIONS[3],
373
+ `${CANDIDATE_INSTRUCTIONS[3]} ${firstNamePeople ? FIRST_NAME_PEOPLE_INSTRUCTION : PERSON_NAME_INSTRUCTION}`,
359
374
  ...(inputs.some((input) => input.known_entities) ? ENTITY_HINTS_INSTRUCTIONS : []),
360
375
  ...CANDIDATE_INSTRUCTIONS.slice(4),
361
376
  ],
@@ -4,16 +4,16 @@ export type LaneHireGrantRow = Pick<LaneRecord, "lane_id" | "state" | "senior" |
4
4
  export declare function aliveDirectChildren<T extends LaneHireGrantRow>(lanes: readonly T[], parentLaneId: string): T[];
5
5
  export declare function assignedChildHireGrants(children: readonly LaneHireGrantRow[], exceptLaneId?: string): number;
6
6
  export interface HireGrantAdmissionSnapshot {
7
- parent: LaneRecord;
8
- children: LaneRecord[];
7
+ parent: LaneHireGrantRow;
8
+ children: LaneHireGrantRow[];
9
9
  parentGrant: number;
10
10
  availableChildGrant: number;
11
11
  }
12
12
  /** Shared refusal logic for resuming a terminal lane under an active senior. */
13
13
  export declare function terminalChildResumeHireGrantError(current: LaneRecord, snapshot: HireGrantAdmissionSnapshot): LanesError | null;
14
- export declare function hireGrantCapacityError(parent: LaneRecord, children: readonly LaneRecord[]): LanesError;
14
+ export declare function hireGrantCapacityError(parent: LaneHireGrantRow, children: readonly LaneHireGrantRow[]): LanesError;
15
15
  /** Keep the occupancy snapshot and the resulting lane write in one parent-lock scope. */
16
- export declare function withHireGrantAdmission<T>(hqRoot: string, parentLaneId: string, fn: (snapshot: HireGrantAdmissionSnapshot) => T): T;
16
+ export declare function withHireGrantAdmission<T>(hqRoot: string, parentLaneId: string, fn: (snapshot: HireGrantAdmissionSnapshot) => T, suppliedHierarchy?: readonly LaneHireGrantRow[]): T;
17
17
  export interface HireGrantBackfillRow {
18
18
  lane_id: string;
19
19
  existing_grant: number | null;
@@ -72,10 +72,28 @@ export function hireGrantCapacityError(parent, children) {
72
72
  });
73
73
  }
74
74
  /** Keep the occupancy snapshot and the resulting lane write in one parent-lock scope. */
75
- export function withHireGrantAdmission(hqRoot, parentLaneId, fn) {
75
+ export function withHireGrantAdmission(hqRoot, parentLaneId, fn, suppliedHierarchy) {
76
76
  return withLaneHireLocks(hqRoot, [parentLaneId], () => {
77
- const parent = readLane(hqRoot, parentLaneId);
78
- const allLanes = listLaneIds(hqRoot).map((id) => readLane(hqRoot, id));
77
+ const ownerLanes = listLaneIds(hqRoot).map((id) => readLane(hqRoot, id));
78
+ let allLanes;
79
+ let parent;
80
+ if (suppliedHierarchy === undefined) {
81
+ parent = readLane(hqRoot, parentLaneId);
82
+ allLanes = ownerLanes;
83
+ }
84
+ else {
85
+ const byId = new Map(suppliedHierarchy.map((lane) => [lane.lane_id, lane]));
86
+ // The owner copy is authoritative for every lane it stores; the supplied
87
+ // primary snapshot fills only hierarchy rows that live on another host.
88
+ for (const lane of ownerLanes)
89
+ byId.set(lane.lane_id, lane);
90
+ allLanes = [...byId.values()];
91
+ const suppliedParent = byId.get(parentLaneId);
92
+ if (!suppliedParent) {
93
+ throw new LanesError("hire_grant_context_invalid", `The dispatched hierarchy is missing senior lane ${parentLaneId}`, { senior_lane_id: parentLaneId });
94
+ }
95
+ parent = suppliedParent;
96
+ }
79
97
  const children = aliveDirectChildren(allLanes, parentLaneId);
80
98
  const parentGrant = parent.hire_grant ?? 0;
81
99
  const availableChildGrant = Math.max(0, parentGrant - assignedChildHireGrants(children));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.230.1",
3
+ "version": "5.231.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {