@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
@@ -0,0 +1,76 @@
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 * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ import { entitiesRoot, safeWriteFile } from "./company.js";
11
+ import { patchEntityFile } from "./entity-file.js";
12
+ import { isWritableEntityType } from "./identity.js";
13
+ import { findDuplicateGroups, recordOutbox } from "./multi-writer.js";
14
+ import { buildEntityIndex } from "./tree.js";
15
+ export const DROP_ALIASES_FLAG = "checkpoint.ontology.drop-aliases";
16
+ export function dropAliases(options) {
17
+ const { hqRoot, company, mode, drops, now } = options;
18
+ const index = buildEntityIndex(company.dir);
19
+ const report = {
20
+ company: company.slug,
21
+ mode,
22
+ duplicate_groups_before: findDuplicateGroups(index).length,
23
+ duplicate_groups_after: 0,
24
+ planned: {},
25
+ written: [],
26
+ skipped: [],
27
+ errors: [],
28
+ };
29
+ const byEntity = new Map();
30
+ for (const drop of drops) {
31
+ const entity = index.byRel.get(drop.entity);
32
+ if (!entity) {
33
+ report.skipped.push({ ...drop, reason: "no_entity" });
34
+ }
35
+ else if (!isWritableEntityType(entity.type)) {
36
+ report.skipped.push({ ...drop, reason: "type_not_writable" });
37
+ }
38
+ else if (!entity.aliases.includes(drop.alias)) {
39
+ report.skipped.push({ ...drop, reason: "alias_absent" });
40
+ }
41
+ else {
42
+ byEntity.set(drop.entity, [...(byEntity.get(drop.entity) ?? []), drop]);
43
+ }
44
+ }
45
+ const writes = new Map();
46
+ for (const [rel, list] of [...byEntity].sort(([a], [b]) => a.localeCompare(b))) {
47
+ const content = fs.readFileSync(path.join(company.dir, rel), "utf8");
48
+ const patched = patchEntityFile(content, { removeAliases: list.map((drop) => drop.alias), lastUpdated: now.toISOString() });
49
+ if (!patched.ok) {
50
+ for (const drop of list)
51
+ report.skipped.push({ ...drop, reason: "patch_failed", detail: patched.reason });
52
+ continue;
53
+ }
54
+ report.planned[rel] = [...new Set(list.map((drop) => drop.alias))];
55
+ writes.set(rel, patched.content);
56
+ }
57
+ if (mode === "apply") {
58
+ for (const [rel, content] of writes) {
59
+ try {
60
+ // Outbox first: sync pushes an entity file only when its bytes match.
61
+ recordOutbox(hqRoot, company, [{ rel, content }], now);
62
+ safeWriteFile(hqRoot, [entitiesRoot(company)], path.join(company.dir, rel), content);
63
+ report.written.push(rel);
64
+ }
65
+ catch (error) {
66
+ report.errors.push({ path: rel, error: error.name });
67
+ }
68
+ }
69
+ report.duplicate_groups_after = findDuplicateGroups(buildEntityIndex(company.dir)).length;
70
+ }
71
+ else {
72
+ report.duplicate_groups_after = report.duplicate_groups_before;
73
+ }
74
+ return report;
75
+ }
76
+ //# sourceMappingURL=drop-aliases.js.map
@@ -47,6 +47,8 @@ export declare function renderNewEntity(entity: NewEntity): string;
47
47
  export declare const OWNED_KEYS: ReadonlySet<string>;
