@cgh567/agent 2.4.1 → 2.4.3

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 (169) hide show
  1. package/bin/helios +0 -0
  2. package/bin/helios-rpc-node-wrapper.cjs +0 -0
  3. package/bin/helios-rpc-wrapper.sh +0 -0
  4. package/daemon/adapters/helios-rpc-adapter.js +47 -25
  5. package/daemon/adapters/tui_wakeup.js +8 -0
  6. package/daemon/config/com.familiar.helios-daemon.plist +5 -0
  7. package/daemon/config/helios-daemon.service +4 -0
  8. package/daemon/context-enrichment.js +59 -21
  9. package/daemon/daemon-manager.js +1 -1
  10. package/daemon/db/email-infrastructure-migrate.js +192 -0
  11. package/daemon/db/hbo-core-migrate.js +189 -0
  12. package/daemon/helios-api.js +723 -57
  13. package/daemon/helios-company-daemon.js +616 -134
  14. package/daemon/lib/harada/cascade-judge.js +12 -50
  15. package/daemon/lib/harada/mandala.js +20 -0
  16. package/daemon/lib/harada/pillar-dispatcher.js +1 -1
  17. package/daemon/lib/harada/project-factory.js +7 -2
  18. package/daemon/lib/hbo-bridge.js +32 -13
  19. package/daemon/lib/hed-engine.js +10 -292
  20. package/daemon/lib/helios-hitl-host.js +15 -2
  21. package/daemon/lib/hitl-interaction-service.js +0 -0
  22. package/daemon/lib/memgraph-verify.js +38 -33
  23. package/daemon/lib/project-drift-detector.js +7 -17
  24. package/daemon/lib/project-semantic-updater.js +1 -14
  25. package/daemon/lib/task-completion-processor.js +11 -0
  26. package/daemon/lib/wizard-engine.js +57 -6
  27. package/daemon/routes/channels.js +10 -5
  28. package/daemon/routes/harada-map.js +11 -48
  29. package/daemon/routes/hbo.js +342 -75
  30. package/daemon/routes/hitl.js +0 -0
  31. package/daemon/routes/project.js +194 -62
  32. package/daemon/routes/routines.js +14 -0
  33. package/daemon/routes/tasks.js +15 -1
  34. package/daemon/routes/wizard.js +11 -4
  35. package/daemon/schema-apply.js +174 -0
  36. package/daemon/schema-definitions.js +423 -0
  37. package/daemon/schema-migrations-hbo.js +10 -0
  38. package/daemon/schema-migrations-hed.js +18 -0
  39. package/daemon/schema-migrations-hitl.js +0 -0
  40. package/daemon/schema-migrations-proj.js +131 -0
  41. package/extensions/001-tool-output-cap.ts +0 -0
  42. package/extensions/context-compaction.ts +45 -26
  43. package/extensions/cortex/activation-bridge.ts +5 -0
  44. package/extensions/cortex/learn.ts +26 -0
  45. package/extensions/cortex/wal-replay.ts +91 -0
  46. package/extensions/email/backfill.ts +0 -0
  47. package/extensions/helios-governance/analysis/ambiguity.ts +0 -0
  48. package/extensions/helios-governance/analysis/compliance.ts +0 -0
  49. package/extensions/helios-governance/analysis/long-task-detector.ts +0 -0
  50. package/extensions/helios-governance/analysis/output-contract.ts +0 -0
  51. package/extensions/helios-governance/analysis/patterns.ts +0 -0
  52. package/extensions/helios-governance/analysis/preflight.ts +0 -0
  53. package/extensions/helios-governance/analysis/recurring-violations.ts +0 -0
  54. package/extensions/helios-governance/analysis/task-classification.ts +0 -0
  55. package/extensions/helios-governance/analysis/task-intent.ts +0 -0
  56. package/extensions/helios-governance/gates/high-impact.ts +1 -1
  57. package/extensions/helios-governance/handlers/_jiti-require.ts +15 -8
  58. package/extensions/helios-governance/handlers/proxy-test-detector.ts +0 -0
  59. package/extensions/hema-dispatch-v3/graph-memory.ts +10 -0
  60. package/extensions/hema-dispatch-v3/index.ts +72 -47
  61. package/extensions/lib/elo-engine.js +0 -0
  62. package/extensions/lib/elo-engine.test.js +0 -0
  63. package/extensions/memgraph-autostart.ts +13 -0
  64. package/extensions/neuroplastic-eval.ts +0 -0
  65. package/extensions/shadow-loop/index.ts +0 -0
  66. package/extensions/warm-tick/warm-tick-maintenance.ts +8 -0
  67. package/lib/__tests__/hbo-core-store.test.js +238 -0
  68. package/lib/brain-v2-budget.js +0 -0
  69. package/lib/brain-v2-circuit-breaker.js +0 -0
  70. package/lib/brain-v2.js +0 -0
  71. package/lib/broker/adaptive-throttle.js +0 -0
  72. package/lib/broker/batch-coalescer.js +0 -0
  73. package/lib/broker/bulkhead.js +0 -0
  74. package/lib/broker/channel-registry.js +0 -0
  75. package/lib/broker/circuit-breaker.js +0 -0
  76. package/lib/broker/evidence-cache.js +0 -0
  77. package/lib/broker/health-monitor.js +0 -0
  78. package/lib/broker/mage-queue.js +0 -0
  79. package/lib/broker/priority-queue.js +0 -0
  80. package/lib/broker/server.js.bak-error2-fix +0 -0
  81. package/lib/broker/session-registry.js +0 -0
  82. package/lib/broker/singleton-timers.js +0 -0
  83. package/lib/broker/types.d.ts +0 -0
  84. package/lib/broker/vegas-limit.js +0 -0
  85. package/lib/compression/dist/ccr-store.js +74 -0
  86. package/lib/compression/dist/content-router.js +115 -0
  87. package/lib/compression/dist/pipeline.js +113 -0
  88. package/lib/compression/dist/server.js +265 -0
  89. package/lib/compression/dist/smart-crusher.js +251 -0
  90. package/lib/context-budget.ts +0 -0
  91. package/lib/context-firewall.js +0 -0
  92. package/lib/crm/integration/triage-bridge.js +0 -0
  93. package/lib/email-utils.ts +0 -0
  94. package/lib/eval/__tests__/preflight-checker.test.ts +0 -0
  95. package/lib/eval/__tests__/task-instruction-parser.test.ts +0 -0
  96. package/lib/eval/__tests__/verifier-runner.test.ts +0 -0
  97. package/lib/eval/index.ts +0 -0
  98. package/lib/eval/preflight-checker.ts +0 -0
  99. package/lib/eval/task-domain-classifier.ts +0 -0
  100. package/lib/eval/task-instruction-parser.ts +0 -0
  101. package/lib/eval/verifier-runner.ts +0 -0
  102. package/lib/event-bus.d.ts +0 -0
  103. package/lib/event-bus.mts +1 -1
  104. package/lib/governance-context-selector.ts +0 -0
  105. package/lib/graph/generate-extension-embeddings.js +0 -0
  106. package/lib/graph/generate-static-embeddings.js +0 -0
  107. package/lib/graph/lib/utils.js +1 -1
  108. package/lib/graph-audit.d.ts +0 -0
  109. package/lib/graph-availability.js +62 -0
  110. package/lib/hbo-core-store.compiled.js +834 -0
  111. package/lib/hbo-core-store.js +124 -0
  112. package/lib/hbo-core-store.ts +908 -0
  113. package/lib/mesh-circuit-breaker.js +0 -0
  114. package/lib/mission-loop/lesson-extractor.ts +0 -0
  115. package/lib/mission-loop/mental-model-scorer.ts +0 -0
  116. package/lib/mission-loop/occ-detector.ts +0 -0
  117. package/lib/mission-loop/query-variants.ts +0 -0
  118. package/lib/mission-loop/verifier-check.ts +0 -0
  119. package/lib/skill-reference-builder.ts +0 -0
  120. package/lib/telemetry/token-breakdown.ts +0 -0
  121. package/lib/tool-compressor.ts +0 -0
  122. package/lib/triage-core/classifier.ts +3 -2
  123. package/lib/triage-core/graph/schema.cypher +10 -0
  124. package/lib/triage-core/legal-routing.ts +0 -0
  125. package/lib/triage-core/mental-model/dunbar-classifier.ts +0 -0
  126. package/lib/triage-core/mental-model/enrich-all.ts +0 -0
  127. package/lib/triage-core/mental-model/identity-resolver.ts +0 -0
  128. package/lib/triage-core/mental-model/key-facts.ts +1 -2
  129. package/lib/triage-core/mental-model/model-assembler.ts +0 -0
  130. package/lib/triage-core/orchestrator.ts +4 -11
  131. package/lib/triage-core/orchestrator.ts.bak-r005-r006-r008 +0 -0
  132. package/package.json +18 -8
  133. package/skills/helios-business-operator/services/signals/upwork-signals.js +0 -0
  134. package/skills/talisman-ceo/SKILL.md +23 -25
  135. package/skills/talisman-comms/SKILL.md +5 -5
  136. package/skills/talisman-engineering/SKILL.md +5 -5
  137. package/skills/talisman-finance/SKILL.md +10 -8
  138. package/skills/talisman-marketing/SKILL.md +10 -10
  139. package/skills/talisman-sales/SKILL.md +12 -15
  140. package/skills/talisman-support/SKILL.md +5 -5
  141. package/agents/business/talisman-ceo.md +0 -183
  142. package/agents/business/talisman-comms.md +0 -257
  143. package/agents/business/talisman-cto.md +0 -153
  144. package/agents/business/talisman-finance.md +0 -246
  145. package/agents/business/talisman-marketing.md +0 -240
  146. package/agents/business/talisman-sales.md +0 -242
  147. package/agents/business/talisman-support.md +0 -236
  148. package/daemon/lib/approval-expiry.js +0 -162
  149. package/daemon/lib/blast-radius-analyzer.js +0 -75
  150. package/daemon/lib/domain-bootstrap-orchestrator.js +0 -267
  151. package/daemon/lib/forensic-log.js +0 -113
  152. package/daemon/lib/goal-research-pipeline.js +0 -644
  153. package/daemon/lib/harada/cascade-research-dispatcher.js +0 -261
  154. package/daemon/lib/headroom-middleware.js +0 -167
  155. package/daemon/lib/headroom-proxy-manager.js +0 -623
  156. package/daemon/lib/mental-model-cache.js +0 -96
  157. package/daemon/lib/project-factory.js +0 -47
  158. package/daemon/lib/session-log-reader.js +0 -93
  159. package/daemon/routes/hed.js +0 -133
  160. package/lib/graph/learning/headroom-learn-bridge.js +0 -215
  161. package/skills/helios-bookkeeping/SKILL.md +0 -321
  162. package/skills/helios-briefer/SKILL.md +0 -44
  163. package/skills/helios-client-relations/SKILL.md +0 -322
  164. package/skills/helios-personal-triager/SKILL.md +0 -45
  165. package/skills/helios-recruitment/SKILL.md +0 -317
  166. package/skills/helios-relationship-nudger/SKILL.md +0 -77
  167. package/skills/helios-researcher/SKILL.md +0 -44
  168. package/skills/helios-scheduler/SKILL.md +0 -58
  169. package/skills/helios-tax-analyst/SKILL.md +0 -280
