@indigoai-us/hq-cli 5.275.0 → 5.277.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 (120) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +12 -0
  3. package/assets/bot-workers/setup/context/USER-GUIDE.md +4 -4
  4. package/assets/scaffold/core/scripts/hq-delegate-pickup.sh +99 -0
  5. package/assets/scaffold/core/scripts/hq-delegate-repo.sh +158 -0
  6. package/assets/scaffold/core/scripts/hq-delegate-resolve.sh +145 -0
  7. package/assets/scaffold/core/scripts/hq-delegate-secrets.sh +129 -0
  8. package/assets/scaffold/core/scripts/hq-delegate-transfer.sh +175 -0
  9. package/assets/scaffold/core/scripts/lib/secret-patterns.sh +58 -0
  10. package/dist/command-catalog.generated.d.ts +239 -1
  11. package/dist/command-catalog.generated.js +302 -1
  12. package/dist/commands/core-checkpoint.d.ts +4 -2
  13. package/dist/commands/core-checkpoint.js +34 -3
  14. package/dist/commands/core-command-catalog.d.ts +4 -2
  15. package/dist/commands/core-command-catalog.js +5 -2
  16. package/dist/commands/core-delegate.d.ts +6 -0
  17. package/dist/commands/core-delegate.js +987 -0
  18. package/dist/commands/core-ontology.js +3 -3
  19. package/dist/commands/core.js +11 -1
  20. package/dist/commands/daemon.d.ts +4 -0
  21. package/dist/commands/daemon.js +175 -2
  22. package/dist/commands/dm.d.ts +2 -0
  23. package/dist/commands/dm.js +11 -4
  24. package/dist/commands/feedback.d.ts +6 -0
  25. package/dist/commands/feedback.js +86 -37
  26. package/dist/commands/lanes-accounts.js +44 -2
  27. package/dist/commands/lanes-message.js +69 -12
  28. package/dist/commands/lanes-resume.js +2 -0
  29. package/dist/commands/lanes-shared.d.ts +3 -0
  30. package/dist/commands/lanes-shared.js +12 -2
  31. package/dist/commands/lanes.d.ts +5 -1
  32. package/dist/commands/lanes.js +402 -49
  33. package/dist/commands/run-feature-telemetry.d.ts +3 -0
  34. package/dist/commands/run-feature-telemetry.js +6 -0
  35. package/dist/commands/run.js +3 -2
  36. package/dist/commands/secrets.d.ts +16 -1
  37. package/dist/commands/secrets.js +80 -45
  38. package/dist/commands/sync-mode.d.ts +11 -0
  39. package/dist/commands/sync-mode.js +30 -0
  40. package/dist/commands/whoami.d.ts +4 -6
  41. package/dist/commands/whoami.js +39 -8
  42. package/dist/fast-core.d.ts +6 -7
  43. package/dist/fast-core.js +22 -8
  44. package/dist/fast-scaffold.d.ts +33 -7
  45. package/dist/fast-scaffold.js +104 -8
  46. package/dist/index.js +32 -1
  47. package/dist/lanes-drain-hook.js +154 -7
  48. package/dist/lib/agent-kit/run/inbox.js +3 -2
  49. package/dist/lib/billing/plan-lock.d.ts +3 -1
  50. package/dist/lib/billing/plan-lock.js +7 -1
  51. package/dist/lib/checkpoint-ontology/apply.d.ts +6 -0
  52. package/dist/lib/checkpoint-ontology/apply.js +60 -3
  53. package/dist/lib/checkpoint-ontology/company.d.ts +8 -4
  54. package/dist/lib/checkpoint-ontology/company.js +15 -7
  55. package/dist/lib/checkpoint-ontology/plan.d.ts +36 -0
  56. package/dist/lib/checkpoint-ontology/plan.js +66 -4
  57. package/dist/lib/checkpoint-ontology/queued-touches.d.ts +32 -0
  58. package/dist/lib/checkpoint-ontology/queued-touches.js +193 -0
  59. package/dist/lib/checkpoint-ontology/tree.d.ts +2 -0
  60. package/dist/lib/checkpoint-ontology/tree.js +34 -0
  61. package/dist/lib/core-utils/jq-json-serializer.d.ts +5 -0
  62. package/dist/lib/core-utils/jq-json-serializer.js +10 -0
  63. package/dist/lib/core-utils/secret-patterns.d.ts +41 -0
  64. package/dist/lib/core-utils/secret-patterns.js +33 -0
  65. package/dist/lib/core-utils/timeout-guard-native.d.ts +7 -0
  66. package/dist/lib/core-utils/timeout-guard-native.js +40 -0
  67. package/dist/lib/core-utils/timeout-guard.d.ts +3 -3
  68. package/dist/lib/core-utils/timeout-guard.js +28 -13
  69. package/dist/lib/daemon/catalog.d.ts +18 -2
  70. package/dist/lib/daemon/catalog.js +18 -2
  71. package/dist/lib/daemon/config.d.ts +6 -0
  72. package/dist/lib/daemon/config.js +15 -2
  73. package/dist/lib/daemon/requests.d.ts +4 -0
  74. package/dist/lib/daemon/requests.js +2 -0
  75. package/dist/lib/daemon/run.js +94 -8
  76. package/dist/lib/daemon/supervisor.d.ts +8 -0
  77. package/dist/lib/daemon/supervisor.js +101 -1
  78. package/dist/lib/daemon/sync-runner.d.ts +58 -0
  79. package/dist/lib/daemon/sync-runner.js +277 -0
  80. package/dist/lib/doctor/__testing__/run-doctor.d.ts +8 -0
  81. package/dist/lib/doctor/__testing__/run-doctor.js +52 -0
  82. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  83. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  84. package/dist/lib/flag-registry.d.ts +12 -7
  85. package/dist/lib/flag-registry.js +66 -75
  86. package/dist/lib/flag-snapshot-file.d.ts +20 -0
  87. package/dist/lib/flag-snapshot-file.js +91 -0
  88. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  89. package/dist/lib/lanes/claude-balance.js +46 -8
  90. package/dist/lib/lanes/config.js +15 -0
  91. package/dist/lib/lanes/hire-grant-dispatch.d.ts +2 -0
  92. package/dist/lib/lanes/hire-grant-dispatch.js +41 -0
  93. package/dist/lib/lanes/index.d.ts +1 -1
  94. package/dist/lib/lanes/keep-alive.d.ts +3 -0
  95. package/dist/lib/lanes/keep-alive.js +3 -0
  96. package/dist/lib/lanes/outpost-dispatch.d.ts +4 -0
  97. package/dist/lib/lanes/outpost-dispatch.js +11 -2
  98. package/dist/lib/lanes/reconcile.js +20 -4
  99. package/dist/lib/lanes/spawn.js +17 -10
  100. package/dist/lib/lanes/store.js +45 -1
  101. package/dist/lib/lanes/types.d.ts +30 -1
  102. package/dist/lib/lanes/types.js +2 -2
  103. package/dist/lib/lanes/watcher.js +3 -2
  104. package/dist/lib/process-flag-company-context.d.ts +8 -0
  105. package/dist/lib/process-flag-company-context.js +67 -0
  106. package/dist/main.js +3 -1
  107. package/dist/native-fast-route.d.ts +3 -0
  108. package/dist/native-fast-route.js +39 -0
  109. package/dist/secrets-env-fast-path.d.ts +5 -0
  110. package/dist/secrets-env-fast-path.js +16 -0
  111. package/dist/secrets-env-fast-route.d.ts +18 -0
  112. package/dist/secrets-env-fast-route.js +54 -0
  113. package/dist/utils/cli-telemetry.d.ts +26 -2
  114. package/dist/utils/cli-telemetry.js +178 -3
  115. package/dist/utils/hq-root-resolver.d.ts +13 -0
  116. package/dist/utils/hq-root-resolver.js +103 -0
  117. package/dist/utils/scaffold-fast-import-guard.js +6 -1
  118. package/dist/utils/vault-api.d.ts +8 -0
  119. package/dist/utils/vault-api.js +13 -1
  120. package/package.json +2 -2
