@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
@@ -390,6 +390,35 @@ Market benchmarks: ${benchmarks}`;
390
390
 
391
391
  this._log(`[CascadeJudge] L2 verdict for pillar ${pillarId}: ${verdict}${critique ? ' — ' + critique : ''}`);
392
392
 
393
+ // T3-06: On PASS, dispatch L3 research for all unreviewed ActionCells in this pillar.
394
+ if (verdict === 'pass') {
395
+ setImmediate(async () => {
396
+ try {
397
+ const cells = await this._mg(
398
+ `MATCH (ac:ActionCell {pillarId: $pillarId, companyId: $cid})
399
+ WHERE ac.l3ReviewStatus IS NULL OR ac.l3ReviewStatus = ''
400
+ RETURN ac.id AS id, ac.agentId AS agentId
401
+ LIMIT 8`,
402
+ { pillarId, cid: this._companyId }
403
+ ).catch(() => null);
404
+ if (!cells?.rows?.length) return;
405
+ const { CascadeResearchDispatcher } = require('./cascade-research-dispatcher');
406
+ const crd = new CascadeResearchDispatcher(this._mg.bind(this), this._companyId);
407
+ for (const cr of cells.rows) {
408
+ const cellId = cr[0] ?? cr['id'];
409
+ const cellAgentId = cr[1] ?? cr['agentId'];
410
+ if (!cellId) continue;
411
+ await crd.dispatchL3Research(cellId, pillarId, this._companyId, cellAgentId || null).catch(e =>
412
+ this._log(`[CascadeJudge] T3-06: L3 dispatch failed for cell ${cellId}: ${e && e.message || e}`)
413
+ );
414
+ }
415
+ this._log(`[CascadeJudge] T3-06: dispatched L3 research for ${cells.rows.length} ActionCells of pillar ${pillarId}`);
416
+ } catch (t3Err) {
417
+ this._log(`[CascadeJudge] T3-06: L3 dispatch error: ${t3Err && t3Err.message || t3Err}`);
418
+ }
419
+ });
420
+ }
421
+
393
422
  // 7/9. Return verdict — caller dispatches L3 (pass) or revised L2 (fail)
394
423
  return { verdict, criteriaResults, critique };
395
424
  } catch (err) {
@@ -607,12 +636,66 @@ Sub-agent execution plan: ${l3Content}`;
607
636
 
608
637
  this._log(`[CascadeJudge] L3 verdict for cell ${cellId}: ${verdict}${critique ? ' — ' + critique : ''}`);
609
638
 
610
- return { verdict, criteriaResults, critique };
639
+ return { verdict, criteriaResults, critique };
611
640
  } catch (err) {
612
641
  this._log(`[CascadeJudge] judgeL3 error for cell ${cellId}: ${err && err.message || err}`);
613
642
  throw err;
614
643
  }
615
644
  }
645
+
646
+ /**
647
+ * T3-04: Bulk scan — judge all GoalPillar nodes with l2ReviewStatus='pending_review'.
648
+ * Called every 5 ticks from helios-company-daemon.js tick loop.
649
+ * Processes up to 5 pillars per tick to avoid blocking.
650
+ */
651
+ async judgeReadyPillars() {
652
+ const cid = this._companyId;
653
+ try {
654
+ const result = await this._mg(
655
+ `MATCH (gp:GoalPillar {companyId: $cid})
656
+ WHERE gp.l2ReviewStatus = 'pending_review'
657
+ RETURN gp.id AS id
658
+ LIMIT 5`,
659
+ { cid }
660
+ ).catch(() => null);
661
+ for (const row of (result?.rows ?? [])) {
662
+ const pillarId = row[0] ?? row['id'];
663
+ if (!pillarId) continue;
664
+ await this.judgeL2(pillarId).catch(e =>
665
+ this._log(`[CascadeJudge] judgeReadyPillars: judgeL2 failed for ${pillarId}: ${e && e.message || e}`)
666
+ );
667
+ }
668
+ } catch (err) {
669
+ this._log(`[CascadeJudge] judgeReadyPillars error: ${err && err.message || err}`);
670
+ }
671
+ }
672
+
673
+ /**
674
+ * T3-04: Bulk scan — judge all ActionCell nodes with l3ReviewStatus='pending_review'.
675
+ * Called every 5 ticks from helios-company-daemon.js tick loop.
676
+ * Processes up to 5 cells per tick to avoid blocking.
677
+ */
678
+ async judgeReadyActionCells() {
679
+ const cid = this._companyId;
680
+ try {
681
+ const result = await this._mg(
682
+ `MATCH (ac:ActionCell {companyId: $cid})
683
+ WHERE ac.l3ReviewStatus = 'pending_review'
684
+ RETURN ac.id AS id
685
+ LIMIT 5`,
686
+ { cid }
687
+ ).catch(() => null);
688
+ for (const row of (result?.rows ?? [])) {
689
+ const cellId = row[0] ?? row['id'];
690
+ if (!cellId) continue;
691
+ await this.judgeL3(cellId).catch(e =>
692
+ this._log(`[CascadeJudge] judgeReadyActionCells: judgeL3 failed for ${cellId}: ${e && e.message || e}`)
693
+ );
694
+ }
695
+ } catch (err) {
696
+ this._log(`[CascadeJudge] judgeReadyActionCells error: ${err && err.message || err}`);
697
+ }
698
+ }
616
699
  }
