@cgh567/agent 2.4.2 → 2.4.4

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 (157) hide show
  1. package/agents/business/talisman-ceo.md +183 -0
  2. package/agents/business/talisman-comms.md +257 -0
  3. package/agents/business/talisman-cto.md +153 -0
  4. package/agents/business/talisman-finance.md +246 -0
  5. package/agents/business/talisman-marketing.md +240 -0
  6. package/agents/business/talisman-sales.md +242 -0
  7. package/agents/business/talisman-support.md +236 -0
  8. package/bin/helios-rpc.js +19 -0
  9. package/daemon/adapters/helios-rpc-adapter.js +5 -12
  10. package/daemon/adapters/tui_wakeup.js +8 -0
  11. package/daemon/context-enrichment.js +27 -0
  12. package/daemon/daemon-manager.js +1 -1
  13. package/daemon/db/email-infrastructure-migrate.js +192 -0
  14. package/daemon/db/hbo-core-migrate.js +189 -0
  15. package/daemon/helios-api.js +863 -64
  16. package/daemon/helios-company-daemon.js +233 -33
  17. package/daemon/lib/blast-radius-analyzer.js +75 -0
  18. package/daemon/lib/domain-bootstrap-orchestrator.js +267 -0
  19. package/daemon/lib/forensic-log.js +113 -0
  20. package/daemon/lib/goal-research-pipeline.js +644 -0
  21. package/daemon/lib/harada/cascade-judge.js +84 -1
  22. package/daemon/lib/harada/cascade-research-dispatcher.js +282 -0
  23. package/daemon/lib/harada/pillar-dispatcher.js +23 -2
  24. package/daemon/lib/hbo-bridge.js +74 -6
  25. package/daemon/lib/headroom-middleware.js +129 -0
  26. package/daemon/lib/headroom-proxy-manager.js +309 -0
  27. package/daemon/lib/hed-engine.js +25 -0
  28. package/daemon/lib/intelligence/department-page-generator.js +46 -1
  29. package/daemon/lib/interpretation-engine.js +92 -0
  30. package/daemon/lib/mental-model-cache.js +96 -0
  31. package/daemon/lib/project-factory.js +47 -0
  32. package/daemon/lib/session-log-reader.js +93 -0
  33. package/daemon/lib/standard-work-bootstrap.js +87 -1
  34. package/daemon/lib/task-completion-processor.js +23 -0
  35. package/daemon/lib/wizard-engine.js +57 -6
  36. package/daemon/package.json +2 -1
  37. package/daemon/routes/agents.js +51 -6
  38. package/daemon/routes/channels.js +116 -2
  39. package/daemon/routes/crm.js +85 -0
  40. package/daemon/routes/dashboard.js +62 -16
  41. package/daemon/routes/dept.js +10 -1
  42. package/daemon/routes/email-triage.js +19 -10
  43. package/daemon/routes/hbo.js +618 -58
  44. package/daemon/routes/hed.js +133 -0
  45. package/daemon/routes/inbox.js +397 -8
  46. package/daemon/routes/project.js +580 -66
  47. package/daemon/routes/routines.js +14 -0
  48. package/daemon/routes/tasks.js +15 -1
  49. package/daemon/schema-apply.js +174 -0
  50. package/daemon/schema-definitions.js +433 -0
  51. package/daemon/schema-migrations-hbo.js +20 -0
  52. package/daemon/schema-migrations-hed.js +18 -0
  53. package/daemon/schema-migrations-proj.js +153 -0
  54. package/extensions/__tests__/codebase-index.test.ts +73 -0
  55. package/extensions/__tests__/extension-command-registration.test.ts +35 -0
  56. package/extensions/__tests__/git-push-guard.test.ts +68 -0
  57. package/extensions/context-compaction.ts +104 -76
  58. package/extensions/cortex/__tests__/cortex-core.test.ts +100 -0
  59. package/extensions/cortex/wal-replay.ts +91 -0
  60. package/extensions/email/actions/draft-response.ts +21 -1
  61. package/extensions/email/auth/accounts.ts +5 -11
  62. package/extensions/email/auth/inbox-dog.ts +5 -2
  63. package/extensions/email/backfill.ts +20 -13
  64. package/extensions/email/providers/gmail.ts +164 -0
  65. package/extensions/email/providers/google-calendar.ts +34 -5
  66. package/extensions/helios-browser/__tests__/browser-routing.test.ts +57 -0
  67. package/extensions/helios-browser/backends/playwright.ts +3 -1
  68. package/extensions/helios-governance/__tests__/governance-gates.test.ts +40 -0
  69. package/extensions/helios-governance/__tests__/tournament-consumer.test.js +66 -0
  70. package/extensions/hema-dispatch-v3/headroom-compress.ts +103 -0
  71. package/extensions/hema-dispatch-v3/index.ts +46 -72
  72. package/extensions/interview/__tests__/server.test.ts +117 -0
  73. package/extensions/lib/helios-root.cjs +46 -0
  74. package/extensions/subagent-mesh/__tests__/handlers.test.ts +98 -0
  75. package/extensions/warm-tick/warm-tick-maintenance.ts +164 -0
  76. package/lib/__tests__/bulk-ingest.live.test.ts +66 -0
  77. package/lib/__tests__/crash-fixes.test.ts +49 -0
  78. package/lib/__tests__/hbo-core-store.test.js +238 -0
  79. package/lib/__tests__/maintenance-mission-wiring.test.ts +35 -0
  80. package/lib/broker/__tests__/jit-subscription.test.js +44 -1
  81. package/lib/broker/__tests__/lifecycle-channels.test.js +25 -1
  82. package/lib/compression/__tests__/ccr-store.test.js +138 -0
  83. package/lib/compression/__tests__/pipeline.test.js +280 -0
  84. package/lib/compression/__tests__/smart-crusher.test.js +242 -0
  85. package/lib/compression/dist/server.js +34 -1
  86. package/lib/compression/dist/start-server.js +77 -0
  87. package/lib/event-bus.mts +1 -1
  88. package/lib/graph/learning/headroom-learn-bridge.js +175 -0
  89. package/lib/graph-availability.js +62 -0
  90. package/lib/hbo-core-store.compiled.js +834 -0
  91. package/lib/hbo-core-store.js +124 -0
  92. package/lib/hbo-core-store.ts +979 -0
  93. package/lib/mission-loop/__tests__/research-handler.test.ts +143 -0
  94. package/lib/skill-sync.js +6 -1
  95. package/lib/startup-integrity.js +9 -2
  96. package/lib/triage-core/__tests__/classifier-fixture.test.ts +254 -0
  97. package/lib/triage-core/__tests__/classifier-post-norm.test.ts +1 -1
  98. package/lib/triage-core/__tests__/classifier.test.ts +45 -7
  99. package/lib/triage-core/__tests__/correction-detector.test.ts +36 -0
  100. package/lib/triage-core/__tests__/d6-dunbar-boost.test.ts +5 -5
  101. package/lib/triage-core/__tests__/orchestrator-pipeline.test.ts +107 -0
  102. package/lib/triage-core/__tests__/orchestrator.test.ts +113 -1
  103. package/lib/triage-core/__tests__/signals.test.ts +357 -0
  104. package/lib/triage-core/__tests__/sql-parity.test.ts +216 -0
  105. package/lib/triage-core/backfill-cost-estimator.ts +91 -0
  106. package/lib/triage-core/backfill-orchestrator.ts +119 -0
  107. package/lib/triage-core/classifier.ts +41 -8
  108. package/lib/triage-core/cos/cross-channel-escalation.ts +2 -3
  109. package/lib/triage-core/cos/response-debt.ts +2 -2
  110. package/lib/triage-core/graph/__tests__/batch-persistence.test.ts +283 -0
  111. package/lib/triage-core/graph/batch-persistence.ts +66 -2
  112. package/lib/triage-core/graph/betweenness-worker.js +75 -0
  113. package/lib/triage-core/graph/graph-rank-sql.ts +67 -0
  114. package/lib/triage-core/graph/persistence.ts +1 -1
  115. package/lib/triage-core/graph/schema-v2.ts +2 -0
  116. package/lib/triage-core/graph/schema.cypher +11 -0
  117. package/lib/triage-core/graph/triage-query.ts +1 -1
  118. package/lib/triage-core/learning.ts +15 -20
  119. package/lib/triage-core/mental-model/bedrock-config.ts +78 -0
  120. package/lib/triage-core/mental-model/cos-integration.ts +1 -1
  121. package/lib/triage-core/mental-model/entity-extractor.ts +51 -4
  122. package/lib/triage-core/mental-model/identity-resolver.ts +5 -5
  123. package/lib/triage-core/mental-model/key-facts.ts +1 -2
  124. package/lib/triage-core/mental-model/model-assembler-sql.ts +200 -0
  125. package/lib/triage-core/mental-model/model-assembler.ts +16 -3
  126. package/lib/triage-core/orchestrator.ts +8 -15
  127. package/lib/triage-core/scheduled-sends.ts +39 -2
  128. package/lib/triage-core/signals/comms-style.ts +1 -1
  129. package/lib/triage-core/signals/cross-channel-escalation.ts +2 -2
  130. package/lib/triage-core/signals/favee-type.ts +6 -1
  131. package/lib/triage-core/signals/goal-relevance.ts +31 -2
  132. package/lib/triage-core/signals/personal-importance.ts +1 -1
  133. package/lib/triage-core/signals/referral-chain.ts +0 -1
  134. package/lib/triage-core/signals/relationship-decay.ts +4 -0
  135. package/lib/triage-core/signals/relationship-health.ts +6 -1
  136. package/lib/triage-core/signals/trajectory-signal.ts +38 -3
  137. package/lib/triage-core/tournament-runner.js +11 -1
  138. package/lib/triage-core/triage-llm-factory.ts +110 -0
  139. package/lib/triage-core/triage-local-llm.ts +145 -0
  140. package/lib/triage-core/triage-sql-store.ts +337 -0
  141. package/lib/triage-core/types.ts +2 -2
  142. package/lib/unified-graph.atomic.test.ts +52 -0
  143. package/lib/unified-graph.failure-categories.test.ts +55 -0
  144. package/package.json +18 -7
  145. package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
  146. package/prebuilds/linux-x64/better_sqlite3.node +0 -0
  147. package/prebuilds/win32-x64/better_sqlite3.node +0 -0
  148. package/skills/helios-bookkeeping/SKILL.md +321 -0
  149. package/skills/helios-briefer/SKILL.md +44 -0
  150. package/skills/helios-client-relations/SKILL.md +322 -0
  151. package/skills/helios-personal-triager/SKILL.md +45 -0
  152. package/skills/helios-recruitment/SKILL.md +317 -0
  153. package/skills/helios-relationship-nudger/SKILL.md +77 -0
  154. package/skills/helios-researcher/SKILL.md +44 -0
  155. package/skills/helios-scheduler/SKILL.md +58 -0
  156. package/skills/helios-tax-analyst/SKILL.md +280 -0
  157. package/lib/triage-core/orchestrator.ts.bak-r005-r006-r008 +0 -1823