@@ -13,6 +13,7 @@ import * as path from "node:path";
13
13
  import { writeCompanyBrief } from "./company-brief.js";
14
14
  import { resolveOntologyCompany, safeWriteFile, stateRoot } from "./company.js";
15
15
  import { startMultiWriterRun } from "./multi-writer.js";
16
+ import { claimQueuedTouches, recordQueuedTouches } from "./queued-touches.js";
16
17
  import { checkSyncClient, readSyncClientVersions } from "./sync-client.js";
17
18
  import { buildEntityIndex, listKnowledgeFiles, listSignalFiles, readSignal } from "./tree.js";
18
19
  export const ONTOLOGY_FLAG = "checkpoint.ontology";
@@ -74,6 +75,17 @@ export const FIRST_NAME_PEOPLE_FLAG = "checkpoint.ontology.first-name-people";
74
75
  * to 0.803 and cost recall 0.023 on a replay of six runs.
75
76
  */
76
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";
77
89
  /**
78
90
  * A knowledge concept candidate whose name is an alias of more than one
79
91
  * entity, and which apply would defer as `ambiguous_alias`, links instead to
@@ -98,6 +110,15 @@ export const CONCEPT_PROJECT_LINK_FLAG = "checkpoint.ontology.concept-project-li
98
110
  * and lost no gold link.
99
111
  */
