@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.
- package/CHANGELOG.md +112 -0
- package/README.md +12 -0
- package/assets/bot-workers/setup/context/USER-GUIDE.md +4 -4
- 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-checkpoint.d.ts +4 -2
- package/dist/commands/core-checkpoint.js +34 -3
- package/dist/commands/core-command-catalog.d.ts +4 -2
- package/dist/commands/core-command-catalog.js +5 -2
- package/dist/commands/core-delegate.d.ts +6 -0
- package/dist/commands/core-delegate.js +987 -0
- package/dist/commands/core-ontology.js +3 -3
- package/dist/commands/core.js +11 -1
- package/dist/commands/daemon.d.ts +4 -0
- package/dist/commands/daemon.js +175 -2
- package/dist/commands/dm.d.ts +2 -0
- package/dist/commands/dm.js +11 -4
- package/dist/commands/feedback.d.ts +6 -0
- package/dist/commands/feedback.js +86 -37
- 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 +402 -49
- package/dist/commands/run-feature-telemetry.d.ts +3 -0
- package/dist/commands/run-feature-telemetry.js +6 -0
- package/dist/commands/run.js +3 -2
- package/dist/commands/secrets.d.ts +16 -1
- package/dist/commands/secrets.js +80 -45
- package/dist/commands/sync-mode.d.ts +11 -0
- package/dist/commands/sync-mode.js +30 -0
- package/dist/commands/whoami.d.ts +4 -6
- package/dist/commands/whoami.js +39 -8
- package/dist/fast-core.d.ts +6 -7
- package/dist/fast-core.js +22 -8
- package/dist/fast-scaffold.d.ts +33 -7
- package/dist/fast-scaffold.js +104 -8
- package/dist/index.js +32 -1
- package/dist/lanes-drain-hook.js +154 -7
- package/dist/lib/agent-kit/run/inbox.js +3 -2
- package/dist/lib/billing/plan-lock.d.ts +3 -1
- package/dist/lib/billing/plan-lock.js +7 -1
- package/dist/lib/checkpoint-ontology/apply.d.ts +6 -0
- package/dist/lib/checkpoint-ontology/apply.js +60 -3
- package/dist/lib/checkpoint-ontology/company.d.ts +8 -4
- package/dist/lib/checkpoint-ontology/company.js +15 -7
- package/dist/lib/checkpoint-ontology/plan.d.ts +36 -0
- package/dist/lib/checkpoint-ontology/plan.js +66 -4
- package/dist/lib/checkpoint-ontology/queued-touches.d.ts +32 -0
- package/dist/lib/checkpoint-ontology/queued-touches.js +193 -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/core-utils/timeout-guard-native.d.ts +7 -0
- package/dist/lib/core-utils/timeout-guard-native.js +40 -0
- package/dist/lib/core-utils/timeout-guard.d.ts +3 -3
- package/dist/lib/core-utils/timeout-guard.js +28 -13
- 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/flag-registry.d.ts +12 -7
- package/dist/lib/flag-registry.js +66 -75
- package/dist/lib/flag-snapshot-file.d.ts +20 -0
- package/dist/lib/flag-snapshot-file.js +91 -0
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +46 -8
- package/dist/lib/lanes/config.js +15 -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.js +20 -4
- package/dist/lib/lanes/spawn.js +17 -10
- package/dist/lib/lanes/store.js +45 -1
- package/dist/lib/lanes/types.d.ts +30 -1
- package/dist/lib/lanes/types.js +2 -2
- package/dist/lib/lanes/watcher.js +3 -2
- package/dist/lib/process-flag-company-context.d.ts +8 -0
- package/dist/lib/process-flag-company-context.js +67 -0
- package/dist/main.js +3 -1
- package/dist/native-fast-route.d.ts +3 -0
- package/dist/native-fast-route.js +39 -0
- 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/dist/utils/cli-telemetry.d.ts +26 -2
- package/dist/utils/cli-telemetry.js +178 -3
- package/dist/utils/hq-root-resolver.d.ts +13 -0
- package/dist/utils/hq-root-resolver.js +103 -0
- package/dist/utils/scaffold-fast-import-guard.js +6 -1
- package/dist/utils/vault-api.d.ts +8 -0
- package/dist/utils/vault-api.js +13 -1
- 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
|
|
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
|
|
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 {
|
|
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
|