@indigoai-us/hq-cli 5.345.178 → 5.345.180

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/command-catalog.generated.d.ts +197 -1
  3. package/dist/command-catalog.generated.js +241 -1
  4. package/dist/commands/bot-jobs.d.ts +11 -9
  5. package/dist/commands/bot-jobs.js +56 -21
  6. package/dist/commands/bot.d.ts +37 -1
  7. package/dist/commands/bot.js +289 -13
  8. package/dist/commands/channels.d.ts +18 -1
  9. package/dist/commands/channels.js +185 -1
  10. package/dist/commands/daemon.d.ts +2 -0
  11. package/dist/commands/daemon.js +15 -4
  12. package/dist/commands/integrations-core.d.ts +2 -0
  13. package/dist/commands/integrations-core.js +1 -0
  14. package/dist/commands/lanes.d.ts +1 -1
  15. package/dist/commands/lanes.js +6 -2
  16. package/dist/commands/mesh.js +19 -0
  17. package/dist/commands/sources.d.ts +89 -0
  18. package/dist/commands/sources.js +363 -1
  19. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2285 -1743
  20. package/dist/hq-cloud-sync-runner/package.json +1 -1
  21. package/dist/lib/bot/config.d.ts +8 -0
  22. package/dist/lib/bot/fleet-local.d.ts +1 -1
  23. package/dist/lib/bot/fleet-local.js +82 -30
  24. package/dist/lib/bot/jobs/hot-signal-sync.d.ts +109 -0
  25. package/dist/lib/bot/jobs/hot-signal-sync.js +177 -0
  26. package/dist/lib/bot/local-jobs.d.ts +40 -4
  27. package/dist/lib/bot/local-jobs.js +72 -5
  28. package/dist/lib/bot/run.d.ts +15 -0
  29. package/dist/lib/bot/run.js +99 -1
  30. package/dist/lib/bot/runtime-auth.d.ts +119 -0
  31. package/dist/lib/bot/runtime-auth.js +251 -0
  32. package/dist/lib/daemon/catalog.d.ts +1 -0
  33. package/dist/lib/daemon/catalog.js +1 -1
  34. package/dist/lib/daemon/handoff.d.ts +4 -0
  35. package/dist/lib/daemon/handoff.js +6 -2
  36. package/dist/lib/daemon/install.d.ts +70 -2
  37. package/dist/lib/daemon/install.js +185 -27
  38. package/dist/lib/daemon/legacy.d.ts +9 -3
  39. package/dist/lib/daemon/legacy.js +11 -4
  40. package/dist/lib/daemon/migrate.js +1 -1
  41. package/dist/lib/daemon/run.d.ts +5 -2
  42. package/dist/lib/daemon/run.js +74 -22
  43. package/dist/lib/doctor/checks/anywhere.d.ts +6 -0
  44. package/dist/lib/doctor/checks/anywhere.js +35 -6
  45. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  46. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  47. package/dist/lib/hot-channels/adapters/__fixtures__/dummy.d.ts +33 -0
  48. package/dist/lib/hot-channels/adapters/__fixtures__/dummy.js +53 -0
  49. package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.d.ts +38 -0
  50. package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.js +64 -0
  51. package/dist/lib/hot-channels/adapters/attio.d.ts +67 -0
  52. package/dist/lib/hot-channels/adapters/attio.js +374 -0
  53. package/dist/lib/hot-channels/adapters/gmail.d.ts +111 -0
  54. package/dist/lib/hot-channels/adapters/gmail.js +413 -0
  55. package/dist/lib/hot-channels/adapters/index.d.ts +23 -0
  56. package/dist/lib/hot-channels/adapters/index.js +39 -0
  57. package/dist/lib/hot-channels/adapters/linear.d.ts +79 -0
  58. package/dist/lib/hot-channels/adapters/linear.js +321 -0
  59. package/dist/lib/hot-channels/adapters/meeting-providers.d.ts +86 -0
  60. package/dist/lib/hot-channels/adapters/meeting-providers.js +253 -0
  61. package/dist/lib/hot-channels/adapters/meetings.d.ts +81 -0
  62. package/dist/lib/hot-channels/adapters/meetings.js +525 -0
  63. package/dist/lib/hot-channels/adapters/slack.d.ts +70 -0
  64. package/dist/lib/hot-channels/adapters/slack.js +277 -0
  65. package/dist/lib/hot-channels/alert.d.ts +78 -0
  66. package/dist/lib/hot-channels/alert.js +179 -0
  67. package/dist/lib/hot-channels/audience.d.ts +15 -0
  68. package/dist/lib/hot-channels/audience.js +25 -0
  69. package/dist/lib/hot-channels/contract.d.ts +146 -0
  70. package/dist/lib/hot-channels/contract.js +20 -0
  71. package/dist/lib/hot-channels/cursors.d.ts +41 -0
  72. package/dist/lib/hot-channels/cursors.js +84 -0
  73. package/dist/lib/hot-channels/index.d.ts +10 -0
  74. package/dist/lib/hot-channels/index.js +10 -0
  75. package/dist/lib/hot-channels/landing.d.ts +83 -0
  76. package/dist/lib/hot-channels/landing.js +175 -0
  77. package/dist/lib/hot-channels/ledger.d.ts +101 -0
  78. package/dist/lib/hot-channels/ledger.js +203 -0
  79. package/dist/lib/hot-channels/loop.d.ts +74 -0
  80. package/dist/lib/hot-channels/loop.js +221 -0
  81. package/dist/lib/hot-channels/prompts/signals-pass.md +22 -0
  82. package/dist/lib/hot-channels/registry.d.ts +28 -0
  83. package/dist/lib/hot-channels/registry.js +121 -0
  84. package/dist/lib/hot-channels/run.d.ts +137 -0
  85. package/dist/lib/hot-channels/run.js +324 -0
  86. package/dist/lib/hot-channels/signals-pass.d.ts +150 -0
  87. package/dist/lib/hot-channels/signals-pass.js +581 -0
  88. package/dist/lib/hot-channels/sync-lock.d.ts +51 -0
  89. package/dist/lib/hot-channels/sync-lock.js +224 -0
  90. package/dist/lib/install/hook-target.d.ts +6 -2
  91. package/dist/lib/install/hook-target.js +9 -4
  92. package/dist/lib/lanes/accounts.d.ts +1 -17
  93. package/dist/lib/lanes/accounts.js +5 -45
  94. package/dist/lib/lanes/loop.d.ts +10 -0
  95. package/dist/lib/lanes/loop.js +18 -1
  96. package/dist/lib/lanes/provider-home.d.ts +24 -0
  97. package/dist/lib/lanes/provider-home.js +52 -0
  98. package/dist/lib/lanes/spawn.d.ts +12 -0
  99. package/dist/lib/lanes/spawn.js +27 -0
  100. package/dist/lib/mesh/live/daemon/doctor.d.ts +3 -0
  101. package/dist/lib/mesh/live/daemon/doctor.js +13 -0
  102. package/dist/lib/mesh/live/daemon/owner-inbox-push.d.ts +94 -0
  103. package/dist/lib/mesh/live/daemon/owner-inbox-push.js +273 -0
  104. package/dist/lib/mesh/live/daemon/run.js +31 -0
  105. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +14 -0
  106. package/dist/lib/mesh/live/daemon/work-subscribe.js +210 -3
  107. package/dist/lib/mesh/live/paths.d.ts +2 -0
  108. package/dist/lib/mesh/live/paths.js +2 -0
  109. package/dist/lib/sync-core/flag.d.ts +1 -1
  110. package/dist/lib/sync-core/flag.js +1 -1
  111. package/dist/lib/sync-core/reconcile.d.ts +1 -1
  112. package/dist/lib/sync-core/reconcile.js +9 -7
  113. package/dist/lib/sync-core/release.d.ts +3 -3
  114. package/dist/lib/sync-core/release.js +4 -4
  115. package/dist/lib/sync-core/rollout-state.d.ts +1 -0
  116. package/dist/lib/sync-core/rollout-state.js +1 -0
  117. package/package.json +8 -8