100
112
  export const NAMED_EVIDENCE_FLAG = "checkpoint.ontology.named-evidence";
113
+ /**
114
+ * CO-59: consume the files touched by company checkpoints that were queued
115
+ * behind a busy sibling (see queued-touches.ts). On, the plan adds the
116
+ * company's queued touches to this run's session-touched set, and a run whose
117
+ * own payload names no company plans the company the parent picked for it
118
+ * (`ontology_company`), without that run's session text. Off, queued touches
119
+ * wait on disk and a company-less run skips the ontology step as before.
120
+ */
121
+ export const QUEUED_TOUCHES_FLAG = "checkpoint.ontology.queued-touches";
101
122
  export const PLAN_FILE = "ontology-plan.json";
102
123
  export const CANDIDATES_FILE = "ontology-candidates.json";
103
124
  export const REPORT_FILE = "ontology-report.json";
@@ -255,13 +276,32 @@ export function refreshLedger(company, existing, touched) {
255
276
  }
256
277
  return { ledger: { schema: LEDGER_SCHEMA, company: company.slug, boundary, entries }, firstRun };
257
278
  }
279
+ /**
280
+ * The company this run's ontology step works on: the payload's own company,
281
+ * or else the `ontology_company` the checkpoint parent picked for queued
282
+ * touches (`borrowed`).
283
+ */
284
+ export function payloadOntologyCompany(payload) {
285
+ if (typeof payload.company === "string" && payload.company)
286
+ return { slug: payload.company, borrowed: false };
287
+ if (typeof payload.ontology_company === "string" && payload.ontology_company) {
288
+ return { slug: payload.ontology_company, borrowed: true };
289
+ }
290
+ return { slug: null, borrowed: false };
291
+ }
258
292
  /** Plan this run. Never throws for a skip; returns the reason instead. */
