@indigoai-us/hq-cli 5.345.198 → 5.345.199
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 +45 -0
- package/dist/command-catalog.generated.d.ts +106 -6
- package/dist/command-catalog.generated.js +130 -6
- package/dist/commands/bot-events.d.ts +19 -0
- package/dist/commands/bot-events.js +46 -0
- package/dist/commands/bot-jobs.d.ts +2 -0
- package/dist/commands/bot-jobs.js +43 -2
- package/dist/commands/bot.d.ts +5 -0
- package/dist/commands/bot.js +33 -1
- package/dist/commands/lanes-report.d.ts +2 -1
- package/dist/commands/lanes-report.js +44 -1
- package/dist/commands/lanes.d.ts +9 -0
- package/dist/commands/lanes.js +95 -9
- package/dist/commands/meetings.js +1 -21
- package/dist/commands/signals-run.d.ts +2 -0
- package/dist/commands/signals-run.js +6 -1
- package/dist/lanes-drain-hook.js +9 -0
- package/dist/lib/bot/api.d.ts +5 -0
- package/dist/lib/bot/claude-login.d.ts +31 -0
- package/dist/lib/bot/claude-login.js +160 -0
- package/dist/lib/bot/events.d.ts +18 -0
- package/dist/lib/bot/events.js +160 -0
- package/dist/lib/bot/executive-settings.d.ts +10 -0
- package/dist/lib/bot/executive-settings.js +43 -0
- package/dist/lib/bot/fleet-cron.js +4 -0
- package/dist/lib/bot/fleet-local.js +10 -10
- package/dist/lib/bot/job-run.js +1 -1
- package/dist/lib/bot/jobs/hot-signal-sync.d.ts +8 -0
- package/dist/lib/bot/jobs/hot-signal-sync.js +2 -0
- package/dist/lib/bot/jobs/meeting-detect.d.ts +28 -0
- package/dist/lib/bot/jobs/meeting-detect.js +176 -0
- package/dist/lib/bot/jobs/meeting-rider.d.ts +53 -0
- package/dist/lib/bot/jobs/meeting-rider.js +284 -0
- package/dist/lib/bot/jobs/signal-to-lane.d.ts +32 -0
- package/dist/lib/bot/jobs/signal-to-lane.js +169 -0
- package/dist/lib/bot/local-jobs.d.ts +15 -2
- package/dist/lib/bot/local-jobs.js +24 -7
- package/dist/lib/bot/run.d.ts +7 -49
- package/dist/lib/bot/run.js +100 -4
- package/dist/lib/bot/status.d.ts +21 -0
- package/dist/lib/doctor/checks/bots.d.ts +6 -0
- package/dist/lib/doctor/checks/bots.js +47 -1
- package/dist/lib/hot-channels/adapters/meetings.d.ts +1 -0
- package/dist/lib/hot-channels/adapters/meetings.js +3 -1
- package/dist/lib/hot-channels/meeting-source-ref.d.ts +3 -0
- package/dist/lib/hot-channels/meeting-source-ref.js +7 -0
- package/dist/lib/hot-channels/schedule.js +2 -2
- package/dist/lib/hot-channels/signals-pass.d.ts +9 -32
- package/dist/lib/hot-channels/signals-pass.js +51 -17
- package/dist/lib/lanes/admission.d.ts +2 -0
- package/dist/lib/lanes/admission.js +4 -0
- package/dist/lib/lanes/approval-gate.js +4 -1
- package/dist/lib/lanes/links.d.ts +8 -1
- package/dist/lib/lanes/links.js +84 -3
- package/dist/lib/lanes/outcome.d.ts +154 -0
- package/dist/lib/lanes/outcome.js +151 -0
- package/dist/lib/lanes/reconcile.js +1 -0
- package/dist/lib/lanes/signal-brief-template.d.ts +7 -0
- package/dist/lib/lanes/signal-brief-template.js +40 -0
- package/dist/lib/lanes/signal-lane.d.ts +24 -0
- package/dist/lib/lanes/signal-lane.js +99 -0
- package/dist/lib/lanes/source-report.d.ts +37 -0
- package/dist/lib/lanes/source-report.js +66 -0
- package/dist/lib/lanes/spawn.d.ts +6 -0
- package/dist/lib/lanes/spawn.js +21 -0
- package/dist/lib/lanes/store.d.ts +3 -0
- package/dist/lib/lanes/store.js +64 -0
- package/dist/lib/lanes/story-ledger.d.ts +3 -0
- package/dist/lib/lanes/story-ledger.js +1 -0
- package/dist/lib/lanes/types.d.ts +137 -0
- package/dist/lib/lanes/types.js +17 -2
- package/dist/lib/lanes/watcher.js +4 -0
- package/dist/lib/live-meetings.d.ts +19 -0
- package/dist/lib/live-meetings.js +23 -0
- package/dist/lib/mesh/live/daemon/doctor.js +6 -7
- package/dist/lib/mesh/live/spool.d.ts +8 -0
- package/dist/lib/mesh/live/spool.js +59 -40
- package/dist/lib/plan-limit-nag.d.ts +30 -4
- package/dist/lib/plan-limit-nag.js +68 -17
- package/dist/lib/signals/engine.d.ts +4 -0
- package/dist/lib/signals/engine.js +25 -5
- package/dist/lib/signals/incremental.d.ts +42 -0
- package/dist/lib/signals/incremental.js +191 -0
- package/dist/lib/signals/prior-signals.d.ts +11 -0
- package/dist/lib/signals/prior-signals.js +44 -0
- package/dist/lib/signals/reply.js +6 -1
- package/dist/lib/signals/settings.d.ts +5 -0
- package/dist/lib/signals/settings.js +17 -0
- package/dist/lib/signals/steps/extract.d.ts +23 -2
- package/dist/lib/signals/steps/extract.js +142 -9
- package/dist/lib/signals/steps/types.d.ts +1 -0
- package/dist/lib/signals/write-guard.js +7 -0
- package/dist/utils/cognito-callback-port.d.ts +15 -0
- package/dist/utils/cognito-callback-port.js +51 -0
- package/dist/utils/cognito-config.js +2 -3
- package/dist/utils/cognito-session.d.ts +7 -6
- package/dist/utils/cognito-session.js +14 -14
- package/dist/utils/interactive-caller.d.ts +15 -0
- package/dist/utils/interactive-caller.js +70 -0
- package/dist/utils/login-browser.js +3 -1
- package/dist/utils/resident-package-preload.js +4 -0
- package/package.json +7 -7
|
@@ -63,6 +63,52 @@ export const PLAN_LIMIT_DIMENSION_LABELS = {
|
|
|
63
63
|
deployments: "deployments",
|
|
64
64
|
storageBytes: "storage",
|
|
65
65
|
};
|
|
66
|
+
/**
|
|
67
|
+
* Bytes in one GB as plan limits count them: 2^30. hq-pro stores the "10 GB"
|
|
68
|
+
* and "2 GB" storage ceilings as GiB and its limit emails print them as
|
|
69
|
+
* "10 GB" / "2 GB"; `lib/billing/usage-cost.ts` uses the same unit
|
|
70
|
+
* (BYTES_PER_GB). Kept local so this hot-path module loads no billing code.
|
|
71
|
+
*/
|
|
72
|
+
const STORAGE_BYTES_PER_GB = 1024 ** 3;
|
|
73
|
+
/**
|
|
74
|
+
* Render one amount of a resource. Storage arrives in bytes; printed raw it
|
|
75
|
+
* reads as "47012700666 of 10737418240". It is shown in GB with one decimal,
|
|
76
|
+
* so a 10 GB ceiling reads "10.0 GB", matching the plan's own wording.
|
|
77
|
+
*/
|
|
78
|
+
export function formatPlanLimitAmount(key, n) {
|
|
79
|
+
if (key !== "storageBytes")
|
|
80
|
+
return String(n);
|
|
81
|
+
return `${(n / STORAGE_BYTES_PER_GB).toFixed(1)} GB`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The account as a noun phrase: "your personal account", "the acme company",
|
|
85
|
+
* or "this account" when an older hq-pro sent no label.
|
|
86
|
+
*/
|
|
87
|
+
export function planLimitAccountPhrase(account) {
|
|
88
|
+
if (account?.kind === "person")
|
|
89
|
+
return "your personal account";
|
|
90
|
+
if (account?.kind === "company" && account.slug)
|
|
91
|
+
return `the ${account.slug} company`;
|
|
92
|
+
return "this account";
|
|
93
|
+
}
|
|
94
|
+
function parseAccount(body) {
|
|
95
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const raw = body.account;
|
|
99
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
100
|
+
return null;
|
|
101
|
+
const rec = raw;
|
|
102
|
+
if (rec.kind === "person")
|
|
103
|
+
return { kind: "person" };
|
|
104
|
+
if (rec.kind === "company") {
|
|
105
|
+
const slug = typeof rec.slug === "string" && rec.slug.trim().length > 0
|
|
106
|
+
? rec.slug.trim()
|
|
107
|
+
: null;
|
|
108
|
+
return { kind: "company", slug };
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
66
112
|
function dimensionSuffix(key) {
|
|
67
113
|
const label = PLAN_LIMIT_DIMENSION_LABELS[key];
|
|
68
114
|
return label && label !== key ? ` (${label})` : "";
|
|
@@ -252,14 +298,15 @@ export function recordPlanLimitStatus(body, opts = {}) {
|
|
|
252
298
|
legacyUrl: status?.upgradeUrl ?? PLAN_LIMIT_UPGRADE_URL,
|
|
253
299
|
});
|
|
254
300
|
}
|
|
301
|
+
const account = parseAccount(body);
|
|
255
302
|
if (status !== null) {
|
|
256
303
|
const anyOver = Object.values(status.limits).some((e) => e.over);
|
|
257
|
-
lastSeen = { ...status, anyOver, upgradeUrl: resolvedUpgradeUrl };
|
|
304
|
+
lastSeen = { ...status, anyOver, upgradeUrl: resolvedUpgradeUrl, account };
|
|
258
305
|
}
|
|
259
306
|
// Independent of `planLimits`: the server can announce a crossing on a
|
|
260
307
|
// response whose nag map this CLI could not parse, and the notice is the
|
|
261
308
|
// one surface that must land inside a session that is already running.
|
|
262
|
-
emitPlanLimitThresholdNotice(crossings, resolvedUpgradeUrl, opts);
|
|
309
|
+
emitPlanLimitThresholdNotice(crossings, resolvedUpgradeUrl, { ...opts, account });
|
|
263
310
|
}
|
|
264
311
|
catch {
|
|
265
312
|
// Never throw from record path.
|
|
@@ -287,7 +334,7 @@ function formatPct(entry) {
|
|
|
287
334
|
return `${Math.round((entry.used / entry.limit) * 100)}%`;
|
|
288
335
|
}
|
|
289
336
|
function formatEntryLine(key, entry) {
|
|
290
|
-
return `${key} at ${entry.used}/${entry.limit} (${formatPct(entry)})${dimensionSuffix(key)}`;
|
|
337
|
+
return `${key} at ${formatPlanLimitAmount(key, entry.used)}/${formatPlanLimitAmount(key, entry.limit)} (${formatPct(entry)})${dimensionSuffix(key)}`;
|
|
291
338
|
}
|
|
292
339
|
/**
|
|
293
340
|
* Read the whole state file. A missing, unreadable or malformed file is an
|
|
@@ -360,16 +407,18 @@ function withinNagWindow(shownAt, nowMs) {
|
|
|
360
407
|
/**
|
|
361
408
|
* The over-limit per-turn line (US-036 §2.2). One line, not the old box: it
|
|
362
409
|
* now repeats up to four times a day, and a multi-line box at that cadence
|
|
363
|
-
* reads as breakage rather than as a nag. It
|
|
364
|
-
*
|
|
410
|
+
* reads as breakage rather than as a nag. It names the account and states the
|
|
411
|
+
* facts. It claims no pause: being over is not being refused. A refusal comes
|
|
412
|
+
* back from hq-pro as its own error with its own sentence, and that is the
|
|
413
|
+
* only place a pause is reported (feedback_b1867fe6, where this line told a
|
|
414
|
+
* person their uploads were paused right after two uploads succeeded).
|
|
365
415
|
*/
|
|
366
|
-
export function buildOverLine(overEntries, upgradeUrl) {
|
|
416
|
+
export function buildOverLine(overEntries, upgradeUrl, account = null) {
|
|
367
417
|
const facts = overEntries
|
|
368
|
-
.map(([key, entry]) => `${PLAN_LIMIT_DIMENSION_LABELS[key] ?? key} ${entry.used} of ${entry.limit}`)
|
|
418
|
+
.map(([key, entry]) => `${PLAN_LIMIT_DIMENSION_LABELS[key] ?? key} ${formatPlanLimitAmount(key, entry.used)} of ${formatPlanLimitAmount(key, entry.limit)}`)
|
|
369
419
|
.join(", ");
|
|
370
420
|
// US-024: names no plan and offers no upgrade; the link is the Billing page.
|
|
371
|
-
return
|
|
372
|
-
`Billing: ${upgradeUrl}`);
|
|
421
|
+
return `⚠ Over ${planLimitAccountPhrase(account)}'s limits: ${facts}. Billing: ${upgradeUrl}`;
|
|
373
422
|
}
|
|
374
423
|
// ---------------------------------------------------------------------------
|
|
375
424
|
// Personal feature notice (US-054, copy per US-024)
|
|
@@ -424,12 +473,14 @@ function dimensionName(resource) {
|
|
|
424
473
|
* their own story, and a notice that announces a pause nobody is experiencing
|
|
425
474
|
* reads as a bug.
|
|
426
475
|
*/
|
|
427
|
-
export function renderThresholdNotice(crossing, upgradeUrl) {
|
|
476
|
+
export function renderThresholdNotice(crossing, upgradeUrl, account = null) {
|
|
428
477
|
const name = dimensionName(crossing.resource);
|
|
429
|
-
const counts = `${crossing.used} of ${crossing.limit}`;
|
|
478
|
+
const counts = `${formatPlanLimitAmount(crossing.resource, crossing.used)} of ${formatPlanLimitAmount(crossing.resource, crossing.limit)}`;
|
|
479
|
+
// An unlabelled payload (older hq-pro) keeps the original wording.
|
|
480
|
+
const where = account ? ` for ${planLimitAccountPhrase(account)}` : "";
|
|
430
481
|
const headline = crossing.band === "over"
|
|
431
|
-
? `You are past your ${name} limit (${counts}).`
|
|
432
|
-
: `You have reached 80% of your ${name} limit (${counts}).`;
|
|
482
|
+
? `You are past your ${name} limit${where} (${counts}).`
|
|
483
|
+
: `You have reached 80% of your ${name} limit${where} (${counts}).`;
|
|
433
484
|
return `${headline} Billing: ${upgradeUrl}`;
|
|
434
485
|
}
|
|
435
486
|
/**
|
|
@@ -461,7 +512,7 @@ export function emitPlanLimitThresholdNotice(crossings, upgradeUrl, opts = {}) {
|
|
|
461
512
|
continue;
|
|
462
513
|
thresholdsShownThisSession.set(key, crossing.firstSeenAt);
|
|
463
514
|
printed[key] = crossing.firstSeenAt;
|
|
464
|
-
write(chalk.yellow(renderThresholdNotice(crossing, resolvedUpgradeUrl)) + "\n");
|
|
515
|
+
write(chalk.yellow(renderThresholdNotice(crossing, resolvedUpgradeUrl, opts.account ?? null)) + "\n");
|
|
465
516
|
if (isCliLimitResourceKind(crossing.resource)) {
|
|
466
517
|
queueCliLimitNoticeExposure(crossing.resource, "advisory");
|
|
467
518
|
}
|
|
@@ -489,7 +540,7 @@ export function emitPlanLimitNag(opts = {}) {
|
|
|
489
540
|
const write = opts.write ?? ((s) => process.stderr.write(s));
|
|
490
541
|
const now = opts.now ?? (() => new Date());
|
|
491
542
|
const statePath = opts.statePath ?? defaultStatePath();
|
|
492
|
-
const { limits, anyOver, upgradeUrl } = lastSeen;
|
|
543
|
+
const { limits, anyOver, upgradeUrl, account } = lastSeen;
|
|
493
544
|
const resolvedUpgradeUrl = upgradeUrl ??
|
|
494
545
|
planLimitUpgradeFallbackUrl(resolveProcessFlagCompanyContext().companySlug, PLAN_LIMIT_UPGRADE_URL);
|
|
495
546
|
const entries = Object.entries(limits);
|
|
@@ -504,7 +555,7 @@ export function emitPlanLimitNag(opts = {}) {
|
|
|
504
555
|
return;
|
|
505
556
|
overShownThisSession = true;
|
|
506
557
|
const overEntries = entries.filter(([, e]) => e.over);
|
|
507
|
-
const line = buildOverLine(overEntries, resolvedUpgradeUrl);
|
|
558
|
+
const line = buildOverLine(overEntries, resolvedUpgradeUrl, account);
|
|
508
559
|
write(chalk.yellow(line) + "\n");
|
|
509
560
|
const resourceKind = overEntries[0]?.[0];
|
|
510
561
|
if (resourceKind && isCliLimitResourceKind(resourceKind)) {
|
|
@@ -520,7 +571,7 @@ export function emitPlanLimitNag(opts = {}) {
|
|
|
520
571
|
if (worst === null)
|
|
521
572
|
return;
|
|
522
573
|
warningShownThisSession = true;
|
|
523
|
-
const line = `⚠ Nearing
|
|
574
|
+
const line = `⚠ Nearing ${planLimitAccountPhrase(account)}'s limits: ${formatEntryLine(worst.key, worst.entry)}. Make room before you hit the cap. Billing: ${resolvedUpgradeUrl}`;
|
|
524
575
|
write(chalk.yellow(line) + "\n");
|
|
525
576
|
if (isCliLimitResourceKind(worst.key)) {
|
|
526
577
|
queueCliLimitNoticeExposure(worst.key, "advisory");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type IncrementalOptions } from "./incremental.js";
|
|
1
2
|
import { localDate } from "./local-date.js";
|
|
2
3
|
import { type LockTakeover } from "./run-lock.js";
|
|
3
4
|
import { type UploadDeps } from "./steps/upload.js";
|
|
@@ -130,6 +131,9 @@ export interface RunSignalsResult {
|
|
|
130
131
|
alerts?: Record<string, RaiseSignalsFailureAlertsResult>;
|
|
131
132
|
}
|
|
132
133
|
export interface RunSignalsOptions {
|
|
134
|
+
source?: string;
|
|
135
|
+
incremental?: boolean;
|
|
136
|
+
incrementalOptions?: IncrementalOptions;
|
|
133
137
|
hqRoot: string;
|
|
134
138
|
company?: string;
|
|
135
139
|
dryRun?: boolean;
|
|
@@ -12,7 +12,8 @@ import { spawnSync } from "node:child_process";
|
|
|
12
12
|
import * as fs from "node:fs";
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import yaml from "js-yaml";
|
|
15
|
-
import { loadSignalsSettings } from "./settings.js";
|
|
15
|
+
import { loadSignalsSettings, loadIncrementalSettings } from "./settings.js";
|
|
16
|
+
import { createIncrementalStep } from "./incremental.js";
|
|
16
17
|
import { localDate } from "./local-date.js";
|
|
17
18
|
import { guardedWrite, WriteGuardError } from "./write-guard.js";
|
|
18
19
|
import { acquireCompanyLock, withSummaryLock } from "./run-lock.js";
|
|
@@ -46,6 +47,7 @@ export const defaultSpawn = (cmd, args, opts) => {
|
|
|
46
47
|
cwd: opts.cwd,
|
|
47
48
|
env: opts.env ?? process.env,
|
|
48
49
|
encoding: "utf8",
|
|
50
|
+
input: opts.input,
|
|
49
51
|
maxBuffer: 64 * 1024 * 1024,
|
|
50
52
|
...(opts.timeoutMs !== undefined ? { timeout: opts.timeoutMs, killSignal: "SIGKILL" } : {}),
|
|
51
53
|
});
|
|
@@ -246,9 +248,22 @@ export async function runSignals(opts) {
|
|
|
246
248
|
const stepChoice = opts.step ?? "all";
|
|
247
249
|
const dryRun = opts.dryRun === true;
|
|
248
250
|
const settings = loadSignalsSettings(hqRoot);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
+
if (opts.source !== undefined || opts.incremental) {
|
|
252
|
+
if (!opts.source || !opts.incremental || !opts.company)
|
|
253
|
+
throw new SignalsUsageError("--source and --incremental require each other and --company");
|
|
254
|
+
if ((opts.step && opts.step !== "all" && opts.step !== "extract") || opts.redo?.length)
|
|
255
|
+
throw new SignalsUsageError("--incremental cannot be combined with --redo or another step");
|
|
256
|
+
if (!/^[a-zA-Z0-9_-]+:[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(opts.source))
|
|
257
|
+
throw new SignalsUsageError("invalid --source: expected <channel>:<file>");
|
|
258
|
+
}
|
|
259
|
+
const limits = opts.incremental ? loadIncrementalSettings(hqRoot) : settings;
|
|
260
|
+
const maxTurns = opts.maxTurns ?? limits.maxTurns;
|
|
261
|
+
const maxBudgetUsd = opts.maxBudgetUsd ?? limits.maxBudgetUsd;
|
|
262
|
+
if (!Number.isInteger(maxTurns) || maxTurns < 1 || !Number.isFinite(maxBudgetUsd) || maxBudgetUsd < 0)
|
|
263
|
+
throw new SignalsUsageError("invalid model cost limits");
|
|
251
264
|
const stepImpls = { ...DEFAULT_STEPS, ...(opts.steps ?? {}) };
|
|
265
|
+
if (opts.incremental)
|
|
266
|
+
stepImpls.extract = createIncrementalStep(opts.source, opts.incrementalOptions);
|
|
252
267
|
if (opts.company !== undefined && !SLUG_RE.test(opts.company)) {
|
|
253
268
|
throw new SignalsUsageError(`invalid company slug: ${opts.company}`);
|
|
254
269
|
}
|
|
@@ -266,12 +281,14 @@ export async function runSignals(opts) {
|
|
|
266
281
|
const companies = opts.company !== undefined ? [opts.company] : listCompanies(hqRoot);
|
|
267
282
|
const start = now();
|
|
268
283
|
const date = localDate(start);
|
|
269
|
-
const lateStart = trigger === "job" && start.getHours() >= settings.lateStartHour;
|
|
284
|
+
const lateStart = !opts.incremental && trigger === "job" && start.getHours() >= settings.lateStartHour;
|
|
270
285
|
let plan = stepChoice === "all" ? STEP_ORDER : STEP_ORDER.filter((s) => s === stepChoice);
|
|
271
286
|
if (stepChoice !== "all")
|
|
272
287
|
plan = plan.slice(0, 1);
|
|
273
288
|
if (lateStart)
|
|
274
289
|
plan = plan.filter((s) => s === "garden").slice(0, 1);
|
|
290
|
+
if (opts.incremental)
|
|
291
|
+
plan = ["extract"];
|
|
275
292
|
const records = [];
|
|
276
293
|
const locked = [];
|
|
277
294
|
for (const company of companies) {
|
|
@@ -324,7 +341,10 @@ export async function runSignals(opts) {
|
|
|
324
341
|
};
|
|
325
342
|
const finish = () => {
|
|
326
343
|
const decidedSkip = record.skipReason;
|
|
344
|
+
const previousJobRanAt = record.jobRanAt;
|
|
327
345
|
finalizeRecord(record, { lateStart, lateStartHour: settings.lateStartHour, finishedAt: now() });
|
|
346
|
+
if (opts.incremental)
|
|
347
|
+
record.jobRanAt = previousJobRanAt;
|
|
328
348
|
if (dryRun) {
|
|
329
349
|
if (!decidedSkip) {
|
|
330
350
|
record.status = "dry-run";
|
|
@@ -343,7 +363,7 @@ export async function runSignals(opts) {
|
|
|
343
363
|
// 2. The day's job already ran: do nothing and leave the record as it is.
|
|
344
364
|
const prior = readJson(hqRoot, rel);
|
|
345
365
|
const priorJobRanAt = typeof prior?.jobRanAt === "string" ? prior.jobRanAt : null;
|
|
346
|
-
if (trigger === "job" && !opts.force && prior && priorJobRanAt) {
|
|
366
|
+
if (!opts.incremental && trigger === "job" && !opts.force && prior && priorJobRanAt) {
|
|
347
367
|
process.stderr.write(`hq signals: ${company}: already-ran (the job ran at ${priorJobRanAt})\n`);
|
|
348
368
|
records.push({ ...prior, alreadyRanToday: true });
|
|
349
369
|
continue;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type PriorSignal } from "./steps/extract.js";
|
|
2
|
+
import type { SignalsStep, StepContext } from "./steps/types.js";
|
|
3
|
+
export { meetingSignalSourceRef } from "../hot-channels/meeting-source-ref.js";
|
|
4
|
+
export interface IncrementalCursor {
|
|
5
|
+
revision: number;
|
|
6
|
+
segment_count: number;
|
|
7
|
+
last_run_at: string;
|
|
8
|
+
signal_ids: string[];
|
|
9
|
+
digest: string;
|
|
10
|
+
pending_events?: SignalWritten[];
|
|
11
|
+
}
|
|
12
|
+
export interface SourceSnapshot {
|
|
13
|
+
sourceRef: string;
|
|
14
|
+
text: string;
|
|
15
|
+
segmentCount: number;
|
|
16
|
+
revision?: number;
|
|
17
|
+
audience: string[];
|
|
18
|
+
}
|
|
19
|
+
export type SourceReader = (ctx: StepContext, id: string) => Promise<SourceSnapshot>;
|
|
20
|
+
export type SignalWritten = {
|
|
21
|
+
event: "signal.written";
|
|
22
|
+
signal_id: string;
|
|
23
|
+
type: string;
|
|
24
|
+
source_ref: string;
|
|
25
|
+
company: string;
|
|
26
|
+
key: string;
|
|
27
|
+
};
|
|
28
|
+
export interface IncrementalOptions {
|
|
29
|
+
readers?: Readonly<Record<string, SourceReader>>;
|
|
30
|
+
/** US-004/005 integration seam: one callback per newly persisted signal. */
|
|
31
|
+
onSignalWritten?: (event: SignalWritten) => Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
export declare const SOURCE_READERS: Readonly<Record<string, SourceReader>>;
|
|
34
|
+
export declare function readIncrementalSource(ctx: StepContext, ref: string, readers?: Readonly<Record<string, SourceReader>>): Promise<SourceSnapshot>;
|
|
35
|
+
export declare function cursorPath(company: string, sourceRef: string): string;
|
|
36
|
+
/** Only read the current audience partition, never another audience's signals. */
|
|
37
|
+
export declare function priorSignalsFor(ctx: StepContext, sourceRef: string, audience: string[]): Array<PriorSignal & {
|
|
38
|
+
key: string;
|
|
39
|
+
live: boolean;
|
|
40
|
+
}>;
|
|
41
|
+
export declare function createIncrementalStep(source: string, options?: IncrementalOptions): SignalsStep;
|
|
42
|
+
//# sourceMappingURL=incremental.d.ts.map
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/** Incremental source readers and cursor transaction, executed under the engine's company lock. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import yaml from "js-yaml";
|
|
6
|
+
import { ensureCognitoToken } from "../../utils/cognito-session.js";
|
|
7
|
+
import { getCompanyUid, resolveCallerPersonUid, vaultApiFetch } from "../../utils/vault-api.js";
|
|
8
|
+
import { audienceKey } from "../hot-channels/audience.js";
|
|
9
|
+
import { assertSafeSegment } from "../hot-channels/landing.js";
|
|
10
|
+
import { appendSignalIndex, signalId, signalTypeDir, splitFrontmatter } from "../hot-channels/signals-pass.js";
|
|
11
|
+
import { acquireSyncLock } from "../hot-channels/sync-lock.js";
|
|
12
|
+
import { assertAllowedPath } from "./write-guard.js";
|
|
13
|
+
import { createExtractStep } from "./steps/extract.js";
|
|
14
|
+
import { meetingSignalSourceRef } from "../hot-channels/meeting-source-ref.js";
|
|
15
|
+
export { meetingSignalSourceRef } from "../hot-channels/meeting-source-ref.js";
|
|
16
|
+
/** Stable across partial revisions and the adapter's landed transcript. */
|
|
17
|
+
const meetingReader = async (ctx, id) => {
|
|
18
|
+
const sourceRef = meetingSignalSourceRef(id);
|
|
19
|
+
const token = await ensureCognitoToken();
|
|
20
|
+
const companyId = await getCompanyUid(token, ctx.company);
|
|
21
|
+
const response = await vaultApiFetch({ token, path: `/v1/meetings/${encodeURIComponent(id)}`, query: { companyId, view: "live" } });
|
|
22
|
+
if (!response.ok)
|
|
23
|
+
throw new Error("Could not read this live meeting. Check meeting access and retry.");
|
|
24
|
+
const data = await response.json();
|
|
25
|
+
if (!Number.isInteger(data.revision) || !Array.isArray(data.segments) || data.full === false)
|
|
26
|
+
throw new Error("Invalid full live transcript snapshot");
|
|
27
|
+
const lines = data.segments.map((s) => {
|
|
28
|
+
if (!s || typeof s !== "object" || typeof s.text !== "string")
|
|
29
|
+
throw new Error("Invalid live transcript segment");
|
|
30
|
+
const segment = s;
|
|
31
|
+
return `[${typeof segment.startSeconds === "number" ? segment.startSeconds : 0}] ${typeof segment.speaker === "string" ? segment.speaker : "Speaker"}: ${segment.text}`;
|
|
32
|
+
});
|
|
33
|
+
// Never broaden live meeting content to the company. The authenticated reader
|
|
34
|
+
// has access; a final transcript supplies the authoritative attendee audience.
|
|
35
|
+
const person = await resolveCallerPersonUid(token);
|
|
36
|
+
return { sourceRef, text: lines.join("\n"), segmentCount: lines.length, revision: data.revision, audience: [person] };
|
|
37
|
+
};
|
|
38
|
+
export const SOURCE_READERS = { meeting: meetingReader };
|
|
39
|
+
export async function readIncrementalSource(ctx, ref, readers = SOURCE_READERS) {
|
|
40
|
+
const colon = ref.indexOf(":");
|
|
41
|
+
if (colon < 1)
|
|
42
|
+
throw new Error("--source must be <channel>:<file> or meeting:<id>");
|
|
43
|
+
const channel = ref.slice(0, colon);
|
|
44
|
+
const id = ref.slice(colon + 1);
|
|
45
|
+
assertSafeSegment(channel, "source channel");
|
|
46
|
+
assertSafeSegment(id, "source id");
|
|
47
|
+
const reader = Object.hasOwn(readers, channel) ? readers[channel] : undefined;
|
|
48
|
+
if (reader)
|
|
49
|
+
return reader(ctx, id);
|
|
50
|
+
const name = id.endsWith(".md") ? id : `${id}.md`;
|
|
51
|
+
const key = `sources/${channel}/${name}`;
|
|
52
|
+
const file = assertAllowedPath(ctx.hqRoot, `companies/${ctx.company}/${key}`, { company: ctx.company });
|
|
53
|
+
const text = fs.readFileSync(file, "utf8");
|
|
54
|
+
const { frontmatter } = splitFrontmatter(text);
|
|
55
|
+
const audience = Array.isArray(frontmatter.audience) ? frontmatter.audience.filter((p) => typeof p === "string" && p.trim().length > 0) : [];
|
|
56
|
+
if (audience.length === 0)
|
|
57
|
+
throw new Error("Source has no audience; add its intended audience before extracting signals");
|
|
58
|
+
const sourceRef = typeof frontmatter.signal_source_ref === "string" && /^meeting:[a-zA-Z0-9._-]+$/.test(frontmatter.signal_source_ref) ? frontmatter.signal_source_ref : key;
|
|
59
|
+
return { sourceRef, text, segmentCount: text.split(/\r?\n/).filter(Boolean).length, audience };
|
|
60
|
+
}
|
|
61
|
+
export function cursorPath(company, sourceRef) {
|
|
62
|
+
assertSafeSegment(company, "company");
|
|
63
|
+
const safe = createHash("sha256").update(sourceRef).digest("hex");
|
|
64
|
+
return `workspace/executive/signals/${company}/${safe}.cursor.json`;
|
|
65
|
+
}
|
|
66
|
+
function readCursor(ctx, sourceRef) {
|
|
67
|
+
const file = assertAllowedPath(ctx.hqRoot, cursorPath(ctx.company, sourceRef), { company: ctx.company });
|
|
68
|
+
if (!fs.existsSync(file))
|
|
69
|
+
return null;
|
|
70
|
+
const c = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
71
|
+
if (!Number.isInteger(c.revision) || c.revision < 0 || !Number.isInteger(c.segment_count) || c.segment_count < 0 ||
|
|
72
|
+
typeof c.digest !== "string" || !Array.isArray(c.signal_ids) || !c.signal_ids.every((id) => /^[a-f0-9]{64}$/.test(id)))
|
|
73
|
+
throw new Error("Invalid incremental cursor; restore it before retrying");
|
|
74
|
+
if (c.pending_events !== undefined && (!Array.isArray(c.pending_events) || c.pending_events.some((e) => !e || e.event !== "signal.written" || e.company !== ctx.company || e.source_ref !== sourceRef || !c.signal_ids.includes(e.signal_id) || typeof e.type !== "string" || typeof e.key !== "string")))
|
|
75
|
+
throw new Error("Invalid incremental event cursor");
|
|
76
|
+
return c;
|
|
77
|
+
}
|
|
78
|
+
/** Only read the current audience partition, never another audience's signals. */
|
|
79
|
+
export function priorSignalsFor(ctx, sourceRef, audience) {
|
|
80
|
+
const partition = audienceKey(audience);
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const type of ["action_item", "commitment", "decision", "risk", "question", "key_point", "participant_contribution", "summary"]) {
|
|
83
|
+
const dir = signalTypeDir(partition, type);
|
|
84
|
+
const root = assertAllowedPath(ctx.hqRoot, `companies/${ctx.company}/${dir}/probe.md`, { company: ctx.company });
|
|
85
|
+
if (!fs.existsSync(path.dirname(root)))
|
|
86
|
+
continue;
|
|
87
|
+
for (const name of fs.readdirSync(path.dirname(root))) {
|
|
88
|
+
if (!/^[a-f0-9]{64}\.md$/.test(name))
|
|
89
|
+
continue;
|
|
90
|
+
const key = `${dir}/${name}`;
|
|
91
|
+
const file = assertAllowedPath(ctx.hqRoot, `companies/${ctx.company}/${key}`, { company: ctx.company });
|
|
92
|
+
const { frontmatter: fm } = splitFrontmatter(fs.readFileSync(file, "utf8"));
|
|
93
|
+
if (fm.source_ref !== sourceRef || fm.signal_id !== name.slice(0, -3) || fm.type !== type || typeof fm.canonical_content !== "string")
|
|
94
|
+
continue;
|
|
95
|
+
out.push({ signal_id: fm.signal_id, type, title: fm.canonical_content, key, live: fm.origin === "live" });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
export function createIncrementalStep(source, options = {}) {
|
|
101
|
+
return {
|
|
102
|
+
name: "extract",
|
|
103
|
+
async run(ctx) {
|
|
104
|
+
const readers = { ...SOURCE_READERS, ...options.readers };
|
|
105
|
+
let snapshot = await readIncrementalSource(ctx, source, readers);
|
|
106
|
+
const vaultRoot = path.join(ctx.hqRoot, "companies", ctx.company);
|
|
107
|
+
const lock = await acquireSyncLock(vaultRoot, `incremental-${createHash("sha256").update(snapshot.sourceRef).digest("hex")}`);
|
|
108
|
+
if (!lock)
|
|
109
|
+
throw new Error("Another incremental run holds this source; retry later");
|
|
110
|
+
try {
|
|
111
|
+
// Resolve the lock identity first, then read content while holding it.
|
|
112
|
+
// A slow initial read must not overwrite a newer run's snapshot.
|
|
113
|
+
const lockedSnapshot = await readIncrementalSource(ctx, source, readers);
|
|
114
|
+
if (lockedSnapshot.sourceRef !== snapshot.sourceRef)
|
|
115
|
+
throw new Error("Source identity changed while acquiring its lock; retry later");
|
|
116
|
+
snapshot = lockedSnapshot;
|
|
117
|
+
const digest = createHash("sha256").update(snapshot.text).update(JSON.stringify(snapshot.audience)).digest("hex");
|
|
118
|
+
const cursor = readCursor(ctx, snapshot.sourceRef);
|
|
119
|
+
// Events are persisted with the cursor before delivery. A failed callback
|
|
120
|
+
// is retried on the next pass without asking the model to recreate a file.
|
|
121
|
+
const deliver = async (state) => {
|
|
122
|
+
while (options.onSignalWritten && state.pending_events?.length) {
|
|
123
|
+
await options.onSignalWritten(state.pending_events[0]);
|
|
124
|
+
await lock.assertHeld();
|
|
125
|
+
state.pending_events.shift();
|
|
126
|
+
ctx.write(cursorPath(ctx.company, snapshot.sourceRef), `${JSON.stringify(state, null, 2)}\n`);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
if (cursor)
|
|
130
|
+
await deliver(cursor);
|
|
131
|
+
const empty = { status: "ok", itemsIn: 0, itemsOut: 0, modelTurns: 0, estimatedSpendUsd: 0, detail: { new_segments: 0, signals_written: 0 } };
|
|
132
|
+
if (cursor?.digest === digest || (!cursor && !snapshot.text.trim()))
|
|
133
|
+
return empty;
|
|
134
|
+
const revision = (cursor?.revision ?? 0) + 1;
|
|
135
|
+
const prior = priorSignalsFor(ctx, snapshot.sourceRef, snapshot.audience);
|
|
136
|
+
const result = await createExtractStep({ documentOverride: { ...snapshot, partial: true, revision }, priorSignals: prior.map(({ signal_id, type, title }) => ({ signal_id, type, title })) }).run(ctx);
|
|
137
|
+
if (result.status !== "ok")
|
|
138
|
+
return result;
|
|
139
|
+
const signals = result.detail?.signals;
|
|
140
|
+
for (const signal of signals) {
|
|
141
|
+
if (signal.update && !prior.some((p) => p.signal_id === signal.update && p.type === signal.type))
|
|
142
|
+
throw new Error("Update id is not a prior signal of this source and type");
|
|
143
|
+
}
|
|
144
|
+
await lock.assertHeld();
|
|
145
|
+
const partition = audienceKey(snapshot.audience);
|
|
146
|
+
const at = ctx.now().toISOString();
|
|
147
|
+
const added = [];
|
|
148
|
+
const ids = new Set(cursor?.signal_ids ?? []);
|
|
149
|
+
if (partition !== "company")
|
|
150
|
+
ctx.write(`companies/${ctx.company}/signals/@${partition}/_audience.yaml`, yaml.dump({ principals: snapshot.audience, audience_source: "incremental" }));
|
|
151
|
+
const rows = [];
|
|
152
|
+
// Recover files persisted before a crash prevented the cursor/index commit.
|
|
153
|
+
// The model may correctly return no repeats, so recovery cannot depend on
|
|
154
|
+
// it returning those signals again.
|
|
155
|
+
const recovered = prior.filter((p) => p.live && !ids.has(p.signal_id)).map((p) => ({ event: "signal.written", signal_id: p.signal_id, type: p.type, source_ref: snapshot.sourceRef, company: ctx.company, key: p.key }));
|
|
156
|
+
for (const event of recovered) {
|
|
157
|
+
ids.add(event.signal_id);
|
|
158
|
+
if (!rows.some((row) => row.signal_id === event.signal_id))
|
|
159
|
+
rows.push({ signal_id: event.signal_id, type: event.type, source_ref: event.source_ref, key: event.key, written_at: at });
|
|
160
|
+
}
|
|
161
|
+
for (const signal of signals) {
|
|
162
|
+
const id = signal.update ?? signalId(snapshot.sourceRef, signal.type, signal.title);
|
|
163
|
+
ids.add(id);
|
|
164
|
+
const key = `${signalTypeDir(partition, signal.type)}/${id}.md`;
|
|
165
|
+
const rel = `companies/${ctx.company}/${key}`;
|
|
166
|
+
const file = assertAllowedPath(ctx.hqRoot, rel, { company: ctx.company });
|
|
167
|
+
const existed = fs.existsSync(file);
|
|
168
|
+
if (existed && !signal.update)
|
|
169
|
+
continue;
|
|
170
|
+
const fm = { signal_id: id, type: signal.type, source_ref: snapshot.sourceRef, canonical_content: signal.title, audience_key: partition, origin: "live", partial_revision: revision, written_at: at };
|
|
171
|
+
ctx.write(rel, `---\n${yaml.dump(fm, { lineWidth: -1 })}---\n${signal.body}\n`);
|
|
172
|
+
const event = { event: "signal.written", signal_id: id, type: signal.type, source_ref: snapshot.sourceRef, company: ctx.company, key };
|
|
173
|
+
if (!existed)
|
|
174
|
+
added.push(event);
|
|
175
|
+
if (!rows.some((row) => row.signal_id === id))
|
|
176
|
+
rows.push({ signal_id: id, type: signal.type, source_ref: snapshot.sourceRef, key, written_at: at });
|
|
177
|
+
}
|
|
178
|
+
await appendSignalIndex(vaultRoot, at.slice(0, 10), rows);
|
|
179
|
+
await lock.assertHeld();
|
|
180
|
+
const next = { revision, segment_count: snapshot.segmentCount, last_run_at: at, signal_ids: [...ids], digest, pending_events: [...(cursor?.pending_events ?? []), ...recovered, ...added] };
|
|
181
|
+
ctx.write(cursorPath(ctx.company, snapshot.sourceRef), `${JSON.stringify(next, null, 2)}\n`);
|
|
182
|
+
await deliver(next);
|
|
183
|
+
return { ...result, itemsOut: added.length, detail: { new_segments: Math.max(0, snapshot.segmentCount - (cursor?.segment_count ?? 0)), signals_written: added.length } };
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
await lock.release();
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=incremental.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface StoredPriorSignal {
|
|
2
|
+
signal_id: string;
|
|
3
|
+
type: string;
|
|
4
|
+
title: string;
|
|
5
|
+
origin: string;
|
|
6
|
+
key: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function priorSignalPath(hqRoot: string, company: string, key: string): string;
|
|
9
|
+
export declare function findPriorSignals(hqRoot: string, company: string, sourceRef: string, now?: Date, audienceKey?: string): StoredPriorSignal[];
|
|
10
|
+
export declare function normalizeSignalTitle(title: string): string;
|
|
11
|
+
//# sourceMappingURL=prior-signals.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Source-scoped lookup for the final pass. Index keys are untrusted vault data. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { splitFrontmatter } from "../hot-channels/signals-pass.js";
|
|
5
|
+
import { assertAllowedPath } from "./write-guard.js";
|
|
6
|
+
export function priorSignalPath(hqRoot, company, key) {
|
|
7
|
+
if (!/^signals\/(?:@[a-zA-Z0-9_-]+\/)?[a-z_]+\/[a-f0-9]{64}\.md$/.test(key))
|
|
8
|
+
throw new Error("Invalid prior signal key");
|
|
9
|
+
return assertAllowedPath(hqRoot, `companies/${company}/${key}`, { company });
|
|
10
|
+
}
|
|
11
|
+
export function findPriorSignals(hqRoot, company, sourceRef, now = new Date(), audienceKey) {
|
|
12
|
+
const result = new Map();
|
|
13
|
+
for (let days = 0; days < 14; days++) {
|
|
14
|
+
const date = new Date(now);
|
|
15
|
+
date.setUTCDate(date.getUTCDate() - days);
|
|
16
|
+
const index = assertAllowedPath(hqRoot, `companies/${company}/signals/_index/${date.toISOString().slice(0, 10)}.json`, { company });
|
|
17
|
+
if (!fs.existsSync(index))
|
|
18
|
+
continue;
|
|
19
|
+
const rows = JSON.parse(fs.readFileSync(index, "utf8"));
|
|
20
|
+
if (!Array.isArray(rows))
|
|
21
|
+
throw new Error("Invalid signal index");
|
|
22
|
+
for (const row of rows) {
|
|
23
|
+
if (!row || row.source_ref !== sourceRef || typeof row.key !== "string" || typeof row.type !== "string" || typeof row.signal_id !== "string" || !/^[a-f0-9]{64}$/.test(row.signal_id))
|
|
24
|
+
continue;
|
|
25
|
+
const file = priorSignalPath(hqRoot, company, row.key);
|
|
26
|
+
if (!fs.existsSync(file))
|
|
27
|
+
continue;
|
|
28
|
+
const { frontmatter: fm } = splitFrontmatter(fs.readFileSync(file, "utf8"));
|
|
29
|
+
if (fm.source_ref !== sourceRef || fm.signal_id !== row.signal_id || fm.type !== row.type || path.basename(file) !== `${row.signal_id}.md`)
|
|
30
|
+
continue;
|
|
31
|
+
if (audienceKey !== undefined && fm.audience_key !== audienceKey)
|
|
32
|
+
continue;
|
|
33
|
+
const title = typeof fm.title === "string" ? fm.title : fm.canonical_content;
|
|
34
|
+
if (typeof title !== "string" || !title.trim())
|
|
35
|
+
continue;
|
|
36
|
+
result.set(row.signal_id, { signal_id: row.signal_id, type: row.type, title, origin: typeof fm.origin === "string" ? fm.origin : "", key: row.key });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return [...result.values()];
|
|
40
|
+
}
|
|
41
|
+
export function normalizeSignalTitle(title) {
|
|
42
|
+
return title.toLowerCase().replace(/\s+/g, " ").trim().replace(/[\p{P}\s]+$/gu, "");
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=prior-signals.js.map
|
|
@@ -106,8 +106,13 @@ export function runCountItems(record, summary, opts) {
|
|
|
106
106
|
const breakdown = [...parts].map(([k, v]) => `${k} ${v}`).join(", ");
|
|
107
107
|
items.push({ key: "Sources read", value: `${plural(read, "item")}${breakdown ? ` (${breakdown})` : ""}` });
|
|
108
108
|
}
|
|
109
|
-
if (extract)
|
|
109
|
+
if (extract) {
|
|
110
110
|
items.push({ key: "Signals found", value: String(extract.itemsOut) });
|
|
111
|
+
const counts = extract.detail;
|
|
112
|
+
if (counts && ["new", "updated", "skipped"].every((k) => typeof counts[k] === "number")) {
|
|
113
|
+
items.push({ key: "Signal changes", value: `new: ${counts.new}, updated: ${counts.updated}, skipped: ${counts.skipped}` });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
111
116
|
const garden = steps(record, "garden").filter((s) => s.status !== "skipped");
|
|
112
117
|
if (garden.length) {
|
|
113
118
|
const tidied = garden.reduce((n, s) => n + s.itemsIn, 0);
|
|
@@ -7,4 +7,9 @@ export interface SignalsSettings {
|
|
|
7
7
|
export declare const DEFAULT_SIGNALS_SETTINGS: SignalsSettings;
|
|
8
8
|
export declare const SIGNALS_SETTINGS_REL: string;
|
|
9
9
|
export declare function loadSignalsSettings(hqRoot: string): SignalsSettings;
|
|
10
|
+
/** Per-pass limits for a growing source; normal runs retain their own defaults. */
|
|
11
|
+
export declare function loadIncrementalSettings(hqRoot: string): {
|
|
12
|
+
maxTurns: number;
|
|
13
|
+
maxBudgetUsd: number;
|
|
14
|
+
};
|
|
10
15
|
//# sourceMappingURL=settings.d.ts.map
|
|
@@ -45,4 +45,21 @@ export function loadSignalsSettings(hqRoot) {
|
|
|
45
45
|
}
|
|
46
46
|
return out;
|
|
47
47
|
}
|
|
48
|
+
/** Per-pass limits for a growing source; normal runs retain their own defaults. */
|
|
49
|
+
export function loadIncrementalSettings(hqRoot) {
|
|
50
|
+
const out = { maxTurns: 12, maxBudgetUsd: 0.4 };
|
|
51
|
+
try {
|
|
52
|
+
const raw = yaml.load(fs.readFileSync(path.join(hqRoot, SIGNALS_SETTINGS_REL), "utf8"));
|
|
53
|
+
const v = raw?.incremental;
|
|
54
|
+
if (typeof v?.max_turns === "number" && VALID.maxTurns(v.max_turns))
|
|
55
|
+
out.maxTurns = v.max_turns;
|
|
56
|
+
if (typeof v?.max_budget_usd === "number" && VALID.maxBudgetUsd(v.max_budget_usd))
|
|
57
|
+
out.maxBudgetUsd = v.max_budget_usd;
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (err.code !== "ENOENT")
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
48
65
|
//# sourceMappingURL=settings.js.map
|