@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.
- package/CHANGELOG.md +99 -0
- package/assets/scaffold/core/scripts/hq-delegate-pickup.sh +99 -0
- package/assets/scaffold/core/scripts/hq-delegate-repo.sh +158 -0
- package/assets/scaffold/core/scripts/hq-delegate-resolve.sh +145 -0
- package/assets/scaffold/core/scripts/hq-delegate-secrets.sh +129 -0
- package/assets/scaffold/core/scripts/hq-delegate-transfer.sh +175 -0
- package/assets/scaffold/core/scripts/lib/secret-patterns.sh +58 -0
- package/dist/command-catalog.generated.d.ts +239 -1
- package/dist/command-catalog.generated.js +302 -1
- package/dist/commands/core-delegate.d.ts +6 -0
- package/dist/commands/core-delegate.js +987 -0
- package/dist/commands/core.js +10 -0
- package/dist/commands/daemon.d.ts +3 -0
- package/dist/commands/daemon.js +173 -1
- package/dist/commands/dm.d.ts +2 -0
- package/dist/commands/dm.js +11 -4
- package/dist/commands/lanes-accounts.js +44 -2
- package/dist/commands/lanes-message.js +69 -12
- package/dist/commands/lanes-resume.js +2 -0
- package/dist/commands/lanes-shared.d.ts +3 -0
- package/dist/commands/lanes-shared.js +12 -2
- package/dist/commands/lanes.d.ts +5 -1
- package/dist/commands/lanes.js +399 -47
- package/dist/commands/secrets.d.ts +16 -1
- package/dist/commands/secrets.js +77 -48
- package/dist/commands/sync-mode.d.ts +11 -0
- package/dist/commands/sync-mode.js +30 -0
- package/dist/fast-scaffold.d.ts +17 -0
- package/dist/fast-scaffold.js +91 -0
- package/dist/index.js +17 -1
- package/dist/lanes-drain-hook.js +154 -7
- package/dist/lib/agent-kit/run/inbox.js +3 -2
- package/dist/lib/checkpoint-ontology/apply.d.ts +6 -0
- package/dist/lib/checkpoint-ontology/apply.js +111 -3
- package/dist/lib/checkpoint-ontology/plan.d.ts +23 -0
- package/dist/lib/checkpoint-ontology/plan.js +23 -0
- package/dist/lib/checkpoint-ontology/tree.d.ts +2 -0
- package/dist/lib/checkpoint-ontology/tree.js +34 -0
- package/dist/lib/core-utils/jq-json-serializer.d.ts +5 -0
- package/dist/lib/core-utils/jq-json-serializer.js +10 -0
- package/dist/lib/core-utils/secret-patterns.d.ts +41 -0
- package/dist/lib/core-utils/secret-patterns.js +33 -0
- package/dist/lib/daemon/catalog.d.ts +18 -2
- package/dist/lib/daemon/catalog.js +18 -2
- package/dist/lib/daemon/config.d.ts +6 -0
- package/dist/lib/daemon/config.js +15 -2
- package/dist/lib/daemon/requests.d.ts +4 -0
- package/dist/lib/daemon/requests.js +2 -0
- package/dist/lib/daemon/run.js +94 -8
- package/dist/lib/daemon/supervisor.d.ts +8 -0
- package/dist/lib/daemon/supervisor.js +101 -1
- package/dist/lib/daemon/sync-runner.d.ts +58 -0
- package/dist/lib/daemon/sync-runner.js +277 -0
- package/dist/lib/doctor/__testing__/run-doctor.d.ts +8 -0
- package/dist/lib/doctor/__testing__/run-doctor.js +52 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +46 -8
- package/dist/lib/lanes/config.d.ts +2 -1
- package/dist/lib/lanes/config.js +31 -0
- package/dist/lib/lanes/hire-grant-dispatch.d.ts +2 -0
- package/dist/lib/lanes/hire-grant-dispatch.js +41 -0
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.d.ts +3 -0
- package/dist/lib/lanes/keep-alive.js +3 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +4 -0
- package/dist/lib/lanes/outpost-dispatch.js +11 -2
- package/dist/lib/lanes/reconcile.d.ts +9 -0
- package/dist/lib/lanes/reconcile.js +127 -2
- package/dist/lib/lanes/runaway-tool-guard.d.ts +38 -0
- package/dist/lib/lanes/runaway-tool-guard.js +284 -0
- package/dist/lib/lanes/spawn.js +17 -10
- package/dist/lib/lanes/store.js +45 -1
- package/dist/lib/lanes/types.d.ts +37 -1
- package/dist/lib/lanes/types.js +2 -2
- package/dist/lib/lanes/watcher.d.ts +2 -0
- package/dist/lib/lanes/watcher.js +13 -2
- package/dist/secrets-env-fast-path.d.ts +5 -0
- package/dist/secrets-env-fast-path.js +16 -0
- package/dist/secrets-env-fast-route.d.ts +18 -0
- package/dist/secrets-env-fast-route.js +54 -0
- 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(
|
|
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({
|
|
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.
|
|
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 = {
|
|
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
|