@indigoai-us/hq-cli 5.276.0 → 5.278.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 (83) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-pickup.sh +99 -0
  3. package/assets/scaffold/core/scripts/hq-delegate-repo.sh +158 -0
  4. package/assets/scaffold/core/scripts/hq-delegate-resolve.sh +145 -0
  5. package/assets/scaffold/core/scripts/hq-delegate-secrets.sh +129 -0
  6. package/assets/scaffold/core/scripts/hq-delegate-transfer.sh +175 -0
  7. package/assets/scaffold/core/scripts/lib/secret-patterns.sh +58 -0
  8. package/dist/command-catalog.generated.d.ts +239 -1
  9. package/dist/command-catalog.generated.js +302 -1
  10. package/dist/commands/core-delegate.d.ts +6 -0
  11. package/dist/commands/core-delegate.js +987 -0
  12. package/dist/commands/core.js +10 -0
  13. package/dist/commands/daemon.d.ts +3 -0
  14. package/dist/commands/daemon.js +173 -1
  15. package/dist/commands/dm.d.ts +2 -0
  16. package/dist/commands/dm.js +11 -4
  17. package/dist/commands/lanes-accounts.js +44 -2
  18. package/dist/commands/lanes-message.js +69 -12
  19. package/dist/commands/lanes-resume.js +2 -0
  20. package/dist/commands/lanes-shared.d.ts +3 -0
  21. package/dist/commands/lanes-shared.js +12 -2
  22. package/dist/commands/lanes.d.ts +5 -1
  23. package/dist/commands/lanes.js +399 -47
  24. package/dist/commands/secrets.d.ts +16 -1
  25. package/dist/commands/secrets.js +77 -48
  26. package/dist/commands/sync-mode.d.ts +11 -0
  27. package/dist/commands/sync-mode.js +30 -0
  28. package/dist/fast-scaffold.d.ts +17 -0
  29. package/dist/fast-scaffold.js +91 -0
  30. package/dist/index.js +17 -1
  31. package/dist/lanes-drain-hook.js +154 -7
  32. package/dist/lib/agent-kit/run/inbox.js +3 -2
  33. package/dist/lib/checkpoint-ontology/apply.d.ts +6 -0
  34. package/dist/lib/checkpoint-ontology/apply.js +111 -3
  35. package/dist/lib/checkpoint-ontology/plan.d.ts +23 -0
  36. package/dist/lib/checkpoint-ontology/plan.js +23 -0
  37. package/dist/lib/checkpoint-ontology/tree.d.ts +2 -0
  38. package/dist/lib/checkpoint-ontology/tree.js +34 -0
  39. package/dist/lib/core-utils/jq-json-serializer.d.ts +5 -0
  40. package/dist/lib/core-utils/jq-json-serializer.js +10 -0
  41. package/dist/lib/core-utils/secret-patterns.d.ts +41 -0
  42. package/dist/lib/core-utils/secret-patterns.js +33 -0
  43. package/dist/lib/daemon/catalog.d.ts +18 -2
  44. package/dist/lib/daemon/catalog.js +18 -2
  45. package/dist/lib/daemon/config.d.ts +6 -0
  46. package/dist/lib/daemon/config.js +15 -2
  47. package/dist/lib/daemon/requests.d.ts +4 -0
  48. package/dist/lib/daemon/requests.js +2 -0
  49. package/dist/lib/daemon/run.js +94 -8
  50. package/dist/lib/daemon/supervisor.d.ts +8 -0
  51. package/dist/lib/daemon/supervisor.js +101 -1
  52. package/dist/lib/daemon/sync-runner.d.ts +58 -0
  53. package/dist/lib/daemon/sync-runner.js +277 -0
  54. package/dist/lib/doctor/__testing__/run-doctor.d.ts +8 -0
  55. package/dist/lib/doctor/__testing__/run-doctor.js +52 -0
  56. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  57. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  58. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  59. package/dist/lib/lanes/claude-balance.js +46 -8
  60. package/dist/lib/lanes/config.d.ts +2 -1
  61. package/dist/lib/lanes/config.js +31 -0
  62. package/dist/lib/lanes/hire-grant-dispatch.d.ts +2 -0
  63. package/dist/lib/lanes/hire-grant-dispatch.js +41 -0
  64. package/dist/lib/lanes/index.d.ts +1 -1
  65. package/dist/lib/lanes/keep-alive.d.ts +3 -0
  66. package/dist/lib/lanes/keep-alive.js +3 -0
  67. package/dist/lib/lanes/outpost-dispatch.d.ts +4 -0
  68. package/dist/lib/lanes/outpost-dispatch.js +11 -2
  69. package/dist/lib/lanes/reconcile.d.ts +9 -0
  70. package/dist/lib/lanes/reconcile.js +127 -2
  71. package/dist/lib/lanes/runaway-tool-guard.d.ts +38 -0
  72. package/dist/lib/lanes/runaway-tool-guard.js +284 -0
  73. package/dist/lib/lanes/spawn.js +17 -10
  74. package/dist/lib/lanes/store.js +45 -1
  75. package/dist/lib/lanes/types.d.ts +37 -1
  76. package/dist/lib/lanes/types.js +2 -2
  77. package/dist/lib/lanes/watcher.d.ts +2 -0
  78. package/dist/lib/lanes/watcher.js +13 -2
  79. package/dist/secrets-env-fast-path.d.ts +5 -0
  80. package/dist/secrets-env-fast-path.js +16 -0
  81. package/dist/secrets-env-fast-route.d.ts +18 -0
  82. package/dist/secrets-env-fast-route.js +54 -0
  83. package/package.json +2 -2