@@ -104,6 +104,7 @@ import { recordDispatchOutcome } from '../../brainv2/channel-outcome-tracker.ts'
104
104
  import { shouldInjectOracle, formatOracleInjection } from './oracle.ts';
105
105
  import { shouldInjectHEMA } from './conditional-inject.ts';
106
106
  import { routeSkills, formatSkillHints } from './skill-router.ts';
107
+ import { compressTextViaHeadroom } from './headroom-compress.ts';
107
108
 
108
109
  // Expose skill router for cross-extension access (cortex uses this)
109
110
  (globalThis as any).__hemaSkillRouter = { routeSkills };
@@ -682,15 +683,20 @@ export default function hemaDispatchV3(pi: any): void {
682
683
  } catch (_) { process.stderr.write(`[hema:catch] fail-open: graceful degradation: ${(_ as Error)?.message || _}\n`); } // HeliosRuntime may not be initialized — fail-open
683
684
 
684
685
  // ═══ TASK-13: Budget Pre-Admission Gate ═══
685
- // Reads ~/helios-agent/brainv2/budget-status.json (written by GraphSyncService).
686
+ // MT-02: Path resolves from HELIOS_ROOT env var (required). If unset, daemon logs a startup error.
687
+ // budget-status.json is written by GraphSyncService at $HELIOS_ROOT/brainv2/budget-status.json.
686
688
  // Fail-open: if file missing or unreadable, proceed normally.