617
700
 
618
701
  module.exports = { CascadeJudge };
@@ -0,0 +1,282 @@
1
+ 'use strict';
2
+ /**
3
+ * cascade-research-dispatcher.js — Dispatches L2 and L3 research tasks
4
+ * for the Hoshin Kanri cascade with full parent context in the task body.
5
+ *
6
+ * L2: Dept head agent receives company goal + GoalResearchBrief + pillar domain
7
+ * L3: Sub-agent receives pillar L2 content + action cell description
8
+ */
9
+
10
+ class CascadeResearchDispatcher {
11
+ /**
12
+ * @param {Function} mgQuery - (cypher, params) => Promise<result>
13
+ * @param {string} companyId - Company identifier
14
+ * @param {Function} [log] - Optional logger function
15
+ */
16
+ constructor(mgQuery, companyId, log) {
17
+ this._mg = mgQuery;
18
+ this._companyId = companyId;
19
+ this._log = log || (() => {});
20
+ }
21
+
22
+ // ── L2 Research Dispatch ──────────────────────────────────────────────────
23
+
24
+ /**
25
+ * Dispatch a harada_l2_research task to the dept head agent for a pillar.
26
+ *
27
+ * @param {string} pillarId - GoalPillar node id
28
+ * @param {string} goalId - CompanyGoal node id
29
+ * @param {string} companyId - Company identifier (overrides constructor value if provided)
30
+ * @param {string} agentId - Assignee agent id
31
+ * @param {string} [l2ReviewCritique] - Previous CEO critique (for revision cycles)
32
+ * @returns {{ taskId: string, agentId: string }}
33
+ */
34
+ async dispatchL2Research(pillarId, goalId, companyId, agentId, l2ReviewCritique) {
35
+ const cid = companyId || this._companyId;
36
+
37
+ // 1. Fetch GoalPillar name, GoalResearchBrief, and CompanyGoal
38
+ const contextRows = await this._mg(
39
+ `MATCH (p:GoalPillar {id: $pillarId})
40
+ OPTIONAL MATCH (g:CompanyGoal {id: $goalId})
41
+ OPTIONAL MATCH (grb:GoalResearchBrief {companyId: $cid})
42
+ WHERE grb.goalId = $goalId OR grb.id STARTS WITH ('grb:' + $goalId)
43
+ RETURN
44
+ p.name AS pillarName,
45
+ g.title AS goalTitle,
46
+ g.description AS goalDescription,
47
+ grb.tournamentWinner AS tournamentWinner,
48
+ grb.marketContext AS marketContext,
49
+ grb.benchmarks AS benchmarks,
50
+ grb.crmContext AS crmContext
51
+ LIMIT 1`,
52
+ { pillarId, goalId, cid }
53
+ );
54
+
55
+ const row = contextRows && contextRows.rows && contextRows.rows[0]
56
+ ? (Array.isArray(contextRows.rows[0])
57
+ ? {
58
+ pillarName: contextRows.rows[0][0],
59
+ goalTitle: contextRows.rows[0][1],
60
+ goalDescription: contextRows.rows[0][2],
61
+ tournamentWinner: contextRows.rows[0][3],
62
+ marketContext: contextRows.rows[0][4],
63
+ benchmarks: contextRows.rows[0][5],
64
+ crmContext: contextRows.rows[0][6],
65
+ }
66
+ : contextRows.rows[0])
67
+ : {};
68
+
69
+ const pillarName = row.pillarName || pillarId;
70
+ const goalTitle = row.goalTitle || 'Company Goal';
71
+ const goalDescription = row.goalDescription || '';
72
+ const tournamentWinner = row.tournamentWinner || 'TBD';
73
+ const marketContext = row.marketContext || 'Not available';
74
+ const benchmarks = row.benchmarks || 'Not available';
75
+ const crmContext = row.crmContext || 'Not available';
76
+
77
+ // 2. Build task body
78
+ const previousCritiqueSection = l2ReviewCritique
79
+ ? `\nPrevious CEO critique: ${l2ReviewCritique}`
80
+ : '';
81
+
82
+ const taskBody = `Goal: ${goalTitle}
83
+ Description: ${goalDescription}
84
+ Research Brief Summary: ${marketContext} | Tournament winner: ${tournamentWinner}
85
+ Benchmarks: ${benchmarks}
86
+ CRM context: ${crmContext}
87
+ Your pillar: ${pillarName} (id: ${pillarId})
88
+ Your role: ${pillarName} execution lead
89
+
90
+ Research your pillar domain using web search and your available tools.
91
+ Produce:
92
+ 1. A 50-word purpose statement for this pillar
93
+ 2. A strategy label (≤20 chars) matching or improving on the tournament winner
94
+ 3. 3 key actions this pillar should execute
95
+
96
+ When complete, update pillar content by calling:
97
+ POST /api/hbo/pillar/${pillarId}/l2content
98
+ Body: { content: string, strategy: string, keyActions: string[] }
99
+ ${previousCritiqueSection}`;
100
+
101
+ const taskId = `harada-l2:${pillarId}`;
102
+
103
+ // 3. MERGE Task
104
+ await this._mg(
105
+ `MERGE (t:Task {id: $taskId, companyId: $cid})
106
+ ON CREATE SET
107
+ t.originKind = $originKind,
108
+ t.assigneeAgentId = $agentId,
109
+ t.status = 'todo',
110
+ t.priority = 1,
111
+ t.body = $body,
112
+ t.createdAt = datetime()
113
+ ON MATCH SET
114
+ t.body = $body,
115
+ t.status = 'todo',
116
+ t.updatedAt = datetime()`,
117
+ {
118
+ taskId,
119
+ cid,
120
+ originKind: 'harada_l2_research',
121
+ agentId,
122
+ body: taskBody,
123
+ }
124
+ );
125
+
126
+ // 4. Create AgentReadySignal
127
+ const arsId = `ars:harada-l2:${pillarId}`;
128
+ await this._mg(
129
+ `MERGE (s:AgentReadySignal {id: $arsId, companyId: $cid})
130
+ ON CREATE SET
131
+ s.agentId = $agentId,
132
+ s.status = 'pending',
133
+ s.createdAt = datetime()
134
+ ON MATCH SET
135
+ s.status = 'pending',
136
+ s.updatedAt = datetime()`,
137
+ { arsId, cid, agentId }
138
+ );
139
+
140
+ this._log(`[CascadeResearchDispatcher] Dispatched L2 research task ${taskId} to agent ${agentId}`);
141
+
142
+ // 5. Return
143
+ return { taskId, agentId };
144
+ }
145
+
146
+ // ── L3 Research Dispatch ──────────────────────────────────────────────────
147
+
148
+ /**
149
+ * Dispatch a harada_l3_research task to a sub-agent for an action cell.
150
+ *
151
+ * @param {string} cellId - ActionCell node id
152
+ * @param {string} pillarId - Parent GoalPillar node id
153
+ * @param {string} companyId - Company identifier (overrides constructor value if provided)
154
+ * @param {string} agentId - Assignee agent id
155
+ * @param {string} [l3ReviewCritique] - Previous dept head critique (for revision cycles)
156
+ * @returns {{ taskId: string, agentId: string }}
157
+ */
158
+ async dispatchL3Research(cellId, pillarId, companyId, agentId, l3ReviewCritique) {
159
+ const cid = companyId || this._companyId;
160
+
161
+ // 1. Fetch ActionCell description, GoalPillar l2Content, l2Strategy, pillar name
162
+ // and goal info via the pillar
163
+ const contextRows = await this._mg(
164
+ `MATCH (p:GoalPillar {id: $pillarId})
165
+ OPTIONAL MATCH (c:ActionCell {id: $cellId})
166
+ OPTIONAL MATCH (g:CompanyGoal)-[:HAS_PILLAR]->(p)
167
+ RETURN
168
+ c.description AS cellDescription,
169
+ p.name AS pillarName,
170
+ p.l2Content AS l2Content,
171
+ p.l2Strategy AS l2Strategy,
172
+ g.title AS goalTitle
173
+ LIMIT 1`,
174
+ { cellId, pillarId }
175
+ );
176
+
177
+ const row = contextRows && contextRows.rows && contextRows.rows[0]
178
+ ? (Array.isArray(contextRows.rows[0])
179
+ ? {
180
+ cellDescription: contextRows.rows[0][0],
181
+ pillarName: contextRows.rows[0][1],
182
+ l2Content: contextRows.rows[0][2],
183
+ l2Strategy: contextRows.rows[0][3],
184
+ goalTitle: contextRows.rows[0][4],
185
+ }
186
+ : contextRows.rows[0])
187
+ : {};
188
+
189
+ const cellDescription = row.cellDescription || cellId;
190
+ const pillarName = row.pillarName || pillarId;
191
+ const l2Content = row.l2Content || '';
192
+ const l2Strategy = row.l2Strategy || '';
193
+ const goalTitle = row.goalTitle || 'Company Goal';
194
+
195
+ // 2. Build task body
196
+ const previousCritiqueSection = l3ReviewCritique
197
+ ? `\nPrevious dept head critique: ${l3ReviewCritique}`
198
+ : '';
199
+
200
+ const taskBody = `Goal: ${goalTitle}
201
+ Pillar: ${pillarName}
202
+ Pillar strategy: ${l2Strategy}
203
+ Pillar purpose: ${l2Content}
204
+
205
+ Your action cell: ${cellDescription} (id: ${cellId})
206
+
207
+ Research this specific action cell using available tools.
208
+ Produce a specific execution plan (≤100 words) for this action.
209
+
210
+ When complete, update by calling:
211
+ POST /api/hbo/actioncell/${cellId}/l3content
212
+ Body: { content: string }
213
+ ${previousCritiqueSection}`;
214
+
215
+ const taskId = `harada-l3:${cellId}`;
216
+
217
+ // 3. MERGE Task
218
+ await this._mg(
219
+ `MERGE (t:Task {id: $taskId, companyId: $cid})
220
+ ON CREATE SET
221
+ t.originKind = $originKind,
222
+ t.assigneeAgentId = $agentId,
223
+ t.status = 'todo',
224
+ t.priority = 1,
225
+ t.body = $body,
226
+ t.createdAt = datetime()
227
+ ON MATCH SET
228
+ t.body = $body,
229
+ t.status = 'todo',
230
+ t.updatedAt = datetime()`,
231
+ {
232
+ taskId,
233
+ cid,
234
+ originKind: 'harada_l3_research',
235
+ agentId,
236
+ body: taskBody,
237
+ }
238
+ );
239
+
240
+ // 4. Create AgentReadySignal
241
+ const arsId = `ars:harada-l3:${cellId}`;
242
+ await this._mg(
243
+ `MERGE (s:AgentReadySignal {id: $arsId, companyId: $cid})
244
+ ON CREATE SET
245
+ s.agentId = $agentId,
246
+ s.status = 'pending',
247
+ s.createdAt = datetime()
248
+ ON MATCH SET
249
+ s.status = 'pending',
250
+ s.updatedAt = datetime()`,
251
+ { arsId, cid, agentId }
252
+ );
253
+
254
+ this._log(`[CascadeResearchDispatcher] Dispatched L3 research task ${taskId} to agent ${agentId}`);
255
+
256
+ // P_EXPLAIN-01: Post a Three Cs interpretation block as a Comment on the task
257
+ // so the user immediately sees what Helios understood this action cell to mean.
258
+ // Fire-and-forget (non-blocking, errors are silently swallowed).
259
+ setImmediate(async () => {
260
+ try {
261
+ const commentId = `comment:interp:${taskId}`;
262
+ const interpretationBody = '```interpretation\n' + JSON.stringify({
263
+ stated: cellDescription,
264
+ inferred: `Execute action cell "${cellDescription}" as part of the "${pillarName}" pillar strategy: ${l2Strategy || '(pending L2 review)'}`,
265
+ defaulted: 'Harada L3 research task — produce ≤100 word execution plan',
266
+ confidence: 0.75,
267
+ }) + '\n```';
268
+ await this._mg(
269
+ `MERGE (c:Comment {id: $commentId, companyId: $cid})
270
+ ON CREATE SET c.taskId=$taskId, c.body=$body, c.authorType='system',
271
+ c.createdAt=datetime(), c.kind='interpretation'`,
272
+ { commentId, cid, taskId, body: interpretationBody }
273
+ );
274
+ } catch (_) {}
275
+ });
276
+
277
+ // 5. Return
278
+ return { taskId, agentId };
279
+ }
280
+ }
281
+
282
+ module.exports = { CascadeResearchDispatcher };
@@ -20,9 +20,10 @@
20
20
  // Load the async tournament runner — the sync runTournament export was deprecated
