@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
@@ -383,7 +383,9 @@ async function handleCreateTask(req, res, ctx) {
383
383
  const cid = ctx.cid;
384
384
 
385
385
  const body = await parseBody(req);
386
- const { title, assigneeAgentId, priority, body: taskBody, executionPolicy } = body;
386
+ const { title, assigneeAgentId, priority, body: taskBody, executionPolicy,
387
+ // Phase 2.5-B additions: sourceType, sourceId, originKind, projectId for email-to-task
388
+ sourceType, sourceId, originKind, projectId } = body;
387
389
 
388
390
  if (!title || typeof title !== 'string' || title.trim() === '') {
389
391
  jsonResponse(res, 400, { error: 'title is required and must be a non-empty string' });
@@ -439,19 +441,24 @@ async function handleCreateTask(req, res, ctx) {
439
441
  const resolvedPriority = Number.isFinite(Number(priority)) ? Number(priority) : 2;
440
442
 
441
443
  try {
444
+ // HIGH-3: use MERGE when sourceId provided to prevent duplicate tasks on network-error retry
445
+ const _taskCypher = (sourceId && originKind)
446
+ ? `MERGE (t:Task {sourceId: \, companyId: \, originKind: \})
447
+ ON CREATE SET
448
+ t.id = \, t.title = \, t.status = 'todo', t.priority = toInteger(\),
449
+ t.assigneeAgentId = \, t.body = \,
450
+ t.executionPolicyJson = \, t.executionStateJson = \,
451
+ t.sourceType = \, t.projectId = \, t.createdAt = datetime()
452
+ ON MATCH SET t.updatedAt = datetime()`
453
+ : `CREATE (t:Task {
454
+ id: \, companyId: \, title: \, status: 'todo',
455
+ priority: toInteger(\), assigneeAgentId: \, body: \,
456
+ executionPolicyJson: \, executionStateJson: \,
457
+ sourceType: \, sourceId: \, originKind: \,
458
+ projectId: \, createdAt: datetime()
459
+ })`;
442
460
  await mgQuery(
443
- `CREATE (t:Task {
444
- id: $id,
445
- companyId: $cid,
446
- title: $title,
447
- status: 'todo',
448
- priority: toInteger($priority),
449
- assigneeAgentId: $assigneeAgentId,
450
- body: $body,
451
- executionPolicyJson: $policyJson,
452
- executionStateJson: $stateJson,
453
- createdAt: datetime()
454
- })`,
461
+ _taskCypher,
455
462
  {
456
463
  id: taskId,
457
464
  cid,
@@ -461,8 +468,26 @@ async function handleCreateTask(req, res, ctx) {
461
468
  body: taskBody ? String(taskBody) : null,
462
469
  policyJson: validatedPolicyJson,
463
470
  stateJson: initialStateJson,
471
+ sourceType: sourceType ? String(sourceType) : null,
472
+ sourceId: sourceId ? String(sourceId) : null,
473
+ originKind: originKind ? String(originKind) : null,
474
+ projectId: projectId ? String(projectId) : null,
464
475
  }
465
- );
476
+ ));
477
+
478
+ // Phase 2.5-B: OI-P3 fix — write BELONGS_TO_PROJECT edge when projectId is provided
479
+ if (projectId) {
480
+ try {
481
+ await mgQuery(
482
+ `MATCH (t:Task {id: $taskId}), (p:HeliosProject {id: $projectId, companyId: $cid})
483
+ MERGE (t)-[:BELONGS_TO_PROJECT]->(p)`,
484
+ { taskId, projectId: String(projectId), cid }
485
+ );
486
+ } catch (_edgeErr) {
487
+ // Edge write is best-effort — task is already created; log but don't fail
488
+ log('warn', 'BELONGS_TO_PROJECT edge write failed (OI-P3)', { taskId, projectId });
489
+ }
490
+ }
466
491
 
467
492
  mirrorHboStore('task.create', () => hboStore.createTask?.({
468
493
  id: taskId,
@@ -479,6 +504,17 @@ async function handleCreateTask(req, res, ctx) {
479
504
 
480
505
  broadcast({ type: 'task.created', taskId, status: 'todo', companyId: cid });
481
506
  jsonResponse(res, 201, { task: { id: taskId, title, status: 'todo', priority: resolvedPriority, assigneeAgentId, executionPolicyJson: validatedPolicyJson } });
507
+
508
+ // P_EXPLAIN-01: fire-and-forget interpretation explanation background job
509
+ const { rawWrite: _interpRawWrite } = require('../lib/safe-memgraph.js');
510
+ setImmediate(() => {
511
+ try {
512
+ const { createInterpretationExplanation } = require('./lib/interpretation-engine.js');
513
+ createInterpretationExplanation(taskId, cid, _interpRawWrite).catch(e =>
514
+ log('warn', 'P_EXPLAIN-01 failed', { e: e.message })
515
+ );
516
+ } catch (_) { /* interpretation is non-blocking */ }
517
+ });
482
518
  } catch (err) {
483
519
  jsonResponse(res, 500, { error: `Create failed: ${err.message}` });
484
520
  }
@@ -491,7 +527,9 @@ async function handleUpdateTask(req, res, ctx, taskId) {
491
527
  const cid = ctx.cid;
492
528
 
493
529
  const body = await parseBody(req);
494
- const { status, priority } = body;
530
+ // P6-06a: workspacePath added desktop sets this after acquiring a SessionWorktree
531
+ // for isolated execution; daemon reads it into adapterContext.workspacePath
532
+ const { status, priority, workspacePath, blockedReason, clearBlocked, hillPosition } = body;
495
533
 
496
534
  // P4-01: 'in_review' added — allows execution policy gate to hold a task for review
497
535
  const VALID_STATUSES = new Set(['todo', 'in_progress', 'done', 'failed', 'cancelled', 'blocked', 'escalated', 'in_review']);
@@ -504,8 +542,8 @@ async function handleUpdateTask(req, res, ctx, taskId) {
504
542
  return;
505
543
  }
506
544
 
507
- if (status === undefined && priority === undefined) {
508
- jsonResponse(res, 400, { error: 'Provide at least one field to update: status, priority' });
545
+ if (status === undefined && priority === undefined && workspacePath === undefined && blockedReason === undefined && clearBlocked === undefined && hillPosition === undefined) {
546
+ jsonResponse(res, 400, { error: 'Provide at least one field to update: status, priority, workspacePath, blockedReason, clearBlocked, hillPosition' });
509
547
  return;
510
548
  }
511
549
 
@@ -767,6 +805,17 @@ async function handleUpdateTask(req, res, ctx, taskId) {
767
805
  const params = { id: taskId, cid };
768
806
  if (status !== undefined) { setClauses.push('t.status = $status'); params.status = status; }
769
807
  if (priority !== undefined) { setClauses.push('t.priority = toInteger($priority)'); params.priority = Number(priority); }
808
+ // P6-06a: persist workspacePath on Task node so daemon can read it into adapterContext
809
+ if (workspacePath !== undefined) { setClauses.push('t.workspacePath = $workspacePath'); params.workspacePath = workspacePath ? String(workspacePath) : null; }
810
+ // P1-G: blockedReason + blockedAt — clearBlocked=true nulls both fields and resets status to todo.
811
+ // COALESCE preserves existing reason if not re-set; blockedAt only set once on initial block.
812
+ if (blockedReason !== undefined || clearBlocked !== undefined) {
813
+ params.clearBlocked = clearBlocked === true;
814
+ params.blockedReason = (blockedReason !== undefined && blockedReason !== null) ? String(blockedReason) : null;
815
+ setClauses.push('t.blockedReason = CASE WHEN $clearBlocked = true THEN null ELSE COALESCE($blockedReason, t.blockedReason) END');
816
+ setClauses.push('t.blockedAt = CASE WHEN $clearBlocked = true THEN null WHEN $blockedReason IS NOT NULL AND t.blockedAt IS NULL THEN datetime() ELSE t.blockedAt END');
817
+ setClauses.push('t.status = CASE WHEN $clearBlocked = true AND t.status = \'blocked\' THEN \'todo\' ELSE t.status END');
818
+ }
770
819
  setClauses.push('t.updatedAt = datetime()');
771
820
 
772
821
  // B-09: Single atomic check+update — no TOCTOU window between existence check and SET
@@ -779,9 +828,25 @@ async function handleUpdateTask(req, res, ctx, taskId) {
779
828
  return;
780
829
  }
781
830
 
831
+ // T4-02: HillChart write — track task work-cycle position for hill chart visualisation
832
+ if (hillPosition !== undefined && Number.isFinite(Number(hillPosition))) {
833
+ const hPos = Math.max(0, Math.min(1, Number(hillPosition)));
834
+ const hPhase = hPos < 0.5 ? 'uphill' : hPos < 1.0 ? 'downhill' : 'complete';
835
+ mgQuery(
836
+ `MERGE (h:HillChart {id: 'hill:' + $taskId})
837
+ SET h.taskId = $taskId, h.companyId = $cid,
838
+ h.position = $pos, h.phase = $phase, h.updatedAt = datetime()
839
+ WITH h
840
+ MATCH (t:Task {id: $taskId, companyId: $cid})
841
+ MERGE (t)-[:HAS_HILL_CHART]->(h)`,
842
+ { taskId, cid, pos: hPos, phase: hPhase }
843
+ ).catch(e => log('warn', `HillChart MERGE failed for task ${taskId}: ${e.message}`));
844
+ }
845
+
782
846
  const storeUpdate = {};
783
847
  if (status !== undefined) storeUpdate.status = status;
784
848
  if (priority !== undefined) storeUpdate.priority = Number(priority);
849
+ if (workspacePath !== undefined) storeUpdate.workspacePath = workspacePath ? String(workspacePath) : null;
785
850
  mirrorHboStore('task.update', () => hboStore.updateTask?.(taskId, cid, storeUpdate));
786
851
 
787
852
  broadcast({ type: 'task.updated', taskId, ...(status !== undefined ? { status } : {}), companyId: cid });
@@ -907,6 +972,7 @@ async function handleGetApprovals(req, res, ctx) {
907
972
  a.status AS status, a.requestedBy AS requestedBy,
908
973
  a.agentId AS agentId, a.taskId AS taskId,
909
974
  a.createdAt AS createdAt, a.followUpTaskCreated AS followUpTaskCreated,
975
+ a.followUpTaskId AS followUpTaskId,
910
976
  a.expiresAt AS expiresAt,
911
977
  a.kaizenProposalId AS kaizenProposalId,
912
978
  a.description AS description,
@@ -954,7 +1020,7 @@ async function handleGetApprovals(req, res, ctx) {
954
1020
  const rows = parseRows(result);
955
1021
  const keys = [
956
1022
  'id', 'type', 'title', 'status', 'requestedBy', 'agentId', 'taskId',
957
- 'createdAt', 'followUpTaskCreated', 'expiresAt', 'kaizenProposalId',
1023
+ 'createdAt', 'followUpTaskCreated', 'followUpTaskId', 'expiresAt', 'kaizenProposalId',
958
1024
  'description', 'payload', 'body', 'question', 'pillarId',
959
1025
  'sourceAgent', 'resolvedAt', 'department', 'templateKey',
960
1026
  'inferredAnswer', 'defaultAnswer',
@@ -1089,6 +1155,8 @@ async function handleApproveApproval(req, res, ctx, approvalId) {
1089
1155
  }
1090
1156
 
1091
1157
  broadcast({ type: 'approval.approved', approvalId, companyId: cid });
1158
+ // H-01: record approval.approve in ActivityLogger
1159
+ ctx.activityLogger?.record({ action: 'approval.approve', actor: cid, entityId: approvalId, companyId: cid });
1092
1160
  jsonResponse(res, 200, { approved: true, approvalId });
1093
1161
  } catch (err) {
1094
1162
  jsonResponse(res, 500, { error: `Approve failed: ${err.message}` });
@@ -1118,6 +1186,8 @@ async function handleRejectApproval(req, res, ctx, approvalId) {
1118
1186
  }
1119
1187
  mirrorHboStore('approval.reject', () => hboStore.updateApproval?.(approvalId, cid, { status: 'rejected', rejectReason: reason, rejectedAt: Date.now() }));
1120
1188
  broadcast({ type: 'approval.rejected', approvalId, reason, companyId: cid });
1189
+ // H-01: record approval.reject in ActivityLogger
1190
+ ctx.activityLogger?.record({ action: 'approval.reject', actor: cid, entityId: approvalId, companyId: cid });
1121
1191
  jsonResponse(res, 200, { rejected: true, approvalId, reason });
1122
1192
  } catch (err) {
1123
1193
  jsonResponse(res, 500, { error: `Reject failed: ${err.message}` });
@@ -1396,16 +1466,39 @@ async function handleGetAgents(req, res, ctx) {
1396
1466
 
1397
1467
  async function handleGetAgent(req, res, ctx, agentId) {
1398
1468
  const { mgQuery } = ctx;
1399
- if (!mgQuery) { jsonResponse(res, 503, { error: 'Memgraph not connected' }); return; }
1400
1469
  if (!agentId) { jsonResponse(res, 400, { error: 'Missing agent ID' }); return; }
1401
1470
  const cid = ctx.cid;
1402
1471
 
1472
+ // P7-A1: SQL parity — fall back to SQLite when Memgraph unavailable
1473
+ if (!mgQuery) {
1474
+ try {
1475
+ const a = hboStore.getBusinessAgent ? hboStore.getBusinessAgent(agentId, cid) : null;
1476
+ if (!a) { jsonResponse(res, 404, { error: `Agent not found: ${agentId}` }); return; }
1477
+ // H-1 fix: also include recentTasks from SQLite so shape matches Memgraph path
1478
+ const sqliteTasks = hboStore.getTasksByCompanyStatus
1479
+ ? hboStore.getTasksByCompanyStatus(cid, ['todo', 'in_progress', 'done']).filter(t => t.assigneeAgentId === agentId || t.assignee_agent_id === agentId).slice(0, 10)
1480
+ : [];
1481
+ a.recentTasks = sqliteTasks.map(t => ({ id: t.id, title: t.title ?? null, status: t.status ?? null, priority: t.priority ?? null }));
1482
+ return jsonResponse(res, 200, { agent: a, _source: 'sqlite' });
1483
+ } catch (storeErr) {
1484
+ return jsonResponse(res, 503, { error: 'Agent unavailable: Memgraph not connected', agent: null });
1485
+ }
1486
+ }
1487
+
1403
1488
  try {
1404
1489
  const [agentResult, tasksResult] = await Promise.all([
1405
1490
  mgQuery(
1491
+ // P7-A1: extended RETURN to include all required fields for agent detail page
1406
1492
  `MATCH (a:BusinessAgent {id: $id, companyId: $cid})
1407
- RETURN a.id AS id, a.title AS title, a.status AS status,
1408
- a.lastHeartbeatAt AS lastHeartbeat, a.pauseReason AS pauseReason`,
1493
+ RETURN a.id AS id, a.title AS title, a.role AS role, a.status AS status,
1494
+ a.skill AS skill, a.adapter AS adapter,
1495
+ a.budgetMonthlyCents AS budgetMonthlyCents,
1496
+ a.heartbeatIntervalMs AS heartbeatIntervalMs,
1497
+ a.capabilities AS capabilities,
1498
+ a.defaultSkills AS defaultSkills,
1499
+ a.lastHeartbeatAt AS lastHeartbeatAt,
1500
+ a.pauseReason AS pauseReason,
1501
+ a.companyId AS companyId`,
1409
1502
  { id: agentId, cid }
1410
1503
  ),
1411
1504
  mgQuery(
@@ -1422,7 +1515,9 @@ async function handleGetAgent(req, res, ctx, agentId) {
1422
1515
  return;
1423
1516
  }
1424
1517
 
1425
- const agentKeys = ['id', 'title', 'status', 'lastHeartbeat', 'pauseReason'];
1518
+ const agentKeys = ['id', 'title', 'role', 'status', 'skill', 'adapter',
1519
+ 'budgetMonthlyCents', 'heartbeatIntervalMs', 'capabilities', 'defaultSkills',
1520
+ 'lastHeartbeatAt', 'pauseReason', 'companyId'];
1426
1521
  const taskKeys = ['id', 'title', 'status', 'priority'];
1427
1522
  const agent = rowToObj(agentRows[0], agentKeys);
1428
1523
  agent.recentTasks = parseRows(tasksResult).map(r => rowToObj(r, taskKeys));
@@ -1433,6 +1528,37 @@ async function handleGetAgent(req, res, ctx, agentId) {
1433
1528
  }
1434
1529
  }
1435
1530
 
1531
+ // P7-A2: GET /api/agents/:id/runs — HeartbeatRun history for an agent
1532
+ async function handleGetAgentRuns(req, res, ctx, agentId) {
1533
+ const { mgQuery } = ctx;
1534
+ if (!agentId) { jsonResponse(res, 400, { error: 'Missing agent ID' }); return; }
1535
+ const cid = ctx.cid;
1536
+
1537
+ // SQL parity: HeartbeatRun nodes are Memgraph-only (no SQLite mirror yet)
1538
+ if (!mgQuery) {
1539
+ return jsonResponse(res, 200, { runs: [], _source: 'sqlite' });
1540
+ }
1541
+
1542
+ try {
1543
+ const result = await mgQuery(
1544
+ // H-2 fix: removed h.runId (field doesn't exist on HeartbeatRun nodes — they use h.id)
1545
+ // M-4 fix: use toString() for consistent datetime ordering (matches all other ORDER BY in codebase)
1546
+ `MATCH (h:HeartbeatRun {agentId: $agentId, companyId: $cid})
1547
+ RETURN h.id AS id, h.agentId AS agentId, h.companyId AS companyId,
1548
+ h.status AS status, h.startedAt AS startedAt, h.endedAt AS endedAt,
1549
+ h.taskId AS taskId
1550
+ ORDER BY toString(h.startedAt) DESC LIMIT toInteger(50)`,
1551
+ { agentId, cid }
1552
+ );
1553
+ const rows = parseRows(result);
1554
+ const keys = ['id', 'agentId', 'companyId', 'status', 'startedAt', 'endedAt', 'taskId'];
1555
+ const runs = rows.map(r => rowToObj(r, keys));
1556
+ return jsonResponse(res, 200, { runs, count: runs.length });
1557
+ } catch (err) {
1558
+ return jsonResponse(res, 500, { error: `Query failed: ${err.message}` });
1559
+ }
1560
+ }
1561
+
1436
1562
  // ── Activity ──────────────────────────────────────────────────────────────────
1437
1563
 
1438
1564
  async function handleGetActivity(req, res, ctx) {
@@ -1821,6 +1947,38 @@ async function handleReviseApproval(req, res, ctx, approvalId) {
1821
1947
  return;
1822
1948
  }
1823
1949
  broadcast({ type: 'approval.revised', approvalId, companyId: cid });
1950
+
1951
+ // Record revision in ActivityLogger so the activity feed shows it
1952
+ ctx.activityLogger?.record({ action: 'approval.revise', actor: cid, entityId: approvalId, companyId: cid });
1953
+
1954
+ // B-19 fix: create AgentReadySignal so the assigned agent wakes and processes the revision.
1955
+ // Without this, the agent that owns the approval's task never re-evaluates after revision.
1956
+ setImmediate(async () => {
1957
+ try {
1958
+ const taskRows = await mgQuery(
1959
+ `MATCH (a:Approval {id: $id, companyId: $cid})-[:FOR_TASK]->(t:Task)
1960
+ RETURN t.id AS taskId, t.assigneeAgentId AS agentId`,
1961
+ { id: approvalId, cid }
1962
+ );
1963
+ const taskRow = parseRows(taskRows)[0];
1964
+ if (taskRow && taskRow.taskId && taskRow.agentId) {
1965
+ await mgQuery(
1966
+ `MERGE (s:AgentReadySignal {id: 'signal:revise:' + $taskId})
1967
+ ON CREATE SET s.agentId = $agentId, s.companyId = $cid, s.status = 'pending',
1968
+ s.origin = 'approval_revision', s.taskId = $taskId,
1969
+ s.approvalId = $approvalId, s.createdAt = localdatetime()
1970
+ ON MATCH SET s.status = 'pending', s.approvalId = $approvalId`,
1971
+ { taskId: taskRow.taskId, agentId: taskRow.agentId, cid, approvalId }
1972
+ );
1973
+ } else {
1974
+ // MED-8: B-19 — no [:FOR_TASK] edge found; agent not signaled
1975
+ log('warn', 'B-19: no Task linked to approval via FOR_TASK edge -- agent not signaled', { approvalId, cid });
1976
+ }
1977
+ } catch (sigErr) {
1978
+ log('warn', 'B-19: AgentReadySignal creation failed after revision', { approvalId, err: sigErr.message });
1979
+ }
1980
+ });
1981
+
1824
1982
  jsonResponse(res, 200, { revised: true, approvalId });
1825
1983
  } catch (err) {
1826
1984
  jsonResponse(res, 500, { error: { code: 'UPDATE_FAILED', message: `Revise failed: ${err.message}` } });
@@ -2674,6 +2832,10 @@ async function handleGetRoutineTriggers(req, res, ctx, routineId) {
2674
2832
 
2675
2833
  async function handleCreateRoutineTrigger(req, res, ctx, routineId) {
2676
2834
  const { mgQuery } = ctx;
2835
+ // HIGH-2 fix: guard against Memgraph unavailability
2836
+ if (!mgQuery) { return jsonResponse(res, 503, { error: 'Memgraph not connected' }); }
2837
+ // CRIT-2 fix: declare cid so broadcast() does not throw ReferenceError
2838
+ const cid = ctx.cid;
2677
2839
  const body = await parseBody(req);
2678
2840
  const kind = body.kind;
2679
2841
  if (!kind || !['schedule', 'webhook', 'api'].includes(kind)) {
@@ -2698,7 +2860,7 @@ async function handleCreateRoutineTrigger(req, res, ctx, routineId) {
2698
2860
  {
2699
2861
  triggerId,
2700
2862
  routineId,
2701
- cid: ctx.cid,
2863
+ cid,
2702
2864
  kind,
2703
2865
  publicId,
2704
2866
  config: JSON.stringify(body.config || {}),
@@ -2900,13 +3062,17 @@ async function handleCreateRoutine(req, res, ctx) {
2900
3062
 
2901
3063
  async function handlePatchRoutine(req, res, ctx, routineId) {
2902
3064
  const { mgQuery } = ctx;
3065
+ // HIGH-2 fix: guard against Memgraph unavailability
3066
+ if (!mgQuery) { return jsonResponse(res, 503, { error: 'Memgraph not connected' }); }
3067
+ // CRIT-2 fix: declare cid so broadcast() does not throw ReferenceError
3068
+ const cid = ctx.cid;
2903
3069
  const body = await parseBody(req);
2904
3070
 
2905
3071
  try {
2906
- // Get current routine
3072
+ // Get current routine — CRIT-5 fix: use r.name (not r.title) consistently
2907
3073
  const existing = await mgQuery(
2908
- `MATCH (r:Routine {id: $routineId, companyId: $cid}) RETURN r.title AS title, r.config AS config`,
2909
- { routineId, cid: ctx.cid }
3074
+ `MATCH (r:Routine {id: $routineId, companyId: $cid}) RETURN r.name AS name, r.config AS config`,
3075
+ { routineId, cid }
2910
3076
  );
2911
3077
  const rows = parseRows(existing);
2912
3078
  if (rows.length === 0) return jsonResponse(res, 404, { error: 'Routine not found' });
@@ -2914,48 +3080,69 @@ async function handlePatchRoutine(req, res, ctx, routineId) {
2914
3080
  // Count existing revisions to determine revision number
2915
3081
  const revCount = await mgQuery(
2916
3082
  `MATCH (rv:RoutineRevision {routineId: $routineId, companyId: $cid}) RETURN count(rv) AS cnt`,
2917
- { routineId, cid: ctx.cid }
3083
+ { routineId, cid }
2918
3084
  );
2919
3085
  const revRows = parseRows(revCount);
2920
3086
  const revisionNumber = (revRows[0]?.[0] || 0) + 1;
2921
3087
 
2922
- // Create revision
2923
- const revisionId = `revision:${crypto.randomUUID()}`;
2924
- await mgQuery(
2925
- `CREATE (rv:RoutineRevision {
2926
- id: $revisionId,
2927
- routineId: $routineId,
2928
- companyId: $cid,
2929
- revisionNumber: $revisionNumber,
2930
- config: $config,
2931
- createdAt: datetime()
2932
- })`,
2933
- {
2934
- revisionId,
2935
- routineId,
2936
- cid: ctx.cid,
2937
- revisionNumber,
2938
- config: JSON.stringify(body)
2939
- }
2940
- );
2941
-
2942
3088
  // Update routine with new values
3089
+ // CRIT-3 fix: handle body.status (for soft-delete via 'inactive' + other status changes)
3090
+ // CRIT-5 fix: use r.name not r.title
2943
3091
  const sets = [];
2944
- const params = { routineId, cid: ctx.cid };
2945
- if (body.title) { sets.push('r.title = $title'); params.title = body.title; }
2946
- if (body.catchUpPolicy) { sets.push('r.catchUpPolicy = $catchUpPolicy'); params.catchUpPolicy = body.catchUpPolicy; }
3092
+ const params = { routineId, cid };
3093
+ if (body.name) { sets.push('r.name = $name'); params.name = String(body.name); }
3094
+ if (body.title) { sets.push('r.name = $name'); params.name = String(body.title); } // legacy alias
3095
+ if (body.status) { sets.push('r.status = $status'); params.status = String(body.status); }
3096
+ if (body.catchUpPolicy) { sets.push('r.catchUpPolicy = $catchUpPolicy'); params.catchUpPolicy = body.catchUpPolicy; }
2947
3097
  if (body.concurrencyPolicy) { sets.push('r.concurrencyPolicy = $concurrencyPolicy'); params.concurrencyPolicy = body.concurrencyPolicy; }
2948
- if (body.config) { sets.push('r.config = $config'); params.config = JSON.stringify(body.config); }
3098
+ if (body.cronExpr) { sets.push('r.cronExpr = $cronExpr'); params.cronExpr = body.cronExpr; }
3099
+ if (body.config) { sets.push('r.config = $config'); params.config = JSON.stringify(body.config); }
3100
+ sets.push('r.updatedAt = datetime()');
3101
+
3102
+ // MED-3 fix: only create a revision if there are actual field changes (not on no-op patches)
3103
+ const hasChanges = sets.length > 1; // always has updatedAt, so check > 1
3104
+ if (hasChanges) {
3105
+ const revisionId = `revision:${crypto.randomUUID()}`;
3106
+ await mgQuery(
3107
+ `CREATE (rv:RoutineRevision {
3108
+ id: $revisionId,
3109
+ routineId: $routineId,
3110
+ companyId: $cid,
3111
+ revisionNumber: $revisionNumber,
3112
+ config: $config,
3113
+ createdAt: datetime()
3114
+ })`,
3115
+ {
3116
+ revisionId,
3117
+ routineId,
3118
+ cid,
3119
+ revisionNumber,
3120
+ config: JSON.stringify(body)
3121
+ }
3122
+ );
3123
+ }
2949
3124
 
2950
- if (sets.length > 0) {
3125
+ if (sets.length > 1) {
2951
3126
  await mgQuery(
2952
3127
  `MATCH (r:Routine {id: $routineId, companyId: $cid}) SET ${sets.join(', ')}`,
2953
3128
  params
2954
3129
  );
2955
3130
  }
2956
3131
 
2957
- broadcast({ type: 'routine.updated', routineId, revisionNumber, companyId: cid });
2958
- return jsonResponse(res, 200, { updated: true, routineId, revisionNumber, revisionId });
3132
+ // HIGH-3 fix: SQLite write-through after successful Memgraph update
3133
+ if (sets.length > 1 && hboStore?.updateRoutine) {
3134
+ const storeUpdate = {};
3135
+ if (body.name || body.title) storeUpdate.name = body.name ?? body.title;
3136
+ if (body.status) storeUpdate.status = body.status;
3137
+ if (body.catchUpPolicy) storeUpdate.catch_up_policy = body.catchUpPolicy;
3138
+ if (body.concurrencyPolicy) storeUpdate.concurrency_policy = body.concurrencyPolicy;
3139
+ if (body.cronExpr) storeUpdate.cron_expr = body.cronExpr;
3140
+ try { hboStore.updateRoutine(routineId, cid, storeUpdate); }
3141
+ catch (storeErr) { log('error', 'hboStore.updateRoutine failed', { err: storeErr.message, routineId }); }
3142
+ }
3143
+
3144
+ broadcast({ type: 'routine.updated', routineId, revisionNumber: hasChanges ? revisionNumber : null, companyId: cid });
3145
+ return jsonResponse(res, 200, { updated: true, routineId, revisionNumber: hasChanges ? revisionNumber : null });
2959
3146
  } catch (err) {
2960
3147
  return jsonResponse(res, 500, { error: err.message });
2961
3148
  }
@@ -2981,6 +3168,12 @@ async function handleGetRoutineRevisions(req, res, ctx, routineId) {
2981
3168
 
2982
3169
  async function handleGetRoutineRuns(req, res, ctx, routineId) {
2983
3170
  const { mgQuery } = ctx;
3171
+ const cid = ctx.cid;
3172
+ // MED-5 fix: SQL parity — fallback to SQLite when Memgraph unavailable
3173
+ if (!mgQuery) {
3174
+ // No RoutineRun SQLite table exists yet — return empty gracefully
3175
+ return jsonResponse(res, 200, { runs: [], _source: 'sqlite' });
3176
+ }
2984
3177
  try {
2985
3178
  const result = await mgQuery(
2986
3179
  `MATCH (rr:RoutineRun {routineId: $routineId, companyId: $cid})
@@ -2988,7 +3181,7 @@ async function handleGetRoutineRuns(req, res, ctx, routineId) {
2988
3181
  rr.startedAt AS startedAt, rr.completedAt AS completedAt
2989
3182
  ORDER BY rr.startedAt DESC
2990
3183
  LIMIT toInteger($lim)`,
2991
- { routineId, cid: ctx.cid, lim: 50 }
3184
+ { routineId, cid, lim: 50 }
2992
3185
  );
2993
3186
  const rows = parseRows(result);
2994
3187
  const keys = ['id', 'status', 'taskId', 'startedAt', 'completedAt'];
@@ -3348,9 +3541,14 @@ const approvalsRoute = require('./routes/approvals')({
3348
3541
 
3349
3542
  const agentsRoute = require('./routes/agents')({
3350
3543
  handleGetAgents,
3544
+ handleGetAgent, // P7-A1: wire single-agent GET (was dead code — not passed before)
3545
+ handleGetAgentRuns, // P7-A2: HeartbeatRun history
3351
3546
  handleSyncSkills,
3352
3547
  handleApproveAgent,
3353
3548
  handleTerminateAgent,
3549
+ handlePauseAgent, // Bug fix: was missing, caused TypeError in agents.js
3550
+ handleResumeAgent, // Bug fix: was missing
3551
+ handlePauseAll: null, // pause-all is handled in hbo.js; stub null to prevent TypeError
3354
3552
  });
3355
3553
 
3356
3554
  const skillsRoute = require('./routes/skills')({
@@ -3405,7 +3603,7 @@ const inboxRoute = require('./routes/inbox')({ broadcast });
3405
3603
  const queueRoute = require('./routes/queue')({ broadcast });
3406
3604
  const { handleDraftsRoute } = require('./routes/drafts');
3407
3605
  const goalsRoute = require('./routes/goals');
3408
- const hboRoute = require('./routes/hbo')({ broadcast });
3606
+ let hboRoute = require('./routes/hbo')({ broadcast });
3409
3607
  const emailTriageRoute = require('./routes/email-triage')();
3410
3608
  const andonRoute = require('./routes/andon')({ handleGetAndonBoard });
3411
3609
  const wizardRoute = require('./routes/wizard')({});
@@ -3435,6 +3633,9 @@ let mandalaRoute;
3435
3633
  try { mandalaRoute = require('./routes/mandala').mandalaRoute; } catch (_) { mandalaRoute = () => false; }
3436
3634
  let personRoute;
3437
3635
  try { personRoute = require('./routes/person')({ handleGetPersonProfile, handleGetPersonByEmail, handleGetPersonEpisodes }); } catch (_) { personRoute = () => false; }
3636
+ // P5-S5c: CRM dual-write route — accepts POST /api/crm/contacts from desktop crmSyncService
3637
+ let crmRoute;
3638
+ try { crmRoute = require('./routes/crm')({ parseBody, jsonResponse }); } catch (_) { crmRoute = () => false; }
3438
3639
 
3439
3640
 
3440
3641
  // ── WS-nonce store (single-use, 60-second TTL) ───────────────────────────────
@@ -3499,6 +3700,25 @@ async function route(req, res, ctx) {
3499
3700
  return;
3500
3701
  }
3501
3702
 
3703
+ // SP3: Read receipt tracking pixel — must be before auth middleware, no auth required
3704
+ if (req.method === 'GET' && /^\/t\/[a-zA-Z0-9_-]+\.png$/.test(req.url || '')) {
3705
+ const trackingId = (req.url || '').slice(3, -4); // strip leading /t/ and trailing .png
3706
+ const gif1x1 = Buffer.from('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'base64');
3707
+ res.writeHead(200, {
3708
+ 'Content-Type': 'image/gif',
3709
+ 'Content-Length': gif1x1.length,
3710
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
3711
+ 'Pragma': 'no-cache',
3712
+ });
3713
+ res.end(gif1x1);
3714
+ // Fire-and-forget: log open event to Memgraph
3715
+ try {
3716
+ const { rawWrite: rw } = require('../lib/safe-memgraph.js');
3717
+ rw(`MERGE (o:OpenEvent {trackingId: $id}) ON CREATE SET o.count = 1, o.firstOpenedAt = datetime() ON MATCH SET o.count = o.count + 1, o.lastOpenedAt = datetime()`, { id: trackingId }).catch(() => {});
3718
+ } catch {}
3719
+ return;
3720
+ }
3721
+
3502
3722
  // ── Bearer token auth ────────────────────────────────────────────────────
3503
3723
  // Public routes that skip auth
3504
3724
  const PUBLIC_PATHS = new Set(['/api/health', '/api/auth/ws-nonce', '/login', '/signup', '/api/auth/signup', '/api/auth/login', '/api/auth/logout']);
@@ -3875,6 +4095,7 @@ async function route(req, res, ctx) {
3875
4095
  if (await suggestionsRoute(req, res, reqCtx, pathname, method)) return;
3876
4096
  if (await mandalaRoute(req, res, reqCtx, pathname, method)) return;
3877
4097
  if (await personRoute(req, res, reqCtx, pathname, method)) return;
4098
+ if (await crmRoute(req, res, reqCtx, pathname, method)) return;
3878
4099
 
3879
4100
  // ── Phase 3: SystemAim endpoints ─────────────────────────────────────────
3880
4101
  if (pathname === '/api/system-aim') {
@@ -4151,11 +4372,26 @@ async function handleGetDepartmentPageRoute(req, res, ctx, department) {
4151
4372
  if (narrativeStr && lastGenStr) {
4152
4373
  const lastGen = new Date(String(lastGenStr)).getTime();
4153
4374
  if (!isNaN(lastGen) && Date.now() - lastGen < CACHE_TTL_MS) {
4154
- // Cache hit — return as-is
4375
+ // Cache hit — return as-is, also include tasks created from this page
4155
4376
  let narrative;
4156
4377
  try { narrative = JSON.parse(String(narrativeStr)); }
4157
4378
  catch (_) { narrative = { governingThought: String(narrativeStr) }; }
4158
- jsonResponse(res, 200, { department, narrative, generatedAt: lastGenStr, cached: true });
4379
+
4380
+ // P8E-01: Include tasks created from this department page's decisionsStructured
4381
+ const tasksResult = await mg(
4382
+ `MATCH (dp:DepartmentPage {companyId: $cid, department: $dept})
4383
+ WHERE dp.narrative IS NOT NULL
4384
+ OPTIONAL MATCH (dp)-[:CREATED_TASK]->(t:Task)
4385
+ RETURN collect({id: t.id, title: t.title, status: t.status, originKind: t.originKind}) AS tasks
4386
+ ORDER BY dp.lastGeneratedAt DESC LIMIT 1`,
4387
+ { cid, dept: department }
4388
+ ).catch(() => null);
4389
+ const tasksRow = tasksResult && tasksResult.rows ? tasksResult.rows[0] : null;
4390
+ const tasks = tasksRow
4391
+ ? (Array.isArray(tasksRow) ? (tasksRow[0] || []) : (tasksRow.tasks || []))
4392
+ : [];
4393
+
4394
+ jsonResponse(res, 200, { department, narrative, generatedAt: lastGenStr, cached: true, tasks });
4159
4395
  return;
4160
4396
  }
4161
4397
  }
@@ -4407,6 +4643,21 @@ function startApi(mgQuery, config = {}, state = {}) {
4407
4643
  deptRoute = require('./routes/dept')({ mgQuery, broadcast: state.broadcast ?? (() => {}) });
4408
4644
  }
4409
4645
 
4646
+ // Reinitialize hboRoute with triggerGoalDecompose hook so POST /api/hbo/goals immediately
4647
+ // fires tickGoalDecompose instead of waiting up to 150s for the next 5th-tick.
4648
+ // state.daemon._mods is the per-company module registry built by buildForCompany().
4649
+ if (state.daemon && typeof state.broadcast === 'function') {
4650
+ const _triggerFn = (cid) => {
4651
+ try {
4652
+ const mods = state.daemon._mods && state.daemon._mods.get ? state.daemon._mods.get(cid) : (state.daemon._mods?.[cid]);
4653
+ if (mods && mods.hboBridge && typeof mods.hboBridge.tickGoalDecompose === 'function') {
4654
+ mods.hboBridge.tickGoalDecompose({ fromGoalCreate: true }).catch(() => {});
4655
+ }
4656
+ } catch (_) {}
4657
+ };
4658
+ hboRoute = require('./routes/hbo')({ broadcast: state.broadcast, mgQuery, triggerGoalDecompose: _triggerFn });
4659
+ }
4660
+
4410
4661
  // HED routes — requires mgQuery for HEDEngine
4411
4662
  try {
4412
4663
  const { createHedRoutes } = require('./routes/hed');
@@ -4439,6 +4690,7 @@ function startApi(mgQuery, config = {}, state = {}) {
4439
4690
  draining: false,
4440
4691
  apiToken: config.apiToken || null, // null = no auth (backward compat)
4441
4692
  daemon: state.daemon ?? null, // daemon instance ref — used by wizard route for onCompanyReady
4693
+ activityLogger: state.activityLogger ?? null, // H-01: for recording approval events
4442
4694
  modules: [
4443
4695
  'RoutineEvaluator', 'BudgetEnforcer', 'LivenessWatchdog',
4444
4696
  'AgentDispatcher', 'ActivityLogger', 'TaskCompletionWatchdog',