@indigoai-us/hq-cli 5.345.193 → 5.345.195

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.
@@ -23,6 +23,7 @@
23
23
  import { randomUUID } from "node:crypto";
24
24
  import * as fs from "node:fs";
25
25
  import * as path from "node:path";
26
+ import { InvalidArgumentError } from "commander";
26
27
  import chalk from "chalk";
27
28
  import { SOURCE_CHANNELS, assertSourceChannel, resolveEntity, listAvailableEntities, listSources, getSource, VaultClient, } from "@indigoai-us/hq-cloud";
28
29
  import { ensureCognitoToken, ensureCognitoIdToken, buildVaultConfig, DEFAULT_COGNITO, DEFAULT_HQ_ROOT, } from "../utils/cognito-session.js";
@@ -34,7 +35,8 @@ import { resolveUploadAuthor } from "../utils/upload-author.js";
34
35
  import { createSignalsAlertChannelPoster, raiseSignalsFailureAlerts } from "../lib/hot-channels/alert.js";
35
36
  import { audienceKey } from "../lib/hot-channels/audience.js";
36
37
  import { appendRunLedgerEntry, ledgerDay, readRunLedger, summarizeRunLedger, } from "../lib/hot-channels/ledger.js";
37
- import { createClaudeSignalsPass } from "../lib/hot-channels/signals-pass.js";
38
+ import { createClaudeSignalsPass, DEFAULT_MAX_SIGNAL_DOCS, readPendingSignals, SignalsPassError, } from "../lib/hot-channels/signals-pass.js";
39
+ import { backlogRunResult, parseSince, queueSignalsRedo, selectSignalsRedo, SignalsRedoUsageError, } from "../lib/hot-channels/signals-redo.js";
38
40
  import { assertSafeSegment } from "../lib/hot-channels/landing.js";
39
41
  import { acquireSyncLock, SYNC_LOCK_BUSY_REASON } from "../lib/hot-channels/sync-lock.js";
40
42
  import { createBuiltinHotChannelRegistry } from "../lib/hot-channels/adapters/index.js";
@@ -642,6 +644,139 @@ function renderSyncTable(company, report) {
642
644
  }
643
645
  return lines.join("\n");
644
646
  }