21
21
  // and is now a no-op stub. All callers must use runTournamentAsync which delegates
22
22
  // to tournament-node-runner.js (cross-platform: Windows, macOS, Linux).
23
+ // Path: from daemon/lib/harada/ → ../../../ reaches project root → lib/triage-core/
23
24
  let runTournamentAsync = null;
24
25
  try {
25
- const tr = require('../../lib/triage-core/tournament-runner.js');
26
+ const tr = require('../../../lib/triage-core/tournament-runner.js');
26
27
  runTournamentAsync = tr.runTournamentAsync || null;
27
28
  if (!runTournamentAsync) {
28
29
  process.stderr.write('[pillar-dispatcher] WARN: tournament-runner.js loaded but runTournamentAsync not exported — pillar strategy selection will use unranked first candidate\n');
@@ -381,6 +382,26 @@ class PillarDispatcher {
381
382
  // Wire first open ActionCell to this task
382
383
  await this._assignFirstOpenCell(pillar.id, agentId);
383
384
 
385
+ // P_EXPLAIN-02: Post a Three Cs interpretation block as a Comment on the L2 pillar task
386
+ // so the user immediately sees what Helios understood about this strategic dispatch.
387
+ setImmediate(async () => {
388
+ try {
389
+ const commentId = `comment:interp:${taskId}`;
390
+ const interpretationBody = '```interpretation\n' + JSON.stringify({
391
+ stated: `Execute pillar: ${pillar.name}`,
392
+ inferred: `Implement the "${strategy.label}" strategy for pillar "${pillar.name}" by working with your assigned action cells and reporting progress via POST /api/hbo/pillar/${pillar.id}/l2content`,
393
+ defaulted: 'Harada L2 pillar dispatch — strategic execution, workType=strategic',
394
+ confidence: 0.85,
395
+ }) + '\n```';
396
+ await this._mg(
397
+ `MERGE (c:Comment {id: $commentId, companyId: $cid})
398
+ ON CREATE SET c.taskId=$taskId, c.body=$body, c.authorType='system',
399
+ c.createdAt=datetime(), c.kind='interpretation'`,
400
+ { commentId, cid: companyId, taskId, body: interpretationBody }
401
+ );
402
+ } catch (_) {}
403
+ });
404
+
384
405
  this._log('info', `PillarDispatcher: dispatched "${pillar.name}" → ${agentId} [${strategy.label}]`);
385
406
  }
386
407
 
@@ -455,7 +476,7 @@ class PillarDispatcher {
455
476
  }
456
477
  } else {
457
478
  // Tournament runner was not loaded — this means pillar strategy selection is
458
- // operating blind. Log at 'warn' (not 'debug') so operators see this in production.
479
+ // operating blind. Log at 'warn' so operators see this in production.
459
480
  this._log('warn', `PillarDispatcher: tournament runner unavailable for "${pillar.name}" — strategy selection will use first candidate (unranked). Check tournament-runner.js installation.`);
460
481
  }
461
482
 
@@ -74,6 +74,48 @@ function loadService(varRef, relPath) {
74
74
 
75
75
  const MAX_TOURNAMENT_QUEUE = 50; // L1: module-level constant
76
76
 
77
+ /**
78
+ * _rankingsToPillars(rankingsJson) — converts GoalResearchBrief tournament rankings
79
+ * into { name, rationale, weight }[] for MandalaManager.initializeMandala().
80
+ *
81
+ * Weight allocation: proportional to TrueSkill conservativeScore when available,
82
+ * otherwise equal-weight with remainder added to rank-1 entry.
83
+ * Enforces sum=1.0 ± 0.001 tolerance.
84
+ */
85
+ function _rankingsToPillars(rankingsJson) {
86
+ if (!rankingsJson) return null;
87
+ let rankings;
88
+ try { rankings = typeof rankingsJson === 'string' ? JSON.parse(rankingsJson) : rankingsJson; }
89
+ catch { return null; }
90
+ if (!Array.isArray(rankings) || rankings.length === 0) return null;
91
+
92
+ const n = rankings.length;
93
+ let weights;
94
+ if (rankings[0] && typeof rankings[0].conservativeScore === 'number') {
95
+ // TrueSkill-proportional weights
96
+ const total = rankings.reduce((s, r) => s + Math.max(0, r.conservativeScore), 0);
97
+ if (total > 0) {
98
+ weights = rankings.map(r => Math.round(Math.max(0, r.conservativeScore) / total * 10000) / 10000);
99
+ }
100
+ }
101
+ if (!weights) {
102
+ // Equal weights
103
+ const base = Math.round(10000 / n) / 10000;
104
+ weights = Array(n).fill(base);
105
+ }
106
+ // Normalize floating-point drift
107
+ const sum = weights.reduce((s, w) => s + w, 0);
108
+ if (Math.abs(sum - 1.0) > 0.001) {
109
+ weights[0] += (1.0 - sum);
110
+ weights[0] = Math.round(weights[0] * 10000) / 10000;
111
+ }
112
+ return rankings.map((r, i) => ({
113
+ name: r.pillarName || r.name || `Pillar ${i + 1}`,
114
+ rationale: r.rationale || r.description || '',
115
+ weight: weights[i],
116
+ }));
117
+ }
118
+
77
119
  // ── HBO Bridge ─────────────────────────────────────────────────────────────────
78
120
 
79
121
  class HBOBridge {
@@ -685,13 +727,11 @@ class HBOBridge {
685
727
  }
686
728
 
687
729
  this._log('debug', `tickBudgetSync: synced ${spendRows.rows.length} agent(s) spend data`);
688
- } catch (e) {
730
+ } catch (e) {
689
731
  this._log('warn', `tickBudgetSync error (non-fatal): ${e.message}`);
690
732
  }
691
733
  }
692
734
 
693
- // ── tickGoalDecompose ──────────────────────────────────────────────────────
694
-
695
735
  /**
696
736
  * Runs every 5 ticks (same cadence as tickGoalSync).
697
737
  * Detects CompanyGoal nodes that have status='active' but no child
@@ -737,7 +777,7 @@ class HBOBridge {
737
777
  // Find active goals (CompanyGoal OR BusinessGoal) with no child BusinessTasks
738
778
  // and no existing decomposition task (checked by deterministic MERGE on t.id).
739
779
  // CompanyGoal: created by seed-company.js at startup
740
- // BusinessGoal: created via POST /api/hbo/goals or business-goal-service.js
780
+ // BusinessGoal: created via POST /api/hbo/goals or the wizard's write-through goal path
741
781
  const undecomposed = await this._mg(
742
782
  `MATCH (g)
743
783
  WHERE (g:CompanyGoal OR g:BusinessGoal)
@@ -780,7 +820,21 @@ class HBOBridge {
780
820
  try {
781
821
  const { MandalaManager } = require('./harada/mandala');
782
822
  const mandala = new MandalaManager(this._mg.bind(this), this._companyId);
783
- await mandala.initializeMandala(goalId, goalTitle, null);
823
+ // GoalResearchBrief: fetch tournament rankings for pillar name seeding
824
+ let _pillarNames = null;
825
+ try {
826
+ const _briefRows = await this._mgQuery(
827
+ `MATCH (g:CompanyGoal {id: $gid})-[:HAS_RESEARCH]->(grb:GoalResearchBrief)
828
+ WHERE grb.expiresAt > localdatetime()
829
+ RETURN grb.tournamentRankings AS rankings
830
+ ORDER BY grb.createdAt DESC LIMIT 1`,
831
+ { gid: goalId }
832
+ );
833
+ if (_briefRows && _briefRows.length > 0 && _briefRows[0].rankings) {
834
+ _pillarNames = _rankingsToPillars(_briefRows[0].rankings);
835
+ }
836
+ } catch { /* non-fatal — initializeMandala works without pillar names */ }
837
+ await mandala.initializeMandala(goalId, goalTitle, _pillarNames);
784
838
  this._log('info', `tickGoalDecompose: Mandala initialized for goal "${goalTitle}" (8 pillars, 64 cells)`);