@@ -16,7 +16,7 @@ import { checkSyncClient, readSyncClientVersions } from "./sync-client.js";
16
16
  import { appendBackRefs, parseEntityFile, patchEntityFile, previewSignalContent, renderNewEntity, validateEntityFrontmatter, } from "./entity-file.js";
17
17
  import { canonicalizeName, entityId, isRejectedName, isWritableEntityType, normalizeEntityName, squash, toSlug, } from "./identity.js";
18
18
  import { CANDIDATES_FILE, CANDIDATES_SCHEMA, PLAN_FILE, PLAN_SCHEMA, REPORT_FILE, readLedger, writeLedger, } from "./plan.js";
19
- import { buildEntityIndex, indexAlias, indexEntity, listSignalFiles, readSignal } from "./tree.js";
19
+ import { buildEntityIndex, countInboundLinks, indexAlias, indexEntity, listSignalFiles, readSignal } from "./tree.js";
20
20
  export const CAPS = {
21
21
  creates: 25,
22
22
  updates: 100,
@@ -26,6 +26,14 @@ export const CAPS = {
26
26
  nameLength: MAX_NAME_LENGTH,
27
27
  };
28
28
  export const MIN_CREATE_CONFIDENCE = 0.7;
29
+ /** Inbound links the dominant sibling must have for each one the named entity has. */
30
+ const DOMINANT_SIBLING_RATIO = 3;
31
+ /** Inbound links the dominant sibling must have before a redirect. */
32
+ const DOMINANT_SIBLING_MIN_INBOUND = 5;
33
+ /** checkpoint.ontology.hub-mention-floor: other entity files that must link here before the floor applies. */
34
+ const HUB_MIN_INBOUND = 50;
35
+ /** checkpoint.ontology.hub-mention-floor: whole-word canonical-name mentions required to keep the link. */
36
+ const HUB_MIN_MENTIONS = 2;
29
37
  export class ApplyError extends Error {
30
38
  constructor(message) {
31
39
  super(message);
@@ -63,6 +71,16 @@ export function parseCandidate(raw) {
63
71
  evidence: typeof c.evidence === "string" ? c.evidence : "",
64
72
  };
65
73
  }
74
+ function escapeRegExp(name) {
75
+ return name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
76
+ }
77
+ /** Whole-word, any-case matches of `name` in `text`, with Unicode letters and digits as word characters. Empty or whitespace-only names match nothing. */
78
+ function countMentions(text, name) {
79
+ if (name.trim() === "")
80
+ return 0;
81
+ const pattern = new RegExp(`(?<![\\p{L}\\p{N}])${escapeRegExp(name)}(?![\\p{L}\\p{N}])`, "giu");
82
+ return text.match(pattern)?.length ?? 0;
83
+ }
66
84
  /**
67
85
  * checkpoint.ontology.named-evidence: true when `evidence` holds one of
68
86
  * `names` as a word, matching case. A name inside a path, domain, handle, tag
@@ -211,6 +229,7 @@ export function applyOntologyRun(options) {
211
229
  matched: 0,
212
230
  session_links: [],
213
231
  links: [],
232
+ ...(plan.dominant_sibling_link === true ? { dominant_sibling_redirects: [] } : {}),
214
233
  rejected: [],
215
234
  deferred: [],
216
235
  suspect_duplicates: [],
@@ -300,10 +319,77 @@ export function applyOntologyRun(options) {
300
319
  if (types.size > 1)
301
320
  crossTypeNames.add(name);
302
321
  }
322
+ // checkpoint.ontology.dominant-sibling-link and checkpoint.ontology.hub-mention-floor:
323
+ // how many other entity files link to each entity.
324
+ const inbound = plan.dominant_sibling_link === true || plan.hub_mention_floor === true
325
+ ? countInboundLinks(company.dir, index)
326
+ : new Map();
327
+ const siblings = new Map();
328
+ if (plan.dominant_sibling_link === true) {
329
+ const grouped = new Map();
330
+ for (const entity of index.byRel.values()) {
331
+ const key = crossTypeKey(entity.canonical_name);
332
+ if (key === "")
333
+ continue;
334
+ const members = grouped.get(key) ?? [];
335
+ members.push({ rel: entity.rel, type: entity.type });
336
+ grouped.set(key, members);
337
+ }
338
+ for (const [key, members] of grouped) {
339
+ if (new Set(members.map((member) => member.type)).size < 2)
340
+ continue;
341
+ siblings.set(key, members.map((member) => member.rel));
342
+ }
343
+ }
344
+ const dominantSibling = (rel) => {
345
+ const group = siblings.get(crossTypeKey(index.byRel.get(rel).canonical_name));
346
+ if (!group)
347
+ return null;
348
+ let best = group[0];
349
+ for (const member of group) {
350
+ const count = inbound.get(member) ?? 0;
351
+ const bestCount = inbound.get(best) ?? 0;
352
+ if (count > bestCount || (count === bestCount && member.localeCompare(best) < 0))
353
+ best = member;
354
+ }
355
+ const relCount = inbound.get(rel) ?? 0;
356
+ const bestCount = inbound.get(best) ?? 0;
357
+ if (relCount >= bestCount)
358
+ return null;
359
+ if (bestCount >= DOMINANT_SIBLING_RATIO * relCount && bestCount >= DOMINANT_SIBLING_MIN_INBOUND)
360
+ return best;
361
+ return null;
362
+ };
303
363
  // checkpoint.ontology.named-evidence: the company's own company entity.
304
364
  const ownCompanyRel = `ontology/entities/company/${company.slug}.md`;
305
365
  const isOwnCompany = (entity, rel) => entity.type === "company" && (rel === ownCompanyRel || crossTypeKey(entity.canonical_name) === crossTypeKey(company.slug));
306
366
  const inputs = liveInputs(options.hqRoot, company, plan);
367
+ const inputTexts = new Map();
368
+ // The text the model was given for this input: the annotation-stripped copy
369
+ // when there is one, cut to the chunk's line range.
370
+ const inputText = (input) => {
371
+ const cached = inputTexts.get(input.id);
372
+ if (cached !== undefined)
373
+ return cached;
374
+ if (input.read_path !== undefined && input.read_path.split("/").includes("..")) {
375
+ throw new ApplyError("ontology plan read_path leaves the run directory");
376
+ }
377
+ const file = input.read_path !== undefined
378
+ ? path.join(options.runDir, input.read_path)
379
+ : path.join(company.dir, input.path);
380
+ let text = fs.readFileSync(file, "utf8");
381
+ if (input.lines !== null)
382
+ text = text.split("\n").slice(input.lines[0] - 1, input.lines[1]).join("\n");
383
+ inputTexts.set(input.id, text);
384
+ return text;
385
+ };
386
+ // checkpoint.ontology.hub-mention-floor: the final target of a knowledge link is a
387
+ // non-person entity many files link to, and the input names it fewer than twice.
388
+ const underMentionedHub = (input, rel, entity) => plan.hub_mention_floor === true &&
389
+ input.kind === "knowledge" &&
390
+ entity.type !== "person" &&
391
+ (inbound.get(rel) ?? 0) >= HUB_MIN_INBOUND &&
392
+ countMentions(inputText(input), entity.canonical_name) < HUB_MIN_MENTIONS;
307
393
  const blocked = new Set();
308
394
  const updates = new Map();
309
395
  const update = (rel) => {
@@ -361,19 +447,37 @@ export function applyOntologyRun(options) {
361
447
  note(report.rejected, "ambiguous_type");
362
448
  continue;
363
449
  }
450
+ const redirect = input.kind === "knowledge" && plan.dominant_sibling_link === true ? dominantSibling(resolved.target) : null;
451
+ const targetRel = redirect ?? resolved.target;
452
+ const targetEntity = redirect ? index.byRel.get(targetRel) : linked;
364
453
  if (plan.named_evidence === true &&
365
454
  input.kind === "knowledge" &&
366
- isOwnCompany(linked, resolved.target) &&
455
+ isOwnCompany(targetEntity, targetRel) &&
367
456
  !evidenceNames(candidate.evidence, [candidate.name, ...candidate.aliases])) {
368
457
  note(report.rejected, "unnamed_in_evidence");
369
458
  continue;
370
459
  }
460
+ if (underMentionedHub(input, targetRel, targetEntity)) {
461
+ note(report.rejected, "hub_under_mentioned");
462
+ continue;
463
+ }
371
464
  report.matched += 1;
372
- report.links.push({ input: candidate.input, action: "link", type: linked.type, canonical_name: linked.canonical_name, path: resolved.target });
465
+ report.links.push({
466
+ input: candidate.input,
467
+ action: "link",
468
+ type: targetEntity.type,
469
+ canonical_name: targetEntity.canonical_name,
470
+ path: targetRel,
471
+ });
373
472
  if (resolved.tier === 1 && resolved.duplicates.length > 0) {
374
473
  report.suspect_duplicates.push({ name: canonical, type: candidate.type, paths: [resolved.target, ...resolved.duplicates] });
474
+ }
475
+ if (redirect) {
476
+ report.dominant_sibling_redirects?.push({ input: candidate.input, from: resolved.target, to: targetRel });
375
477
  continue;
376
478
  }
479
+ if (resolved.tier === 1 && resolved.duplicates.length > 0)
480
+ continue;
377
481
  // checkpoint.ontology.no-link-aliases: the link is reported and the entity file is left as it is.
378
482
  if (plan.link_aliases === false)
379
483
  continue;
@@ -413,6 +517,10 @@ export function applyOntologyRun(options) {
413
517
  continue;
414
518
  }
415
519
  const linked = index.byRel.get(projects[0]);
520
+ if (underMentionedHub(input, projects[0], linked)) {
521
+ note(report.rejected, "hub_under_mentioned");
522
+ continue;
523
+ }
416
524
  report.matched += 1;
417
525
  report.links.push({ input: candidate.input, action: "link", type: linked.type, canonical_name: linked.canonical_name, path: projects[0] });
418
526
  continue;
@@ -70,6 +70,25 @@ export declare const FIRST_NAME_PEOPLE_FLAG = "checkpoint.ontology.first-name-pe
70
70
  * to 0.803 and cost recall 0.023 on a replay of six runs.
71
71
  */
72
72
  export declare const SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG = "checkpoint.ontology.skip-ambiguous-concept-links";
73
+ /**
74
+ * A knowledge link to the weaker of two entities that share a canonical name
75
+ * across types is redirected to the sibling other entity files link to
76
+ * (`project/build-your-own-agi` beside `concept/build-your-own-agi`, or
77
+ * `project/hq-core 2` beside `project/hq-core` when a concept also holds the
78
+ * name). The plan carries `dominant_sibling_link: true`, and apply reports
79
+ * the move in `dominant_sibling_redirects` without writing the entity files.
80
+ * CO-60: a replay of six runs raised knowledge precision from 0.725 to 0.754
81
+ * and recall from 0.784 to 0.816, and never changed a create.
82
+ */
83
+ export declare const DOMINANT_SIBLING_LINK_FLAG = "checkpoint.ontology.dominant-sibling-link";
84
+ /**
85
+ * A knowledge link to a non-person entity that 50 or more
86
+ * other entity files link to is rejected as `hub_under_mentioned` when the
87
+ * input names it fewer than 2 times (whole word, any case, canonical name
88
+ * only). The plan carries `hub_mention_floor: true`. CO-62: a replay of six
89
+ * runs raised knowledge precision from 0.753 to 0.781 at the same recall.
90
+ */
91
+ export declare const HUB_MENTION_FLOOR_FLAG = "checkpoint.ontology.hub-mention-floor";
73
92
  /**
74
93
  * A knowledge concept candidate whose name is an alias of more than one
75
94
  * entity, and which apply would defer as `ambiguous_alias`, links instead to
@@ -173,6 +192,10 @@ export interface OntologyPlan {
173
192
  first_name_people?: true;
174
193
  /** Set by checkpoint.ontology.skip-ambiguous-concept-links: apply rejects a knowledge link to a concept whose name another type also holds. */
175
194
  skip_ambiguous_concept_links?: true;
195
+ /** Set by checkpoint.ontology.dominant-sibling-link: apply redirects a knowledge link from a weak cross-type sibling to the one other files link to. */
196
+ dominant_sibling_link?: true;
197
+ /** Set by checkpoint.ontology.hub-mention-floor: apply rejects a knowledge link to an under-mentioned non-person hub. */
198
+ hub_mention_floor?: true;
176
199
  /** Set by checkpoint.ontology.concept-project-link: apply links a knowledge concept with an ambiguous alias to the one project of that canonical name. */
177
200
  concept_project_link?: true;
178
201
  /** Set by checkpoint.ontology.named-evidence: apply rejects a knowledge link to the company's own entity, or a concept-project-link redirect, whose evidence does not name it as a word. */
@@ -75,6 +75,25 @@ export const FIRST_NAME_PEOPLE_FLAG = "checkpoint.ontology.first-name-people";
75
75
  * to 0.803 and cost recall 0.023 on a replay of six runs.
76
76
  */
77
77
  export const SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG = "checkpoint.ontology.skip-ambiguous-concept-links";
78
+ /**
79
+ * A knowledge link to the weaker of two entities that share a canonical name
80
+ * across types is redirected to the sibling other entity files link to
81
+ * (`project/build-your-own-agi` beside `concept/build-your-own-agi`, or
82
+ * `project/hq-core 2` beside `project/hq-core` when a concept also holds the
83
+ * name). The plan carries `dominant_sibling_link: true`, and apply reports
84
+ * the move in `dominant_sibling_redirects` without writing the entity files.
85
+ * CO-60: a replay of six runs raised knowledge precision from 0.725 to 0.754
86
+ * and recall from 0.784 to 0.816, and never changed a create.
87
+ */
88
+ export const DOMINANT_SIBLING_LINK_FLAG = "checkpoint.ontology.dominant-sibling-link";
89
+ /**
90
+ * A knowledge link to a non-person entity that 50 or more
91
+ * other entity files link to is rejected as `hub_under_mentioned` when the
92
+ * input names it fewer than 2 times (whole word, any case, canonical name
93
+ * only). The plan carries `hub_mention_floor: true`. CO-62: a replay of six
94
+ * runs raised knowledge precision from 0.753 to 0.781 at the same recall.
95
+ */
96
+ export const HUB_MENTION_FLOOR_FLAG = "checkpoint.ontology.hub-mention-floor";
78
97
  /**
79
98
  * A knowledge concept candidate whose name is an alias of more than one
80
99
  * entity, and which apply would defer as `ambiguous_alias`, links instead to
@@ -305,6 +324,8 @@ export async function planOntologyRun(options) {
305
324
  const noLinkAliases = await options.readFlag(NO_LINK_ALIASES_FLAG, company);
306
325
  const firstNamePeople = await options.readFlag(FIRST_NAME_PEOPLE_FLAG, company);
307
326
  const skipAmbiguousConceptLinks = await options.readFlag(SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG, company);
327
+ const dominantSiblingLink = await options.readFlag(DOMINANT_SIBLING_LINK_FLAG, company);
328
+ const hubMentionFloor = await options.readFlag(HUB_MENTION_FLOOR_FLAG, company);
308
329
  const conceptProjectLink = await options.readFlag(CONCEPT_PROJECT_LINK_FLAG, company);
309
330
  const namedEvidence = await options.readFlag(NAMED_EVIDENCE_FLAG, company);
310
331
  const queuedTouchesOn = borrowed || (await options.readFlag(QUEUED_TOUCHES_FLAG, company));
@@ -428,6 +449,8 @@ export async function planOntologyRun(options) {
428
449
  ...(noLinkAliases ? { link_aliases: false } : {}),
429
450
  ...(firstNamePeople ? { first_name_people: true } : {}),
430
451
  ...(skipAmbiguousConceptLinks ? { skip_ambiguous_concept_links: true } : {}),
452
+ ...(dominantSiblingLink ? { dominant_sibling_link: true } : {}),
453
+ ...(hubMentionFloor ? { hub_mention_floor: true } : {}),
431
454
  ...(conceptProjectLink ? { concept_project_link: true } : {}),
432
455
  ...(namedEvidence ? { named_evidence: true } : {}),
433
456
  inputs,
@@ -36,6 +36,8 @@ export declare function indexAlias(index: EntityIndex, type: string, rel: string
36
36
  * type conflict, but they are never written.
37
37
  */
38
38
  export declare function buildEntityIndex(companyDir: string): EntityIndex;
39
+ /** Entity path → number of other entity files whose body links to it. */
40
+ export declare function countInboundLinks(companyDir: string, index: EntityIndex): Map<string, number>;
39
41
  export interface SignalRecord {
40
42
  /** Company-relative path, `signals/<type>/<file>.md`. */
41
43
  rel: string;
@@ -90,6 +90,40 @@ export function buildEntityIndex(companyDir) {
90
90
  }
91
91
  return index;
92
92
  }
93
+ // Spaces are allowed in the target, as merge-copies writes `../person/jane-doe 2.md`.
94
+ const INBOUND_LINK = /\]\(([^)\n]+?\.md)(?:#[^)\n]*)?\)/g;
95
+ /** Entity path → number of other entity files whose body links to it. */
96
+ export function countInboundLinks(companyDir, index) {
97
+ const counts = new Map();
98
+ for (const rel of index.byRel.keys())
99
+ counts.set(rel, 0);
100
+ for (const rel of index.byRel.keys()) {
101
+ const parsed = parseEntityFile(fs.readFileSync(path.join(companyDir, rel), "utf8"));
102
+ if (!parsed)
103
+ continue;
104
+ const seen = new Set();
105
+ for (const match of parsed.body.matchAll(INBOUND_LINK)) {
106
+ const target = match[1];
107
+ if (target.startsWith("http:") || target.startsWith("https:") || target.startsWith("mailto:"))
108
+ continue;
109
+ let decoded = target;
110
+ try {
111
+ decoded = decodeURI(target);
112
+ }
113
+ catch (error) {
114
+ if (!(error instanceof URIError))
115
+ throw error;
116
+ }
117
+ const resolved = path.posix.normalize(path.posix.join(path.posix.dirname(rel), decoded));
118
+ if (resolved === rel || !index.byRel.has(resolved))
119
+ continue;
120
+ seen.add(resolved);
121
+ }
122
+ for (const targetRel of seen)
123
+ counts.set(targetRel, (counts.get(targetRel) ?? 0) + 1);
124
+ }
125
+ return counts;
126
+ }
93
127
  /** Parse the frontmatter of one company-audience signal file. */
94
128
  export function readSignal(companyDir, rel) {
95
129
  let content;
@@ -0,0 +1,5 @@
1
+ /** JSON output formats used by the scaffold scripts that previously called jq. */
2
+ export type JqJsonOutput = "compact" | "raw" | "pretty";
3
+ /** Serialize one jq result, including jq's output newline. */
4
+ export declare function serializeJqJson(value: unknown, output?: JqJsonOutput): string;
5
+ //# sourceMappingURL=jq-json-serializer.d.ts.map
@@ -0,0 +1,10 @@
1
+ /** Serialize one jq result, including jq's output newline. */
2
+ export function serializeJqJson(value, output = "pretty") {
3
+ if (output === "raw" && typeof value === "string")
4
+ return `${value}\n`;
5
+ const serialized = output === "compact"
6
+ ? JSON.stringify(value)
7
+ : JSON.stringify(value, null, 2);
8
+ return serialized === undefined ? "" : `${serialized.replace(/\u007f/g, "\\u007f")}\n`;
9
+ }
10
+ //# sourceMappingURL=jq-json-serializer.js.map
@@ -0,0 +1,41 @@
1
+ /** Shared secret detector patterns mirrored from the scaffold shell library. */
2
+ export declare const SECRET_PATTERN_BOUNDARY = "(^|[^a-zA-Z0-9])";
3
+ export declare const SECRET_PATTERNS: readonly [{
4
+ readonly pattern: "sk-[a-zA-Z0-9._-]{20,}";
5
+ readonly name: "OpenAI/Stripe key";
6
+ }, {
7
+ readonly pattern: "ghp_[a-zA-Z0-9]{36,}";
8
+ readonly name: "GitHub PAT";
9
+ }, {
10
+ readonly pattern: "AKIA[0-9A-Z]{16}";
11
+ readonly name: "AWS access key";
12
+ }, {
13
+ readonly pattern: "xox[bpsa]-[a-zA-Z0-9-]+";
14
+ readonly name: "Slack token";
15
+ }, {
16
+ readonly pattern: "Bearer [a-zA-Z0-9._-]{20,}";
17
+ readonly name: "Bearer token";
18
+ }, {
19
+ readonly pattern: "-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----";
20
+ readonly name: "Private key";
21
+ }, {
22
+ readonly pattern: "glpat-[a-zA-Z0-9_-]{20,}";
23
+ readonly name: "GitLab PAT";
24
+ }, {
25
+ readonly pattern: "gho_[a-zA-Z0-9]{36,}";
26
+ readonly name: "GitHub OAuth token";
27
+ }, {
28
+ readonly pattern: "github_pat_[a-zA-Z0-9_]{22,}";
29
+ readonly name: "Fine-grained GitHub PAT";
30
+ }, {
31
+ readonly pattern: "eyJ[a-zA-Z0-9_-]{20,}\\.[a-zA-Z0-9_-]{20,}\\.[a-zA-Z0-9_-]{10,}";
32
+ readonly name: "JWT";
33
+ }];
34
+ /** Return the pattern names that match, in the shell library's declared order. */
35
+ export declare function matchingSecretPatterns(value: string): string[];
36
+ /** Print only pattern names and paths; never include matched file content. */
37
+ export declare function scanSecretFiles(files: readonly string[]): {
38
+ code: 0 | 1;
39
+ hits: string[];
40
+ };
41
+ //# sourceMappingURL=secret-patterns.d.ts.map
@@ -0,0 +1,33 @@
1
+ /** Shared secret detector patterns mirrored from the scaffold shell library. */
2
+ import * as fs from "node:fs";
3
+ export const SECRET_PATTERN_BOUNDARY = "(^|[^a-zA-Z0-9])";
4
+ export const SECRET_PATTERNS = [
5
+ { pattern: "sk-[a-zA-Z0-9._-]{20,}", name: "OpenAI/Stripe key" },
6
+ { pattern: "ghp_[a-zA-Z0-9]{36,}", name: "GitHub PAT" },
7
+ { pattern: "AKIA[0-9A-Z]{16}", name: "AWS access key" },
8
+ { pattern: "xox[bpsa]-[a-zA-Z0-9-]+", name: "Slack token" },
9
+ { pattern: "Bearer [a-zA-Z0-9._-]{20,}", name: "Bearer token" },
10
+ { pattern: "-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----", name: "Private key" },
11
+ { pattern: "glpat-[a-zA-Z0-9_-]{20,}", name: "GitLab PAT" },
12
+ { pattern: "gho_[a-zA-Z0-9]{36,}", name: "GitHub OAuth token" },
13
+ { pattern: "github_pat_[a-zA-Z0-9_]{22,}", name: "Fine-grained GitHub PAT" },
14
+ { pattern: "eyJ[a-zA-Z0-9_-]{20,}\\.[a-zA-Z0-9_-]{20,}\\.[a-zA-Z0-9_-]{10,}", name: "JWT" },
15
+ ];
16
+ /** Return the pattern names that match, in the shell library's declared order. */
17
+ export function matchingSecretPatterns(value) {
18
+ return SECRET_PATTERNS.flatMap(({ pattern, name }) => new RegExp(`${SECRET_PATTERN_BOUNDARY}${pattern}`).test(value) ? [name] : []);
19
+ }
20
+ /** Print only pattern names and paths; never include matched file content. */
21
+ export function scanSecretFiles(files) {
22
+ const hits = [];
23
+ for (const file of files) {
24
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile())
25
+ continue;
26
+ const contents = fs.readFileSync(file, "utf8");
27
+ for (const name of matchingSecretPatterns(contents)) {
28
+ hits.push(`secret-scan: pattern '${name}' matched in ${file}`);
29
+ }
30
+ }
31
+ return { code: hits.length > 0 ? 1 : 0, hits };
32
+ }
33
+ //# sourceMappingURL=secret-patterns.js.map
@@ -10,6 +10,8 @@
10
10
  * them instead of silently dropping them.
11
11
  */
12
12
  import type { MachineType } from "./machine-type.js";
13
+ import type { SyncOwner } from "./config.js";
14
+ import { type SyncRunnerProcess } from "./sync-runner.js";
13
15
  export declare const SERVICE_NAMES: readonly ["sync", "mesh", "heartbeat", "inbox", "bots", "remote-control", "codex-remote-control", "session-host", "lanes-reconcile", "search-index", "jobs-reconcile", "updater", "housekeeping"];
14
16
  export type ServiceName = (typeof SERVICE_NAMES)[number];
15
17
  export declare function isServiceName(value: unknown): value is ServiceName;
@@ -51,6 +53,8 @@ export interface UnitPlan {
51
53
  restart: "always" | "on-failure";
52
54
  intervalSec?: number;
53
55
  initialDelaySec?: number;
56
+ /** Kill and restart this child after silence on its liveness stream. */
57
+ heartbeatTimeoutMs?: number;
54
58
  command: UnitCommand | null;
55
59
  env: Record<string, string>;
56
60
  blocked?: UnitBlock;
@@ -75,11 +79,23 @@ export interface PlanContext {
75
79
  sessionHostRunnerPath: string | null;
76
80
  /** Environment retained from legacy outpost sync systemd drop-ins. */
77
81
  legacySyncEnv: Record<string, string>;
82
+ /** Keep the local sync runner stopped until the operator resumes it. */
83
+ syncPaused: boolean;
84
+ /** Persisted/effective owner selected for this machine. */
85
+ syncOwner: SyncOwner;
86
+ /** A legacy desktop app or sync unit is still managing sync. */
87
+ desktopAppManagesSync: boolean;
88
+ /** Full process list, or null when the daemon could not read it. */
89
+ syncRunnerProcesses: readonly SyncRunnerProcess[] | null;
90
+ /** PID recorded in the watch-owner status, when it is readable. */
91
+ syncRunnerOwnerPid: number | null;
92
+ /** Sync runner children already started by this daemon before their lease is written. */
93
+ daemonSyncRunnerPids?: ReadonlySet<number>;
94
+ /** The protocol-event heartbeat watchdog timeout for the sync runner. */
95
+ syncHeartbeatTimeoutMs: number;
78
96
  signedIn: boolean;
79
97
  /** ~/.hq-agent/kit.json exists. */
80
98
  kitConfigured: boolean;
81
- /** The HQ desktop app runs its own sync on this machine. */
82
- desktopAppManagesSync: boolean;
83
99
  bots: readonly string[];
84
100
  /** Name of a pre-daemon unit that still runs this unit id, or null. */
85
101
  legacyOwner: (unitId: string) => string | null;
@@ -9,6 +9,7 @@
9
9
  * with no entrypoint yet stay in the catalog as `unavailable` so status shows
10
10
  * them instead of silently dropping them.
11
11
  */
12
+ import { SYNC_RUNNER_OWNER, syncRunnerUnleasedBlockReason } from "./sync-runner.js";
12
13
  export const SERVICE_NAMES = [
13
14
  "sync",
14
15
  "mesh",
@@ -127,14 +128,29 @@ function planService(service, ctx) {
127
128
  command: {
128
129
  type: "node",
129
130
  script: ctx.syncRunnerPath,
130
- args: ["--companies", "--direction", "both", "--on-conflict", "keep", "--hq-root", ctx.hqRoot ?? "", "--watch", "--event-push"],
131
+ args: ["--companies", "--direction", "both", "--on-conflict", "keep", "--hq-root", ctx.hqRoot ?? "", "--watch", "--event-push", "--owner", SYNC_RUNNER_OWNER],
131
132
  },
132
133
  env: { ...ctx.legacySyncEnv },
134
+ heartbeatTimeoutMs: ctx.syncHeartbeatTimeoutMs,
133
135
  });
136
+ let ownerBlock;
137
+ if (ctx.machineType === "desktop" && ctx.syncOwner === "desktop-app" && ctx.desktopAppManagesSync) {
138
+ ownerBlock = "the desktop app is still managing sync (run `hq daemon sync take-ownership` after it stops its runner)";
139
+ }
140
+ else if (ctx.machineType === "desktop" &&
141
+ ctx.syncOwner === "daemon" &&
142
+ !ctx.syncPaused &&
143
+ ctx.hqRoot &&
144
+ ctx.signedIn) {
145
+ ownerBlock = ctx.syncRunnerProcesses === null
146
+ ? "could not check for an existing sync runner; waiting to avoid starting a duplicate"
147
+ : syncRunnerUnleasedBlockReason(ctx.hqRoot, ctx.syncRunnerProcesses, ctx.syncRunnerOwnerPid, ctx.platform, ctx.daemonSyncRunnerPids);
148
+ }
134
149
  unit.blocked = firstBlock([
135
- [ctx.desktopAppManagesSync, "the HQ desktop app is handling sync on this machine"],
150
+ [ctx.syncPaused, "sync is paused (run `hq daemon sync resume`)"],
136
151
  [!ctx.hqRoot, NO_HQ_ROOT],
137
152
  [!ctx.signedIn, NOT_SIGNED_IN],
153
+ [!!ownerBlock, ownerBlock ?? ""],
138
154
  ]);
139
155
  return [unit];
140
156
  }
@@ -12,11 +12,17 @@ export interface DaemonConfig {
12
12
  hqRoot?: string;
13
13
  /** Exact hq-cloud package version preferred by this daemon, including prereleases. */
14
14
  hqCloudVersion?: string;
15
+ /** Persisted machine-local sync pause. */
16
+ syncPaused?: boolean;
17
+ /** Explicit desktop handoff; absent values preserve the legacy per-machine owner. */
18
+ syncOwner?: SyncOwner;
15
19
  /** Services turned on here beyond the machine type's defaults. */
16
20
  enable: string[];
17
21
  /** Default services turned off here. */
18
22
  disable: string[];
19
23
  }
24
+ export type SyncOwner = "daemon" | "desktop-app";
25
+ export declare function effectiveSyncOwner(config: Pick<DaemonConfig, "syncOwner">, machineType: MachineType): SyncOwner;
20
26
  export declare function emptyDaemonConfig(): DaemonConfig;
21
27
  export declare function readDaemonConfig(file: string): DaemonConfig;
22
28
  export declare function writeDaemonConfig(file: string, config: DaemonConfig): void;
@@ -10,8 +10,11 @@ import { valid as validSemver } from "semver";
10
10
  import { isServiceName } from "./catalog.js";
11
11
  import { isMachineType } from "./machine-type.js";
12
12
  import { ensurePrivateDir } from "./paths.js";
13
+ export function effectiveSyncOwner(config, machineType) {
14
+ return config.syncOwner ?? (machineType === "desktop" ? "desktop-app" : "daemon");
15
+ }
13
16
  export function emptyDaemonConfig() {
14
- return { version: 1, enable: [], disable: [] };
17
+ return { version: 1, syncPaused: false, enable: [], disable: [] };
15
18
  }
16
19
  function stringList(value) {
17
20
  return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
@@ -36,9 +39,16 @@ export function readDaemonConfig(file) {
36
39
  if (!raw || typeof raw !== "object")
37
40
  throw new Error(`hq daemon config at ${file} is not a JSON object`);
38
41
  const obj = raw;
39
- const config = { version: 1, enable: stringList(obj.enable), disable: stringList(obj.disable) };
42
+ const config = {
43
+ version: 1,
44
+ syncPaused: obj.syncPaused === true,
45
+ enable: stringList(obj.enable),
46
+ disable: stringList(obj.disable),
47
+ };
40
48
  if (isMachineType(obj.machineType))
41
49
  config.machineType = obj.machineType;
50
+ if (obj.syncOwner === "daemon" || obj.syncOwner === "desktop-app")
51
+ config.syncOwner = obj.syncOwner;
42
52
  if (typeof obj.hqRoot === "string" && obj.hqRoot.trim())
43
53
  config.hqRoot = obj.hqRoot;
44
54
  if (typeof obj.hqCloudVersion === "string" && validSemver(obj.hqCloudVersion) === obj.hqCloudVersion) {
@@ -56,6 +66,9 @@ export function writeDaemonConfig(file, config) {
56
66
  ordered.hqRoot = config.hqRoot;
57
67
  if (config.hqCloudVersion)
58
68
  ordered.hqCloudVersion = config.hqCloudVersion;
69
+ ordered.syncPaused = config.syncPaused === true;
70
+ if (config.syncOwner)
71
+ ordered.syncOwner = config.syncOwner;
59
72
  ordered.enable = config.enable;
60
73
  ordered.disable = config.disable;
61
74
  fs.writeFileSync(tmp, `${JSON.stringify(ordered, null, 2)}\n`, { mode: 0o600 });
@@ -15,6 +15,10 @@ export type ControlRequest = {
15
15
  action: "stop";
16
16
  unit: string;
17
17
  }
18
+ /** Re-read persisted configuration and apply any changed blocks immediately. */
19
+ | {
20
+ action: "reconcile";
21
+ }
18
22
  /** Restart the whole daemon, for example after the updater installed a new CLI. */
19
23
  | {
20
24
  action: "restart-daemon";
@@ -19,6 +19,8 @@ function isControlRequest(value) {
19
19
  return typeof v.unit === "string";
20
20
  if (v.action === "restart-daemon")
21
21
  return true;
22
+ if (v.action === "reconcile")
23
+ return true;
22
24
  return false;
23
25
  }
24
26
  // Orders requests written by one process within the same millisecond; the