687
689
  // blocked=true → hard block, return early with ⛔ message
688
690
  // warningActive → inject budget warning into agent context (soft signal)
689
691
  let _budgetStatus: { blocked?: boolean; warningActive?: boolean; policies?: any[] } = {};
690
692
  let _budgetWarning: string | null = null;
691
693
  try {
692
- const _budgetStatusPath = join(homedir(), 'helios-agent/brainv2/budget-status.json');
693
- if (fs.existsSync(_budgetStatusPath)) {
694
+ const _heliosRoot = process.env['HELIOS_ROOT'];
695
+ if (!_heliosRoot) {
696
+ process.stderr.write('[hema] HELIOS_ROOT not set — budget-status.json cannot be read. Set HELIOS_ROOT to the helios-agent repo root.\n');
697
+ }
698
+ const _budgetStatusPath = _heliosRoot ? join(_heliosRoot, 'brainv2/budget-status.json') : null;
699
+ if (_budgetStatusPath && fs.existsSync(_budgetStatusPath)) {
694
700
  _budgetStatus = JSON.parse(fs.readFileSync(_budgetStatusPath, 'utf8'));
695
701
  }
696
702
  } catch (_budgetErr) {
@@ -1505,71 +1511,7 @@ export default function hemaDispatchV3(pi: any): void {
1505
1511
  // No context available from either path
1506
1512
  enrichedTask = taskText;
1507
1513
  logHemaEvent('hema_no_context', { nativePacksUsed, legacyUsed: admitted.admitted?.length > 0 });
1508
- }
1509
-
1510
- // ── Helios Compression: compress HEMA recall payload before injection ──
1511
- // The recall context contains JSON graph payloads (leads, signals, tasks,
1512
- // goals, code nodes). Send the assembled text as a tool_result block to
1513
- // the compression server — it will find and compress embedded JSON arrays.
1514
- //
1515
- // Uses direct HTTP to HEADROOM_PROXY_URL (same pattern as context-compaction.ts)
1516
- // rather than the headroom-ai npm package so this works in Pi subprocess context.
1517
- // Applies to all companies: the role injection budgets enforce per-agent limits.
1518
- if (enrichedTask.length > 2000) {
1519
- const _hrUrl = process.env.HEADROOM_PROXY_URL;
1520
- if (_hrUrl) {
1521
- try {
1522
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1523
- const http = require('http');
1524
- const _payload = JSON.stringify({
1525
- messages: [{
1526
- role: 'user',
1527
- content: [{
1528
- type: 'tool_result',
1529
- tool_use_id: 'hema_recall',
1530
- content: enrichedTask,
1531
- }],
1532
- }],
1533
- });
1534
- const _result: any = await new Promise((resolve, reject) => {
1535
- const _url = new URL('/headroom/compress', _hrUrl);
1536
- const _req = http.request(
1537
- {
1538
- hostname: _url.hostname,
1539
- port: parseInt(_url.port || '8787', 10),
1540
- path: '/headroom/compress',
1541
- method: 'POST',
1542
- headers: {
1543
- 'Content-Type': 'application/json',
1544
- 'Content-Length': Buffer.byteLength(_payload),
1545
- },
1546
- },
1547
- (res: any) => {
1548
- let body = '';
1549
- res.on('data', (c: Buffer) => { body += c; });
1550
- res.on('end', () => { try { resolve(JSON.parse(body)); } catch { reject(new Error('bad json')); } });
1551
- res.on('error', reject);
1552
- }
1553
- );
1554
- _req.setTimeout(3000, () => { _req.destroy(); reject(new Error('timeout')); });
1555
- _req.on('error', reject);
1556
- _req.write(_payload);
1557
- _req.end();
1558
- });
1559
-
1560
- const _compressed = _result?.messages?.[0]?.content?.[0]?.content ?? enrichedTask;
1561
- if (typeof _compressed === 'string' && _compressed.length < enrichedTask.length) {
1562
- const _saved = enrichedTask.length - _compressed.length;
1563
- process.stderr.write(`[hema-dispatch-v3] Headroom compressed recall context: -${_saved} chars (${agentType})\n`);
1564
- enrichedTask = _compressed;
1565
- logHemaEvent('hema_headroom_compressed', { saved: _saved, agentType });
1566
- }
1567
- } catch (_hrErr: any) {
1568
- // Non-fatal: log and continue with uncompressed enrichedTask
1569
- process.stderr.write(`[hema-dispatch-v3] Headroom recall compress skipped: ${_hrErr?.message}\n`);
1570
- }
1571
- }
1572
- }
1514
+ }
1573
1515
 
1574
1516
  // Wire getReasoningHint for proven reasoning paths (lazy require to avoid circular import under jiti)
1575
1517
  try {
@@ -1655,6 +1597,22 @@ export default function hemaDispatchV3(pi: any): void {
1655
1597
  triggerMatrixRefresh(projectPath);
1656
1598
  }
1657
1599
 
1600
+ // ── Helios Compression: compress full enrichedTask AFTER all appends ──
1601
+ // Fires after reasoningHint, V-Gate block, skillHints, and oracleInjection
1602
+ // are all appended. Compresses embedded JSON arrays (signals, leads, pipeline,
1603
+ // tasks) from the recall context. Prose appends pass through unchanged.
1604
+ // Uses compressTextViaHeadroom() — fail-open, 3s timeout, no throws.
1605
+ {
1606
+ const _hrUrl = process.env.HEADROOM_PROXY_URL;
1607
+ const _taskBefore = enrichedTask.length;
1608
+ enrichedTask = await compressTextViaHeadroom(enrichedTask, _hrUrl, 'hema_task');
1609
+ if (enrichedTask.length < _taskBefore) {
1610
+ const _saved = _taskBefore - enrichedTask.length;
1611
+ process.stderr.write(`[hema-dispatch-v3] Headroom compressed full task: -${_saved} chars (${agentType})\n`);
1612
+ logHemaEvent('hema_headroom_compressed', { saved: _saved, agentType });
1613
+ }
1614
+ }
1615
+
1658
1616
  t.task = enrichedTask;
1659
1617
  if (_budgetWarning) {
1660
1618
  t.task = `${_budgetWarning}\n\n${t.task}`;
@@ -2676,9 +2634,10 @@ export default function hemaDispatchV3(pi: any): void {
2676
2634
  } catch (_) { process.stderr.write(`[hema:catch] fail-open: ${(_ as Error)?.message || _}\n`); }
2677
2635
 
2678
2636
  // Load hot memory — ensure file exists
2679
- const hotMemoryPath = join(homedir(), 'helios-agent', 'sessions', 'hot-memory.json');
2637
+ const _heliosRootForMem = process.env['HELIOS_ROOT'] ?? join(homedir(), 'helios-agent');
2638
+ const hotMemoryPath = join(_heliosRootForMem, 'sessions', 'hot-memory.json');
2680
2639
  if (!fs.existsSync(hotMemoryPath)) {
2681
- const sessDir = join(homedir(), 'helios-agent', 'sessions');
2640
+ const sessDir = join(_heliosRootForMem, 'sessions');
2682
2641
  if (!fs.existsSync(sessDir)) fs.mkdirSync(sessDir, { recursive: true });
2683
2642
  fs.writeFileSync(hotMemoryPath, JSON.stringify({ sessions: [] }, null, 2), 'utf8');
2684
2643
  }
@@ -2706,7 +2665,7 @@ export default function hemaDispatchV3(pi: any): void {
2706
2665
  const pendingIds = [..._pendingDispatchIds.keys()];
2707
2666
  try {
2708
2667
  const _walSessionId = di('sessionId') || process.pid;
2709
- const walPath = join(homedir(), `helios-agent/sessions/.pending-dispatches-${_walSessionId}.json`);
2668
+ const walPath = join(process.env['HELIOS_ROOT'] ?? join(homedir(), 'helios-agent'), `sessions/.pending-dispatches-${_walSessionId}.json`);
2710
2669
  fs.writeFileSync(walPath, JSON.stringify({ ids: pendingIds, timestamp: Date.now(), sessionId: di('sessionId') || 'unknown' }));
2711
2670
  process.stderr.write(`[hema] session_shutdown: wrote ${pendingIds.length} pending dispatch ID(s) to WAL\n`);
2712
2671
  } catch (e: any) {
@@ -3144,7 +3103,7 @@ export default function hemaDispatchV3(pi: any): void {
3144
3103
  return { systemPrompt: appendDynamic(event.systemPrompt, '\n' + evalContext) };
3145
3104
  }
3146
3105
 
3147
- const hotMemoryPath = join(homedir(), 'helios-agent', 'sessions', 'hot-memory.json');
3106
+ const hotMemoryPath = join(process.env['HELIOS_ROOT'] ?? join(homedir(), 'helios-agent'), 'sessions', 'hot-memory.json');
3148
3107
 
3149
3108
  let hotMemory: any = null;
3150
3109
  if (fs.existsSync(hotMemoryPath)) {
@@ -3532,6 +3491,21 @@ export default function hemaDispatchV3(pi: any): void {
3532
3491
  }
3533
3492
  } catch { /* fail-open */ }
3534
3493
 
3494
+ // ── Helios Compression: compress combined system-prompt block ─────────
3495
+ // `combined` contains company context (JSON graph payloads: goals, tasks,
3496
+ // signals, pipeline), code matrix, EvidencePacks, skill hints, and mission
3497
+ // context. JSON arrays in company context and code matrix benefit from
3498
+ // SmartCrusher. Prose blocks pass through unchanged.
3499
+ // Uses compressTextViaHeadroom() — fail-open, 3s timeout, no throws.
3500
+ {
3501
+ const _hrUrlBAS = process.env.HEADROOM_PROXY_URL;
3502
+ const _combinedBefore = combined.length;
3503
+ combined = await compressTextViaHeadroom(combined, _hrUrlBAS, 'hema_before_agent');
3504
+ if (combined.length < _combinedBefore) {
3505
+ process.stderr.write(`[hema-dispatch-v3] Headroom compressed before_agent_start: -${_combinedBefore - combined.length} chars\n`);
3506
+ }
3507
+ }
3508
+
3535
3509
  return { systemPrompt: appendDynamic(event.systemPrompt, '\n' + combined + observationsBlock) };
3536
3510
  } catch (err) { /* fail-open: system prompt enrichment */ if (process.env.HELIOS_DEBUG) console.error(`[hema] system prompt enrichment error: ${String(err)}`); }
3537
3511
  });
@@ -0,0 +1,117 @@
1
+ /**
2
+ * extensions/interview/__tests__/server.test.ts
3
+ * P3-N6: Interview server tests
4
+ */
5
+ import { describe, it, expect, afterAll, beforeAll } from 'vitest';
6
+ import * as http from 'node:http';
7
+ import * as fs from 'node:fs';
8
+ import * as path from 'node:path';
9
+ import * as os from 'node:os';
10
+
11
+ const SERVER_PATH = path.resolve(__dirname, '../server.ts');
12
+
13
+ // Helper to make HTTP requests to the test server
14
+ function httpRequest(
15
+ port: number,
16
+ method: string,
17
+ urlPath: string,
18
+ body?: unknown
19
+ ): Promise<{ status: number; body: unknown }> {
20
+ return new Promise((resolve, reject) => {
21
+ const data = body ? JSON.stringify(body) : undefined;
22
+ const req = http.request(
23
+ { hostname: '127.0.0.1', port, method, path: urlPath,
24
+ headers: { 'Content-Type': 'application/json',
25
+ 'Content-Length': data ? Buffer.byteLength(data) : 0 } },
26
+ (res) => {
27
+ let raw = '';
28
+ res.on('data', c => { raw += c; });
29
+ res.on('end', () => {
30
+ try { resolve({ status: res.statusCode ?? 0, body: JSON.parse(raw) }); }
31
+ catch { resolve({ status: res.statusCode ?? 0, body: raw }); }
32
+ });
33
+ }
34
+ );
35
+ req.on('error', reject);
36
+ if (data) req.write(data);
37
+ req.end();
38
+ });
39
+ }
40
+
41
+ describe('interview/server — HTTP contract', () => {
42
+ let server: any = null;
43
+ let port = 0;
44
+ const tempSessionFile = path.join(os.tmpdir(), `interview-sessions-test-${Date.now()}.json`);
45
+
46
+ beforeAll(async () => {
47
+ if (!fs.existsSync(SERVER_PATH)) return;
48
+ const mod = await import('../server.ts').catch(() => null) as any;
49
+ if (!mod) return;
50
+ const createFn = mod.createServer ?? mod.default;
51
+ if (typeof createFn !== 'function') return;
52
+
53
+ try {
54
+ server = createFn({ sessionFile: tempSessionFile, port: 0 });
55
+ if (server && typeof server.listen === 'function') {
56
+ await new Promise<void>((res) => {
57
+ server.listen(0, '127.0.0.1', () => {
58
+ port = (server.address() as any)?.port ?? 0;
59
+ res();
60
+ });
61
+ });
62
+ }
63
+ } catch {
64
+ server = null;
65
+ }
66
+ });
67
+
68
+ afterAll(async () => {
69
+ if (server && typeof server.close === 'function') {
70
+ await new Promise<void>((res) => server.close(() => res()));
71
+ }
72
+ if (fs.existsSync(tempSessionFile)) fs.unlinkSync(tempSessionFile);
73
+ });
74
+
75
+ it('createServer returns an http.Server instance (if server starts)', () => {
76
+ if (!fs.existsSync(SERVER_PATH)) return;
77
+ if (server === null) return; // guard: module needs runtime
78
+ expect(server).toBeDefined();
79
+ expect(typeof server.listen === 'function').toBe(true);
80
+ });
81
+
82
+ it('GET /sessions returns 200 with array body', async () => {
83
+ if (!server || port === 0) return;
84
+ const { status, body } = await httpRequest(port, 'GET', '/sessions');
85
+ expect(status).toBe(200);
86
+ expect(Array.isArray(body)).toBe(true);
87
+ });
88
+
89
+ it('POST /sessions creates session; GET /sessions includes it', async () => {
90
+ if (!server || port === 0) return;
91
+ const { status: createStatus, body: created } = await httpRequest(
92
+ port, 'POST', '/sessions', { goal: 'test interview goal', context: {} }
93
+ );
94
+ expect(createStatus).toBeLessThan(300);
95
+
96
+ const { body: list } = await httpRequest(port, 'GET', '/sessions');
97
+ expect(Array.isArray(list)).toBe(true);
98
+ });
99
+
100
+ it('session file is written after session creation', async () => {
101
+ if (!server || port === 0) return;
102
+ await httpRequest(port, 'POST', '/sessions', { goal: 'file write test', context: {} });
103
+ // Give the server time to write
104
+ await new Promise(r => setTimeout(r, 100));
105
+ if (fs.existsSync(tempSessionFile)) {
106
+ const data = JSON.parse(fs.readFileSync(tempSessionFile, 'utf8'));
107
+ expect(Array.isArray(data) || typeof data === 'object').toBe(true);
108
+ }
109
+ });
110
+
111
+ it('server.ts source exports startInterviewServer or createServer', () => {
112
+ if (!fs.existsSync(SERVER_PATH)) return;
113
+ const source = fs.readFileSync(SERVER_PATH, 'utf8');
114
+ const hasExport = source.includes('startInterviewServer') || source.includes('createServer') || source.includes('module.exports') || source.includes('export function');
115
+ expect(hasExport).toBe(true);
116
+ });
117
+ });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * helios-root.cjs — Cross-platform HELIOS_ROOT resolution.
3
+ *
4
+ * CommonJS module (works in both require() and jiti-transpiled CJS contexts).
5
+ * The extensions/ package declares "type":"commonjs" so this must use module.exports.
6
+ *
7
+ * Priority order:
8
+ * 1. HELIOS_ROOT env var — injected by helios-rpc.cjs at spawn time
9
+ * 2. HELIOS_CODING_AGENT_DIR — alternate env var used by some tools
10
+ * 3. ~/helios-agent — backward-compatible fallback for direct installs
11
+ *
12
+ * Works on:
13
+ * - Windows Desktop: C:\Users\<user>\Desktop\Helios\helios-agent-main (HELIOS_ROOT injected)
14
+ * - macOS/Linux: ~/helios-agent (fallback)
15
+ * - CI: any arbitrary checkout path (via HELIOS_ROOT env var)
16
+ */
17
+ 'use strict';
18
+
19
+ const { homedir } = require('node:os');
20
+ const { join } = require('node:path');
21
+
22
+ /**
23
+ * The helios-agent repo root. Set by helios-rpc.cjs as HELIOS_ROOT.
24
+ * Falls back to ~/helios-agent for backward compatibility.
25
+ */
26
+ const HELIOS_ROOT =
27
+ process.env.HELIOS_ROOT ||
28
+ process.env.HELIOS_CODING_AGENT_DIR ||
29
+ join(homedir(), 'helios-agent');
30
+
31
+ /**
32
+ * Build an absolute path under HELIOS_ROOT.
33
+ * @param {...string} parts - path segments to join
34
+ * @returns {string} absolute path
35
+ * @example heliosPath('settings.json') // → /path/to/helios-agent/settings.json
36
+ * @example heliosPath('extensions', '.manifest.json')
37
+ */
38
+ const heliosPath = (...parts) => join(HELIOS_ROOT, ...parts);
39
+
40
+ /**
41
+ * Cross-platform home directory (os.homedir()).
42
+ * Use this instead of process.env.HOME — HOME is not set on Windows.
43
+ */
44
+ const HOME = homedir();
45
+
46
+ module.exports = { HELIOS_ROOT, heliosPath, HOME };
@@ -0,0 +1,98 @@
1
+ /**
2
+ * extensions/subagent-mesh/__tests__/handlers.test.ts
3
+ * P3-N2: Subagent mesh handler tests
4
+ *
5
+ * This module requires the Pi runtime for full execution.
6
+ * Guards emit named warnings so gaps are visible in CI output.
7
+ */
8
+ import { describe, it, expect, vi } from 'vitest';
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+
12
+ describe('subagent-mesh/handlers — module contract', () => {
13
+ it('handlers.ts exports a handler registration function', async () => {
14
+ const mod = await import('../handlers.ts').catch((e) => {
15
+ console.warn('[subagent-mesh/handlers] import failed (Pi runtime required):', String(e).slice(0, 120));
16
+ return null;
17
+ }) as any;
18
+ if (!mod) return;
19
+ const hasExport = typeof mod.registerHandlers === 'function'
20
+ || typeof mod.registerToolCallHandler === 'function'
21
+ || typeof mod.registerToolResultHandlers === 'function'
22
+ || typeof mod.default === 'function';
23
+ if (!hasExport) {
24
+ console.warn('[subagent-mesh/handlers] no handler registration export found — check export name');
25
+ }
26
+ // Module loaded — must be an object
27
+ expect(typeof mod).toBe('object');
28
+ });
29
+
30
+ it('registerHandlers called with mock Pi registers tool hooks', async () => {
31
+ const mod = await import('../handlers.ts').catch(() => null) as any;
32
+ if (!mod) {
33
+ console.warn('[subagent-mesh/handlers] SKIP: module unavailable — Pi runtime required');
34
+ return;
35
+ }
36
+ const registerFn = mod.registerHandlers ?? mod.registerToolCallHandler;
37
+ if (typeof registerFn !== 'function') {
38
+ console.warn('[subagent-mesh/handlers] SKIP: no register function found — check export name');
39
+ return;
40
+ }
41
+
42
+ const registeredHooks: string[] = [];
43
+ const mockPi: any = {
44
+ on: (event: string, _handler: unknown) => { registeredHooks.push(event); },
45
+ registerTool: vi.fn(),
46
+ log: vi.fn(),
47
+ };
48
+
49
+ expect(() => registerFn(mockPi)).not.toThrow();
50
+ if (registeredHooks.length === 0) {
51
+ console.warn('[subagent-mesh/handlers] no hooks registered — handler may need Pi session context');
52
+ return;
53
+ }
54
+ // At least one of tool_call or tool_result must be registered
55
+ const hasToolHook = registeredHooks.includes('tool_call') || registeredHooks.includes('tool_result');
56
+ expect(hasToolHook).toBe(true);
57
+ });
58
+
59
+ it('Thompson sampling bridge: source has error handling around dynamic require', () => {
60
+ const handlersPath = path.resolve(__dirname, '../handlers.ts');
61
+ if (!fs.existsSync(handlersPath)) {
62
+ console.warn('[subagent-mesh/handlers] SKIP: handlers.ts not found at', handlersPath);
63
+ return;
64
+ }
65
+ const source = fs.readFileSync(handlersPath, 'utf8');
66
+ const hasThompson = source.includes('thompson') || source.includes('Thompson');
67
+ if (!hasThompson) {
68
+ console.warn('[subagent-mesh/handlers] Thompson sampling not referenced — cortex bridge is absent');
69
+ return;
70
+ }
71
+ // Thompson require must be guarded
72
+ const hasTryCatch = source.includes('catch') || source.includes('?.') || source.includes('|| null');
73
+ expect(hasTryCatch).toBe(true);
74
+ });
75
+
76
+ it('handlers.ts source file exists and is non-empty', () => {
77
+ const handlersPath = path.resolve(__dirname, '../handlers.ts');
78
+ if (!fs.existsSync(handlersPath)) {
79
+ console.warn('[subagent-mesh/handlers] SKIP: handlers.ts not found');
80
+ return;
81
+ }
82
+ const stat = fs.statSync(handlersPath);
83
+ expect(stat.size).toBeGreaterThan(100);
84
+ });
85
+
86
+ it('checkpoint eviction: source defines _activeCheckpoints with size limit', () => {
87
+ const handlersPath = path.resolve(__dirname, '../handlers.ts');
88
+ if (!fs.existsSync(handlersPath)) return;
89
+ const source = fs.readFileSync(handlersPath, 'utf8');
90
+ const hasCheckpoints = source.includes('_activeCheckpoints') || source.includes('activeCheckpoints');
91
+ if (!hasCheckpoints) {
92
+ console.warn('[subagent-mesh/handlers] _activeCheckpoints not found in source — eviction is unimplemented');
93
+ return;
94
+ }
95
+ const hasLimit = source.includes('50') || source.includes('.size >=') || source.includes('.size>') || source.includes('.size >');
96
+ expect(hasLimit).toBe(true);
97
+ });
98
+ });
@@ -713,6 +713,14 @@ export async function runScheduledMaintenance(): Promise<void> {
713
713
  }
714
714
  }
715
715
 
716
+ // SEC-5: Daily cleanup of expired DraftAction PII nodes (30-day TTL)
717
+ try {
718
+ const { rawWrite: _rawWrite } = require('../../lib/safe-memgraph.js');
719
+ await _rawWrite('MATCH (da:DraftAction) WHERE da.expiresAt < datetime() DETACH DELETE da', {});
720
+ } catch (err: any) {
721
+ console.warn('[warm-tick] DraftAction TTL cleanup failed:', err?.message || String(err));
722
+ }
723
+
716
724
  // Daily: incremental label backfill (ensures critical graph labels are never empty)
717
725
  try {
718
726
  const { runIncrementalBackfill } = await import('./warm-tick-backfill.js');
@@ -726,6 +734,24 @@ export async function runScheduledMaintenance(): Promise<void> {
726
734
  } catch (bfErr: any) {
727
735
  process.stderr.write("[warm-tick] backfill failed (non-fatal): " + (bfErr?.message || String(bfErr)) + "\n");
728
736
  }
737
+ // EN3: comms-style enrichment — daily cadence
738
+ try {
739
+ const { computeAllStyles } = await import('../../lib/triage-core/mental-model/comms-style-computer.js');
740
+ await computeAllStyles();
741
+ process.stderr.write('[warm-tick] warm-tick: comms styles computed\n');
742
+ } catch (err) {
743
+ process.stderr.write(`[warm-tick] warm-tick: computeAllStyles failed: ${err}\n`);
744
+ }
745
+
746
+ // EN6: quality profiles — daily cadence
747
+ try {
748
+ const { computeAllQualityProfiles } = await import('../../lib/triage-core/mental-model/quality-scorer-v2.js');
749
+ await computeAllQualityProfiles();
750
+ process.stderr.write('[warm-tick] warm-tick: quality profiles computed\n');
751
+ } catch (err) {
752
+ process.stderr.write(`[warm-tick] warm-tick: computeAllQualityProfiles failed: ${err}\n`);
753
+ }
754
+
729
755
  await budgetedYield(); // yield between cadence checks
730
756
 
731
757
  // Every maintenance cycle: embedding catch-all (Layer 2 of 3-layer auto-embed pipeline).
@@ -1178,6 +1204,100 @@ export async function runScheduledMaintenance(): Promise<void> {
1178
1204
  ` roles=${report.rolesChanged} dunbar=${report.dunbarUpdated} elapsed=${report.durationMs}ms\n`
1179
1205
  );
1180
1206
  }
1207
+
1208
+ // SP1: Unsnooze emails past their snooze time
1209
+ try {
1210
+ const { rawRead: rRead, rawWrite: rWrite } = require('../../lib/safe-memgraph.js');
1211
+ const snoozed = await rRead(
1212
+ `MATCH (e:Email) WHERE e.snoozed = true AND e.snoozedUntil IS NOT NULL AND e.snoozedUntil < datetime() RETURN e.messageId AS mid, e.accountEmail AS acct`,
1213
+ {}
1214
+ );
1215
+ if (snoozed && snoozed.length > 0) {
1216
+ const { loadToken, refreshAccessToken } = require('../email/auth/inbox-dog.js');
1217
+ for (const row of snoozed) {
1218
+ try {
1219
+ const token = await loadToken(row.acct);
1220
+ const refreshed = token ? await refreshAccessToken(token) : null;
1221
+ const at = refreshed?.access_token || token?.access_token;
1222
+ if (at) {
1223
+ // Re-add INBOX, remove SNOOZED via raw Gmail API
1224
+ const body = JSON.stringify({ addLabelIds: ['INBOX'], removeLabelIds: ['SNOOZED'] });
1225
+ await new Promise<void>((resolve) => {
1226
+ const req = require('https').request({ hostname: 'gmail.googleapis.com', path: `/gmail/v1/users/me/messages/${encodeURIComponent(row.mid)}/modify`, method: 'POST', headers: { 'Authorization': `Bearer ${at}`, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } }, (r: any) => { r.resume(); r.on('end', resolve); });
1227
+ req.on('error', () => resolve()); req.write(body); req.end();
1228
+ });
1229
+ }
1230
+ await rWrite(`MATCH (e:Email {messageId: $mid}) REMOVE e.snoozed, e.snoozedUntil`, { mid: row.mid });
1231
+ } catch (e: any) { console.warn(`[warm-tick] unsnooze failed for ${row.mid}: ${e.message}`); }
1232
+ }
1233
+ console.info(`[warm-tick] unsnoozed ${snoozed.length} email(s)`);
1234
+ }
1235
+ } catch (e: any) { console.warn(`[warm-tick] SP1 unsnooze error: ${e.message}`); }
1236
+
1237
+ // SP2: Execute due scheduled sends
1238
+ try {
1239
+ const scheduledSends = require('../../lib/triage-core/scheduled-sends.js');
1240
+ const due = scheduledSends.getDueMessages();
1241
+ if (due && due.length > 0) {
1242
+ // Load GmailProvider for each unique account
1243
+ for (const msg of due) {
1244
+ try {
1245
+ const { loadToken, refreshAccessToken } = require('../email/auth/inbox-dog.js');
1246
+ const token = await loadToken(msg.accountEmail || ''); // H1/M3: use accountEmail, not msg.to
1247
+ const refreshed = token ? await refreshAccessToken(token) : null;
1248
+ const at = refreshed?.access_token || token?.access_token;
1249
+ if (at && msg.draftId) {
1250
+ const body = JSON.stringify({ id: msg.draftId });
1251
+ await new Promise<void>((resolve) => {
1252
+ const req = require('https').request({ hostname: 'gmail.googleapis.com', path: '/gmail/v1/users/me/drafts/send', method: 'POST', headers: { 'Authorization': `Bearer ${at}`, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } }, (r: any) => { r.resume(); r.on('end', resolve); });
1253
+ req.on('error', () => resolve()); req.write(body); req.end();
1254
+ });
1255
+ scheduledSends.markSent(msg.id);
1256
+ console.info(`[warm-tick] SP2 fired scheduled send ${msg.id}`);
1257
+ } else {
1258
+ scheduledSends.markSent(msg.id);
1259
+ console.info(`[warm-tick] SP2 fired scheduled send ${msg.id}`);
1260
+ }
1261
+ } catch (e: any) {
1262
+ console.warn(`[warm-tick] SP2 scheduled send failed for ${msg.id}: ${e.message}`);
1263
+ }
1264
+ }
1265
+ }
1266
+ } catch (e: any) { console.warn(`[warm-tick] SP2 scheduled sends error: ${e.message}`); }
1267
+
1268
+ // SP5: Auto-trash emails from blocked senders
1269
+ try {
1270
+ const { rawRead: rRead2, rawWrite: rWrite2 } = require('../../lib/safe-memgraph.js');
1271
+ const blocked = await rRead2(`MATCH (bs:BlockedSender) RETURN bs.email AS email`, {});
1272
+ if (blocked && blocked.length > 0) {
1273
+ const blockedEmails = blocked.map((r: any) => r.email);
1274
+ const newEmails = await rRead2(
1275
+ `MATCH (e:Email) WHERE e.from IN $emails AND NOT 'TRASH' IN e.labels AND e.snoozed IS NULL RETURN e.messageId AS mid, e.from AS from LIMIT 50`,
1276
+ { emails: blockedEmails }
1277
+ );
1278
+ if (newEmails && newEmails.length > 0) {
1279
+ // M3: load the authenticated account token (not the blocked sender's email)
1280
+ const { loadToken: _loadDefaultToken, refreshAccessToken: _refreshDefault } = require('../email/auth/inbox-dog.js');
1281
+ const _defaultToken = await _loadDefaultToken(null).catch(() => null);
1282
+ const _refreshed = _defaultToken ? await _refreshDefault(_defaultToken).catch(() => null) : null;
1283
+ const _defaultAt = _refreshed?.access_token || _defaultToken?.access_token;
1284
+ for (const row of newEmails) {
1285
+ try {
1286
+ const body = JSON.stringify({ addLabelIds: ['TRASH'], removeLabelIds: ['INBOX', 'UNREAD'] });
1287
+ const at = _defaultAt;
1288
+ if (at) {
1289
+ await new Promise<void>((resolve) => {
1290
+ const req = require('https').request({ hostname: 'gmail.googleapis.com', path: `/gmail/v1/users/me/messages/${encodeURIComponent(row.mid)}/modify`, method: 'POST', headers: { 'Authorization': `Bearer ${at}`, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } }, (r: any) => { r.resume(); r.on('end', resolve); });
1291
+ req.on('error', () => resolve()); req.write(body); req.end();
1292
+ });
1293
+ }
1294
+ await rWrite2(`MATCH (e:Email {messageId: $mid}) SET e.labels = e.labels + ['TRASH']`, { mid: row.mid });
1295
+ } catch (e: any) { console.warn(`[warm-tick] SP5 block_sender trash failed for ${row.mid}: ${e.message}`); }
1296
+ }
1297
+ console.info(`[warm-tick] SP5 auto-trashed ${newEmails.length} email(s) from blocked senders`);
1298
+ }
1299
+ }
1300
+ } catch (e: any) { console.warn(`[warm-tick] SP5 block sender error: ${e.message}`); }
1181
1301
  } catch (err: any) {
1182
1302
  console.debug('[warm-tick] mental-model maintenance failed (non-fatal):', err?.message || String(err));
1183
1303
  } finally {
@@ -1296,6 +1416,50 @@ export async function runScheduledMaintenance(): Promise<void> {
1296
1416
  const counts: Record<string, number> = {};
1297
1417
  for (const r of trajResults) counts[r.direction] = (counts[r.direction] || 0) + 1;
1298
1418
  process.stderr.write(`[warm-tick] trajectory-detection: ${trajResults.length} contacts, distribution=${JSON.stringify(counts)}\n`);
1419
+
1420
+ // EN5: Snapshot prevCompositeStrength before trajectory overwrites it
1421
+ try {
1422
+ const { rawWrite: _rawWrite } = require('../../lib/safe-memgraph.js');
1423
+ await _rawWrite(
1424
+ "MATCH ()-[k:KNOWS]->() WHERE k.compositeStrength IS NOT NULL SET k.prevCompositeStrength = k.compositeStrength",
1425
+ {}
1426
+ );
1427
+ } catch (err) {
1428
+ process.stderr.write(`[warm-tick] warm-tick: prevCompositeStrength snapshot failed: ${err}\n`);
1429
+ }
1430
+
1431
+ // EN1b: computeTrajectories (trajectoryVelocity) — weekly
1432
+ try {
1433
+ const { computeTrajectories } = await import('../../lib/triage-core/mental-model/strength-tracker.js');
1434
+ await computeTrajectories();
1435
+ process.stderr.write('[warm-tick] warm-tick: trajectoryVelocity computed\n');
1436
+ } catch (err) {
1437
+ process.stderr.write(`[warm-tick] warm-tick: computeTrajectories failed: ${err}\n`);
1438
+ }
1439
+
1440
+ // EN2: clusteringCoefficient — weekly MAGE call
1441
+ try {
1442
+ const { rawWrite: _rawWrite } = require('../../lib/safe-memgraph.js');
1443
+ await _rawWrite(
1444
+ 'MATCH (p:Person) CALL local_clustering_coefficient.get() YIELD node, clustering_coefficient WHERE node = p SET p.clusteringCoefficient = clustering_coefficient',
1445
+ {}
1446
+ );
1447
+ process.stderr.write('[warm-tick] warm-tick: clusteringCoefficient computed\n');
1448
+ } catch (err) {
1449
+ process.stderr.write(`[warm-tick] warm-tick: local_clustering_coefficient unavailable: ${err}\n`);
1450
+ }
1451
+
1452
+ // 7D: computeGraphRanksSQL — PageRank + Louvain on SQLite graph (weekly, non-blocking)
1453
+ try {
1454
+ const { computeGraphRanksSQL } = await import('../../lib/triage-core/graph/graph-rank-sql.js');
1455
+ const rankResult = await Promise.race([
1456
+ computeGraphRanksSQL(),
1457
+ new Promise<never>((_, reject) => trackTimer(setTimeout(() => reject(new Error('computeGraphRanksSQL timeout (10min)')), 600000))),
1458
+ ]);
1459
+ process.stderr.write(`[warm-tick] computeGraphRanksSQL: personCount=${rankResult.personCount} edgeCount=${rankResult.edgeCount}\n`);
1460
+ } catch (err: any) {
1461
+ process.stderr.write(`[warm-tick] computeGraphRanksSQL failed (non-fatal): ${err?.message || err}\n`);
1462
+ }
1299
1463
  } catch (err: any) {
1300
1464
  console.debug('[warm-tick] favee-snapshots/trajectory failed (non-fatal):', err?.message || String(err));
1301
1465
  } finally {