@@ -14,6 +14,7 @@ const { randomUUID } = crypto;
14
14
  const { HELIOS_ROOT } = require('../lib/paths');
15
15
  const { CompanyBeliefService } = require('../lib/company-belief-service');
16
16
  const { CompanyBeliefDiscovery } = require('../lib/company-belief-discovery');
17
+ const hboStore = require('../../lib/hbo-core-store');
17
18
 
18
19
 
19
20
  const HBO_ROOT = path.resolve(__dirname, '../../skills/helios-business-operator');
@@ -31,7 +32,7 @@ function getCorsHeaders(req) {
31
32
  const allowedOrigin = origin && HBO_ALLOWED_ORIGINS.has(origin) ? origin : 'http://localhost:9093';
32
33
  return {
33
34
  'Access-Control-Allow-Origin': allowedOrigin,
34
- 'Access-Control-Allow-Methods': 'GET, POST, PATCH, OPTIONS',
35
+ 'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE, OPTIONS',
35
36
  'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization',
36
37
  'Vary': 'Origin',
37
38
  };
@@ -430,28 +431,37 @@ async function handleTransitionBusinessTask(req, res, ctx, taskId) {
430
431
  const taskService = loadHBOService('lib/business-task-service.js');
431
432
  if (taskService && typeof taskService.transition === 'function') {
432
433
  try {
433
- const task = await Promise.race([
434
- taskService.transition(taskId, status, cid),
435
- new Promise((_, reject) =>
436
- setTimeout(() => reject(new Error('task-service timeout — falling back to direct')), 5000)
437
- ),
438
- ]);
439
- jsonOk(res, { task });
440
- return;
441
- } catch (_svcErr) {
442
- // service failed for any reason — fall through to direct Memgraph path below
443
- }
444
- }
445
-
446
- // Fallback: update directly via ctx.mgQuery
447
- const now = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
448
- await mgQuery(
449
- `MATCH (bt:BusinessTask {id: $id, companyId: $cid})
450
- SET bt.status = $status, bt.updatedAt = datetime($now)`,
451
- { id: taskId, cid, status, now }
452
- );
453
- _bc({ type: 'task.updated', taskId, companyId: cid, status });
454
- jsonOk(res, { updated: true, taskId, status });
434
+ const task = await Promise.race([
435
+ taskService.transition(taskId, status, cid),
436
+ new Promise((_, reject) =>
437
+ setTimeout(() => reject(new Error('task-service timeout — falling back to direct')), 5000)
438
+ ),
439
+ ]);
440
+ if (task !== null && task !== undefined) {
441
+ jsonOk(res, { task });
442
+ return;
443
+ }
444
+ // Service did not find a task for this company; fall through to the
445
+ // direct path so we can return a clear 404 instead of a silent success.
446
+ } catch (_svcErr) {
447
+ // service failed for any reason — fall through to direct Memgraph path below
448
+ }
449
+ }
450
+
451
+ // Fallback: update directly via ctx.mgQuery
452
+ const now = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
453
+ const result = await mgQuery(
454
+ `MATCH (bt:BusinessTask {id: $id, companyId: $cid})
455
+ SET bt.status = $status, bt.updatedAt = datetime($now)
456
+ RETURN bt.id AS id`,
457
+ { id: taskId, cid, status, now }
458
+ );
459
+ if (parseRows(result).length === 0) {
460
+ jsonErr(res, 404, `BusinessTask ${taskId} not found for company ${cid}`);
461
+ return;
462
+ }
463
+ _bc({ type: 'task.updated', taskId, companyId: cid, status });
464
+ jsonOk(res, { updated: true, taskId, status });
455
465
  } catch (e) {
456
466
  jsonErr(res, 500, `Transition BusinessTask failed: ${safeErrMsg(e)}`);
457
467
  }
