@indigoai-us/hq-cli 5.291.0 → 5.293.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +53 -8
  2. package/assets/scaffold/core/scripts/handoff-finalize.sh +18 -0
  3. package/assets/scaffold/core/scripts/handoff-post.sh +71 -6
  4. package/assets/scaffold/core/scripts/hq-delegate-bundle.sh +307 -0
  5. package/assets/scaffold/core/scripts/hq-delegate-grant.sh +226 -0
  6. package/assets/scaffold/core/scripts/hq-delegate-publish.sh +91 -0
  7. package/assets/scaffold/core/scripts/hq-delegate-send.sh +259 -0
  8. package/assets/scaffold/core/scripts/hq-delegate-verify.sh +213 -0
  9. package/assets/scaffold/core/scripts/hq-job-notify.sh +389 -0
  10. package/assets/scaffold/core/scripts/hq-job-probe.sh +835 -0
  11. package/assets/scaffold/core/scripts/hq-job-remediate.sh +359 -0
  12. package/assets/scaffold/core/scripts/hq-job-run.sh +1025 -0
  13. package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +830 -0
  14. package/dist/command-catalog.generated.d.ts +109 -1
  15. package/dist/command-catalog.generated.js +136 -1
  16. package/dist/commands/core-delegate-w1b.d.ts +8 -0
  17. package/dist/commands/core-delegate-w1b.js +1049 -0
  18. package/dist/commands/core-delegate.d.ts +28 -0
  19. package/dist/commands/core-delegate.js +15 -13
  20. package/dist/commands/core-handoff.js +152 -23
  21. package/dist/commands/core-jobs-native.d.ts +15 -0
  22. package/dist/commands/core-jobs-native.js +709 -2
  23. package/dist/commands/core-ontology.d.ts +22 -4
  24. package/dist/commands/core-ontology.js +90 -8
  25. package/dist/commands/core.js +10 -0
  26. package/dist/commands/lanes-move.js +54 -1
  27. package/dist/commands/lanes-questions.js +4 -0
  28. package/dist/commands/lanes.d.ts +2 -0
  29. package/dist/commands/lanes.js +119 -5
  30. package/dist/lib/checkpoint-ontology/drop-aliases.d.ts +41 -0
  31. package/dist/lib/checkpoint-ontology/drop-aliases.js +76 -0
  32. package/dist/lib/checkpoint-ontology/entity-file.d.ts +6 -3
  33. package/dist/lib/checkpoint-ontology/entity-file.js +12 -6
  34. package/dist/lib/checkpoint-ontology/merge-copies.d.ts +27 -8
  35. package/dist/lib/checkpoint-ontology/merge-copies.js +64 -97
  36. package/dist/lib/core-utils/sentry-report.js +2 -2
  37. package/dist/lib/daemon/flag.d.ts +2 -0
  38. package/dist/lib/daemon/flag.js +2 -0
  39. package/dist/lib/daemon/run.d.ts +11 -0
  40. package/dist/lib/daemon/run.js +17 -1
  41. package/dist/lib/lanes/autopilot.d.ts +13 -1
  42. package/dist/lib/lanes/autopilot.js +271 -28
  43. package/dist/lib/lanes/grants.d.ts +1 -1
  44. package/dist/lib/lanes/grants.js +7 -5
  45. package/dist/lib/lanes/index.d.ts +1 -1
  46. package/dist/lib/lanes/keep-alive.d.ts +5 -0
  47. package/dist/lib/lanes/keep-alive.js +9 -4
  48. package/dist/lib/lanes/questions.d.ts +2 -1
  49. package/dist/lib/lanes/questions.js +2 -0
  50. package/dist/lib/lanes/reconcile.js +186 -9
  51. package/dist/lib/lanes/spawn.js +4 -0
  52. package/dist/lib/lanes/store.js +21 -1
  53. package/dist/lib/lanes/types.d.ts +13 -1
  54. package/dist/lib/lanes/types.js +2 -2
  55. package/dist/lib/lanes/watcher.js +3 -1
  56. package/dist/utils/scaffold-sibling-references.js +4 -4
  57. package/dist/utils/self-update.d.ts +13 -0
  58. package/dist/utils/self-update.js +33 -7
  59. package/dist/utils/staged-install.d.ts +34 -0
  60. package/dist/utils/staged-install.js +296 -0
  61. package/dist/utils/version-gate.d.ts +15 -0
  62. package/dist/utils/version-gate.js +46 -2
  63. package/package.json +1 -1
  64. package/scripts/mesh-daemon-if-stale.mjs +4 -0
@@ -9,7 +9,8 @@
9
9
  */
10
10
  import { Command } from "commander";
11
11
  import { type FlagReader } from "../lib/checkpoint-ontology/plan.js";
12
- import { type DeleteVaultKey, type ListedPair } from "../lib/checkpoint-ontology/merge-copies.js";
12
+ import { type AliasDrop } from "../lib/checkpoint-ontology/drop-aliases.js";
13
+ import { type DeleteVaultKey, type ListedPair, type MergeCopiesReport } from "../lib/checkpoint-ontology/merge-copies.js";
13
14
  export declare const ONTOLOGY_PAYLOAD_FILE = "payload.json";
14
15
  /** The hq-flags read, scoped to the payload company. */
15
16
  export declare const registryFlagReader: FlagReader;
@@ -31,12 +32,15 @@ export interface MergeCopiesCommandOptions {
31
32
  apply?: boolean;
32
33
  delete?: boolean;
33
34
  variants?: boolean;
35
+ /** Merge exactly the `--only` pairs, whatever their names (flag merge-listed-pairs). */
36
+ listed?: boolean;
34
37
  limit?: number;
35
38
  only?: string;
36
39
  /** Also re-point refs and links held today (flag merge-rewrite-refs). */
37
40
  rewriteRefs?: boolean;
38
41
  json?: boolean;
39
42
  }