647
+ /**
648
+ * `hq sources signals`: queue landed documents for the signals pass and run
649
+ * the pass now, under the same per-person lock as `hq sources sync`. The pass
650
+ * keeps its cap (`--max-docs`, default 20); what the cap leaves stays queued
651
+ * and the next run (this command or the daily sync) picks it up.
652
+ */
653
+ export async function runSignalsRedo(options, overrides = {}) {
654
+ const company = options.company ?? readActiveCompanySlug(options.hqRoot);
655
+ const fail = (message, exitCode) => {
656
+ process.stderr.write(chalk.red(`✗ ${message}\n`));
657
+ process.exitCode = exitCode;
658
+ throw new SignalsRedoExit(message, exitCode);
659
+ };
660
+ if (!company)
661
+ return fail("No company specified. Pass --company <slug> or set activeCompany in .hq/config.json.", 2);
662
+ assertSafeSegment(company, "company");
663
+ if (options.since === undefined && (options.key ?? []).length === 0)
664
+ return fail("Pass --since <date> or --key <sources/channel/id.md>.", 2);
665
+ const vaultRoot = path.join(options.hqRoot, "companies", company);
666
+ if (!fs.existsSync(vaultRoot))
667
+ return fail(`No company directory: companies/${company}`, 2);
668
+ const maxDocs = options.maxDocs ?? DEFAULT_MAX_SIGNAL_DOCS;
669
+ let selection;
670
+ try {
671
+ selection = await selectSignalsRedo({
672
+ vaultRoot,
673
+ ...(options.key ? { keys: options.key } : {}),
674
+ ...(options.since !== undefined ? { since: parseSince(options.since) } : {}),
675
+ ...(options.channel !== undefined ? { channel: options.channel } : {}),
676
+ });
677
+ }
678
+ catch (err) {
679
+ if (err instanceof SignalsRedoUsageError)
680
+ return fail(err.message, 2);
681
+ throw err;
682
+ }
683
+ const report = {
684
+ company,
685
+ dryRun: options.dryRun === true,
686
+ queued: { pending: Object.fromEntries(selection.pending), worklist: Object.fromEntries(selection.worklist) },
687
+ signalsWritten: 0,
688
+ pendingLeft: {},
689
+ notices: [...selection.notices],
690
+ error: null,
691
+ exitCode: 0,
692
+ };
693
+ const channels = [...new Set([...selection.pending.keys(), ...selection.worklist.keys()])];
694
+ if (!report.dryRun && channels.length > 0) {
695
+ const author = overrides.author !== undefined ? overrides.author : resolveUploadAuthor();
696
+ if (!author)
697
+ return fail(`${SIGN_IN_PROMPT} (the run takes your hot-channel sync lock)`, 1);
698
+ const lock = await acquireSyncLock(vaultRoot, audienceKey([author.email]), overrides.now ? { now: overrides.now } : {});
699
+ if (!lock)
700
+ return fail(`Another hot-channel sync for ${company} is running. Try again when it finishes.`, 1);
701
+ try {
702
+ await queueSignalsRedo({
703
+ hqRoot: options.hqRoot,
704
+ company,
705
+ selection,
706
+ beforeWrite: lock.assertHeld,
707
+ ...(overrides.worklistScript ? { worklistScript: overrides.worklistScript } : {}),
708
+ });
709
+ const pass = overrides.createPass
710
+ ? overrides.createPass(maxDocs)
711
+ : createClaudeSignalsPass({ hqRoot: options.hqRoot, company, maxDocs });
712
+ for (const channel of channels) {
713
+ report.signalsWritten += await pass(backlogRunResult(channel), { beforeWrite: lock.assertHeld });
714
+ }
715
+ }
716
+ catch (err) {
717
+ report.error = err instanceof SignalsPassError ? err.code : err instanceof Error ? err.message : String(err);
718
+ report.exitCode = 1;
719
+ }
720
+ finally {
721
+ await lock.release();
722
+ }
723
+ }
724
+ for (const channel of selection.pending.keys()) {
725
+ report.pendingLeft[channel] = (await readPendingSignals(vaultRoot, channel)).length;
726
+ }
727
+ if (options.json) {
728
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
729
+ }
730
+ else {
731
+ process.stdout.write(`${renderSignalsRedo(report)}\n`);
732
+ }
733
+ if (report.exitCode !== 0)
734
+ process.exitCode = report.exitCode;
735
+ return report;
736
+ }
737
+ /** A usage or setup failure `runSignalsRedo` already printed; the exit code is set. */
738
+ export class SignalsRedoExit extends Error {
739
+ exitCode;
740
+ constructor(message, exitCode) {
741
+ super(message);
742
+ this.name = "SignalsRedoExit";
743
+ this.exitCode = exitCode;
744
+ }
745
+ }
746
+ function renderSignalsRedo(report) {
747
+ const lines = [`${report.dryRun ? "Would queue" : "Queued"} for signals in ${report.company}:`];
748
+ const groups = [
749
+ ...Object.entries(report.queued.pending).map(([channel, keys]) => ({ channel, keys, how: "pending" })),
750
+ ...Object.entries(report.queued.worklist).map(([channel, keys]) => ({ channel, keys, how: "worklist --redo" })),
751
+ ];
752
+ if (groups.length === 0)
753
+ lines.push(" nothing (no matching landed document without signals)");
754
+ for (const group of groups) {
755
+ lines.push(` sources/${group.channel}: ${group.keys.length} (${group.how})`);
756
+ for (const key of group.keys)
757
+ lines.push(` ${key}`);
758
+ }
759
+ lines.push(...report.notices);
760
+ if (!report.dryRun && groups.length > 0) {
761
+ lines.push("", `${report.signalsWritten} signal(s) written.`);
762
+ for (const [channel, left] of Object.entries(report.pendingLeft)) {
763
+ if (left > 0)
764
+ lines.push(`${left} document(s) still queued in sources/${channel}; the next run picks them up.`);
765
+ }
766
+ if (report.error)
767
+ lines.push(chalk.red(`The signals pass stopped: ${report.error}. Queued documents stay queued.`));
768
+ }
769
+ return lines.join("\n");
770
+ }
771
+ function collectKey(value, previous = []) {
772
+ return [...previous, value];
773
+ }
774
+ function parseMaxDocs(value) {
775
+ const n = Number(value);
776
+ if (!Number.isInteger(n) || n <= 0)
777
+ throw new InvalidArgumentError("must be a positive integer");
778
+ return n;
779
+ }
645
780
  function fleetBotsIn(root) {
646
781
  let names;
647
782
  try {
@@ -842,6 +977,27 @@ export function registerSourcesCommand(program) {
842
977
  .action(withErrorHandling(async (options) => {
843
978
  await runSchedule(options);
844
979
  }));
980
+ sources
981
+ .command("signals")
982
+ .description("Process landed sources for signals again: queue them for the signals pass and run it now (the pass's document cap still applies)")
983
+ .option("--company <slug>", "Company slug (defaults to .hq/config.json activeCompany)")
984
+ .option("--since <date>", "Landed documents from this date on (YYYY-MM-DD is local midnight) that have no signals yet")
985
+ .option("--key <key>", "A landed document to process again, signals or not (sources/<channel>/<id>.md; repeatable)", collectKey)
986
+ .option("--channel <channel>", "Limit --since to one channel")
987
+ .option("--max-docs <n>", `Documents sent to claude in this run (default ${DEFAULT_MAX_SIGNAL_DOCS})`, parseMaxDocs)
988
+ .option("--dry-run", "Print what would be queued; queue nothing and call no model")
989
+ .option("--json", "Print the report as JSON")
990
+ .option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
991
+ .action(withErrorHandling(async (options) => {
992
+ try {
993
+ await runSignalsRedo(options);
994
+ }
995
+ catch (err) {
996
+ // Usage and setup failures are already printed with their exit code.
997
+ if (!(err instanceof SignalsRedoExit))
998
+ throw err;
999
+ }
1000
+ }));
845
1001
  sources
846
1002
  .command("sync")
847
1003
  .description("Run every hot channel now: each adapter once per connection you can use")
@@ -21,6 +21,13 @@ export interface WhoamiDisplayIdentity {
21
21
  * email with the token subject of the delegating machine.
22
22
  */
23
23
  export declare function resolveWhoamiIdentity(identity: WhoamiTokenIdentity): WhoamiDisplayIdentity;
24
+ /**
25
+ * Bound on the person-uid lookup, so `--json` callers with short timeouts still
26
+ * get an answer. Kept at 1s: with every endpoint stalled, a whole `whoami
27
+ * --json` must still finish inside the foreground network budget plus startup
28
+ * (foreground-network-budget.test.ts); 3s overran it.
29
+ */
30
+ export declare const PERSON_UID_LOOKUP_TIMEOUT_MS = 1000;
24
31
  export declare function renderWhoamiPlanBlock(status: PlanLimitStatus, opts?: {
25
32
  inviteTeammateEnabled?: boolean;
26
33
  }): string | null;
@@ -8,6 +8,7 @@ import { ensureCognitoToken, loadMachineCachedTokens, resolveCognitoTokenSource,
8
8
  import { colorizePlanLimitNotice, fetchPlanLimitStatus, renderPlanLine, renderPlanLimitNotice, } from "../lib/billing/plan-lock.js";
9
9
  import { resolveCompanyFlagGate, } from "../lib/flag-registry.js";
10
10
  import { queuePlanLimitInviteTeammateExposure } from "../utils/cli-telemetry.js";
11
+ import { resolveCallerPersonUid } from "../utils/vault-api.js";
11
12
  export const CLI_LIMIT_INVITE_TEAMMATE_FLAG = "cli.limit-invite-teammate-v1";
12
13
  /**
13
14
  * Resolves the person represented by an ID token without pairing a delegated
@@ -44,6 +45,36 @@ function peekIdToken(idToken) {
44
45
  : undefined,
45
46
  };
46
47
  }
48
+ /**
49
+ * Bound on the person-uid lookup, so `--json` callers with short timeouts still
50
+ * get an answer. Kept at 1s: with every endpoint stalled, a whole `whoami
51
+ * --json` must still finish inside the foreground network budget plus startup
52
+ * (foreground-network-budget.test.ts); 3s overran it.
53
+ */
54
+ export const PERSON_UID_LOOKUP_TIMEOUT_MS = 1_000;
55
+ /**
56
+ * The signed-in person's canonical `prs_*` uid for `whoami --json`. Person ID
57
+ * tokens carry no `custom:entityType`/`custom:entityUid` claims, so the token
58
+ * alone almost never has it; the vault's person-entity lookup does (the same
59
+ * rule `hq secrets --personal` uses). Best-effort: no session, a timeout or a
60
+ * network failure resolve to null and the rest of the identity still prints.
61
+ */
62
+ async function readPersonUid(identity) {
63
+ if (identity.entityType === 'person' && identity.entityUid)
64
+ return identity.entityUid;
65
+ try {
66
+ const token = await ensureCognitoToken({ interactive: false });
67
+ if (!token)
68
+ return null;
69
+ return await resolveCallerPersonUid(token, undefined, {
70
+ command: 'hq whoami',
71
+ signal: AbortSignal.timeout(PERSON_UID_LOOKUP_TIMEOUT_MS),
72
+ });
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
47
78
  /**
48
79
  * starter-plan-hard-limits / US-011 — read the workspace plan-limit nag for
49
80
  * `--company <slug>` (the bind notice).
@@ -148,9 +179,11 @@ export function registerWhoamiCommand(program) {
148
179
  : boundLegacyAgent;
149
180
  // planLimits is additive: absent-or-null means UNKNOWN.
150
181
  const planLimits = await readPlanLimits(opts.company);
182
+ const authenticated = machine ? Boolean(machineCreds) : Boolean(cached && !isExpiring(cached, 0));
183
+ const personUid = !machine && authenticated ? await readPersonUid(identity) : null;
151
184
  console.log(JSON.stringify({
152
185
  schemaVersion: 1,
153
- authenticated: machine ? Boolean(machineCreds) : Boolean(cached && !isExpiring(cached, 0)),
186
+ authenticated,
154
187
  tokenSource: machine ? 'machine' : 'person',
155
188
  email: machine ? null : displayedIdentity.email ?? null,
156
189
  // Cognito subject of the person shown in `email` (null for machine
@@ -162,7 +195,7 @@ export function registerWhoamiCommand(program) {
162
195
  ? identity.delegatedEmail : null,
163
196
  delegatedSub: identity.delegatedEmail !== undefined && identity.delegatedSub !== undefined
164
197
  ? identity.delegatedSub : null,
165
- personUid: !machine && identity.entityType === 'person' ? identity.entityUid ?? null : null,
198
+ personUid,
166
199
  agentUid,
167
200
  username: machineCreds?.username ?? null,
168
201
  planLimits,
@@ -324,7 +324,9 @@ export declare function pipeInheritedStdio(stdio: unknown): {
324
324
  */
325
325
  export declare function windowsExtractionPathProblem(destination: string, longest: number, longPathsEnabled: number | null): string | null;
326
326
  /** Moves a fresh machine credential into the bot's secure store and deletes the file. */
327
- export declare function importFleetLocalMachineCredential(agentUid: string, machineCredentials: string, deps?: FleetLocalControlDeps): void;
327
+ export declare function importFleetLocalMachineCredential(agentUid: string, machineCredentials: string, deps?: FleetLocalControlDeps, opts?: {
328
+ keepSource?: boolean;
329
+ }): void;
328
330
  export declare function bootstrapFleetLocalBot(input: FleetLocalProfileInput & {
329
331
  machineCredentials?: string;
330
332
  }, deps?: FleetLocalBootstrapDeps): Promise<FleetLocalBootstrapResult>;
@@ -79,15 +79,15 @@ export function sharedRuntimeLayout(root, id, platform) {
79
79
  }
80
80
  export const HQ_FLEET_LOCAL_RELEASE = {
81
81
  repository: "indigoai-us/hq-fleet-local",
82
- tag: "v0.4.20",
83
- version: "0.4.20",
84
- artifact: "hq-fleet-local-0.4.20.zip",
85
- artifactSha256: "5c49402f17408f3eaf99d27b3c2d4d0f59a24f33946aa8dfc3098a3d09e13798",
86
- runtimeArtifact: "hq-fleet-runtime-abccac13bd9e.tar",
87
- runtimeArtifactSha256: "29bcfe718ad7fbd3636afd00623772f2a25bc84c4f6f10d8d57f38e549d528d5",
88
- manifestSha256: "3cec47eb7f47af13e66d4ad29669d3c103e1ca452661f5eaad760243118ce214",
89
- signatureSha256: "5504c3a956f36d148f3cdf2212a9b171d852098c82eb199c7c415ad3545e3b05",
90
- runtimeCommit: "abccac13bd9ea593b24c8cd9c0ba28d5c6050cc9",
82
+ tag: "v0.4.21",
83
+ version: "0.4.21",
84
+ artifact: "hq-fleet-local-0.4.21.zip",
85
+ artifactSha256: "24162dcc733fdc4a8e7a3482f5b2c445f5b6af96acf1695dd7dc5c7803272e3f",
86
+ runtimeArtifact: "hq-fleet-runtime-24211b85a414.tar",
87
+ runtimeArtifactSha256: "9084d88147bc9d70052aa00cc963eec2f80525bf61539d22abf45497712bd8b2",
88
+ manifestSha256: "5b52bd4f071470941ab1cd9a4f6baf614179f70073f60bfdf63f8a7ba38aa7ad",
89
+ signatureSha256: "85c35d5b2ca365503c408cd83a6d19758382f2e3049840fa055860e6f2d21746",
90
+ runtimeCommit: "24211b85a414441bac6405d862b1c872b44a78a1",
91
91
  };
92
92
  /** hq-fleet-local trust/release-public.pem (rotated 2026-10-30; signed v0.4.17). */
93
93
  export const HQ_FLEET_LOCAL_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
@@ -1585,7 +1585,7 @@ function assertWindowsExtractionFits(run, python, archive, destination) {
1585
1585
  throw expectedError(problem);
1586
1586
  }
1587
1587
  /** Moves a fresh machine credential into the bot's secure store and deletes the file. */
1588
- export function importFleetLocalMachineCredential(agentUid, machineCredentials, deps = {}) {
1588
+ export function importFleetLocalMachineCredential(agentUid, machineCredentials, deps = {}, opts = {}) {
1589
1589
  const run = deps.run ?? execFileSync;
1590
1590
  const files = fleetLocalExecutables(agentUid, deps);
1591
1591
  const machineValue = fs.readFileSync(machineCredentials, "utf8").trim();
@@ -1600,7 +1600,8 @@ export function importFleetLocalMachineCredential(agentUid, machineCredentials,
1600
1600
  "--bot-id",
1601
1601
  agentUid,
1602
1602
  ], { input: machineValue, stdio: ["pipe", "ignore", "pipe"] }, "machine credential import");
1603
- fs.rmSync(machineCredentials, { force: true });
1603
+ if (!opts.keepSource)
1604
+ fs.rmSync(machineCredentials, { force: true });
1604
1605
  }
1605
1606
  async function installSharedFleetRuntime(installation, root, python, platform, run, spawn) {
1606
1607
  const identity = digest(Buffer.from(JSON.stringify({
@@ -0,0 +1,15 @@
1
+ import { type BotConfig } from "./config.js";
2
+ export declare const KEEP_LEGACY_RUNTIME_ENV = "HQ_BOT_KEEP_LEGACY_RUNTIME";
3
+ export type LegacyConversionDecision = {
4
+ convert: true;
5
+ } | {
6
+ convert: false;
7
+ reason: "already-fleet" | "kept-by-env" | "needs-sign-in" | "own-claude-account";
8
+ };
9
+ export declare function legacyConversionDecision(config: Pick<BotConfig, "engine" | "runtime" | "runtimeAuth">, env?: NodeJS.ProcessEnv): LegacyConversionDecision;
10
+ /** How long a legacy runner's own start skips the move after one failed. */
11
+ export declare const FLEET_MOVE_RETRY_MS: number;
12
+ export declare function fleetMoveFailurePath(dir: string): string;
13
+ export declare function writeFleetMoveFailure(dir: string, now?: Date): void;
14
+ export declare function fleetMoveRecentlyFailed(dir: string, now?: number): boolean;
15
+ //# sourceMappingURL=legacy-convert.d.ts.map
@@ -0,0 +1,38 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { HQ_FLEET_RUNTIME_ENGINE_ID } from "./config.js";
4
+ export const KEEP_LEGACY_RUNTIME_ENV = "HQ_BOT_KEEP_LEGACY_RUNTIME";
5
+ export function legacyConversionDecision(config, env = process.env) {
6
+ if (config.engine?.id === HQ_FLEET_RUNTIME_ENGINE_ID)
7
+ return { convert: false, reason: "already-fleet" };
8
+ if (env[KEEP_LEGACY_RUNTIME_ENV] === "1")
9
+ return { convert: false, reason: "kept-by-env" };
10
+ if (config.runtime !== "claude")
11
+ return { convert: false, reason: "needs-sign-in" };
12
+ if (config.runtimeAuth?.claude)
13
+ return { convert: false, reason: "own-claude-account" };
14
+ return { convert: true };
15
+ }
16
+ /** How long a legacy runner's own start skips the move after one failed. */
17
+ export const FLEET_MOVE_RETRY_MS = 60 * 60 * 1000;
18
+ export function fleetMoveFailurePath(dir) {
19
+ return path.join(dir, "fleet-move-failed.json");
20
+ }
21
+ export function writeFleetMoveFailure(dir, now = new Date()) {
22
+ try {
23
+ fs.writeFileSync(fleetMoveFailurePath(dir), JSON.stringify({ at: now.toISOString() }) + "\n", { mode: 0o600 });
24
+ }
25
+ catch {
26
+ /* the next start just tries again */
27
+ }
28
+ }
29
+ export function fleetMoveRecentlyFailed(dir, now = Date.now()) {
30
+ try {
31
+ const at = Date.parse(JSON.parse(fs.readFileSync(fleetMoveFailurePath(dir), "utf8")).at ?? "");
32
+ return Number.isFinite(at) && now - at < FLEET_MOVE_RETRY_MS;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ //# sourceMappingURL=legacy-convert.js.map
@@ -12,6 +12,12 @@ export interface HotChannelRunResult {
12
12
  /** Adapter id, or the catalog provider when it did not resolve. */
13
13
  adapterId: string;
14
14
  provider: string;
15
+ /**
16
+ * The adapter's channel (`sources/{channel}/`). Set once the adapter
17
+ * resolves, so the signals pass can drain that channel's worklist even
18
+ * when the run landed nothing.
19
+ */
20
+ channel?: string;
15
21
  /** The connection this run used; null for a channel with no connection. */
16
22
  connectionId: string | null;
17
23
  transport: HotTransport | null;
@@ -209,6 +209,7 @@ export async function runHotChannelLoop(input) {
209
209
  try {
210
210
  const adapter = input.registry.resolve(entry);
211
211
  result.adapterId = adapter.id;
212
+ result.channel = adapter.channel;
212
213
  result.transport = adapter.transport;
213
214
  if (adapter.transport === "push") {
214
215
  await drainPush(ctx, adapter, result);
@@ -1,9 +1,22 @@
1
1
  /**
2
- * The local signals pass (US-011). After a hot-channel run lands sources, the
3
- * pass takes the run's landed documents through the ontology worklist, asks
2
+ * The local signals pass (US-011). After each hot-channel adapter run, the
3
+ * pass takes the adapter's channel through the ontology worklist, asks
4
4
  * `claude -p` for signals one document at a time, and writes those signals to
5
5
  * `companies/{co}/signals/`. It returns the count for the run ledger.
6
6
  *
7
+ * Work comes from the worklist, not only from this run's landings: when the
8
+ * worklist script exits 0, the pass takes the channel's unprocessed items in
9
+ * the worklist's order (newest first) up to the run's document cap. Items the
10
+ * cap leaves over stay unmarked, and a later run picks them up, including a
11
+ * run that landed nothing (a bounded backlog drain: at most
12
+ * DEFAULT_MAX_SIGNAL_DOCS claude calls per run, and no claude call at all when
13
+ * the backlog is empty). A channel with no source.yaml (worklist exit 2, the
14
+ * usual case for a hot channel such as `meeting-events`) has no worklist, so
15
+ * the pass keeps its own backlog: landed documents it has not processed yet
16
+ * are listed in `sources/_signals-pending/{channel}.json` and drained, newest
17
+ * first by `when` (else `created_at`), on later runs under the same cap. A
18
+ * cloud-run source (exit 4) is skipped.
19
+ *
7
20
  * A landed document with both `payload_ref` and `raw_ref` is a pointer (the
8
21
  * meetings, Slack and Attio adapters land these). For it, claude gets the
9
22
  * pointer followed by the document `payload_ref` names, capped at
@@ -118,6 +131,24 @@ interface ParsedSignal {
118
131
  }
119
132
  export declare function defaultPromptFile(): string;
120
133
  export declare const CLAUDE_LOGGED_OUT_RE: RegExp;
134
+ export declare function channelOf(key: string): string | null;
135
+ /**
136
+ * Vault-relative file that lists a channel's landed documents the pass has not
137
+ * processed yet. Only channels with no worklist (no source.yaml) use it.
138
+ */
139
+ export declare function pendingSignalsKey(channel: string): string;
140
+ /** The channel's pending landed keys; a missing or unreadable file is an empty backlog. */
141
+ export declare function readPendingSignals(vaultRoot: string, channel: string): Promise<string[]>;
142
+ /** Replace the channel's pending list; an empty list removes the file. */
143
+ export declare function writePendingSignals(vaultRoot: string, channel: string, keys: readonly string[], beforeWrite?: () => Promise<void>): Promise<void>;
144
+ /** Add landed keys to the channel's pending list (kept once each). */
145
+ export declare function queuePendingSignals(vaultRoot: string, channel: string, keys: readonly string[], beforeWrite?: () => Promise<void>): Promise<void>;
146
+ /** When a landed document happened: `when`, else `created_at`; unknown sorts last. */
147
+ export declare function landedDocTime(frontmatter: Record<string, unknown>): number;
148
+ export declare function splitFrontmatter(text: string): {
149
+ frontmatter: Record<string, unknown>;
150
+ body: string;
151
+ };
121
152
  /**
122
153
  * The text claude gets for one landed document. A pointer (both `payload_ref`
123
154
  * and `raw_ref` set) gets its payload appended after a separator line. Any