@cgh567/agent 2.4.3 → 2.4.5

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 (141) 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-wrapper.sh +4 -1
  9. package/bin/helios-rpc.js +19 -0
  10. package/daemon/adapters/helios-rpc-adapter.js +5 -12
  11. package/daemon/context-enrichment.js +27 -0
  12. package/daemon/helios-api.js +310 -58
  13. package/daemon/helios-company-daemon.js +179 -53
  14. package/daemon/lib/blast-radius-analyzer.js +75 -0
  15. package/daemon/lib/domain-bootstrap-orchestrator.js +267 -0
  16. package/daemon/lib/forensic-log.js +113 -0
  17. package/daemon/lib/goal-research-pipeline.js +644 -0
  18. package/daemon/lib/harada/cascade-judge.js +84 -1
  19. package/daemon/lib/harada/cascade-research-dispatcher.js +282 -0
  20. package/daemon/lib/harada/pillar-dispatcher.js +23 -2
  21. package/daemon/lib/hbo-bridge.js +73 -5
  22. package/daemon/lib/headroom-middleware.js +129 -0
  23. package/daemon/lib/headroom-proxy-manager.js +319 -0
  24. package/daemon/lib/intelligence/department-page-generator.js +46 -1
  25. package/daemon/lib/interpretation-engine.js +92 -0
  26. package/daemon/lib/mental-model-cache.js +96 -0
  27. package/daemon/lib/project-factory.js +47 -0
  28. package/daemon/lib/session-log-reader.js +93 -0
  29. package/daemon/lib/standard-work-bootstrap.js +87 -1
  30. package/daemon/lib/task-completion-processor.js +12 -0
  31. package/daemon/package.json +2 -1
  32. package/daemon/routes/agents.js +51 -6
  33. package/daemon/routes/channels.js +116 -2
  34. package/daemon/routes/crm.js +85 -0
  35. package/daemon/routes/dashboard.js +62 -16
  36. package/daemon/routes/dept.js +10 -1
  37. package/daemon/routes/email-triage.js +19 -10
  38. package/daemon/routes/hbo.js +367 -13
  39. package/daemon/routes/hed.js +133 -0
  40. package/daemon/routes/inbox.js +466 -10
  41. package/daemon/routes/project.js +392 -9
  42. package/daemon/schema-definitions.js +10 -0
  43. package/daemon/schema-migrations-hbo.js +10 -0
  44. package/daemon/schema-migrations-proj.js +22 -0
  45. package/extensions/__tests__/codebase-index.test.ts +73 -0
  46. package/extensions/__tests__/extension-command-registration.test.ts +35 -0
  47. package/extensions/__tests__/git-push-guard.test.ts +68 -0
  48. package/extensions/context-compaction.ts +104 -76
  49. package/extensions/cortex/__tests__/cortex-core.test.ts +100 -0
  50. package/extensions/email/actions/draft-response.ts +21 -1
  51. package/extensions/email/auth/accounts.ts +5 -11
  52. package/extensions/email/auth/inbox-dog.ts +5 -2
  53. package/extensions/email/backfill.ts +20 -13
  54. package/extensions/email/providers/gmail.ts +164 -0
  55. package/extensions/email/providers/google-calendar.ts +34 -5
  56. package/extensions/helios-browser/__tests__/browser-routing.test.ts +57 -0
  57. package/extensions/helios-browser/backends/playwright.ts +3 -1
  58. package/extensions/helios-governance/__tests__/governance-gates.test.ts +40 -0
  59. package/extensions/helios-governance/__tests__/tournament-consumer.test.js +66 -0
  60. package/extensions/hema-dispatch-v3/headroom-compress.ts +103 -0
  61. package/extensions/hema-dispatch-v3/index.ts +33 -65
  62. package/extensions/interview/__tests__/server.test.ts +117 -0
  63. package/extensions/lib/helios-root.cjs +46 -0
  64. package/extensions/subagent-mesh/__tests__/handlers.test.ts +98 -0
  65. package/extensions/warm-tick/warm-tick-maintenance.ts +156 -0
  66. package/lib/__tests__/bulk-ingest.live.test.ts +66 -0
  67. package/lib/__tests__/crash-fixes.test.ts +49 -0
  68. package/lib/__tests__/maintenance-mission-wiring.test.ts +35 -0
  69. package/lib/broker/__tests__/jit-subscription.test.js +44 -1
  70. package/lib/broker/__tests__/lifecycle-channels.test.js +25 -1
  71. package/lib/compression/__tests__/ccr-store.test.js +138 -0
  72. package/lib/compression/__tests__/pipeline.test.js +280 -0
  73. package/lib/compression/__tests__/smart-crusher.test.js +242 -0
  74. package/lib/compression/dist/server.js +34 -1
  75. package/lib/compression/dist/start-server.js +77 -0
  76. package/lib/graph/learning/headroom-learn-bridge.js +175 -0
  77. package/lib/hbo-core-store.ts +71 -0
  78. package/lib/mission-loop/__tests__/research-handler.test.ts +143 -0
  79. package/lib/skill-sync.js +6 -1
  80. package/lib/startup-integrity.js +9 -2
  81. package/lib/triage-core/__tests__/classifier-fixture.test.ts +254 -0
  82. package/lib/triage-core/__tests__/classifier-post-norm.test.ts +1 -1
  83. package/lib/triage-core/__tests__/classifier.test.ts +45 -7
  84. package/lib/triage-core/__tests__/correction-detector.test.ts +36 -0
  85. package/lib/triage-core/__tests__/d6-dunbar-boost.test.ts +5 -5
  86. package/lib/triage-core/__tests__/orchestrator-pipeline.test.ts +107 -0
  87. package/lib/triage-core/__tests__/orchestrator.test.ts +113 -1
  88. package/lib/triage-core/__tests__/signals.test.ts +357 -0
  89. package/lib/triage-core/__tests__/sql-parity.test.ts +216 -0
  90. package/lib/triage-core/backfill-cost-estimator.ts +91 -0
  91. package/lib/triage-core/backfill-orchestrator.ts +119 -0
  92. package/lib/triage-core/classifier.ts +38 -6
  93. package/lib/triage-core/cos/cross-channel-escalation.ts +2 -3
  94. package/lib/triage-core/cos/response-debt.ts +2 -2
  95. package/lib/triage-core/graph/__tests__/batch-persistence.test.ts +283 -0
  96. package/lib/triage-core/graph/batch-persistence.ts +66 -2
  97. package/lib/triage-core/graph/betweenness-worker.js +75 -0
  98. package/lib/triage-core/graph/graph-rank-sql.ts +67 -0
  99. package/lib/triage-core/graph/persistence.ts +1 -1
  100. package/lib/triage-core/graph/schema-v2.ts +2 -0
  101. package/lib/triage-core/graph/schema.cypher +1 -0
  102. package/lib/triage-core/graph/triage-query.ts +1 -1
  103. package/lib/triage-core/learning.ts +15 -20
  104. package/lib/triage-core/mental-model/bedrock-config.ts +78 -0
  105. package/lib/triage-core/mental-model/cos-integration.ts +1 -1
  106. package/lib/triage-core/mental-model/entity-extractor.ts +51 -4
  107. package/lib/triage-core/mental-model/identity-resolver.ts +5 -5
  108. package/lib/triage-core/mental-model/model-assembler-sql.ts +200 -0
  109. package/lib/triage-core/mental-model/model-assembler.ts +16 -3
  110. package/lib/triage-core/orchestrator.ts +4 -4
  111. package/lib/triage-core/scheduled-sends.ts +39 -2
  112. package/lib/triage-core/signals/comms-style.ts +1 -1
  113. package/lib/triage-core/signals/cross-channel-escalation.ts +2 -2
  114. package/lib/triage-core/signals/favee-type.ts +6 -1
  115. package/lib/triage-core/signals/goal-relevance.ts +31 -2
  116. package/lib/triage-core/signals/personal-importance.ts +1 -1
  117. package/lib/triage-core/signals/referral-chain.ts +0 -1
  118. package/lib/triage-core/signals/relationship-decay.ts +4 -0
  119. package/lib/triage-core/signals/relationship-health.ts +6 -1
  120. package/lib/triage-core/signals/trajectory-signal.ts +38 -3
  121. package/lib/triage-core/tournament-runner.js +11 -1
  122. package/lib/triage-core/triage-llm-factory.ts +110 -0
  123. package/lib/triage-core/triage-local-llm.ts +145 -0
  124. package/lib/triage-core/triage-sql-store.ts +337 -0
  125. package/lib/triage-core/types.ts +2 -2
  126. package/lib/unified-graph.atomic.test.ts +52 -0
  127. package/lib/unified-graph.failure-categories.test.ts +55 -0
  128. package/package.json +10 -3
  129. package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
  130. package/prebuilds/linux-x64/better_sqlite3.node +0 -0
  131. package/prebuilds/win32-x64/better_sqlite3.node +0 -0
  132. package/skills/helios-bookkeeping/SKILL.md +321 -0
  133. package/skills/helios-briefer/SKILL.md +44 -0
  134. package/skills/helios-client-relations/SKILL.md +322 -0
  135. package/skills/helios-personal-triager/SKILL.md +45 -0
  136. package/skills/helios-recruitment/SKILL.md +317 -0
  137. package/skills/helios-relationship-nudger/SKILL.md +77 -0
  138. package/skills/helios-researcher/SKILL.md +44 -0
  139. package/skills/helios-scheduler/SKILL.md +58 -0
  140. package/skills/helios-tax-analyst/SKILL.md +280 -0
  141. package/lib/triage-core/orchestrator.ts.bak-r005-r006-r008 +0 -1823
