@indigoai-us/hq-cli 5.345.191 → 5.345.194

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 (129) hide show
  1. package/CHANGELOG.md +431 -0
  2. package/assets/bot-workers/setup/worker.yaml +91 -34
  3. package/dist/command-catalog.generated.d.ts +145 -19
  4. package/dist/command-catalog.generated.js +184 -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 +4 -3
  8. package/dist/commands/bot.js +265 -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 +46 -0
  16. package/dist/commands/signals-run.js +180 -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 +58 -0
  20. package/dist/commands/sources.js +294 -26
  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/commands/whoami.d.ts +7 -0
  24. package/dist/commands/whoami.js +35 -2
  25. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +31171 -52512
  26. package/dist/hq-cloud-sync-runner/package.json +1 -1
  27. package/dist/lib/bot/api.d.ts +3 -2
  28. package/dist/lib/bot/config.d.ts +2 -0
  29. package/dist/lib/bot/fleet-cron.d.ts +121 -0
  30. package/dist/lib/bot/fleet-cron.js +319 -0
  31. package/dist/lib/bot/fleet-local.d.ts +93 -1
  32. package/dist/lib/bot/fleet-local.js +546 -34
  33. package/dist/lib/bot/fleet-owner-companies.d.ts +12 -0
  34. package/dist/lib/bot/fleet-owner-companies.js +30 -0
  35. package/dist/lib/bot/fleet-watch.d.ts +25 -0
  36. package/dist/lib/bot/fleet-watch.js +51 -0
  37. package/dist/lib/bot/index.d.ts +1 -1
  38. package/dist/lib/bot/index.js +1 -1
  39. package/dist/lib/bot/job-run.d.ts +5 -4
  40. package/dist/lib/bot/job-run.js +5 -4
  41. package/dist/lib/bot/jobs/hot-signal-sync.d.ts +57 -3
  42. package/dist/lib/bot/jobs/hot-signal-sync.js +163 -4
  43. package/dist/lib/bot/local-jobs.d.ts +13 -2
  44. package/dist/lib/bot/local-jobs.js +17 -4
  45. package/dist/lib/bot/progress.d.ts +14 -0
  46. package/dist/lib/bot/progress.js +47 -1
  47. package/dist/lib/bot/run.d.ts +55 -2
  48. package/dist/lib/bot/run.js +192 -10
  49. package/dist/lib/bot/setup-tool-offer.d.ts +1 -0
  50. package/dist/lib/bot/setup-tool-offer.js +16 -0
  51. package/dist/lib/bot/signals-engine-flag.d.ts +18 -0
  52. package/dist/lib/bot/signals-engine-flag.js +30 -0
  53. package/dist/lib/daemon/catalog.d.ts +4 -0
  54. package/dist/lib/daemon/catalog.js +29 -9
  55. package/dist/lib/daemon/legacy.d.ts +6 -0
  56. package/dist/lib/daemon/legacy.js +16 -10
  57. package/dist/lib/daemon/run.d.ts +1 -0
  58. package/dist/lib/daemon/run.js +10 -0
  59. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  60. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  61. package/dist/lib/flag-registry.d.ts +13 -1
  62. package/dist/lib/flag-registry.js +46 -0
  63. package/dist/lib/hot-channels/adapters/attio.d.ts +75 -2
  64. package/dist/lib/hot-channels/adapters/attio.js +367 -29
  65. package/dist/lib/hot-channels/adapters/gmail.d.ts +14 -0
  66. package/dist/lib/hot-channels/adapters/gmail.js +59 -2
  67. package/dist/lib/hot-channels/adapters/linear.d.ts +24 -0
  68. package/dist/lib/hot-channels/adapters/linear.js +71 -1
  69. package/dist/lib/hot-channels/adapters/meetings.js +4 -1
  70. package/dist/lib/hot-channels/adapters/slack.js +7 -3
  71. package/dist/lib/hot-channels/alert.js +1 -1
  72. package/dist/lib/hot-channels/contract.d.ts +11 -0
  73. package/dist/lib/hot-channels/contract.js +1 -0
  74. package/dist/lib/hot-channels/cursors.js +1 -1
  75. package/dist/lib/hot-channels/landing.d.ts +6 -2
  76. package/dist/lib/hot-channels/landing.js +13 -6
  77. package/dist/lib/hot-channels/ledger.js +2 -0
  78. package/dist/lib/hot-channels/loop.d.ts +6 -0
  79. package/dist/lib/hot-channels/loop.js +19 -4
  80. package/dist/lib/hot-channels/registry.js +9 -1
  81. package/dist/lib/hot-channels/run.d.ts +18 -0
  82. package/dist/lib/hot-channels/run.js +15 -4
  83. package/dist/lib/hot-channels/signals-pass.d.ts +34 -2
  84. package/dist/lib/hot-channels/signals-pass.js +149 -38
  85. package/dist/lib/hot-channels/signals-redo.d.ts +60 -0
  86. package/dist/lib/hot-channels/signals-redo.js +207 -0
  87. package/dist/lib/hot-channels/sync-lock.js +22 -6
  88. package/dist/lib/mesh/live/daemon/install.d.ts +22 -0
  89. package/dist/lib/mesh/live/daemon/install.js +117 -30
  90. package/dist/lib/signals/adapters-catalog.d.ts +77 -0
  91. package/dist/lib/signals/adapters-catalog.js +194 -0
  92. package/dist/lib/signals/engine.d.ts +202 -0
  93. package/dist/lib/signals/engine.js +504 -0
  94. package/dist/lib/signals/failure-alert.d.ts +89 -0
  95. package/dist/lib/signals/failure-alert.js +175 -0
  96. package/dist/lib/signals/local-date.d.ts +3 -0
  97. package/dist/lib/signals/local-date.js +6 -0
  98. package/dist/lib/signals/projection.d.ts +42 -0
  99. package/dist/lib/signals/projection.js +67 -0
  100. package/dist/lib/signals/reply.d.ts +112 -0
  101. package/dist/lib/signals/reply.js +368 -0
  102. package/dist/lib/signals/run-lock.d.ts +36 -0
  103. package/dist/lib/signals/run-lock.js +148 -0
  104. package/dist/lib/signals/run-record.d.ts +24 -0
  105. package/dist/lib/signals/run-record.js +82 -0
  106. package/dist/lib/signals/sanitize.d.ts +105 -0
  107. package/dist/lib/signals/sanitize.js +439 -0
  108. package/dist/lib/signals/settings.d.ts +10 -0
  109. package/dist/lib/signals/settings.js +48 -0
  110. package/dist/lib/signals/steps/adapters.d.ts +14 -0
  111. package/dist/lib/signals/steps/adapters.js +156 -0
  112. package/dist/lib/signals/steps/entity-similarity.d.ts +10 -0
  113. package/dist/lib/signals/steps/entity-similarity.js +47 -0
  114. package/dist/lib/signals/steps/extract.d.ts +118 -0
  115. package/dist/lib/signals/steps/extract.js +793 -0
  116. package/dist/lib/signals/steps/garden.d.ts +24 -0
  117. package/dist/lib/signals/steps/garden.js +250 -0
  118. package/dist/lib/signals/steps/types.d.ts +117 -0
  119. package/dist/lib/signals/steps/types.js +4 -0
  120. package/dist/lib/signals/steps/upload.d.ts +43 -0
  121. package/dist/lib/signals/steps/upload.js +324 -0
  122. package/dist/lib/signals/write-guard.d.ts +63 -0
  123. package/dist/lib/signals/write-guard.js +287 -0
  124. package/dist/utils/resident-package-preload.js +16 -0
  125. package/dist/utils/vault-api.d.ts +1 -0
  126. package/dist/utils/vault-api.js +1 -1
  127. package/package.json +9 -9
  128. package/dist/lib/bot/fleet-default-flag.d.ts +0 -31
  129. package/dist/lib/bot/fleet-default-flag.js +0 -127