43
+ export declare function mergeCopiesSummary(report: MergeCopiesReport): string;
40
44
  /** Reads a `--only` file: a JSON array of `{ copy, original }` company-relative paths. */
41
45
  export declare function readListedPairs(file: string): ListedPair[];
42
46
  /**
@@ -47,10 +51,24 @@ export declare function readListedPairs(file: string): ListedPair[];
47
51
  * With `--variants`, merges two-file duplicate groups whose names differ only
48
52
  * in case, spacing or punctuation instead, gated by
49
53
  * `checkpoint.ontology.merge-variants`. `--rewrite-refs` (flag
50
- * `checkpoint.ontology.merge-rewrite-refs`) also re-points signal refs,
51
- * invoice and bill links, source and knowledge links, and carries external
52
- * ids and web domains.
54
+ * `checkpoint.ontology.merge-rewrite-refs`) also re-points invoice and
55
+ * knowledge links to the survivor and carries external ids and web domains;
56
+ * pairs named by signals or sources stay held. `--listed` (flag
57
+ * `checkpoint.ontology.merge-listed-pairs`) merges exactly the `--only` pairs,
58
+ * which a person reviewed, instead of the pairs a finder produces.
53
59
  */
54
60
  export declare function runOntologyMergeCopies(hqRoot: string, slug: string | undefined, options: MergeCopiesCommandOptions, deps?: OntologyCommandDeps): Promise<string>;
61
+ /** Reads a `drop-aliases --only` file: a JSON array of `{ entity, alias }`. */
62
+ export declare function readAliasDrops(file: string): AliasDrop[];
63
+ /**
64
+ * `hq core ontology drop-aliases`: remove the reviewed `{ entity, alias }`
65
+ * pairs in `--only` from entity files (`--apply`); without it only reports.
66
+ * Gated by `checkpoint.ontology.drop-aliases` and `checkpoint.ontology.multi-writer`.
67
+ */
68
+ export declare function runOntologyDropAliases(hqRoot: string, slug: string | undefined, options: {
69
+ only?: string;
70
+ apply?: boolean;
71
+ json?: boolean;
72
+ }, deps?: OntologyCommandDeps): Promise<string>;
55
73
  export declare function registerCoreOntologyCommand(core: Command): void;
56
74
  //# sourceMappingURL=core-ontology.d.ts.map
@@ -17,7 +17,8 @@ import { applyOntologyRun, ApplyError } from "../lib/checkpoint-ontology/apply.j
17
17
  import { resolveOntologyCompany, WriteBoundsError } from "../lib/checkpoint-ontology/company.js";
18
18
  import { findDuplicateGroups, summarizeMergeBack, } from "../lib/checkpoint-ontology/multi-writer.js";
19
19
  import { buildEntityIndex } from "../lib/checkpoint-ontology/tree.js";
20
- import { MERGE_COPIES_FLAG, MERGE_REWRITE_REFS_FLAG, MERGE_VARIANTS_FLAG, mergeCopies, } from "../lib/checkpoint-ontology/merge-copies.js";
20
+ import { DROP_ALIASES_FLAG, dropAliases } from "../lib/checkpoint-ontology/drop-aliases.js";
21
+ import { MERGE_COPIES_FLAG, MERGE_LISTED_PAIRS_FLAG, MERGE_REWRITE_REFS_FLAG, MERGE_VARIANTS_FLAG, mergeCopies, } from "../lib/checkpoint-ontology/merge-copies.js";
21
22
  import { callDeleteEndpoint } from "./files.js";
22
23
  import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
23
24
  import { registerCoreOntologyNativeCommands } from "./core-ontology-native.js";
@@ -142,7 +143,7 @@ async function companyVaultDeleter(companyUid) {
142
143
  return (await callDeleteEndpoint({ token, companyUid, prefix: key, dryRun: false })).deleted;
143
144
  };
144
145
  }