@@ -0,0 +1,319 @@
1
+ 'use strict';
2
+ /**
3
+ * daemon/lib/headroom-proxy-manager.js
4
+ *
5
+ * Singleton process manager for the Helios Compression Server
6
+ * (lib/compression/dist/server.js).
7
+ *
8
+ * Responsibilities:
9
+ * - Find a free TCP port in range 8787–8796
10
+ * - Spawn server.js as a child process (no shell — cross-platform)
11
+ * - Wait for the {"event":"ready",...} JSON line on child stdout
12
+ * - Expose getBaseUrl() so all call sites can reach the server
13
+ * - Auto-restart on crash with exponential backoff (1s → 2s → ... → 30s)
14
+ * - Log lifecycle events to daemon/headroom-proxy.log (append)
15
+ *
16
+ * Interface (matches all call sites in helios-api.js, helios-company-daemon.js,
17
+ * helios-rpc-adapter.js, planning.ts):
18
+ * HeadroomProxyManager.getInstance(): HeadroomProxyManager
19
+ * instance.start(): Promise<{baseUrl: string}>
20
+ * instance.getBaseUrl(): string | null
21
+ * instance.stop(): Promise<void>
22
+ *
23
+ * Cross-platform: uses process.execPath (Node binary), path.join, net.createServer.
24
+ * No hardcoded company IDs. Port is dynamic — injected via HEADROOM_PROXY_URL env.
25
+ */
26
+
27
+ const path = require('path');
28
+ const net = require('net');
29
+ const fs = require('fs');
30
+ const { spawn } = require('child_process');
31
+
32
+ // Path to the compiled compression server entry point.
33
+ // Relative from this file: daemon/lib/ → ../../lib/compression/dist/server.js
34
+ const SERVER_SCRIPT = path.join(__dirname, '..', '..', 'lib', 'compression', 'dist', 'server.js');
35
+
36
+ // Log file: daemon/headroom-proxy.log (same dir as daemon/, sibling of lib/)
37
+ const LOG_FILE = path.join(__dirname, '..', 'headroom-proxy.log');
38
+
39
+ // Port scan range — 50 ports gives headroom when multiple test suites run concurrently
40
+ // (vitest maxWorkers=4, each test file may start its own server instance).
41
+ //
42
+ // PRECONDITION (O2): HEADROOM_PORT_START must be set in process.env BEFORE the first
43
+ // require() of this module. PORT_START is a module-level constant evaluated once at
44
+ // load time. Setting the env var after require() has no effect on the running instance.
45
+ // Tests that need a custom range must: (1) set process.env.HEADROOM_PORT_START,
46
+ // (2) delete require.cache[managerPath], (3) require() again. See test files for pattern.
47
+ //
48
+ // Can be overridden via env for specific deployment constraints:
49
+ // HEADROOM_PORT_START=9000 to use a different range entirely.
50
+ const PORT_START = parseInt(process.env.HEADROOM_PORT_START || '8787', 10);
51
+ const PORT_END = PORT_START + 49; // 50 ports
52
+
53
+ // Backoff config (ms)
54
+ const BACKOFF_INITIAL = 1000;
55
+ const BACKOFF_MAX = 30000;
56
+ const READY_TIMEOUT = 15000; // 15s max wait for ready signal
57
+
58
+ // ── Helpers ───────────────────────────────────────────────────────────────────
59
+
60
+ /**
61
+ * Append a JSON line to the proxy log. Fail-open — never throws.
62
+ */
63
+ function appendLog(obj) {
64
+ try {
65
+ fs.appendFileSync(LOG_FILE, JSON.stringify({ ...obj, ts: Date.now() }) + '\n');
66
+ } catch (_) { /* non-critical */ }
67
+ }
68
+
69
+ /**
70
+ * Find the first free TCP port in [start, end].
71
+ * Returns a Promise<number> or rejects if none are free.
72
+ */
73
+ function findFreePort(start, end) {
74
+ return new Promise((resolve, reject) => {
75
+ let current = start;
76
+
77
+ function tryPort() {
78
+ if (current > end) {
79
+ reject(new Error(`No free port found in range ${start}–${end}`));
80
+ return;
81
+ }
82
+ const server = net.createServer();
83
+ server.unref();
84
+ server.listen(current, '127.0.0.1', () => {
85
+ const port = server.address().port;
86
+ server.close(() => resolve(port));
87
+ });
88
+ server.on('error', (err) => {
89
+ if (err.code === 'EADDRINUSE') {
90
+ current++;
91
+ tryPort();
92
+ } else {
93
+ reject(err);
94
+ }
95
+ });
96
+ }
97
+
98
+ tryPort();
99
+ });
100
+ }
101
+
102
+ // ── Singleton ─────────────────────────────────────────────────────────────────
103
+
104
+ let _instance = null;
105
+
106
+ class HeadroomProxyManager {
107
+ constructor() {
108
+ this._child = null;
109
+ this._baseUrl = null;
110
+ this._port = null;
111
+ this._stopped = false;
112
+ this._backoffMs = BACKOFF_INITIAL;
113
+ this._starting = false;
114
+ this._startPromise = null;
115
+ }
116
+
117
+ /**
118
+ * Returns the module-level singleton. Safe to call from multiple require()s.
119
+ */
120
+ static getInstance() {
121
+ if (!_instance) _instance = new HeadroomProxyManager();
122
+ return _instance;
123
+ }
124
+
125
+ /**
126
+ * Start the compression server. Idempotent — multiple calls return the same promise.
127
+ * Resolves with {baseUrl} once the server emits its ready signal.
128
+ * Rejects if server fails to start within READY_TIMEOUT ms.
129
+ */
130
+ start() {
131
+ // Already started
132
+ if (this._baseUrl) return Promise.resolve({ baseUrl: this._baseUrl });
133
+ // In-flight start — reuse same promise
134
+ if (this._startPromise) return this._startPromise;
135
+
136
+ this._startPromise = this._doStart().then((result) => {
137
+ this._startPromise = null;
138
+ return result;
139
+ }).catch((err) => {
140
+ this._startPromise = null;
141
+ throw err;
142
+ });
143
+
144
+ return this._startPromise;
145
+ }
146
+
147
+ /**
148
+ * Internal: find a port, spawn the server, wait for ready.
149
+ */
150
+ async _doStart() {
151
+ this._stopped = false;
152
+
153
+ // Verify server script exists
154
+ if (!fs.existsSync(SERVER_SCRIPT)) {
155
+ throw new Error(`Compression server not found at ${SERVER_SCRIPT}`);
156
+ }
157
+
158
+ const port = await findFreePort(PORT_START, PORT_END);
159
+ this._port = port;
160
+
161
+ return new Promise((resolve, reject) => {
162
+ const timer = setTimeout(() => {
163
+ reject(new Error(`Compression server did not emit ready within ${READY_TIMEOUT}ms on port ${port}`));
164
+ if (this._child) {
165
+ try { this._child.kill(); } catch (_) {}
166
+ this._child = null;
167
+ }
168
+ }, READY_TIMEOUT);
169
+
170
+ const child = spawn(
171
+ process.execPath,
172
+ [SERVER_SCRIPT],
173
+ {
174
+ stdio : ['ignore', 'pipe', 'pipe'],
175
+ env : { ...process.env, HEADROOM_PORT: String(port) },
176
+ shell : false,
177
+ }
178
+ );
179
+
180
+ this._child = child;
181
+
182
+ let stdoutBuf = '';
183
+ child.stdout.on('data', (chunk) => {
184
+ stdoutBuf += chunk.toString('utf-8');
185
+ const lines = stdoutBuf.split('\n');
186
+ stdoutBuf = lines.pop(); // keep partial line
187
+
188
+ for (const line of lines) {
189
+ if (!line.trim()) continue;
190
+ try {
191
+ const parsed = JSON.parse(line);
192
+ if (parsed.event === 'ready') {
193
+ clearTimeout(timer);
194
+ this._baseUrl = `http://127.0.0.1:${parsed.port || port}`;
195
+ this._backoffMs = BACKOFF_INITIAL; // reset backoff on successful start
196
+ appendLog({ event: 'ready', port: parsed.port || port, pid: child.pid });
197
+ resolve({ baseUrl: this._baseUrl });
198
+ }
199
+ } catch (_) { /* non-JSON stdout line — skip */ }
200
+ }
201
+ });
202
+
203
+ child.stderr.on('data', (chunk) => {
204
+ // Forward compression server stderr to our stderr for visibility
205
+ process.stderr.write(`[compression-server] ${chunk.toString('utf-8')}`);
206
+ });
207
+
208
+ child.on('error', (err) => {
209
+ clearTimeout(timer);
210
+ appendLog({ event: 'spawn_error', error: err.message });
211
+ reject(err);
212
+ });
213
+
214
+ child.on('exit', (code, signal) => {
215
+ // On Windows, Node.js reports abnormal OS termination as exit code 4294967295
216
+ // (0xFFFFFFFF — the unsigned 32-bit representation of -1).
217
+ // This indicates an OS-level process termination (e.g. OOM, crash).
218
+ if (code === 4294967295) {
219
+ process.stderr.write(
220
+ `[headroom-proxy-manager] WARN: compression server on port ${port} exited with abnormal code 0xFFFFFFFF (-1) — ` +
221
+ `likely an OOM kill or unhandled exception in the server process. ` +
222
+ `Scheduling restart with backoff.\n`
223
+ );
224
+ }
225
+ appendLog({ event: 'exit', code, signal, port });
226
+ this._child = null;
227
+ this._baseUrl = null;
228
+
229
+ // Schedule restart unless intentionally stopped
230
+ if (!this._stopped) {
231
+ const delay = this._backoffMs;
232
+ this._backoffMs = Math.min(this._backoffMs * 2, BACKOFF_MAX);
233
+ appendLog({ event: 'restart_scheduled', delayMs: delay });
234
+ setTimeout(() => {
235
+ if (!this._stopped) {
236
+ this._doStart().catch((restartErr) => {
237
+ process.stderr.write(
238
+ `[headroom-proxy-manager] Restart failed: ${restartErr.message}\n`
239
+ );
240
+ });
241
+ }
242
+ }, delay);
243
+ }
244
+ });
245
+ });
246
+ }
247
+
248
+ /**
249
+ * Returns the base URL of the running compression server, or null if not running.
250
+ * Never throws.
251
+ */
252
+ getBaseUrl() {
253
+ return this._baseUrl;
254
+ }
255
+
256
+ /**
257
+ * Stop the compression server and disable auto-restart.
258
+ *
259
+ * Awaits the child process exit event (with a 3s safety-net timeout) before
260
+ * resolving. This eliminates the H2 race where the old child still holds the
261
+ * TCP port while a subsequent start() call runs findFreePort.
262
+ *
263
+ * Safe to call multiple times — idempotent after the first call.
264
+ */
265
+ async stop() {
266
+ this._stopped = true;
267
+ this._baseUrl = null;
268
+ const child = this._child;
269
+ this._child = null;
270
+
271
+ if (child) {
272
+ await new Promise((resolve) => {
273
+ // Safety net: if exit never fires (e.g. zombie process), resolve after 3s
274
+ // so callers are never blocked indefinitely.
275
+ const timeout = setTimeout(resolve, 3000);
276
+ timeout.unref?.(); // don't hold the event loop open
277
+
278
+ child.once('exit', () => {
279
+ clearTimeout(timeout);
280
+ resolve();
281
+ });
282
+
283
+ // Send the kill signal. kill() may throw on Windows if the process
284
+ // has already exited — catch and resolve immediately in that case.
285
+ try {
286
+ child.kill();
287
+ } catch (_) {
288
+ clearTimeout(timeout);
289
+ resolve();
290
+ }
291
+ });
292
+ }
293
+
294
+ appendLog({ event: 'stopped' });
295
+ }
296
+ }
297
+
298
+ /**
299
+ * buildCompressionEnv() — returns the env fragment to inject into Pi subprocess spawns.
300
+ *
301
+ * Called from helios-rpc-adapter.js when building the subprocess env object.
302
+ * Exported here so it can be tested directly — the test starts a real proxy manager,
303
+ * calls this function, and asserts HEADROOM_PROXY_URL is set to the real baseUrl.
304
+ *
305
+ * Returns { HEADROOM_PROXY_URL: string } when the proxy is running,
306
+ * or {} when the proxy is not started / getBaseUrl() is null.
307
+ * Never throws.
308
+ */
309
+ function buildCompressionEnv() {
310
+ try {
311
+ const baseUrl = HeadroomProxyManager.getInstance().getBaseUrl();
312
+ if (baseUrl) {
313
+ return { HEADROOM_PROXY_URL: baseUrl };
314
+ }
315
+ } catch (_) {}
316
+ return {};
317
+ }
318
+
319
+ module.exports = { HeadroomProxyManager, buildCompressionEnv };
@@ -327,9 +327,54 @@ function makeDepartmentPageGenerator({ mgQuery, broadcast }) {
327
327
 
328
328
  rawData = await deptModule.fetchData(companyId);
329
329
 
330
+ // ── Step 1b: Harada cascade enrichment ───────────────────────────────────────
331
+ // Inject GoalPillar cascade progress for this department so the LLM has
332
+ // real strategic context even when department-specific signals are sparse.
333
+ // This is the primary data source for "Work in Progress" and "Decisions & Asks".
334
+ const cascadeRows = await mgQuery(
335
+ `MATCH (gp:GoalPillar {companyId: $cid})
336
+ WHERE gp.department = $dept
337
+ OR toLower(gp.name) CONTAINS toLower($dept)
338
+ OPTIONAL MATCH (gp)-[:HAS_CELL]->(ac:ActionCell)
339
+ OPTIONAL MATCH (t:Task {companyId: $cid})
340
+ WHERE t.pillarId = gp.id
341
+ RETURN gp.id AS pillarId, gp.name AS pillarName,
342
+ gp.l2Strategy AS l2Strategy, gp.l2Content AS l2Content,
343
+ gp.l2ReviewStatus AS l2ReviewStatus,
344
+ count(DISTINCT ac) AS totalCells,
345
+ count(DISTINCT CASE WHEN ac.status = 'closed' THEN ac END) AS closedCells,
346
+ collect(DISTINCT {id: t.id, title: t.title, status: t.status, originKind: t.originKind})[..5] AS recentTasks
347
+ ORDER BY gp.createdAt DESC LIMIT 3`,
348
+ { cid: companyId, dept: department }
349
+ ).catch(() => null);
350
+
351
+ const cascadeArr = cascadeRows && cascadeRows.rows
352
+ ? cascadeRows.rows
353
+ : (Array.isArray(cascadeRows) ? cascadeRows : []);
354
+
355
+ if (cascadeArr.length > 0) {
356
+ const pillarSummary = cascadeArr.map((r, i) => {
357
+ const row = Array.isArray(r) ? {
358
+ pillarName: r[1], l2Strategy: r[2], l2Content: r[3], l2ReviewStatus: r[4],
359
+ totalCells: r[5], closedCells: r[6], recentTasks: r[7],
360
+ } : r;
361
+ const progress = row.totalCells > 0
362
+ ? `${row.closedCells}/${row.totalCells} cells closed`
363
+ : 'no action cells yet';
364
+ const status = row.l2ReviewStatus ? `review: ${row.l2ReviewStatus}` : 'pending review';
365
+ return `${row.pillarName}: ${status}, ${progress}. Strategy: ${row.l2Strategy || row.l2Content || '(not yet defined)'}`;
366
+ }).join('\n');
367
+ rawData._haradaCascadeSummary = pillarSummary;
368
+ }
369
+
330
370
  // ── Step 2: Build base prompt ────────────────────────────────────────────────
331
371
  const basePrompt = deptModule.buildPrompt(rawData, today);
332
372
 
373
+ // ── Step 2a: Harada cascade context block ────────────────────────────────────
374
+ const cascadeBlock = rawData._haradaCascadeSummary
375
+ ? `\nStrategic cascade progress (Hoshin Kanri):\n${rawData._haradaCascadeSummary}\n`
376
+ : '';
377
+
333
378
  // ── Step 2b: Prior learnings priming block ────────────────────────────
334
379
  const priorLearningsRaw = await mgQuery(
335
380
  `MATCH (l:DeptLearning {companyId: $cid, department: $dept})
@@ -356,7 +401,7 @@ Additionally, include these fields in your JSON output:
356
401
  "learningsStructured": [{"text": "string", "type": "observation|principle|required_change"}]
357
402
  `;
358
403
 
359
- const prompt = basePrompt + primingBlock + additionalOutputSchema;
404
+ const prompt = basePrompt + cascadeBlock + primingBlock + additionalOutputSchema;
360
405
 
361
406
  // ── Step 3: Call LLM ───────────────────────────────────────────────────
362
407
  const llmText = await callLLM(prompt, MAX_TOKENS);
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+ /**
3
+ * interpretation-engine.js — P_EXPLAIN interpretation loop
4
+ * Creates HITLInteraction nodes for new tasks and queues agent explanation.
5
+ *
6
+ * API contract: rawWriteFn is the safe-memgraph rawWrite function
7
+ * (exported as `{ rawWrite }` from lib/safe-memgraph.js), not an object.
8
+ * It is called as: rawWriteFn(cypher, params)
9
+ *
10
+ * Retry policy (inline CJS — no p-retry dependency):
11
+ * 3 attempts, exponential backoff 500ms→8000ms, randomised ±25%.
12
+ * Every failed attempt logs [hitl_write_retry] at warn level.
13
+ * Permanent failure (all attempts exhausted) logs [hitl_write_failed_permanently] at error level.
14
+ */
15
+
16
+ const _logger = (() => { try { return require('../lib/logger.js'); } catch { return null; } })();
17
+ const log = (lvl, msg, meta) => {
18
+ const fn = _logger?.[lvl === 'error' ? 'error' : 'warn'];
19
+ const text = `[${msg}]${meta ? ' ' + JSON.stringify(meta) : ''}`;
20
+ if (fn) fn(text);
21
+ else console[lvl === 'error' ? 'error' : 'warn'](`[${lvl}] ${msg}`, meta ?? '');
22
+ };
23
+
24
+ /**
25
+ * Inline exponential-backoff retry — CJS, no external deps.
26
+ * @param {() => Promise<void>} fn — async function to retry
27
+ * @param {string} label — structured log event name prefix
28
+ * @param {{ retries?: number, minTimeout?: number, maxTimeout?: number }} [opts]
29
+ */
30
+ async function withRetry(fn, label, opts = {}) {
31
+ const retries = opts.retries ?? 3;
32
+ const minTimeout = opts.minTimeout ?? 500;
33
+ const maxTimeout = opts.maxTimeout ?? 8000;
34
+
35
+ let lastErr;
36
+ for (let attempt = 1; attempt <= retries + 1; attempt++) {
37
+ try {
38
+ return await fn();
39
+ } catch (err) {
40
+ lastErr = err;
41
+ const retriesLeft = retries + 1 - attempt;
42
+ if (retriesLeft === 0) break;
43
+
44
+ // Exponential backoff with ±25% jitter
45
+ const base = Math.min(minTimeout * Math.pow(2, attempt - 1), maxTimeout);
46
+ const jitter = base * 0.25 * (Math.random() * 2 - 1);
47
+ const delay = Math.max(minTimeout, Math.round(base + jitter));
48
+
49
+ log('warn', 'hitl_write_retry', {
50
+ event: `${label}_retry`,
51
+ attempt,
52
+ retriesLeft,
53
+ delayMs: delay,
54
+ err: err instanceof Error ? err.message : String(err),
55
+ });
56
+ await new Promise(r => setTimeout(r, delay));
57
+ }
58
+ }
59
+
60
+ log('error', 'hitl_write_failed_permanently', {
61
+ event: `${label}_failed_permanently`,
62
+ err: lastErr instanceof Error ? lastErr.message : String(lastErr),
63
+ });
64
+ throw lastErr;
65
+ }
66
+
67
+ async function createInterpretationExplanation(taskId, companyId, rawWriteFn) {
68
+ if (!rawWriteFn || typeof rawWriteFn !== 'function' || !taskId || !companyId) return;
69
+ const hiId = `hitl:interp:${taskId}:${Date.now()}`;
70
+ await withRetry(
71
+ () => rawWriteFn(
72
+ "MERGE (h:HITLInteraction {id: $hiId}) SET h.taskId = $taskId, h.companyId = $companyId, h.kind = 'interpretation', h.status = 'pending', h.createdAt = datetime()",
73
+ { hiId, taskId, companyId }
74
+ ),
75
+ 'hitl_interp_create'
76
+ );
77
+ }
78
+
79
+ async function resolveInterpretation(hiId, taskId, action, body, rawWriteFn) {
80
+ if (!rawWriteFn || typeof rawWriteFn !== 'function' || !hiId) return;
81
+ await rawWriteFn(
82
+ "MATCH (h:HITLInteraction {id: $hiId}) SET h.status = 'resolved', h.action = $action, h.resolvedAt = datetime()",
83
+ { hiId, action }
84
+ );
85
+ const commentId = `sys:interp:${hiId}`;
86
+ await rawWriteFn(
87
+ "MERGE (c:Comment {id: $commentId}) SET c.taskId = $taskId, c.body = $body, c.author = 'system', c.createdAt = datetime()",
88
+ { commentId, taskId, body: body || `Interpretation ${action}` }
89
+ );
90
+ }
91
+
92
+ module.exports = { createInterpretationExplanation, resolveInterpretation };
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+ /**
3
+ * mental-model-cache.js — CachedPersonModel node CRUD
4
+ * Pre-assembled mental models for fast agent brief injection.
5
+ * Cache is invalidated by enrichmentNeeded flag on Person node.
6
+ */
7
+
8
+ const SCHEMA_VERSION = 1;
9
+
10
+ /**
11
+ * writeCachedModel — upsert a CachedPersonModel node for a person in a company.
12
+ *
13
+ * @param {Function} mgQuery — Memgraph query function (async)
14
+ * @param {string} personId — Person node id
15
+ * @param {string} companyId — Company node id
16
+ * @param {object} assembledModel — Output from assembleMentalModel()
17
+ */
18
+ async function writeCachedModel(mgQuery, personId, companyId, assembledModel) {
19
+ const modelId = `model:${companyId}:${personId}`;
20
+ const contextBrief = assembledModel.contextBrief || '';
21
+ const faveeJson = JSON.stringify(assembledModel.favee || {});
22
+ const topicsJson = JSON.stringify((assembledModel.activeTopics || []).slice(0, 10));
23
+ const questionsJson = JSON.stringify((assembledModel.openQuestions || []).slice(0, 5));
24
+ const completeness = contextBrief ? 'full' : (faveeJson !== '{}' ? 'partial' : 'none');
25
+
26
+ await mgQuery(
27
+ `MERGE (m:CachedPersonModel {id: $id})
28
+ ON CREATE SET
29
+ m.personId = $personId, m.companyId = $companyId,
30
+ m.contextBrief = $brief, m.faveeJson = $favee,
31
+ m.topicsJson = $topics, m.questionsJson = $questions,
32
+ m.assembledAt = localdatetime(), m.completeness = $completeness,
33
+ m.schemaVersion = $sv
34
+ ON MATCH SET
35
+ m.contextBrief = $brief, m.faveeJson = $favee,
36
+ m.topicsJson = $topics, m.questionsJson = $questions,
37
+ m.assembledAt = localdatetime(), m.completeness = $completeness,
38
+ m.dirtyAt = null
39
+ WITH m
40
+ MATCH (p:Person {id: $personId})
41
+ MERGE (p)-[:HAS_CACHED_MODEL]->(m)`,
42
+ { id: modelId, personId, companyId, brief: contextBrief,
43
+ favee: faveeJson, topics: topicsJson, questions: questionsJson,
44
+ completeness, sv: SCHEMA_VERSION }
45
+ );
46
+ }
47
+
48
+ /**
49
+ * invalidateCache — mark a CachedPersonModel dirty so next read triggers a refresh.
50
+ * Non-fatal: silently ignores errors if the model node doesn't exist yet.
51
+ *
52
+ * @param {Function} mgQuery
53
+ * @param {string} personId
54
+ * @param {string} companyId
55
+ */
56
+ async function invalidateCache(mgQuery, personId, companyId) {
57
+ const modelId = `model:${companyId}:${personId}`;
58
+ await mgQuery(
59
+ `MATCH (m:CachedPersonModel {id: $id}) SET m.dirtyAt = localdatetime()`,
60
+ { id: modelId }
61
+ ).catch(() => {});
62
+ }
63
+
64
+ /**
65
+ * getCachedModel — return a cached model if it exists, is not dirty, and is within maxAge.
66
+ * Returns null if the cache is cold, dirty, or expired.
67
+ *
68
+ * @param {Function} mgQuery
69
+ * @param {string} personId
70
+ * @param {string} companyId
71
+ * @param {number} maxAgeMinutes — default 60 minutes
72
+ * @returns {object|null}
73
+ */
74
+ async function getCachedModel(mgQuery, personId, companyId, maxAgeMinutes = 60) {
75
+ const modelId = `model:${companyId}:${personId}`;
76
+ const rows = await mgQuery(
77
+ `MATCH (m:CachedPersonModel {id: $id})
78
+ WHERE m.dirtyAt IS NULL
79
+ AND m.assembledAt > localdatetime() - duration({minutes: $maxAge})
80
+ RETURN m.contextBrief, m.faveeJson, m.topicsJson, m.questionsJson,
81
+ m.assembledAt, m.completeness`,
82
+ { id: modelId, maxAge: maxAgeMinutes }
83
+ );
84
+ if (!rows?.rows?.length) return null;
85
+ const r = rows.rows[0];
86
+ return {
87
+ contextBrief: r[0],
88
+ faveeJson: r[1],
89
+ topicsJson: r[2],
90
+ questionsJson: r[3],
91
+ assembledAt: r[4],
92
+ completeness: r[5],
93
+ };
94
+ }
95
+
96
+ module.exports = { writeCachedModel, invalidateCache, getCachedModel, SCHEMA_VERSION };
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+ /**
3
+ * project-factory.js — Creates HeliosProject + ProjectDocument nodes when
4
+ * a GoalPillar is initialized via tickGoalDecompose().
5
+ *
6
+ * Called from daemon/lib/hbo-bridge.js tickGoalDecompose() after
7
+ * mandala.initializeMandala() succeeds for each GoalPillar.
8
+ *
9
+ * ID conventions:
10
+ * HeliosProject: proj:<companyId>:<pillarSlug>
11
+ * ProjectDocument: pdoc:<projectId>:main
12
+ */
13
+
14
+ async function ensureProject(mgQuery, companyId, goalId, pillarId, pillarTitle) {
15
+ const slug = pillarId.replace(/[^a-z0-9]/gi, '-').toLowerCase().slice(0, 40);
16
+ const projId = `proj:${companyId}:${slug}`;
17
+ const docId = `pdoc:${projId}:main`;
18
+
19
+ await mgQuery(
20
+ `MERGE (p:HeliosProject {id: $projId})
21
+ ON CREATE SET
22
+ p.companyId = $cid,
23
+ p.goalId = $goalId,
24
+ p.pillarId = $pillarId,
25
+ p.name = $name,
26
+ p.status = 'planning',
27
+ p.phase = 'planning',
28
+ p.createdAt = datetime()`,
29
+ { projId, cid: companyId, goalId, pillarId, name: pillarTitle }
30
+ ).catch(() => {});
31
+
32
+ await mgQuery(
33
+ `MERGE (d:ProjectDocument {id: $docId})
34
+ ON CREATE SET
35
+ d.projectId = $projId,
36
+ d.purpose = '',
37
+ d.approach = '',
38
+ d.successCriteria = '[]',
39
+ d.intentAnchor = '',
40
+ d.exclusions = '[]',
41
+ d.version = toInteger(1),
42
+ d.updatedAt = datetime()`,
43
+ { docId, projId }
44
+ ).catch(() => {});
45
+ }
46
+
47
+ module.exports = { ensureProject };