@@ -461,34 +471,199 @@ async function handleTransitionBusinessTask(req, res, ctx, taskId) {
461
471
 
462
472
  async function handleGetBusinessGoals(req, res, ctx) {
463
473
  const { mgQuery, cid } = ctx;
464
- if (!mgQuery) { jsonErr(res, 503, 'Memgraph not connected'); return; }
474
+ // SQL-P1: SQLite fallback when Memgraph is unavailable — not all users have Memgraph.
475
+ // Try Memgraph first; on failure (no connection or query error), fall through to SQLite.
476
+ if (mgQuery) {
477
+ try {
478
+ // Get all BusinessGoal nodes
479
+ // BUG-12 fix: BusinessGoal relationships are stored as (child)-[:CHILD_OF]->(parent).
480
+ const result = await mgQuery(
481
+ `MATCH (bg:BusinessGoal {companyId: $cid})
482
+ OPTIONAL MATCH (child:BusinessGoal)-[:CHILD_OF]->(bg)
483
+ WITH bg, [x IN collect(child.id) WHERE x IS NOT NULL] AS childIds
484
+ RETURN bg.id AS id, bg.title AS title, bg.level AS level,
485
+ bg.ownerAgentId AS ownerAgentId, bg.status AS status,
486
+ bg.progress AS progress, childIds
487
+ ORDER BY bg.level ASC`,
488
+ { cid }
489
+ );
490
+ const rows = parseRows(result);
491
+ const keys = ['id', 'title', 'level', 'ownerAgentId', 'status', 'progress', 'childIds'];
492
+ const goals = rows.map(r => {
493
+ const goal = rowToObj(r, keys);
494
+ if (goal.level !== null && goal.level !== undefined) goal.level = String(goal.level);
495
+ return goal;
496
+ });
497
+ const rootGoals = goals.filter(g => !g.level || String(g.level) === 'company');
498
+ return jsonOk(res, { goals, rootGoals, count: goals.length });
499
+ } catch (e) {
500
+ // Memgraph query failed — fall through to SQLite fallback below
501
+ process.stderr.write(`[hbo] handleGetBusinessGoals Memgraph failed, falling back to SQLite: ${e.message}\n`);
502
+ }
503
+ }
504
+ // SQLite fallback (Memgraph unavailable or query failed)
465
505
  try {
466
- // Get all BusinessGoal nodes
467
- // BUG-12 fix: BusinessGoal relationships are stored as (child)-[:CHILD_OF]->(parent).
468
- // The old query used (bg)-[:PARENT_OF]->(child) which matched zero edges.
469
- // Migration note: run `MATCH (bg:BusinessGoal)-[:PARENT_OF]->(c:BusinessGoal)
470
- // MERGE (c)-[:CHILD_OF]->(bg) DELETE relationship` if old edges exist.
471
- const result = await mgQuery(
472
- `MATCH (bg:BusinessGoal {companyId: $cid})
473
- OPTIONAL MATCH (child:BusinessGoal)-[:CHILD_OF]->(bg)
474
- WITH bg, [x IN collect(child.id) WHERE x IS NOT NULL] AS childIds
475
- RETURN bg.id AS id, bg.title AS title, bg.level AS level,
476
- bg.ownerAgentId AS ownerAgentId, bg.status AS status,
477
- bg.progress AS progress, childIds
478
- ORDER BY bg.level ASC`,
479
- { cid }
480
- );
481
- const rows = parseRows(result);
482
- const keys = ['id', 'title', 'level', 'ownerAgentId', 'status', 'progress', 'childIds'];
483
- const goals = rows.map(r => rowToObj(r, keys));
484
- // Build hierarchy tree
485
- const rootGoals = goals.filter(g => !g.level || g.level === 0 || g.level === 'company');
486
- jsonOk(res, { goals, rootGoals, count: goals.length });
487
- } catch (e) {
488
- jsonErr(res, 500, `BusinessGoal query failed: ${safeErrMsg(e)}`);
506
+ const storeGoals = hboStore.getGoalsByCompany ? hboStore.getGoalsByCompany(cid) : [];
507
+ const goals = (storeGoals || []).map(g => ({
508
+ id: g.id ?? g.id,
509
+ title: g.title ?? null,
510
+ level: g.level ? String(g.level) : null,
511
+ ownerAgentId: g.ownerAgentId ?? null,
512
+ status: g.status ?? null,
513
+ progress: g.progress ?? null,
514
+ childIds: g.childIds ?? [],
515
+ }));
516
+ const rootGoals = goals.filter(g => !g.level || g.level === 'company');
517
+ return jsonOk(res, { goals, rootGoals, count: goals.length, _source: 'sqlite' });
518
+ } catch (storeErr) {
519
+ return jsonErr(res, 503, `Goals unavailable: Memgraph not connected and SQLite fallback failed: ${storeErr.message}`);
520
+ }
521
+ }
522
+
523
+ async function handleGetBusinessGoal(req, res, ctx, goalId) {
524
+ const { mgQuery, cid } = ctx;
525
+ if (!goalId) { jsonErr(res, 400, 'Missing goal ID'); return; }
526
+ // SQL-P2: SQLite fallback when Memgraph unavailable.
527
+ if (mgQuery) {
528
+ try {
529
+ const result = await mgQuery(
530
+ `MATCH (g:BusinessGoal {id: $id, companyId: $cid})
531
+ RETURN g.id AS id, g.title AS title, g.description AS description,
532
+ g.level AS level, g.status AS status, g.parentId AS parentId,
533
+ g.ownerAgentId AS ownerAgentId, g.progress AS progress`,
534
+ { id: goalId, cid }
535
+ );
536
+ const rows = parseRows(result);
537
+ if (rows.length === 0) { jsonErr(res, 404, `BusinessGoal ${goalId} not found`); return; }
538
+ const keys = ['id', 'title', 'description', 'level', 'status', 'parentId', 'ownerAgentId', 'progress'];
539
+ return jsonOk(res, { goal: rowToObj(rows[0], keys) });
540
+ } catch (e) {
541
+ process.stderr.write(`[hbo] handleGetBusinessGoal Memgraph failed, falling back to SQLite: ${e.message}\n`);
542
+ }
543
+ }
544
+ // SQLite fallback
545
+ try {
546
+ const g = hboStore.getGoal ? hboStore.getGoal(goalId, cid) : null;
547
+ if (!g) { jsonErr(res, 404, `BusinessGoal ${goalId} not found`); return; }
548
+ return jsonOk(res, { goal: g, _source: 'sqlite' });
549
+ } catch (storeErr) {
550
+ return jsonErr(res, 503, `Goal unavailable: ${storeErr.message}`);
489
551
  }
490
552
  }
491
553
 
554
+ async function handleCreateBusinessGoal(req, res, ctx) {
555
+ const { mgQuery, cid } = ctx;
556
+ if (!mgQuery) { jsonErr(res, 503, 'Memgraph not connected'); return; }
557
+ // H-4 fix: reject if cid is missing or is the fallback sentinel — prevents silent writes to 'default-company'
558
+ if (!cid || cid === 'default-company') { jsonErr(res, 400, 'companyId is required'); return; }
559
+ try {
560
+ const body = await readBody(req);
561
+ if (!assertValidBody(body, res)) return;
562
+ const title = typeof body.title === 'string' ? body.title.trim() : '';
563
+ if (!title) { jsonErr(res, 400, 'title is required'); return; }
564
+
565
+ const goalId = body.id ? String(body.id) : `bg:${randomUUID()}`;
566
+ const description = body.description !== undefined ? String(body.description) : null;
567
+ const level = body.level !== undefined ? String(body.level) : 'company';
568
+ const parentId = body.parentId || body.parentGoalId ? String(body.parentId || body.parentGoalId) : null;
569
+
570
+ await mgQuery(
571
+ `CREATE (g:BusinessGoal {
572
+ id: $id, companyId: $cid, title: $title, description: $desc,
573
+ level: $level, status: 'active', parentId: $parentId,
574
+ parentGoalId: $parentId, createdAt: datetime(), updatedAt: datetime()
575
+ })`,
576
+ { id: goalId, cid, title, desc: description, level, parentId }
577
+ );
578
+ try {
579
+ hboStore.createGoal?.({ id: goalId, companyId: cid, title, description, level, status: 'active', parentId, createdAt: Date.now() });
580
+ } catch (storeErr) {
581
+ process.stderr.write(`[hbo] goal mirror failed: ${storeErr.message}\n`);
582
+ }
583
+ try { ctx._bc?.({ type: 'goal.created', companyId: cid, goalId }); } catch (_) {}
584
+ jsonOk(res, { goal: { id: goalId, companyId: cid, title, description, level, status: 'active', parentId } }, 201);
585
+ } catch (e) {
586
+ jsonErr(res, 500, `Create BusinessGoal failed: ${safeErrMsg(e)}`);
587
+ }
588
+ }
589
+
590
+ async function handleUpdateBusinessGoal(req, res, ctx, goalId) {
591
+ const { mgQuery, cid } = ctx;
592
+ if (!mgQuery) { jsonErr(res, 503, 'Memgraph not connected'); return; }
593
+ if (!goalId) { jsonErr(res, 400, 'Missing goal ID'); return; }
594
+ try {
595
+ const body = await readBody(req);
596
+ if (!assertValidBody(body, res)) return;
597
+ const title = body.title !== undefined ? String(body.title).trim() : null;
598
+ const description = body.description !== undefined ? String(body.description) : null;
599
+ const level = body.level !== undefined ? String(body.level) : null;
600
+ const status = body.status !== undefined ? String(body.status) : null;
601
+ const parentId = body.parentId !== undefined ? (body.parentId ? String(body.parentId) : null) : undefined;
602
+
603
+ if (title === '' || (title === null && description === null && level === null && status === null && parentId === undefined)) {
604
+ jsonErr(res, 400, 'At least one of title, description, level, status, parentId must be provided');
605
+ return;
606
+ }
607
+
608
+ const result = await mgQuery(
609
+ `MATCH (g:BusinessGoal {id: $id, companyId: $cid})
610
+ SET g.title = COALESCE($title, g.title),
611
+ g.description = CASE WHEN $hasDescription THEN $description ELSE g.description END,
612
+ g.level = COALESCE($level, g.level),
613
+ g.status = COALESCE($status, g.status),
614
+ g.parentId = CASE WHEN $hasParentId THEN $parentId ELSE g.parentId END,
615
+ g.parentGoalId = CASE WHEN $hasParentId THEN $parentId ELSE g.parentGoalId END,
616
+ g.updatedAt = datetime()
617
+ RETURN g.id AS id, g.title AS title, g.description AS description,
618
+ g.level AS level, g.status AS status, g.parentId AS parentId`,
619
+ {
620
+ id: goalId, cid, title, description, level, status,
621
+ parentId: parentId === undefined ? null : parentId,
622
+ hasDescription: body.description !== undefined,
623
+ hasParentId: parentId !== undefined,
624
+ }
625
+ );
626
+ const rows = parseRows(result);
627
+ if (rows.length === 0) { jsonErr(res, 404, `BusinessGoal ${goalId} not found`); return; }
628
+ const goal = rowToObj(rows[0], ['id', 'title', 'description', 'level', 'status', 'parentId']);
629
+ // H-3 fix: only pass fields that were explicitly provided in the PATCH body to updateGoal.
630
+ // rowToObj fills missing Memgraph fields with null — passing null for e.g. title would
631
+ // overwrite the existing SQLite title with null on a status-only update.
632
+ const storeUpdate: Record<string, any> = { id: goalId };
633
+ if (title !== null) storeUpdate.title = goal.title;
634
+ if (description !== null) storeUpdate.description = goal.description;
635
+ if (level !== null) storeUpdate.level = goal.level;
636
+ if (status !== null) storeUpdate.status = goal.status;
637
+ if (parentId !== undefined) storeUpdate.parentId = goal.parentId;
638
+ try { hboStore.updateGoal?.(goalId, cid, storeUpdate); } catch (storeErr) { process.stderr.write(`[hbo] goal mirror failed: ${storeErr.message}\n`); }
639
+ try { ctx._bc?.({ type: 'goal.updated', companyId: cid, goalId }); } catch (_) {}
640
+ jsonOk(res, { goal });
641
+ } catch (e) {
642
+ jsonErr(res, 500, `Update BusinessGoal failed: ${safeErrMsg(e)}`);
643
+ }
644
+ }
645
+
646
+ async function handleDeleteBusinessGoal(req, res, ctx, goalId) {
647
+ const { mgQuery, cid } = ctx;
648
+ if (!mgQuery) { jsonErr(res, 503, 'Memgraph not connected'); return; }
649
+ if (!goalId) { jsonErr(res, 400, 'Missing goal ID'); return; }
650
+ try {
651
+ const result = await mgQuery(
652
+ `MATCH (g:BusinessGoal {id: $id, companyId: $cid})
653
+ WITH g, g.id AS deletedId
654
+ DETACH DELETE g
655
+ RETURN deletedId AS id`,
656
+ { id: goalId, cid }
657
+ );
658
+ if (parseRows(result).length === 0) { jsonErr(res, 404, `BusinessGoal ${goalId} not found`); return; }
659
+ try { hboStore.deleteGoal?.(goalId, cid); } catch (storeErr) { process.stderr.write(`[hbo] goal mirror failed: ${storeErr.message}\n`); }
660
+ try { ctx._bc?.({ type: 'goal.deleted', companyId: cid, goalId }); } catch (_) {}
661
+ jsonOk(res, { deleted: true, goalId });
662
+ } catch (e) {
663
+ jsonErr(res, 500, `Delete BusinessGoal failed: ${safeErrMsg(e)}`);
664
+ }
665
+ }
666
+
492
667
  // ── Warm signals handler ──────────────────────────────────────────────────────
493
668
 
494
669
  async function handleGetWarmSignals(req, res, ctx) {
@@ -648,20 +823,42 @@ async function handleGetMandala(req, res, ctx) {
648
823
  // Build the pillar MATCH differently depending on whether goalId is provided.
649
824
  // Memgraph requires extracting all properties into aliases BEFORE OPTIONAL MATCH;
650
825
  // referencing gp.property AFTER OPTIONAL MATCH causes "Unbound variable: gp."
651
- const pillarQuery = goalId
652
- ? `MATCH (gp:GoalPillar {companyId: $cid, goalId: $goalId})
653
- WITH gp.id AS id, gp.name AS name, gp.pillarIndex AS pillarIndex,
654
- gp.goalId AS goalId, gp.description AS description, gp AS gpNode
655
- OPTIONAL MATCH (gpNode)-[:HAS_CELL]->(ac:ActionCell)
656
- RETURN id, name, pillarIndex, goalId, description,
657
- collect({id: ac.id, action: ac.action, status: ac.status, weekIndex: ac.weekIndex}) AS cells
658
- ORDER BY pillarIndex ASC LIMIT toInteger(8)`
659
- : `MATCH (gp:GoalPillar {companyId: $cid})
660
- WITH gp.id AS id, gp.name AS name, gp.pillarIndex AS pillarIndex,
661
- gp.goalId AS goalId, gp.description AS description, gp AS gpNode
662
- OPTIONAL MATCH (gpNode)-[:HAS_CELL]->(ac:ActionCell)
663
- RETURN id, name, pillarIndex, goalId, description,
664
- collect({id: ac.id, action: ac.action, status: ac.status, weekIndex: ac.weekIndex}) AS cells
826
+ const pillarQuery = goalId
827
+ ? `MATCH (gp:GoalPillar {companyId: $cid, goalId: $goalId})
828
+ WITH gp.id AS id, gp.name AS name, gp.pillarIndex AS pillarIndex,
829
+ gp.goalId AS goalId, gp.description AS description,
830
+ coalesce(gp.l2ReviewStatus, null) AS l2ReviewStatus,
831
+ coalesce(gp.l2Strategy, null) AS l2Strategy,
832
+ gp AS gpNode
833
+ OPTIONAL MATCH (gpNode)-[:HAS_CELL]->(ac:ActionCell)
834
+ OPTIONAL MATCH (gpNode)<-[:FOR_PILLAR]-(approval:Approval)
835
+ WHERE approval.type IN ['harada_l2_review','harada_l3_review'] AND approval.status = 'pending'
836
+ WITH id, name, pillarIndex, goalId, description, l2ReviewStatus, l2Strategy, gpNode,
837
+ collect(DISTINCT {id: ac.id, description: ac.description, status: ac.status, cellIndex: ac.cellIndex}) AS cells,
838
+ count(DISTINCT approval) AS openReviewCount
839
+ WITH id, name, pillarIndex, goalId, description, l2ReviewStatus, l2Strategy, cells, openReviewCount,
840
+ size(cells) AS cellCount,
841
+ reduce(acc = 0, c IN cells | acc + CASE WHEN c.status = 'closed' THEN 1 ELSE 0 END) AS closedCellCount
842
+ RETURN id, name, pillarIndex, goalId, description, l2ReviewStatus, l2Strategy,
843
+ cells, openReviewCount, cellCount, closedCellCount
844
+ ORDER BY pillarIndex ASC LIMIT toInteger(8)`
845
+ : `MATCH (gp:GoalPillar {companyId: $cid})
846
+ WITH gp.id AS id, gp.name AS name, gp.pillarIndex AS pillarIndex,
847
+ gp.goalId AS goalId, gp.description AS description,
848
+ coalesce(gp.l2ReviewStatus, null) AS l2ReviewStatus,
849
+ coalesce(gp.l2Strategy, null) AS l2Strategy,
850
+ gp AS gpNode
851
+ OPTIONAL MATCH (gpNode)-[:HAS_CELL]->(ac:ActionCell)
852
+ OPTIONAL MATCH (gpNode)<-[:FOR_PILLAR]-(approval:Approval)
853
+ WHERE approval.type IN ['harada_l2_review','harada_l3_review'] AND approval.status = 'pending'
854
+ WITH id, name, pillarIndex, goalId, description, l2ReviewStatus, l2Strategy, gpNode,
855
+ collect(DISTINCT {id: ac.id, description: ac.description, status: ac.status, cellIndex: ac.cellIndex}) AS cells,
856
+ count(DISTINCT approval) AS openReviewCount
857
+ WITH id, name, pillarIndex, goalId, description, l2ReviewStatus, l2Strategy, cells, openReviewCount,
858
+ size(cells) AS cellCount,
859
+ reduce(acc = 0, c IN cells | acc + CASE WHEN c.status = 'closed' THEN 1 ELSE 0 END) AS closedCellCount
860
+ RETURN id, name, pillarIndex, goalId, description, l2ReviewStatus, l2Strategy,
861
+ cells, openReviewCount, cellCount, closedCellCount
665
862
  ORDER BY pillarIndex ASC LIMIT toInteger(8)`;
666
863
 
667
864
  const saQuery = agentId
@@ -715,7 +912,20 @@ async function handleGetMandala(req, res, ctx) {
715
912
  mgQuery(deptAgentQuery, { cid }).catch(() => ({ rows: [], keys: [] })),
716
913
  ]);
717
914
 
718
- const pillarKeys = ['id', 'name', 'pillarIndex', 'goalId', 'description', 'cells'];
915
+ // Map pillar names to department slugs GoalPillar nodes never have a .department
916
+ // property written to them, so we derive the slug from the pillar's canonical name.
917
+ const PILLAR_NAME_TO_DEPT_SLUG = {
918
+ 'Revenue & Sales': 'sales',
919
+ 'Product & Delivery': 'engineering',
920
+ 'Customer Success': 'customer_success',
921
+ 'Technology & Infrastructure': 'engineering',
922
+ 'Team & Capability': 'people',
923
+ 'Brand & Positioning': 'marketing',
924
+ 'Finance & Sustainability': 'finance',
925
+ 'Operations & Process': 'operations',
926
+ };
927
+
928
+ const pillarKeys = ['id', 'name', 'pillarIndex', 'goalId', 'description', 'l2ReviewStatus', 'l2Strategy', 'cells', 'openReviewCount', 'cellCount', 'closedCellCount'];
719
929
  const saKeys = ['id', 'agentId', 'strengths', 'weaknesses', 'opportunities', 'createdAt'];
720
930
  const cascadeKeys = ['id', 'agentId', 'weekNumber', 'weeklyGoal', 'dailyTasks', 'completionRate'];
721
931
  const deptAgentKeys = ['id', 'role', 'departmentId', 'status', 'currentTaskId', 'currentTask', 'taskStartedAt'];
@@ -733,7 +943,13 @@ async function handleGetMandala(req, res, ctx) {
733
943
  });
734
944
 
735
945
  jsonOk(res, {
736
- pillars: parseRows(pillarResult).map(r => rowToObj(r, pillarKeys)),
946
+ pillars: parseRows(pillarResult).map(r => {
947
+ const p = rowToObj(r, pillarKeys);
948
+ // Derive department slug from pillar name so the frontend can build dept nav items.
949
+ // GoalPillar.department is indexed in the schema but never written by any daemon path.
950
+ p.department = PILLAR_NAME_TO_DEPT_SLUG[p.name] || null;
951
+ return p;
952
+ }),
737
953
  selfAnalysis: parseRows(selfAnalysisResult).map(r => rowToObj(r, saKeys)),
738
954
  cascades: parseRows(cascadeResult).map(r => rowToObj(r, cascadeKeys)),
739
955
  deptAgents, // live BusinessAgent + Task join for DepartmentPage TeamTab
@@ -1172,13 +1388,20 @@ async function handleCreatePdsa(req, res, ctx) {
1172
1388
  // in every department's decisions tab regardless of which pillar they belong to.
1173
1389
  // Task.pillarId is set by pillar-dispatcher.js when a task is dispatched to a pillar.
1174
1390
  try {
1175
- const taskRows = await mgQuery(
1176
- `MATCH (t:Task {id: $taskId, companyId: $cid}) RETURN t.pillarId AS pillarId LIMIT 1`,
1177
- { taskId: taskId.trim(), cid }
1178
- );
1179
- const pillarId = taskRows && taskRows.rows && taskRows.rows[0]
1180
- ? (Array.isArray(taskRows.rows[0]) ? taskRows.rows[0][0] : taskRows.rows[0].pillarId)
1181
- : null;
1391
+ // SQLite-first task read for pillarId (P2-10)
1392
+ let pillarId = null;
1393
+ const _storeTask = hboStore.getTask ? hboStore.getTask(taskId.trim(), cid) : null;
1394
+ if (_storeTask) {
1395
+ pillarId = _storeTask.pillarId ?? null;
1396
+ } else {
1397
+ const taskRows = await mgQuery(
1398
+ `MATCH (t:Task {id: $taskId, companyId: $cid}) RETURN t.pillarId AS pillarId LIMIT 1`,
1399
+ { taskId: taskId.trim(), cid }
1400
+ );
1401
+ pillarId = taskRows && taskRows.rows && taskRows.rows[0]
1402
+ ? (Array.isArray(taskRows.rows[0]) ? taskRows.rows[0][0] : taskRows.rows[0].pillarId)
1403
+ : null;
1404
+ }
1182
1405
  if (pillarId && typeof pillarId === 'string' && pillarId.trim()) {
1183
1406
  await mgQuery(
1184
1407
  `MATCH (p:PDSACycle {id: $id, companyId: $cid}) SET p.pillarId = $pillarId`,
@@ -2126,6 +2349,7 @@ async function handleResolveEmailInfraApproval(req, res, ctx, approvalId) {
2126
2349
  */
2127
2350
  async function handleGetHboApprovals(req, res, ctx) {
2128
2351
  try {
2352
+ if (!ctx.mgQuery) { jsonErr(res, 503, 'Memgraph not connected'); return; }
2129
2353
  const url = new URL(req.url, 'http://localhost');
2130
2354
  const status = url.searchParams.get('status') || null;
2131
2355
  const type = url.searchParams.get('type') || null;
@@ -2149,6 +2373,17 @@ async function handleGetHboApprovals(req, res, ctx) {
2149
2373
  agentId: p?.agentId ?? null, taskId: p?.taskId ?? null,
2150
2374
  urgency: p?.urgency ?? null, humanAnswer: p?.humanAnswer ?? null,
2151
2375
  answeredVia: p?.answeredVia ?? null, createdAt: p?.createdAt ?? null,
2376
+ description: p?.description ?? null,
2377
+ question: p?.question ?? null,
2378
+ defaultAnswer: p?.defaultAnswer ?? null,
2379
+ ruleStrength: p?.ruleStrength ?? null,
2380
+ sourceAgent: p?.sourceAgent ?? null,
2381
+ pillarId: p?.pillarId ?? null,
2382
+ expiresAt: p?.expiresAt ?? null,
2383
+ kaizenProposalId: p?.kaizenProposalId ?? null,
2384
+ department: p?.department ?? null,
2385
+ templateKey: p?.templateKey ?? null,
2386
+ inferredAnswer: p?.inferredAnswer ?? null,
2152
2387
  };
2153
2388
  });
2154
2389
  jsonOk(res, { approvals, count: approvals.length });
@@ -2338,14 +2573,14 @@ async function handleGetTriageSummary(req, res, ctx) {
2338
2573
  { cid }
2339
2574
  );
2340
2575
  const rows = parseRows(result);
2341
- const counts = { p0: 0, p1: 0, p2: 0, p3: 0, total: 0 };
2576
+ const counts = { p0Count: 0, p1Count: 0, p2Count: 0, p3Count: 0, total: 0, topItems: [], generatedAt: new Date().toISOString() };
2342
2577
  for (const row of rows) {
2343
2578
  const pri = (row[0] ?? row['priority'] ?? '').toString().toUpperCase();
2344
2579
  const cnt = Number(row[1] ?? row['cnt'] ?? 0);
2345
- if (pri === 'P0') counts.p0 = cnt;
2346
- else if (pri === 'P1') counts.p1 = cnt;
2347
- else if (pri === 'P2') counts.p2 = cnt;
2348
- else if (pri === 'P3') counts.p3 = cnt;
2580
+ if (pri === 'P0') counts.p0Count = cnt;
2581
+ else if (pri === 'P1') counts.p1Count = cnt;
2582
+ else if (pri === 'P2') counts.p2Count = cnt;
2583
+ else if (pri === 'P3') counts.p3Count = cnt;
2349
2584
  counts.total += cnt;
2350
2585
  }
2351
2586
  jsonOk(res, counts);
@@ -2683,6 +2918,14 @@ module.exports = function createHBORouter(handlers) {
2683
2918
  return false;
2684
2919
  }
2685
2920
 
2921
+ // H-2 fix: Handle OPTIONS preflight for all HBO routes (including DELETE /api/hbo/goals/:id).
2922
+ // Must be BEFORE auth check — browsers never send credentials on preflight requests.
2923
+ if (method === 'OPTIONS') {
2924
+ res.writeHead(204, getCorsHeaders(req));
2925
+ res.end();
2926
+ return true;
2927
+ }
2928
+
2686
2929
  // Auth check for all HBO routes
2687
2930
  const authHeader = (req.headers || {})['authorization'];
2688
2931
  const expected = ctx.apiToken;
@@ -2773,6 +3016,30 @@ module.exports = function createHBORouter(handlers) {
2773
3016
  return true;
2774
3017
  }
2775
3018
 
3019
+ // GET /api/hbo/goals/:id — must be checked before POST/PATCH/DELETE goalMatch
3020
+ const goalIdOnlyMatch = pathname.match(/^\/api\/hbo\/goals\/([^/]+)$/);
3021
+ if (method === 'GET' && goalIdOnlyMatch) {
3022
+ await handleGetBusinessGoal(req, res, ctx, decodeURIComponent(goalIdOnlyMatch[1]));
3023
+ return true;
3024
+ }
3025
+
3026
+ // POST /api/hbo/goals
3027
+ if (method === 'POST' && pathname === '/api/hbo/goals') {
3028
+ await handleCreateBusinessGoal(req, res, ctx);
3029
+ return true;
3030
+ }
3031
+
3032
+ // PATCH/DELETE /api/hbo/goals/:id
3033
+ const goalMatch = pathname.match(/^\/api\/hbo\/goals\/(.+)$/);
3034
+ if (method === 'PATCH' && goalMatch) {
3035
+ await handleUpdateBusinessGoal(req, res, ctx, decodeURIComponent(goalMatch[1]));
3036
+ return true;
3037
+ }
3038
+ if (method === 'DELETE' && goalMatch) {
3039
+ await handleDeleteBusinessGoal(req, res, ctx, decodeURIComponent(goalMatch[1]));
3040
+ return true;
3041
+ }
3042
+
2776
3043
  // GET /api/hbo/triage-summary
2777
3044
  if (method === 'GET' && pathname === '/api/hbo/triage-summary') {
2778
3045
  await handleGetTriageSummary(req, res, ctx);
File without changes