haansi 0.1.47 → 0.1.49

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 (2) hide show
  1. package/dist/haansi.js +2534 -645
  2. package/package.json +1 -1
package/dist/haansi.js CHANGED
@@ -95,7 +95,7 @@ var require_package = __commonJS({
95
95
  "package.json"(exports2, module2) {
96
96
  module2.exports = {
97
97
  name: "haansi",
98
- version: "0.1.47",
98
+ version: "0.1.49",
99
99
  description: "Haansi CLI - Session collector and MCP server for Claude Code & OpenAI Codex",
100
100
  bin: {
101
101
  haansi: "./dist/haansi.js"
@@ -122,13 +122,694 @@ var require_package = __commonJS({
122
122
  }
123
123
  });
124
124
 
125
+ // ../service-capture/common/agents/types.ts
126
+ var AGENT_IDS;
127
+ var init_types = __esm({
128
+ "../service-capture/common/agents/types.ts"() {
129
+ "use strict";
130
+ AGENT_IDS = ["claude-code", "codex"];
131
+ }
132
+ });
133
+
134
+ // ../service-capture/common/agents/claude-code.ts
135
+ function projectsDir() {
136
+ return (0, import_path.join)((0, import_os.homedir)(), ".claude", "projects");
137
+ }
138
+ function extractMeta(filePath) {
139
+ const result = {
140
+ cwd: null,
141
+ timestamp: null,
142
+ gitBranch: null
143
+ };
144
+ try {
145
+ const lines = (0, import_fs.readFileSync)(filePath, "utf-8").split("\n").filter((l) => l.trim()).slice(0, 50);
146
+ for (const line of lines) {
147
+ try {
148
+ const entry2 = JSON.parse(line);
149
+ if (!result.cwd && typeof entry2.cwd === "string")
150
+ result.cwd = entry2.cwd;
151
+ if (!result.timestamp && typeof entry2.timestamp === "string")
152
+ result.timestamp = entry2.timestamp;
153
+ if (!result.gitBranch && typeof entry2.gitBranch === "string")
154
+ result.gitBranch = entry2.gitBranch;
155
+ if (result.cwd && result.timestamp && result.gitBranch) break;
156
+ } catch {
157
+ }
158
+ }
159
+ } catch {
160
+ }
161
+ return result;
162
+ }
163
+ var import_fs, import_path, import_os, claudeCodeAgent;
164
+ var init_claude_code = __esm({
165
+ "../service-capture/common/agents/claude-code.ts"() {
166
+ "use strict";
167
+ import_fs = require("fs");
168
+ import_path = require("path");
169
+ import_os = require("os");
170
+ claudeCodeAgent = {
171
+ id: "claude-code",
172
+ displayName: "Claude Code",
173
+ sessionsRoot() {
174
+ return projectsDir();
175
+ },
176
+ isInstalled() {
177
+ return (0, import_fs.existsSync)(projectsDir());
178
+ },
179
+ discoverSessions() {
180
+ const root = projectsDir();
181
+ if (!(0, import_fs.existsSync)(root)) return [];
182
+ const sessions = [];
183
+ const projectDirs = (0, import_fs.readdirSync)(root).map((name) => (0, import_path.join)(root, name)).filter((p) => {
184
+ try {
185
+ return (0, import_fs.statSync)(p).isDirectory();
186
+ } catch {
187
+ return false;
188
+ }
189
+ });
190
+ for (const dir of projectDirs) {
191
+ let files;
192
+ try {
193
+ files = (0, import_fs.readdirSync)(dir).filter((f) => f.endsWith(".jsonl"));
194
+ } catch {
195
+ continue;
196
+ }
197
+ for (const file2 of files) {
198
+ const filePath = (0, import_path.join)(dir, file2);
199
+ const sessionId = (0, import_path.basename)(file2, ".jsonl");
200
+ try {
201
+ const stat = (0, import_fs.statSync)(filePath);
202
+ const content = (0, import_fs.readFileSync)(filePath, "utf-8");
203
+ const lineCount = content.split("\n").filter((l) => l.trim()).length;
204
+ const meta3 = extractMeta(filePath);
205
+ sessions.push({
206
+ sessionId,
207
+ filePath,
208
+ lineCount,
209
+ sizeBytes: stat.size,
210
+ ...meta3
211
+ });
212
+ } catch {
213
+ }
214
+ }
215
+ }
216
+ return sessions;
217
+ }
218
+ };
219
+ }
220
+ });
221
+
222
+ // ../service-capture/common/agents/codex.ts
223
+ function codexHome() {
224
+ return process.env.CODEX_HOME || (0, import_path2.join)((0, import_os2.homedir)(), ".codex");
225
+ }
226
+ function sessionsDir() {
227
+ return (0, import_path2.join)(codexHome(), "sessions");
228
+ }
229
+ function walkRollouts(dir, out) {
230
+ let entries;
231
+ try {
232
+ entries = (0, import_fs2.readdirSync)(dir);
233
+ } catch {
234
+ return;
235
+ }
236
+ for (const name of entries) {
237
+ const full = (0, import_path2.join)(dir, name);
238
+ let isDir = false;
239
+ try {
240
+ isDir = (0, import_fs2.statSync)(full).isDirectory();
241
+ } catch {
242
+ continue;
243
+ }
244
+ if (isDir) {
245
+ walkRollouts(full, out);
246
+ } else if (name.startsWith("rollout-") && name.endsWith(".jsonl")) {
247
+ out.push(full);
248
+ }
249
+ }
250
+ }
251
+ function readRolloutMeta(filePath, content) {
252
+ const fileName = (0, import_path2.basename)(filePath, ".jsonl");
253
+ const fromName = UUID_RE.exec(fileName)?.[1];
254
+ let cwd = null;
255
+ let timestamp = null;
256
+ let gitBranch = null;
257
+ let gitRemoteUrl = null;
258
+ let gitCommit = null;
259
+ let metaId = null;
260
+ const lines = content.split("\n").filter((l) => l.trim()).slice(0, 50);
261
+ for (const line of lines) {
262
+ try {
263
+ const entry2 = JSON.parse(line);
264
+ if (!timestamp && typeof entry2.timestamp === "string") {
265
+ timestamp = entry2.timestamp;
266
+ }
267
+ const p = entry2.payload ?? {};
268
+ if (entry2.type === "session_meta") {
269
+ if (typeof p.cwd === "string") cwd = p.cwd;
270
+ if (typeof p.id === "string") metaId = p.id;
271
+ const g = p.git ?? {};
272
+ const branch = g.branch ?? p.git_branch;
273
+ if (typeof branch === "string") gitBranch = branch;
274
+ const remote = g.repository_url ?? g.remote_url;
275
+ if (typeof remote === "string") gitRemoteUrl = remote;
276
+ const commit = g.commit_hash ?? g.commit;
277
+ if (typeof commit === "string") gitCommit = commit;
278
+ }
279
+ if (cwd && gitBranch && gitCommit && timestamp && metaId) break;
280
+ } catch {
281
+ }
282
+ }
283
+ return {
284
+ sessionId: fromName ?? metaId ?? fileName,
285
+ cwd,
286
+ timestamp,
287
+ gitBranch,
288
+ gitRemoteUrl,
289
+ gitCommit
290
+ };
291
+ }
292
+ var import_fs2, import_path2, import_os2, UUID_RE, codexAgent;
293
+ var init_codex = __esm({
294
+ "../service-capture/common/agents/codex.ts"() {
295
+ "use strict";
296
+ import_fs2 = require("fs");
297
+ import_path2 = require("path");
298
+ import_os2 = require("os");
299
+ UUID_RE = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i;
300
+ codexAgent = {
301
+ id: "codex",
302
+ displayName: "OpenAI Codex",
303
+ sessionsRoot() {
304
+ return sessionsDir();
305
+ },
306
+ isInstalled() {
307
+ return (0, import_fs2.existsSync)(codexHome());
308
+ },
309
+ discoverSessions() {
310
+ const root = sessionsDir();
311
+ if (!(0, import_fs2.existsSync)(root)) return [];
312
+ const files = [];
313
+ walkRollouts(root, files);
314
+ const sessions = [];
315
+ for (const filePath of files) {
316
+ try {
317
+ const stat = (0, import_fs2.statSync)(filePath);
318
+ const content = (0, import_fs2.readFileSync)(filePath, "utf-8");
319
+ const lineCount = content.split("\n").filter((l) => l.trim()).length;
320
+ const meta3 = readRolloutMeta(filePath, content);
321
+ sessions.push({
322
+ sessionId: meta3.sessionId,
323
+ filePath,
324
+ lineCount,
325
+ sizeBytes: stat.size,
326
+ cwd: meta3.cwd,
327
+ timestamp: meta3.timestamp,
328
+ gitBranch: meta3.gitBranch,
329
+ gitRemoteUrl: meta3.gitRemoteUrl,
330
+ gitCommit: meta3.gitCommit
331
+ });
332
+ } catch {
333
+ }
334
+ }
335
+ return sessions;
336
+ }
337
+ };
338
+ }
339
+ });
340
+
341
+ // ../service-capture/common/agents/registry.ts
342
+ function getAgents() {
343
+ return AGENTS;
344
+ }
345
+ function getAgent(id) {
346
+ const agent = AGENTS.find((a) => a.id === id);
347
+ if (!agent) throw new Error(`Unknown agent: ${id}`);
348
+ return agent;
349
+ }
350
+ function detectInstalled() {
351
+ return AGENTS.filter((a) => a.isInstalled());
352
+ }
353
+ var AGENTS;
354
+ var init_registry = __esm({
355
+ "../service-capture/common/agents/registry.ts"() {
356
+ "use strict";
357
+ init_claude_code();
358
+ init_codex();
359
+ AGENTS = [claudeCodeAgent, codexAgent];
360
+ }
361
+ });
362
+
363
+ // ../service-capture/common/agents/index.ts
364
+ var agents_exports = {};
365
+ __export(agents_exports, {
366
+ AGENT_IDS: () => AGENT_IDS,
367
+ claudeCodeAgent: () => claudeCodeAgent,
368
+ codexAgent: () => codexAgent,
369
+ detectInstalled: () => detectInstalled,
370
+ getAgent: () => getAgent,
371
+ getAgents: () => getAgents
372
+ });
373
+ var init_agents = __esm({
374
+ "../service-capture/common/agents/index.ts"() {
375
+ "use strict";
376
+ init_types();
377
+ init_claude_code();
378
+ init_codex();
379
+ init_registry();
380
+ }
381
+ });
382
+
383
+ // src/lib/agent-selection.ts
384
+ var agent_selection_exports = {};
385
+ __export(agent_selection_exports, {
386
+ parseAgentFlag: () => parseAgentFlag,
387
+ promptForAgents: () => promptForAgents,
388
+ readSavedAgents: () => readSavedAgents,
389
+ resolveAgentSelection: () => resolveAgentSelection,
390
+ saveAgents: () => saveAgents
391
+ });
392
+ function parseAgentFlag(value) {
393
+ if (!value) return null;
394
+ const normalized = value.trim().toLowerCase();
395
+ if (normalized === "both" || normalized === "all") return [...VALID];
396
+ const out = [];
397
+ for (const part of normalized.split(",").map((s) => s.trim())) {
398
+ if (part === "claude" || part === "claude-code") {
399
+ if (!out.includes("claude-code")) out.push("claude-code");
400
+ } else if (part === "codex") {
401
+ if (!out.includes("codex")) out.push("codex");
402
+ }
403
+ }
404
+ return out.length > 0 ? out : null;
405
+ }
406
+ function readSavedAgents() {
407
+ if (!(0, import_node_fs2.existsSync)(CONFIG_FILE)) return null;
408
+ try {
409
+ const cfg = JSON.parse((0, import_node_fs2.readFileSync)(CONFIG_FILE, "utf-8"));
410
+ if (Array.isArray(cfg.agents)) {
411
+ const agents = cfg.agents.filter(
412
+ (a) => VALID.includes(a)
413
+ );
414
+ return agents.length > 0 ? agents : null;
415
+ }
416
+ } catch {
417
+ }
418
+ return null;
419
+ }
420
+ function saveAgents(agents) {
421
+ let cfg = {};
422
+ if ((0, import_node_fs2.existsSync)(CONFIG_FILE)) {
423
+ try {
424
+ cfg = JSON.parse((0, import_node_fs2.readFileSync)(CONFIG_FILE, "utf-8"));
425
+ } catch {
426
+ cfg = {};
427
+ }
428
+ }
429
+ cfg.agents = agents;
430
+ (0, import_node_fs2.mkdirSync)(HAANSI_DIR, { recursive: true });
431
+ (0, import_node_fs2.writeFileSync)(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", {
432
+ encoding: "utf-8",
433
+ mode: 384
434
+ });
435
+ }
436
+ function resolveAgentSelection(flagValue) {
437
+ const fromFlag = parseAgentFlag(flagValue);
438
+ if (fromFlag) return { agents: fromFlag, source: "flag" };
439
+ const saved = readSavedAgents();
440
+ if (saved) return { agents: saved, source: "config" };
441
+ const detected = detectInstalled().map((a) => a.id);
442
+ if (detected.length > 0) return { agents: detected, source: "detected" };
443
+ return { agents: ["claude-code"], source: "default" };
444
+ }
445
+ async function promptForAgents() {
446
+ const installed = new Set(detectInstalled().map((a) => a.id));
447
+ const agents = getAgents();
448
+ console.log("\nWhich coding agents should Haansi connect?\n");
449
+ agents.forEach((a, i) => {
450
+ const mark = installed.has(a.id) ? "installed" : "not detected";
451
+ console.log(` ${i + 1}) ${a.displayName} (${mark})`);
452
+ });
453
+ const rl = readline.createInterface({
454
+ input: process.stdin,
455
+ output: process.stdout
456
+ });
457
+ const answer = await new Promise((resolve5) => {
458
+ rl.question(
459
+ "\nEnter numbers (comma-separated), or press Enter for all installed: ",
460
+ (a) => {
461
+ rl.close();
462
+ resolve5(a.trim());
463
+ }
464
+ );
465
+ });
466
+ if (!answer) {
467
+ const detected = agents.filter((a) => installed.has(a.id)).map((a) => a.id);
468
+ return detected.length > 0 ? detected : [...VALID];
469
+ }
470
+ const picked = [];
471
+ for (const token of answer.split(",").map((s) => s.trim())) {
472
+ const idx = parseInt(token, 10) - 1;
473
+ const agent = agents[idx];
474
+ if (agent && !picked.includes(agent.id)) picked.push(agent.id);
475
+ }
476
+ return picked.length > 0 ? picked : [...VALID];
477
+ }
478
+ var import_node_fs2, readline, VALID;
479
+ var init_agent_selection = __esm({
480
+ "src/lib/agent-selection.ts"() {
481
+ "use strict";
482
+ import_node_fs2 = require("node:fs");
483
+ readline = __toESM(require("node:readline"));
484
+ init_agents();
485
+ init_paths();
486
+ VALID = ["claude-code", "codex"];
487
+ }
488
+ });
489
+
490
+ // src/lib/codex-config.ts
491
+ function tomlString(value) {
492
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
493
+ return `"${escaped}"`;
494
+ }
495
+ function tomlStringArray(values) {
496
+ return `[${values.map(tomlString).join(", ")}]`;
497
+ }
498
+ function tomlInlineTable(entries) {
499
+ const parts = Object.entries(entries).map(
500
+ ([k, v]) => `${k} = ${tomlString(v)}`
501
+ );
502
+ return `{ ${parts.join(", ")} }`;
503
+ }
504
+ function upsertTomlTable(content, tablePath, entries) {
505
+ const bodyLines = Object.entries(entries).map(([k, v]) => `${k} = ${v}`);
506
+ const block = [`[${tablePath}]`, ...bodyLines].join("\n");
507
+ const lines = content.split("\n");
508
+ const headerRe = new RegExp(
509
+ `^\\s*\\[\\s*${tablePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\]\\s*$`
510
+ );
511
+ const startIdx = lines.findIndex((l) => headerRe.test(l));
512
+ if (startIdx === -1) {
513
+ const trimmed = content.replace(/\s*$/, "");
514
+ return trimmed.length > 0 ? `${trimmed}
515
+
516
+ ${block}
517
+ ` : `${block}
518
+ `;
519
+ }
520
+ let endIdx = lines.length;
521
+ for (let i = startIdx + 1; i < lines.length; i++) {
522
+ if (/^\s*\[/.test(lines[i])) {
523
+ endIdx = i;
524
+ break;
525
+ }
526
+ }
527
+ const before = lines.slice(0, startIdx);
528
+ const after = lines.slice(endIdx);
529
+ const rebuilt = [...before, ...block.split("\n"), ...after].join("\n");
530
+ return rebuilt.replace(/\n{3,}/g, "\n\n");
531
+ }
532
+ var init_codex_config = __esm({
533
+ "src/lib/codex-config.ts"() {
534
+ "use strict";
535
+ }
536
+ });
537
+
538
+ // src/commands/setup-mcp.ts
539
+ var setup_mcp_exports = {};
540
+ __export(setup_mcp_exports, {
541
+ HTTP_SERVERS: () => HTTP_SERVERS,
542
+ parseTarget: () => parseTarget,
543
+ refreshClaudeConfigTokens: () => refreshClaudeConfigTokens,
544
+ refreshCodexConfigTokens: () => refreshCodexConfigTokens,
545
+ refreshMcpTokens: () => refreshMcpTokens,
546
+ setupMcp: () => setupMcp
547
+ });
548
+ function parseTarget(arg) {
549
+ if (!arg || arg === "all") return "all";
550
+ if (arg === "solutions" || arg === "exchange" || arg === "digests" || arg === "onboarding" || arg === "skills")
551
+ return arg;
552
+ throw new Error(
553
+ `Unknown target: ${arg}. Use one of: solutions, exchange, digests, onboarding, skills, all (default).`
554
+ );
555
+ }
556
+ function httpServerUrl(path2) {
557
+ return `${apiUrl().replace(/\/+$/, "")}${path2}`;
558
+ }
559
+ function setupClaudeMcp(target) {
560
+ let config6 = {};
561
+ if ((0, import_node_fs3.existsSync)(CLAUDE_JSON)) {
562
+ try {
563
+ config6 = JSON.parse((0, import_node_fs3.readFileSync)(CLAUDE_JSON, "utf-8"));
564
+ } catch {
565
+ console.error(
566
+ `Warning: Could not parse ${CLAUDE_JSON} \u2014 will overwrite with new config.`
567
+ );
568
+ }
569
+ }
570
+ if (!config6.mcpServers) config6.mcpServers = {};
571
+ const installed = [];
572
+ const warnings = [];
573
+ if (target === "all" || target === "solutions") {
574
+ const haansiBin = resolveHaansiBin();
575
+ config6.mcpServers["haansi-solutions"] = {
576
+ command: haansiBin,
577
+ args: ["mcp-server"],
578
+ env: { HAANSI_AGENT_SOURCE: "claude-code" }
579
+ };
580
+ installed.push(`haansi-solutions (stdio \u2192 ${haansiBin} mcp-server)`);
581
+ }
582
+ for (const http of HTTP_SERVERS) {
583
+ if (target !== "all" && target !== http.target) continue;
584
+ const token = resolveToken();
585
+ if (!token) {
586
+ warnings.push(
587
+ `${http.name} not registered: no token found. Run \`haansi auth login\` first, or set HAANSI_TOKEN.`
588
+ );
589
+ continue;
590
+ }
591
+ const url2 = httpServerUrl(http.path);
592
+ config6.mcpServers[http.name] = {
593
+ type: "http",
594
+ url: url2,
595
+ headers: { Authorization: `Bearer ${token}` }
596
+ };
597
+ installed.push(`${http.name} (http \u2192 ${url2})`);
598
+ }
599
+ (0, import_node_fs3.writeFileSync)(CLAUDE_JSON, JSON.stringify(config6, null, 2) + "\n", "utf-8");
600
+ console.log(`
601
+ Claude Code \u2014 configured in ${CLAUDE_JSON}`);
602
+ for (const entry2 of installed) console.log(` \u2022 ${entry2}`);
603
+ for (const w of warnings) console.log(` ! ${w}`);
604
+ return { installed, warnings };
605
+ }
606
+ function setupCodexMcp(target) {
607
+ let content = "";
608
+ if ((0, import_node_fs3.existsSync)(CODEX_CONFIG)) {
609
+ try {
610
+ content = (0, import_node_fs3.readFileSync)(CODEX_CONFIG, "utf-8");
611
+ } catch {
612
+ console.error(
613
+ `Warning: Could not read ${CODEX_CONFIG} \u2014 will create a new file.`
614
+ );
615
+ }
616
+ }
617
+ const installed = [];
618
+ const warnings = [];
619
+ if (target === "all" || target === "solutions") {
620
+ const haansiBin = resolveHaansiBin();
621
+ content = upsertTomlTable(content, "mcp_servers.haansi-solutions", {
622
+ command: tomlString(haansiBin),
623
+ args: tomlStringArray(["mcp-server"]),
624
+ env: tomlInlineTable({ HAANSI_AGENT_SOURCE: "codex" })
625
+ });
626
+ installed.push(`haansi-solutions (stdio \u2192 ${haansiBin} mcp-server)`);
627
+ }
628
+ for (const http of HTTP_SERVERS) {
629
+ if (target !== "all" && target !== http.target) continue;
630
+ const token = resolveToken();
631
+ if (!token) {
632
+ warnings.push(
633
+ `${http.name} not registered: no token found. Run \`haansi auth login\` first, or set HAANSI_TOKEN.`
634
+ );
635
+ continue;
636
+ }
637
+ const url2 = httpServerUrl(http.path);
638
+ content = upsertTomlTable(content, `mcp_servers.${http.name}`, {
639
+ url: tomlString(url2),
640
+ http_headers: tomlInlineTable({ Authorization: `Bearer ${token}` })
641
+ });
642
+ installed.push(`${http.name} (http \u2192 ${url2})`);
643
+ }
644
+ (0, import_node_fs3.mkdirSync)(CODEX_HOME, { recursive: true });
645
+ (0, import_node_fs3.writeFileSync)(CODEX_CONFIG, content, "utf-8");
646
+ console.log(`
647
+ OpenAI Codex \u2014 configured in ${CODEX_CONFIG}`);
648
+ for (const entry2 of installed) console.log(` \u2022 ${entry2}`);
649
+ for (const w of warnings) console.log(` ! ${w}`);
650
+ return { installed, warnings };
651
+ }
652
+ function refreshClaudeConfigTokens(config6, token) {
653
+ const changed = [];
654
+ const projects = config6.projects ?? {};
655
+ const scopes = [
656
+ config6.mcpServers,
657
+ ...Object.values(projects).map(
658
+ (p) => p?.mcpServers
659
+ )
660
+ ];
661
+ for (const servers of scopes) {
662
+ if (!servers || typeof servers !== "object") continue;
663
+ for (const [name, entry2] of Object.entries(
664
+ servers
665
+ )) {
666
+ if (!HTTP_SERVER_NAMES.has(name)) continue;
667
+ const auth = entry2?.headers?.Authorization;
668
+ if (typeof auth !== "string" || !HSK_BEARER.test(auth)) continue;
669
+ const next = `Bearer ${token}`;
670
+ if (auth === next || !entry2?.headers) continue;
671
+ entry2.headers.Authorization = next;
672
+ changed.push(name);
673
+ }
674
+ }
675
+ return changed;
676
+ }
677
+ function refreshCodexConfigTokens(content, token) {
678
+ const changed = [];
679
+ const lines = content.split("\n");
680
+ let current = null;
681
+ for (let i = 0; i < lines.length; i++) {
682
+ const header = /^\s*\[\s*mcp_servers\.([^\]\s]+)\s*\]\s*$/.exec(lines[i]);
683
+ if (header) {
684
+ current = HTTP_SERVER_NAMES.has(header[1]) ? header[1] : null;
685
+ continue;
686
+ }
687
+ if (/^\s*\[/.test(lines[i])) {
688
+ current = null;
689
+ continue;
690
+ }
691
+ if (!current || !HSK_BEARER.test(lines[i])) continue;
692
+ const next = lines[i].replace(HSK_BEARER, `Bearer ${token}`);
693
+ if (next !== lines[i]) {
694
+ lines[i] = next;
695
+ if (!changed.includes(current)) changed.push(current);
696
+ }
697
+ }
698
+ return { content: lines.join("\n"), changed };
699
+ }
700
+ function refreshMcpTokens(token, files = {}) {
701
+ const claudeJson = files.claudeJson ?? CLAUDE_JSON;
702
+ const codexConfig = files.codexConfig ?? CODEX_CONFIG;
703
+ const updated = [];
704
+ try {
705
+ if ((0, import_node_fs3.existsSync)(claudeJson)) {
706
+ const config6 = JSON.parse((0, import_node_fs3.readFileSync)(claudeJson, "utf-8"));
707
+ const changed = refreshClaudeConfigTokens(config6, token);
708
+ if (changed.length > 0) {
709
+ (0, import_node_fs3.writeFileSync)(
710
+ claudeJson,
711
+ JSON.stringify(config6, null, 2) + "\n",
712
+ "utf-8"
713
+ );
714
+ updated.push(...changed.map((n) => `Claude Code: ${n}`));
715
+ }
716
+ }
717
+ } catch {
718
+ console.error(
719
+ `Warning: could not update the MCP token in ${claudeJson}. Run \`haansi setup-mcp\`.`
720
+ );
721
+ }
722
+ try {
723
+ if ((0, import_node_fs3.existsSync)(codexConfig)) {
724
+ const result = refreshCodexConfigTokens(
725
+ (0, import_node_fs3.readFileSync)(codexConfig, "utf-8"),
726
+ token
727
+ );
728
+ if (result.changed.length > 0) {
729
+ (0, import_node_fs3.writeFileSync)(codexConfig, result.content, "utf-8");
730
+ updated.push(...result.changed.map((n) => `Codex: ${n}`));
731
+ }
732
+ }
733
+ } catch {
734
+ console.error(
735
+ `Warning: could not update the MCP token in ${codexConfig}. Run \`haansi setup-mcp\`.`
736
+ );
737
+ }
738
+ if (updated.length > 0) {
739
+ console.log("Updated the token for your Haansi MCP servers:");
740
+ for (const u of updated) console.log(` \u2022 ${u}`);
741
+ console.log(
742
+ "Restart your coding agent (or reconnect with /mcp) to use it."
743
+ );
744
+ }
745
+ return updated;
746
+ }
747
+ async function setupMcp(targetArg, agentFlag) {
748
+ const target = parseTarget(targetArg);
749
+ const selection = resolveAgentSelection(agentFlag);
750
+ for (const agentId of selection.agents) {
751
+ WRITERS[agentId](target);
752
+ }
753
+ saveAgents(selection.agents);
754
+ const restart = selection.agents.map((a) => a === "codex" ? "Codex" : "Claude Code").join(" / ");
755
+ console.log(`
756
+ Restart ${restart} to activate the changes.`);
757
+ }
758
+ var import_node_fs3, import_node_path2, import_node_os2, CLAUDE_JSON, CODEX_HOME, CODEX_CONFIG, HTTP_SERVERS, HSK_BEARER, HTTP_SERVER_NAMES, WRITERS;
759
+ var init_setup_mcp = __esm({
760
+ "src/commands/setup-mcp.ts"() {
761
+ "use strict";
762
+ import_node_fs3 = require("node:fs");
763
+ import_node_path2 = require("node:path");
764
+ import_node_os2 = require("node:os");
765
+ init_paths();
766
+ init_agent_selection();
767
+ init_codex_config();
768
+ CLAUDE_JSON = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".claude.json");
769
+ CODEX_HOME = process.env.CODEX_HOME || (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".codex");
770
+ CODEX_CONFIG = (0, import_node_path2.join)(CODEX_HOME, "config.toml");
771
+ HTTP_SERVERS = [
772
+ {
773
+ target: "exchange",
774
+ name: "haansi-exchange",
775
+ path: "/api/v1/memory/exchange/mcp"
776
+ },
777
+ {
778
+ target: "digests",
779
+ name: "haansi-digests",
780
+ path: "/api/v1/memory/digests/mcp"
781
+ },
782
+ // Published onboarding guides, so an agent can pick up a workstream from
783
+ // its handoff doc (docs/product/plans/2026-09-24-onboarding-guides.md §8.3).
784
+ {
785
+ target: "onboarding",
786
+ name: "haansi-onboarding",
787
+ path: "/api/v1/memory/onboarding/mcp"
788
+ },
789
+ // Published team skills, loaded progressively by coding agents
790
+ // (docs/product/plans/2026-10-10-memory-skills.md §7).
791
+ {
792
+ target: "skills",
793
+ name: "haansi-skills",
794
+ path: "/api/v1/memory/skills/mcp"
795
+ }
796
+ ];
797
+ HSK_BEARER = /Bearer hsk_[A-Za-z0-9_-]+/;
798
+ HTTP_SERVER_NAMES = new Set(HTTP_SERVERS.map((s) => s.name));
799
+ WRITERS = {
800
+ "claude-code": setupClaudeMcp,
801
+ codex: setupCodexMcp
802
+ };
803
+ }
804
+ });
805
+
125
806
  // src/commands/auth-login.ts
126
807
  var auth_login_exports = {};
127
808
  __export(auth_login_exports, {
128
809
  authLogin: () => authLogin
129
810
  });
130
811
  function openBrowser(url2) {
131
- const cmd = (0, import_node_os2.platform)() === "darwin" ? "open" : (0, import_node_os2.platform)() === "win32" ? "start" : "xdg-open";
812
+ const cmd = (0, import_node_os3.platform)() === "darwin" ? "open" : (0, import_node_os3.platform)() === "win32" ? "start" : "xdg-open";
132
813
  try {
133
814
  (0, import_node_child_process2.exec)(`${cmd} ${JSON.stringify(url2)}`);
134
815
  return true;
@@ -218,8 +899,8 @@ If the browser didn't open, visit this URL:
218
899
  );
219
900
  }
220
901
  const { token: hskToken } = await tokenRes.json();
221
- (0, import_node_fs2.mkdirSync)(HAANSI_DIR2, { recursive: true });
222
- (0, import_node_fs2.writeFileSync)(
902
+ (0, import_node_fs4.mkdirSync)(HAANSI_DIR2, { recursive: true });
903
+ (0, import_node_fs4.writeFileSync)(
223
904
  CREDENTIALS_FILE2,
224
905
  JSON.stringify({ token: hskToken }, null, 2),
225
906
  {
@@ -230,21 +911,23 @@ If the browser didn't open, visit this URL:
230
911
  console.log(`
231
912
  Authenticated successfully${orgName ? ` (${orgName})` : ""}!`);
232
913
  console.log(`Token saved to ${CREDENTIALS_FILE2}`);
914
+ refreshMcpTokens(hskToken);
233
915
  console.log(
234
916
  "Run `haansi collect` to upload sessions or `haansi setup-mcp` to configure Claude Code."
235
917
  );
236
918
  }
237
- var import_node_child_process2, import_node_fs2, import_node_path2, import_node_os2, HAANSI_DIR2, CREDENTIALS_FILE2, POLL_INTERVAL_MS, TIMEOUT_MS;
919
+ var import_node_child_process2, import_node_fs4, import_node_path3, import_node_os3, HAANSI_DIR2, CREDENTIALS_FILE2, POLL_INTERVAL_MS, TIMEOUT_MS;
238
920
  var init_auth_login = __esm({
239
921
  "src/commands/auth-login.ts"() {
240
922
  "use strict";
241
923
  import_node_child_process2 = require("node:child_process");
242
- import_node_fs2 = require("node:fs");
243
- import_node_path2 = require("node:path");
244
- import_node_os2 = require("node:os");
924
+ import_node_fs4 = require("node:fs");
925
+ import_node_path3 = require("node:path");
926
+ import_node_os3 = require("node:os");
245
927
  init_paths();
246
- HAANSI_DIR2 = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".haansi");
247
- CREDENTIALS_FILE2 = (0, import_node_path2.join)(HAANSI_DIR2, "credentials.json");
928
+ init_setup_mcp();
929
+ HAANSI_DIR2 = (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".haansi");
930
+ CREDENTIALS_FILE2 = (0, import_node_path3.join)(HAANSI_DIR2, "credentials.json");
248
931
  POLL_INTERVAL_MS = 2e3;
249
932
  TIMEOUT_MS = 5 * 60 * 1e3;
250
933
  }
@@ -256,7 +939,7 @@ __export(init_exports, {
256
939
  init: () => init
257
940
  });
258
941
  function prompt(question) {
259
- const rl = readline.createInterface({
942
+ const rl = readline2.createInterface({
260
943
  input: process.stdin,
261
944
  output: process.stdout
262
945
  });
@@ -304,28 +987,30 @@ async function init() {
304
987
  }
305
988
  console.log("\nValidating token...");
306
989
  await validateToken(token, apiUrl2);
307
- (0, import_node_fs3.mkdirSync)(HAANSI_DIR3, { recursive: true });
308
- (0, import_node_fs3.writeFileSync)(CREDENTIALS_FILE3, JSON.stringify({ token }, null, 2), {
990
+ (0, import_node_fs5.mkdirSync)(HAANSI_DIR3, { recursive: true });
991
+ (0, import_node_fs5.writeFileSync)(CREDENTIALS_FILE3, JSON.stringify({ token }, null, 2), {
309
992
  encoding: "utf-8",
310
993
  mode: 384
311
994
  });
312
995
  console.log(`
313
996
  Token saved to ${CREDENTIALS_FILE3}`);
997
+ refreshMcpTokens(token);
314
998
  console.log(
315
999
  "Run `haansi daemon` to start collecting or `haansi setup-mcp` to configure your coding agent(s) (Claude Code / Codex)."
316
1000
  );
317
1001
  }
318
- var readline, import_node_fs3, import_node_path3, import_node_os3, HAANSI_DIR3, CREDENTIALS_FILE3;
1002
+ var readline2, import_node_fs5, import_node_path4, import_node_os4, HAANSI_DIR3, CREDENTIALS_FILE3;
319
1003
  var init_init = __esm({
320
1004
  "src/commands/init.ts"() {
321
1005
  "use strict";
322
- readline = __toESM(require("node:readline"));
323
- import_node_fs3 = require("node:fs");
324
- import_node_path3 = require("node:path");
325
- import_node_os3 = require("node:os");
1006
+ readline2 = __toESM(require("node:readline"));
1007
+ import_node_fs5 = require("node:fs");
1008
+ import_node_path4 = require("node:path");
1009
+ import_node_os4 = require("node:os");
326
1010
  init_paths();
327
- HAANSI_DIR3 = (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".haansi");
328
- CREDENTIALS_FILE3 = (0, import_node_path3.join)(HAANSI_DIR3, "credentials.json");
1011
+ init_setup_mcp();
1012
+ HAANSI_DIR3 = (0, import_node_path4.join)((0, import_node_os4.homedir)(), ".haansi");
1013
+ CREDENTIALS_FILE3 = (0, import_node_path4.join)(HAANSI_DIR3, "credentials.json");
329
1014
  }
330
1015
  });
331
1016
 
@@ -765,7 +1450,7 @@ var init_esm_node = __esm({
765
1450
  });
766
1451
 
767
1452
  // ../../packages/@company/observability/src/types.ts
768
- var init_types = __esm({
1453
+ var init_types2 = __esm({
769
1454
  "../../packages/@company/observability/src/types.ts"() {
770
1455
  "use strict";
771
1456
  }
@@ -1028,7 +1713,7 @@ var init_logger = __esm({
1028
1713
  "use strict";
1029
1714
  import_async_hooks = require("async_hooks");
1030
1715
  init_esm_node();
1031
- init_types();
1716
+ init_types2();
1032
1717
  SENSITIVE_KEY_RE = /^(password|passwd|secret|token|apikey|authorization|auth|authtoken|authkey|credentials?|connectionstring|privatekey)$/;
1033
1718
  USER_CONTENT_KEY_RE = /^(question|questiontext|query|querytext|q|searchquery|searchtext|searchterm|searchterms|userquery|prompt|userprompt|usermessage|messagetext|messagecontent)$/;
1034
1719
  FIRST_LINE_CAUSE_CHARS = 300;
@@ -1232,7 +1917,7 @@ var _cloudBackend2, MetricsService;
1232
1917
  var init_metrics = __esm({
1233
1918
  "../../packages/@company/observability/src/metrics.ts"() {
1234
1919
  "use strict";
1235
- init_types();
1920
+ init_types2();
1236
1921
  MetricsService = class {
1237
1922
  projectId;
1238
1923
  serviceName;
@@ -1526,7 +2211,7 @@ var init_observability_service = __esm({
1526
2211
  "use strict";
1527
2212
  init_logger();
1528
2213
  init_metrics();
1529
- init_types();
2214
+ init_types2();
1530
2215
  ObservabilityService = class {
1531
2216
  logger;
1532
2217
  metrics;
@@ -1713,7 +2398,7 @@ var SCALING_METRICS_SERVICE, SCALING_METRICS_NAMESPACE, SCALING_METRICS;
1713
2398
  var init_scaling_metrics = __esm({
1714
2399
  "../../packages/@company/observability/src/scaling-metrics.ts"() {
1715
2400
  "use strict";
1716
- init_types();
2401
+ init_types2();
1717
2402
  SCALING_METRICS_SERVICE = "backlog-reconciler";
1718
2403
  SCALING_METRICS_NAMESPACE = `Haansi/${SCALING_METRICS_SERVICE}`;
1719
2404
  SCALING_METRICS = {
@@ -1736,7 +2421,7 @@ var init_scaling_metrics = __esm({
1736
2421
  REPLAY_PENDING_EVENTS: spec({
1737
2422
  name: "haansi/replay/pending_events",
1738
2423
  unit: "Count",
1739
- description: "source_events a RUNNING re-run (live hard-reset lease) has yet to replay, past its watermark, all orgs.",
2424
+ description: "source_events a RUNNING replay (live re-run lease, or a checkpoint saved within the heartbeat) has yet to replay, past its watermark, all orgs.",
1740
2425
  dimensions: ["environment"],
1741
2426
  treatMissingData: "breaching",
1742
2427
  missingDataRationale: "A re-run embeds every event inline through the batch embedders, but those rows are already normalized and never become claims, so neither backlog_rows nor claims_pending nor canonical_backlog_events can see the demand: one TEI task served a whole re-run at ~1 s/text (dev 2026-10-06). This is the only publisher of that demand; silence means it is unobserved, not finished. Breaching keeps the embedders up."
@@ -2038,7 +2723,7 @@ var init_health = __esm({
2038
2723
  var init_degradation = __esm({
2039
2724
  "../../packages/@company/observability/src/degradation.ts"() {
2040
2725
  "use strict";
2041
- init_types();
2726
+ init_types2();
2042
2727
  init_observability_service();
2043
2728
  }
2044
2729
  });
@@ -2056,7 +2741,7 @@ var init_src2 = __esm({
2056
2741
  init_public_error();
2057
2742
  init_health();
2058
2743
  init_degradation();
2059
- init_types();
2744
+ init_types2();
2060
2745
  defaultObservability = null;
2061
2746
  getDefaultObservability = () => {
2062
2747
  if (!defaultObservability) {
@@ -16580,9 +17265,1623 @@ var init_read_gate = __esm({
16580
17265
  });
16581
17266
 
16582
17267
  // ../../packages/@company/encryption/src/exclusions.ts
17268
+ function exchangeExclusions(database, table, columns) {
17269
+ return Object.entries(columns).map(([column, [category, reason]]) => ({
17270
+ database,
17271
+ table,
17272
+ column,
17273
+ category,
17274
+ reason
17275
+ }));
17276
+ }
17277
+ var EXCHANGE_CONTENT_REASON, EXCHANGE_VOCAB_REASON, EXCHANGE_LABEL_REASON, EXCHANGE_AUDIT_REASON, INTEL, EXCHANGE_QA_EXCLUSIONS, EXPLICIT_EXCLUSIONS;
16583
17278
  var init_exclusions = __esm({
16584
17279
  "../../packages/@company/encryption/src/exclusions.ts"() {
16585
17280
  "use strict";
17281
+ EXCHANGE_CONTENT_REASON = "Exchange content published across orgs after redaction (exchange-redesign \xA75). Every org reads it, so per-org encryption cannot apply; the redaction pipeline is the control.";
17282
+ EXCHANGE_VOCAB_REASON = "A fixed vocabulary checked by a constraint; queries route and count on it.";
17283
+ EXCHANGE_LABEL_REASON = "A descriptive label for an agent client (e.g. an MCP client name) or a fixed taxonomy facet, shown next to a contribution; not personal content.";
17284
+ EXCHANGE_AUDIT_REASON = "An audit field describing a platform moderation or worker action on Exchange content, written by our doorways; audit queries filter on it.";
17285
+ INTEL = "haansi_intelligence_prod";
17286
+ EXCHANGE_QA_EXCLUSIONS = [
17287
+ ...exchangeExclusions(INTEL, "exchange_posts", {
17288
+ post_type: ["operational", EXCHANGE_VOCAB_REASON],
17289
+ author_kind: ["operational", EXCHANGE_VOCAB_REASON],
17290
+ agent_label: ["not-personal", EXCHANGE_LABEL_REASON],
17291
+ disciplines: ["operational", EXCHANGE_LABEL_REASON],
17292
+ work_functions: ["operational", EXCHANGE_LABEL_REASON],
17293
+ desired_outcome: ["not-personal", EXCHANGE_CONTENT_REASON],
17294
+ constraints_text: ["not-personal", EXCHANGE_CONTENT_REASON],
17295
+ tried: ["not-personal", EXCHANGE_CONTENT_REASON]
17296
+ }),
17297
+ ...exchangeExclusions(INTEL, "exchange_answers", {
17298
+ author_kind: ["operational", EXCHANGE_VOCAB_REASON],
17299
+ moderation_status: ["operational", EXCHANGE_VOCAB_REASON],
17300
+ agent_label: ["not-personal", EXCHANGE_LABEL_REASON],
17301
+ approach: ["not-personal", EXCHANGE_CONTENT_REASON],
17302
+ steps: ["not-personal", EXCHANGE_CONTENT_REASON],
17303
+ limitations: ["not-personal", EXCHANGE_CONTENT_REASON]
17304
+ }),
17305
+ ...exchangeExclusions(INTEL, "exchange_revisions", {
17306
+ editor_kind: ["operational", EXCHANGE_VOCAB_REASON],
17307
+ redaction_version: ["operational", EXCHANGE_VOCAB_REASON],
17308
+ content: ["not-personal", EXCHANGE_CONTENT_REASON]
17309
+ }),
17310
+ ...exchangeExclusions(INTEL, "exchange_comments", {
17311
+ author_kind: ["operational", EXCHANGE_VOCAB_REASON],
17312
+ moderation_status: ["operational", EXCHANGE_VOCAB_REASON],
17313
+ body: ["not-personal", EXCHANGE_CONTENT_REASON]
17314
+ }),
17315
+ ...exchangeExclusions(INTEL, "exchange_reuse_events", {
17316
+ actor_kind: ["operational", EXCHANGE_VOCAB_REASON],
17317
+ outcome: ["operational", EXCHANGE_VOCAB_REASON],
17318
+ evidence_kind: ["operational", EXCHANGE_VOCAB_REASON],
17319
+ agent_label: ["not-personal", EXCHANGE_LABEL_REASON],
17320
+ context: ["not-personal", EXCHANGE_CONTENT_REASON],
17321
+ note: ["not-personal", EXCHANGE_CONTENT_REASON],
17322
+ idempotency_key: [
17323
+ "operational",
17324
+ "A client-chosen retry key, compared by equality to dedupe a write; carries no content."
17325
+ ]
17326
+ }),
17327
+ ...exchangeExclusions(INTEL, "exchange_reports", {
17328
+ target_kind: ["operational", EXCHANGE_VOCAB_REASON],
17329
+ reason: ["operational", EXCHANGE_VOCAB_REASON],
17330
+ detail: [
17331
+ "not-personal",
17332
+ "A reporter's note about Exchange content, read only by the reporter and platform moderators."
17333
+ ]
17334
+ }),
17335
+ ...exchangeExclusions(INTEL, "exchange_audit_outbox", {
17336
+ actor_kind: ["audit-actor", EXCHANGE_AUDIT_REASON],
17337
+ worker_name: ["audit-actor", EXCHANGE_AUDIT_REASON],
17338
+ action: ["operational", EXCHANGE_AUDIT_REASON],
17339
+ target_kind: ["operational", EXCHANGE_AUDIT_REASON],
17340
+ reason: ["operational", EXCHANGE_AUDIT_REASON],
17341
+ before_state: ["operational", EXCHANGE_AUDIT_REASON],
17342
+ after_state: ["operational", EXCHANGE_AUDIT_REASON]
17343
+ }),
17344
+ ...exchangeExclusions("haansi_prod", "exchange_moderation_log", {
17345
+ actor_kind: ["audit-actor", EXCHANGE_AUDIT_REASON],
17346
+ worker_name: ["audit-actor", EXCHANGE_AUDIT_REASON],
17347
+ target_kind: ["operational", EXCHANGE_AUDIT_REASON],
17348
+ reason: ["operational", EXCHANGE_AUDIT_REASON],
17349
+ before_state: ["operational", EXCHANGE_AUDIT_REASON],
17350
+ after_state: ["operational", EXCHANGE_AUDIT_REASON]
17351
+ })
17352
+ ];
17353
+ EXPLICIT_EXCLUSIONS = [
17354
+ ...EXCHANGE_QA_EXCLUSIONS,
17355
+ // ── cli_tokens (cli-token-scopes/001): delegated scopes ──
17356
+ {
17357
+ database: "haansi_prod",
17358
+ table: "cli_tokens",
17359
+ column: "scopes",
17360
+ category: "operational",
17361
+ reason: "A fixed vocabulary of delegated permissions ('exchange:write'), checked by a constraint and read on every request to authorize writes."
17362
+ },
17363
+ // ── qna_messages (qna-charts/003): answer dependency manifest ──
17364
+ {
17365
+ database: "haansi_intelligence_prod",
17366
+ table: "qna_messages",
17367
+ column: "dependency_manifest",
17368
+ category: "operational",
17369
+ reason: "Tool names and artifact ids an answer was built from (no content); read on every follow-up to re-check access, server-side only."
17370
+ },
17371
+ // ── organization_mcp_servers (custom-mcp-servers/004): Q&A tool allowlist ──
17372
+ {
17373
+ database: "haansi_prod",
17374
+ table: "organization_mcp_servers",
17375
+ column: "tool_policy",
17376
+ category: "operational",
17377
+ reason: "An admin's map of a server's tool names to 'read' | 'write' (shape-checked, no free text); read on every ask to decide which tools Q&A may offer."
17378
+ },
17379
+ // ── qna_write_operations (qna-charts/002): retry-safe Q&A writes ──
17380
+ {
17381
+ database: "haansi_intelligence_prod",
17382
+ table: "qna_write_operations",
17383
+ column: "operation_id",
17384
+ category: "join-key",
17385
+ reason: "Server-assigned key of one Q&A tool action: '<question id>:<tool name>'. Matched by equality on every retry."
17386
+ },
17387
+ {
17388
+ database: "haansi_intelligence_prod",
17389
+ table: "qna_write_operations",
17390
+ column: "tool",
17391
+ category: "operational",
17392
+ reason: "Name of the built-in tool that wrote (e.g. create_artifact); a fixed vocabulary."
17393
+ },
17394
+ {
17395
+ database: "haansi_intelligence_prod",
17396
+ table: "qna_write_operations",
17397
+ column: "payload_fingerprint",
17398
+ category: "derived",
17399
+ reason: "sha256 of the tool payload, compared by equality to tell a retry from a changed request. Encrypting it would break the comparison; the payload itself is not stored."
17400
+ },
17401
+ {
17402
+ database: "haansi_intelligence_prod",
17403
+ table: "qna_write_operations",
17404
+ column: "state",
17405
+ category: "operational",
17406
+ reason: "'claimed' | 'done' (CHECK-constrained)."
17407
+ },
17408
+ {
17409
+ database: "haansi_intelligence_prod",
17410
+ table: "qna_write_operations",
17411
+ column: "surface",
17412
+ category: "operational",
17413
+ reason: "Product surface the write came from (web, slack, vscode, cli)."
17414
+ },
17415
+ {
17416
+ database: "haansi_intelligence_prod",
17417
+ table: "qna_write_operations",
17418
+ column: "notify_state",
17419
+ category: "operational",
17420
+ reason: "Outbox delivery state (CHECK-constrained vocabulary)."
17421
+ },
17422
+ {
17423
+ database: "haansi_intelligence_prod",
17424
+ table: "qna_write_operations",
17425
+ column: "notify_error",
17426
+ category: "operational",
17427
+ reason: "Error class name or a fixed sweep message; never message text or a Slack response body."
17428
+ },
17429
+ // ── slack_user_links (slack-installations/005): verified Slack ↔ member links ──
17430
+ {
17431
+ database: "haansi_prod",
17432
+ table: "slack_user_links",
17433
+ column: "team_id",
17434
+ category: "join-key",
17435
+ reason: "The Slack workspace id, the same key as organization_integrations.source_account_id; matched by equality on every bot event."
17436
+ },
17437
+ {
17438
+ database: "haansi_prod",
17439
+ table: "slack_user_links",
17440
+ column: "slack_user_id",
17441
+ category: "join-key",
17442
+ reason: "Slack user id (U\u2026), a pseudonymous platform identifier looked up by equality on every bot event, the same shape as source_entity_id."
17443
+ },
17444
+ {
17445
+ database: "haansi_prod",
17446
+ table: "slack_user_links",
17447
+ column: "link_source",
17448
+ category: "operational",
17449
+ reason: "A fixed enumeration (CHECK) of how the link was made; routes nothing personal."
17450
+ },
17451
+ {
17452
+ database: "haansi_prod",
17453
+ table: "slack_user_links",
17454
+ column: "revoked_reason",
17455
+ category: "operational",
17456
+ reason: "A short code written by our own code (e.g. 'relinked'); never user text."
17457
+ },
17458
+ // ── slack_installations (slack-installations/001): the workspace's own
17459
+ // installation record. Platform-level, no org; holds Slack-issued ids and
17460
+ // the credential ciphertext. ──
17461
+ {
17462
+ database: "haansi_prod",
17463
+ table: "slack_installations",
17464
+ column: "credentials_encrypted",
17465
+ category: "derived",
17466
+ reason: "Already encrypted by the application before it is stored, like organization_integrations.credentials_encrypted; encrypting the ciphertext again adds nothing."
17467
+ },
17468
+ {
17469
+ database: "haansi_prod",
17470
+ table: "slack_installations",
17471
+ column: "team_id",
17472
+ category: "join-key",
17473
+ reason: "The Slack workspace id, the SAME key as organization_integrations.source_account_id for slack rows and workspace_organization_mapping.workspace_id; looked up by equality on every bot event."
17474
+ },
17475
+ {
17476
+ database: "haansi_prod",
17477
+ table: "slack_installations",
17478
+ column: "app_id",
17479
+ category: "operational",
17480
+ reason: "Slack's id for OUR app (A\u2026), the same value in every row."
17481
+ },
17482
+ {
17483
+ database: "haansi_prod",
17484
+ table: "slack_installations",
17485
+ column: "enterprise_id",
17486
+ category: "join-key",
17487
+ reason: "Slack Enterprise Grid org id (E\u2026), a pseudonymous workspace-level identifier matched by equality; NULL for ordinary workspaces."
17488
+ },
17489
+ {
17490
+ database: "haansi_prod",
17491
+ table: "slack_installations",
17492
+ column: "bot_user_id",
17493
+ category: "operational",
17494
+ reason: "The bot's own Slack user id (U\u2026), needed in plaintext for Bolt to ignore its own messages. Identifies our bot, not a person."
17495
+ },
17496
+ {
17497
+ database: "haansi_prod",
17498
+ table: "slack_installations",
17499
+ column: "bot_id",
17500
+ category: "operational",
17501
+ reason: "The bot's own Slack bot id (B\u2026); identifies our bot, not a person."
17502
+ },
17503
+ {
17504
+ database: "haansi_prod",
17505
+ table: "slack_installations",
17506
+ column: "scopes",
17507
+ category: "operational",
17508
+ reason: "Granted OAuth scopes, a fixed list from the manifest; read to decide whether a reinstall is needed."
17509
+ },
17510
+ {
17511
+ database: "haansi_prod",
17512
+ table: "slack_installations",
17513
+ column: "installed_by_user_id",
17514
+ category: "join-key",
17515
+ reason: "Slack user id (U\u2026) of the admin who installed the app \u2014 a pseudonymous platform identifier, the same shape as source_entity_id elsewhere; used to attribute the install, never displayed."
17516
+ },
17517
+ // ── TI-58 runtime credential (WS9): already ciphertext ──
17518
+ {
17519
+ database: "haansi_prod",
17520
+ table: "organization_database_configs",
17521
+ column: "runtime_user_encrypted",
17522
+ category: "derived",
17523
+ reason: "Already encrypted by the application before it is stored, like the existing BYOD credential columns; encrypting the ciphertext again adds nothing."
17524
+ },
17525
+ {
17526
+ database: "haansi_prod",
17527
+ table: "organization_database_configs",
17528
+ column: "runtime_password_encrypted",
17529
+ category: "derived",
17530
+ reason: "Already encrypted by the application before it is stored, like the existing BYOD credential columns; encrypting the ciphertext again adds nothing."
17531
+ },
17532
+ // ── exchange_moderation_log (TI-18): an audit trail of OUR actions ──
17533
+ {
17534
+ database: "haansi_prod",
17535
+ table: "exchange_moderation_log",
17536
+ column: "action",
17537
+ category: "operational",
17538
+ reason: "A fixed vocabulary of moderation actions ('delete_post'), checked by a constraint. Audit queries filter on it."
17539
+ },
17540
+ {
17541
+ database: "haansi_prod",
17542
+ table: "exchange_moderation_log",
17543
+ column: "target_id",
17544
+ category: "structural",
17545
+ reason: "The id of the moderated Exchange post. An identifier the audit is looked up by, not content."
17546
+ },
17547
+ // ── session_object_pointers (WS6 phase 1): object locations, not content ──
17548
+ {
17549
+ database: "haansi_prod",
17550
+ table: "session_object_pointers",
17551
+ column: "user_id",
17552
+ category: "structural",
17553
+ reason: "The owning user's id, kept as text because it is part of the object key. An identifier the readers and the purge filter on, not personal content."
17554
+ },
17555
+ {
17556
+ database: "haansi_prod",
17557
+ table: "session_object_pointers",
17558
+ column: "session_id",
17559
+ category: "structural",
17560
+ reason: "The session id that names the object. Readers and the purge filter on it; it carries no content."
17561
+ },
17562
+ {
17563
+ database: "haansi_prod",
17564
+ table: "session_object_pointers",
17565
+ column: "bucket_kind",
17566
+ category: "operational",
17567
+ reason: "Which bucket the pointer names ('staging' or 'clean'). Routing state."
17568
+ },
17569
+ {
17570
+ database: "haansi_prod",
17571
+ table: "session_object_pointers",
17572
+ column: "object_key",
17573
+ category: "structural",
17574
+ reason: "The object's storage key, `<org>/<user>/<session>`-shaped and constrained to the org's prefix. Reads and versioned deletes resolve it by equality."
17575
+ },
17576
+ {
17577
+ database: "haansi_prod",
17578
+ table: "session_object_pointers",
17579
+ column: "version_id",
17580
+ category: "structural",
17581
+ reason: "The S3 version id a versioned delete targets. An opaque storage identifier."
17582
+ },
17583
+ // ── incident_signals agent columns: provenance and pointers, not content ──
17584
+ //
17585
+ // proposed_fields is NOT here — it can carry working_theory / root_cause
17586
+ // prose and is registered as an encrypted column. These four cannot.
17587
+ {
17588
+ database: "haansi_intelligence_prod",
17589
+ table: "incident_signals",
17590
+ column: "origin",
17591
+ category: "structural",
17592
+ reason: "A two-value vocabulary term ('system' | 'agent') that the server sets from the credential and every read filters on. Encrypting it would make the agent-proposal index unusable and hide provenance from the people it exists to inform."
17593
+ },
17594
+ {
17595
+ database: "haansi_intelligence_prod",
17596
+ table: "incident_signals",
17597
+ column: "agent_id",
17598
+ category: "operational",
17599
+ reason: "Which workload made a claim, derived by us from the credential \u2014 'credential:cli', 'user:<uuid>'. It names a caller of ours, not anything a customer wrote, and it is what an operator groups by when a particular agent starts proposing badly."
17600
+ },
17601
+ {
17602
+ database: "haansi_intelligence_prod",
17603
+ table: "incident_signals",
17604
+ column: "citations",
17605
+ category: "join-key",
17606
+ reason: "Typed references \u2014 {ref_type, ref_id} \u2014 to process_activity_facts rows in the same incident, validated server-side before the row is written. Pointers to content, never content; the rows they point at carry their own encryption."
17607
+ },
17608
+ {
17609
+ database: "haansi_intelligence_prod",
17610
+ table: "incident_signals",
17611
+ column: "producer",
17612
+ category: "not-personal",
17613
+ reason: "What produced a claim: model name, prompt version. Ours, not the customer's, and kept readable so a proposal that ages badly can be traced to the model that made it."
17614
+ },
17615
+ // ── incident_idempotency: request bookkeeping, never content ──
17616
+ //
17617
+ // The table records THAT a mutation already ran, keyed by
17618
+ // (organization_id, endpoint, idempotency_key). It deliberately stores no
17619
+ // response body — only the revision the mutation produced — because the
17620
+ // response would have been an incident overlay carrying root_cause and
17621
+ // resolution_summary, both registered encrypted columns, and a copy here
17622
+ // would be their plaintext sitting beside their own ciphertext. That design
17623
+ // choice is what makes these two columns safe to leave clear.
17624
+ {
17625
+ database: "haansi_intelligence_prod",
17626
+ table: "incident_idempotency",
17627
+ column: "endpoint",
17628
+ category: "operational",
17629
+ reason: "Our own route label \u2014 'declare', 'dismiss', 'patch'. Names a thing we wrote, not anything a customer did, and it is half of the UNIQUE constraint that makes a replay detectable: encrypting it destroys the conflict target and the same key would be accepted twice."
17630
+ },
17631
+ {
17632
+ database: "haansi_intelligence_prod",
17633
+ table: "incident_idempotency",
17634
+ column: "process_id",
17635
+ category: "join-key",
17636
+ reason: "The mined workstream's process_id \u2014 the same internal join key the incident overlay, signal and audit tables carry in the clear, kept here so a replay can read the overlay back without a second lookup. An identifier we generate, not anything a customer wrote."
17637
+ },
17638
+ {
17639
+ database: "haansi_intelligence_prod",
17640
+ table: "incident_idempotency",
17641
+ column: "idempotency_key",
17642
+ category: "join-key",
17643
+ reason: "An opaque client-generated token, matched by equality on every write as the other half of the UNIQUE constraint. Randomized encryption makes two copies of one key unequal, which turns every retry into a second apply \u2014 the exact failure the table exists to prevent."
17644
+ },
17645
+ {
17646
+ database: "haansi_intelligence_prod",
17647
+ table: "qna_messages",
17648
+ column: "rating_reasons",
17649
+ category: "operational",
17650
+ reason: "A server-validated five-code enum used by ARRAY DISTINCT/ORDER BY and platform-mining GROUP BY counts. Randomized encryption makes equal codes unequal and silently corrupts both aggregates; free-text feedback belongs in the separately encrypted rating_comment column."
17651
+ },
17652
+ // ── redaction_claims: a work queue that holds references, never content ──
17653
+ //
17654
+ // Same shape as embedding_claims below. The queue records WHICH staging
17655
+ // object needs redacting and WHO holds the lease; the session text itself is
17656
+ // fetched from object storage at claim time, redacted, and written to the
17657
+ // clean bucket. No session content is ever stored in a row here — that is
17658
+ // the property the whole design depends on, and it is why these four are
17659
+ // exclusions rather than a tier.
17660
+ {
17661
+ database: "haansi_prod",
17662
+ table: "redaction_claims",
17663
+ column: "staging_path",
17664
+ category: "join-key",
17665
+ reason: "The object key \u2014 org/user/session.jsonl. Part of the UNIQUE constraint that makes enqueue idempotent, so it is matched by equality on every insert; encrypting it destroys the conflict target and the queue would accept duplicate claims for one object. The identifiers it embeds are already columns on this table."
17666
+ },
17667
+ {
17668
+ database: "haansi_prod",
17669
+ table: "redaction_claims",
17670
+ column: "staging_token",
17671
+ category: "join-key",
17672
+ reason: "Identifies WHICH VERSION of the object this claim is for \u2014 the object's x-haansi-uploaded-at. The other half of the same UNIQUE constraint, matched by equality for the same reason. A timestamp, not personal data."
17673
+ },
17674
+ {
17675
+ database: "haansi_prod",
17676
+ table: "redaction_claims",
17677
+ column: "owner",
17678
+ category: "operational",
17679
+ reason: "Which WORKER holds the lease \u2014 hostname:pid. Names a process, not a person, and is compared by equality in the guarded write-back so a worker cannot complete another's claim. Encrypting it breaks that guard."
17680
+ },
17681
+ {
17682
+ database: "haansi_prod",
17683
+ table: "redaction_claims",
17684
+ column: "last_error",
17685
+ category: "operational",
17686
+ reason: "Operator diagnostics: why a claim was requeued or parked \u2014 a Presidio timeout, an object-store refusal. Truncated to 2000 characters at the write. It must never carry session TEXT, and does not: the worker records the exception message, never the content it was redacting. Encrypting it would make it unreadable in exactly the situation it exists for, which is someone asking why redaction stopped."
17687
+ },
17688
+ // ── embedding_claims: a work queue that holds references, never content ──
17689
+ //
17690
+ // The three text columns below exist because the leased embedding worker
17691
+ // needs to know WHICH row to embed and WHO holds the lease. The text to
17692
+ // embed is fetched from the target row at claim time and the vector written
17693
+ // straight back, so no customer content ever rests in this table.
17694
+ {
17695
+ database: "haansi_intelligence_prod",
17696
+ table: "embedding_claims",
17697
+ column: "source_table",
17698
+ category: "operational",
17699
+ reason: "A table NAME from the checked-in EMBEDDING_TABLES registry \u2014 one of six fixed values, never user input. Encrypting it would break the worker's own routing, since it dispatches on this column."
17700
+ },
17701
+ {
17702
+ database: "haansi_intelligence_prod",
17703
+ table: "embedding_claims",
17704
+ column: "owner",
17705
+ category: "operational",
17706
+ reason: "Identifies the WORKER holding the lease (hostname:pid), not a person. It is compared for equality on every write-back \u2014 the guard that stops a worker whose lease expired mid-inference overwriting the worker that took over \u2014 so it must remain matchable."
17707
+ },
17708
+ {
17709
+ database: "haansi_intelligence_prod",
17710
+ table: "embedding_claims",
17711
+ column: "last_error",
17712
+ category: "operational",
17713
+ reason: "A database or embedder error message, truncated to 2000 chars. It records why a claim failed (connection refused, embedder timeout), not the content being embedded \u2014 the worker fails BEFORE it holds any customer text, and the text is never part of the message. Kept readable because its only purpose is an operator diagnosing a stuck queue."
17714
+ },
17715
+ // ── source_events / sync_state / schema_migrations, classified 2026-09-13 ──
17716
+ // These arrived with the unified-integrations work and reached main
17717
+ // unclassified, which is what the ratchet caught.
17718
+ {
17719
+ database: "haansi_integrations_prod",
17720
+ table: "source_events",
17721
+ column: "normalize_error",
17722
+ category: "blocked",
17723
+ reason: "Should be encrypted, and is not. Unlike embedding_claims.last_error \u2014 which records a failure that happens BEFORE the worker holds any customer text \u2014 this is written from inside the normalizer while it is parsing raw_payload (normalization.ts markSourceEventNormalizeError, called with error.message from an arbitrary throw). A parse or validation error can quote the fragment it choked on, and that fragment is customer content. The predicates that read it are NULL checks, which survive encryption, so registering it is feasible \u2014 it needs the encryption job to sweep the existing plaintext rows, which is a backfill rather than a classification."
17724
+ },
17725
+ {
17726
+ database: "haansi_integrations_prod",
17727
+ table: "source_events",
17728
+ column: "object_type",
17729
+ category: "structural",
17730
+ reason: "Names the shape of the captured object (message, issue, pull_request). The normalizer dispatches on it and it is part of the uniqueness key, so it must stay readable; it is a vocabulary term, not content."
17731
+ },
17732
+ {
17733
+ database: "haansi_integrations_prod",
17734
+ table: "sync_state",
17735
+ column: "object_type",
17736
+ category: "structural",
17737
+ reason: "The same vocabulary term as source_events.object_type, keying the per-(platform, object_type) watermark. Encrypting it would make the cursor unreadable to the reader that must find it by that key."
17738
+ },
17739
+ {
17740
+ database: "haansi_integrations_prod",
17741
+ table: "source_events",
17742
+ column: "source_object_id",
17743
+ category: "join-key",
17744
+ reason: "The provider's own identifier for the object (a Slack ts, a GitHub node id). It is how a captured row is matched back to its source and de-duplicated across re-captures; encrypting it would break that equality. Pseudonymous by construction \u2014 it identifies an object, not a person."
17745
+ },
17746
+ {
17747
+ database: "haansi_integrations_prod",
17748
+ table: "source_events",
17749
+ column: "source_event_key",
17750
+ category: "join-key",
17751
+ reason: "The idempotency key a capture writes under, and the column the uniqueness constraint is built on. Equality here is what stops a replayed tap run from duplicating the corpus; an encrypted value with a random IV compares unequal to itself."
17752
+ },
17753
+ {
17754
+ database: "haansi_integrations_prod",
17755
+ table: "source_events",
17756
+ column: "ingest_mode",
17757
+ category: "operational",
17758
+ reason: "How the row arrived \u2014 tap, webhook, backfill. Read by the replay and catch-up paths to decide what may be re-processed. A closed vocabulary with no customer content."
17759
+ },
17760
+ {
17761
+ database: "haansi_integrations_prod",
17762
+ table: "source_events",
17763
+ column: "cursor_token",
17764
+ category: "operational",
17765
+ reason: "The provider watermark the row was captured at, in the same class as sync_state.cursor_value: opaque control-plane state that the resume path must read to avoid restarting a sync. Not source content."
17766
+ },
17767
+ {
17768
+ database: "haansi_integrations_prod",
17769
+ table: "source_events",
17770
+ column: "simulation_run_id",
17771
+ category: "not-personal",
17772
+ reason: "Tags a row as belonging to a simulation run so production reads can exclude it. Generated by us, never derived from the source."
17773
+ },
17774
+ {
17775
+ database: "haansi_integrations_prod",
17776
+ table: "schema_migrations",
17777
+ column: "git_sha",
17778
+ category: "operational",
17779
+ reason: "The commit a migration was applied from \u2014 SOC 2 CC8.1 audit-trail evidence about our own deploys. Carries no tenant data, and must stay readable to be evidence at all."
17780
+ },
17781
+ {
17782
+ database: "haansi_integrations_prod",
17783
+ table: "schema_migrations",
17784
+ column: "notes",
17785
+ category: "operational",
17786
+ reason: "Operator annotation on a ledger row (why a CONCURRENTLY migration was applied outside a transaction, that a row was backfill-stamped). Written by us about our own schema changes, not by or about a tenant."
17787
+ },
17788
+ {
17789
+ database: "haansi_integrations_prod",
17790
+ table: "source_accounts",
17791
+ column: "display_name",
17792
+ category: "blocked",
17793
+ reason: 'Personal data that should be encrypted, and is not. source_accounts has a COMPOSITE primary key (organization_id, source_platform, source_account_id) and no `id` column, while the encryption job addresses rows as `WHERE "<primaryKey>" = $1` \u2014 a single column. It was previously registered with primaryKey "id", which does not exist, so the job aborted when it reached this table. Pointing it at source_account_id instead would be worse than the crash: that column is not unique on its own, so one organization\'s UPDATE would rewrite another\'s row. Needs composite-key support in the job.'
17794
+ },
17795
+ {
17796
+ database: "haansi_intelligence_prod",
17797
+ table: "qna_charts",
17798
+ column: "engine",
17799
+ category: "structural",
17800
+ reason: "Which renderer draws the saved chart ('vega-lite'), CHECK-constrained. The reader dispatches on it; a code vocabulary term, not content."
17801
+ },
17802
+ {
17803
+ database: "haansi_intelligence_prod",
17804
+ table: "qna_charts",
17805
+ column: "tool_auth_scope",
17806
+ category: "operational",
17807
+ reason: "The access rule the chart's data was produced under ('org', 'permission:experts:read'), re-checked on every read. A code-authored policy name the read path must evaluate; no customer content."
17808
+ },
17809
+ {
17810
+ database: "haansi_integrations_prod",
17811
+ table: "capture_scan_ledger",
17812
+ column: "stream",
17813
+ category: "structural",
17814
+ reason: "The tap/poller stream name a scan covered (messages, issues). A fixed vocabulary the coverage reader filters on; no customer content."
17815
+ },
17816
+ {
17817
+ database: "haansi_integrations_prod",
17818
+ table: "capture_scan_ledger",
17819
+ column: "scope",
17820
+ category: "operational",
17821
+ reason: 'A fixed, code-authored description of what a scan covered ("every channel the app has joined"), taken from SCAN_PROOFS. Never customer content.'
17822
+ },
17823
+ {
17824
+ database: "haansi_integrations_prod",
17825
+ table: "capture_scan_ledger",
17826
+ column: "failure_reason",
17827
+ category: "operational",
17828
+ reason: "Why a capture scan stopped (a provider error class or our own message). Control-plane diagnostics for our runs, read by operators; it carries no captured content."
17829
+ },
17830
+ {
17831
+ database: "haansi_integrations_prod",
17832
+ table: "sync_state",
17833
+ column: "cursor_type",
17834
+ category: "operational",
17835
+ reason: "Names the cursor protocol, such as timestamp or page_token. The sync reader dispatches on it and it contains no customer content; encrypting it would prevent the ingestion worker from interpreting cursor_value."
17836
+ },
17837
+ {
17838
+ database: "haansi_integrations_prod",
17839
+ table: "sync_state",
17840
+ column: "cursor_value",
17841
+ category: "operational",
17842
+ reason: "An opaque provider watermark used to resume incremental ingestion. It is control-plane state rather than source content and must remain readable by the raw cursor upsert/read path to avoid restarting syncs."
17843
+ },
17844
+ {
17845
+ database: "haansi_integrations_prod",
17846
+ table: "source_accounts",
17847
+ column: "account_type",
17848
+ category: "operational",
17849
+ reason: "A fixed routing enum (user_oauth, org_oauth, or service_account), not a person attribute. Ingestion uses it to choose the account flow, so encrypting it would break control-plane dispatch predicates."
17850
+ },
17851
+ {
17852
+ database: "haansi_integrations_prod",
17853
+ table: "source_accounts",
17854
+ column: "metadata",
17855
+ category: "blocked",
17856
+ reason: "Provider metadata may contain tenant data and should be encrypted, but source_accounts has a composite primary key while the conversion job supports only one primary-key column. It shares display_name's blocker."
17857
+ },
17858
+ {
17859
+ database: "haansi_integrations_prod",
17860
+ table: "google_workspace_service_accounts",
17861
+ column: "key_json_encrypted",
17862
+ category: "structural",
17863
+ reason: "The service-account private key is already encrypted by the owning Google Workspace credential helper. Registering it for transparent encryption would double-wrap it and break the explicit decrypt reader."
17864
+ },
17865
+ {
17866
+ database: "haansi_integrations_prod",
17867
+ table: "google_workspace_service_accounts",
17868
+ column: "domain",
17869
+ category: "not-personal",
17870
+ reason: "The customer's Workspace domain identifies an organization rather than an individual and is required as routing configuration for delegated Google API calls. Individual administrator email is encrypted separately."
17871
+ },
17872
+ {
17873
+ database: "haansi_integrations_prod",
17874
+ table: "google_workspace_service_accounts",
17875
+ column: "delegated_scopes",
17876
+ category: "operational",
17877
+ reason: "A list of fixed Google OAuth permission names selected for delegation. It contains configuration rather than subject data and must remain readable when constructing the delegated credential."
17878
+ },
17879
+ {
17880
+ database: "haansi_integrations_prod",
17881
+ table: "google_workspace_service_accounts",
17882
+ column: "discovery_status",
17883
+ category: "operational",
17884
+ reason: "A bounded workflow-state value such as pending or completed. Operators and discovery code filter on it; it carries no customer content and encrypting it would break those status predicates."
17885
+ },
17886
+ {
17887
+ database: "haansi_integrations_prod",
17888
+ table: "google_workspace_service_accounts",
17889
+ column: "discovery_error",
17890
+ category: "blocked",
17891
+ reason: "A Google API error can repeat a tenant email or domain and should be encrypted, but updateDiscoveryStatus writes through a raw pool without organization context. Move that writer through the doorway before tiering it."
17892
+ },
17893
+ {
17894
+ database: "haansi_integrations_prod",
17895
+ table: "github_path_token_stats",
17896
+ column: "common_tokens",
17897
+ category: "derived",
17898
+ reason: "A derived set of high-frequency repository path tokens used for exact membership filtering during graph edge building. Encrypting the derived lookup set would make its token comparisons unusable."
17899
+ },
17900
+ {
17901
+ database: "haansi_prod",
17902
+ table: "platform_admin_access_log",
17903
+ column: "actor_email",
17904
+ category: "audit-actor",
17905
+ reason: "HAANSI staff, not a data subject. This log exists to prove WHO reached a customer's data; encrypting the actor defeats the control and the SOC 2 evidence it produces. The same address in organization_users IS encrypted \u2014 the distinction is the role the row plays, which no name-based rule can see."
17906
+ },
17907
+ {
17908
+ database: "haansi_prod",
17909
+ table: "machine_credential_audit_log",
17910
+ column: "actor_email",
17911
+ category: "audit-actor",
17912
+ reason: "Same as platform_admin_access_log.actor_email \u2014 audit trail actor."
17913
+ },
17914
+ {
17915
+ database: "haansi_prod",
17916
+ table: "waitlist_activity_log",
17917
+ column: "actor_email",
17918
+ category: "audit-actor",
17919
+ reason: "Same as platform_admin_access_log.actor_email \u2014 audit trail actor."
17920
+ },
17921
+ {
17922
+ database: "haansi_prod",
17923
+ table: "organizations",
17924
+ column: "name",
17925
+ category: "not-personal",
17926
+ reason: "The organisation's own name, which the product renders on every page and every admin surface. A company name is not personal data; for a one-person org it can be identifying, which is accepted here because encrypting it would blind the platform to its own tenants."
17927
+ },
17928
+ // Residency control plane (migration byod-residency/001). Recorded here the
17929
+ // moment the columns were added, which is the same gate doing the same job as
17930
+ // it did for the per-org key columns below.
17931
+ //
17932
+ // These are ROUTING STATE, not tenant content: closed unions enforced by CHECK
17933
+ // constraints, read on the resolution path of every tenant request to decide
17934
+ // which database answers it. Encrypting them would be self-defeating in the
17935
+ // literal sense — the resolver would have to decrypt a value in order to learn
17936
+ // where the decryption key's data lives. Same reasoning that kept
17937
+ // qna_messages.rating_reasons out: a value the database itself must compare
17938
+ // cannot be randomised ciphertext.
17939
+ {
17940
+ database: "haansi_prod",
17941
+ table: "organizations",
17942
+ column: "authoritative_residency",
17943
+ category: "not-personal",
17944
+ reason: "Which database is authoritative for this org's tenant content: 'platform' or 'byod', enforced by a CHECK constraint. Operational routing state, read before every tenant query, and not derived from customer data."
17945
+ },
17946
+ {
17947
+ database: "haansi_prod",
17948
+ table: "organizations",
17949
+ column: "migration_phase",
17950
+ category: "not-personal",
17951
+ reason: "Which residency migration step is in progress, from a closed set enforced by a CHECK constraint. Says nothing about the org beyond the fact that an operator started a migration, and the write fence compares it on the admission path."
17952
+ },
17953
+ // The per-org key columns themselves (migration encryption/005). Recorded
17954
+ // the moment they were added, because the Phase 0 gate demanded it — which
17955
+ // is the gate doing its job on the very next schema change.
17956
+ {
17957
+ database: "haansi_prod",
17958
+ table: "organizations",
17959
+ column: "encryption_key_id",
17960
+ category: "structural",
17961
+ reason: "Names which key encrypted this org's data (e.g. org:<uuid>:v1). It is the LABEL on the key, not personal data, and the resolver reads it to decide which key to unwrap \u2014 encrypting it would make the ciphertext undecryptable by the very lookup that needs it."
17962
+ },
17963
+ {
17964
+ database: "haansi_prod",
17965
+ table: "organizations",
17966
+ column: "encryption_key_wrapped",
17967
+ category: "structural",
17968
+ reason: "The org's DEK, already encrypted \u2014 by KMS, under the CMK. Encrypting it again with a key derived FROM it is circular. At rest it is an opaque blob useless without kms:Decrypt, which is the whole reason it can live in Postgres rather than as per-org Secrets Manager sprawl."
17969
+ },
17970
+ {
17971
+ database: "haansi_prod",
17972
+ table: "organizations",
17973
+ column: "encryption_previous_keys",
17974
+ category: "structural",
17975
+ reason: "Retired DEKs as {keyId: KMS-wrapped DEK}. Same argument as encryption_key_wrapped, one generation back: already encrypted under the CMK, and encrypting it with a key derived from itself is circular. Kept so ciphertext written before a rotation stays readable \u2014 and kept on this row so the deletion tick's crypto-shred destroys every generation in one statement."
17976
+ },
17977
+ {
17978
+ database: "haansi_prod",
17979
+ table: "organizations",
17980
+ column: "encryption_cmk_id",
17981
+ category: "structural",
17982
+ reason: "A KMS key ARN \u2014 infrastructure identity, not personal data. Needed in plaintext to know which CMK can unwrap this org's DEK, which matters the moment a BYOK org brings its own."
17983
+ },
17984
+ {
17985
+ database: "haansi_intelligence_prod",
17986
+ table: "person_source_identities",
17987
+ column: "native_user_id",
17988
+ category: "join-key",
17989
+ reason: "The platform-assigned user id (GitHub numeric id, Slack U\u2026, Atlassian accountId) that makes a native identity unique and lets a reintegration find the same row again. Pseudonymous, never an email: email-only identities leave it NULL and use the encrypted email + email_blind."
17990
+ },
17991
+ {
17992
+ database: "haansi_intelligence_prod",
17993
+ table: "activity_participants",
17994
+ column: "role",
17995
+ category: "operational",
17996
+ reason: "A seven-value CHECK-constrained enum (author, reviewer, \u2026) that the contribution model weights and filters on. No personal data."
17997
+ },
17998
+ {
17999
+ database: "haansi_intelligence_prod",
18000
+ table: "process_activity_facts",
18001
+ column: "event_verb",
18002
+ category: "operational",
18003
+ reason: "A nine-value CHECK-constrained enum (created, commented, reviewed, \u2026) copied from the canonical activity. No personal data."
18004
+ },
18005
+ {
18006
+ database: "haansi_intelligence_prod",
18007
+ table: "organization_mining_config",
18008
+ column: "heuristic_calibration",
18009
+ category: "derived",
18010
+ reason: "Calibrated heuristic thresholds (object-graph/122): three score cuts, the percentiles and sample size they came from, and a timestamp. Numbers computed from score distributions; no content or personal data."
18011
+ },
18012
+ {
18013
+ database: "haansi_intelligence_prod",
18014
+ table: "organization_mining_config",
18015
+ column: "rerun_hold_reason",
18016
+ category: "operational",
18017
+ reason: "Why a hard-reset re-run stopped midway (object-graph/123): a service error message such as 'Replay cancelled (service shutting down)', shown to operators. No content or personal data."
18018
+ },
18019
+ {
18020
+ database: "haansi_intelligence_prod",
18021
+ table: "process_activity_facts",
18022
+ column: "evidence_fingerprint",
18023
+ category: "derived",
18024
+ reason: "A sha256 hex digest of the event's sorted (role, object id) pairs, stamped by the extractor and compared by equality to tell whether an event's inputs changed. One-way over opaque ids; no content or personal data."
18025
+ },
18026
+ {
18027
+ database: "haansi_intelligence_prod",
18028
+ table: "expert_dirty_queue",
18029
+ column: "scope",
18030
+ category: "operational",
18031
+ reason: "A five-value CHECK-constrained enum naming what kind of thing is dirty. Part of the coalescing primary key; no personal data."
18032
+ },
18033
+ {
18034
+ database: "haansi_intelligence_prod",
18035
+ table: "expert_dirty_queue",
18036
+ column: "scope_id",
18037
+ category: "join-key",
18038
+ reason: "The UUID (as text) of the dirty person object, identity group, source identity or member, or '' for a whole org. Part of the coalescing primary key, matched by equality on every enqueue."
18039
+ },
18040
+ {
18041
+ database: "haansi_intelligence_prod",
18042
+ table: "expert_dirty_queue",
18043
+ column: "reasons",
18044
+ category: "operational",
18045
+ reason: "Fixed trigger reason codes (participants_insert, activity_update, \u2026) unioned with DISTINCT on coalesce. No personal data."
18046
+ },
18047
+ {
18048
+ database: "haansi_intelligence_prod",
18049
+ table: "expert_dirty_queue",
18050
+ column: "lease_owner",
18051
+ category: "operational",
18052
+ reason: "The worker id holding the processing lease. Infrastructure identity, compared by equality on acknowledgement."
18053
+ },
18054
+ {
18055
+ database: "haansi_prod",
18056
+ table: "expert_member_outbox",
18057
+ column: "change",
18058
+ category: "operational",
18059
+ reason: "A three-value CHECK-constrained enum (insert, update, delete). No personal data."
18060
+ },
18061
+ // ── mining_pending_decisions (object-graph/113): ids, codes and error strings ──
18062
+ {
18063
+ database: "haansi_intelligence_prod",
18064
+ table: "mining_pending_decisions",
18065
+ column: "reason",
18066
+ category: "operational",
18067
+ reason: "A six-value CHECK-constrained code for why the activity is pending (awaiting_llm, llm_empty, \u2026). No personal data."
18068
+ },
18069
+ {
18070
+ database: "haansi_intelligence_prod",
18071
+ table: "mining_pending_decisions",
18072
+ column: "last_error",
18073
+ category: "operational",
18074
+ reason: "The error class and message of the failed LLM attempt, truncated. Writers pass error text, never model output or event content."
18075
+ },
18076
+ {
18077
+ database: "haansi_intelligence_prod",
18078
+ table: "mining_pending_decisions",
18079
+ column: "rejected_reference",
18080
+ category: "operational",
18081
+ reason: "The process id or activity id the rejected decision named, echoed into the retry prompt. An identifier, not content."
18082
+ },
18083
+ {
18084
+ database: "haansi_intelligence_prod",
18085
+ table: "mining_pending_decisions",
18086
+ column: "candidate_process_ids",
18087
+ category: "join-key",
18088
+ reason: "Process ids the scorer offered for this activity; the decision is validated against this set by equality."
18089
+ },
18090
+ {
18091
+ database: "haansi_intelligence_prod",
18092
+ table: "mining_pending_decisions",
18093
+ column: "heuristic",
18094
+ category: "derived",
18095
+ reason: "Scorer output for the activity: numeric scores keyed by candidate process id. Derived from the evidence graph, carries no content."
18096
+ },
18097
+ // ── process_state_history (object-graph/114): ids, codes, the category ──
18098
+ {
18099
+ database: "haansi_intelligence_prod",
18100
+ table: "process_state_history",
18101
+ column: "process_id",
18102
+ category: "join-key",
18103
+ reason: "The mined process's process_id \u2014 the internal join key to process_registry (composite FK). An identifier we generate."
18104
+ },
18105
+ {
18106
+ database: "haansi_intelligence_prod",
18107
+ table: "process_state_history",
18108
+ column: "curation_state",
18109
+ category: "operational",
18110
+ reason: "A five-value CHECK-constrained curation code copied from process_registry.curation_state. No personal data."
18111
+ },
18112
+ {
18113
+ database: "haansi_intelligence_prod",
18114
+ table: "process_state_history",
18115
+ column: "category",
18116
+ category: "derived",
18117
+ reason: "The process's taxonomy category at that point, copied from process_registry.category, which is stored in the clear and read by equality. A label the classifier or a curator picked, not content."
18118
+ },
18119
+ {
18120
+ database: "haansi_intelligence_prod",
18121
+ table: "process_state_history",
18122
+ column: "merged_into",
18123
+ category: "join-key",
18124
+ reason: "The process_id this process was merged into \u2014 a process_registry join key the as-of read follows by equality."
18125
+ },
18126
+ {
18127
+ database: "haansi_intelligence_prod",
18128
+ table: "process_state_history",
18129
+ column: "actor_kind",
18130
+ category: "operational",
18131
+ reason: "A three-value CHECK-constrained code (user, system, model). The user, when there is one, is actor_user_id (a uuid)."
18132
+ },
18133
+ {
18134
+ database: "haansi_intelligence_prod",
18135
+ table: "process_state_history",
18136
+ column: "source_command",
18137
+ category: "operational",
18138
+ reason: "The typed command that wrote the row (merge, curation, \u2026), a CHECK-constrained lowercase identifier. No personal data."
18139
+ },
18140
+ {
18141
+ database: "haansi_intelligence_prod",
18142
+ table: "identity_disputes",
18143
+ column: "link_source",
18144
+ category: "operational",
18145
+ reason: "A two-value CHECK-constrained enum (manual, email_blind). No personal data."
18146
+ },
18147
+ {
18148
+ database: "haansi_intelligence_prod",
18149
+ table: "identity_disputes",
18150
+ column: "status",
18151
+ category: "operational",
18152
+ reason: "A four-value CHECK-constrained dispute state. No personal data."
18153
+ },
18154
+ {
18155
+ database: "haansi_intelligence_prod",
18156
+ table: "identity_disputes",
18157
+ column: "resolution",
18158
+ category: "operational",
18159
+ reason: "A CHECK-constrained resolution code. No personal data."
18160
+ },
18161
+ {
18162
+ database: "haansi_intelligence_prod",
18163
+ table: "member_identity_links",
18164
+ column: "link_source",
18165
+ category: "operational",
18166
+ reason: "A two-value CHECK-constrained enum (manual, email_blind). No personal data."
18167
+ },
18168
+ {
18169
+ database: "haansi_intelligence_prod",
18170
+ table: "member_identity_links",
18171
+ column: "state",
18172
+ category: "operational",
18173
+ reason: "A four-value CHECK-constrained link state. No personal data."
18174
+ },
18175
+ {
18176
+ database: "haansi_intelligence_prod",
18177
+ table: "member_identity_links",
18178
+ column: "conflict_reason",
18179
+ category: "operational",
18180
+ reason: "A CHECK-constrained reason code. No personal data."
18181
+ },
18182
+ {
18183
+ database: "haansi_intelligence_prod",
18184
+ table: "expert_profile_facts",
18185
+ column: "platform_breakdown",
18186
+ category: "derived",
18187
+ reason: "Counts and scores per platform and entity type. Numbers and enum labels only \u2014 no names or content."
18188
+ },
18189
+ {
18190
+ database: "haansi_intelligence_prod",
18191
+ table: "expert_profile_facts",
18192
+ column: "role_breakdown",
18193
+ category: "derived",
18194
+ reason: "Counts per participant role enum. No personal data."
18195
+ },
18196
+ {
18197
+ database: "haansi_intelligence_prod",
18198
+ table: "expert_profile_facts",
18199
+ column: "top_workstreams",
18200
+ category: "derived",
18201
+ reason: "Process ids with counts, scores and timestamps. Names are joined at read time from process_registry, so no plaintext name is copied here."
18202
+ },
18203
+ {
18204
+ database: "haansi_intelligence_prod",
18205
+ table: "expert_profile_facts",
18206
+ column: "collaborators",
18207
+ category: "derived",
18208
+ reason: "Organization-user ids with shared-activity counts. Names are joined at read time from organization_users."
18209
+ },
18210
+ {
18211
+ database: "haansi_intelligence_prod",
18212
+ table: "expert_profile_facts",
18213
+ column: "recent_activities",
18214
+ category: "derived",
18215
+ reason: "Activity ids, target object ids, role and verb enums, and timestamps for the profile feed. Titles are read from objects at request time, so no content or name is copied here."
18216
+ },
18217
+ // Digest delivery (migrations/digests-platform/001). Schedules, delivery
18218
+ // state and inbox references only; digest text stays in the encrypted
18219
+ // entry and is never copied here.
18220
+ {
18221
+ database: "haansi_prod",
18222
+ table: "digest_subscriptions",
18223
+ column: "scope",
18224
+ category: "operational",
18225
+ reason: "A two-value CHECK-constrained enum: member or org."
18226
+ },
18227
+ {
18228
+ database: "haansi_prod",
18229
+ table: "digest_subscriptions",
18230
+ column: "recipe",
18231
+ category: "operational",
18232
+ reason: "The prompt recipe key, a fixed code-defined string."
18233
+ },
18234
+ {
18235
+ database: "haansi_prod",
18236
+ table: "digest_subscriptions",
18237
+ column: "cadence",
18238
+ category: "operational",
18239
+ reason: "A CHECK-constrained enum naming the schedule length."
18240
+ },
18241
+ {
18242
+ database: "haansi_prod",
18243
+ table: "digest_subscriptions",
18244
+ column: "timezone",
18245
+ category: "operational",
18246
+ reason: "An IANA timezone name the schedule runs in."
18247
+ },
18248
+ {
18249
+ database: "haansi_prod",
18250
+ table: "digest_subscriptions",
18251
+ column: "channels",
18252
+ category: "operational",
18253
+ reason: "CHECK-constrained channel names (email, slack_dm)."
18254
+ },
18255
+ // Teams (migrations/digests-platform/004): admin-entered labels for how
18256
+ // the organization groups its people, shown to its own members. Not
18257
+ // customer content drawn from integrations.
18258
+ // Work logs (migrations/digests/004): `text` is encrypted (column
18259
+ // registry); these two are machine values.
18260
+ {
18261
+ database: "haansi_intelligence_prod",
18262
+ table: "digest_work_logs",
18263
+ column: "workstream_id",
18264
+ category: "operational",
18265
+ reason: "A process_registry id the author tagged the entry with."
18266
+ },
18267
+ {
18268
+ database: "haansi_intelligence_prod",
18269
+ table: "digest_work_logs",
18270
+ column: "source",
18271
+ category: "operational",
18272
+ reason: "A CHECK-constrained enum: web, cli, slack or mcp."
18273
+ },
18274
+ {
18275
+ database: "haansi_prod",
18276
+ table: "teams",
18277
+ column: "name",
18278
+ category: "operational",
18279
+ reason: "An admin-entered team label (e.g. Payments), shown to the org's own members."
18280
+ },
18281
+ {
18282
+ database: "haansi_prod",
18283
+ table: "teams",
18284
+ column: "description",
18285
+ category: "operational",
18286
+ reason: "An admin-entered one-line description of a team, shown to the org's own members."
18287
+ },
18288
+ {
18289
+ database: "haansi_prod",
18290
+ table: "digest_org_policies",
18291
+ column: "org_timezone",
18292
+ category: "operational",
18293
+ reason: "An IANA timezone name the org digest's weeks are in."
18294
+ },
18295
+ {
18296
+ database: "haansi_prod",
18297
+ table: "digest_org_policies",
18298
+ column: "allowed_channels",
18299
+ category: "operational",
18300
+ reason: "CHECK-constrained channel names (email, slack_dm)."
18301
+ },
18302
+ {
18303
+ database: "haansi_prod",
18304
+ table: "digest_deliveries",
18305
+ column: "timezone",
18306
+ category: "operational",
18307
+ reason: "An IANA timezone name the week was computed in."
18308
+ },
18309
+ {
18310
+ database: "haansi_prod",
18311
+ table: "digest_deliveries",
18312
+ column: "channel",
18313
+ category: "operational",
18314
+ reason: "A CHECK-constrained enum naming the delivery channel."
18315
+ },
18316
+ {
18317
+ database: "haansi_prod",
18318
+ table: "digest_deliveries",
18319
+ column: "status",
18320
+ category: "operational",
18321
+ reason: "A CHECK-constrained enum of delivery state."
18322
+ },
18323
+ {
18324
+ database: "haansi_prod",
18325
+ table: "digest_deliveries",
18326
+ column: "reason",
18327
+ category: "operational",
18328
+ reason: "A short code such as quiet or no_slack_identity, never an error message."
18329
+ },
18330
+ {
18331
+ database: "haansi_prod",
18332
+ table: "user_notifications",
18333
+ column: "kind",
18334
+ category: "operational",
18335
+ reason: "A CHECK-constrained enum naming the notification type."
18336
+ },
18337
+ {
18338
+ database: "haansi_prod",
18339
+ table: "user_notifications",
18340
+ column: "ref_id",
18341
+ category: "join-key",
18342
+ reason: "The id of the thing the notification points at, e.g. a digest entry UUID."
18343
+ },
18344
+ {
18345
+ database: "haansi_prod",
18346
+ table: "user_notifications",
18347
+ column: "data",
18348
+ category: "structural",
18349
+ reason: "Non-sensitive display fields only (period dates, status); content stays in the referenced record."
18350
+ },
18351
+ // Digests (migrations/digests/002). Queue state, citation ids and the
18352
+ // access log carry no content: the entry's text lives in knowledge_artifacts,
18353
+ // whose columns are already encrypted.
18354
+ {
18355
+ database: "haansi_intelligence_prod",
18356
+ table: "digest_generation_queue",
18357
+ column: "digest_scope",
18358
+ category: "operational",
18359
+ reason: "A two-value CHECK-constrained enum: member or org."
18360
+ },
18361
+ {
18362
+ database: "haansi_intelligence_prod",
18363
+ table: "digest_generation_queue",
18364
+ column: "period_kind",
18365
+ category: "operational",
18366
+ reason: "A CHECK-constrained enum naming the period length."
18367
+ },
18368
+ {
18369
+ database: "haansi_intelligence_prod",
18370
+ table: "digest_generation_queue",
18371
+ column: "timezone",
18372
+ category: "operational",
18373
+ reason: "An IANA timezone name the week is computed in."
18374
+ },
18375
+ {
18376
+ database: "haansi_intelligence_prod",
18377
+ table: "digest_generation_queue",
18378
+ column: "recipe",
18379
+ category: "operational",
18380
+ reason: "The prompt recipe key, a fixed code-defined string."
18381
+ },
18382
+ {
18383
+ database: "haansi_intelligence_prod",
18384
+ table: "digest_generation_queue",
18385
+ column: "reason",
18386
+ category: "operational",
18387
+ reason: "A three-value CHECK-constrained enum: scheduled, on_demand or regenerate."
18388
+ },
18389
+ {
18390
+ database: "haansi_intelligence_prod",
18391
+ table: "digest_generation_queue",
18392
+ column: "status",
18393
+ category: "operational",
18394
+ reason: "A four-value CHECK-constrained enum of queue state."
18395
+ },
18396
+ {
18397
+ database: "haansi_intelligence_prod",
18398
+ table: "digest_generation_queue",
18399
+ column: "lease_owner",
18400
+ category: "operational",
18401
+ reason: "The worker's host and process id holding the lease."
18402
+ },
18403
+ {
18404
+ database: "haansi_intelligence_prod",
18405
+ table: "digest_generation_queue",
18406
+ column: "error_code",
18407
+ category: "operational",
18408
+ reason: "A short code such as generation_error, never an error message."
18409
+ },
18410
+ {
18411
+ database: "haansi_intelligence_prod",
18412
+ table: "onboarding_guides",
18413
+ column: "status",
18414
+ category: "operational",
18415
+ reason: "A CHECK-constrained lifecycle enum (draft, published, unpublished, deleted) that decides who may see the guide; read by SQL predicates."
18416
+ },
18417
+ {
18418
+ database: "haansi_intelligence_prod",
18419
+ table: "onboarding_guides",
18420
+ column: "template_id",
18421
+ category: "operational",
18422
+ reason: "The id of a builtin report template (e.g. workstream-handoff), never user text."
18423
+ },
18424
+ {
18425
+ database: "haansi_intelligence_prod",
18426
+ table: "onboarding_guides",
18427
+ column: "audience",
18428
+ category: "operational",
18429
+ reason: "A short audience code (engineer, business, agent) chosen from a fixed list."
18430
+ },
18431
+ {
18432
+ database: "haansi_intelligence_prod",
18433
+ table: "onboarding_guide_source_refs",
18434
+ column: "ref_type",
18435
+ category: "operational",
18436
+ reason: "A CHECK-constrained enum naming the kind of cited source."
18437
+ },
18438
+ {
18439
+ database: "haansi_intelligence_prod",
18440
+ table: "onboarding_guide_source_refs",
18441
+ column: "ref_id",
18442
+ category: "join-key",
18443
+ reason: "The id of the cited workstream, incident, solution, artifact, object or guide \u2014 a UUID or a process id, matched by equality."
18444
+ },
18445
+ {
18446
+ database: "haansi_intelligence_prod",
18447
+ table: "onboarding_guide_source_refs",
18448
+ column: "state",
18449
+ category: "operational",
18450
+ reason: "A CHECK-constrained enum (live, revoked)."
18451
+ },
18452
+ {
18453
+ database: "haansi_intelligence_prod",
18454
+ table: "digest_evidence_refs",
18455
+ column: "evidence_key",
18456
+ category: "structural",
18457
+ reason: "The E-key (E1, E2, \u2026) the entry cites."
18458
+ },
18459
+ {
18460
+ database: "haansi_intelligence_prod",
18461
+ table: "digest_evidence_refs",
18462
+ column: "ref_type",
18463
+ category: "operational",
18464
+ reason: "A CHECK-constrained enum naming the kind of cited source."
18465
+ },
18466
+ {
18467
+ database: "haansi_intelligence_prod",
18468
+ table: "digest_evidence_refs",
18469
+ column: "ref_id",
18470
+ category: "join-key",
18471
+ reason: "The id of the cited activity, workstream, artifact or incident \u2014 a UUID or a process id, matched by equality."
18472
+ },
18473
+ {
18474
+ database: "haansi_intelligence_prod",
18475
+ table: "digest_access_log",
18476
+ column: "action",
18477
+ category: "operational",
18478
+ reason: "A CHECK-constrained enum naming what the actor did."
18479
+ },
18480
+ {
18481
+ database: "haansi_intelligence_prod",
18482
+ table: "digest_access_log",
18483
+ column: "surface",
18484
+ category: "operational",
18485
+ reason: "The product surface of the request (web, cli, mcp), taken from the credential."
18486
+ },
18487
+ {
18488
+ database: "haansi_intelligence_prod",
18489
+ table: "expert_evidence_refs",
18490
+ column: "source_type",
18491
+ category: "operational",
18492
+ reason: "A four-value CHECK-constrained enum naming the kind of cited source."
18493
+ },
18494
+ {
18495
+ database: "haansi_intelligence_prod",
18496
+ table: "expert_evidence_refs",
18497
+ column: "source_id",
18498
+ category: "join-key",
18499
+ reason: "The id of the cited activity, object, workstream or artifact \u2014 a UUID or a process id, matched by equality when a source is deleted."
18500
+ },
18501
+ {
18502
+ database: "haansi_intelligence_prod",
18503
+ table: "expert_profiles",
18504
+ column: "model",
18505
+ category: "operational",
18506
+ reason: "The model id that generated the version. Configuration, not content."
18507
+ },
18508
+ {
18509
+ database: "haansi_intelligence_prod",
18510
+ table: "expert_profiles",
18511
+ column: "prompt_version",
18512
+ category: "operational",
18513
+ reason: "A prompt revision label. Configuration, not content."
18514
+ },
18515
+ {
18516
+ database: "haansi_intelligence_prod",
18517
+ table: "expert_profiles",
18518
+ column: "input_hash",
18519
+ category: "derived",
18520
+ reason: "SHA-256 of the evidence pack, compared by equality to skip an unchanged regeneration. A digest, not content."
18521
+ },
18522
+ {
18523
+ database: "haansi_intelligence_prod",
18524
+ table: "expert_areas",
18525
+ column: "horizon",
18526
+ category: "operational",
18527
+ reason: "A two-value CHECK-constrained enum (established, current)."
18528
+ },
18529
+ {
18530
+ database: "haansi_intelligence_prod",
18531
+ table: "expert_areas",
18532
+ column: "depth",
18533
+ category: "operational",
18534
+ reason: "A three-value CHECK-constrained enum (owner, contributor, familiar)."
18535
+ },
18536
+ {
18537
+ database: "haansi_intelligence_prod",
18538
+ table: "expert_areas",
18539
+ column: "confidence",
18540
+ category: "operational",
18541
+ reason: "A three-value CHECK-constrained enum (high, medium, low)."
18542
+ },
18543
+ {
18544
+ database: "haansi_intelligence_prod",
18545
+ table: "expert_profiles",
18546
+ column: "validation_drops",
18547
+ category: "derived",
18548
+ reason: "object-graph/102. Integer counts of what the profile validator rejected, keyed by reason (area_work_shaped, uncited_claim, \u2026). Numbers only \u2014 none of the rejected text is kept, which is the whole point: the counts exist so a thin profile can be explained without storing what the model wrote about a person."
18549
+ },
18550
+ // Org-level expertise grouping (object-graph/100). The pass's lease and its
18551
+ // audit log; the only content-bearing columns on either table are the area
18552
+ // names, which are classified in column-registry.ts.
18553
+ {
18554
+ database: "haansi_intelligence_prod",
18555
+ table: "expert_grouping_state",
18556
+ column: "claimed_by",
18557
+ category: "operational",
18558
+ reason: "The pipeline worker id holding the org's grouping lease. Infrastructure identity, never content."
18559
+ },
18560
+ {
18561
+ database: "haansi_intelligence_prod",
18562
+ table: "expert_grouping_state",
18563
+ column: "last_input_hash",
18564
+ category: "derived",
18565
+ reason: "A SHA-256 digest of the vocabulary the last run saw, used to skip an unchanged org. One-way and carries no names."
18566
+ },
18567
+ {
18568
+ database: "haansi_intelligence_prod",
18569
+ table: "expert_area_merge_log",
18570
+ column: "action",
18571
+ category: "operational",
18572
+ reason: "A three-value CHECK-constrained enum (merge, rename, describe)."
18573
+ },
18574
+ {
18575
+ database: "haansi_intelligence_prod",
18576
+ table: "expert_area_merge_log",
18577
+ column: "model",
18578
+ category: "operational",
18579
+ reason: "The provider model id that produced the grouping, for attributing a bad run. Vendor identifier, never content."
18580
+ },
18581
+ {
18582
+ database: "haansi_intelligence_prod",
18583
+ table: "expert_profile_citations",
18584
+ column: "contribution",
18585
+ category: "derived",
18586
+ reason: "What the member contributed to one cited source: two counts and two dates (object-graph/128). No titles, names or text."
18587
+ },
18588
+ // Trending topics (trending-clustering/001, 002). A topic is stored as ids:
18589
+ // which item is its centre and which items belong to it. Titles are read
18590
+ // from the items themselves (knowledge_artifacts.title,
18591
+ // process_registry.name — both encrypted), never copied here.
18592
+ {
18593
+ database: "haansi_intelligence_prod",
18594
+ table: "artifact_clusters",
18595
+ column: "theme",
18596
+ category: "derived",
18597
+ reason: "Always the empty string. It once held a copy of the centre's title; the trending tick now writes it empty and the API reads the title from the item. Kept only because the column is NOT NULL."
18598
+ },
18599
+ {
18600
+ database: "haansi_intelligence_prod",
18601
+ table: "artifact_clusters",
18602
+ column: "theme_kind",
18603
+ category: "operational",
18604
+ reason: "Enum: 'artifact' or 'workstream' \u2014 which kind of item is the topic's centre. Never content."
18605
+ },
18606
+ {
18607
+ database: "haansi_intelligence_prod",
18608
+ table: "artifact_clusters",
18609
+ column: "theme_process_id",
18610
+ category: "derived",
18611
+ reason: "The process id of the workstream at a topic's centre. An opaque identifier into process_registry; its name stays there, encrypted."
18612
+ },
18613
+ {
18614
+ database: "haansi_intelligence_prod",
18615
+ table: "artifact_cluster_workstreams",
18616
+ column: "process_id",
18617
+ category: "derived",
18618
+ reason: "The process id of a workstream that belongs to a topic. An opaque identifier into process_registry; no name or text."
18619
+ },
18620
+ {
18621
+ database: "haansi_intelligence_prod",
18622
+ table: "org_expertise_areas",
18623
+ column: "curated_at",
18624
+ category: "operational",
18625
+ reason: "Manual edit marker used by grouping and description writers."
18626
+ },
18627
+ // Cited expertise-area descriptions (object-graph/129). The description
18628
+ // text itself is org_expertise_areas.description, which is encrypted.
18629
+ {
18630
+ database: "haansi_intelligence_prod",
18631
+ table: "org_expertise_areas",
18632
+ column: "description_citations",
18633
+ category: "derived",
18634
+ reason: "Per sentence of the description, the sources it cites, as '<source_type>:<source_id>' keys. Ids only \u2014 no titles, names or text."
18635
+ },
18636
+ {
18637
+ database: "haansi_intelligence_prod",
18638
+ table: "org_expertise_areas",
18639
+ column: "description_input_hash",
18640
+ category: "derived",
18641
+ reason: "SHA-256 of the evidence the description was written from, compared by equality to skip an unchanged area. One-way; carries no content."
18642
+ },
18643
+ {
18644
+ database: "haansi_intelligence_prod",
18645
+ table: "org_expertise_areas",
18646
+ column: "description_model",
18647
+ category: "operational",
18648
+ reason: "The provider model id that wrote the description. Vendor identifier, never content."
18649
+ },
18650
+ // Expert profile scheduling (object-graph/127).
18651
+ {
18652
+ database: "haansi_intelligence_prod",
18653
+ table: "expert_load_cycle_state",
18654
+ column: "hold_reason",
18655
+ category: "operational",
18656
+ reason: "A four-value CHECK-constrained enum naming why profile generation is held (initial_sync_pending, normalization_lag, mining_lag, settling)."
18657
+ },
18658
+ {
18659
+ database: "haansi_intelligence_prod",
18660
+ table: "expert_profile_facts",
18661
+ column: "synthesis_check_outcome",
18662
+ category: "operational",
18663
+ reason: "The synthesis pass's outcome code for the member's last evidence check (written, unchanged, no_evidence, rejected_*, failed). A fixed set of pipeline codes, never content."
18664
+ },
18665
+ {
18666
+ database: "haansi_intelligence_prod",
18667
+ table: "expert_profile_facts",
18668
+ column: "synthesis_check_hash",
18669
+ category: "derived",
18670
+ reason: "SHA-256 of the evidence pack seen at the last check, the same digest as expert_profiles.input_hash. One-way; carries no content."
18671
+ },
18672
+ {
18673
+ database: "haansi_intelligence_prod",
18674
+ table: "expert_synthesis_claims",
18675
+ column: "claimed_by",
18676
+ category: "operational",
18677
+ reason: "The pipeline worker id holding the synthesis lease. Infrastructure identity, never content."
18678
+ },
18679
+ {
18680
+ database: "haansi_intelligence_prod",
18681
+ table: "expert_synthesis_claims",
18682
+ column: "pending_input",
18683
+ category: "derived",
18684
+ reason: "A parked batch member's citation map: evidence keys (E1\u2026) mapped to source type, source id, integration platform and account key, plus an activity count, an input digest and a prompt version. No titles, names or summaries \u2014 the prompt text is never persisted (citationsOf strips it). Deleted when the attempt ends."
18685
+ },
18686
+ {
18687
+ database: "haansi_intelligence_prod",
18688
+ table: "expert_profile_curation",
18689
+ column: "hidden_platforms",
18690
+ category: "operational",
18691
+ reason: "Integration type names (github, slack, \u2026) whose evidence the member hid. A closed vocabulary, not content."
18692
+ },
18693
+ {
18694
+ database: "haansi_intelligence_prod",
18695
+ table: "expert_profile_curation_log",
18696
+ column: "changed_fields",
18697
+ category: "operational",
18698
+ reason: "Names of the curation fields an edit changed (headline, pinned areas, \u2026). Field names, never their values."
18699
+ },
18700
+ {
18701
+ database: "haansi_intelligence_prod",
18702
+ table: "expert_profile_curation_log",
18703
+ column: "before_hash",
18704
+ category: "derived",
18705
+ reason: "SHA-256 of the overlay before the edit, for audit comparison. A digest, not content."
18706
+ },
18707
+ {
18708
+ database: "haansi_intelligence_prod",
18709
+ table: "expert_profile_curation_log",
18710
+ column: "after_hash",
18711
+ category: "derived",
18712
+ reason: "SHA-256 of the overlay after the edit, for audit comparison. A digest, not content."
18713
+ },
18714
+ {
18715
+ database: "haansi_intelligence_prod",
18716
+ table: "objects",
18717
+ column: "canonical_id",
18718
+ category: "structural",
18719
+ reason: "The object graph's identity key \u2014 identity resolution and the merge path match on it, and object_relationships/event_object_links join through the row it identifies. Often an email address, so this is a KNOWN accepted disclosure, recorded rather than hidden: encrypting it non-deterministically breaks entity resolution, and deterministically leaks by frequency while breaking nothing an attacker relies on."
18720
+ },
18721
+ {
18722
+ database: "haansi_prod",
18723
+ table: "byod_migration_jobs",
18724
+ column: "operation",
18725
+ category: "operational",
18726
+ reason: "A closed enum of migration verbs, pinned by byod_migration_jobs_operation_check ('activate', 'disable', 'remove', 'enable_object_graph', 'disable_object_graph', 'retry'). It carries no tenant content, and the durable worker reads it to decide crash-safe finalization \u2014 encrypting a discriminator the recovery path compares against would make a half-finished migration unresumable."
18727
+ },
18728
+ // ── WS6 phase 2: session object attempts, revocations, erasure milestones ──
18729
+ {
18730
+ database: "haansi_prod",
18731
+ table: "session_object_attempts",
18732
+ column: "user_id",
18733
+ category: "structural",
18734
+ reason: "Identifier in the storage key layout, used to address and erase objects; the object bytes are encrypted under the per-attempt key."
18735
+ },
18736
+ {
18737
+ database: "haansi_prod",
18738
+ table: "session_object_attempts",
18739
+ column: "session_id",
18740
+ category: "structural",
18741
+ reason: "Identifier in the storage key layout, used to address and erase objects; the object bytes are encrypted under the per-attempt key."
18742
+ },
18743
+ {
18744
+ database: "haansi_prod",
18745
+ table: "session_object_attempts",
18746
+ column: "object_key",
18747
+ category: "structural",
18748
+ reason: "Identifier in the storage key layout, used to address and erase objects; the object bytes are encrypted under the per-attempt key."
18749
+ },
18750
+ {
18751
+ database: "haansi_prod",
18752
+ table: "session_object_attempts",
18753
+ column: "version_id",
18754
+ category: "structural",
18755
+ reason: "S3 version id returned by the store; addresses an object version for erasure."
18756
+ },
18757
+ {
18758
+ database: "haansi_prod",
18759
+ table: "session_object_attempts",
18760
+ column: "bucket_kind",
18761
+ category: "operational",
18762
+ reason: "Closed CHECK vocabulary the erasure and publication paths filter on."
18763
+ },
18764
+ {
18765
+ database: "haansi_prod",
18766
+ table: "session_object_attempts",
18767
+ column: "source_kind",
18768
+ category: "operational",
18769
+ reason: "Names the source whose generation the attempt was minted under; a vocabulary term, not content."
18770
+ },
18771
+ {
18772
+ database: "haansi_prod",
18773
+ table: "session_object_attempts",
18774
+ column: "status",
18775
+ category: "operational",
18776
+ reason: "Closed CHECK vocabulary the erasure and publication paths filter on."
18777
+ },
18778
+ {
18779
+ database: "haansi_prod",
18780
+ table: "session_object_attempts",
18781
+ column: "wrapped_key",
18782
+ category: "derived",
18783
+ reason: "Already ciphertext: the per-attempt data key wrapped by the org key."
18784
+ },
18785
+ {
18786
+ database: "haansi_prod",
18787
+ table: "session_object_attempts",
18788
+ column: "content_token",
18789
+ category: "derived",
18790
+ reason: "Staging token and redaction-manifest hash for idempotent re-publication; hashes and tokens, not content."
18791
+ },
18792
+ {
18793
+ database: "haansi_prod",
18794
+ table: "session_object_revocations",
18795
+ column: "scope_kind",
18796
+ category: "operational",
18797
+ reason: "Closed CHECK vocabulary the erasure and publication paths filter on."
18798
+ },
18799
+ {
18800
+ database: "haansi_prod",
18801
+ table: "session_object_revocations",
18802
+ column: "scope_id",
18803
+ category: "structural",
18804
+ reason: "User or session-source identifier a revocation applies to; erasure filters on it."
18805
+ },
18806
+ {
18807
+ database: "haansi_prod",
18808
+ table: "session_erasure_records",
18809
+ column: "user_id",
18810
+ category: "structural",
18811
+ reason: "Identifier of the erased user's scope; the milestone record must outlive the user's data to prove erasure."
18812
+ },
18813
+ {
18814
+ database: "haansi_prod",
18815
+ table: "session_erasure_records",
18816
+ column: "reason",
18817
+ category: "operational",
18818
+ reason: "Closed CHECK vocabulary the erasure and publication paths filter on."
18819
+ },
18820
+ // Memory → Skills (skills/001).
18821
+ {
18822
+ database: "haansi_intelligence_prod",
18823
+ table: "skills",
18824
+ column: "approved_audience",
18825
+ category: "operational",
18826
+ reason: "Closed CHECK vocabulary (org, owner) the skill access check filters on."
18827
+ },
18828
+ {
18829
+ database: "haansi_intelligence_prod",
18830
+ table: "skill_publications",
18831
+ column: "approved_audience",
18832
+ category: "operational",
18833
+ reason: "Closed CHECK vocabulary (org, owner) recorded per publish; read by the access check."
18834
+ },
18835
+ {
18836
+ database: "haansi_intelligence_prod",
18837
+ table: "skill_sources",
18838
+ column: "source_kind",
18839
+ category: "operational",
18840
+ reason: "Closed CHECK vocabulary naming which table a dependency resolves against."
18841
+ },
18842
+ {
18843
+ database: "haansi_intelligence_prod",
18844
+ table: "skill_sources",
18845
+ column: "content_fingerprint",
18846
+ category: "derived",
18847
+ reason: "sha256 of the captured source text, compared by equality to detect a changed source. Encrypting it would break the comparison; the text itself is not stored here."
18848
+ },
18849
+ {
18850
+ database: "haansi_intelligence_prod",
18851
+ table: "skill_candidates",
18852
+ column: "lane",
18853
+ category: "operational",
18854
+ reason: "Closed CHECK vocabulary (recurring, single) the Suggested tab filters on."
18855
+ },
18856
+ {
18857
+ database: "haansi_intelligence_prod",
18858
+ table: "skill_candidates",
18859
+ column: "audience_key",
18860
+ category: "structural",
18861
+ reason: "'shared' or a member's user id: which audience a group was formed within. An identifier, never text."
18862
+ },
18863
+ {
18864
+ database: "haansi_intelligence_prod",
18865
+ table: "skill_candidates",
18866
+ column: "member_fingerprint",
18867
+ category: "derived",
18868
+ reason: "sha256 of the sorted member ids, compared by equality to detect a changed group."
18869
+ },
18870
+ {
18871
+ database: "haansi_intelligence_prod",
18872
+ table: "skill_events",
18873
+ column: "channel",
18874
+ category: "operational",
18875
+ reason: "Closed CHECK vocabulary (mcp, qna, cli, web): where a skill was loaded from."
18876
+ },
18877
+ {
18878
+ database: "haansi_intelligence_prod",
18879
+ table: "skill_events",
18880
+ column: "event",
18881
+ category: "operational",
18882
+ reason: "Closed CHECK vocabulary (loaded, resource_loaded, synced, applied, helped, not_helped)."
18883
+ }
18884
+ ];
16586
18885
  }
16587
18886
  });
16588
18887
 
@@ -22877,6 +25176,38 @@ var init_mining_config_fields = __esm({
22877
25176
  }
22878
25177
  });
22879
25178
 
25179
+ // ../../packages/@company/types/src/exchange.ts
25180
+ var EXCHANGE_DISCIPLINES, EXCHANGE_WORK_FUNCTIONS, EXCHANGE_DISCIPLINE_IDS, EXCHANGE_WORK_FUNCTION_IDS;
25181
+ var init_exchange = __esm({
25182
+ "../../packages/@company/types/src/exchange.ts"() {
25183
+ "use strict";
25184
+ EXCHANGE_DISCIPLINES = [
25185
+ { id: "engineering", label: "Engineering" },
25186
+ { id: "data", label: "Data" },
25187
+ { id: "design", label: "Design" },
25188
+ { id: "product", label: "Product" },
25189
+ { id: "security", label: "Security" },
25190
+ { id: "operations", label: "Operations" },
25191
+ { id: "go_to_market", label: "Go-to-market" },
25192
+ { id: "people", label: "People" }
25193
+ ];
25194
+ EXCHANGE_WORK_FUNCTIONS = [
25195
+ { id: "software_development", label: "Software development" },
25196
+ { id: "infrastructure", label: "Infrastructure" },
25197
+ { id: "incident_response", label: "Incident response" },
25198
+ { id: "analytics", label: "Analytics" },
25199
+ { id: "research", label: "Research" },
25200
+ { id: "customer_success", label: "Customer success" },
25201
+ { id: "sales", label: "Sales" },
25202
+ { id: "marketing", label: "Marketing" },
25203
+ { id: "finance", label: "Finance" },
25204
+ { id: "legal", label: "Legal" }
25205
+ ];
25206
+ EXCHANGE_DISCIPLINE_IDS = EXCHANGE_DISCIPLINES.map((d) => d.id);
25207
+ EXCHANGE_WORK_FUNCTION_IDS = EXCHANGE_WORK_FUNCTIONS.map((w) => w.id);
25208
+ }
25209
+ });
25210
+
22880
25211
  // ../../packages/@company/types/src/index.ts
22881
25212
  var init_src5 = __esm({
22882
25213
  "../../packages/@company/types/src/index.ts"() {
@@ -22889,6 +25220,7 @@ var init_src5 = __esm({
22889
25220
  init_query_dialect();
22890
25221
  init_report_template();
22891
25222
  init_mining_config_fields();
25223
+ init_exchange();
22892
25224
  }
22893
25225
  });
22894
25226
 
@@ -24760,11 +27092,149 @@ var init_path_policy = __esm({
24760
27092
  });
24761
27093
 
24762
27094
  // ../../packages/@company/pii/src/deterministic.ts
27095
+ var RULES, DETERMINISTIC_ENTITY_TYPES;
24763
27096
  var init_deterministic = __esm({
24764
27097
  "../../packages/@company/pii/src/deterministic.ts"() {
24765
27098
  "use strict";
24766
27099
  init_spans();
24767
27100
  init_path_policy();
27101
+ RULES = [
27102
+ {
27103
+ id: "PRIVATE_KEY",
27104
+ entityType: "PRIVATE_KEY_BLOCK",
27105
+ pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g,
27106
+ replacement: "[REDACTED:PRIVATE_KEY]"
27107
+ },
27108
+ {
27109
+ id: "JWT",
27110
+ entityType: "JWT",
27111
+ pattern: /eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
27112
+ replacement: "[REDACTED:JWT]"
27113
+ },
27114
+ {
27115
+ id: "AWS_ACCESS_KEY",
27116
+ entityType: "AWS_ACCESS_KEY",
27117
+ pattern: /(?:^|[^A-Z0-9])(AKIA[0-9A-Z]{16})(?:[^A-Z0-9]|$)/gm,
27118
+ capture: 1,
27119
+ replacement: "[REDACTED:AWS_ACCESS_KEY]"
27120
+ },
27121
+ {
27122
+ id: "AWS_SECRET_KEY",
27123
+ entityType: "AWS_SECRET_KEY",
27124
+ pattern: /(?:aws[_-]?secret[_-]?access[_-]?key|AWS_SECRET_ACCESS_KEY)\s*[=:]\s*["']?([A-Za-z0-9/+]{40})["']?/gi,
27125
+ capture: 1,
27126
+ replacement: "[REDACTED:AWS_SECRET_KEY]"
27127
+ },
27128
+ {
27129
+ id: "API_KEY_ASSIGNMENT",
27130
+ entityType: "API_KEY",
27131
+ pattern: /(?:api[_-]?key|apikey|api[_-]?token|access[_-]?token|auth[_-]?token|bearer[_-]?token)\s*[=:]\s*["']?([A-Za-z0-9\-_.]{20,}?)["']?(?:\s|,|;|\n|$)/gi,
27132
+ capture: 1,
27133
+ replacement: "[REDACTED:API_KEY]"
27134
+ },
27135
+ {
27136
+ id: "ANTHROPIC_KEY",
27137
+ entityType: "ANTHROPIC_API_KEY",
27138
+ pattern: /sk-ant-[A-Za-z0-9\-_]{32,}/g,
27139
+ replacement: "[REDACTED:ANTHROPIC_KEY]"
27140
+ },
27141
+ {
27142
+ id: "OPENAI_KEY",
27143
+ entityType: "OPENAI_API_KEY",
27144
+ pattern: /sk-(?:proj-)?[A-Za-z0-9]{20,}/g,
27145
+ replacement: "[REDACTED:OPENAI_KEY]"
27146
+ },
27147
+ {
27148
+ id: "GITHUB_TOKEN",
27149
+ entityType: "GITHUB_TOKEN",
27150
+ pattern: /(?:ghp_|ghs_|github_pat_)[A-Za-z0-9_]{36,}/g,
27151
+ replacement: "[REDACTED:GITHUB_TOKEN]"
27152
+ },
27153
+ {
27154
+ id: "SLACK_TOKEN",
27155
+ entityType: "SLACK_TOKEN",
27156
+ pattern: /xox[bpars]-[A-Za-z0-9\-]{16,}/g,
27157
+ replacement: "[REDACTED:SLACK_TOKEN]"
27158
+ },
27159
+ {
27160
+ id: "GCP_PRIVATE_KEY",
27161
+ entityType: "GCP_PRIVATE_KEY",
27162
+ pattern: /"private_key"\s*:\s*"(-----BEGIN[^"]+)"/g,
27163
+ capture: 1,
27164
+ replacement: "[REDACTED:GCP_PRIVATE_KEY]"
27165
+ },
27166
+ {
27167
+ id: "DB_CONNECTION_STRING",
27168
+ entityType: "DB_CONNECTION_STRING",
27169
+ // The username may be EMPTY — `redis://:password@host` is the documented
27170
+ // Redis form and leaked past `[^@\s]+:` because that requires one. amqp,
27171
+ // amqps and rediss are the same shape and were simply missing from the
27172
+ // scheme list.
27173
+ pattern: /(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|rediss?|amqps?|clickhouse|mssql|mariadb):\/\/[^@\s]*:[^@\s]+@[^\s"']+/gi,
27174
+ replacement: "[REDACTED:DB_CONNECTION_STRING]"
27175
+ },
27176
+ {
27177
+ id: "SECRET_ASSIGNMENT",
27178
+ entityType: "SECRET",
27179
+ // QUOTED form. An unquoted assignment is a separate rule below: folding
27180
+ // them into one alternation makes the quote handling ambiguous and risks
27181
+ // the closing quote landing inside the captured value.
27182
+ pattern: /["']?(?:password|passwd|secret|client[_-]?secret|private[_-]?key|encryption[_-]?key|api[_-]?secret|auth[_-]?token)["']?\s*[=:]\s*["']([^"'\s]{6,})["']/gi,
27183
+ capture: 1,
27184
+ replacement: "[REDACTED:SECRET]"
27185
+ },
27186
+ {
27187
+ // UNQUOTED assignment: `DATABASE_PASSWORD=sup3rs3cret`, `PGPASSWORD=x`,
27188
+ // `--password xyz`. This is how a credential appears in a shell command or
27189
+ // an env file, which is most of how one appears in a coding transcript.
27190
+ //
27191
+ // The key must END in a secret-ish word so `PASSWORD_RESET_URL=/x` and
27192
+ // `SECRET_NAME=haansi/dev/FOO` do not match: those name a secret rather
27193
+ // than being one, and redacting them destroys useful context. A minimum
27194
+ // length of 6 keeps `password=` and `secret=true` out.
27195
+ id: "SECRET_ASSIGNMENT_UNQUOTED",
27196
+ entityType: "SECRET",
27197
+ pattern: /(?:^|[\s;&|"'`([{,])(?:[A-Za-z0-9_.-]*(?:password|passwd|secret|token|apikey|api_key))\s*[=:]\s*([^\s"'`;&|)\]}]{6,})/gim,
27198
+ capture: 1,
27199
+ replacement: "[REDACTED:SECRET]"
27200
+ },
27201
+ {
27202
+ id: "BASIC_AUTH_URL",
27203
+ entityType: "BASIC_AUTH",
27204
+ pattern: /https?:\/\/([^:@\s/]+:[^@\s/]+)@/g,
27205
+ capture: 1,
27206
+ replacement: "[REDACTED:BASIC_AUTH]"
27207
+ },
27208
+ {
27209
+ id: "CREDIT_CARD",
27210
+ entityType: "CREDIT_CARD",
27211
+ pattern: /\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|6(?:011|5[0-9]{2})[0-9]{12}|(?:2131|1800|35\d{3})\d{11})\b/g,
27212
+ replacement: "[REDACTED:CREDIT_CARD]"
27213
+ },
27214
+ {
27215
+ id: "HAANSI_TOKEN",
27216
+ entityType: "HAANSI_CLI_TOKEN",
27217
+ pattern: /hsk_[a-f0-9]{32,}/g,
27218
+ replacement: "[REDACTED:HAANSI_TOKEN]"
27219
+ },
27220
+ {
27221
+ id: "BEARER_TOKEN",
27222
+ entityType: "BEARER_TOKEN",
27223
+ pattern: /Authorization:\s*Bearer\s+([A-Za-z0-9\-_.+/=]{20,})/gi,
27224
+ capture: 1,
27225
+ replacement: "[REDACTED:BEARER_TOKEN]"
27226
+ },
27227
+ {
27228
+ id: "HEX_SECRET",
27229
+ entityType: "HEX_SECRET",
27230
+ pattern: /(?:token|secret|key|hash|salt)\s*[=:]\s*["']?([a-f0-9]{32,})["']?/gi,
27231
+ capture: 1,
27232
+ replacement: "[REDACTED:HEX_SECRET]"
27233
+ }
27234
+ ];
27235
+ DETERMINISTIC_ENTITY_TYPES = new Set(
27236
+ RULES.map((rule) => rule.entityType)
27237
+ );
24768
27238
  }
24769
27239
  });
24770
27240
 
@@ -24812,264 +27282,6 @@ var init_src7 = __esm({
24812
27282
  }
24813
27283
  });
24814
27284
 
24815
- // ../service-capture/common/agents/types.ts
24816
- var AGENT_IDS;
24817
- var init_types2 = __esm({
24818
- "../service-capture/common/agents/types.ts"() {
24819
- "use strict";
24820
- AGENT_IDS = ["claude-code", "codex"];
24821
- }
24822
- });
24823
-
24824
- // ../service-capture/common/agents/claude-code.ts
24825
- function projectsDir() {
24826
- return (0, import_path.join)((0, import_os.homedir)(), ".claude", "projects");
24827
- }
24828
- function extractMeta(filePath) {
24829
- const result = {
24830
- cwd: null,
24831
- timestamp: null,
24832
- gitBranch: null
24833
- };
24834
- try {
24835
- const lines = (0, import_fs.readFileSync)(filePath, "utf-8").split("\n").filter((l) => l.trim()).slice(0, 50);
24836
- for (const line of lines) {
24837
- try {
24838
- const entry2 = JSON.parse(line);
24839
- if (!result.cwd && typeof entry2.cwd === "string")
24840
- result.cwd = entry2.cwd;
24841
- if (!result.timestamp && typeof entry2.timestamp === "string")
24842
- result.timestamp = entry2.timestamp;
24843
- if (!result.gitBranch && typeof entry2.gitBranch === "string")
24844
- result.gitBranch = entry2.gitBranch;
24845
- if (result.cwd && result.timestamp && result.gitBranch) break;
24846
- } catch {
24847
- }
24848
- }
24849
- } catch {
24850
- }
24851
- return result;
24852
- }
24853
- var import_fs, import_path, import_os, claudeCodeAgent;
24854
- var init_claude_code = __esm({
24855
- "../service-capture/common/agents/claude-code.ts"() {
24856
- "use strict";
24857
- import_fs = require("fs");
24858
- import_path = require("path");
24859
- import_os = require("os");
24860
- claudeCodeAgent = {
24861
- id: "claude-code",
24862
- displayName: "Claude Code",
24863
- sessionsRoot() {
24864
- return projectsDir();
24865
- },
24866
- isInstalled() {
24867
- return (0, import_fs.existsSync)(projectsDir());
24868
- },
24869
- discoverSessions() {
24870
- const root = projectsDir();
24871
- if (!(0, import_fs.existsSync)(root)) return [];
24872
- const sessions = [];
24873
- const projectDirs = (0, import_fs.readdirSync)(root).map((name) => (0, import_path.join)(root, name)).filter((p) => {
24874
- try {
24875
- return (0, import_fs.statSync)(p).isDirectory();
24876
- } catch {
24877
- return false;
24878
- }
24879
- });
24880
- for (const dir of projectDirs) {
24881
- let files;
24882
- try {
24883
- files = (0, import_fs.readdirSync)(dir).filter((f) => f.endsWith(".jsonl"));
24884
- } catch {
24885
- continue;
24886
- }
24887
- for (const file2 of files) {
24888
- const filePath = (0, import_path.join)(dir, file2);
24889
- const sessionId = (0, import_path.basename)(file2, ".jsonl");
24890
- try {
24891
- const stat = (0, import_fs.statSync)(filePath);
24892
- const content = (0, import_fs.readFileSync)(filePath, "utf-8");
24893
- const lineCount = content.split("\n").filter((l) => l.trim()).length;
24894
- const meta3 = extractMeta(filePath);
24895
- sessions.push({
24896
- sessionId,
24897
- filePath,
24898
- lineCount,
24899
- sizeBytes: stat.size,
24900
- ...meta3
24901
- });
24902
- } catch {
24903
- }
24904
- }
24905
- }
24906
- return sessions;
24907
- }
24908
- };
24909
- }
24910
- });
24911
-
24912
- // ../service-capture/common/agents/codex.ts
24913
- function codexHome() {
24914
- return process.env.CODEX_HOME || (0, import_path2.join)((0, import_os2.homedir)(), ".codex");
24915
- }
24916
- function sessionsDir() {
24917
- return (0, import_path2.join)(codexHome(), "sessions");
24918
- }
24919
- function walkRollouts(dir, out) {
24920
- let entries;
24921
- try {
24922
- entries = (0, import_fs2.readdirSync)(dir);
24923
- } catch {
24924
- return;
24925
- }
24926
- for (const name of entries) {
24927
- const full = (0, import_path2.join)(dir, name);
24928
- let isDir = false;
24929
- try {
24930
- isDir = (0, import_fs2.statSync)(full).isDirectory();
24931
- } catch {
24932
- continue;
24933
- }
24934
- if (isDir) {
24935
- walkRollouts(full, out);
24936
- } else if (name.startsWith("rollout-") && name.endsWith(".jsonl")) {
24937
- out.push(full);
24938
- }
24939
- }
24940
- }
24941
- function readRolloutMeta(filePath, content) {
24942
- const fileName = (0, import_path2.basename)(filePath, ".jsonl");
24943
- const fromName = UUID_RE.exec(fileName)?.[1];
24944
- let cwd = null;
24945
- let timestamp = null;
24946
- let gitBranch = null;
24947
- let gitRemoteUrl = null;
24948
- let gitCommit = null;
24949
- let metaId = null;
24950
- const lines = content.split("\n").filter((l) => l.trim()).slice(0, 50);
24951
- for (const line of lines) {
24952
- try {
24953
- const entry2 = JSON.parse(line);
24954
- if (!timestamp && typeof entry2.timestamp === "string") {
24955
- timestamp = entry2.timestamp;
24956
- }
24957
- const p = entry2.payload ?? {};
24958
- if (entry2.type === "session_meta") {
24959
- if (typeof p.cwd === "string") cwd = p.cwd;
24960
- if (typeof p.id === "string") metaId = p.id;
24961
- const g = p.git ?? {};
24962
- const branch = g.branch ?? p.git_branch;
24963
- if (typeof branch === "string") gitBranch = branch;
24964
- const remote = g.repository_url ?? g.remote_url;
24965
- if (typeof remote === "string") gitRemoteUrl = remote;
24966
- const commit = g.commit_hash ?? g.commit;
24967
- if (typeof commit === "string") gitCommit = commit;
24968
- }
24969
- if (cwd && gitBranch && gitCommit && timestamp && metaId) break;
24970
- } catch {
24971
- }
24972
- }
24973
- return {
24974
- sessionId: fromName ?? metaId ?? fileName,
24975
- cwd,
24976
- timestamp,
24977
- gitBranch,
24978
- gitRemoteUrl,
24979
- gitCommit
24980
- };
24981
- }
24982
- var import_fs2, import_path2, import_os2, UUID_RE, codexAgent;
24983
- var init_codex = __esm({
24984
- "../service-capture/common/agents/codex.ts"() {
24985
- "use strict";
24986
- import_fs2 = require("fs");
24987
- import_path2 = require("path");
24988
- import_os2 = require("os");
24989
- UUID_RE = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i;
24990
- codexAgent = {
24991
- id: "codex",
24992
- displayName: "OpenAI Codex",
24993
- sessionsRoot() {
24994
- return sessionsDir();
24995
- },
24996
- isInstalled() {
24997
- return (0, import_fs2.existsSync)(codexHome());
24998
- },
24999
- discoverSessions() {
25000
- const root = sessionsDir();
25001
- if (!(0, import_fs2.existsSync)(root)) return [];
25002
- const files = [];
25003
- walkRollouts(root, files);
25004
- const sessions = [];
25005
- for (const filePath of files) {
25006
- try {
25007
- const stat = (0, import_fs2.statSync)(filePath);
25008
- const content = (0, import_fs2.readFileSync)(filePath, "utf-8");
25009
- const lineCount = content.split("\n").filter((l) => l.trim()).length;
25010
- const meta3 = readRolloutMeta(filePath, content);
25011
- sessions.push({
25012
- sessionId: meta3.sessionId,
25013
- filePath,
25014
- lineCount,
25015
- sizeBytes: stat.size,
25016
- cwd: meta3.cwd,
25017
- timestamp: meta3.timestamp,
25018
- gitBranch: meta3.gitBranch,
25019
- gitRemoteUrl: meta3.gitRemoteUrl,
25020
- gitCommit: meta3.gitCommit
25021
- });
25022
- } catch {
25023
- }
25024
- }
25025
- return sessions;
25026
- }
25027
- };
25028
- }
25029
- });
25030
-
25031
- // ../service-capture/common/agents/registry.ts
25032
- function getAgents() {
25033
- return AGENTS;
25034
- }
25035
- function getAgent(id) {
25036
- const agent = AGENTS.find((a) => a.id === id);
25037
- if (!agent) throw new Error(`Unknown agent: ${id}`);
25038
- return agent;
25039
- }
25040
- function detectInstalled() {
25041
- return AGENTS.filter((a) => a.isInstalled());
25042
- }
25043
- var AGENTS;
25044
- var init_registry = __esm({
25045
- "../service-capture/common/agents/registry.ts"() {
25046
- "use strict";
25047
- init_claude_code();
25048
- init_codex();
25049
- AGENTS = [claudeCodeAgent, codexAgent];
25050
- }
25051
- });
25052
-
25053
- // ../service-capture/common/agents/index.ts
25054
- var agents_exports = {};
25055
- __export(agents_exports, {
25056
- AGENT_IDS: () => AGENT_IDS,
25057
- claudeCodeAgent: () => claudeCodeAgent,
25058
- codexAgent: () => codexAgent,
25059
- detectInstalled: () => detectInstalled,
25060
- getAgent: () => getAgent,
25061
- getAgents: () => getAgents
25062
- });
25063
- var init_agents = __esm({
25064
- "../service-capture/common/agents/index.ts"() {
25065
- "use strict";
25066
- init_types2();
25067
- init_claude_code();
25068
- init_codex();
25069
- init_registry();
25070
- }
25071
- });
25072
-
25073
27285
  // ../service-capture/agent-sessions/collector.ts
25074
27286
  var collector_exports = {};
25075
27287
  __export(collector_exports, {
@@ -25596,113 +27808,6 @@ var init_collector = __esm({
25596
27808
  }
25597
27809
  });
25598
27810
 
25599
- // src/lib/agent-selection.ts
25600
- var agent_selection_exports = {};
25601
- __export(agent_selection_exports, {
25602
- parseAgentFlag: () => parseAgentFlag,
25603
- promptForAgents: () => promptForAgents,
25604
- readSavedAgents: () => readSavedAgents,
25605
- resolveAgentSelection: () => resolveAgentSelection,
25606
- saveAgents: () => saveAgents
25607
- });
25608
- function parseAgentFlag(value) {
25609
- if (!value) return null;
25610
- const normalized = value.trim().toLowerCase();
25611
- if (normalized === "both" || normalized === "all") return [...VALID];
25612
- const out = [];
25613
- for (const part of normalized.split(",").map((s) => s.trim())) {
25614
- if (part === "claude" || part === "claude-code") {
25615
- if (!out.includes("claude-code")) out.push("claude-code");
25616
- } else if (part === "codex") {
25617
- if (!out.includes("codex")) out.push("codex");
25618
- }
25619
- }
25620
- return out.length > 0 ? out : null;
25621
- }
25622
- function readSavedAgents() {
25623
- if (!(0, import_node_fs4.existsSync)(CONFIG_FILE)) return null;
25624
- try {
25625
- const cfg = JSON.parse((0, import_node_fs4.readFileSync)(CONFIG_FILE, "utf-8"));
25626
- if (Array.isArray(cfg.agents)) {
25627
- const agents = cfg.agents.filter(
25628
- (a) => VALID.includes(a)
25629
- );
25630
- return agents.length > 0 ? agents : null;
25631
- }
25632
- } catch {
25633
- }
25634
- return null;
25635
- }
25636
- function saveAgents(agents) {
25637
- let cfg = {};
25638
- if ((0, import_node_fs4.existsSync)(CONFIG_FILE)) {
25639
- try {
25640
- cfg = JSON.parse((0, import_node_fs4.readFileSync)(CONFIG_FILE, "utf-8"));
25641
- } catch {
25642
- cfg = {};
25643
- }
25644
- }
25645
- cfg.agents = agents;
25646
- (0, import_node_fs4.mkdirSync)(HAANSI_DIR, { recursive: true });
25647
- (0, import_node_fs4.writeFileSync)(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", {
25648
- encoding: "utf-8",
25649
- mode: 384
25650
- });
25651
- }
25652
- function resolveAgentSelection(flagValue) {
25653
- const fromFlag = parseAgentFlag(flagValue);
25654
- if (fromFlag) return { agents: fromFlag, source: "flag" };
25655
- const saved = readSavedAgents();
25656
- if (saved) return { agents: saved, source: "config" };
25657
- const detected = detectInstalled().map((a) => a.id);
25658
- if (detected.length > 0) return { agents: detected, source: "detected" };
25659
- return { agents: ["claude-code"], source: "default" };
25660
- }
25661
- async function promptForAgents() {
25662
- const installed = new Set(detectInstalled().map((a) => a.id));
25663
- const agents = getAgents();
25664
- console.log("\nWhich coding agents should Haansi connect?\n");
25665
- agents.forEach((a, i) => {
25666
- const mark = installed.has(a.id) ? "installed" : "not detected";
25667
- console.log(` ${i + 1}) ${a.displayName} (${mark})`);
25668
- });
25669
- const rl = readline2.createInterface({
25670
- input: process.stdin,
25671
- output: process.stdout
25672
- });
25673
- const answer = await new Promise((resolve5) => {
25674
- rl.question(
25675
- "\nEnter numbers (comma-separated), or press Enter for all installed: ",
25676
- (a) => {
25677
- rl.close();
25678
- resolve5(a.trim());
25679
- }
25680
- );
25681
- });
25682
- if (!answer) {
25683
- const detected = agents.filter((a) => installed.has(a.id)).map((a) => a.id);
25684
- return detected.length > 0 ? detected : [...VALID];
25685
- }
25686
- const picked = [];
25687
- for (const token of answer.split(",").map((s) => s.trim())) {
25688
- const idx = parseInt(token, 10) - 1;
25689
- const agent = agents[idx];
25690
- if (agent && !picked.includes(agent.id)) picked.push(agent.id);
25691
- }
25692
- return picked.length > 0 ? picked : [...VALID];
25693
- }
25694
- var import_node_fs4, readline2, VALID;
25695
- var init_agent_selection = __esm({
25696
- "src/lib/agent-selection.ts"() {
25697
- "use strict";
25698
- import_node_fs4 = require("node:fs");
25699
- readline2 = __toESM(require("node:readline"));
25700
- init_agents();
25701
- init_paths();
25702
- VALID = ["claude-code", "codex"];
25703
- }
25704
- });
25705
-
25706
27811
  // ../../node_modules/node-cron/src/task.js
25707
27812
  var require_task = __commonJS({
25708
27813
  "../../node_modules/node-cron/src/task.js"(exports2, module2) {
@@ -43651,11 +45756,11 @@ var require_util2 = __commonJS({
43651
45756
  return false;
43652
45757
  }
43653
45758
  exports2.schemaHasRules = schemaHasRules;
43654
- function schemaHasRulesButRef(schema, RULES) {
45759
+ function schemaHasRulesButRef(schema, RULES2) {
43655
45760
  if (typeof schema == "boolean")
43656
45761
  return !schema;
43657
45762
  for (const key in schema)
43658
- if (key !== "$ref" && RULES.all[key])
45763
+ if (key !== "$ref" && RULES2.all[key])
43659
45764
  return true;
43660
45765
  return false;
43661
45766
  }
@@ -45049,17 +47154,17 @@ var require_validate = __commonJS({
45049
47154
  }
45050
47155
  function schemaKeywords(it, types2, typeErrors, errsCount) {
45051
47156
  const { gen, schema, data, allErrors, opts, self } = it;
45052
- const { RULES } = self;
45053
- if (schema.$ref && (opts.ignoreKeywordsWithRef || !(0, util_1.schemaHasRulesButRef)(schema, RULES))) {
45054
- gen.block(() => keywordCode(it, "$ref", RULES.all.$ref.definition));
47157
+ const { RULES: RULES2 } = self;
47158
+ if (schema.$ref && (opts.ignoreKeywordsWithRef || !(0, util_1.schemaHasRulesButRef)(schema, RULES2))) {
47159
+ gen.block(() => keywordCode(it, "$ref", RULES2.all.$ref.definition));
45055
47160
  return;
45056
47161
  }
45057
47162
  if (!opts.jtd)
45058
47163
  checkStrictTypes(it, types2);
45059
47164
  gen.block(() => {
45060
- for (const group of RULES.rules)
47165
+ for (const group of RULES2.rules)
45061
47166
  groupKeywords(group);
45062
- groupKeywords(RULES.post);
47167
+ groupKeywords(RULES2.post);
45063
47168
  });
45064
47169
  function groupKeywords(group) {
45065
47170
  if (!(0, applicability_1.shouldUseGroup)(schema, group))
@@ -46761,10 +48866,10 @@ var require_core = __commonJS({
46761
48866
  }
46762
48867
  // Remove keyword
46763
48868
  removeKeyword(keyword) {
46764
- const { RULES } = this;
46765
- delete RULES.keywords[keyword];
46766
- delete RULES.all[keyword];
46767
- for (const group of RULES.rules) {
48869
+ const { RULES: RULES2 } = this;
48870
+ delete RULES2.keywords[keyword];
48871
+ delete RULES2.all[keyword];
48872
+ for (const group of RULES2.rules) {
46768
48873
  const i = group.rules.findIndex((rule) => rule.keyword === keyword);
46769
48874
  if (i >= 0)
46770
48875
  group.rules.splice(i, 1);
@@ -46932,9 +49037,9 @@ var require_core = __commonJS({
46932
49037
  }
46933
49038
  var KEYWORD_NAME = /^[a-z_$][a-z0-9_$:-]*$/i;
46934
49039
  function checkKeyword(keyword, def) {
46935
- const { RULES } = this;
49040
+ const { RULES: RULES2 } = this;
46936
49041
  (0, util_1.eachItem)(keyword, (kwd) => {
46937
- if (RULES.keywords[kwd])
49042
+ if (RULES2.keywords[kwd])
46938
49043
  throw new Error(`Keyword ${kwd} is already defined`);
46939
49044
  if (!KEYWORD_NAME.test(kwd))
46940
49045
  throw new Error(`Keyword ${kwd} has invalid name`);
@@ -46950,13 +49055,13 @@ var require_core = __commonJS({
46950
49055
  const post = definition === null || definition === void 0 ? void 0 : definition.post;
46951
49056
  if (dataType && post)
46952
49057
  throw new Error('keyword with "post" flag cannot have "type"');
46953
- const { RULES } = this;
46954
- let ruleGroup = post ? RULES.post : RULES.rules.find(({ type: t }) => t === dataType);
49058
+ const { RULES: RULES2 } = this;
49059
+ let ruleGroup = post ? RULES2.post : RULES2.rules.find(({ type: t }) => t === dataType);
46955
49060
  if (!ruleGroup) {
46956
49061
  ruleGroup = { type: dataType, rules: [] };
46957
- RULES.rules.push(ruleGroup);
49062
+ RULES2.rules.push(ruleGroup);
46958
49063
  }
46959
- RULES.keywords[keyword] = true;
49064
+ RULES2.keywords[keyword] = true;
46960
49065
  if (!definition)
46961
49066
  return;
46962
49067
  const rule = {
@@ -46971,7 +49076,7 @@ var require_core = __commonJS({
46971
49076
  addBeforeRule.call(this, ruleGroup, rule, definition.before);
46972
49077
  else
46973
49078
  ruleGroup.rules.push(rule);
46974
- RULES.all[keyword] = rule;
49079
+ RULES2.all[keyword] = rule;
46975
49080
  (_a3 = definition.implements) === null || _a3 === void 0 ? void 0 : _a3.forEach((kwd) => this.addKeyword(kwd));
46976
49081
  }
46977
49082
  function addBeforeRule(ruleGroup, rule, before) {
@@ -50306,11 +52411,11 @@ var require_util3 = __commonJS({
50306
52411
  return false;
50307
52412
  }
50308
52413
  exports2.schemaHasRules = schemaHasRules;
50309
- function schemaHasRulesButRef(schema, RULES) {
52414
+ function schemaHasRulesButRef(schema, RULES2) {
50310
52415
  if (typeof schema == "boolean")
50311
52416
  return !schema;
50312
52417
  for (const key in schema)
50313
- if (key !== "$ref" && RULES.all[key])
52418
+ if (key !== "$ref" && RULES2.all[key])
50314
52419
  return true;
50315
52420
  return false;
50316
52421
  }
@@ -51669,17 +53774,17 @@ var require_validate2 = __commonJS({
51669
53774
  }
51670
53775
  function schemaKeywords(it, types2, typeErrors, errsCount) {
51671
53776
  const { gen, schema, data, allErrors, opts, self } = it;
51672
- const { RULES } = self;
51673
- if (schema.$ref && (opts.ignoreKeywordsWithRef || !(0, util_1.schemaHasRulesButRef)(schema, RULES))) {
51674
- gen.block(() => keywordCode(it, "$ref", RULES.all.$ref.definition));
53777
+ const { RULES: RULES2 } = self;
53778
+ if (schema.$ref && (opts.ignoreKeywordsWithRef || !(0, util_1.schemaHasRulesButRef)(schema, RULES2))) {
53779
+ gen.block(() => keywordCode(it, "$ref", RULES2.all.$ref.definition));
51675
53780
  return;
51676
53781
  }
51677
53782
  if (!opts.jtd)
51678
53783
  checkStrictTypes(it, types2);
51679
53784
  gen.block(() => {
51680
- for (const group of RULES.rules)
53785
+ for (const group of RULES2.rules)
51681
53786
  groupKeywords(group);
51682
- groupKeywords(RULES.post);
53787
+ groupKeywords(RULES2.post);
51683
53788
  });
51684
53789
  function groupKeywords(group) {
51685
53790
  if (!(0, applicability_1.shouldUseGroup)(schema, group))
@@ -52659,10 +54764,10 @@ var require_core3 = __commonJS({
52659
54764
  }
52660
54765
  // Remove keyword
52661
54766
  removeKeyword(keyword) {
52662
- const { RULES } = this;
52663
- delete RULES.keywords[keyword];
52664
- delete RULES.all[keyword];
52665
- for (const group of RULES.rules) {
54767
+ const { RULES: RULES2 } = this;
54768
+ delete RULES2.keywords[keyword];
54769
+ delete RULES2.all[keyword];
54770
+ for (const group of RULES2.rules) {
52666
54771
  const i = group.rules.findIndex((rule) => rule.keyword === keyword);
52667
54772
  if (i >= 0)
52668
54773
  group.rules.splice(i, 1);
@@ -52830,9 +54935,9 @@ var require_core3 = __commonJS({
52830
54935
  }
52831
54936
  var KEYWORD_NAME = /^[a-z_$][a-z0-9_$:-]*$/i;
52832
54937
  function checkKeyword(keyword, def) {
52833
- const { RULES } = this;
54938
+ const { RULES: RULES2 } = this;
52834
54939
  (0, util_1.eachItem)(keyword, (kwd) => {
52835
- if (RULES.keywords[kwd])
54940
+ if (RULES2.keywords[kwd])
52836
54941
  throw new Error(`Keyword ${kwd} is already defined`);
52837
54942
  if (!KEYWORD_NAME.test(kwd))
52838
54943
  throw new Error(`Keyword ${kwd} has invalid name`);
@@ -52848,13 +54953,13 @@ var require_core3 = __commonJS({
52848
54953
  const post = definition === null || definition === void 0 ? void 0 : definition.post;
52849
54954
  if (dataType && post)
52850
54955
  throw new Error('keyword with "post" flag cannot have "type"');
52851
- const { RULES } = this;
52852
- let ruleGroup = post ? RULES.post : RULES.rules.find(({ type: t }) => t === dataType);
54956
+ const { RULES: RULES2 } = this;
54957
+ let ruleGroup = post ? RULES2.post : RULES2.rules.find(({ type: t }) => t === dataType);
52853
54958
  if (!ruleGroup) {
52854
54959
  ruleGroup = { type: dataType, rules: [] };
52855
- RULES.rules.push(ruleGroup);
54960
+ RULES2.rules.push(ruleGroup);
52856
54961
  }
52857
- RULES.keywords[keyword] = true;
54962
+ RULES2.keywords[keyword] = true;
52858
54963
  if (!definition)
52859
54964
  return;
52860
54965
  const rule = {
@@ -52869,7 +54974,7 @@ var require_core3 = __commonJS({
52869
54974
  addBeforeRule.call(this, ruleGroup, rule, definition.before);
52870
54975
  else
52871
54976
  ruleGroup.rules.push(rule);
52872
- RULES.all[keyword] = rule;
54977
+ RULES2.all[keyword] = rule;
52873
54978
  (_a3 = definition.implements) === null || _a3 === void 0 ? void 0 : _a3.forEach((kwd) => this.addKeyword(kwd));
52874
54979
  }
52875
54980
  function addBeforeRule(ruleGroup, rule, before) {
@@ -81109,222 +83214,6 @@ var init_mcp_server2 = __esm({
81109
83214
  }
81110
83215
  });
81111
83216
 
81112
- // src/lib/codex-config.ts
81113
- function tomlString(value) {
81114
- const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
81115
- return `"${escaped}"`;
81116
- }
81117
- function tomlStringArray(values) {
81118
- return `[${values.map(tomlString).join(", ")}]`;
81119
- }
81120
- function tomlInlineTable(entries) {
81121
- const parts = Object.entries(entries).map(
81122
- ([k, v]) => `${k} = ${tomlString(v)}`
81123
- );
81124
- return `{ ${parts.join(", ")} }`;
81125
- }
81126
- function upsertTomlTable(content, tablePath, entries) {
81127
- const bodyLines = Object.entries(entries).map(([k, v]) => `${k} = ${v}`);
81128
- const block = [`[${tablePath}]`, ...bodyLines].join("\n");
81129
- const lines = content.split("\n");
81130
- const headerRe = new RegExp(
81131
- `^\\s*\\[\\s*${tablePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\]\\s*$`
81132
- );
81133
- const startIdx = lines.findIndex((l) => headerRe.test(l));
81134
- if (startIdx === -1) {
81135
- const trimmed = content.replace(/\s*$/, "");
81136
- return trimmed.length > 0 ? `${trimmed}
81137
-
81138
- ${block}
81139
- ` : `${block}
81140
- `;
81141
- }
81142
- let endIdx = lines.length;
81143
- for (let i = startIdx + 1; i < lines.length; i++) {
81144
- if (/^\s*\[/.test(lines[i])) {
81145
- endIdx = i;
81146
- break;
81147
- }
81148
- }
81149
- const before = lines.slice(0, startIdx);
81150
- const after = lines.slice(endIdx);
81151
- const rebuilt = [...before, ...block.split("\n"), ...after].join("\n");
81152
- return rebuilt.replace(/\n{3,}/g, "\n\n");
81153
- }
81154
- var init_codex_config = __esm({
81155
- "src/lib/codex-config.ts"() {
81156
- "use strict";
81157
- }
81158
- });
81159
-
81160
- // src/commands/setup-mcp.ts
81161
- var setup_mcp_exports = {};
81162
- __export(setup_mcp_exports, {
81163
- HTTP_SERVERS: () => HTTP_SERVERS,
81164
- parseTarget: () => parseTarget,
81165
- setupMcp: () => setupMcp
81166
- });
81167
- function parseTarget(arg) {
81168
- if (!arg || arg === "all") return "all";
81169
- if (arg === "solutions" || arg === "exchange" || arg === "digests" || arg === "onboarding" || arg === "skills")
81170
- return arg;
81171
- throw new Error(
81172
- `Unknown target: ${arg}. Use one of: solutions, exchange, digests, onboarding, skills, all (default).`
81173
- );
81174
- }
81175
- function httpServerUrl(path2) {
81176
- return `${apiUrl().replace(/\/+$/, "")}${path2}`;
81177
- }
81178
- function setupClaudeMcp(target) {
81179
- let config6 = {};
81180
- if ((0, import_node_fs5.existsSync)(CLAUDE_JSON)) {
81181
- try {
81182
- config6 = JSON.parse((0, import_node_fs5.readFileSync)(CLAUDE_JSON, "utf-8"));
81183
- } catch {
81184
- console.error(
81185
- `Warning: Could not parse ${CLAUDE_JSON} \u2014 will overwrite with new config.`
81186
- );
81187
- }
81188
- }
81189
- if (!config6.mcpServers) config6.mcpServers = {};
81190
- const installed = [];
81191
- const warnings = [];
81192
- if (target === "all" || target === "solutions") {
81193
- const haansiBin = resolveHaansiBin();
81194
- config6.mcpServers["haansi-solutions"] = {
81195
- command: haansiBin,
81196
- args: ["mcp-server"],
81197
- env: { HAANSI_AGENT_SOURCE: "claude-code" }
81198
- };
81199
- installed.push(`haansi-solutions (stdio \u2192 ${haansiBin} mcp-server)`);
81200
- }
81201
- for (const http of HTTP_SERVERS) {
81202
- if (target !== "all" && target !== http.target) continue;
81203
- const token = resolveToken();
81204
- if (!token) {
81205
- warnings.push(
81206
- `${http.name} not registered: no token found. Run \`haansi auth login\` first, or set HAANSI_TOKEN.`
81207
- );
81208
- continue;
81209
- }
81210
- const url2 = httpServerUrl(http.path);
81211
- config6.mcpServers[http.name] = {
81212
- type: "http",
81213
- url: url2,
81214
- headers: { Authorization: `Bearer ${token}` }
81215
- };
81216
- installed.push(`${http.name} (http \u2192 ${url2})`);
81217
- }
81218
- (0, import_node_fs5.writeFileSync)(CLAUDE_JSON, JSON.stringify(config6, null, 2) + "\n", "utf-8");
81219
- console.log(`
81220
- Claude Code \u2014 configured in ${CLAUDE_JSON}`);
81221
- for (const entry2 of installed) console.log(` \u2022 ${entry2}`);
81222
- for (const w of warnings) console.log(` ! ${w}`);
81223
- return { installed, warnings };
81224
- }
81225
- function setupCodexMcp(target) {
81226
- let content = "";
81227
- if ((0, import_node_fs5.existsSync)(CODEX_CONFIG)) {
81228
- try {
81229
- content = (0, import_node_fs5.readFileSync)(CODEX_CONFIG, "utf-8");
81230
- } catch {
81231
- console.error(
81232
- `Warning: Could not read ${CODEX_CONFIG} \u2014 will create a new file.`
81233
- );
81234
- }
81235
- }
81236
- const installed = [];
81237
- const warnings = [];
81238
- if (target === "all" || target === "solutions") {
81239
- const haansiBin = resolveHaansiBin();
81240
- content = upsertTomlTable(content, "mcp_servers.haansi-solutions", {
81241
- command: tomlString(haansiBin),
81242
- args: tomlStringArray(["mcp-server"]),
81243
- env: tomlInlineTable({ HAANSI_AGENT_SOURCE: "codex" })
81244
- });
81245
- installed.push(`haansi-solutions (stdio \u2192 ${haansiBin} mcp-server)`);
81246
- }
81247
- for (const http of HTTP_SERVERS) {
81248
- if (target !== "all" && target !== http.target) continue;
81249
- const token = resolveToken();
81250
- if (!token) {
81251
- warnings.push(
81252
- `${http.name} not registered: no token found. Run \`haansi auth login\` first, or set HAANSI_TOKEN.`
81253
- );
81254
- continue;
81255
- }
81256
- const url2 = httpServerUrl(http.path);
81257
- content = upsertTomlTable(content, `mcp_servers.${http.name}`, {
81258
- url: tomlString(url2),
81259
- http_headers: tomlInlineTable({ Authorization: `Bearer ${token}` })
81260
- });
81261
- installed.push(`${http.name} (http \u2192 ${url2})`);
81262
- }
81263
- (0, import_node_fs5.mkdirSync)(CODEX_HOME, { recursive: true });
81264
- (0, import_node_fs5.writeFileSync)(CODEX_CONFIG, content, "utf-8");
81265
- console.log(`
81266
- OpenAI Codex \u2014 configured in ${CODEX_CONFIG}`);
81267
- for (const entry2 of installed) console.log(` \u2022 ${entry2}`);
81268
- for (const w of warnings) console.log(` ! ${w}`);
81269
- return { installed, warnings };
81270
- }
81271
- async function setupMcp(targetArg, agentFlag) {
81272
- const target = parseTarget(targetArg);
81273
- const selection = resolveAgentSelection(agentFlag);
81274
- for (const agentId of selection.agents) {
81275
- WRITERS[agentId](target);
81276
- }
81277
- saveAgents(selection.agents);
81278
- const restart = selection.agents.map((a) => a === "codex" ? "Codex" : "Claude Code").join(" / ");
81279
- console.log(`
81280
- Restart ${restart} to activate the changes.`);
81281
- }
81282
- var import_node_fs5, import_node_path4, import_node_os4, CLAUDE_JSON, CODEX_HOME, CODEX_CONFIG, HTTP_SERVERS, WRITERS;
81283
- var init_setup_mcp = __esm({
81284
- "src/commands/setup-mcp.ts"() {
81285
- "use strict";
81286
- import_node_fs5 = require("node:fs");
81287
- import_node_path4 = require("node:path");
81288
- import_node_os4 = require("node:os");
81289
- init_paths();
81290
- init_agent_selection();
81291
- init_codex_config();
81292
- CLAUDE_JSON = (0, import_node_path4.join)((0, import_node_os4.homedir)(), ".claude.json");
81293
- CODEX_HOME = process.env.CODEX_HOME || (0, import_node_path4.join)((0, import_node_os4.homedir)(), ".codex");
81294
- CODEX_CONFIG = (0, import_node_path4.join)(CODEX_HOME, "config.toml");
81295
- HTTP_SERVERS = [
81296
- {
81297
- target: "exchange",
81298
- name: "haansi-exchange",
81299
- path: "/api/v1/memory/exchange/mcp"
81300
- },
81301
- {
81302
- target: "digests",
81303
- name: "haansi-digests",
81304
- path: "/api/v1/memory/digests/mcp"
81305
- },
81306
- // Published onboarding guides, so an agent can pick up a workstream from
81307
- // its handoff doc (docs/product/plans/2026-09-24-onboarding-guides.md §8.3).
81308
- {
81309
- target: "onboarding",
81310
- name: "haansi-onboarding",
81311
- path: "/api/v1/memory/onboarding/mcp"
81312
- },
81313
- // Published team skills, loaded progressively by coding agents
81314
- // (docs/product/plans/2026-10-10-memory-skills.md §7).
81315
- {
81316
- target: "skills",
81317
- name: "haansi-skills",
81318
- path: "/api/v1/memory/skills/mcp"
81319
- }
81320
- ];
81321
- WRITERS = {
81322
- "claude-code": setupClaudeMcp,
81323
- codex: setupCodexMcp
81324
- };
81325
- }
81326
- });
81327
-
81328
83217
  // src/commands/setup-daemon.ts
81329
83218
  var setup_daemon_exports = {};
81330
83219
  __export(setup_daemon_exports, {