@indigoai-us/hq-cli 5.345.190 → 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.
Files changed (122) hide show
  1. package/CHANGELOG.md +420 -0
  2. package/assets/bot-workers/setup/worker.yaml +91 -34
  3. package/dist/command-catalog.generated.d.ts +96 -19
  4. package/dist/command-catalog.generated.js +124 -21
  5. package/dist/commands/bot-jobs.d.ts +21 -18
  6. package/dist/commands/bot-jobs.js +127 -39
  7. package/dist/commands/bot.d.ts +7 -4
  8. package/dist/commands/bot.js +289 -140
  9. package/dist/commands/core-jobs-schedule.js +10 -0
  10. package/dist/commands/daemon.js +37 -0
  11. package/dist/commands/files.d.ts +41 -6
  12. package/dist/commands/files.js +108 -27
  13. package/dist/commands/pack-start-bot.d.ts +4 -6
  14. package/dist/commands/pack-start-bot.js +4 -8
  15. package/dist/commands/signals-run.d.ts +45 -0
  16. package/dist/commands/signals-run.js +175 -0
  17. package/dist/commands/signals.d.ts +1 -0
  18. package/dist/commands/signals.js +3 -0
  19. package/dist/commands/sources.d.ts +11 -0
  20. package/dist/commands/sources.js +137 -25
  21. package/dist/commands/staff/reset-test-account.d.ts +10 -0
  22. package/dist/commands/staff/reset-test-account.js +18 -2
  23. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +31171 -52512
  24. package/dist/hq-cloud-sync-runner/package.json +1 -1
  25. package/dist/lib/bot/api.d.ts +3 -2
  26. package/dist/lib/bot/config.d.ts +2 -0
  27. package/dist/lib/bot/fleet-cron.d.ts +121 -0
  28. package/dist/lib/bot/fleet-cron.js +319 -0
  29. package/dist/lib/bot/fleet-local.d.ts +113 -2
  30. package/dist/lib/bot/fleet-local.js +597 -35
  31. package/dist/lib/bot/fleet-owner-companies.d.ts +12 -0
  32. package/dist/lib/bot/fleet-owner-companies.js +30 -0
  33. package/dist/lib/bot/fleet-watch.d.ts +25 -0
  34. package/dist/lib/bot/fleet-watch.js +51 -0
  35. package/dist/lib/bot/index.d.ts +1 -1
  36. package/dist/lib/bot/index.js +1 -1
  37. package/dist/lib/bot/job-run.d.ts +5 -4
  38. package/dist/lib/bot/job-run.js +5 -4
  39. package/dist/lib/bot/jobs/hot-signal-sync.d.ts +57 -3
  40. package/dist/lib/bot/jobs/hot-signal-sync.js +163 -4
  41. package/dist/lib/bot/local-jobs.d.ts +13 -2
  42. package/dist/lib/bot/local-jobs.js +17 -4
  43. package/dist/lib/bot/progress.d.ts +14 -0
  44. package/dist/lib/bot/progress.js +47 -1
  45. package/dist/lib/bot/run.d.ts +55 -2
  46. package/dist/lib/bot/run.js +192 -10
  47. package/dist/lib/bot/setup-tool-offer.d.ts +1 -0
  48. package/dist/lib/bot/setup-tool-offer.js +16 -0
  49. package/dist/lib/bot/signals-engine-flag.d.ts +18 -0
  50. package/dist/lib/bot/signals-engine-flag.js +30 -0
  51. package/dist/lib/daemon/catalog.d.ts +4 -0
  52. package/dist/lib/daemon/catalog.js +29 -9
  53. package/dist/lib/daemon/legacy.d.ts +6 -0
  54. package/dist/lib/daemon/legacy.js +16 -10
  55. package/dist/lib/daemon/run.d.ts +1 -0
  56. package/dist/lib/daemon/run.js +10 -0
  57. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  58. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  59. package/dist/lib/flag-registry.d.ts +13 -1
  60. package/dist/lib/flag-registry.js +46 -0
  61. package/dist/lib/hot-channels/adapters/attio.d.ts +75 -2
  62. package/dist/lib/hot-channels/adapters/attio.js +367 -29
  63. package/dist/lib/hot-channels/adapters/gmail.d.ts +14 -0
  64. package/dist/lib/hot-channels/adapters/gmail.js +59 -2
  65. package/dist/lib/hot-channels/adapters/linear.d.ts +24 -0
  66. package/dist/lib/hot-channels/adapters/linear.js +71 -1
  67. package/dist/lib/hot-channels/adapters/meetings.js +4 -1
  68. package/dist/lib/hot-channels/adapters/slack.js +7 -3
  69. package/dist/lib/hot-channels/alert.js +1 -1
  70. package/dist/lib/hot-channels/contract.d.ts +11 -0
  71. package/dist/lib/hot-channels/contract.js +1 -0
  72. package/dist/lib/hot-channels/cursors.js +1 -1
  73. package/dist/lib/hot-channels/landing.d.ts +6 -2
  74. package/dist/lib/hot-channels/landing.js +13 -6
  75. package/dist/lib/hot-channels/ledger.js +2 -0
  76. package/dist/lib/hot-channels/loop.js +18 -4
  77. package/dist/lib/hot-channels/registry.js +9 -1
  78. package/dist/lib/hot-channels/run.d.ts +18 -0
  79. package/dist/lib/hot-channels/run.js +15 -4
  80. package/dist/lib/hot-channels/signals-pass.d.ts +1 -0
  81. package/dist/lib/hot-channels/signals-pass.js +7 -5
  82. package/dist/lib/hot-channels/sync-lock.js +22 -6
  83. package/dist/lib/mesh/live/daemon/install.d.ts +22 -0
  84. package/dist/lib/mesh/live/daemon/install.js +117 -30
  85. package/dist/lib/signals/adapters-catalog.d.ts +77 -0
  86. package/dist/lib/signals/adapters-catalog.js +194 -0
  87. package/dist/lib/signals/engine.d.ts +196 -0
  88. package/dist/lib/signals/engine.js +495 -0
  89. package/dist/lib/signals/failure-alert.d.ts +89 -0
  90. package/dist/lib/signals/failure-alert.js +175 -0
  91. package/dist/lib/signals/local-date.d.ts +3 -0
  92. package/dist/lib/signals/local-date.js +6 -0
  93. package/dist/lib/signals/projection.d.ts +42 -0
  94. package/dist/lib/signals/projection.js +67 -0
  95. package/dist/lib/signals/reply.d.ts +112 -0
  96. package/dist/lib/signals/reply.js +368 -0
  97. package/dist/lib/signals/run-lock.d.ts +36 -0
  98. package/dist/lib/signals/run-lock.js +148 -0
  99. package/dist/lib/signals/run-record.d.ts +24 -0
  100. package/dist/lib/signals/run-record.js +82 -0
  101. package/dist/lib/signals/sanitize.d.ts +105 -0
  102. package/dist/lib/signals/sanitize.js +439 -0
  103. package/dist/lib/signals/settings.d.ts +10 -0
  104. package/dist/lib/signals/settings.js +48 -0
  105. package/dist/lib/signals/steps/adapters.d.ts +14 -0
  106. package/dist/lib/signals/steps/adapters.js +156 -0
  107. package/dist/lib/signals/steps/entity-similarity.d.ts +10 -0
  108. package/dist/lib/signals/steps/entity-similarity.js +47 -0
  109. package/dist/lib/signals/steps/extract.d.ts +113 -0
  110. package/dist/lib/signals/steps/extract.js +765 -0
  111. package/dist/lib/signals/steps/garden.d.ts +24 -0
  112. package/dist/lib/signals/steps/garden.js +250 -0
  113. package/dist/lib/signals/steps/types.d.ts +112 -0
  114. package/dist/lib/signals/steps/types.js +4 -0
  115. package/dist/lib/signals/steps/upload.d.ts +43 -0
  116. package/dist/lib/signals/steps/upload.js +324 -0
  117. package/dist/lib/signals/write-guard.d.ts +63 -0
  118. package/dist/lib/signals/write-guard.js +287 -0
  119. package/dist/utils/resident-package-preload.js +16 -0
  120. package/package.json +9 -9
  121. package/dist/lib/bot/fleet-default-flag.d.ts +0 -31
  122. package/dist/lib/bot/fleet-default-flag.js +0 -127