@@ -0,0 +1,105 @@
1
+ export declare const SANITIZED_CONTRIBUTION_SCHEMA = "sanitized-contribution.v1";
2
+ export declare const SANITIZER_VERSION = "local-sanitizer/v1";
3
+ export declare const SCHEMA_VERSION = "local-signal/v1";
4
+ export declare const DEFAULT_PROMPT_VERSION = "hq-signals-extract/v1";
5
+ export declare const MEMBER_SIGNALS_ROOT = "member-signals/";
6
+ export declare const SIGNAL_TYPES: readonly ["summary", "key_point", "decision", "action_item", "participant_contribution", "risk", "commitment", "question"];
7
+ export type SignalType = (typeof SIGNAL_TYPES)[number];
8
+ export declare const MAX_CONTENT_CHARS = 600;
9
+ export declare const MAX_CITATIONS = 8;
10
+ export declare const MAX_ENTITIES = 16;
11
+ export declare const MAX_FIELD_CHARS = 256;
12
+ export declare const MEETING_SOURCE_RE: RegExp;
13
+ export interface SanitizedContributionV1 {
14
+ schema: string;
15
+ sanitizerVersion: string;
16
+ observationId: string;
17
+ signalType: SignalType;
18
+ content: string;
19
+ citations: Array<{
20
+ startMs: number;
21
+ endMs: number;
22
+ }>;
23
+ sourceRef: string;
24
+ sourceDigest: string;
25
+ entityRefs: string[];
26
+ provisionalEntities: string[];
27
+ owner?: string;
28
+ reasoning?: string;
29
+ speaker?: string;
30
+ companyUid: string;
31
+ memberUid: string;
32
+ audienceKey: string;
33
+ audienceVersion: number;
34
+ audienceReceiptRef: string;
35
+ promptVersion: string;
36
+ schemaVersion: string;
37
+ classification: string;
38
+ projectionState: string;
39
+ projectionWatermark: string;
40
+ }
41
+ /** Same key set as hq-pro CONTRIBUTION_KEYS. */
42
+ export declare const CONTRIBUTION_KEYS: ReadonlySet<string>;
43
+ export declare function normalizeContent(s: string): string;
44
+ export declare function computeObservationId(c: {
45
+ memberUid: string;
46
+ sourceRef: string;
47
+ sourceDigest: string;
48
+ signalType: string;
49
+ audienceKey: string;
50
+ content: string;
51
+ promptVersion: string;
52
+ schemaVersion: string;
53
+ }): string;
54
+ export declare function sha256Digest(text: string): string;
55
+ /** Audience keys are `sha256:<hex>`; `:` is not a key-safe path character. */
56
+ export declare function audienceKeySegment(audienceKey: string): string;
57
+ export declare function memberSignalsPrefix(memberUid: string): string;
58
+ /** Canonical destination; the server requires this exact key. */
59
+ export declare function contributionKey(memberUid: string, audienceKey: string, signalType: SignalType, observationId: string): string;
60
+ /** Returns the list of problems; empty means the server's shape check passes. */
61
+ export declare function validateContributionShape(raw: unknown): string[];
62
+ /**
63
+ * Remove quoted passages longer than one sentence: a raw transcript excerpt
64
+ * of two or more sentences is replaced with `[quote removed]`.
65
+ */
66
+ export declare function stripLongQuotes(s: string): string;
67
+ /** Cap at MAX_CONTENT_CHARS code points, cutting at a word boundary. */
68
+ export declare function capContent(s: string, max?: number): string;
69
+ /** `HH:MM:SS`, `MM:SS`, or seconds → milliseconds. */
70
+ export declare function parseLocationMs(v: unknown): number | null;
71
+ export declare function splitFrontmatter(text: string): {
72
+ frontmatter: Record<string, unknown>;
73
+ body: string;
74
+ };
75
+ export interface AudienceReceiptLocal {
76
+ ref: string;
77
+ audienceKey: string;
78
+ audienceVersion: number;
79
+ sourceDigest: string;
80
+ invitedMemberUids: string[];
81
+ }
82
+ /** Highest-version audience receipt for a meeting, read from the local company tree. */
83
+ export declare function readAudienceReceipt(companyRoot: string, meetingId: string): AudienceReceiptLocal | null;
84
+ export interface SanitizeInput {
85
+ /** Absolute path of `companies/{co}`. */
86
+ companyRoot: string;
87
+ /** Company-relative path of the signal: `signals/@{key}/{type}/{sha}.md` or `signals/{type}/{sha}.md`. */
88
+ signalRel: string;
89
+ memberUid: string;
90
+ companyUid: string;
91
+ projectionState: string;
92
+ projectionWatermark: string;
93
+ }
94
+ export type SanitizeResult = {
95
+ ok: true;
96
+ contribution: SanitizedContributionV1;
97
+ destination: string;
98
+ sha: string;
99
+ } | {
100
+ ok: false;
101
+ reason: string;
102
+ sha: string;
103
+ };
104
+ export declare function sanitizeSignal(input: SanitizeInput): SanitizeResult;
105
+ //# sourceMappingURL=sanitize.d.ts.map
@@ -0,0 +1,439 @@
1
+ /**
2
+ * TypeScript sanitizer for local promoted signals (Hot-Signal Sync US-005).
3
+ *
4
+ * Turns `companies/{co}/signals/@{key}/{type}/{sha}.md` (or
5
+ * `signals/{type}/{sha}.md`, audience `company`) into a
6
+ * `sanitized-contribution.v1` object, the body hq-pro's
7
+ * `POST /v1/files/contributions/company/{slug}/meetings` accepts.
8
+ *
9
+ * The shape, caps and observation id mirror hq-pro
10
+ * `src/signals/member-contributions/contribution.ts` (origin/main 9f172092d)
11
+ * exactly: the server rejects anything else with CONTRIBUTION_REJECTED. The
12
+ * desktop's Rust sanitizer (`local_signals/sanitize.rs`) is the reference
13
+ * producer; this file is its CLI twin.
14
+ *
15
+ * What the contribution never carries to member-signals: citation quote text,
16
+ * the signal body beyond MAX_CONTENT_CHARS, quoted passages longer than one
17
+ * sentence, and any field not in CONTRIBUTION_KEYS.
18
+ *
19
+ * This limits the contribution only. Hot-Signal Sync does not change vault
20
+ * sync (owner decision 2026-11-01: "we shouldn't interrupt existing processes
21
+ * until we are sure things are working"). The promoted signal file itself
22
+ * (companies/{co}/signals/@{key}/{type}/{sha}.md, with its full body and
23
+ * citation quotes) and company sources/ stay local today, because hq-cloud
24
+ * refuses client uploads of signals/** and sources/**. Sharing promoted
25
+ * signals with the company through audience ACLs is a later, separate
26
+ * hq-cloud change. The signals and ontology _candidates folders are in the
27
+ * shipped .hqignore.
28
+ */
29
+ import { createHash } from "node:crypto";
30
+ import * as fs from "node:fs";
31
+ import * as path from "node:path";
32
+ import yaml from "js-yaml";
33
+ export const SANITIZED_CONTRIBUTION_SCHEMA = "sanitized-contribution.v1";
34
+ export const SANITIZER_VERSION = "local-sanitizer/v1";
35
+ export const SCHEMA_VERSION = "local-signal/v1";
36
+ export const DEFAULT_PROMPT_VERSION = "hq-signals-extract/v1";
37
+ export const MEMBER_SIGNALS_ROOT = "member-signals/";
38
+ const OBSERVATION_ID_DOMAIN = "hq-local-meeting-observation-id/v1";
39
+ export const SIGNAL_TYPES = [
40
+ "summary",
41
+ "key_point",
42
+ "decision",
43
+ "action_item",
44
+ "participant_contribution",
45
+ "risk",
46
+ "commitment",
47
+ "question",
48
+ ];
49
+ export const MAX_CONTENT_CHARS = 600;
50
+ export const MAX_CITATIONS = 8;
51
+ export const MAX_ENTITIES = 16;
52
+ export const MAX_FIELD_CHARS = 256;
53
+ const MAX_ENTITY_CHARS = 200;
54
+ const UID_RE = /^[A-Za-z0-9_-]{1,128}$/;
55
+ const SEGMENT_RE = /^[A-Za-z0-9_.-]{1,256}$/;
56
+ const OBSERVATION_ID_RE = /^[0-9a-f]{64}$/;
57
+ export const MEETING_SOURCE_RE = /^sources\/meetings\/([^/\\]+)\.md$/;
58
+ /** Same key set as hq-pro CONTRIBUTION_KEYS. */
59
+ export const CONTRIBUTION_KEYS = new Set([
60
+ "schema", "sanitizerVersion", "observationId", "signalType", "content", "citations", "sourceRef",
61
+ "sourceDigest", "entityRefs", "provisionalEntities", "owner", "reasoning", "speaker", "companyUid",
62
+ "memberUid", "audienceKey", "audienceVersion", "audienceReceiptRef", "promptVersion", "schemaVersion",
63
+ "classification", "projectionState", "projectionWatermark",
64
+ ]);
65
+ // ── identity (byte-for-byte with hq-pro) ─────────────────────────────────────
66
+ const WHITE_SPACE_RE = new RegExp("[" +
67
+ [0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x20, 0x85, 0xa0, 0x1680, 0x2028, 0x2029, 0x202f, 0x205f, 0x3000]
68
+ .map((c) => `\\u${c.toString(16).padStart(4, "0")}`)
69
+ .join("") +
70
+ "\\u2000-\\u200a]+");
71
+ export function normalizeContent(s) {
72
+ return s
73
+ .split(WHITE_SPACE_RE)
74
+ .filter((w) => w.length > 0)
75
+ .join(" ")
76
+ .toLowerCase();
77
+ }
78
+ export function computeObservationId(c) {
79
+ const h = createHash("sha256");
80
+ for (const part of [
81
+ OBSERVATION_ID_DOMAIN,
82
+ c.memberUid,
83
+ c.sourceRef,
84
+ c.sourceDigest,
85
+ c.signalType,
86
+ c.audienceKey,
87
+ normalizeContent(c.content),
88
+ c.promptVersion,
89
+ c.schemaVersion,
90
+ ]) {
91
+ const bytes = Buffer.from(part, "utf8");
92
+ const len = Buffer.alloc(8);
93
+ len.writeBigUInt64BE(BigInt(bytes.length));
94
+ h.update(len);
95
+ h.update(bytes);
96
+ }
97
+ return h.digest("hex");
98
+ }
99
+ export function sha256Digest(text) {
100
+ return `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`;
101
+ }
102
+ /** Audience keys are `sha256:<hex>`; `:` is not a key-safe path character. */
103
+ export function audienceKeySegment(audienceKey) {
104
+ return audienceKey.replace(/^sha256:/, "sha256-");
105
+ }
106
+ export function memberSignalsPrefix(memberUid) {
107
+ if (!UID_RE.test(memberUid))
108
+ throw new Error("invalid member uid");
109
+ return `${MEMBER_SIGNALS_ROOT}${memberUid}/`;
110
+ }
111
+ /** Canonical destination; the server requires this exact key. */
112
+ export function contributionKey(memberUid, audienceKey, signalType, observationId) {
113
+ const seg = audienceKeySegment(audienceKey);
114
+ if (!SEGMENT_RE.test(seg) || !OBSERVATION_ID_RE.test(observationId))
115
+ throw new Error("invalid contribution path");
116
+ return `${memberSignalsPrefix(memberUid)}observations/meetings/@${seg}/${signalType}/${observationId}.md`;
117
+ }
118
+ // ── shape validation (mirror of hq-pro parseShape) ───────────────────────────
119
+ function isRecord(v) {
120
+ return typeof v === "object" && v !== null && !Array.isArray(v);
121
+ }
122
+ function hasControlChar(v) {
123
+ for (const ch of v) {
124
+ const c = ch.codePointAt(0) ?? 0;
125
+ if (c < 0x20 || c === 0x7f)
126
+ return true;
127
+ }
128
+ return false;
129
+ }
130
+ function boundedString(v, max = MAX_FIELD_CHARS) {
131
+ return typeof v === "string" && v.trim().length > 0 && [...v].length <= max && !hasControlChar(v);
132
+ }
133
+ /** Returns the list of problems; empty means the server's shape check passes. */
134
+ export function validateContributionShape(raw) {
135
+ const detail = [];
136
+ if (!isRecord(raw))
137
+ return ["contribution must be an object"];
138
+ for (const k of Object.keys(raw))
139
+ if (!CONTRIBUTION_KEYS.has(k))
140
+ detail.push(`unknown field ${k}`);
141
+ const str = [
142
+ "schema", "sanitizerVersion", "observationId", "sourceRef", "sourceDigest", "companyUid", "memberUid",
143
+ "audienceKey", "audienceReceiptRef", "promptVersion", "schemaVersion", "classification",
144
+ "projectionState", "projectionWatermark",
145
+ ];
146
+ for (const k of str)
147
+ if (!boundedString(raw[k]))
148
+ detail.push(`${k} must be a bounded string`);
149
+ if (!SIGNAL_TYPES.includes(raw.signalType))
150
+ detail.push("signalType is not a registered type");
151
+ if (!boundedString(raw.content, MAX_CONTENT_CHARS))
152
+ detail.push("content is empty or too long");
153
+ if (!Number.isInteger(raw.audienceVersion) || raw.audienceVersion < 1)
154
+ detail.push("audienceVersion must be a positive integer");
155
+ const cites = raw.citations;
156
+ if (!Array.isArray(cites) || cites.length === 0 || cites.length > MAX_CITATIONS) {
157
+ detail.push(`citations must hold 1-${MAX_CITATIONS} ranges`);
158
+ }
159
+ else {
160
+ for (const c of cites) {
161
+ if (!isRecord(c) || Object.keys(c).some((k) => k !== "startMs" && k !== "endMs") ||
162
+ !Number.isInteger(c.startMs) || !Number.isInteger(c.endMs) ||
163
+ c.startMs < 0 || c.startMs > c.endMs) {
164
+ detail.push("citation must be {startMs <= endMs}");
165
+ break;
166
+ }
167
+ }
168
+ }
169
+ for (const k of ["entityRefs", "provisionalEntities"]) {
170
+ const v = raw[k];
171
+ if (!Array.isArray(v) || v.length > MAX_ENTITIES || !v.every((x) => boundedString(x, MAX_ENTITY_CHARS))) {
172
+ detail.push(`${k} must be at most ${MAX_ENTITIES} bounded strings`);
173
+ }
174
+ }
175
+ for (const k of ["owner", "reasoning", "speaker"]) {
176
+ if (raw[k] !== undefined && !boundedString(raw[k], MAX_ENTITY_CHARS))
177
+ detail.push(`${k} must be a bounded string`);
178
+ }
179
+ if (raw.schema !== SANITIZED_CONTRIBUTION_SCHEMA)
180
+ detail.push("schema must be sanitized-contribution.v1");
181
+ if (raw.sanitizerVersion !== SANITIZER_VERSION)
182
+ detail.push("sanitizerVersion must be local-sanitizer/v1");
183
+ if (raw.schemaVersion !== SCHEMA_VERSION)
184
+ detail.push("schemaVersion must be local-signal/v1");
185
+ if (raw.classification !== "company-eligible")
186
+ detail.push("classification must be company-eligible");
187
+ return detail;
188
+ }
189
+ // ── text scrubbing ───────────────────────────────────────────────────────────
190
+ /** Collapse all whitespace (and control characters) to single spaces. */
191
+ function flatten(s) {
192
+ // eslint-disable-next-line no-control-regex
193
+ return s.replace(/[\u0000-\u001f\u007f]+/g, " ").split(WHITE_SPACE_RE).filter(Boolean).join(" ").trim();
194
+ }
195
+ function sentenceCount(s) {
196
+ return s.split(/[.!?]+(?:\s+|$)/).filter((x) => x.trim().length > 0).length;
197
+ }
198
+ /**
199
+ * Remove quoted passages longer than one sentence: a raw transcript excerpt
200
+ * of two or more sentences is replaced with `[quote removed]`.
201
+ */
202
+ export function stripLongQuotes(s) {
203
+ const patterns = [/"([^"]*)"/g, /“([^”]*)”/g, /^>.*$/gm];
204
+ let out = s;
205
+ out = out.replace(patterns[2], (m) => (sentenceCount(m.replace(/^>\s?/, "")) > 1 ? "[quote removed]" : m.replace(/^>\s?/, "")));
206
+ for (const re of patterns.slice(0, 2)) {
207
+ out = out.replace(re, (m, inner) => (sentenceCount(inner) > 1 ? "[quote removed]" : m));
208
+ }
209
+ return out;
210
+ }
211
+ /** Strip markdown headings and emphasis; keep the words. */
212
+ function stripMarkdown(s) {
213
+ return s
214
+ .replace(/^#{1,6}\s+.*$/gm, " ")
215
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
216
+ .replace(/__([^_]+)__/g, "$1")
217
+ .replace(/`([^`]+)`/g, "$1")
218
+ .replace(/^\s*[-*]\s+/gm, "");
219
+ }
220
+ /** Cap at MAX_CONTENT_CHARS code points, cutting at a word boundary. */
221
+ export function capContent(s, max = MAX_CONTENT_CHARS) {
222
+ const chars = [...s];
223
+ if (chars.length <= max)
224
+ return s;
225
+ const cut = chars.slice(0, max - 1).join("");
226
+ const space = cut.lastIndexOf(" ");
227
+ return `${(space > max / 2 ? cut.slice(0, space) : cut).trimEnd()}…`;
228
+ }
229
+ function boundedOrUndefined(v) {
230
+ if (typeof v !== "string")
231
+ return undefined;
232
+ const f = flatten(v);
233
+ return f.length > 0 && [...f].length <= MAX_ENTITY_CHARS ? f : undefined;
234
+ }
235
+ function stringList(v) {
236
+ if (!Array.isArray(v))
237
+ return [];
238
+ const out = [];
239
+ for (const x of v) {
240
+ const s = typeof x === "string" ? x : isRecord(x) && typeof x.name === "string" ? x.name : null;
241
+ const b = s === null ? undefined : boundedOrUndefined(s);
242
+ if (b && !out.includes(b))
243
+ out.push(b);
244
+ if (out.length >= MAX_ENTITIES)
245
+ break;
246
+ }
247
+ return out;
248
+ }
249
+ /** `HH:MM:SS`, `MM:SS`, or seconds → milliseconds. */
250
+ export function parseLocationMs(v) {
251
+ if (typeof v === "number" && Number.isFinite(v) && v >= 0)
252
+ return Math.round(v * 1000);
253
+ if (typeof v !== "string")
254
+ return null;
255
+ const m = /^\s*(?:(\d+):)?(\d{1,2}):(\d{2}(?:\.\d+)?)\s*$/.exec(v);
256
+ if (!m)
257
+ return null;
258
+ const h = m[1] ? Number(m[1]) : 0;
259
+ return Math.round((h * 3600 + Number(m[2]) * 60 + Number(m[3])) * 1000);
260
+ }
261
+ function citationRanges(raw, sourceRef) {
262
+ if (!Array.isArray(raw))
263
+ return [];
264
+ const out = [];
265
+ for (const c of raw) {
266
+ if (!isRecord(c))
267
+ continue;
268
+ if (typeof c.source_ref === "string" && c.source_ref !== sourceRef)
269
+ continue;
270
+ const start = Number.isInteger(c.start_ms) ? c.start_ms : Number.isInteger(c.startMs) ? c.startMs : parseLocationMs(c.location ?? c.timestamp);
271
+ if (start === null || start < 0)
272
+ continue;
273
+ const endRaw = Number.isInteger(c.end_ms) ? c.end_ms : Number.isInteger(c.endMs) ? c.endMs : null;
274
+ const end = endRaw !== null && endRaw >= start ? endRaw : start;
275
+ out.push({ startMs: start, endMs: end });
276
+ if (out.length >= MAX_CITATIONS)
277
+ break;
278
+ }
279
+ return out;
280
+ }
281
+ // ── local files ──────────────────────────────────────────────────────────────
282
+ export function splitFrontmatter(text) {
283
+ const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(text);
284
+ if (!m)
285
+ return { frontmatter: {}, body: text };
286
+ let parsed;
287
+ try {
288
+ parsed = yaml.load(m[1]);
289
+ }
290
+ catch {
291
+ parsed = null;
292
+ }
293
+ return { frontmatter: isRecord(parsed) ? parsed : {}, body: m[2] };
294
+ }
295
+ /** Highest-version audience receipt for a meeting, read from the local company tree. */
296
+ export function readAudienceReceipt(companyRoot, meetingId) {
297
+ const dir = path.join(companyRoot, "sources", "meetings");
298
+ let names;
299
+ try {
300
+ names = fs.readdirSync(dir);
301
+ }
302
+ catch {
303
+ return null;
304
+ }
305
+ let best = null;
306
+ for (const n of names) {
307
+ if (!n.startsWith(`${meetingId}.audience`))
308
+ continue;
309
+ const m = n === `${meetingId}.audience.json` ? ["", "1"] : new RegExp(`^${meetingId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.audience\\.v(\\d+)\\.json$`).exec(n);
310
+ if (!m)
311
+ continue;
312
+ const v = Number(m[1]);
313
+ if (!best || v > best.version)
314
+ best = { version: v, name: n };
315
+ }
316
+ if (!best)
317
+ return null;
318
+ try {
319
+ const r = JSON.parse(fs.readFileSync(path.join(dir, best.name), "utf8"));
320
+ if (typeof r.audienceKey !== "string" || !Number.isInteger(r.audienceVersion) || typeof r.sourceDigest !== "string")
321
+ return null;
322
+ return {
323
+ ref: `sources/meetings/${best.name}`,
324
+ audienceKey: r.audienceKey,
325
+ audienceVersion: r.audienceVersion,
326
+ sourceDigest: r.sourceDigest,
327
+ invitedMemberUids: Array.isArray(r.invitedMemberUids) ? r.invitedMemberUids.filter((x) => typeof x === "string") : [],
328
+ };
329
+ }
330
+ catch {
331
+ return null;
332
+ }
333
+ }
334
+ const SIGNAL_REL_RE = /^signals\/(?:@([^/]+)\/)?([a-z_]+)\/([^/]+)\.md$/;
335
+ export function sanitizeSignal(input) {
336
+ const m = SIGNAL_REL_RE.exec(input.signalRel);
337
+ const sha = m?.[3] ?? path.basename(input.signalRel, ".md");
338
+ const fail = (reason) => ({ ok: false, reason, sha });
339
+ if (!m)
340
+ return fail("not-a-signal-path");
341
+ const folderType = m[2];
342
+ let text;
343
+ try {
344
+ text = fs.readFileSync(path.join(input.companyRoot, input.signalRel), "utf8");
345
+ }
346
+ catch {
347
+ return fail("unreadable");
348
+ }
349
+ const { frontmatter: fm, body } = splitFrontmatter(text);
350
+ const signalType = (typeof fm.type === "string" ? fm.type : folderType);
351
+ if (!SIGNAL_TYPES.includes(signalType) || signalType !== folderType)
352
+ return fail("unknown-type");
353
+ const sourceRef = typeof fm.source_ref === "string" ? fm.source_ref : "";
354
+ const meeting = MEETING_SOURCE_RE.exec(sourceRef);
355
+ if (!meeting)
356
+ return fail("non-meeting-source");
357
+ const receipt = readAudienceReceipt(input.companyRoot, meeting[1]);
358
+ if (!receipt)
359
+ return fail("no-audience-receipt");
360
+ if (!receipt.invitedMemberUids.includes(input.memberUid))
361
+ return fail("not-in-audience");
362
+ // sourceDigest: sha256 of the local source body when readable. The server
363
+ // checks the digest against the frozen receipt and its own bucket copy, so
364
+ // when the local copy differs (edited, or parsed with different frontmatter
365
+ // rules) the receipt's frozen digest is the one that can pass.
366
+ let sourceDigest = receipt.sourceDigest;
367
+ try {
368
+ const src = fs.readFileSync(path.join(input.companyRoot, sourceRef), "utf8");
369
+ const local = sha256Digest(splitFrontmatter(src).body);
370
+ if (local === receipt.sourceDigest)
371
+ sourceDigest = local;
372
+ }
373
+ catch {
374
+ // Source not on disk: the receipt's frozen digest stands.
375
+ }
376
+ const citations = citationRanges(fm.citations, sourceRef);
377
+ if (citations.length === 0)
378
+ return fail("no-timestamps");
379
+ const rawContent = typeof fm.canonical_content === "string" && fm.canonical_content.trim()
380
+ ? fm.canonical_content
381
+ : stripMarkdown(body);
382
+ const content = capContent(flatten(stripLongQuotes(rawContent)));
383
+ if (!content)
384
+ return fail("empty-content");
385
+ const promptVersion = boundedOrUndefined(fm.prompt_version) ?? DEFAULT_PROMPT_VERSION;
386
+ const entityRefs = stringList(fm.entity_refs);
387
+ const provisionalEntities = stringList(fm.provisional_entities ?? fm.entity_mentions).filter((e) => !entityRefs.includes(e));
388
+ const base = {
389
+ memberUid: input.memberUid,
390
+ sourceRef,
391
+ sourceDigest,
392
+ signalType,
393
+ audienceKey: receipt.audienceKey,
394
+ content,
395
+ promptVersion,
396
+ schemaVersion: SCHEMA_VERSION,
397
+ };
398
+ const observationId = computeObservationId(base);
399
+ const contribution = {
400
+ schema: SANITIZED_CONTRIBUTION_SCHEMA,
401
+ sanitizerVersion: SANITIZER_VERSION,
402
+ observationId,
403
+ signalType,
404
+ content,
405
+ citations,
406
+ sourceRef,
407
+ sourceDigest,
408
+ entityRefs,
409
+ provisionalEntities,
410
+ companyUid: input.companyUid,
411
+ memberUid: input.memberUid,
412
+ audienceKey: receipt.audienceKey,
413
+ audienceVersion: receipt.audienceVersion,
414
+ audienceReceiptRef: receipt.ref,
415
+ promptVersion,
416
+ schemaVersion: SCHEMA_VERSION,
417
+ classification: "company-eligible",
418
+ projectionState: input.projectionState,
419
+ projectionWatermark: input.projectionWatermark,
420
+ };
421
+ const owner = boundedOrUndefined(fm.owner);
422
+ if (owner)
423
+ contribution.owner = owner;
424
+ const speaker = boundedOrUndefined(fm.speaker);
425
+ if (speaker)
426
+ contribution.speaker = speaker;
427
+ const problems = validateContributionShape(contribution);
428
+ if (problems.length > 0)
429
+ return fail(`shape: ${problems[0]}`);
430
+ let destination;
431
+ try {
432
+ destination = contributionKey(input.memberUid, receipt.audienceKey, signalType, observationId);
433
+ }
434
+ catch {
435
+ return fail("unaddressable");
436
+ }
437
+ return { ok: true, contribution, destination, sha };
438
+ }
439
+ //# sourceMappingURL=sanitize.js.map
@@ -0,0 +1,10 @@
1
+ export interface SignalsSettings {
2
+ maxTurns: number;
3
+ maxBudgetUsd: number;
4
+ lateStartHour: number;
5
+ briefWaitMinutes: number;
6
+ }
7
+ export declare const DEFAULT_SIGNALS_SETTINGS: SignalsSettings;
8
+ export declare const SIGNALS_SETTINGS_REL: string;
9
+ export declare function loadSignalsSettings(hqRoot: string): SignalsSettings;
10
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +1,48 @@
1
+ /** Loads core/settings/signals.yaml from the HQ root, with built-in defaults. */
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import yaml from "js-yaml";
5
+ export const DEFAULT_SIGNALS_SETTINGS = {
6
+ maxTurns: 40,
7
+ maxBudgetUsd: 2.0,
8
+ lateStartHour: 22,
9
+ briefWaitMinutes: 20,
10
+ };
11
+ /** maxTurns: whole number >= 1. lateStartHour: whole hour 1-24 (0 would make every job late; 24 turns it off). */
12
+ const VALID = {
13
+ maxTurns: (v) => Number.isInteger(v) && v >= 1,
14
+ maxBudgetUsd: (v) => Number.isFinite(v) && v >= 0,
15
+ lateStartHour: (v) => Number.isInteger(v) && v >= 1 && v <= 24,
16
+ briefWaitMinutes: (v) => Number.isFinite(v) && v >= 0,
17
+ };
18
+ export const SIGNALS_SETTINGS_REL = path.join("core", "settings", "signals.yaml");
19
+ export function loadSignalsSettings(hqRoot) {
20
+ const out = { ...DEFAULT_SIGNALS_SETTINGS };
21
+ let raw;
22
+ try {
23
+ raw = yaml.load(fs.readFileSync(path.join(hqRoot, SIGNALS_SETTINGS_REL), "utf8"));
24
+ }
25
+ catch (err) {
26
+ if (err.code !== "ENOENT") {
27
+ const msg = err instanceof Error ? err.message : String(err);
28
+ process.stderr.write(`hq signals: ignoring unreadable ${SIGNALS_SETTINGS_REL}: ${msg}\n`);
29
+ }
30
+ return out;
31
+ }
32
+ if (!raw || typeof raw !== "object")
33
+ return out;
34
+ const obj = raw;
35
+ for (const key of Object.keys(out)) {
36
+ const v = obj[key];
37
+ if (v === undefined)
38
+ continue;
39
+ if (typeof v === "number" && VALID[key](v)) {
40
+ out[key] = v;
41
+ }
42
+ else {
43
+ process.stderr.write(`hq signals: ignoring invalid ${key} in ${SIGNALS_SETTINGS_REL}: ${String(v)}\n`);
44
+ }
45
+ }
46
+ return out;
47
+ }
48
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1,14 @@
1
+ import type { OnDemandSyncReport } from "../../hot-channels/run.js";
2
+ import type { HotChannelSyncFn, SignalsStep, StepContext, StepResult } from "./types.js";
3
+ export declare const defaultHotChannelSync: HotChannelSyncFn;
4
+ export declare function mapSyncReport(report: OnDemandSyncReport): StepResult;
5
+ /**
6
+ * Mirror the signed-in person's vault cursors
7
+ * (`companies/{co}/sources/_cursors/@{personKey}/{key}.json`, written by the
8
+ * loop only after a page's sources land) to the local watermark
9
+ * `workspace/signals/watermarks/{co}/{key}.json`. Teammates' cursor
10
+ * directories are never read: their watermarks are not this machine's.
11
+ */
12
+ export declare function mirrorWatermarks(ctx: Pick<StepContext, "hqRoot" | "company" | "write">, personKey: string): string[];
13
+ export declare const adaptersStep: SignalsStep;
14
+ //# sourceMappingURL=adapters.d.ts.map