@indigoai-us/hq-cli 5.345.191 → 5.345.193
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 +398 -0
- package/assets/bot-workers/setup/worker.yaml +91 -34
- package/dist/command-catalog.generated.d.ts +96 -19
- package/dist/command-catalog.generated.js +124 -21
- package/dist/commands/bot-jobs.d.ts +21 -18
- package/dist/commands/bot-jobs.js +127 -39
- package/dist/commands/bot.d.ts +4 -3
- package/dist/commands/bot.js +265 -140
- package/dist/commands/core-jobs-schedule.js +10 -0
- package/dist/commands/daemon.js +37 -0
- package/dist/commands/files.d.ts +41 -6
- package/dist/commands/files.js +108 -27
- package/dist/commands/pack-start-bot.d.ts +4 -6
- package/dist/commands/pack-start-bot.js +4 -8
- package/dist/commands/signals-run.d.ts +45 -0
- package/dist/commands/signals-run.js +175 -0
- package/dist/commands/signals.d.ts +1 -0
- package/dist/commands/signals.js +3 -0
- package/dist/commands/sources.d.ts +11 -0
- package/dist/commands/sources.js +137 -25
- package/dist/commands/staff/reset-test-account.d.ts +10 -0
- package/dist/commands/staff/reset-test-account.js +18 -2
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +31171 -52512
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/bot/api.d.ts +3 -2
- package/dist/lib/bot/config.d.ts +2 -0
- package/dist/lib/bot/fleet-cron.d.ts +121 -0
- package/dist/lib/bot/fleet-cron.js +319 -0
- package/dist/lib/bot/fleet-local.d.ts +93 -1
- package/dist/lib/bot/fleet-local.js +546 -34
- package/dist/lib/bot/fleet-owner-companies.d.ts +12 -0
- package/dist/lib/bot/fleet-owner-companies.js +30 -0
- package/dist/lib/bot/fleet-watch.d.ts +25 -0
- package/dist/lib/bot/fleet-watch.js +51 -0
- package/dist/lib/bot/index.d.ts +1 -1
- package/dist/lib/bot/index.js +1 -1
- package/dist/lib/bot/job-run.d.ts +5 -4
- package/dist/lib/bot/job-run.js +5 -4
- package/dist/lib/bot/jobs/hot-signal-sync.d.ts +57 -3
- package/dist/lib/bot/jobs/hot-signal-sync.js +163 -4
- package/dist/lib/bot/local-jobs.d.ts +13 -2
- package/dist/lib/bot/local-jobs.js +17 -4
- package/dist/lib/bot/progress.d.ts +14 -0
- package/dist/lib/bot/progress.js +47 -1
- package/dist/lib/bot/run.d.ts +55 -2
- package/dist/lib/bot/run.js +192 -10
- package/dist/lib/bot/setup-tool-offer.d.ts +1 -0
- package/dist/lib/bot/setup-tool-offer.js +16 -0
- package/dist/lib/bot/signals-engine-flag.d.ts +18 -0
- package/dist/lib/bot/signals-engine-flag.js +30 -0
- package/dist/lib/daemon/catalog.d.ts +4 -0
- package/dist/lib/daemon/catalog.js +29 -9
- package/dist/lib/daemon/legacy.d.ts +6 -0
- package/dist/lib/daemon/legacy.js +16 -10
- package/dist/lib/daemon/run.d.ts +1 -0
- package/dist/lib/daemon/run.js +10 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/flag-registry.d.ts +13 -1
- package/dist/lib/flag-registry.js +46 -0
- package/dist/lib/hot-channels/adapters/attio.d.ts +75 -2
- package/dist/lib/hot-channels/adapters/attio.js +367 -29
- package/dist/lib/hot-channels/adapters/gmail.d.ts +14 -0
- package/dist/lib/hot-channels/adapters/gmail.js +59 -2
- package/dist/lib/hot-channels/adapters/linear.d.ts +24 -0
- package/dist/lib/hot-channels/adapters/linear.js +71 -1
- package/dist/lib/hot-channels/adapters/meetings.js +4 -1
- package/dist/lib/hot-channels/adapters/slack.js +7 -3
- package/dist/lib/hot-channels/alert.js +1 -1
- package/dist/lib/hot-channels/contract.d.ts +11 -0
- package/dist/lib/hot-channels/contract.js +1 -0
- package/dist/lib/hot-channels/cursors.js +1 -1
- package/dist/lib/hot-channels/landing.d.ts +6 -2
- package/dist/lib/hot-channels/landing.js +13 -6
- package/dist/lib/hot-channels/ledger.js +2 -0
- package/dist/lib/hot-channels/loop.js +18 -4
- package/dist/lib/hot-channels/registry.js +9 -1
- package/dist/lib/hot-channels/run.d.ts +18 -0
- package/dist/lib/hot-channels/run.js +15 -4
- package/dist/lib/hot-channels/signals-pass.d.ts +1 -0
- package/dist/lib/hot-channels/signals-pass.js +7 -5
- package/dist/lib/hot-channels/sync-lock.js +22 -6
- package/dist/lib/mesh/live/daemon/install.d.ts +22 -0
- package/dist/lib/mesh/live/daemon/install.js +117 -30
- package/dist/lib/signals/adapters-catalog.d.ts +77 -0
- package/dist/lib/signals/adapters-catalog.js +194 -0
- package/dist/lib/signals/engine.d.ts +196 -0
- package/dist/lib/signals/engine.js +495 -0
- package/dist/lib/signals/failure-alert.d.ts +89 -0
- package/dist/lib/signals/failure-alert.js +175 -0
- package/dist/lib/signals/local-date.d.ts +3 -0
- package/dist/lib/signals/local-date.js +6 -0
- package/dist/lib/signals/projection.d.ts +42 -0
- package/dist/lib/signals/projection.js +67 -0
- package/dist/lib/signals/reply.d.ts +112 -0
- package/dist/lib/signals/reply.js +368 -0
- package/dist/lib/signals/run-lock.d.ts +36 -0
- package/dist/lib/signals/run-lock.js +148 -0
- package/dist/lib/signals/run-record.d.ts +24 -0
- package/dist/lib/signals/run-record.js +82 -0
- package/dist/lib/signals/sanitize.d.ts +105 -0
- package/dist/lib/signals/sanitize.js +439 -0
- package/dist/lib/signals/settings.d.ts +10 -0
- package/dist/lib/signals/settings.js +48 -0
- package/dist/lib/signals/steps/adapters.d.ts +14 -0
- package/dist/lib/signals/steps/adapters.js +156 -0
- package/dist/lib/signals/steps/entity-similarity.d.ts +10 -0
- package/dist/lib/signals/steps/entity-similarity.js +47 -0
- package/dist/lib/signals/steps/extract.d.ts +113 -0
- package/dist/lib/signals/steps/extract.js +765 -0
- package/dist/lib/signals/steps/garden.d.ts +24 -0
- package/dist/lib/signals/steps/garden.js +250 -0
- package/dist/lib/signals/steps/types.d.ts +112 -0
- package/dist/lib/signals/steps/types.js +4 -0
- package/dist/lib/signals/steps/upload.d.ts +43 -0
- package/dist/lib/signals/steps/upload.js +324 -0
- package/dist/lib/signals/write-guard.d.ts +63 -0
- package/dist/lib/signals/write-guard.js +287 -0
- package/dist/utils/resident-package-preload.js +16 -0
- package/package.json +9 -9
- package/dist/lib/bot/fleet-default-flag.d.ts +0 -31
- package/dist/lib/bot/fleet-default-flag.js +0 -127
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signals-failure alert for the `hq signals run` engine (Hot-Signal Sync
|
|
3
|
+
* US-018).
|
|
4
|
+
*
|
|
5
|
+
* The engine's run record already counts what each step took in and put out.
|
|
6
|
+
* A run whose extract step read sources and produced no candidates is a
|
|
7
|
+
* silent stall, the shape of the 2026-10-14 stall. Only sources extract reads
|
|
8
|
+
* count: hot-channel landings (Slack, Linear, Gmail) that no declared source
|
|
9
|
+
* channel covers never reach extract, so they cannot show a stall. This module
|
|
10
|
+
* checks the run record for that shape and hands the failure to the existing
|
|
11
|
+
* ledger alert path (`raiseSignalsFailureAlerts` in
|
|
12
|
+
* `src/lib/hot-channels/alert.ts`), so the alert text, the owner DM and the
|
|
13
|
+
* hourly dedupe in `sources/_runs/@{personKey}/_alerts.json` are the same ones
|
|
14
|
+
* the hot-channel sync uses. No new posting path is added here.
|
|
15
|
+
*/
|
|
16
|
+
import { type RaiseSignalsFailureAlertsResult } from "../hot-channels/alert.js";
|
|
17
|
+
import type { RunLedgerEntry } from "../hot-channels/ledger.js";
|
|
18
|
+
import type { RunRecord, StepRecord } from "./engine.js";
|
|
19
|
+
/**
|
|
20
|
+
* Extract skips that are not a stall: no source channel to read, or the
|
|
21
|
+
* ontology worker is not installed. Both are reported in the run record
|
|
22
|
+
* already, and an hourly alert would only repeat them.
|
|
23
|
+
*/
|
|
24
|
+
export declare const EXTRACT_SKIPS_WITHOUT_ALERT: ReadonlySet<string>;
|
|
25
|
+
/** The adapter name the engine's alert uses in the shared dedupe file: one key per company vault. */
|
|
26
|
+
export declare const SIGNALS_RUN_ALERT_ADAPTER = "signals-run";
|
|
27
|
+
/** Who the alert is for, and the key of their dedupe file. */
|
|
28
|
+
export interface SignalsAlertPerson {
|
|
29
|
+
/** The audience key of the person's `sources/_runs/@{personKey}/` folder. */
|
|
30
|
+
personKey: string;
|
|
31
|
+
/** Shown in the alert text; usually the person's email. */
|
|
32
|
+
personName?: string;
|
|
33
|
+
/** The Cognito sub, recorded on the ledger line the alert is built from. */
|
|
34
|
+
personUid?: string;
|
|
35
|
+
}
|
|
36
|
+
/** Where the engine sends a signals-failure alert. */
|
|
37
|
+
export interface SignalsRunAlertSenders {
|
|
38
|
+
/** The primary alert to the owner; throws when it was not delivered. */
|
|
39
|
+
sendOwnerDm: (text: string) => Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* The person whose dedupe file is used. Defaults to the signed-in person
|
|
42
|
+
* (the same key `hq sources sync` uses). Null skips the alert.
|
|
43
|
+
*/
|
|
44
|
+
resolvePerson?: (company: string) => SignalsAlertPerson | null | Promise<SignalsAlertPerson | null>;
|
|
45
|
+
log?: (message: string) => void;
|
|
46
|
+
/**
|
|
47
|
+
* False for a sender no one reads later, such as the terminal of a manual
|
|
48
|
+
* `hq signals run`. Its alert neither reads nor writes the hourly dedupe, so
|
|
49
|
+
* it never suppresses the owner DM the bot sends for the same company.
|
|
50
|
+
* Default true.
|
|
51
|
+
*/
|
|
52
|
+
recordsDelivery?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/** The signed-in person, as `runCompanyHotChannelSync` resolves them; null when no one is signed in. */
|
|
55
|
+
export declare function resolveSignedInAlertPerson(): SignalsAlertPerson | null;
|
|
56
|
+
/** Source items the extract step read: its worklist items minus the ones the worklist skipped. */
|
|
57
|
+
export declare function extractSourcesRead(extract: StepRecord): number;
|
|
58
|
+
/**
|
|
59
|
+
* True when a run's sources produced no signals. The run must be real (not a
|
|
60
|
+
* dry run, not a repeat of an earlier job run) and have both an adapters and an
|
|
61
|
+
* extract step (one-step runs and late starts are not judged), and extract
|
|
62
|
+
* must have produced no candidates. Then either:
|
|
63
|
+
* - extract ran and read at least one source item, or
|
|
64
|
+
* - extract did not finish (failed, or skipped for a reason other than
|
|
65
|
+
* `no-sources` / `ontology-worker-missing`) after the adapters step landed
|
|
66
|
+
* new sources.
|
|
67
|
+
* Hot-channel landings extract never reads do not count on their own, so a
|
|
68
|
+
* day with Slack activity and no declared-source items is not a stall.
|
|
69
|
+
*/
|
|
70
|
+
export declare function isSignalsRunFailure(record: RunRecord): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* The run record as one ledger line, so the existing alert path formats,
|
|
73
|
+
* sends and dedupes it. `sourcesLanded` is the source items extract read (or,
|
|
74
|
+
* when extract read none, what the adapters step landed) and
|
|
75
|
+
* `signalsGenerated` is what extract produced.
|
|
76
|
+
*/
|
|
77
|
+
export declare function signalsRunLedgerLine(record: RunRecord, person: SignalsAlertPerson): RunLedgerEntry;
|
|
78
|
+
/**
|
|
79
|
+
* Alert the owner for each run record that landed sources and produced no
|
|
80
|
+
* signals. Never throws; a failed send is logged and left unrecorded, so the
|
|
81
|
+
* next run tries again. Returns the alert result per company that alerted.
|
|
82
|
+
*/
|
|
83
|
+
export declare function raiseSignalsRunAlerts(input: {
|
|
84
|
+
hqRoot: string;
|
|
85
|
+
records: readonly RunRecord[];
|
|
86
|
+
senders: SignalsRunAlertSenders;
|
|
87
|
+
now: () => Date;
|
|
88
|
+
}): Promise<Record<string, RaiseSignalsFailureAlertsResult>>;
|
|
89
|
+
//# sourceMappingURL=failure-alert.d.ts.map
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signals-failure alert for the `hq signals run` engine (Hot-Signal Sync
|
|
3
|
+
* US-018).
|
|
4
|
+
*
|
|
5
|
+
* The engine's run record already counts what each step took in and put out.
|
|
6
|
+
* A run whose extract step read sources and produced no candidates is a
|
|
7
|
+
* silent stall, the shape of the 2026-10-14 stall. Only sources extract reads
|
|
8
|
+
* count: hot-channel landings (Slack, Linear, Gmail) that no declared source
|
|
9
|
+
* channel covers never reach extract, so they cannot show a stall. This module
|
|
10
|
+
* checks the run record for that shape and hands the failure to the existing
|
|
11
|
+
* ledger alert path (`raiseSignalsFailureAlerts` in
|
|
12
|
+
* `src/lib/hot-channels/alert.ts`), so the alert text, the owner DM and the
|
|
13
|
+
* hourly dedupe in `sources/_runs/@{personKey}/_alerts.json` are the same ones
|
|
14
|
+
* the hot-channel sync uses. No new posting path is added here.
|
|
15
|
+
*/
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import { audienceKey } from "../hot-channels/audience.js";
|
|
18
|
+
import { findSignalsFailures, formatSignalsFailureAlert, raiseSignalsFailureAlerts, signalsAlertKey, signalsAlertStatePath, } from "../hot-channels/alert.js";
|
|
19
|
+
import { assertAllowedPath } from "./write-guard.js";
|
|
20
|
+
import { resolveUploadAuthor } from "../../utils/upload-author.js";
|
|
21
|
+
/**
|
|
22
|
+
* Extract skips that are not a stall: no source channel to read, or the
|
|
23
|
+
* ontology worker is not installed. Both are reported in the run record
|
|
24
|
+
* already, and an hourly alert would only repeat them.
|
|
25
|
+
*/
|
|
26
|
+
export const EXTRACT_SKIPS_WITHOUT_ALERT = new Set(["no-sources", "ontology-worker-missing"]);
|
|
27
|
+
/** The adapter name the engine's alert uses in the shared dedupe file: one key per company vault. */
|
|
28
|
+
export const SIGNALS_RUN_ALERT_ADAPTER = "signals-run";
|
|
29
|
+
/** The signed-in person, as `runCompanyHotChannelSync` resolves them; null when no one is signed in. */
|
|
30
|
+
export function resolveSignedInAlertPerson() {
|
|
31
|
+
const author = resolveUploadAuthor();
|
|
32
|
+
if (!author)
|
|
33
|
+
return null;
|
|
34
|
+
return { personKey: audienceKey([author.email]), personName: author.email, personUid: author.userSub };
|
|
35
|
+
}
|
|
36
|
+
function firstStep(record, name) {
|
|
37
|
+
return record.steps.find((s) => s.name === name);
|
|
38
|
+
}
|
|
39
|
+
/** Source items the extract step read: its worklist items minus the ones the worklist skipped. */
|
|
40
|
+
export function extractSourcesRead(extract) {
|
|
41
|
+
return Math.max(0, extract.itemsIn - (extract.skippedItems?.length ?? 0));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* True when a run's sources produced no signals. The run must be real (not a
|
|
45
|
+
* dry run, not a repeat of an earlier job run) and have both an adapters and an
|
|
46
|
+
* extract step (one-step runs and late starts are not judged), and extract
|
|
47
|
+
* must have produced no candidates. Then either:
|
|
48
|
+
* - extract ran and read at least one source item, or
|
|
49
|
+
* - extract did not finish (failed, or skipped for a reason other than
|
|
50
|
+
* `no-sources` / `ontology-worker-missing`) after the adapters step landed
|
|
51
|
+
* new sources.
|
|
52
|
+
* Hot-channel landings extract never reads do not count on their own, so a
|
|
53
|
+
* day with Slack activity and no declared-source items is not a stall.
|
|
54
|
+
*/
|
|
55
|
+
export function isSignalsRunFailure(record) {
|
|
56
|
+
if (record.dryRun || record.alreadyRanToday)
|
|
57
|
+
return false;
|
|
58
|
+
const adapters = firstStep(record, "adapters");
|
|
59
|
+
const extract = firstStep(record, "extract");
|
|
60
|
+
if (!adapters || !extract)
|
|
61
|
+
return false;
|
|
62
|
+
if (extract.itemsOut !== 0)
|
|
63
|
+
return false;
|
|
64
|
+
if (extract.status === "skipped" && extract.skippedReason && EXTRACT_SKIPS_WITHOUT_ALERT.has(extract.skippedReason)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (extractSourcesRead(extract) > 0)
|
|
68
|
+
return true;
|
|
69
|
+
// extract read nothing. Only a broken extract after new landings is a stall;
|
|
70
|
+
// adapters itemsOut is what landed (itemsIn also counts duplicates).
|
|
71
|
+
return (extract.status === "failed" || extract.status === "skipped") && adapters.itemsOut > 0;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The run record as one ledger line, so the existing alert path formats,
|
|
75
|
+
* sends and dedupes it. `sourcesLanded` is the source items extract read (or,
|
|
76
|
+
* when extract read none, what the adapters step landed) and
|
|
77
|
+
* `signalsGenerated` is what extract produced.
|
|
78
|
+
*/
|
|
79
|
+
export function signalsRunLedgerLine(record, person) {
|
|
80
|
+
const adapters = firstStep(record, "adapters");
|
|
81
|
+
const extract = firstStep(record, "extract");
|
|
82
|
+
const extractNote = extract?.error ?? (extract?.skippedReason ? `extract skipped: ${extract.skippedReason}` : null);
|
|
83
|
+
return {
|
|
84
|
+
runId: `signals-run ${record.company} ${record.startedAt}`,
|
|
85
|
+
trigger: record.trigger === "job" ? "scheduled" : "on-demand",
|
|
86
|
+
adapter: SIGNALS_RUN_ALERT_ADAPTER,
|
|
87
|
+
connectionId: null,
|
|
88
|
+
sourcesLanded: extract && extractSourcesRead(extract) > 0 ? extractSourcesRead(extract) : (adapters?.itemsOut ?? 0),
|
|
89
|
+
signalsGenerated: extract?.itemsOut ?? 0,
|
|
90
|
+
startedAt: record.startedAt,
|
|
91
|
+
finishedAt: record.finishedAt ?? record.startedAt,
|
|
92
|
+
status: record.status === "failed" || record.status === "partial" ? "failed" : "ok",
|
|
93
|
+
error: extractNote,
|
|
94
|
+
personUid: person.personUid ?? person.personKey,
|
|
95
|
+
...(record.catchUp ? { catchUp: true } : {}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Alert the owner for each run record that landed sources and produced no
|
|
100
|
+
* signals. Never throws; a failed send is logged and left unrecorded, so the
|
|
101
|
+
* next run tries again. Returns the alert result per company that alerted.
|
|
102
|
+
*/
|
|
103
|
+
export async function raiseSignalsRunAlerts(input) {
|
|
104
|
+
const out = {};
|
|
105
|
+
const log = input.senders.log ?? (() => { });
|
|
106
|
+
const resolvePerson = input.senders.resolvePerson ?? (() => resolveSignedInAlertPerson());
|
|
107
|
+
for (const record of input.records) {
|
|
108
|
+
if (!isSignalsRunFailure(record))
|
|
109
|
+
continue;
|
|
110
|
+
if (input.senders.recordsDelivery === false) {
|
|
111
|
+
out[record.company] = await sendWithoutDedupe(record, input.senders, resolvePerson, log);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
let person;
|
|
115
|
+
try {
|
|
116
|
+
person = await resolvePerson(record.company);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
log(`signals alert for ${record.company} skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!person) {
|
|
123
|
+
log(`signals alert for ${record.company} skipped: no signed-in person to alert`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const vaultRoot = path.join(input.hqRoot, "companies", record.company);
|
|
127
|
+
try {
|
|
128
|
+
// The dedupe file is the one engine write outside guardedWrite; hold it to the same roots.
|
|
129
|
+
const stateRel = path.relative(input.hqRoot, signalsAlertStatePath(vaultRoot, person.personKey));
|
|
130
|
+
assertAllowedPath(input.hqRoot, stateRel.split(path.sep).join("/"), { company: record.company });
|
|
131
|
+
out[record.company] = await raiseSignalsFailureAlerts({
|
|
132
|
+
vaultRoot,
|
|
133
|
+
personKey: person.personKey,
|
|
134
|
+
entries: [signalsRunLedgerLine(record, person)],
|
|
135
|
+
now: input.now,
|
|
136
|
+
...(person.personName ? { personName: person.personName } : {}),
|
|
137
|
+
sendOwnerDm: input.senders.sendOwnerDm,
|
|
138
|
+
log,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
log(`signals alert for ${record.company} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Deliver one alert to a sender that keeps no record (the terminal). The
|
|
149
|
+
* dedupe file is neither read nor written, and no signed-in person is needed.
|
|
150
|
+
*/
|
|
151
|
+
async function sendWithoutDedupe(record, senders, resolvePerson, log) {
|
|
152
|
+
const result = { sent: [], suppressed: [], failed: [] };
|
|
153
|
+
let person;
|
|
154
|
+
try {
|
|
155
|
+
person = await resolvePerson(record.company);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// The terminal needs no person; print the alert without a name.
|
|
159
|
+
person = null;
|
|
160
|
+
}
|
|
161
|
+
const line = signalsRunLedgerLine(record, person ?? { personKey: "-" });
|
|
162
|
+
if (findSignalsFailures([line]).length === 0)
|
|
163
|
+
return result;
|
|
164
|
+
const key = signalsAlertKey(line);
|
|
165
|
+
try {
|
|
166
|
+
await senders.sendOwnerDm(formatSignalsFailureAlert(line, person?.personName ? { personName: person.personName } : {}));
|
|
167
|
+
result.sent.push(key);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
log(`signals alert for ${key} was not delivered: ${err instanceof Error ? err.message : String(err)}`);
|
|
171
|
+
result.failed.push(key);
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=failure-alert.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Local calendar date, YYYY-MM-DD. The signals engine and its steps name every dated folder with it. */
|
|
2
|
+
export function localDate(d) {
|
|
3
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
4
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=local-date.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Member projection pull for the upload step (Hot-Signal Sync US-005).
|
|
3
|
+
*
|
|
4
|
+
* hq-pro writes each member's entity projection to
|
|
5
|
+
* `ontology/projections/members/{memberUid}/v1/{manifest.json,entities.jsonl}`
|
|
6
|
+
* in the company bucket (src/ontology/member-projection.ts). The desktop only
|
|
7
|
+
* pulls that prefix; this module does the same and mirrors it into
|
|
8
|
+
* `workspace/signals/projections/{company}/{memberUid}/v1/`.
|
|
9
|
+
*/
|
|
10
|
+
export type ProjectionState = "missing" | "stale" | "ready";
|
|
11
|
+
export declare const PROJECTION_STALE_AFTER_MS: number;
|
|
12
|
+
export declare function memberProjectionPrefix(memberUid: string): string;
|
|
13
|
+
export interface ProjectionRefreshResult {
|
|
14
|
+
state: ProjectionState;
|
|
15
|
+
/** Manifest watermark, else generatedAt, else "none". */
|
|
16
|
+
watermark: string;
|
|
17
|
+
entityCount: number;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface RefreshMemberProjectionInput {
|
|
21
|
+
company: string;
|
|
22
|
+
memberUid: string;
|
|
23
|
+
/** Reads one bucket-relative key from the company vault; null when absent. */
|
|
24
|
+
read: (key: string) => Promise<string | null>;
|
|
25
|
+
/** Guarded writer bound to hqRoot. */
|
|
26
|
+
write: (relPath: string, data: string) => void;
|
|
27
|
+
now: () => Date;
|
|
28
|
+
}
|
|
29
|
+
/** Pull the member's own projection prefix into workspace/signals/projections/. */
|
|
30
|
+
export declare function refreshMemberProjection(input: RefreshMemberProjectionInput): Promise<ProjectionRefreshResult>;
|
|
31
|
+
/**
|
|
32
|
+
* Seam for asking hq-pro to regenerate the member projection. hq-pro main
|
|
33
|
+
* (9f172092d, 2026-10-31) has no route that calls `exportMemberProjection`
|
|
34
|
+
* (src/ontology/member-projection.ts has no caller), so there is nothing to
|
|
35
|
+
* call yet. The signals engine is intended to be the only caller once such a
|
|
36
|
+
* route exists; until then this always reports `not-available`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function triggerProjectionExport(_input: {
|
|
39
|
+
company: string;
|
|
40
|
+
memberUid: string;
|
|
41
|
+
}): Promise<"not-available">;
|
|
42
|
+
//# sourceMappingURL=projection.d.ts.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Member projection pull for the upload step (Hot-Signal Sync US-005).
|
|
3
|
+
*
|
|
4
|
+
* hq-pro writes each member's entity projection to
|
|
5
|
+
* `ontology/projections/members/{memberUid}/v1/{manifest.json,entities.jsonl}`
|
|
6
|
+
* in the company bucket (src/ontology/member-projection.ts). The desktop only
|
|
7
|
+
* pulls that prefix; this module does the same and mirrors it into
|
|
8
|
+
* `workspace/signals/projections/{company}/{memberUid}/v1/`.
|
|
9
|
+
*/
|
|
10
|
+
export const PROJECTION_STALE_AFTER_MS = 48 * 60 * 60 * 1000;
|
|
11
|
+
export function memberProjectionPrefix(memberUid) {
|
|
12
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(memberUid))
|
|
13
|
+
throw new Error("invalid member uid");
|
|
14
|
+
return `ontology/projections/members/${memberUid}/v1/`;
|
|
15
|
+
}
|
|
16
|
+
/** Pull the member's own projection prefix into workspace/signals/projections/. */
|
|
17
|
+
export async function refreshMemberProjection(input) {
|
|
18
|
+
const prefix = memberProjectionPrefix(input.memberUid);
|
|
19
|
+
const localDir = `workspace/signals/projections/${input.company}/${input.memberUid}/v1`;
|
|
20
|
+
let manifestText;
|
|
21
|
+
try {
|
|
22
|
+
manifestText = await input.read(`${prefix}manifest.json`);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
return { state: "missing", watermark: "none", entityCount: 0, error: err instanceof Error ? err.message : String(err) };
|
|
26
|
+
}
|
|
27
|
+
if (manifestText === null)
|
|
28
|
+
return { state: "missing", watermark: "none", entityCount: 0 };
|
|
29
|
+
let manifest;
|
|
30
|
+
try {
|
|
31
|
+
manifest = JSON.parse(manifestText);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return { state: "missing", watermark: "none", entityCount: 0, error: "manifest.json is not JSON" };
|
|
35
|
+
}
|
|
36
|
+
let entities;
|
|
37
|
+
try {
|
|
38
|
+
entities = await input.read(`${prefix}entities.jsonl`);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
return { state: "stale", watermark: "none", entityCount: 0, error: err instanceof Error ? err.message : String(err) };
|
|
42
|
+
}
|
|
43
|
+
input.write(`${localDir}/manifest.json`, manifestText);
|
|
44
|
+
if (entities !== null)
|
|
45
|
+
input.write(`${localDir}/entities.jsonl`, entities);
|
|
46
|
+
const generatedAt = typeof manifest.generatedAt === "string" ? Date.parse(manifest.generatedAt) : NaN;
|
|
47
|
+
const fresh = Number.isFinite(generatedAt) && input.now().getTime() - generatedAt <= PROJECTION_STALE_AFTER_MS;
|
|
48
|
+
const state = manifest.state === "ready" && entities !== null && fresh ? "ready" : "stale";
|
|
49
|
+
const watermark = typeof manifest.watermark === "string" && manifest.watermark
|
|
50
|
+
? manifest.watermark
|
|
51
|
+
: typeof manifest.generatedAt === "string" && manifest.generatedAt
|
|
52
|
+
? manifest.generatedAt
|
|
53
|
+
: "none";
|
|
54
|
+
const entityCount = Number.isInteger(manifest.entityCount) ? manifest.entityCount : 0;
|
|
55
|
+
return { state, watermark, entityCount };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Seam for asking hq-pro to regenerate the member projection. hq-pro main
|
|
59
|
+
* (9f172092d, 2026-10-31) has no route that calls `exportMemberProjection`
|
|
60
|
+
* (src/ontology/member-projection.ts has no caller), so there is nothing to
|
|
61
|
+
* call yet. The signals engine is intended to be the only caller once such a
|
|
62
|
+
* route exists; until then this always reports `not-available`.
|
|
63
|
+
*/
|
|
64
|
+
export async function triggerProjectionExport(_input) {
|
|
65
|
+
return "not-available";
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=projection.js.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The storyboarded bot replies for the signals job
|
|
3
|
+
* (companies/indigo/projects/hot-signal-sync/design/bot-replies.md).
|
|
4
|
+
*
|
|
5
|
+
* Every reply is plain prose followed by ONE `hq-block` fence whose JSON is on
|
|
6
|
+
* one line: `{"v":1,"blocks":[...]}`. Block kinds, tones and icons come from
|
|
7
|
+
* closed enums. Values come only from the run record; a value the record does
|
|
8
|
+
* not carry drops its row. Pure functions: no clock, no I/O.
|
|
9
|
+
*/
|
|
10
|
+
import type { LockedCompany, RunRecord, RunSummary } from "./engine.js";
|
|
11
|
+
export declare const REPLY_BLOCK_KINDS: readonly ["keyValue", "callout", "badge", "progress", "decision"];
|
|
12
|
+
export declare const REPLY_TONES: readonly ["neutral", "info", "success", "warning", "danger"];
|
|
13
|
+
export declare const REPLY_ICONS: readonly ["check", "clock", "folder", "calendar"];
|
|
14
|
+
export type ReplyTone = (typeof REPLY_TONES)[number];
|
|
15
|
+
export type ReplyIcon = (typeof REPLY_ICONS)[number];
|
|
16
|
+
export type ReplyBlock = {
|
|
17
|
+
kind: "keyValue";
|
|
18
|
+
items: {
|
|
19
|
+
key: string;
|
|
20
|
+
value: string;
|
|
21
|
+
icon?: ReplyIcon;
|
|
22
|
+
}[];
|
|
23
|
+
} | {
|
|
24
|
+
kind: "callout";
|
|
25
|
+
tone: ReplyTone;
|
|
26
|
+
title?: string;
|
|
27
|
+
body: string;
|
|
28
|
+
} | {
|
|
29
|
+
kind: "badge";
|
|
30
|
+
label: string;
|
|
31
|
+
tone: ReplyTone;
|
|
32
|
+
icon?: ReplyIcon;
|
|
33
|
+
} | {
|
|
34
|
+
kind: "progress";
|
|
35
|
+
value: number;
|
|
36
|
+
label: string;
|
|
37
|
+
tone: ReplyTone;
|
|
38
|
+
} | {
|
|
39
|
+
kind: "decision";
|
|
40
|
+
questionId: string;
|
|
41
|
+
question: string;
|
|
42
|
+
options: {
|
|
43
|
+
id: string;
|
|
44
|
+
label: string;
|
|
45
|
+
recommended?: boolean;
|
|
46
|
+
}[];
|
|
47
|
+
allowOther: boolean;
|
|
48
|
+
};
|
|
49
|
+
export interface FormattedReply {
|
|
50
|
+
/** The full message: prose, blank line, fence. */
|
|
51
|
+
text: string;
|
|
52
|
+
prose: string;
|
|
53
|
+
blocks: ReplyBlock[];
|
|
54
|
+
variant: "complete" | "catch-up" | "late-start" | "partial" | "failed" | "already-ran" | "skipped" | "running";
|
|
55
|
+
}
|
|
56
|
+
export interface RunReplyOptions {
|
|
57
|
+
/** IANA zone the times are written in (the job's zone). */
|
|
58
|
+
timeZone: string;
|
|
59
|
+
/** The job's name in words. Default "Morning signals". */
|
|
60
|
+
jobName?: string;
|
|
61
|
+
/** When the run was due (the fired job's `firedAt`); used by the catch-up line. */
|
|
62
|
+
scheduledFor?: string;
|
|
63
|
+
/** Display names per company slug; default title-cases the slug. */
|
|
64
|
+
companyNames?: Record<string, string>;
|
|
65
|
+
/**
|
|
66
|
+
* The bot scheduled one automatic retry at the record's `nextRetryAt`. Only
|
|
67
|
+
* then does a failure reply say when it tries again and offer to wait.
|
|
68
|
+
*/
|
|
69
|
+
retryScheduled?: boolean;
|
|
70
|
+
/** This run was the automatic retry; a failure says no further retry follows. */
|
|
71
|
+
wasRetry?: boolean;
|
|
72
|
+
}
|
|
73
|
+
/** One fence, JSON on one line, not indented. */
|
|
74
|
+
export declare function hqBlockFence(blocks: ReplyBlock[]): string;
|
|
75
|
+
/** "6:02 AM" in the given zone. */
|
|
76
|
+
export declare function timeInWords(iso: string | null | undefined, timeZone: string): string | null;
|
|
77
|
+
/** "Mountain Time" from America/Denver: the long generic zone name, without "Standard"/"Daylight". */
|
|
78
|
+
export declare function timezoneInWords(timeZone: string, at?: Date): string;
|
|
79
|
+
/** The four keyValue rows, dropping any the record does not carry. */
|
|
80
|
+
export declare function runCountItems(record: RunRecord, summary: RunSummary, opts: RunReplyOptions): {
|
|
81
|
+
key: string;
|
|
82
|
+
value: string;
|
|
83
|
+
}[];
|
|
84
|
+
/**
|
|
85
|
+
* The reply for one finished run of the signals job: run complete, catch-up,
|
|
86
|
+
* late start, finished with problems, or run failed (bot-replies.md sections
|
|
87
|
+
* 2, 2b and 3; status mapping from run-record-contract-v2.md).
|
|
88
|
+
*
|
|
89
|
+
* A failure offers "Wait for <time>" only when `retryScheduled` says the bot
|
|
90
|
+
* really scheduled the retry at the record's `nextRetryAt`. Without it (config
|
|
91
|
+
* and auth failures have no nextRetryAt, and an automatic retry is never
|
|
92
|
+
* retried again) the only option is "Try now".
|
|
93
|
+
*/
|
|
94
|
+
export declare function formatRunReply(record: RunRecord, summary: RunSummary, opts: RunReplyOptions): FormattedReply;
|
|
95
|
+
/** Another run holds the company's lock (engine exit 3): nothing was started. */
|
|
96
|
+
export declare function formatLockedReply(locked: LockedCompany, opts: RunReplyOptions): FormattedReply;
|
|
97
|
+
/** A job that could not run the engine at all: the prose, then one danger callout with the reason. */
|
|
98
|
+
export declare function formatJobErrorReply(prose: string, reason: string): FormattedReply;
|
|
99
|
+
export interface JobCreatedInput {
|
|
100
|
+
/** Standard 5-field cron ("0 6 * * *") or the stored six-field cron(...) form. */
|
|
101
|
+
cron: string;
|
|
102
|
+
timezone: string;
|
|
103
|
+
name?: string;
|
|
104
|
+
companies?: string[];
|
|
105
|
+
/** A brief job exists on the same bot. */
|
|
106
|
+
hasBriefJob?: boolean;
|
|
107
|
+
}
|
|
108
|
+
/** "Every day at 6:00 AM" from a daily cron; null when the cron is not a plain daily or weekday time. */
|
|
109
|
+
export declare function scheduleInWords(cron: string): string | null;
|
|
110
|
+
/** The job-created reply (bot-replies.md section 1). The zone comes from the job record, in words. */
|
|
111
|
+
export declare function formatJobCreatedReply(job: JobCreatedInput, timezoneWords?: string): FormattedReply;
|
|
112
|
+
//# sourceMappingURL=reply.d.ts.map
|