259
293
  export async function planOntologyRun(options) {
260
294
  const { hqRoot, runDir, payload } = options;
261
- const resolution = resolveOntologyCompany(hqRoot, typeof payload.company === "string" ? payload.company : null);
295
+ const { slug, borrowed } = payloadOntologyCompany(payload);
296
+ const resolution = resolveOntologyCompany(hqRoot, slug);
262
297
  if (!resolution.ok)
263
298
  return { status: "skipped", reason: resolution.reason };
264
299
  const company = resolution.company;
300
+ // A borrowed company exists only for queued touches, so its run needs the
301
+ // queued-touches flag before anything else is read or written.
302
+ if (borrowed && !(await options.readFlag(QUEUED_TOUCHES_FLAG, company))) {
303
+ return { status: "skipped", reason: "flag off" };
304
+ }
265
305
  const ontologyOn = (await options.readFlag(ONTOLOGY_FLAG, company)) || (await options.readFlag(ONTOLOGY_SYNC_GATED_FLAG, company));
266
306
  const syncClient = ontologyOn ? checkSyncClient((options.syncClientVersions ?? readSyncClientVersions)()) : null;
267
307
  if (!ontologyOn || !syncClient?.ok) {
@@ -276,9 +316,17 @@ export async function planOntologyRun(options) {
276
316
  const noLinkAliases = await options.readFlag(NO_LINK_ALIASES_FLAG, company);
277
317
  const firstNamePeople = await options.readFlag(FIRST_NAME_PEOPLE_FLAG, company);
278
318
  const skipAmbiguousConceptLinks = await options.readFlag(SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG, company);
319
+ const dominantSiblingLink = await options.readFlag(DOMINANT_SIBLING_LINK_FLAG, company);
279
320
  const conceptProjectLink = await options.readFlag(CONCEPT_PROJECT_LINK_FLAG, company);
280
321
  const namedEvidence = await options.readFlag(NAMED_EVIDENCE_FLAG, company);
281
- const touched = sessionTouched(hqRoot, company, payload.files);
322
+ const queuedTouchesOn = borrowed || (await options.readFlag(QUEUED_TOUCHES_FLAG, company));
323
+ // A borrowed company's run belongs to another session: its own files and
324
+ // session text are not this company's input.
325
+ const touched = sessionTouched(hqRoot, company, borrowed ? [] : payload.files);
326
+ if (queuedTouchesOn) {
327
+ for (const rel of sessionTouched(hqRoot, company, claimQueuedTouches(hqRoot, company, runDir)))
328
+ touched.add(rel);
329
+ }
282
330
  // A first run takes its boundary from the entity tree, so the ledger is
283
331
  // refreshed before merge-back moves any `last_updated` forward.
284
332
  const { ledger, firstRun } = refreshLedger(company, readLedger(hqRoot, company), touched);
@@ -293,9 +341,11 @@ export async function planOntologyRun(options) {
293
341
  const inputs = [];
294
342
  let tokens = 0;
295
343
  let remaining = 0;
344
+ const leftover = [];
296
345
  for (const [rel, entry] of pending) {
297
346
  if (inputs.length >= BUDGET.maxInputs || tokens >= BUDGET.maxTokens) {
298
347
  remaining += 1;
348
+ leftover.push(rel);
299
349
  continue;
300
350
  }
301
351
  const text = fs.readFileSync(path.join(company.dir, rel), "utf8");
@@ -329,10 +379,21 @@ export async function planOntologyRun(options) {
329
379
  });
330
380
  tokens += cost;
331
381
  }
332
- if (index < chunks.length)
382
+ if (index < chunks.length) {
333
383
  remaining += 1;
384
+ leftover.push(rel);
385
+ }
334
386
  }
335
387
  writeLedger(hqRoot, company, ledger);
388
+ // The leftover files stay queued so a later company-less run can borrow this company again.
389
+ if (queuedTouchesOn && leftover.length > 0) {
390
+ try {
391
+ recordQueuedTouches(hqRoot, company.slug, leftover.map((rel) => path.relative(hqRoot, path.join(company.dir, rel))), options.now ?? new Date());
392
+ }
393
+ catch {
394
+ // Re-queueing leftovers must not fail the plan.
395
+ }
396
+ }
336
397
  if (inputs.length === 0) {
337
398
  const reason = pending.length > 0 ? "over budget" : "nothing changed";
338
399
  return {
@@ -345,7 +406,7 @@ export async function planOntologyRun(options) {
345
406
  // Session content is scoped to the session's user (design section 6, D2).
346
407
  // v1 records the existing entities it names as links in the report and
347
408
  // writes nothing from it, so it rides along only when files changed.
348
- const sessionText = sessionContent(payload);
409
+ const sessionText = borrowed ? null : sessionContent(payload);
349
410
  if (sessionText) {
350
411
  const cost = estTokens(Buffer.byteLength(sessionText, "utf8"));
351
412
  inputs.unshift({
@@ -379,6 +440,7 @@ export async function planOntologyRun(options) {
379
440
  ...(noLinkAliases ? { link_aliases: false } : {}),
380
441
  ...(firstNamePeople ? { first_name_people: true } : {}),
381
442
  ...(skipAmbiguousConceptLinks ? { skip_ambiguous_concept_links: true } : {}),
443
+ ...(dominantSiblingLink ? { dominant_sibling_link: true } : {}),
382
444
  ...(conceptProjectLink ? { concept_project_link: true } : {}),
383
445
  ...(namedEvidence ? { named_evidence: true } : {}),
384
446
  inputs,
@@ -0,0 +1,32 @@
1
+ import { type OntologyCompany } from "./company.js";
2
+ export declare const QUEUED_TOUCHES_FILE = "queued-touches.jsonl";
3
+ export declare const QUEUED_TOUCHES_CLAIM_FILE = "queued-touches-claimed.jsonl";
4
+ export declare const QUEUED_TOUCHES_BORROWED_FILE = "queued-touches-borrowed-at";
5
+ export declare const MAX_QUEUED_TOUCHES = 200;
6
+ export declare function queuedTouchesPath(hqRoot: string, slug: string): string;
7
+ /**
8
+ * Record the files a queued checkpoint touched inside its company. Files
9
+ * outside `companies/<slug>/` are dropped. Returns the number of files
10
+ * recorded; 0 when the company is not a manifest company or nothing is inside
11
+ * it. The queue keeps its newest {@link MAX_QUEUED_TOUCHES} lines.
12
+ */
13
+ export declare function recordQueuedTouches(hqRoot: string, slug: string | null, files: string[], now: Date): number;
14
+ /**
15
+ * Record that a company-less run borrowed `slug`, so the next such run prefers
16
+ * a company that has waited longer.
17
+ */
18
+ export declare function markBorrowed(hqRoot: string, slug: string, now: Date): void;
19
+ /**
20
+ * The manifest company whose queued touches have waited longest, or null.
21
+ * The order key is the later of the first queued record and the last borrow,
22
+ * so a company a company-less run just borrowed goes to the back. A run whose
23
+ * own payload names no company uses it for its ontology step.
24
+ */
25
+ export declare function oldestQueuedCompany(hqRoot: string): string | null;
26
+ /**
27
+ * Take the company's queued touches by renaming the file into the run
28
+ * directory, then return the HQ-relative paths it lists. Renaming is atomic,
29
+ * so a touch recorded during the claim lands in a fresh file for a later run.
30
+ */
31
+ export declare function claimQueuedTouches(hqRoot: string, company: OntologyCompany, runDir: string): string[];
32
+ //# sourceMappingURL=queued-touches.d.ts.map
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Company files touched by checkpoints that were queued behind a busy sibling.
3
+ *
4
+ * `hq core checkpoint` queues its payload when a sibling is already running,
5
+ * and the next run claims the queue. That run renders the ontology step only
6
+ * for its own payload's company, and the ontology plan records any changed
7
+ * file the run's own payload did not touch as seen. Without this file, the
8
+ * files a queued company checkpoint touched never reach the ontology (CO-59).
9
+ *
10
+ * The checkpoint parent performs no network I/O, so it cannot read the
11
+ * `checkpoint.ontology.queued-touches` flag. It always records the touches in
12
+ * the company's local, unsynced state directory; the ontology plan consumes
13
+ * them only while the flag is on. The file is capped so it stays small while
14
+ * the flag is off.
15
+ */
16
+ import * as fs from "node:fs";
17
+ import * as path from "node:path";
18
+ import { assertSafeTarget, resolveOntologyCompany, safeWriteFile, WriteBoundsError, } from "./company.js";
19
+ export const QUEUED_TOUCHES_FILE = "queued-touches.jsonl";
20
+ export const QUEUED_TOUCHES_CLAIM_FILE = "queued-touches-claimed.jsonl";
21
+ export const QUEUED_TOUCHES_BORROWED_FILE = "queued-touches-borrowed-at";
22
+ export const MAX_QUEUED_TOUCHES = 200;
23
+ function ontologyStateBase(hqRoot) {
24
+ return path.join(hqRoot, "workspace", "checkpoints", "ontology");
25
+ }
26
+ function companyStateDir(hqRoot, slug) {
27
+ return path.join(ontologyStateBase(hqRoot), slug);
28
+ }
29
+ export function queuedTouchesPath(hqRoot, slug) {
30
+ return path.join(companyStateDir(hqRoot, slug), QUEUED_TOUCHES_FILE);
31
+ }
32
+ /**
33
+ * Record the files a queued checkpoint touched inside its company. Files
34
+ * outside `companies/<slug>/` are dropped. Returns the number of files
35
+ * recorded; 0 when the company is not a manifest company or nothing is inside
36
+ * it. The queue keeps its newest {@link MAX_QUEUED_TOUCHES} lines.
37
+ */
38
+ export function recordQueuedTouches(hqRoot, slug, files, now) {
39
+ if (!slug)
40
+ return 0;
41
+ const resolution = resolveOntologyCompany(hqRoot, slug);
42
+ if (!resolution.ok)
43
+ return 0;
44
+ const inside = files.filter((file) => {
45
+ if (typeof file !== "string" || !file.trim())
46
+ return false;
47
+ const rel = path.relative(resolution.company.dir, path.resolve(hqRoot, file.trim()));
48
+ return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
49
+ });
50
+ if (inside.length === 0)
51
+ return 0;
52
+ const target = queuedTouchesPath(hqRoot, slug);
53
+ try {
54
+ assertSafeTarget(hqRoot, [companyStateDir(hqRoot, slug)], target);
55
+ }
56
+ catch (error) {
57
+ if (error instanceof WriteBoundsError)
58
+ return 0;
59
+ throw error;
60
+ }
61
+ const fd = fs.openSync(target, fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_NOFOLLOW);
62
+ try {
63
+ fs.writeSync(fd, `${JSON.stringify({ queued_at: now.toISOString(), files: inside })}\n`);
64
+ }
65
+ finally {
66
+ fs.closeSync(fd);
67
+ }
68
+ const lines = fs.readFileSync(target, "utf8").split("\n").filter((line) => line.trim());
69
+ if (lines.length > MAX_QUEUED_TOUCHES) {
70
+ // Best-effort trim, as for the sibling's pending queue: it can race a
71
+ // concurrent append, but only once the file is already over its cap.
72
+ safeWriteFile(hqRoot, [companyStateDir(hqRoot, slug)], target, `${lines.slice(-MAX_QUEUED_TOUCHES).join("\n")}\n`);
73
+ }
74
+ return inside.length;
75
+ }
76
+ /**
77
+ * Record that a company-less run borrowed `slug`, so the next such run prefers
78
+ * a company that has waited longer.
79
+ */
80
+ export function markBorrowed(hqRoot, slug, now) {
81
+ const target = path.join(companyStateDir(hqRoot, slug), QUEUED_TOUCHES_BORROWED_FILE);
82
+ safeWriteFile(hqRoot, [companyStateDir(hqRoot, slug)], target, `${now.toISOString()}\n`);
83
+ }
84
+ /** `queued_at` of the oldest record, or the file mtime when no line has one. */
85
+ function firstQueuedMs(file, mtimeMs) {
86
+ for (const line of fs.readFileSync(file, "utf8").split("\n")) {
87
+ if (!line.trim())
88
+ continue;
89
+ try {
90
+ const parsed = JSON.parse(line);
91
+ if (typeof parsed.queued_at !== "string")
92
+ continue;
93
+ const ms = Date.parse(parsed.queued_at);
94
+ if (!Number.isNaN(ms))
95
+ return ms;
96
+ }
97
+ catch {
98
+ // A partial line is not a record; a later valid line still counts.
99
+ }
100
+ }
101
+ return mtimeMs;
102
+ }
103
+ /** When this company was last borrowed, or -Infinity when it never was. */
104
+ function borrowedMs(hqRoot, slug) {
105
+ const file = path.join(companyStateDir(hqRoot, slug), QUEUED_TOUCHES_BORROWED_FILE);
106
+ let stat;
107
+ try {
108
+ stat = fs.lstatSync(file);
109
+ }
110
+ catch {
111
+ return Number.NEGATIVE_INFINITY;
112
+ }
113
+ if (!stat.isFile())
114
+ return Number.NEGATIVE_INFINITY;
115
+ const ms = Date.parse(fs.readFileSync(file, "utf8").trim());
116
+ return Number.isNaN(ms) ? Number.NEGATIVE_INFINITY : ms;
117
+ }
118
+ /**
119
+ * The manifest company whose queued touches have waited longest, or null.
120
+ * The order key is the later of the first queued record and the last borrow,
121
+ * so a company a company-less run just borrowed goes to the back. A run whose
122
+ * own payload names no company uses it for its ontology step.
123
+ */
124
+ export function oldestQueuedCompany(hqRoot) {
125
+ let entries;
126
+ try {
127
+ entries = fs.readdirSync(ontologyStateBase(hqRoot));
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ let oldest = null;
133
+ for (const slug of entries.sort()) {
134
+ let stat;
135
+ try {
136
+ stat = fs.lstatSync(queuedTouchesPath(hqRoot, slug));
137
+ }
138
+ catch {
139
+ continue;
140
+ }
141
+ if (!stat.isFile() || stat.size === 0)
142
+ continue;
143
+ if (!resolveOntologyCompany(hqRoot, slug).ok)
144
+ continue;
145
+ const key = Math.max(firstQueuedMs(queuedTouchesPath(hqRoot, slug), stat.mtimeMs), borrowedMs(hqRoot, slug));
146
+ if (!oldest || key < oldest.key)
147
+ oldest = { slug, key };
148
+ }
149
+ return oldest?.slug ?? null;
150
+ }
151
+ /**
152
+ * Take the company's queued touches by renaming the file into the run
153
+ * directory, then return the HQ-relative paths it lists. Renaming is atomic,
154
+ * so a touch recorded during the claim lands in a fresh file for a later run.
155
+ */
156
+ export function claimQueuedTouches(hqRoot, company, runDir) {
157
+ const queuedPath = queuedTouchesPath(hqRoot, company.slug);
158
+ try {
159
+ assertSafeTarget(hqRoot, [companyStateDir(hqRoot, company.slug)], queuedPath);
160
+ }
161
+ catch (error) {
162
+ if (error instanceof WriteBoundsError)
163
+ return [];
164
+ throw error;
165
+ }
166
+ const claimPath = path.join(runDir, QUEUED_TOUCHES_CLAIM_FILE);
167
+ try {
168
+ fs.renameSync(queuedPath, claimPath);
169
+ }
170
+ catch (error) {
171
+ if (error?.code === "ENOENT")
172
+ return [];
173
+ throw error;
174
+ }
175
+ const files = [];
176
+ for (const line of fs.readFileSync(claimPath, "utf8").split("\n")) {
177
+ if (!line.trim())
178
+ continue;
179
+ try {
180
+ const parsed = JSON.parse(line);
181
+ if (Array.isArray(parsed.files)) {
182
+ for (const file of parsed.files)
183
+ if (typeof file === "string")
184
+ files.push(file);
185
+ }
186
+ }
187
+ catch {
188
+ // A partial line must not drop the valid lines around it.
189
+ }
190
+ }
191
+ return files;
192
+ }
193
+ //# sourceMappingURL=queued-touches.js.map
@@ -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
@@ -0,0 +1,7 @@
1
+ /** Native fast-route adapter for the shared timeout-guard command. */
2
+ import type { FlagReader } from "../flag-registry.js";
3
+ import { type PersistedFlagSnapshot } from "../flag-snapshot-file.js";
4
+ import { type TimeoutGuardDeps } from "./timeout-guard.js";
5
+ export declare function createNativeSnapshotFlagReader(persistedSnapshot: PersistedFlagSnapshot | null, companyIdentifiers: readonly string[], env: NodeJS.ProcessEnv): FlagReader;
6
+ export declare function runTimeoutGuardFast(_argv: readonly string[], stdin: string, _cwd: string, _hqRoot: string | undefined, dependencies?: TimeoutGuardDeps): Promise<number>;
7
+ //# sourceMappingURL=timeout-guard-native.d.ts.map
@@ -0,0 +1,40 @@
1
+ /** Native fast-route adapter for the shared timeout-guard command. */
2
+ import { readLocalOverride, resolveFlag, } from "@indigoai-us/hq-flags-client";
3
+ import { resolveProcessFlagCompanyContext } from "../process-flag-company-context.js";
4
+ import { readFlagSnapshotFile, } from "../flag-snapshot-file.js";
5
+ import { timeoutGuardCommand } from "./timeout-guard.js";
6
+ export function createNativeSnapshotFlagReader(persistedSnapshot, companyIdentifiers, env) {
7
+ const snapshot = persistedSnapshot
8
+ ? { version: 0, flags: persistedSnapshot.flags }
9
+ : null;
10
+ return {
11
+ isEnabled(flagKey, lookup = {}) {
12
+ if (snapshot) {
13
+ return resolveFlag({
14
+ flagKey,
15
+ env,
16
+ snapshot,
17
+ companyIdentifiers,
18
+ lookup,
19
+ }).value;
20
+ }
21
+ return readLocalOverride(flagKey, env) ?? (lookup.fallback ?? false);
22
+ },
23
+ };
24
+ }
25
+ function snapshotFlagReader(companyUid, companySlug, env) {
26
+ const persistedSnapshot = companyUid ? readFlagSnapshotFile(companyUid, { env }) : null;
27
+ const companyIdentifiers = [companyUid, companySlug].filter((value) => Boolean(value));
28
+ return createNativeSnapshotFlagReader(persistedSnapshot, companyIdentifiers, env);
29
+ }
30
+ export async function runTimeoutGuardFast(_argv, stdin, _cwd, _hqRoot, dependencies = {}) {
31
+ const env = dependencies.env ?? process.env;
32
+ const company = resolveProcessFlagCompanyContext(env);
33
+ const flagReader = dependencies.flagReader ?? snapshotFlagReader(company.companyUid, company.companySlug, env);
34
+ return timeoutGuardCommand(stdin, {
35
+ ...dependencies,
36
+ env,
37
+ flagReader,
38
+ });
39
+ }
40
+ //# sourceMappingURL=timeout-guard-native.js.map
@@ -20,7 +20,7 @@
20
20
  *
21
21
  * Exit codes: 0 = allow, 2 = block.
22
22
  */
23
- import { type FlagReader } from "../flag-registry.js";
23
+ import type { FlagReader } from "../flag-registry.js";
24
24
  /**
25
25
  * Worst-case inner deadline (seconds) declared in a command: a `timeout` /
26
26
  * `gtimeout` prefix (matched by executable BASENAME, so `/usr/bin/timeout`
@@ -55,11 +55,11 @@ export interface TimeoutGuardDeps {
55
55
  flagReader?: FlagReader;
56
56
  }
57
57
  /** Whether the guard is active for this identity (rollout gate). */
58
- export declare function isGatedUser(email: string | undefined, flagReader?: FlagReader): boolean;
58
+ export declare function isGatedUser(email: string | undefined, flagReader?: FlagReader): Promise<boolean>;
59
59
  /**
60
60
  * Command entry: read the PreToolUse JSON, apply the gate + short-circuits, and
61
61
  * return the exit code (0 allow / 2 block). Never throws — a parse failure or a
62
62
  * missing identity fails OPEN (allow), because a hook must not break tool calls.
63
63
  */
64
- export declare function timeoutGuardCommand(stdin: string, deps?: TimeoutGuardDeps): number;
64
+ export declare function timeoutGuardCommand(stdin: string, deps?: TimeoutGuardDeps): Promise<number>;
65
65
  //# sourceMappingURL=timeout-guard.d.ts.map