48
48
  export interface EntityPatch {
49
49
  addAliases?: string[];
50
+ /** Exact alias strings to remove (reviewed wrong aliases). Applied before `addAliases`. */
51
+ removeAliases?: string[];
50
52
  signalCount?: number;
51
53
  lastUpdated: string;
52
54
  /** The caller changed the body, so `last_updated` moves even with no key change. */
@@ -61,9 +63,10 @@ export type PatchResult = {
61
63
  reason: string;
62
64
  };
63
65
  /**
64
- * Patch an existing, valid entity file. Aliases are only appended, and
65
- * `signal_count` never goes down. When nothing but `last_updated` would
66
- * change, the file is returned unchanged so a rerun writes nothing.
66
+ * Patch an existing, valid entity file. Aliases are appended, and removed
67
+ * only when `removeAliases` names them exactly; `signal_count` never goes
68
+ * down. When nothing but `last_updated` would change, the file is returned
69
+ * unchanged so a rerun writes nothing.
67
70
  */
68
71
  export declare function patchEntityFile(content: string, patch: EntityPatch): PatchResult;
69
72
  /** A namespaced record in a source system, as the cloud schema's `external_refs` holds it. */
@@ -144,9 +144,10 @@ function appendAliases(lines, all, added) {
144
144
  return [...lines.slice(0, end), ...added.map((alias) => `${indent}- ${quote(alias)}`), ...lines.slice(end)];
145
145
  }
146
146
  /**
147
- * Patch an existing, valid entity file. Aliases are only appended, and
148
- * `signal_count` never goes down. When nothing but `last_updated` would
149
- * change, the file is returned unchanged so a rerun writes nothing.
147
+ * Patch an existing, valid entity file. Aliases are appended, and removed
148
+ * only when `removeAliases` names them exactly; `signal_count` never goes
149
+ * down. When nothing but `last_updated` would change, the file is returned
150
+ * unchanged so a rerun writes nothing.
150
151
  */
151
152
  export function patchEntityFile(content, patch) {
152
153
  const match = FRONTMATTER_RE.exec(content);
@@ -156,19 +157,24 @@ export function patchEntityFile(content, patch) {
156
157
  const before = validateEntityFrontmatter(parsed.frontmatter);
157
158
  if (typeof before === "string")
158
159
  return { ok: false, reason: `invalid_existing:${before}` };
159
- const aliases = [...before.aliases];
160
+ const remove = new Set(patch.removeAliases ?? []);
161
+ const kept = before.aliases.filter((alias) => !remove.has(alias));
162
+ const aliases = [...kept];
160
163
  for (const alias of patch.addAliases ?? []) {
161
164
  if (!aliases.includes(alias))
162
165
  aliases.push(alias);
163
166
  }
164
167
  const signalCount = Math.max(before.signal_count, patch.signalCount ?? 0);
165
- const aliasesChanged = aliases.length !== before.aliases.length;
168
+ const removed = kept.length !== before.aliases.length;
169
+ const aliasesChanged = removed || aliases.length !== before.aliases.length;
166
170
  const countChanged = signalCount !== before.signal_count;
167
171
  if (!aliasesChanged && !countChanged && !patch.bodyChanged)
168
172
  return { ok: true, content, changed: false };
169
173
  const newline = content.includes("\r\n") ? "\r\n" : "\n";
170
174
  let lines = match[1].split(/\r?\n/);
171
- if (aliasesChanged)
175
+ if (removed)
176
+ lines = replaceTopLevelKey(lines, "aliases", renderAliases(aliases));
177
+ else if (aliasesChanged)
172
178
  lines = appendAliases(lines, aliases, aliases.slice(before.aliases.length));
173
179
  if (lines && countChanged)
174
180
  lines = replaceTopLevelKey(lines, "signal_count", `signal_count: ${signalCount}`);
@@ -27,16 +27,25 @@
27
27
  *
28
28
  * A signal whose `entity_refs` names a copy holds that copy: signals are
29
29
  * server-owned, so the ref cannot be re-pointed from here. With `rewriteRefs`
30
- * (flag `checkpoint.ontology.merge-rewrite-refs`) signal refs, invoice and
31
- * bill links, source and knowledge links are re-pointed at the survivor, the
32
- * survivor receives the copy's `external_ids` and `web_domains`, and only a
33
- * link from another copy or an external id conflict holds a pair.
30
+ * (flag `checkpoint.ontology.merge-rewrite-refs`) invoice and bill links and
31
+ * knowledge links are re-pointed at the survivor and the survivor receives
32
+ * the copy's `external_ids` and `web_domains`. A signal ref or a link from a
33
+ * source still holds the pair (`server_owned_refs`): the sync client cannot
34
+ * push those files. A link from another copy or an external id conflict also
35
+ * holds a pair.
34
36
  *
35
37
  * With `pairKind: "variants"` (flag `checkpoint.ontology.merge-variants`) the
36
38
  * pairs are two-file duplicate groups whose canonical names differ only in
37
39
  * case, spacing or punctuation; the survivor is the spaced name, then the
38
40
  * higher `signal_count`, then path order. The copy's canonical name becomes
39
41
  * an alias of the survivor. Merge and delete work as for numbered copies.
42
+ *
43
+ * With `pairKind: "listed"` (flag `checkpoint.ontology.merge-listed-pairs`)
44
+ * the pairs are exactly the `only` entries, reviewed by a person: no finder
45
+ * runs, so the names may differ. Each entry must name two entities of one
46
+ * type; a copy listed twice, or listed both as a copy and as an original, is
47
+ * skipped as `listed_conflict`. Holds, merge and delete work as for numbered
48
+ * copies.
40
49
  */
41
50
  import { type OntologyCompany } from "./company.js";
42
51
  import { type EntityIndex } from "./tree.js";
@@ -46,14 +55,16 @@ export declare const MERGE_COPIES_FLAG = "checkpoint.ontology.merge-copies";
46
55
  * names differ only in spacing, case or punctuation (CO-65).
47
56
  */
48
57
  export declare const MERGE_VARIANTS_FLAG = "checkpoint.ontology.merge-variants";
58
+ /** `merge-copies --listed`: merge exactly the reviewed `--only` pairs (CO-74). */
59
+ export declare const MERGE_LISTED_PAIRS_FLAG = "checkpoint.ontology.merge-listed-pairs";
49
60
  /**
50
61
  * `merge-copies --rewrite-refs`: re-point signal `entity_refs`, invoice and
51
62
  * bill links, source and knowledge links from a copy to its survivor, and
52
63
  * carry the copy's `external_ids` and `web_domains` to the survivor.
53
64
  */
54
65
  export declare const MERGE_REWRITE_REFS_FLAG = "checkpoint.ontology.merge-rewrite-refs";
55
- export type CopySkipReason = "no_original" | "original_invalid" | "identity_differs" | "not_in_duplicate_group" | "group_size" | "type_differs" | "names_differ" | "numbered_copy" | "not_listed";
56
- export type PairKind = "numbered" | "variants";
66
+ export type CopySkipReason = "no_original" | "original_invalid" | "identity_differs" | "not_in_duplicate_group" | "group_size" | "type_differs" | "names_differ" | "numbered_copy" | "not_listed" | "listed_conflict";
67
+ export type PairKind = "numbered" | "variants" | "listed";
57
68
  export interface CopySkip {
58
69
  copy: string;
59
70
  reason: CopySkipReason;
@@ -97,7 +108,7 @@ export interface CopyPlan extends CopyPair {
97
108
  /** Signal files whose `entity_refs` name the copy. */
98
109
  signal_refs: string[];
99
110
  /** Why the copy cannot be merged or deleted from here. */
100
- held?: "signal_refs" | "linked_from_unwritable" | "merge_invalid" | "external_id_conflict";
111
+ held?: "signal_refs" | "linked_from_unwritable" | "merge_invalid" | "external_id_conflict" | "server_owned_refs";
101
112
  detail?: string;
102
113
  /** Keys added to the survivor. Set only when `rewriteRefs` is on. */
103
114
  add_external_ids?: string[];
@@ -133,7 +144,7 @@ export interface MergeCopiesReport {
133
144
  }[];
134
145
  /** Listed entries the finder did not produce. Absent unless `--only` was set. */
135
146
  only_unmatched?: ListedPair[];
136
- /** Signal, source and knowledge files re-pointed at a survivor. Absent unless `rewriteRefs` was set. */
147
+ /** Knowledge files re-pointed at a survivor. Absent unless `rewriteRefs` was set. */
137
148
  rewritten?: string[];
138
149
  /** Entity-file links whose target file does not exist. */
139
150
  broken_links_before: number;
@@ -171,6 +182,14 @@ export declare function findVariantPairs(index: EntityIndex): {
171
182
  pairs: CopyPair[];
172
183
  skipped: CopySkip[];
173
184
  };
185
+ /**
186
+ * The reviewed pairs themselves, for `pairKind: "listed"`. An entry is kept
187
+ * when both files are indexed entities of one writable type. Read-only.
188
+ */
189
+ export declare function findListedPairs(index: EntityIndex, only: ListedPair[]): {
190
+ pairs: CopyPair[];
191
+ skipped: CopySkip[];
192
+ };
174
193
  /**
175
194
  * Plan, merge or delete numbered copies for one company. The dry run writes
176
195
  * nothing; merge writes entity files only; delete removes vault keys and
@@ -27,16 +27,25 @@
27
27
  *
28
28
  * A signal whose `entity_refs` names a copy holds that copy: signals are
29
29
  * server-owned, so the ref cannot be re-pointed from here. With `rewriteRefs`
30
- * (flag `checkpoint.ontology.merge-rewrite-refs`) signal refs, invoice and
31
- * bill links, source and knowledge links are re-pointed at the survivor, the
32
- * survivor receives the copy's `external_ids` and `web_domains`, and only a
33
- * link from another copy or an external id conflict holds a pair.
30
+ * (flag `checkpoint.ontology.merge-rewrite-refs`) invoice and bill links and
31
+ * knowledge links are re-pointed at the survivor and the survivor receives
32
+ * the copy's `external_ids` and `web_domains`. A signal ref or a link from a
33
+ * source still holds the pair (`server_owned_refs`): the sync client cannot
34
+ * push those files. A link from another copy or an external id conflict also
35
+ * holds a pair.
34
36
  *
35
37
  * With `pairKind: "variants"` (flag `checkpoint.ontology.merge-variants`) the
36
38
  * pairs are two-file duplicate groups whose canonical names differ only in
37
39
  * case, spacing or punctuation; the survivor is the spaced name, then the
38
40
  * higher `signal_count`, then path order. The copy's canonical name becomes
39
41
  * an alias of the survivor. Merge and delete work as for numbered copies.
42
+ *
43
+ * With `pairKind: "listed"` (flag `checkpoint.ontology.merge-listed-pairs`)
44
+ * the pairs are exactly the `only` entries, reviewed by a person: no finder
45
+ * runs, so the names may differ. Each entry must name two entities of one
46
+ * type; a copy listed twice, or listed both as a copy and as an original, is
47
+ * skipped as `listed_conflict`. Holds, merge and delete work as for numbered
48
+ * copies.
40
49
  */
41
50
  import * as fs from "node:fs";
42
51
  import * as path from "node:path";
@@ -53,6 +62,8 @@ export const MERGE_COPIES_FLAG = "checkpoint.ontology.merge-copies";
53
62
  * names differ only in spacing, case or punctuation (CO-65).
54
63
  */
55
64
  export const MERGE_VARIANTS_FLAG = "checkpoint.ontology.merge-variants";
65
+ /** `merge-copies --listed`: merge exactly the reviewed `--only` pairs (CO-74). */
66
+ export const MERGE_LISTED_PAIRS_FLAG = "checkpoint.ontology.merge-listed-pairs";
56
67
  /**
57
68
  * `merge-copies --rewrite-refs`: re-point signal `entity_refs`, invoice and
58
69
  * bill links, source and knowledge links from a copy to its survivor, and
@@ -219,6 +230,36 @@ export function findVariantPairs(index) {
219
230
  pairs.sort((a, b) => a.copy.localeCompare(b.copy));
220
231
  return { pairs, skipped };
221
232
  }
233
+ /**
234
+ * The reviewed pairs themselves, for `pairKind: "listed"`. An entry is kept
235
+ * when both files are indexed entities of one writable type. Read-only.
236
+ */
237
+ export function findListedPairs(index, only) {
238
+ const pairs = [];
239
+ const skipped = [];
240
+ const copyCount = new Map();
241
+ for (const entry of only)
242
+ copyCount.set(entry.copy, (copyCount.get(entry.copy) ?? 0) + 1);
243
+ const originals = new Set(only.map((entry) => entry.original));
244
+ for (const entry of only) {
245
+ const copy = index.byRel.get(entry.copy);
246
+ const original = index.byRel.get(entry.original);
247
+ if (copyCount.get(entry.copy) > 1 || originals.has(entry.copy) || entry.copy === entry.original) {
248
+ skipped.push({ copy: entry.copy, reason: "listed_conflict" });
249
+ }
250
+ else if (!copy || !original) {
251
+ skipped.push({ copy: entry.copy, reason: index.invalid.has(entry.original) ? "original_invalid" : "no_original" });
252
+ }
253
+ else if (copy.type !== original.type || !isWritableEntityType(copy.type)) {
254
+ skipped.push({ copy: entry.copy, reason: "type_differs" });
255
+ }
256
+ else {
257
+ pairs.push({ type: copy.type, copy: entry.copy, original: entry.original });
258
+ }
259
+ }
260
+ pairs.sort((a, b) => a.copy.localeCompare(b.copy));
261
+ return { pairs, skipped };
262
+ }
222
263
  /** Resolve a relative link in `fromRel` to a company-relative path, or null. */
223
264
  function resolveLink(fromRel, href) {
224
265
  if (/^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith("#") || href.startsWith("/"))
@@ -525,76 +566,13 @@ function referencesCopy(fromRel, content, copy) {
525
566
  }
526
567
  return false;
527
568
  }
528
- function entityRefOf(rel) {
529
- const [, , type, file] = rel.split("/");
530
- return `${type}/${file.slice(0, -3)}`;
531
- }
532
- /** Re-point block-style `entity_refs` items from a copy to its survivor. */
533
- function rewriteSignalRefs(content, copyRef, originalRef) {
534
- const front = frontLines(content);
535
- if (!front)
536
- return content;
537
- const index = front.lines.findIndex((line) => /^entity_refs:\s*$/.test(line));
538
- if (index === -1) {
539
- const inline = front.lines.some((line) => /^entity_refs:\s*\S/.test(line));
540
- if (inline && content.includes(copyRef))
541
- return "signal_ref_format";
542
- return content;
543
- }
544
- let end = index + 1;
545
- // A block sequence may sit at the key's own indent: `entity_refs:\n- company/x`.
546
- while (end < front.lines.length && (/^\s/.test(front.lines[end]) || /^- /.test(front.lines[end])))
547
- end += 1;
548
- const itemRe = /^(\s*)- (["']?)([^"'\n]+?)\2\s*$/;
549
- const refs = [];
550
- for (let i = index + 1; i < end; i += 1) {
551
- const match = itemRe.exec(front.lines[i]);
552
- if (!match)
553
- continue;
554
- const hadMd = match[3].endsWith(".md");
555
- refs.push({
556
- index: i,
557
- value: hadMd ? match[3].slice(0, -3) : match[3],
558
- hadMd,
559
- indent: match[1],
560
- quote: match[2],
561
- });
562
- }
563
- const copyItems = refs.filter((item) => item.value === copyRef);
564
- if (copyItems.length === 0)
565
- return content;
566
- const lines = [...front.lines];
567
- const remove = new Set();
568
- let seenOriginal = refs.some((item) => item.value === originalRef);
569
- for (const item of copyItems) {
570
- if (seenOriginal) {
571
- remove.add(item.index);
572
- }
573
- else {
574
- const value = item.hadMd ? `${originalRef}.md` : originalRef;
575
- lines[item.index] = `${item.indent}- ${item.quote}${value}${item.quote}`;
576
- seenOriginal = true;
577
- }
578
- }
579
- const next = withFrontLines(content, front, lines.filter((_, i) => !remove.has(i)), fileNewline(content));
580
- const oldParsed = parseEntityFile(content);
581
- const newParsed = parseEntityFile(next);
582
- if (!oldParsed || !newParsed || oldParsed.body !== newParsed.body)
583
- return "signal_ref_format";
584
- const keys = new Set([...Object.keys(oldParsed.frontmatter), ...Object.keys(newParsed.frontmatter)]);
585
- for (const key of keys) {
586
- if (key === "entity_refs")
587
- continue;
588
- if (!isDeepStrictEqual(oldParsed.frontmatter[key], newParsed.frontmatter[key]))
589
- return "signal_ref_format";
590
- }
591
- const newRefs = newParsed.frontmatter.entity_refs;
592
- if (!Array.isArray(newRefs) || !newRefs.every((ref) => typeof ref === "string"))
593
- return "signal_ref_format";
594
- const normalized = newRefs.map((ref) => (ref.endsWith(".md") ? ref.slice(0, -3) : ref));
595
- if (!normalized.includes(originalRef) || normalized.includes(copyRef))
596
- return "signal_ref_format";
597
- return next;
569
+ /**
570
+ * Signals and sources are written by the server pipeline and the sync
571
+ * client never pushes them, so a rewrite made here would not reach the
572
+ * vault (hq-cloud cloud-authoritative.ts).
573
+ */
574
+ function isServerOwnedRel(rel) {
575
+ return rel.startsWith("signals/") || rel.startsWith("sources/");
598
576
  }
599
577
  const REF_KEYS = new Set(["system", "namespace", "objectType", "recordId"]);
600
578
  function refKey(ref) {
@@ -871,6 +849,7 @@ function plan(company, index, pairs, allPairs, now, rewriteRefs, resolutions = n
871
849
  const unwritable = rewriteRefs
872
850
  ? inbound.filter((rel) => copies.has(rel))
873
851
  : inbound.filter((rel) => !rel.startsWith("ontology/entities/") || !isWritableEntityType(rel.split("/")[2]) || copies.has(rel));
852
+ const serverOwned = rewriteRefs ? [...refs, ...inbound.filter(isServerOwnedRel)] : [];
874
853
  if (typeof merged === "string") {
875
854
  entry.held = "merge_invalid";
876
855
  entry.detail = merged;
@@ -879,6 +858,10 @@ function plan(company, index, pairs, allPairs, now, rewriteRefs, resolutions = n
879
858
  entry.held = "external_id_conflict";
880
859
  entry.detail = identity.conflicts.join(", ");
881
860
  }
861
+ else if (serverOwned.length > 0) {
862
+ entry.held = "server_owned_refs";
863
+ entry.detail = [...new Set(serverOwned)].sort().join(", ");
864
+ }
882
865
  else if (!rewriteRefs && refs.length > 0) {
883
866
  entry.held = "signal_refs";
884
867
  }
@@ -960,7 +943,13 @@ export async function mergeCopies(options) {
960
943
  const { hqRoot, company, mode, now } = options;
961
944
  const index = buildEntityIndex(company.dir);
962
945
  const pairKind = options.pairKind ?? "numbered";
963
- const found = pairKind === "variants" ? findVariantPairs(index) : findCopyPairs(index);
946
+ if (pairKind === "listed" && !options.only)
947
+ throw new Error("merge-copies: listed pairs need an only list");
948
+ const found = pairKind === "listed"
949
+ ? findListedPairs(index, options.only)
950
+ : pairKind === "variants"
951
+ ? findVariantPairs(index)
952
+ : findCopyPairs(index);
964
953
  const listed = options.only
965
954
  ? found.pairs.filter((pair) => options.only.some((entry) => entry.copy === pair.copy && entry.original === pair.original))
966
955
  : found.pairs;
@@ -1060,28 +1049,6 @@ export async function mergeCopies(options) {
1060
1049
  report.errors.push({ path: rel, error: error.name });
1061
1050
  }
1062
1051
  }
1063
- if (!rewriteRefs)
1064
- continue;
1065
- const copyRef = entityRefOf(entry.copy);
1066
- const originalRef = entityRefOf(entry.original);
1067
- for (const rel of entry.signal_refs) {
1068
- try {
1069
- const content = read(company, rel);
1070
- const next = rewriteSignalRefs(content, copyRef, originalRef);
1071
- if (next === "signal_ref_format") {
1072
- report.errors.push({ path: rel, error: "signal_ref_format" });
1073
- continue;
1074
- }
1075
- if (next === content)
1076
- continue;
1077
- safeWriteFile(hqRoot, [company.dir], path.join(company.dir, rel), next);
1078
- if (!report.rewritten.includes(rel))
1079
- report.rewritten.push(rel);
1080
- }
1081
- catch (error) {
1082
- report.errors.push({ path: rel, error: error.name });
1083
- }
1084
- }
1085
1052
  }
1086
1053
  }
1087
1054
  if (mode === "delete") {
@@ -677,13 +677,13 @@ function scrubbedSentryEnvelope(event, overrides, additionalTags = {}) {
677
677
  ...sentryEnvelope(event, overrides, additionalTags),
678
678
  }, {});
679
679
  }
680
- /** Keep all hook-timeout conditions together by script while retaining tags for triage. */
680
+ /** Group hook-timeout conditions by normalized kind while retaining triage tags. */
681
681
  function hookTimeoutExceededOverrides(event) {
682
682
  if (!isHookTimeoutEvent(event))
683
683
  return undefined;
684
684
  return {
685
685
  level: "warning",
686
- fingerprint: ["hook-timeout", hookScript(event.metadata)],
686
+ fingerprint: ["hook-timeout", hookTimeoutKind(event)],
687
687
  };
688
688
  }
689
689
  /**
@@ -6,6 +6,8 @@
6
6
  import { type FlagClientOptions, type FlagResolution } from "@indigoai-us/hq-flags-client";
7
7
  /** Outposts hand running services to the next daemon on a CLI update instead of restarting them all. */
8
8
  export declare const SUPERVISOR_HANDOFF_FLAG = "daemon.supervisor-handoff";
9
+ /** Self-updates build the new CLI beside the running one and swap it in, so `hq` never disappears mid-update. */
10
+ export declare const STAGED_SELF_UPDATE_FLAG = "cli.staged-self-update";
9
11
  export interface DaemonFlagClient {
10
12
  ready(): Promise<void>;
11
13
  explain(flagKey: string, options?: {
@@ -9,6 +9,8 @@ import { resolveFlagRegistryEndpoint } from "../flag-registry-endpoint.js";
9
9
  import { resolveProcessFlagCompanyContext } from "../flag-registry.js";
10
10
  /** Outposts hand running services to the next daemon on a CLI update instead of restarting them all. */
11
11
  export const SUPERVISOR_HANDOFF_FLAG = "daemon.supervisor-handoff";
12
+ /** Self-updates build the new CLI beside the running one and swap it in, so `hq` never disappears mid-update. */
13
+ export const STAGED_SELF_UPDATE_FLAG = "cli.staged-self-update";
12
14
  const READ_TIMEOUT_MS = 5_000;
13
15
  export async function readDaemonFlag(flagKey, options = {}) {
14
16
  const env = options.env ?? process.env;
@@ -62,5 +62,16 @@ export declare function rotateLogs(logsDir: string, maxBytes?: number): number;
62
62
  * an on-failure unit down. The daemon's updater handles CLI updates.
63
63
  */
64
64
  export declare function unitProcessEnv(base: NodeJS.ProcessEnv, unit: UnitPlan): NodeJS.ProcessEnv;
65
+ /**
66
+ * A systemd system unit starts the daemon as the login user without the
67
+ * variables a login session sets, so `systemctl --user` from its services
68
+ * cannot find the user bus ("Failed to connect to bus: No medium found").
69
+ * Point them at the user's runtime dir when it exists and nothing set one.
70
+ */
71
+ export declare function withUserRuntimeDir(env: NodeJS.ProcessEnv, host: {
72
+ platform: NodeJS.Platform;
73
+ uid: number;
74
+ exists: (dir: string) => boolean;
75
+ }): NodeJS.ProcessEnv;
65
76
  export declare function runDaemon(opts: RunDaemonOptions): Promise<number>;
66
77
  //# sourceMappingURL=run.d.ts.map
@@ -233,8 +233,24 @@ function isFifo(file) {
233
233
  return false;
234
234
  }
235
235
  }
236
+ /**
237
+ * A systemd system unit starts the daemon as the login user without the
238
+ * variables a login session sets, so `systemctl --user` from its services
239
+ * cannot find the user bus ("Failed to connect to bus: No medium found").
240
+ * Point them at the user's runtime dir when it exists and nothing set one.
241
+ */
242
+ export function withUserRuntimeDir(env, host) {
243
+ if (host.platform !== "linux" || host.uid === 0 || env.XDG_RUNTIME_DIR)
244
+ return env;
245
+ const dir = `/run/user/${host.uid}`;
246
+ return host.exists(dir) ? { ...env, XDG_RUNTIME_DIR: dir } : env;
247
+ }
236
248
  export async function runDaemon(opts) {
237
- const env = opts.env ?? process.env;
249
+ const env = withUserRuntimeDir(opts.env ?? process.env, {
250
+ platform: process.platform,
251
+ uid: process.getuid?.() ?? -1,
252
+ exists: fs.existsSync,
253
+ });
238
254
  const home = opts.home ?? os.homedir();
239
255
  const platform = process.platform;
240
256
  const paths = daemonPaths(home, env);
@@ -12,8 +12,9 @@ export interface RegisteredLead {
12
12
  status: string;
13
13
  prd: string;
14
14
  }
15
+ export type AutopilotCondition = "review_finished_worker" | "fill_grant" | "plan_free_slots" | "flat_kpi" | "blocker_record_needs_update" | "project_ready_to_close" | "stale_worker_ledger" | "story_without_live_lane";
15
16
  export interface AutopilotWake {
16
- condition: "review_finished_worker" | "fill_grant";
17
+ condition: AutopilotCondition;
17
18
  lead_lane: string;
18
19
  story_id: string;
19
20
  worker_lane?: string;
@@ -30,6 +31,14 @@ export interface AutopilotPassResult {
30
31
  story_id: string;
31
32
  worker_lane: string;
32
33
  }>;
34
+ escalated: Array<{
35
+ team: string;
36
+ lead_lane: string;
37
+ condition: AutopilotCondition;
38
+ story_id: string;
39
+ at: string;
40
+ fingerprint: string;
41
+ }>;
33
42
  skipped: Array<{
34
43
  lead_lane: string;
35
44
  reason: string;
@@ -38,15 +47,18 @@ export interface AutopilotPassResult {
38
47
  interface ProjectStory {
39
48
  id: string;
40
49
  status: string;
50
+ passes?: boolean;
41
51
  briefPath?: string;
42
52
  laneIds?: string[];
43
53
  updatedAt?: string;
54
+ externalBlocker?: string;
44
55
  }
45
56
  export declare function planLeadAutopilotWakes(input: {
46
57
  lead: LaneRecord;
47
58
  children: readonly LaneRecord[];
48
59
  stories: readonly ProjectStory[];
49
60
  now: Date;
61
+ managerStatus?: Record<string, unknown>;
50
62
  keepAliveResumed?: boolean;
51
63
  }): AutopilotWake[];
52
64
  export declare function runLeadAutopilotPass(input: {