@@ -0,0 +1,137 @@
1
+ /**
2
+ * On-demand hot-channel sync (US-004): `hq sources sync`.
3
+ *
4
+ * Joins the server's hot-channel catalog with the person's usable connections
5
+ * and the adapters this build registers, then runs the shared loop through
6
+ * `runHotChannelSync` with trigger `on-demand`. Each poll adapter runs once
7
+ * per usable connection, with its cursor keyed per connection. A channel whose
8
+ * token comes from the HQ vault (meetings) runs once with no connection.
9
+ *
10
+ * Network access is injected (`deps`), so this module holds no HTTP code and
11
+ * never sees an integration token: adapters call tools through a gateway
12
+ * handle bound to one connection. Writes land only under `sources/` (cursors,
13
+ * landed documents, the run ledger).
14
+ */
15
+ import type { CatchUpWindow, HotTransport, SuggestionRule, TokenScope } from "./contract.js";
16
+ import { type RunLedgerEntry, type RunTrigger, type SignalsGeneratedFn } from "./ledger.js";
17
+ import type { HotChannelRegistry } from "./registry.js";
18
+ /** One item of the catalog API's top-level `hotChannels` array (hq-pro US-002). */
19
+ export interface HotChannelCatalogItem {
20
+ entryId?: string;
21
+ provider: string;
22
+ displayName?: string;
23
+ domain?: string;
24
+ flag?: string;
25
+ hotChannel: {
26
+ transport: HotTransport;
27
+ hotEventTypes: readonly string[];
28
+ catchUpWindow: CatchUpWindow;
29
+ tokenScope: TokenScope;
30
+ suggestionRule: SuggestionRule;
31
+ };
32
+ }
33
+ /** The fields of a usable connection the sync needs (a subset of `UsableApp`). */
34
+ export interface HotChannelApp {
35
+ connectionId: string;
36
+ name: string;
37
+ provider: string;
38
+ status: string;
39
+ /**
40
+ * True when the signed-in person created this connection (server-computed
41
+ * `viewerAccess.isCreator`). Unknown counts as not theirs.
42
+ */
43
+ ownedByCaller?: boolean;
44
+ }
45
+ /** Token-scope providers served from the HQ vault: no external connection. */
46
+ /**
47
+ * The bare, lowercased provider id used to match a connection against a
48
+ * catalog provider or an adapter's `companionProviders`. Factory installs store
49
+ * `factory:{slug}` (CLI installs `factory:cli:{slug}`); Slack stores `slack`.
50
+ */
51
+ export declare function hotChannelProviderKey(provider: string): string;
52
+ /** Usable meeting-recorder connections this person created, for a companion-fed adapter. */
53
+ export declare function companionApps(apps: readonly HotChannelApp[], companionProviders: readonly string[]): HotChannelApp[];
54
+ export declare const VAULT_TOKEN_PROVIDERS: ReadonlySet<string>;
55
+ export declare const UNAVAILABLE_ADAPTER_MESSAGE = "adapter not available in this hq version";
56
+ export type ReconnectReason = "signed-out" | "revoked";
57
+ /**
58
+ * A gateway call failed because the HQ session is gone or the connection's
59
+ * token was revoked. The poll page that raised it is not landed, so the cursor
60
+ * stays where it was.
61
+ */
62
+ export declare class HotChannelReconnectError extends Error {
63
+ readonly connectionId: string;
64
+ readonly provider: string;
65
+ readonly appName: string;
66
+ readonly reason: ReconnectReason;
67
+ readonly code = "HOT_CHANNEL_RECONNECT_REQUIRED";
68
+ constructor(connectionId: string, provider: string, appName: string, reason: ReconnectReason);
69
+ }
70
+ export declare class UnknownSyncAdapterError extends Error {
71
+ readonly adapter: string;
72
+ readonly valid: readonly string[];
73
+ readonly code = "HOT_CHANNEL_UNKNOWN_SYNC_ADAPTER";
74
+ constructor(adapter: string, valid: readonly string[]);
75
+ }
76
+ export interface OnDemandSyncDeps {
77
+ /** The catalog's `hotChannels`; null when the server does not publish them. */
78
+ fetchHotChannels(): Promise<HotChannelCatalogItem[] | null>;
79
+ /** Connections the person created or was granted and may use now. */
80
+ fetchUsableApps(): Promise<HotChannelApp[]>;
81
+ /** Call one tool on one connection through the HQ gateway. */
82
+ callTool(app: HotChannelApp, tool: string, args: Record<string, unknown>): Promise<unknown>;
83
+ }
84
+ export interface OnDemandSyncInput {
85
+ deps: OnDemandSyncDeps;
86
+ registry: HotChannelRegistry;
87
+ vaultRoot: string;
88
+ personUid: string;
89
+ personKey: string;
90
+ /** `--adapter`: a catalog provider or a registered adapter id. */
91
+ adapter?: string;
92
+ now?: () => Date;
93
+ runId?: string;
94
+ maxPagesPerAdapter?: number;
95
+ /** `scheduled` when the bot's hot-signal-sync job runs it; `on-demand` by default. */
96
+ trigger?: RunTrigger;
97
+ /** A scheduled run that fired late, after a missed window. */
98
+ catchUp?: boolean;
99
+ /** The signals pass over each adapter's landed sources (US-011); 0 when absent. */
100
+ signalsGenerated?: SignalsGeneratedFn;
101
+ /** Checked before each document write (sync-lock ownership); a throw stops the run. */
102
+ beforeWrite?: () => Promise<void>;
103
+ }
104
+ export type SyncOutcomeStatus = "ok" | "failed" | "unavailable" | "no-connection" | "gated";
105
+ export interface SyncOutcome {
106
+ adapter: string;
107
+ provider: string;
108
+ connectionId: string | null;
109
+ appName: string | null;
110
+ status: SyncOutcomeStatus;
111
+ landed: number;
112
+ duplicates: number;
113
+ message: string | null;
114
+ /** Set when the person must sign in again or reconnect this connection. */
115
+ reconnect?: {
116
+ reason: ReconnectReason;
117
+ prompt: string;
118
+ };
119
+ }
120
+ export interface OnDemandSyncReport {
121
+ runId: string | null;
122
+ /** True when the HQ session was gone before anything ran; nothing was written. */
123
+ signInRequired: boolean;
124
+ /** True when another sync for this person and company was running; nothing ran. */
125
+ skipped?: boolean;
126
+ outcomes: SyncOutcome[];
127
+ entries: RunLedgerEntry[];
128
+ /** Lines for the person (no channels, sign-in needed). */
129
+ notices: string[];
130
+ exitCode: 0 | 1;
131
+ }
132
+ /** The line that tells the person how to get one connection syncing again. */
133
+ export declare function reconnectPrompt(app: HotChannelApp, reason: ReconnectReason): string;
134
+ /** Refuse any path that does not resolve inside `{vaultRoot}/sources/`. */
135
+ export declare function assertInsideSources(vaultRoot: string, file: string): void;
136
+ export declare function runOnDemandSync(input: OnDemandSyncInput): Promise<OnDemandSyncReport>;
137
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1,324 @@
1
+ /**
2
+ * On-demand hot-channel sync (US-004): `hq sources sync`.
3
+ *
4
+ * Joins the server's hot-channel catalog with the person's usable connections
5
+ * and the adapters this build registers, then runs the shared loop through
6
+ * `runHotChannelSync` with trigger `on-demand`. Each poll adapter runs once
7
+ * per usable connection, with its cursor keyed per connection. A channel whose
8
+ * token comes from the HQ vault (meetings) runs once with no connection.
9
+ *
10
+ * Network access is injected (`deps`), so this module holds no HTTP code and
11
+ * never sees an integration token: adapters call tools through a gateway
12
+ * handle bound to one connection. Writes land only under `sources/` (cursors,
13
+ * landed documents, the run ledger).
14
+ */
15
+ import * as path from "node:path";
16
+ import { isAuthError } from "../../utils/auth-error.js";
17
+ import { bareProvider } from "../integrations/provider-slug.js";
18
+ import { createVaultCursorStore } from "./cursors.js";
19
+ import { cursorFilePath } from "./cursors.js";
20
+ import { runHotChannelSync } from "./ledger.js";
21
+ /** Token-scope providers served from the HQ vault: no external connection. */
22
+ /**
23
+ * The bare, lowercased provider id used to match a connection against a
24
+ * catalog provider or an adapter's `companionProviders`. Factory installs store
25
+ * `factory:{slug}` (CLI installs `factory:cli:{slug}`); Slack stores `slack`.
26
+ */
27
+ export function hotChannelProviderKey(provider) {
28
+ return provider.trim().toLowerCase().replace(/^factory:(cli:)?/, "");
29
+ }
30
+ /** Usable meeting-recorder connections this person created, for a companion-fed adapter. */
31
+ export function companionApps(apps, companionProviders) {
32
+ const names = new Set(companionProviders.map(hotChannelProviderKey));
33
+ // Meetings are personal: only connections this person created, never a
34
+ // teammate's company-scope Fireflies or Granola.
35
+ return apps.filter((app) => names.has(hotChannelProviderKey(app.provider)) && app.status !== "needs-attention" && app.ownedByCaller === true);
36
+ }
37
+ export const VAULT_TOKEN_PROVIDERS = new Set(["hq-vault"]);
38
+ export const UNAVAILABLE_ADAPTER_MESSAGE = "adapter not available in this hq version";
39
+ /**
40
+ * A gateway call failed because the HQ session is gone or the connection's
41
+ * token was revoked. The poll page that raised it is not landed, so the cursor
42
+ * stays where it was.
43
+ */
44
+ export class HotChannelReconnectError extends Error {
45
+ connectionId;
46
+ provider;
47
+ appName;
48
+ reason;
49
+ code = "HOT_CHANNEL_RECONNECT_REQUIRED";
50
+ constructor(connectionId, provider, appName, reason) {
51
+ super(reason === "signed-out"
52
+ ? `HQ session expired while syncing ${appName} (${connectionId})`
53
+ : `${appName} connection ${connectionId} needs to be reconnected`);
54
+ this.connectionId = connectionId;
55
+ this.provider = provider;
56
+ this.appName = appName;
57
+ this.reason = reason;
58
+ this.name = "HotChannelReconnectError";
59
+ }
60
+ }
61
+ export class UnknownSyncAdapterError extends Error {
62
+ adapter;
63
+ valid;
64
+ code = "HOT_CHANNEL_UNKNOWN_SYNC_ADAPTER";
65
+ constructor(adapter, valid) {
66
+ super(valid.length > 0
67
+ ? `Unknown adapter '${adapter}'. Valid adapters: ${valid.join(", ")}.`
68
+ : `Unknown adapter '${adapter}'. No hot channels are available.`);
69
+ this.adapter = adapter;
70
+ this.valid = valid;
71
+ this.name = "UnknownSyncAdapterError";
72
+ }
73
+ }
74
+ const SIGN_IN_PROMPT = "Your HQ session has expired or you're not signed in. Run `hq login`, then `hq sources sync` again.";
75
+ const REVOKED_PATTERN = /revoked|reconnect|invalid_grant|token (has )?expired|OAUTH_REQUIRED/i;
76
+ function statusOf(err) {
77
+ const status = err?.status;
78
+ return typeof status === "number" ? status : undefined;
79
+ }
80
+ function isSignedOut(err) {
81
+ return isAuthError(err) || statusOf(err) === 401;
82
+ }
83
+ function isRevoked(err) {
84
+ if (statusOf(err) === 403)
85
+ return true;
86
+ const code = err?.code;
87
+ if (typeof code === "string" && REVOKED_PATTERN.test(code))
88
+ return true;
89
+ return err instanceof Error && REVOKED_PATTERN.test(err.message);
90
+ }
91
+ /** The line that tells the person how to get one connection syncing again. */
92
+ export function reconnectPrompt(app, reason) {
93
+ if (reason === "signed-out") {
94
+ return `Your HQ session expired while syncing ${app.name} (${app.connectionId}). Run \`hq login\`, then \`hq sources sync\` again.`;
95
+ }
96
+ return `Reconnect ${app.name} (${app.connectionId}): hq integrations connect ${bareProvider(app.provider)}`;
97
+ }
98
+ /**
99
+ * Gated adapters (US-013) whose flag is off for this company. hq-pro lists a
100
+ * gated channel in the catalog only while its flag is on, so a registered
101
+ * adapter with a `gate` that no catalog item resolves to is gated off. These
102
+ * are reported but never planned: no fetch, no cursor and no ledger line.
103
+ */
104
+ function gatedOutcomes(registry, resolvedIds, wanted) {
105
+ const out = [];
106
+ for (const adapter of registry.list()) {
107
+ if (!adapter.gate || resolvedIds.has(adapter.id))
108
+ continue;
109
+ const provider = adapter.tokenScope.provider;
110
+ if (wanted !== undefined && adapter.id !== wanted && provider.toLowerCase() !== wanted)
111
+ continue;
112
+ out.push({
113
+ adapter: adapter.id,
114
+ provider: provider || adapter.id,
115
+ connectionId: null,
116
+ appName: null,
117
+ status: "gated",
118
+ landed: 0,
119
+ duplicates: 0,
120
+ message: `${adapter.id} is not enabled for this company (flag ${adapter.gate.flag} is off)`,
121
+ });
122
+ }
123
+ return out;
124
+ }
125
+ function resolveAdapter(registry, item) {
126
+ for (const key of [item.hotChannel.tokenScope.provider, item.provider]) {
127
+ try {
128
+ return { adapter: registry.resolve({ provider: key }), key };
129
+ }
130
+ catch {
131
+ // Not registered under this key; try the next one.
132
+ }
133
+ }
134
+ return null;
135
+ }
136
+ /**
137
+ * The gateway handle a poll adapter gets for one connection. Auth failures
138
+ * become `HotChannelReconnectError`, which the loop records on the run result
139
+ * before any cursor moves.
140
+ */
141
+ function gatewayFor(app, deps, reconnects) {
142
+ return {
143
+ connectionId: app.connectionId,
144
+ provider: bareProvider(app.provider),
145
+ async call(tool, args) {
146
+ try {
147
+ return await deps.callTool(app, tool, args);
148
+ }
149
+ catch (err) {
150
+ const reason = isSignedOut(err) ? "signed-out" : isRevoked(err) ? "revoked" : null;
151
+ if (reason === null)
152
+ throw err;
153
+ reconnects.set(app.connectionId, reason);
154
+ throw new HotChannelReconnectError(app.connectionId, bareProvider(app.provider), app.name, reason);
155
+ }
156
+ },
157
+ };
158
+ }
159
+ /** Refuse any path that does not resolve inside `{vaultRoot}/sources/`. */
160
+ export function assertInsideSources(vaultRoot, file) {
161
+ const root = path.resolve(vaultRoot, "sources") + path.sep;
162
+ if (!path.resolve(file).startsWith(root)) {
163
+ throw new Error(`hq sources sync only writes under sources/ (refused ${file})`);
164
+ }
165
+ }
166
+ /** The vault cursor store, with every write checked against `sources/`. */
167
+ function sourcesOnlyCursorStore(vaultRoot, now) {
168
+ const store = createVaultCursorStore(vaultRoot, now ? { now } : {});
169
+ return {
170
+ get: (personKey, key) => store.get(personKey, key),
171
+ async set(personKey, key, cursor, lastEventKey) {
172
+ assertInsideSources(vaultRoot, cursorFilePath(vaultRoot, personKey, key));
173
+ await store.set(personKey, key, cursor, lastEventKey);
174
+ },
175
+ };
176
+ }
177
+ export async function runOnDemandSync(input) {
178
+ const report = {
179
+ runId: null,
180
+ signInRequired: false,
181
+ outcomes: [],
182
+ entries: [],
183
+ notices: [],
184
+ exitCode: 0,
185
+ };
186
+ let items;
187
+ let apps;
188
+ try {
189
+ [items, apps] = await Promise.all([input.deps.fetchHotChannels(), input.deps.fetchUsableApps()]);
190
+ }
191
+ catch (err) {
192
+ if (!isSignedOut(err))
193
+ throw err;
194
+ report.signInRequired = true;
195
+ report.notices.push(SIGN_IN_PROMPT);
196
+ report.exitCode = 1;
197
+ return report;
198
+ }
199
+ const wanted = input.adapter?.toLowerCase();
200
+ if (!items || items.length === 0) {
201
+ const gated = gatedOutcomes(input.registry, new Set(), wanted);
202
+ if (wanted !== undefined && gated.length === 0)
203
+ throw new UnknownSyncAdapterError(input.adapter, []);
204
+ if (wanted === undefined)
205
+ report.notices.push("No hot channels are available for this company.");
206
+ report.outcomes.push(...gated);
207
+ return report;
208
+ }
209
+ const resolved = items.map((item) => ({ item, hit: resolveAdapter(input.registry, item) }));
210
+ const gated = gatedOutcomes(input.registry, new Set(resolved.flatMap(({ hit }) => (hit ? [hit.adapter.id] : []))), wanted);
211
+ if (wanted !== undefined && gated.length === 0) {
212
+ const valid = new Set();
213
+ for (const { item, hit } of resolved) {
214
+ valid.add(item.provider.toLowerCase());
215
+ if (hit)
216
+ valid.add(hit.adapter.id);
217
+ }
218
+ if (!valid.has(wanted))
219
+ throw new UnknownSyncAdapterError(input.adapter, [...valid].sort());
220
+ }
221
+ const selected = resolved.filter(({ item, hit }) => wanted === undefined || item.provider.toLowerCase() === wanted || hit?.adapter.id === wanted);
222
+ const planned = [];
223
+ const skipped = [];
224
+ for (const { item, hit } of selected) {
225
+ const blank = { connectionId: null, appName: null, landed: 0, duplicates: 0 };
226
+ if (!hit) {
227
+ skipped.push({
228
+ ...blank,
229
+ adapter: item.provider,
230
+ provider: item.provider,
231
+ status: "unavailable",
232
+ message: UNAVAILABLE_ADAPTER_MESSAGE,
233
+ });
234
+ continue;
235
+ }
236
+ const scopeProvider = item.hotChannel.tokenScope.provider.toLowerCase();
237
+ if (VAULT_TOKEN_PROVIDERS.has(scopeProvider)) {
238
+ const companions = companionApps(apps, hit.adapter.companionProviders ?? []);
239
+ const entry = { provider: hit.key };
240
+ if (companions.length > 0)
241
+ entry.companionConnectionIds = companions.map((app) => app.connectionId);
242
+ planned.push({ entry, item, adapter: hit.adapter, app: null, companions });
243
+ continue;
244
+ }
245
+ const names = new Set([hotChannelProviderKey(scopeProvider), hotChannelProviderKey(item.provider)]);
246
+ const matches = apps.filter((app) => names.has(hotChannelProviderKey(app.provider)));
247
+ if (matches.length === 0) {
248
+ skipped.push({
249
+ ...blank,
250
+ adapter: hit.adapter.id,
251
+ provider: item.provider,
252
+ status: "no-connection",
253
+ message: `no usable ${item.displayName ?? item.provider} connection; connect one with: hq integrations connect ${item.provider}`,
254
+ });
255
+ continue;
256
+ }
257
+ if (hit.adapter.transport === "push") {
258
+ // The push buffer is per person, not per connection: drain it once.
259
+ planned.push({ entry: { provider: hit.key }, item, adapter: hit.adapter, app: null });
260
+ continue;
261
+ }
262
+ for (const app of matches) {
263
+ planned.push({ entry: { provider: hit.key, connectionId: app.connectionId }, item, adapter: hit.adapter, app });
264
+ }
265
+ }
266
+ const reconnects = new Map();
267
+ const appsByConnection = new Map();
268
+ for (const p of planned) {
269
+ if (p.app)
270
+ appsByConnection.set(p.app.connectionId, p.app);
271
+ for (const app of p.companions ?? [])
272
+ appsByConnection.set(app.connectionId, app);
273
+ }
274
+ let results = [];
275
+ if (planned.length > 0) {
276
+ const sync = await runHotChannelSync({
277
+ trigger: input.trigger ?? "on-demand",
278
+ ...(input.catchUp === undefined ? {} : { catchUp: input.catchUp }),
279
+ ...(input.signalsGenerated ? { signalsGenerated: input.signalsGenerated } : {}),
280
+ ...(input.beforeWrite ? { beforeWrite: input.beforeWrite } : {}),
281
+ registry: input.registry,
282
+ catalog: planned.map((p) => p.entry),
283
+ vaultRoot: input.vaultRoot,
284
+ personUid: input.personUid,
285
+ personKey: input.personKey,
286
+ cursorStore: sourcesOnlyCursorStore(input.vaultRoot, input.now),
287
+ gatewayFor: (entry) => {
288
+ const app = entry.connectionId ? appsByConnection.get(entry.connectionId) : undefined;
289
+ return app ? gatewayFor(app, input.deps, reconnects) : undefined;
290
+ },
291
+ ...(input.now ? { now: input.now } : {}),
292
+ ...(input.runId ? { runId: input.runId } : {}),
293
+ ...(input.maxPagesPerAdapter ? { maxPagesPerAdapter: input.maxPagesPerAdapter } : {}),
294
+ });
295
+ report.runId = sync.runId;
296
+ report.entries = sync.entries;
297
+ results = sync.results;
298
+ }
299
+ planned.forEach((p, i) => {
300
+ const result = results[i];
301
+ const failed = !result || result.error !== undefined;
302
+ const outcome = {
303
+ adapter: p.adapter.id,
304
+ provider: p.item.provider,
305
+ connectionId: p.app?.connectionId ?? null,
306
+ appName: p.app?.name ?? null,
307
+ status: failed ? "failed" : "ok",
308
+ landed: result?.landed ?? 0,
309
+ duplicates: result?.duplicates ?? 0,
310
+ message: result?.error?.message ?? null,
311
+ };
312
+ if (failed && p.app) {
313
+ const reason = reconnects.get(p.app.connectionId) ?? (p.app.status === "needs-attention" ? "revoked" : undefined);
314
+ if (reason)
315
+ outcome.reconnect = { reason, prompt: reconnectPrompt(p.app, reason) };
316
+ }
317
+ report.outcomes.push(outcome);
318
+ });
319
+ report.outcomes.push(...skipped, ...gated);
320
+ if (report.outcomes.some((o) => o.status === "failed"))
321
+ report.exitCode = 1;
322
+ return report;
323
+ }
324
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1,150 @@
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
4
+ * `claude -p` for signals one document at a time, and writes those signals to
5
+ * `companies/{co}/signals/`. It returns the count for the run ledger.
6
+ *
7
+ * A landed document with both `payload_ref` and `raw_ref` is a pointer (the
8
+ * meetings, Slack and Attio adapters land these). For it, claude gets the
9
+ * pointer followed by the document `payload_ref` names, capped at
10
+ * MAX_PAYLOAD_BYTES. A payload outside `sources/`, missing or unreadable falls
11
+ * back to the pointer alone, and the reason is warned and kept on the signal.
12
+ *
13
+ * Billing: policy hq-no-headless-print-subscription-billing forbids headless
14
+ * `claude -p` in general. This is its allowed case, per the owner decision of
15
+ * 2026-10-21: the owner's own `claude` login, on their own machine, in a
16
+ * bounded single-turn call. Cloud Claude is not the bot brain, and no model is
17
+ * called from the poller itself. Do not reuse this pass for a shared, fleet or
18
+ * hosted identity.
19
+ */
20
+ import type { HotChannelRunResult, WriteGuard } from "./loop.js";
21
+ /** Ledger `error` values a failed pass records. */
22
+ export type SignalsPassErrorCode = "claude-logged-out" | "claude-timeout" | "claude-bad-output";
23
+ /**
24
+ * A pass failure the run ledger records as `status: failed` with `error` set
25
+ * to the code. Any other error thrown from the pass is a crash.
26
+ */
27
+ export declare class SignalsPassError extends Error {
28
+ readonly code: SignalsPassErrorCode;
29
+ constructor(code: SignalsPassErrorCode, message: string);
30
+ }
31
+ /** Signal types the pass accepts; the same type folders existing signals use. */
32
+ export declare const SIGNAL_TYPES: readonly ["action_item", "commitment", "decision", "key_point", "participant_contribution", "question", "risk", "summary"];
33
+ export type SignalType = (typeof SIGNAL_TYPES)[number];
34
+ /**
35
+ * Flags that cut the signals call off from everything but the model: no
36
+ * built-in tools, no MCP servers, and no user, project or local settings (so
37
+ * user hooks and permission rules do not apply). The documents are untrusted
38
+ * provider content, so a prompt injection must have nothing to call.
39
+ * `--safe-mode` also stops CLAUDE.md and user memory from loading, so private
40
+ * context never reaches a prompt the documents can steer. `--bare` would do the
41
+ * same but reads only ANTHROPIC_API_KEY, which breaks subscription (OAuth)
42
+ * sign-in; `--safe-mode` keeps auth working (checked on Claude Code 2.1.308).
43
+ */
44
+ export declare const CLAUDE_ISOLATION_ARGS: readonly string[];
45
+ /** Per-document `claude -p` budget (policy indigo-executors-bound-all-waits). */
46
+ export declare const DEFAULT_SIGNALS_TIMEOUT_MS = 120000;
47
+ /** Budget for the `claude auth status` preflight. */
48
+ export declare const AUTH_PREFLIGHT_TIMEOUT_MS = 10000;
49
+ /** Budget for one worklist script call. */
50
+ export declare const WORKLIST_TIMEOUT_MS = 60000;
51
+ /** Documents one pass sends to claude across all adapters of a run. */
52
+ export declare const DEFAULT_MAX_SIGNAL_DOCS = 20;
53
+ /** Payload bytes sent to claude per document (200 KB). A longer payload is cut with a marker. */
54
+ export declare const MAX_PAYLOAD_BYTES: number;
55
+ export interface BoundedRun {
56
+ code: number | null;
57
+ stdout: string;
58
+ stderr: string;
59
+ timedOut: boolean;
60
+ /** Set when the process could not start (for example ENOENT). */
61
+ spawnError?: string;
62
+ }
63
+ /**
64
+ * Run a command with a hard timeout. The child leads its own process group;
65
+ * on timeout the whole group gets SIGTERM, then SIGKILL after a short grace
66
+ * (policy indigo-subprocess-wrapping-qmd-use-killpg-on-timeout), so a node
67
+ * child of `claude` cannot outlive the call.
68
+ */
69
+ export declare function runBounded(command: string, args: readonly string[], options: {
70
+ timeoutMs: number;
71
+ input?: string;
72
+ cwd?: string;
73
+ env?: NodeJS.ProcessEnv;
74
+ }): Promise<BoundedRun>;
75
+ export interface ClaudeSignalsPassOptions {
76
+ hqRoot: string;
77
+ company: string;
78
+ /** The `claude` CLI; resolved on PATH by default. */
79
+ claudeBin?: string;
80
+ timeoutMs?: number;
81
+ authTimeoutMs?: number;
82
+ maxDocs?: number;
83
+ /** Defaults to `{hqRoot}/core/scripts/ontology-source-worklist.sh`. */
84
+ worklistScript?: string;
85
+ /** Defaults to the shipped `prompts/signals-pass.md`. */
86
+ promptFile?: string;
87
+ now?: () => Date;
88
+ env?: NodeJS.ProcessEnv;
89
+ /** Where payload fallbacks are reported. Defaults to one line on stderr. */
90
+ warn?: (message: string) => void;
91
+ }
92
+ /** Why the pass sent claude the pointer alone. */
93
+ export type PayloadFallbackReason = "payload-out-of-bounds" | "payload-missing" | "payload-unreadable";
94
+ /** What the pass sends claude for one landed document. */
95
+ export interface SignalsInput {
96
+ text: string;
97
+ /** Set when the payload was read and appended. */
98
+ payloadRef?: string;
99
+ /** Set when the document named a payload the pass could not use. */
100
+ fallback?: PayloadFallbackReason;
101
+ /** The `payload_ref` value behind a fallback. */
102
+ fallbackRef?: string;
103
+ }
104
+ interface WorkItem {
105
+ /** Vault-relative landed document, `sources/{channel}/{id}.md`. */
106
+ file: string;
107
+ channel: string;
108
+ principals: string[];
109
+ audienceKey: string;
110
+ audienceSource: string;
111
+ /** True when the worklist script listed the item and can mark it done. */
112
+ fromWorklist: boolean;
113
+ }
114
+ interface ParsedSignal {
115
+ type: SignalType;
116
+ title: string;
117
+ body: string;
118
+ }
119
+ export declare function defaultPromptFile(): string;
120
+ /**
121
+ * The text claude gets for one landed document. A pointer (both `payload_ref`
122
+ * and `raw_ref` set) gets its payload appended after a separator line. Any
123
+ * other document is sent as landed: without `raw_ref`, `payload_ref` names the
124
+ * document's own `.raw.json` sidecar and the body is the content.
125
+ */
126
+ export declare function buildSignalsInput(vaultRoot: string, file: string): Promise<SignalsInput>;
127
+ /**
128
+ * Parse `claude -p --output-format json` output into signals. The envelope's
129
+ * `result` must itself be one JSON object, optionally inside a single code
130
+ * fence; nothing is scraped out of prose.
131
+ */
132
+ export declare function parseClaudeSignals(stdout: string): ParsedSignal[];
133
+ /** `sha256(source_ref \n type \n title)`: the same signal from the same source keeps its id. */
134
+ export declare function signalId(sourceRef: string, type: string, title: string): string;
135
+ /**
136
+ * Vault-relative directory for one signal type. Company-wide signals keep the
137
+ * existing `signals/{type}/` layout; person-scoped signals live under
138
+ * `signals/@{audienceKey}/{type}/`.
139
+ */
140
+ export declare function signalTypeDir(key: string, type: SignalType): string;
141
+ /** Write `signals/@{key}/_audience.yaml` once, when the partition is first created. */
142
+ export declare function ensureAudienceFile(vaultRoot: string, item: Pick<WorkItem, "audienceKey" | "principals" | "audienceSource">): Promise<void>;
143
+ /**
144
+ * Build the claude signals pass for one company. The pass keeps state for one
145
+ * run: the auth preflight runs once, and the document cap counts across every
146
+ * adapter of the run, so create a new pass per run.
147
+ */
148
+ export declare function createClaudeSignalsPass(options: ClaudeSignalsPassOptions): (result: HotChannelRunResult, guard?: WriteGuard) => Promise<number>;
149
+ export {};
150
+ //# sourceMappingURL=signals-pass.d.ts.map