785
839
  } catch (e) {
786
840
  this._log('debug', `tickGoalDecompose: Mandala init failed for goal ${goalId}: ${e.message}`);
@@ -952,7 +1006,21 @@ class HBOBridge {
952
1006
  { goalId: goal.id, cid: this._companyId }
953
1007
  ).catch(() => null);
954
1008
  if (!existingPillar?.rows?.length) {
955
- await mandala.initializeMandala(goal.id, goal.title, null);
1009
+ // GoalResearchBrief: fetch tournament rankings for pillar name seeding
1010
+ let _pillarNames = null;
1011
+ try {
1012
+ const _briefRows = await this._mg(
1013
+ `MATCH (g:CompanyGoal {id: $gid})-[:HAS_RESEARCH]->(grb:GoalResearchBrief)
1014
+ WHERE grb.expiresAt > localdatetime()
1015
+ RETURN grb.tournamentRankings AS rankings
1016
+ ORDER BY grb.createdAt DESC LIMIT 1`,
1017
+ { gid: goal.id }
1018
+ );
1019
+ if (_briefRows && _briefRows.length > 0 && _briefRows[0].rankings) {
1020
+ _pillarNames = _rankingsToPillars(_briefRows[0].rankings);
1021
+ }
1022
+ } catch { /* non-fatal — initializeMandala works without pillar names */ }
1023
+ await mandala.initializeMandala(goal.id, goal.title, _pillarNames);
956
1024
  this._log('info', `tickGoalDecompose: Mandala initialized for goal "${goal.title}" (8 pillars, 64 cells)`);
957
1025
  }
958
1026
  } catch (e) {
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+ /**
3
+ * daemon/lib/headroom-middleware.js
4
+ *
5
+ * CCR (Compress-Cache-Retrieve) middleware helpers for helios-api.js.
6
+ *
7
+ * Exports:
8
+ * handleCcrRetrieve(req, res) — GET /api/hbo/retrieve/:hash handler
9
+ * isLargeArrayPayload(parsed) — true when an HBO API response body warrants compression
10
+ *
11
+ * Architecture note on CcrStore:
12
+ * The compression server runs as a CHILD PROCESS (spawned by HeadroomProxyManager).
13
+ * It has its own in-process CcrStore. The daemon process has a separate CcrStore
14
+ * instance which is always empty — it never receives the dropped rows.
15
+ *
16
+ * Therefore handleCcrRetrieve MUST proxy to the child process's HTTP endpoint
17
+ * (GET /v1/retrieve/:hash) rather than reading the daemon's local CcrStore.
18
+ * This is consistent with how helios-api.js proxies /api/headroom/health and
19
+ * /api/headroom/stats.
20
+ *
21
+ * No hardcoded company IDs. Cross-platform (http module, path.join).
22
+ */
23
+
24
+ const http = require('http');
25
+
26
+ // ── handleCcrRetrieve ─────────────────────────────────────────────────────────
27
+ /**
28
+ * Handle GET /api/hbo/retrieve/:hash
29
+ *
30
+ * Proxies to the compression child's GET /v1/retrieve/:hash endpoint.
31
+ * req.params.hash is set by the caller (helios-api.js) before invoking.
32
+ *
33
+ * Responses:
34
+ * 200 { hash: string, data: <original JSON>, originalBytes: number }
35
+ * 400 { error: string } — missing or invalid hash param
36
+ * 404 { error: string } — hash not in child store or expired
37
+ * 503 { error: string } — compression server not running
38
+ */
39
+ async function handleCcrRetrieve(req, res) {
40
+ const hash = (req.params && req.params.hash) ? String(req.params.hash) : '';
41
+
42
+ if (!hash || !/^[a-f0-9]{8,64}$/.test(hash)) {
43
+ res.writeHead(400, { 'Content-Type': 'application/json' });
44
+ res.end(JSON.stringify({ error: 'Invalid or missing hash parameter' }));
45
+ return;
46
+ }
47
+
48
+ // Get the compression server URL from HeadroomProxyManager singleton.
49
+ // The child process owns the CcrStore — we must proxy to it.
50
+ let baseUrl;
51
+ try {
52
+ const { HeadroomProxyManager } = require('./headroom-proxy-manager');
53
+ baseUrl = HeadroomProxyManager.getInstance().getBaseUrl();
54
+ } catch (_) {}
55
+
56
+ if (!baseUrl) {
57
+ res.writeHead(503, { 'Content-Type': 'application/json' });
58
+ res.end(JSON.stringify({ error: 'Compression server not running — CCR retrieve unavailable' }));
59
+ return;
60
+ }
61
+
62
+ // Proxy GET /v1/retrieve/:hash to the child process
63
+ const url = new URL(baseUrl);
64
+ const proxyReq = http.request(
65
+ {
66
+ hostname: url.hostname,
67
+ port: parseInt(url.port || '8787', 10),
68
+ path: `/v1/retrieve/${hash}`,
69
+ method: 'GET',
70
+ },
71
+ (proxyRes) => {
72
+ let body = '';
73
+ proxyRes.on('data', (c) => { body += c; });
74
+ proxyRes.on('end', () => {
75
+ res.writeHead(proxyRes.statusCode, { 'Content-Type': 'application/json' });
76
+ res.end(body);
77
+ });
78
+ }
79
+ );
80
+
81
+ proxyReq.setTimeout(3000, () => {
82
+ proxyReq.destroy();
83
+ res.writeHead(503, { 'Content-Type': 'application/json' });
84
+ res.end(JSON.stringify({ error: 'CCR retrieve timeout' }));
85
+ });
86
+
87
+ proxyReq.on('error', () => {
88
+ if (!res.headersSent) {
89
+ res.writeHead(503, { 'Content-Type': 'application/json' });
90
+ res.end(JSON.stringify({ error: 'CCR retrieve: compression server unreachable' }));
91
+ }
92
+ });
93
+
94
+ proxyReq.end();
95
+ }
96
+
97
+ // ── isLargeArrayPayload ───────────────────────────────────────────────────────
98
+ /**
99
+ * Returns true if a parsed JSON body should be routed through Headroom compression.
100
+ *
101
+ * Conditions (any one sufficient):
102
+ * 1. parsed is an array with >= 5 elements
103
+ * 2. parsed is an object with at least one value that is an array with >= 5 elements
104
+ *
105
+ * Threshold 5 matches SmartCrusher.cfg.minItemsToAnalyze (default 5).
106
+ * Below this threshold SmartCrusher always returns passthrough anyway.
107
+ *
108
+ * Called from helios-api.js res.end() wrapper on GET /api/hbo/* routes.
109
+ * No side effects. Pure function.
110
+ */
111
+ function isLargeArrayPayload(parsed) {
112
+ if (parsed === null || parsed === undefined) return false;
113
+
114
+ // Case 1: top-level array
115
+ if (Array.isArray(parsed)) {
116
+ return parsed.length >= 5;
117
+ }
118
+
119
+ // Case 2: object containing an array value
120
+ if (typeof parsed === 'object') {
121
+ const values = Object.values(parsed);
122
+ return values.some((v) => Array.isArray(v) && v.length >= 5);
123
+ }
124
+
125
+ return false;
126
+ }
127
+
128
+ module.exports = { handleCcrRetrieve, isLargeArrayPayload };
129
+