145
- function mergeCopiesSummary(report) {
146
+ export function mergeCopiesSummary(report) {
146
147
  const held = report.plans.filter((plan) => plan.held).length;
147
148
  const lines = [
148
149
  `ontology merge-copies (${report.mode.replace("_", " ")}) in ${report.company} (${report.pair_kind}): ${report.plans.length} of ${report.copies_found} copies, ${held} held, ${report.skipped.length} skipped`,
@@ -150,7 +151,7 @@ function mergeCopiesSummary(report) {
150
151
  ` duplicate groups ${report.duplicate_groups_before} -> ${report.duplicate_groups_after}; broken entity links ${report.broken_links_before} -> ${report.broken_links_after}`,
151
152
  ];
152
153
  for (const plan of report.plans.filter((entry) => entry.held)) {
153
- lines.push(` held ${plan.copy}: ${plan.held}${plan.held === "external_id_conflict" ? ` (${plan.detail})` : ""}`);
154
+ lines.push(` held ${plan.copy}: ${plan.held}${plan.held === "external_id_conflict" || plan.held === "server_owned_refs" ? ` (${plan.detail})` : ""}`);
154
155
  }
155
156
  for (const error of report.errors)
156
157
  lines.push(` error ${error.path}: ${error.error}`);
@@ -216,9 +217,11 @@ function readResolution(copy, raw) {
216
217
  * With `--variants`, merges two-file duplicate groups whose names differ only
217
218
  * in case, spacing or punctuation instead, gated by
218
219
  * `checkpoint.ontology.merge-variants`. `--rewrite-refs` (flag
219
- * `checkpoint.ontology.merge-rewrite-refs`) also re-points signal refs,
220
- * invoice and bill links, source and knowledge links, and carries external
221
- * ids and web domains.
220
+ * `checkpoint.ontology.merge-rewrite-refs`) also re-points invoice and
221
+ * knowledge links to the survivor and carries external ids and web domains;
222
+ * pairs named by signals or sources stay held. `--listed` (flag
223
+ * `checkpoint.ontology.merge-listed-pairs`) merges exactly the `--only` pairs,
224
+ * which a person reviewed, instead of the pairs a finder produces.
222
225
  */
223
226
  export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
224
227
  if (options.apply && options.delete) {
@@ -227,13 +230,20 @@ export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
227
230
  if (options.limit !== undefined && (!Number.isInteger(options.limit) || options.limit < 1)) {
228
231
  throw new OntologyUsageError("ontology: --limit must be a positive whole number");
229
232
  }
233
+ if (options.listed && options.variants) {
234
+ throw new OntologyUsageError("ontology: use either --listed or --variants");
235
+ }
236
+ if (options.listed && options.only === undefined) {
237
+ throw new OntologyUsageError("ontology: --listed needs --only <file>");
238
+ }
230
239
  const only = options.only !== undefined ? readListedPairs(options.only) : undefined;
231
240
  const resolution = resolveOntologyCompany(hqRoot, slug);
232
241
  if (!resolution.ok)
233
242
  throw new OntologyUsageError(`ontology: company rejected: ${resolution.reason}`);
234
243
  const company = resolution.company;
235
244
  const readFlag = deps.readFlag ?? registryFlagReader;
236
- for (const flag of [options.variants ? MERGE_VARIANTS_FLAG : MERGE_COPIES_FLAG, MULTI_WRITER_FLAG, ...(options.rewriteRefs ? [MERGE_REWRITE_REFS_FLAG] : [])]) {
245
+ const kindFlag = options.listed ? MERGE_LISTED_PAIRS_FLAG : options.variants ? MERGE_VARIANTS_FLAG : MERGE_COPIES_FLAG;
246
+ for (const flag of [kindFlag, MULTI_WRITER_FLAG, ...(options.rewriteRefs ? [MERGE_REWRITE_REFS_FLAG] : [])]) {
237
247
  if (!(await readFlag(flag, company))) {
238
248
  return options.json ? JSON.stringify({ company: company.slug, skipped: `${flag} off` }) : `ontology merge-copies: skipped (${flag} is off)`;
239
249
  }
@@ -251,6 +261,7 @@ export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
251
261
  mode,
252
262
  now: deps.now?.() ?? new Date(),
253
263
  ...(options.variants ? { pairKind: "variants" } : {}),
264
+ ...(options.listed ? { pairKind: "listed" } : {}),
254
265
  ...(options.limit !== undefined ? { limit: options.limit } : {}),
255
266
  ...(only ? { only } : {}),
256
267
  ...(options.rewriteRefs ? { rewriteRefs: true } : {}),
@@ -258,6 +269,59 @@ export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
258
269
  });
259
270
  return options.json ? JSON.stringify(report, null, 2) : mergeCopiesSummary(report);
260
271
  }
272
+ /** Reads a `drop-aliases --only` file: a JSON array of `{ entity, alias }`. */
273
+ export function readAliasDrops(file) {
274
+ let parsed;
275
+ try {
276
+ parsed = JSON.parse(fs.readFileSync(file, "utf8"));
277
+ }
278
+ catch (error) {
279
+ throw new OntologyUsageError(`ontology: --only file ${file} is not readable JSON: ${error.message}`);
280
+ }
281
+ const usage = "ontology: --only must be a non-empty JSON array of { entity, alias } strings";
282
+ if (!Array.isArray(parsed) || parsed.length === 0)
283
+ throw new OntologyUsageError(usage);
284
+ return parsed.map((item) => {
285
+ const record = item;
286
+ if (!record || typeof record.entity !== "string" || typeof record.alias !== "string" || record.alias === "") {
287
+ throw new OntologyUsageError(usage);
288
+ }
289
+ return { entity: record.entity, alias: record.alias };
290
+ });
291
+ }
292
+ /**
293
+ * `hq core ontology drop-aliases`: remove the reviewed `{ entity, alias }`
294
+ * pairs in `--only` from entity files (`--apply`); without it only reports.
295
+ * Gated by `checkpoint.ontology.drop-aliases` and `checkpoint.ontology.multi-writer`.
296
+ */
297
+ export async function runOntologyDropAliases(hqRoot, slug, options, deps = {}) {
298
+ if (options.only === undefined)
299
+ throw new OntologyUsageError("ontology: drop-aliases needs --only <file>");
300
+ const drops = readAliasDrops(options.only);
301
+ const resolution = resolveOntologyCompany(hqRoot, slug);
302
+ if (!resolution.ok)
303
+ throw new OntologyUsageError(`ontology: company rejected: ${resolution.reason}`);
304
+ const company = resolution.company;
305
+ const readFlag = deps.readFlag ?? registryFlagReader;
306
+ for (const flag of [DROP_ALIASES_FLAG, MULTI_WRITER_FLAG]) {
307
+ if (!(await readFlag(flag, company))) {
308
+ return options.json ? JSON.stringify({ company: company.slug, skipped: `${flag} off` }) : `ontology drop-aliases: skipped (${flag} is off)`;
309
+ }
310
+ }
311
+ const report = dropAliases({ hqRoot, company, mode: options.apply ? "apply" : "dry_run", drops, now: deps.now?.() ?? new Date() });
312
+ if (options.json)
313
+ return JSON.stringify(report, null, 2);
314
+ const planned = Object.values(report.planned).reduce((sum, list) => sum + list.length, 0);
315
+ const lines = [
316
+ `ontology drop-aliases (${report.mode.replace("_", " ")}) in ${report.company}: ${planned} alias(es) on ${Object.keys(report.planned).length} entities, written ${report.written.length}, skipped ${report.skipped.length}, errors ${report.errors.length}`,
317
+ ` duplicate groups ${report.duplicate_groups_before} -> ${report.duplicate_groups_after}`,
318
+ ];
319
+ for (const skip of report.skipped)
320
+ lines.push(` skipped ${skip.entity} "${skip.alias}": ${skip.reason}${skip.detail ? ` (${skip.detail})` : ""}`);
321
+ for (const error of report.errors)
322
+ lines.push(` error ${error.path}: ${error.error}`);
323
+ return lines.join("\n");
324
+ }
261
325
  function fail(error) {
262
326
  if (error instanceof OntologyUsageError || error instanceof ApplyError || error instanceof WriteBoundsError) {
263
327
  process.stderr.write(`${error.message}\n`);
@@ -306,9 +370,10 @@ export function registerCoreOntologyCommand(core) {
306
370
  .option("--apply", "merge each copy into its original and re-point links (writes entity files)")
307
371
  .option("--delete", "delete fully merged copies from the vault by exact key (after sync pushed the merge)")
308
372
  .option("--variants", "merge two-file duplicate groups whose names differ only in case, spacing or punctuation (checkpoint.ontology.merge-variants)")
373
+ .option("--listed", "merge exactly the pairs in the --only file, whatever their names (checkpoint.ontology.merge-listed-pairs)")
309
374
  .option("--limit <n>", "handle at most this many copies", (value) => Number(value))
310
375
  .option("--only <file>", "handle only the copy -> original pairs listed in this JSON file")
311
- .option("--rewrite-refs", "also re-point signal refs, invoice, source and knowledge links to the survivor and carry external ids and web domains (checkpoint.ontology.merge-rewrite-refs)")
376
+ .option("--rewrite-refs", "also re-point invoice and knowledge links to the survivor and carry external ids and web domains; pairs named by signals or sources stay held (checkpoint.ontology.merge-rewrite-refs)")
312
377
  .option("--json", "print JSON")
313
378
  .option("--hq-root <path>", "HQ installation to operate on")
314
379
  .action(async (options) => {
@@ -320,6 +385,23 @@ export function registerCoreOntologyCommand(core) {
320
385
  }
321
386
  });
322
387
  describeCoreCommand(mergeCopies, { execution: "native", root: "live" });
388
+ const dropAliasesCommand = ontology
389
+ .command("drop-aliases")
390
+ .description("Remove reviewed wrong aliases from entity files (dry run by default)")
391
+ .requiredOption("--company <slug>", "company to change")
392
+ .option("--only <file>", "JSON array of { entity, alias } to remove; entity is a company-relative path")
393
+ .option("--apply", "write the entity files")
394
+ .option("--json", "print JSON")
395
+ .option("--hq-root <path>", "HQ installation to operate on")
396
+ .action(async (options) => {
397
+ try {
398
+ process.stdout.write(`${await runOntologyDropAliases(rootFor(options), options.company, options)}\n`);
399
+ }
400
+ catch (error) {
401
+ fail(error);
402
+ }
403
+ });
404
+ describeCoreCommand(dropAliasesCommand, { execution: "native", root: "live" });
323
405
  const apply = ontology
324
406
  .command("apply")
325
407
  .description("Validate the run's entity candidates and write them to the payload company")
@@ -214,6 +214,11 @@ export const SCAFFOLD_COMMANDS = [
214
214
  export const SCAFFOLD_ONLY_ASSETS = [
215
215
  { asset: "core/scripts/jobs-schedule-parse.sh", root: "live", summary: "Rollback companion for native jobs schedule-parse" },
216
216
  { asset: "core/scripts/jobs-validate.sh", root: "live", summary: "Rollback companion for native jobs validate" },
217
+ { asset: "core/scripts/outpost-jobs-reconcile.sh", root: "live", summary: "Rollback companion for native jobs reconcile" },
218
+ { asset: "core/scripts/hq-job-probe.sh", root: "live", summary: "Scheduled jobs readiness probe used by the reconcile rollback path" },
219
+ { asset: "core/scripts/hq-job-run.sh", root: "live", summary: "Scheduled jobs runner used by generated systemd units" },
220
+ { asset: "core/scripts/hq-job-notify.sh", root: "live", summary: "Scheduled jobs failure notification helper" },
221
+ { asset: "core/scripts/hq-job-remediate.sh", root: "live", summary: "Scheduled jobs bounded remediation helper" },
217
222
  // Shared shell libs sourced by the two relocated scripts (and hooks). Never
218
223
  // dispatched on their own (so no `hq core` command), but they MUST ship in the
219
224
  // tarball, or the bundled hq-session / checkpoint-stop-gate die at `source`
@@ -235,6 +240,11 @@ export const SCAFFOLD_ONLY_ASSETS = [
235
240
  { asset: "core/scripts/hq-delegate-repo.sh", root: "live", summary: "Rollback shell for native delegate repo" },
236
241
  { asset: "core/scripts/hq-delegate-secrets.sh", root: "live", summary: "Rollback shell for native delegate secrets" },
237
242
  { asset: "core/scripts/hq-delegate-transfer.sh", root: "live", summary: "Rollback shell for native delegate transfer" },
243
+ { asset: "core/scripts/hq-delegate-bundle.sh", root: "live", summary: "Rollback shell for native delegate bundle" },
244
+ { asset: "core/scripts/hq-delegate-grant.sh", root: "live", summary: "Rollback shell for native delegate grant" },
245
+ { asset: "core/scripts/hq-delegate-publish.sh", root: "live", summary: "Rollback shell for native delegate publish" },
246
+ { asset: "core/scripts/hq-delegate-send.sh", root: "live", summary: "Rollback shell for native delegate send" },
247
+ { asset: "core/scripts/hq-delegate-verify.sh", root: "live", summary: "Rollback shell for native delegate verify" },
238
248
  { asset: "core/scripts/lib/secret-patterns.sh", root: "live", summary: "Secret-pattern library sourced by the delegate rollback shells" },
239
249
  // Retained rollback companions for the gated native ontology commands. These
240
250
  // are loose HQ-tree assets, but `hq core ontology` dispatches natively.
@@ -19,6 +19,38 @@ export function createLaneMoveHandlers(deps) {
19
19
  lane.last_host_report_at === undefined &&
20
20
  lane.pending_resume === undefined;
21
21
  }
22
+ function sameHost(a, b) {
23
+ return a.kind === b.kind && a.id === b.id;
24
+ }
25
+ function reportAlreadyOnTarget(lane) {
26
+ printJson({
27
+ ok: true,
28
+ action: "move",
29
+ lane_id: lane.lane_id,
30
+ state: lane.state,
31
+ host: lane.host,
32
+ account: lane.account ?? null,
33
+ moved: false,
34
+ reason: "already_on_target",
35
+ lane,
36
+ });
37
+ }
38
+ async function reportAlreadyOnTargetIfCurrent(hqRoot, laneId, provider, target, callerHost, runtimeHost, ctx) {
39
+ const current = await withLaneAdmissionLock(hqRoot, provider, async () => {
40
+ const lane = readLane(hqRoot, laneId);
41
+ if (!laneCanMoveBeforeStart(lane)) {
42
+ throw new LanesError(lane.state === "queued" ? "lane_move_started" : "lane_not_queued", `Lane ${laneId} cannot move after it has started; current state is ${lane.state}`, { lane_id: laneId, state: lane.state });
43
+ }
44
+ return sameHost(lane.host, target) ||
45
+ (laneRunsHere(lane, runtimeHost) && sameHost(callerHost, target))
46
+ ? lane
47
+ : undefined;
48
+ }, ctx.sleep);
49
+ if (current === undefined)
50
+ return false;
51
+ reportAlreadyOnTarget(current);
52
+ return true;
53
+ }
22
54
  function readLaneTimeout(hqRoot, laneId) {
23
55
  const raw = readJsonFile(timeoutPath(hqRoot, laneId));
24
56
  if (raw === null)
@@ -51,6 +83,22 @@ export function createLaneMoveHandlers(deps) {
51
83
  throw new LanesError(source.state === "queued" ? "lane_move_started" : "lane_not_queued", `Lane ${laneId} cannot move after it has started; current state is ${source.state}`, { lane_id: laneId, state: source.state });
52
84
  }
53
85
  const sourceHost = source.host;
86
+ const runtimeHost = reconcileHostFromCtx(ctx);
87
+ const runningHere = laneRunsHere(source, runtimeHost);
88
+ const callerHost = runtimeHost.outpostId
89
+ ? { kind: "outpost", id: runtimeHost.outpostId }
90
+ : { kind: "local", id: runtimeHost.hostname };
91
+ const explicitTarget = policy.kind === "local"
92
+ ? callerHost
93
+ : policy.kind === "outpost"
94
+ ? { kind: "outpost", id: policy.id }
95
+ : undefined;
96
+ if (explicitTarget !== undefined &&
97
+ (sameHost(sourceHost, explicitTarget) ||
98
+ (runningHere && sameHost(callerHost, explicitTarget)))) {
99
+ if (await reportAlreadyOnTargetIfCurrent(hqRoot, laneId, source.provider, explicitTarget, callerHost, runtimeHost, ctx))
100
+ return;
101
+ }
54
102
  const config = loadLanesConfig(hqRoot);
55
103
  const capacity = await loadHostCapacitySnapshots(ctx, hqRoot, config);
56
104
  let targetSnapshots = capacity.snapshots.filter((snapshot) => snapshot.host.kind !== sourceHost.kind || snapshot.host.id !== sourceHost.id);
@@ -69,8 +117,13 @@ export function createLaneMoveHandlers(deps) {
69
117
  ...(source.account !== undefined ? { account: source.account } : {}),
70
118
  ...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
71
119
  });
120
+ if (sameHost(sourceHost, choice.host) ||
121
+ (runningHere && sameHost(callerHost, choice.host))) {
122
+ if (await reportAlreadyOnTargetIfCurrent(hqRoot, laneId, source.provider, choice.host, callerHost, runtimeHost, ctx))
123
+ return;
124
+ }
72
125
  let moved;
73
- if (laneRunsHere(source, reconcileHostFromCtx(ctx))) {
126
+ if (runningHere) {
74
127
  moved = await commitLaneMove(ctx, laneId, choice.host, choice.account, placementEnabled);
75
128
  }
76
129
  else if (source.host.kind === "outpost") {
@@ -475,6 +475,8 @@ export function createLaneQuestionsHandlers(deps) {
475
475
  .map((q) => {
476
476
  const row = {
477
477
  ...q,
478
+ priority: q.priority ?? null,
479
+ priority_display: q.priority ?? "-",
478
480
  audience: resolveAudience(hqRoot, q, ctx),
479
481
  ...(q.question_kind === "continue" ? { flag: "continue-loop" } : {}),
480
482
  };
@@ -503,6 +505,8 @@ export function createLaneQuestionsHandlers(deps) {
503
505
  })
504
506
  .map(({ question: q, host, audience }) => ({
505
507
  ...q,
508
+ priority: q.priority ?? null,
509
+ priority_display: q.priority ?? "-",
506
510
  host,
507
511
  audience,
508
512
  ...(q.question_kind === "continue" ? { flag: "continue-loop" } : {}),
@@ -114,6 +114,8 @@ export declare function parseQuestionBody(raw: unknown): QuestionBody | null;
114
114
  */
115
115
  export declare const ASK_BACKOFF_MS: readonly [5000, 15000];
116
116
  export declare const ASK_ATTEMPTS: number;
117
+ /** Flip to true in the next release to make --priority mandatory. */
118
+ export declare const ASK_PRIORITY_REQUIRED = false;
117
119
  export declare function casUpdateQuestion(hqRoot: string, questionId: string, updater: (current: QuestionRecord) => QuestionRecord): QuestionRecord;
118
120
  export declare function casUpdateLane(hqRoot: string, laneId: string, updater: (current: LaneRecord) => LaneRecord): LaneRecord;
119
121
  /**
@@ -490,6 +490,67 @@ function mergeOwnerLaneHierarchy(supplied, ownerLanes) {
490
490
  byId.set(lane.lane_id, lane);
491
491
  return [...byId.values()];
492
492
  }
493
+ function hierarchyWithRemoteSenior(hierarchy, remoteSenior) {
494
+ return [
495
+ ...hierarchy.filter((lane) => lane.lane_id !== remoteSenior.lane_id),
496
+ remoteSenior,
497
+ ];
498
+ }
499
+ async function readRemoteHireGrantSenior(ctx, hqRoot, seniorLaneId) {
500
+ if (!listLaneIds(hqRoot).includes(seniorLaneId))
501
+ return undefined;
502
+ const mirror = readLane(hqRoot, seniorLaneId);
503
+ if (mirror.host.kind !== "outpost" ||
504
+ laneRunsHere(mirror, reconcileHostFromCtx(ctx))) {
505
+ return undefined;
506
+ }
507
+ const outpostId = mirror.host.id;
508
+ const unavailable = (reason) => new LanesError("hire_grant_senior_unavailable", `Cannot verify senior lane ${seniorLaneId} from owning Outpost ${outpostId}; refusing hire-grant admission (${reason})`, { senior_lane_id: seniorLaneId, outpost_id: outpostId, reason });
509
+ let ownerRecords;
510
+ try {
511
+ ownerRecords = await readOutpostLaneRecords(ctx, outpostId, {
512
+ laneIds: [seniorLaneId],
513
+ snapshot: true,
514
+ strictSnapshot: true,
515
+ }, {}, { bypassCache: true });
516
+ }
517
+ catch (error) {
518
+ throw unavailable(isLanesError(error) ? error.error : "outpost_lane_list_unavailable");
519
+ }
520
+ const matches = ownerRecords.filter((lane) => lane.lane_id === seniorLaneId);
521
+ const owner = matches[0];
522
+ const ownerIsHostedByOutpost = owner !== undefined && ((owner.host.kind === "outpost" && owner.host.id === outpostId) ||
523
+ (owner.host.kind === "local" && owner.host.id.trim() !== ""));
524
+ if (matches.length !== 1 || owner === undefined || !ownerIsHostedByOutpost) {
525
+ throw unavailable(matches.length === 0
526
+ ? "owner_did_not_return_senior"
527
+ : matches.length > 1
528
+ ? "owner_returned_duplicate_seniors"
529
+ : "owner_senior_identity_mismatch");
530
+ }
531
+ try {
532
+ const mirrorCompany = resolveLaneCompany(hqRoot, mirror).company;
533
+ const ownerCompany = resolveLaneCompany(hqRoot, owner).company;
534
+ if ((ownerCompany?.uid ?? null) !== (mirrorCompany?.uid ?? null) ||
535
+ (ownerCompany?.slug ?? null) !== (mirrorCompany?.slug ?? null)) {
536
+ throw unavailable("owner_company_did_not_match_mirror");
537
+ }
538
+ }
539
+ catch (error) {
540
+ if (isLanesError(error) && error.error === "hire_grant_senior_unavailable")
541
+ throw error;
542
+ throw unavailable("owner_company_identity_unavailable");
543
+ }
544
+ return owner;
545
+ }
546
+ async function hierarchyWithVerifiedRemoteSenior(ctx, hqRoot, lane, hierarchy) {
547
+ if (lane.senior.kind !== "lane" || !lane.senior.id)
548
+ return [...hierarchy];
549
+ const remoteSenior = await readRemoteHireGrantSenior(ctx, hqRoot, lane.senior.id);
550
+ return remoteSenior === undefined
551
+ ? [...hierarchy]
552
+ : mergeOwnerLaneRecords(hierarchy, [remoteSenior]);
553
+ }
493
554
  function mergeOwnerLaneRecords(supplied, ownerLanes) {
494
555
  const byId = new Map(supplied.map((lane) => [lane.lane_id, lane]));
495
556
  for (const lane of ownerLanes)
@@ -911,8 +972,18 @@ async function withResumeHireAdmission(ctx, lane, fn, grantContext) {
911
972
  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 });
912
973
  }
913
974
  await refreshRemoteLaneMirrors(ctx, hqRoot, loadAllLanes(hqRoot), new Set([lane.senior.id]));
975
+ const remoteSenior = grantContext === undefined
976
+ ? await readRemoteHireGrantSenior(ctx, hqRoot, lane.senior.id)
977
+ : undefined;
978
+ const suppliedHierarchy = grantContext?.hierarchy ?? (remoteSenior === undefined
979
+ ? undefined
980
+ : hierarchyWithRemoteSenior(loadAllLanes(hqRoot), remoteSenior));
914
981
  return withHireGrantAdmission(hqRoot, lane.senior.id, (snapshot) => {
915
982
  const current = readLane(hqRoot, lane.lane_id);
983
+ if (remoteSenior !== undefined &&
984
+ (current.senior.kind !== "lane" || current.senior.id !== remoteSenior.lane_id)) {
985
+ throw new LanesError("hire_grant_senior_changed", `Lane ${current.lane_id}'s senior changed while its owning Outpost was being checked`, { lane_id: current.lane_id, expected_senior_lane_id: remoteSenior.lane_id, current_senior: current.senior });
986
+ }
916
987
  if (grantContext !== undefined &&
917
988
  !sameHireGrantSenior(current.senior, grantContext.expected_senior)) {
918
989
  throw new LanesError("hire_grant_senior_changed", `Lane ${current.lane_id}'s senior changed before the Outpost resumed it`, {
@@ -929,7 +1000,7 @@ async function withResumeHireAdmission(ctx, lane, fn, grantContext) {
929
1000
  if (refusal !== null)
930
1001
  throw refusal;
931
1002
  return fn();
932
- }, grantContext?.hierarchy);
1003
+ }, suppliedHierarchy, remoteSenior === undefined ? undefined : new Set([remoteSenior.lane_id]));
933
1004
  }
934
1005
  const CAPACITY_PROBE_TIMEOUT_MS = 15_000;
935
1006
  const MAX_PLACEMENT_OUTPOSTS = 16;
@@ -1205,8 +1276,11 @@ async function readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOpt
1205
1276
  }
1206
1277
  return lanes;
1207
1278
  }
1208
- async function readOutpostLaneRecords(ctx, outpostId, requestedQuery = {}, dispatchOptions = {}) {
1279
+ async function readOutpostLaneRecords(ctx, outpostId, requestedQuery = {}, dispatchOptions = {}, options = {}) {
1209
1280
  const query = normalizedLaneListQuery(requestedQuery);
1281
+ if (options.bypassCache === true) {
1282
+ return readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOptions);
1283
+ }
1210
1284
  let invocationMemo = outpostLaneListMemo.get(ctx);
1211
1285
  if (!invocationMemo) {
1212
1286
  invocationMemo = new OutpostSnapshotInvocationMemo();
@@ -2144,6 +2218,10 @@ function assertKnownOutpost(requested, known) {
2144
2218
  */
2145
2219
  export const ASK_BACKOFF_MS = [5_000, 15_000];
2146
2220
  export const ASK_ATTEMPTS = ASK_BACKOFF_MS.length + 1;
2221
+ /** Flip to true in the next release to make --priority mandatory. */
2222
+ export const ASK_PRIORITY_REQUIRED = false;
2223
+ const ASK_PRIORITY_WARNING = "--priority will be required in the next release.";
2224
+ const QUESTION_PRIORITIES = ["P0", "P1", "P2"];
2147
2225
  function withCasRetry(fn, attempts = 5) {
2148
2226
  let last;
2149
2227
  for (let i = 0; i < attempts; i += 1) {
@@ -3980,19 +4058,21 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
3980
4058
  let grantContext;
3981
4059
  let grantHierarchy;
3982
4060
  if (action === "grant") {
3983
- const hierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId, {
4061
+ const ownerHierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId, {
3984
4062
  hierarchy: [lane.lane_id],
3985
4063
  snapshot: true,
3986
4064
  }));
4065
+ const hierarchy = await hierarchyWithVerifiedRemoteSenior(ctx, ctx.hqRoot(), lane, ownerHierarchy);
3987
4066
  grantHierarchy = hierarchy;
3988
4067
  grantContext = createOutpostHireGrantContext(lane, hierarchy);
3989
4068
  }
3990
4069
  else if ((action === "resume" && lane.senior.kind === "lane") ||
3991
4070
  (action === "message" && (lane.senior.kind === "lane" || lane.senior.kind === "session"))) {
3992
- const hierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId, {
4071
+ const ownerHierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId, {
3993
4072
  hierarchy: [lane.lane_id],
3994
4073
  snapshot: true,
3995
4074
  }));
4075
+ const hierarchy = await hierarchyWithVerifiedRemoteSenior(ctx, ctx.hqRoot(), lane, ownerHierarchy);
3996
4076
  grantContext = createOutpostHireGrantContext(lane, hierarchy, action === "message" ? options.supersededMessageCallerId : undefined, action === "message" ? options.supersededMessageSessionId : undefined);
3997
4077
  }
3998
4078
  let dispatched;
@@ -5254,6 +5334,10 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
5254
5334
  }
5255
5335
  await refreshRemoteLaneMirrors(ctx, hqRoot, loadAllLanes(hqRoot), parentLaneIds);
5256
5336
  }
5337
+ const remoteSenior = authorization === "senior" &&
5338
+ initial.senior.kind === "lane" && initial.senior.id
5339
+ ? await readRemoteHireGrantSenior(ctx, hqRoot, initial.senior.id)
5340
+ : undefined;
5257
5341
  const updated = ctx.withGrantHireLocks(hqRoot, lockedLaneIds, () => {
5258
5342
  const current = readLane(hqRoot, laneId);
5259
5343
  const currentCallerAuthorization = authorization === "senior"
@@ -5270,11 +5354,18 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
5270
5354
  (!grantContext || !sameHireGrantSenior(current.senior, grantContext.expected_senior))) {
5271
5355
  throw new LanesError("hire_grant_senior_changed", `Lane ${laneId}'s senior changed before the Outpost applied the grant update`, { lane_id: laneId, current_senior: current.senior });
5272
5356
  }
5273
- const hierarchy = grantContext
5357
+ if (remoteSenior !== undefined &&
5358
+ (current.senior.kind !== "lane" || current.senior.id !== remoteSenior.lane_id)) {
5359
+ throw new LanesError("hire_grant_senior_changed", `Lane ${current.lane_id}'s senior changed while its owning Outpost was being checked`, { lane_id: current.lane_id, expected_senior_lane_id: remoteSenior.lane_id, current_senior: current.senior });
5360
+ }
5361
+ const currentHierarchy = grantContext
5274
5362
  ? authorization === "ssm-owner-dispatch"
5275
5363
  ? mergeOwnerLaneHierarchy(grantContext.hierarchy, loadAllLanes(hqRoot))
5276
5364
  : grantContext.hierarchy
5277
5365
  : loadAllLanes(hqRoot);
5366
+ const hierarchy = remoteSenior === undefined
5367
+ ? currentHierarchy
5368
+ : hierarchyWithRemoteSenior(currentHierarchy, remoteSenior);
5278
5369
  if (current.senior.kind === "lane") {
5279
5370
  if (!current.senior.id) {
5280
5371
  throw new LanesError("hire_grant_senior_invalid", `Lane ${laneId} has a lane senior without an id`, {
@@ -7443,7 +7534,23 @@ export function interruptNotResumableWarning(laneId) {
7443
7534
  return `hq lanes: warning: lane ${laneId} was interrupted before its provider reported a session id, ` +
7444
7535
  `so hq lanes resume cannot continue it. Start a new lane for the remaining work.\n`;
7445
7536
  }
7537
+ function parseQuestionPriority(value) {
7538
+ if (value === undefined) {
7539
+ if (ASK_PRIORITY_REQUIRED) {
7540
+ throw new LanesError("ask_priority_required", "--priority is required; choose P0, P1, or P2", {
7541
+ allowed: [...QUESTION_PRIORITIES],
7542
+ });
7543
+ }
7544
+ return undefined;
7545
+ }
7546
+ const normalized = value.toUpperCase();
7547
+ if (!QUESTION_PRIORITIES.includes(normalized)) {
7548
+ throw new LanesError("ask_priority_invalid", `Invalid --priority ${JSON.stringify(value)}. Allowed values: P0, P1, P2`, { allowed: [...QUESTION_PRIORITIES], value });
7549
+ }
7550
+ return normalized;
7551
+ }
7446
7552
  async function runAsk(ctx, opts) {
7553
+ const priority = parseQuestionPriority(opts.priority);
7447
7554
  const hqRoot = ctx.hqRoot();
7448
7555
  const laneId = ctx.env.HQ_LANE_ID?.trim();
7449
7556
  if (!laneId) {
@@ -7482,6 +7589,7 @@ async function runAsk(ctx, opts) {
7482
7589
  at,
7483
7590
  activeCompany: questionCompany,
7484
7591
  questionId,
7592
+ priority,
7485
7593
  });
7486
7594
  questionId = withBody.question_id;
7487
7595
  await emitLaneMeshEvent(ctx, lane.mesh_company ?? lane.company, lane.thread_id, "lane_question", {
@@ -7496,12 +7604,16 @@ async function runAsk(ctx, opts) {
7496
7604
  question_id: withBody.question_id,
7497
7605
  });
7498
7606
  }
7607
+ if (priority === undefined) {
7608
+ process.stderr.write(`hq lanes ask: warning: ${ASK_PRIORITY_WARNING}\n`);
7609
+ }
7499
7610
  printJson({
7500
7611
  ok: true,
7501
7612
  action: "ask",
7502
7613
  question: withBody,
7503
7614
  lane_id: lane.lane_id,
7504
7615
  lane_state: lane.state,
7616
+ ...(priority === undefined ? { warnings: [ASK_PRIORITY_WARNING] } : {}),
7505
7617
  });
7506
7618
  return;
7507
7619
  }
@@ -8493,6 +8605,8 @@ export function registerLanesCommand(parent, deps = {}) {
8493
8605
  .option("--context <text>", "Context for the audience")
8494
8606
  .option("--option <text>", "Answer choice (repeatable)", collectRepeatable)
8495
8607
  .option("--recommended <text>", "Recommended option")
8608
+ .option("--priority <P0|P1|P2>", "Urgency: P0 blocks now, P1 needed today, P2 can wait")
8609
+ .addHelpText("after", "\nThe audience may take up to 24 hours to answer; keep working meanwhile.\n")
8496
8610
  .action(wrap(async (opts) => {
8497
8611
  await runAsk(ctx, opts);
8498
8612
  }));
@@ -0,0 +1,41 @@
1
+ /**
2
+ * `hq core ontology drop-aliases`: remove reviewed wrong aliases from entity
3
+ * files (CO-74). An alias that names a different entity joins two unrelated
4
+ * entities into one duplicate group; a person lists each `{ entity, alias }`
5
+ * to remove, and nothing else changes. The dry run writes nothing; `apply`
6
+ * writes the entity files through the multi-writer outbox so sync pushes them.
7
+ */
8
+ import { type OntologyCompany } from "./company.js";
9
+ export declare const DROP_ALIASES_FLAG = "checkpoint.ontology.drop-aliases";
10
+ /** One reviewed removal: `entity` is a company-relative entity path. */
11
+ export interface AliasDrop {
12
+ entity: string;
13
+ alias: string;
14
+ }
15
+ export type AliasDropSkipReason = "no_entity" | "type_not_writable" | "alias_absent" | "patch_failed";
16
+ export interface AliasDropReport {
17
+ company: string;
18
+ mode: "dry_run" | "apply";
19
+ duplicate_groups_before: number;
20
+ duplicate_groups_after: number;
21
+ /** Entity -> aliases removed (dry run: would be removed). */
22
+ planned: Record<string, string[]>;
23
+ written: string[];
24
+ skipped: Array<AliasDrop & {
25
+ reason: AliasDropSkipReason;
26
+ detail?: string;
27
+ }>;
28
+ errors: Array<{
29
+ path: string;
30
+ error: string;
31
+ }>;
32
+ }
33
+ export interface DropAliasesOptions {
34
+ hqRoot: string;
35
+ company: OntologyCompany;
36
+ mode: "dry_run" | "apply";
37
+ drops: AliasDrop[];
38
+ now: Date;
39
+ }
40
+ export declare function dropAliases(options: DropAliasesOptions): AliasDropReport;
41
+ //# sourceMappingURL=drop-aliases.d.ts.map