@@ -0,0 +1,196 @@
1
+ import { localDate } from "./local-date.js";
2
+ import { type LockTakeover } from "./run-lock.js";
3
+ import { type UploadDeps } from "./steps/upload.js";
4
+ import { type SignalsRunAlertSenders } from "./failure-alert.js";
5
+ import type { RaiseSignalsFailureAlertsResult } from "../hot-channels/alert.js";
6
+ import type { ErrorKind, HotChannelSyncFn, SignalsStep, SpawnSyncFn, StepFailure, StepName, StepStatus } from "./steps/types.js";
7
+ import type { AdapterFlagReader } from "./adapters-catalog.js";
8
+ export declare const SIGNALS_TRIGGERS: readonly ["job", "chat", "manual", "brief"];
9
+ export type SignalsTrigger = (typeof SIGNALS_TRIGGERS)[number];
10
+ export declare const SIGNALS_STEP_CHOICES: readonly ["garden", "extract", "adapters", "upload", "all"];
11
+ export type SignalsStepChoice = (typeof SIGNALS_STEP_CHOICES)[number];
12
+ export declare const STEP_ORDER: StepName[];
13
+ export declare const RUN_RECORD_SCHEMA_VERSION: 2;
14
+ /** A failed run with a transient error is due for a retry this long after it finished. */
15
+ export declare const RETRY_DELAY_MS: number;
16
+ export interface StepRecord {
17
+ name: StepName;
18
+ itemsIn: number;
19
+ itemsOut: number;
20
+ durationMs: number;
21
+ status: StepStatus;
22
+ skippedReason?: string;
23
+ error?: string;
24
+ /** The step's own error detail (reconnect, signed-out, ...). The record-level errorKind is the coarse v2 kind. */
25
+ errorKind?: ErrorKind;
26
+ modelTurns?: number;
27
+ estimatedSpendUsd?: number;
28
+ byAdapter?: Record<string, number>;
29
+ itemsInByAdapter?: Record<string, number>;
30
+ tokenLevels?: Record<string, string>;
31
+ adapterSkips?: Record<string, string>;
32
+ uploaded?: {
33
+ count: number;
34
+ bytes: number;
35
+ };
36
+ plan?: string[];
37
+ detail?: Record<string, unknown>;
38
+ skipped?: number;
39
+ review?: number;
40
+ failures?: StepFailure[];
41
+ skippedItems?: {
42
+ file: string;
43
+ reason: string;
44
+ }[];
45
+ rejectedCandidates?: number;
46
+ }
47
+ /** "dry-run" is only ever returned in memory; a dry run writes no record. */
48
+ export type RunStatus = "running" | "complete" | "partial" | "failed" | "skipped" | "dry-run";
49
+ export type RunErrorKind = "transient" | "config" | "auth";
50
+ export interface RunRecord {
51
+ schemaVersion: typeof RUN_RECORD_SCHEMA_VERSION;
52
+ company: string;
53
+ startedAt: string;
54
+ finishedAt: string | null;
55
+ trigger: SignalsTrigger;
56
+ pid: number;
57
+ catchUp: boolean;
58
+ dryRun: boolean;
59
+ status: RunStatus;
60
+ /** prefs-off, late-start, ... on a skipped record; else null. */
61
+ skipReason: string | null;
62
+ /** v1 name of skipReason, kept for older readers. */
63
+ skippedReason?: string;
64
+ currentStep: StepName | null;
65
+ steps: StepRecord[];
66
+ maxBudgetUsd: number;
67
+ maxTurns: number;
68
+ /** Set when a job run finishes complete or partial; carried forward by every later run that day. */
69
+ jobRanAt: string | null;
70
+ /** On failed with errorKind transient: finishedAt + 15 minutes. */
71
+ nextRetryAt: string | null;
72
+ errorKind: RunErrorKind | null;
73
+ /** One plain-language sentence on failed and partial records. */
74
+ failureReason: string | null;
75
+ /** late-start, step-failed:{step}, step-partial:{step}, or null. */
76
+ partialReason: string | null;
77
+ /** v1 name of partialReason, kept for older readers. */
78
+ partial: string | null;
79
+ /** In-memory only: this call found the day's job already ran and did nothing. */
80
+ alreadyRanToday: boolean;
81
+ counts: {
82
+ sourcesIn: number;
83
+ signalsOut: number;
84
+ };
85
+ lockTakeover?: LockTakeover;
86
+ }
87
+ /** A company another run is holding the lock for. Nothing was started for it. */
88
+ export interface LockedCompany {
89
+ company: string;
90
+ status: "running";
91
+ startedAt: string;
92
+ trigger: string;
93
+ }
94
+ export interface RunSummaryEntry {
95
+ company: string;
96
+ status: RunStatus;
97
+ trigger: SignalsTrigger;
98
+ /** The record's catchUp flag, so it always matches the company's record. */
99
+ catchUp: boolean;
100
+ finishedAt: string | null;
101
+ skipReason?: string;
102
+ skippedReason?: string;
103
+ itemsIn: number;
104
+ itemsOut: number;
105
+ estimatedSpendUsd: number;
106
+ }
107
+ export interface RunSummary {
108
+ schemaVersion: typeof RUN_RECORD_SCHEMA_VERSION;
109
+ date: string;
110
+ trigger: SignalsTrigger;
111
+ companies: RunSummaryEntry[];
112
+ totals: {
113
+ itemsIn: number;
114
+ itemsOut: number;
115
+ estimatedSpendUsd: number;
116
+ };
117
+ status: RunStatus;
118
+ finishedAt: string;
119
+ }
120
+ export interface RunSignalsResult {
121
+ date: string;
122
+ records: RunRecord[];
123
+ locked: LockedCompany[];
124
+ /** Summary of this call's companies. summary.json on disk holds every company's latest entry. */
125
+ summary: RunSummary;
126
+ /**
127
+ * The signals-failure alert result per company whose run landed sources and
128
+ * produced no signals (US-018). Omitted when no alert sender was given.
129
+ */
130
+ alerts?: Record<string, RaiseSignalsFailureAlertsResult>;
131
+ }
132
+ export interface RunSignalsOptions {
133
+ hqRoot: string;
134
+ company?: string;
135
+ dryRun?: boolean;
136
+ step?: SignalsStepChoice;
137
+ trigger?: SignalsTrigger;
138
+ force?: boolean;
139
+ catchUp?: boolean;
140
+ maxTurns?: number;
141
+ maxBudgetUsd?: number;
142
+ now?: () => Date;
143
+ spawn?: SpawnSyncFn;
144
+ hotChannelSync?: HotChannelSyncFn;
145
+ /** Adapter ids the adapters step gates and syncs. Defaults to the builtin registry. */
146
+ registeredAdapterIds?: readonly string[];
147
+ uploadDeps?: Partial<UploadDeps>;
148
+ /**
149
+ * Company flag reader for the adapters step's gate. Unset in production:
150
+ * the hot-channel sync then reads the adapter flags through the shared
151
+ * hq-flags evaluator with the signed-in session.
152
+ */
153
+ isFlagOn?: AdapterFlagReader;
154
+ steps?: Partial<Record<StepName, SignalsStep>>;
155
+ /**
156
+ * Where the signals-failure alert goes (US-018): after the run, a company
157
+ * whose extract step read sources and produced nothing (or failed after new
158
+ * landings) alerts the owner through the ledger alert path, deduped hourly.
159
+ * Unset = no alert check.
160
+ */
161
+ signalsAlert?: SignalsRunAlertSenders;
162
+ /** Lock liveness check; defaults to process.kill(pid, 0). */
163
+ isPidAlive?: (pid: number) => boolean;
164
+ }
165
+ /** Bad input from the caller (company slug, missing company): exit 2. */
166
+ export declare class SignalsUsageError extends Error {
167
+ constructor(message: string);
168
+ }
169
+ /** Broken local setup (missing or failing knowledge-prefs.sh): errorKind config. */
170
+ export declare class SignalsConfigError extends Error {
171
+ constructor(message: string);
172
+ }
173
+ export declare const defaultSpawn: SpawnSyncFn;
174
+ export { localDate };
175
+ export declare function runRecordRel(date: string, company: string): string;
176
+ export declare function summaryRel(date: string): string;
177
+ /** Companies under companies/ that are also listed in companies/manifest.yaml. */
178
+ export declare function listCompanies(hqRoot: string): string[];
179
+ /** Reads one knowledge preference. Throws SignalsConfigError when the script is missing or fails. */
180
+ export declare function readPref(hqRoot: string, company: string, field: string, spawn: SpawnSyncFn): boolean;
181
+ /** Coarse v2 kind for a step's detailed kind. */
182
+ export declare function toRunErrorKind(kind: ErrorKind | undefined): RunErrorKind;
183
+ /** Fills the v2 outcome fields of a finished record from its steps. */
184
+ export declare function finalizeRecord(record: RunRecord, input: {
185
+ lateStart: boolean;
186
+ lateStartHour: number;
187
+ finishedAt: Date;
188
+ }): void;
189
+ export declare function runSignals(opts: RunSignalsOptions): Promise<RunSignalsResult>;
190
+ /**
191
+ * Exit code for `hq signals run`: 1 when a company failed (a job that already
192
+ * ran today does not count), else 3 when another run held a company's lock,
193
+ * else 0 (complete, partial, skipped, already ran).
194
+ */
195
+ export declare function signalsExitCode(result: RunSignalsResult): 0 | 1 | 3;
196
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1,495 @@
1
+ /**
2
+ * Orchestrator for `hq signals run`. Callable programmatically via
3
+ * runSignals(); clock, spawn and HQ root are injected for tests.
4
+ *
5
+ * Step order: garden -> adapters -> extract -> garden -> upload.
6
+ * Records (run record contract v2): workspace/signals/runs/{date}/{company}.json
7
+ * is the source of truth for one company's day; summary.json in the same
8
+ * folder is read-modify-written per company and never drops other companies.
9
+ * Each company runs under workspace/signals/runs/{date}/{company}.lock.
10
+ */
11
+ import { spawnSync } from "node:child_process";
12
+ import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
+ import yaml from "js-yaml";
15
+ import { loadSignalsSettings } from "./settings.js";
16
+ import { localDate } from "./local-date.js";
17
+ import { guardedWrite, WriteGuardError } from "./write-guard.js";
18
+ import { acquireCompanyLock, withSummaryLock } from "./run-lock.js";
19
+ import { gardenStep } from "./steps/garden.js";
20
+ import { adaptersStep } from "./steps/adapters.js";
21
+ import { extractStep } from "./steps/extract.js";
22
+ import { uploadStep } from "./steps/upload.js";
23
+ import { raiseSignalsRunAlerts } from "./failure-alert.js";
24
+ export const SIGNALS_TRIGGERS = ["job", "chat", "manual", "brief"];
25
+ export const SIGNALS_STEP_CHOICES = ["garden", "extract", "adapters", "upload", "all"];
26
+ export const STEP_ORDER = ["garden", "adapters", "extract", "garden", "upload"];
27
+ export const RUN_RECORD_SCHEMA_VERSION = 2;
28
+ /** A failed run with a transient error is due for a retry this long after it finished. */
29
+ export const RETRY_DELAY_MS = 15 * 60 * 1000;
30
+ /** Bad input from the caller (company slug, missing company): exit 2. */
31
+ export class SignalsUsageError extends Error {
32
+ constructor(message) {
33
+ super(message);
34
+ this.name = "SignalsUsageError";
35
+ }
36
+ }
37
+ /** Broken local setup (missing or failing knowledge-prefs.sh): errorKind config. */
38
+ export class SignalsConfigError extends Error {
39
+ constructor(message) {
40
+ super(message);
41
+ this.name = "SignalsConfigError";
42
+ }
43
+ }
44
+ export const defaultSpawn = (cmd, args, opts) => {
45
+ const r = spawnSync(cmd, args, {
46
+ cwd: opts.cwd,
47
+ env: opts.env ?? process.env,
48
+ encoding: "utf8",
49
+ maxBuffer: 64 * 1024 * 1024,
50
+ ...(opts.timeoutMs !== undefined ? { timeout: opts.timeoutMs, killSignal: "SIGKILL" } : {}),
51
+ });
52
+ return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "", error: r.error };
53
+ };
54
+ const DEFAULT_STEPS = {
55
+ garden: gardenStep,
56
+ adapters: adaptersStep,
57
+ extract: extractStep,
58
+ upload: uploadStep,
59
+ };
60
+ // Lives in its own module so steps can use it without importing the engine.
61
+ export { localDate };
62
+ export function runRecordRel(date, company) {
63
+ return `workspace/signals/runs/${date}/${company}.json`;
64
+ }
65
+ export function summaryRel(date) {
66
+ return `workspace/signals/runs/${date}/summary.json`;
67
+ }
68
+ const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
69
+ /** Companies under companies/ that are also listed in companies/manifest.yaml. */
70
+ export function listCompanies(hqRoot) {
71
+ let manifest;
72
+ try {
73
+ manifest = yaml.load(fs.readFileSync(path.join(hqRoot, "companies", "manifest.yaml"), "utf8"));
74
+ }
75
+ catch (err) {
76
+ const msg = err instanceof Error ? err.message : String(err);
77
+ process.stderr.write(`hq signals: cannot read companies/manifest.yaml: ${msg}\n`);
78
+ return [];
79
+ }
80
+ const listed = manifest?.companies ?? {};
81
+ return Object.keys(listed)
82
+ .filter((co) => SLUG_RE.test(co) && fs.existsSync(path.join(hqRoot, "companies", co)))
83
+ .sort();
84
+ }
85
+ const TRUE_VALUES = new Set(["true", "yes", "on", "1"]);
86
+ /** Reads one knowledge preference. Throws SignalsConfigError when the script is missing or fails. */
87
+ export function readPref(hqRoot, company, field, spawn) {
88
+ const script = path.join(hqRoot, "core", "scripts", "knowledge-prefs.sh");
89
+ if (!fs.existsSync(script)) {
90
+ throw new SignalsConfigError("core/scripts/knowledge-prefs.sh is missing, so the capture settings could not be read");
91
+ }
92
+ const r = spawn("bash", [script, "get", company, field], {
93
+ cwd: hqRoot,
94
+ env: { ...process.env, HQ_ROOT: hqRoot },
95
+ });
96
+ if (r.error || r.status !== 0) {
97
+ const detail = (r.error?.message ?? r.stderr.trim()) || `exit ${String(r.status)}`;
98
+ throw new SignalsConfigError(`knowledge-prefs.sh could not read ${field}: ${detail.slice(0, 200)}`);
99
+ }
100
+ return TRUE_VALUES.has(r.stdout.trim().toLowerCase());
101
+ }
102
+ function readJson(hqRoot, rel) {
103
+ const abs = path.join(hqRoot, rel);
104
+ if (!fs.existsSync(abs))
105
+ return null;
106
+ try {
107
+ return JSON.parse(fs.readFileSync(abs, "utf8"));
108
+ }
109
+ catch (err) {
110
+ const msg = err instanceof Error ? err.message : String(err);
111
+ process.stderr.write(`hq signals: ignoring unreadable ${rel}: ${msg}\n`);
112
+ return null;
113
+ }
114
+ }
115
+ function baseStatus(steps) {
116
+ if (steps.some((s) => s.status === "failed")) {
117
+ return steps.some((s) => s.status === "ok" || s.status === "partial") ? "partial" : "failed";
118
+ }
119
+ if (steps.some((s) => s.status === "partial"))
120
+ return "partial";
121
+ if (steps.length > 0 && steps.every((s) => s.status === "skipped"))
122
+ return "skipped";
123
+ return "complete";
124
+ }
125
+ const KIND_RANK = { auth: 3, config: 2, transient: 1 };
126
+ /** Coarse v2 kind for a step's detailed kind. */
127
+ export function toRunErrorKind(kind) {
128
+ if (kind === "reconnect" || kind === "signed-out")
129
+ return "auth";
130
+ if (kind === "config")
131
+ return "config";
132
+ return "transient";
133
+ }
134
+ function stepErrorKindFor(err) {
135
+ return err instanceof SignalsConfigError || err instanceof WriteGuardError ? "config" : "other";
136
+ }
137
+ function sentence(text) {
138
+ const t = text.trim().replace(/[.\s]+$/, "");
139
+ return `${t}.`;
140
+ }
141
+ /** Fills the v2 outcome fields of a finished record from its steps. */
142
+ export function finalizeRecord(record, input) {
143
+ record.currentStep = null;
144
+ record.finishedAt = input.finishedAt.toISOString();
145
+ const problems = record.steps.filter((s) => s.status === "failed" || s.status === "partial");
146
+ // A skip decided before any step (prefs-off, late-start with nothing to run) stays a skip.
147
+ let status = record.skipReason ? "skipped" : baseStatus(record.steps);
148
+ let partialReason = null;
149
+ let failureReason = null;
150
+ if (input.lateStart && status !== "failed" && status !== "skipped") {
151
+ status = "partial";
152
+ partialReason = "late-start";
153
+ failureReason = `The run started after ${String(input.lateStartHour).padStart(2, "0")}:00, so only the garden step ran.`;
154
+ }
155
+ else if (status === "partial" || status === "failed") {
156
+ const first = problems.find((s) => s.status === "failed") ?? problems[0];
157
+ if (first) {
158
+ if (status === "partial")
159
+ partialReason = `step-${first.status === "failed" ? "failed" : "partial"}:${first.name}`;
160
+ failureReason =
161
+ first.status === "failed"
162
+ ? sentence(`The ${first.name} step failed${first.error ? `: ${first.error}` : ""}`)
163
+ : sentence(`The ${first.name} step finished with problems${first.error ? `: ${first.error}` : ""}`);
164
+ }
165
+ }
166
+ let errorKind = null;
167
+ if (status === "partial" || status === "failed") {
168
+ errorKind = "transient";
169
+ for (const s of problems) {
170
+ const k = toRunErrorKind(s.errorKind);
171
+ if (KIND_RANK[k] > KIND_RANK[errorKind])
172
+ errorKind = k;
173
+ }
174
+ }
175
+ record.status = status;
176
+ record.partialReason = partialReason;
177
+ record.partial = partialReason;
178
+ record.failureReason = failureReason;
179
+ record.errorKind = errorKind;
180
+ record.nextRetryAt =
181
+ status === "failed" && errorKind === "transient"
182
+ ? new Date(input.finishedAt.getTime() + RETRY_DELAY_MS).toISOString()
183
+ : null;
184
+ if (status === "skipped") {
185
+ record.skipReason = record.skipReason ?? record.steps.find((s) => s.skippedReason)?.skippedReason ?? "nothing-to-run";
186
+ record.skippedReason = record.skipReason;
187
+ }
188
+ if (record.trigger === "job" && (status === "complete" || status === "partial")) {
189
+ record.jobRanAt = record.finishedAt;
190
+ }
191
+ record.counts = {
192
+ sourcesIn: record.steps.filter((s) => s.name === "adapters").reduce((n, s) => n + s.itemsOut, 0),
193
+ signalsOut: record.steps.filter((s) => s.name === "extract").reduce((n, s) => n + s.itemsOut, 0),
194
+ };
195
+ }
196
+ function summaryEntry(r) {
197
+ const sum = (f) => r.steps.reduce((n, s) => n + f(s), 0);
198
+ return {
199
+ company: r.company,
200
+ status: r.status,
201
+ trigger: r.trigger,
202
+ catchUp: r.catchUp === true,
203
+ finishedAt: r.finishedAt,
204
+ ...(r.skipReason ? { skipReason: r.skipReason, skippedReason: r.skipReason } : {}),
205
+ itemsIn: sum((s) => s.itemsIn),
206
+ itemsOut: sum((s) => s.itemsOut),
207
+ estimatedSpendUsd: sum((s) => s.estimatedSpendUsd ?? 0),
208
+ };
209
+ }
210
+ function buildSummary(date, trigger, entries, finishedAt, dryRun) {
211
+ const totals = { itemsIn: 0, itemsOut: 0, estimatedSpendUsd: 0 };
212
+ for (const e of entries) {
213
+ totals.itemsIn += e.itemsIn;
214
+ totals.itemsOut += e.itemsOut;
215
+ totals.estimatedSpendUsd += e.estimatedSpendUsd;
216
+ }
217
+ const statuses = entries.map((e) => e.status);
218
+ const status = dryRun
219
+ ? "dry-run"
220
+ : statuses.includes("failed")
221
+ ? "failed"
222
+ : statuses.includes("partial")
223
+ ? "partial"
224
+ : statuses.includes("running")
225
+ ? "running"
226
+ : statuses.length > 0 && statuses.every((s) => s === "skipped")
227
+ ? "skipped"
228
+ : "complete";
229
+ return { schemaVersion: RUN_RECORD_SCHEMA_VERSION, date, trigger, companies: entries, totals, status, finishedAt };
230
+ }
231
+ /** Replaces this company's entry in summary.json, keeping every other company's. */
232
+ function updateSummaryFile(hqRoot, date, record, finishedAt) {
233
+ withSummaryLock(hqRoot, date, () => {
234
+ const existing = readJson(hqRoot, summaryRel(date));
235
+ const others = (Array.isArray(existing?.companies) ? existing.companies : []).filter((e) => typeof e === "object" && e !== null && typeof e.company === "string" && e.company !== record.company);
236
+ const entries = [...others, summaryEntry(record)].sort((a, b) => a.company.localeCompare(b.company));
237
+ const summary = buildSummary(date, record.trigger, entries, finishedAt, false);
238
+ guardedWrite(hqRoot, summaryRel(date), `${JSON.stringify(summary, null, 2)}\n`);
239
+ });
240
+ }
241
+ export async function runSignals(opts) {
242
+ const hqRoot = path.resolve(opts.hqRoot);
243
+ const now = opts.now ?? (() => new Date());
244
+ const spawn = opts.spawn ?? defaultSpawn;
245
+ const trigger = opts.trigger ?? "manual";
246
+ const stepChoice = opts.step ?? "all";
247
+ const dryRun = opts.dryRun === true;
248
+ const settings = loadSignalsSettings(hqRoot);
249
+ const maxTurns = opts.maxTurns ?? settings.maxTurns;
250
+ const maxBudgetUsd = opts.maxBudgetUsd ?? settings.maxBudgetUsd;
251
+ const stepImpls = { ...DEFAULT_STEPS, ...(opts.steps ?? {}) };
252
+ if (opts.company !== undefined && !SLUG_RE.test(opts.company)) {
253
+ throw new SignalsUsageError(`invalid company slug: ${opts.company}`);
254
+ }
255
+ if (opts.company !== undefined && !fs.existsSync(path.join(hqRoot, "companies", opts.company))) {
256
+ throw new SignalsUsageError(`no company directory: companies/${opts.company}`);
257
+ }
258
+ const companies = opts.company !== undefined ? [opts.company] : listCompanies(hqRoot);
259
+ const start = now();
260
+ const date = localDate(start);
261
+ const lateStart = trigger === "job" && start.getHours() >= settings.lateStartHour;
262
+ let plan = stepChoice === "all" ? STEP_ORDER : STEP_ORDER.filter((s) => s === stepChoice);
263
+ if (stepChoice !== "all")
264
+ plan = plan.slice(0, 1);
265
+ if (lateStart)
266
+ plan = plan.filter((s) => s === "garden").slice(0, 1);
267
+ const records = [];
268
+ const locked = [];
269
+ for (const company of companies) {
270
+ const rel = runRecordRel(date, company);
271
+ // One company scope per run: every write for this company is held to
272
+ // companies/{company}/ (plus the workspace and personal signals roots), and
273
+ // a link that resolves into another company's folder is refused.
274
+ const write = (r, data) => guardedWrite(hqRoot, r, data, { company });
275
+ // 1. Take the company lock (a dry run writes nothing, so it takes none).
276
+ let release = null;
277
+ let takeover = null;
278
+ if (!dryRun) {
279
+ const got = acquireCompanyLock({ hqRoot, date, company, trigger, now, ...(opts.isPidAlive ? { isPidAlive: opts.isPidAlive } : {}) });
280
+ if (!got.ok) {
281
+ process.stderr.write(`hq signals: ${company}: another run (${got.holder.trigger}, started ${got.holder.startedAt}) holds the lock; nothing started\n`);
282
+ locked.push({ company, status: "running", startedAt: got.holder.startedAt, trigger: got.holder.trigger });
283
+ continue;
284
+ }
285
+ release = got.release;
286
+ takeover = got.takeover;
287
+ }
288
+ const record = {
289
+ schemaVersion: RUN_RECORD_SCHEMA_VERSION,
290
+ company,
291
+ startedAt: now().toISOString(),
292
+ finishedAt: null,
293
+ trigger,
294
+ pid: process.pid,
295
+ catchUp: opts.catchUp === true,
296
+ dryRun,
297
+ status: "running",
298
+ skipReason: null,
299
+ currentStep: null,
300
+ steps: [],
301
+ maxBudgetUsd,
302
+ maxTurns,
303
+ jobRanAt: null,
304
+ nextRetryAt: null,
305
+ errorKind: null,
306
+ failureReason: null,
307
+ partialReason: null,
308
+ partial: null,
309
+ alreadyRanToday: false,
310
+ counts: { sourcesIn: 0, signalsOut: 0 },
311
+ ...(takeover ? { lockTakeover: takeover } : {}),
312
+ };
313
+ const persist = () => {
314
+ if (!dryRun)
315
+ write(rel, `${JSON.stringify(record, null, 2)}\n`);
316
+ };
317
+ const finish = () => {
318
+ const decidedSkip = record.skipReason;
319
+ finalizeRecord(record, { lateStart, lateStartHour: settings.lateStartHour, finishedAt: now() });
320
+ if (dryRun) {
321
+ if (!decidedSkip) {
322
+ record.status = "dry-run";
323
+ record.skipReason = null;
324
+ delete record.skippedReason;
325
+ }
326
+ return;
327
+ }
328
+ persist();
329
+ updateSummaryFile(hqRoot, date, record, record.finishedAt ?? now().toISOString());
330
+ };
331
+ try {
332
+ if (takeover) {
333
+ process.stderr.write(`hq signals: ${company}: took over a stale lock (${takeover.reason}) from pid ${takeover.pid}, started ${takeover.startedAt}\n`);
334
+ }
335
+ // 2. The day's job already ran: do nothing and leave the record as it is.
336
+ const prior = readJson(hqRoot, rel);
337
+ const priorJobRanAt = typeof prior?.jobRanAt === "string" ? prior.jobRanAt : null;
338
+ if (trigger === "job" && !opts.force && prior && priorJobRanAt) {
339
+ process.stderr.write(`hq signals: ${company}: already-ran (the job ran at ${priorJobRanAt})\n`);
340
+ records.push({ ...prior, alreadyRanToday: true });
341
+ continue;
342
+ }
343
+ record.jobRanAt = priorJobRanAt;
344
+ // 3. Mark the run as running before any step.
345
+ persist();
346
+ let signalsOn;
347
+ let ontologyOn;
348
+ try {
349
+ signalsOn = readPref(hqRoot, company, "signals_capture", spawn);
350
+ ontologyOn = readPref(hqRoot, company, "ontology_capture", spawn);
351
+ }
352
+ catch (err) {
353
+ if (!(err instanceof SignalsConfigError))
354
+ throw err;
355
+ record.currentStep = null;
356
+ record.finishedAt = now().toISOString();
357
+ record.status = dryRun ? "dry-run" : "failed";
358
+ record.errorKind = "config";
359
+ record.failureReason = sentence(err.message.charAt(0).toUpperCase() + err.message.slice(1));
360
+ record.nextRetryAt = null;
361
+ if (!dryRun) {
362
+ persist();
363
+ updateSummaryFile(hqRoot, date, record, record.finishedAt ?? now().toISOString());
364
+ }
365
+ records.push(record);
366
+ continue;
367
+ }
368
+ if (!signalsOn && !ontologyOn) {
369
+ record.skipReason = "prefs-off";
370
+ finish();
371
+ records.push(record);
372
+ continue;
373
+ }
374
+ if (plan.length === 0) {
375
+ record.skipReason = lateStart ? "late-start" : "nothing-to-run";
376
+ finish();
377
+ records.push(record);
378
+ continue;
379
+ }
380
+ // 4. Run the steps.
381
+ for (const name of plan) {
382
+ record.currentStep = name;
383
+ persist();
384
+ const t0 = now().getTime();
385
+ const ctx = {
386
+ hqRoot,
387
+ company,
388
+ dryRun,
389
+ trigger,
390
+ catchUp: record.catchUp,
391
+ maxTurns,
392
+ maxBudgetUsd,
393
+ now,
394
+ spawn,
395
+ write,
396
+ ...(opts.hotChannelSync ? { hotChannelSync: opts.hotChannelSync } : {}),
397
+ ...(opts.uploadDeps ? { uploadDeps: opts.uploadDeps } : {}),
398
+ ...(opts.isFlagOn ? { isFlagOn: opts.isFlagOn } : {}),
399
+ ...(opts.registeredAdapterIds ? { registeredAdapterIds: opts.registeredAdapterIds } : {}),
400
+ };
401
+ let stepRec;
402
+ const planner = stepImpls[name].dryRun;
403
+ if (dryRun && planner) {
404
+ // A step with a read-only planner reports what it would do.
405
+ try {
406
+ const res = await planner.call(stepImpls[name], ctx);
407
+ const { errorKind: _ignored, ...rest } = res;
408
+ stepRec = { name, ...rest, status: "skipped", skippedReason: "dry-run", durationMs: 0 };
409
+ }
410
+ catch (err) {
411
+ const msg = err instanceof Error ? err.message : String(err);
412
+ stepRec = {
413
+ name,
414
+ itemsIn: 0,
415
+ itemsOut: 0,
416
+ durationMs: 0,
417
+ status: "skipped",
418
+ skippedReason: "dry-run",
419
+ plan: [`plan failed: ${msg.slice(0, 200)}`],
420
+ };
421
+ }
422
+ }
423
+ else if (dryRun) {
424
+ stepRec = { name, itemsIn: 0, itemsOut: 0, durationMs: 0, status: "skipped", skippedReason: "dry-run" };
425
+ }
426
+ else {
427
+ try {
428
+ const res = await stepImpls[name].run(ctx);
429
+ stepRec = { name, ...res, durationMs: Math.max(0, now().getTime() - t0) };
430
+ }
431
+ catch (err) {
432
+ const msg = err instanceof Error ? err.message : String(err);
433
+ stepRec = {
434
+ name,
435
+ itemsIn: 0,
436
+ itemsOut: 0,
437
+ durationMs: Math.max(0, now().getTime() - t0),
438
+ status: "failed",
439
+ error: msg.slice(0, 500),
440
+ errorKind: stepErrorKindFor(err),
441
+ };
442
+ }
443
+ }
444
+ record.steps.push(stepRec);
445
+ persist();
446
+ }
447
+ // 5. Final record and summary entry.
448
+ finish();
449
+ records.push(record);
450
+ }
451
+ catch (err) {
452
+ // Anything outside a step (a refused write, say): record the failure
453
+ // instead of leaving a "running" record behind.
454
+ const msg = err instanceof Error ? err.message : String(err);
455
+ record.currentStep = null;
456
+ record.finishedAt = now().toISOString();
457
+ record.status = dryRun ? "dry-run" : "failed";
458
+ record.errorKind = err instanceof WriteGuardError ? "config" : "transient";
459
+ record.failureReason = sentence(`The run stopped: ${msg.slice(0, 300)}`);
460
+ record.nextRetryAt =
461
+ record.errorKind === "transient" ? new Date(new Date(record.finishedAt).getTime() + RETRY_DELAY_MS).toISOString() : null;
462
+ try {
463
+ persist();
464
+ }
465
+ catch {
466
+ // the failure is already reported on stderr below
467
+ }
468
+ process.stderr.write(`hq signals: ${company}: ${msg}\n`);
469
+ records.push(record);
470
+ }
471
+ finally {
472
+ release?.();
473
+ }
474
+ }
475
+ const summary = buildSummary(date, trigger, records.map(summaryEntry), now().toISOString(), dryRun);
476
+ if (!dryRun && opts.signalsAlert) {
477
+ // The signals-failure alert (US-018). Already-ran and dry-run records are never judged.
478
+ const alerts = await raiseSignalsRunAlerts({ hqRoot, records, senders: opts.signalsAlert, now });
479
+ return { date, records, locked, summary, alerts };
480
+ }
481
+ return { date, records, locked, summary };
482
+ }
483
+ /**
484
+ * Exit code for `hq signals run`: 1 when a company failed (a job that already
485
+ * ran today does not count), else 3 when another run held a company's lock,
486
+ * else 0 (complete, partial, skipped, already ran).
487
+ */
488
+ export function signalsExitCode(result) {
489
+ if (result.records.some((r) => !r.alreadyRanToday && r.status === "failed"))
490
+ return 1;
491
+ if (result.locked.length > 0)
492
+ return 3;
493
+ return 0;
494
+ }
495
+ //# sourceMappingURL=engine.js.map