@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.
- package/agents/business/talisman-ceo.md +183 -0
- package/agents/business/talisman-comms.md +257 -0
- package/agents/business/talisman-cto.md +153 -0
- package/agents/business/talisman-finance.md +246 -0
- package/agents/business/talisman-marketing.md +240 -0
- package/agents/business/talisman-sales.md +242 -0
- package/agents/business/talisman-support.md +236 -0
- package/bin/helios-rpc.js +19 -0
- package/daemon/adapters/helios-rpc-adapter.js +5 -12
- package/daemon/adapters/tui_wakeup.js +8 -0
- package/daemon/context-enrichment.js +27 -0
- package/daemon/daemon-manager.js +1 -1
- package/daemon/db/email-infrastructure-migrate.js +192 -0
- package/daemon/db/hbo-core-migrate.js +189 -0
- package/daemon/helios-api.js +863 -64
- package/daemon/helios-company-daemon.js +233 -33
- package/daemon/lib/blast-radius-analyzer.js +75 -0
- package/daemon/lib/domain-bootstrap-orchestrator.js +267 -0
- package/daemon/lib/forensic-log.js +113 -0
- package/daemon/lib/goal-research-pipeline.js +644 -0
- package/daemon/lib/harada/cascade-judge.js +84 -1
- package/daemon/lib/harada/cascade-research-dispatcher.js +282 -0
- package/daemon/lib/harada/pillar-dispatcher.js +23 -2
- package/daemon/lib/hbo-bridge.js +74 -6
- package/daemon/lib/headroom-middleware.js +129 -0
- package/daemon/lib/headroom-proxy-manager.js +309 -0
- package/daemon/lib/hed-engine.js +25 -0
- package/daemon/lib/intelligence/department-page-generator.js +46 -1
- package/daemon/lib/interpretation-engine.js +92 -0
- package/daemon/lib/mental-model-cache.js +96 -0
- package/daemon/lib/project-factory.js +47 -0
- package/daemon/lib/session-log-reader.js +93 -0
- package/daemon/lib/standard-work-bootstrap.js +87 -1
- package/daemon/lib/task-completion-processor.js +23 -0
- package/daemon/lib/wizard-engine.js +57 -6
- package/daemon/package.json +2 -1
- package/daemon/routes/agents.js +51 -6
- package/daemon/routes/channels.js +116 -2
- package/daemon/routes/crm.js +85 -0
- package/daemon/routes/dashboard.js +62 -16
- package/daemon/routes/dept.js +10 -1
- package/daemon/routes/email-triage.js +19 -10
- package/daemon/routes/hbo.js +618 -58
- package/daemon/routes/hed.js +133 -0
- package/daemon/routes/inbox.js +397 -8
- package/daemon/routes/project.js +580 -66
- package/daemon/routes/routines.js +14 -0
- package/daemon/routes/tasks.js +15 -1
- package/daemon/schema-apply.js +174 -0
- package/daemon/schema-definitions.js +433 -0
- package/daemon/schema-migrations-hbo.js +20 -0
- package/daemon/schema-migrations-hed.js +18 -0
- package/daemon/schema-migrations-proj.js +153 -0
- package/extensions/__tests__/codebase-index.test.ts +73 -0
- package/extensions/__tests__/extension-command-registration.test.ts +35 -0
- package/extensions/__tests__/git-push-guard.test.ts +68 -0
- package/extensions/context-compaction.ts +104 -76
- package/extensions/cortex/__tests__/cortex-core.test.ts +100 -0
- package/extensions/cortex/wal-replay.ts +91 -0
- package/extensions/email/actions/draft-response.ts +21 -1
- package/extensions/email/auth/accounts.ts +5 -11
- package/extensions/email/auth/inbox-dog.ts +5 -2
- package/extensions/email/backfill.ts +20 -13
- package/extensions/email/providers/gmail.ts +164 -0
- package/extensions/email/providers/google-calendar.ts +34 -5
- package/extensions/helios-browser/__tests__/browser-routing.test.ts +57 -0
- package/extensions/helios-browser/backends/playwright.ts +3 -1
- package/extensions/helios-governance/__tests__/governance-gates.test.ts +40 -0
- package/extensions/helios-governance/__tests__/tournament-consumer.test.js +66 -0
- package/extensions/hema-dispatch-v3/headroom-compress.ts +103 -0
- package/extensions/hema-dispatch-v3/index.ts +46 -72
- package/extensions/interview/__tests__/server.test.ts +117 -0
- package/extensions/lib/helios-root.cjs +46 -0
- package/extensions/subagent-mesh/__tests__/handlers.test.ts +98 -0
- package/extensions/warm-tick/warm-tick-maintenance.ts +164 -0
- package/lib/__tests__/bulk-ingest.live.test.ts +66 -0
- package/lib/__tests__/crash-fixes.test.ts +49 -0
- package/lib/__tests__/hbo-core-store.test.js +238 -0
- package/lib/__tests__/maintenance-mission-wiring.test.ts +35 -0
- package/lib/broker/__tests__/jit-subscription.test.js +44 -1
- package/lib/broker/__tests__/lifecycle-channels.test.js +25 -1
- package/lib/compression/__tests__/ccr-store.test.js +138 -0
- package/lib/compression/__tests__/pipeline.test.js +280 -0
- package/lib/compression/__tests__/smart-crusher.test.js +242 -0
- package/lib/compression/dist/server.js +34 -1
- package/lib/compression/dist/start-server.js +77 -0
- package/lib/event-bus.mts +1 -1
- package/lib/graph/learning/headroom-learn-bridge.js +175 -0
- package/lib/graph-availability.js +62 -0
- package/lib/hbo-core-store.compiled.js +834 -0
- package/lib/hbo-core-store.js +124 -0
- package/lib/hbo-core-store.ts +979 -0
- package/lib/mission-loop/__tests__/research-handler.test.ts +143 -0
- package/lib/skill-sync.js +6 -1
- package/lib/startup-integrity.js +9 -2
- package/lib/triage-core/__tests__/classifier-fixture.test.ts +254 -0
- package/lib/triage-core/__tests__/classifier-post-norm.test.ts +1 -1
- package/lib/triage-core/__tests__/classifier.test.ts +45 -7
- package/lib/triage-core/__tests__/correction-detector.test.ts +36 -0
- package/lib/triage-core/__tests__/d6-dunbar-boost.test.ts +5 -5
- package/lib/triage-core/__tests__/orchestrator-pipeline.test.ts +107 -0
- package/lib/triage-core/__tests__/orchestrator.test.ts +113 -1
- package/lib/triage-core/__tests__/signals.test.ts +357 -0
- package/lib/triage-core/__tests__/sql-parity.test.ts +216 -0
- package/lib/triage-core/backfill-cost-estimator.ts +91 -0
- package/lib/triage-core/backfill-orchestrator.ts +119 -0
- package/lib/triage-core/classifier.ts +41 -8
- package/lib/triage-core/cos/cross-channel-escalation.ts +2 -3
- package/lib/triage-core/cos/response-debt.ts +2 -2
- package/lib/triage-core/graph/__tests__/batch-persistence.test.ts +283 -0
- package/lib/triage-core/graph/batch-persistence.ts +66 -2
- package/lib/triage-core/graph/betweenness-worker.js +75 -0
- package/lib/triage-core/graph/graph-rank-sql.ts +67 -0
- package/lib/triage-core/graph/persistence.ts +1 -1
- package/lib/triage-core/graph/schema-v2.ts +2 -0
- package/lib/triage-core/graph/schema.cypher +11 -0
- package/lib/triage-core/graph/triage-query.ts +1 -1
- package/lib/triage-core/learning.ts +15 -20
- package/lib/triage-core/mental-model/bedrock-config.ts +78 -0
- package/lib/triage-core/mental-model/cos-integration.ts +1 -1
- package/lib/triage-core/mental-model/entity-extractor.ts +51 -4
- package/lib/triage-core/mental-model/identity-resolver.ts +5 -5
- package/lib/triage-core/mental-model/key-facts.ts +1 -2
- package/lib/triage-core/mental-model/model-assembler-sql.ts +200 -0
- package/lib/triage-core/mental-model/model-assembler.ts +16 -3
- package/lib/triage-core/orchestrator.ts +8 -15
- package/lib/triage-core/scheduled-sends.ts +39 -2
- package/lib/triage-core/signals/comms-style.ts +1 -1
- package/lib/triage-core/signals/cross-channel-escalation.ts +2 -2
- package/lib/triage-core/signals/favee-type.ts +6 -1
- package/lib/triage-core/signals/goal-relevance.ts +31 -2
- package/lib/triage-core/signals/personal-importance.ts +1 -1
- package/lib/triage-core/signals/referral-chain.ts +0 -1
- package/lib/triage-core/signals/relationship-decay.ts +4 -0
- package/lib/triage-core/signals/relationship-health.ts +6 -1
- package/lib/triage-core/signals/trajectory-signal.ts +38 -3
- package/lib/triage-core/tournament-runner.js +11 -1
- package/lib/triage-core/triage-llm-factory.ts +110 -0
- package/lib/triage-core/triage-local-llm.ts +145 -0
- package/lib/triage-core/triage-sql-store.ts +337 -0
- package/lib/triage-core/types.ts +2 -2
- package/lib/unified-graph.atomic.test.ts +52 -0
- package/lib/unified-graph.failure-categories.test.ts +55 -0
- package/package.json +18 -7
- package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
- package/prebuilds/linux-x64/better_sqlite3.node +0 -0
- package/prebuilds/win32-x64/better_sqlite3.node +0 -0
- package/skills/helios-bookkeeping/SKILL.md +321 -0
- package/skills/helios-briefer/SKILL.md +44 -0
- package/skills/helios-client-relations/SKILL.md +322 -0
- package/skills/helios-personal-triager/SKILL.md +45 -0
- package/skills/helios-recruitment/SKILL.md +317 -0
- package/skills/helios-relationship-nudger/SKILL.md +77 -0
- package/skills/helios-researcher/SKILL.md +44 -0
- package/skills/helios-scheduler/SKILL.md +58 -0
- package/skills/helios-tax-analyst/SKILL.md +280 -0
- package/lib/triage-core/orchestrator.ts.bak-r005-r006-r008 +0 -1823
package/daemon/routes/hbo.js
CHANGED
|
@@ -32,7 +32,7 @@ function getCorsHeaders(req) {
|
|
|
32
32
|
const allowedOrigin = origin && HBO_ALLOWED_ORIGINS.has(origin) ? origin : 'http://localhost:9093';
|
|
33
33
|
return {
|
|
34
34
|
'Access-Control-Allow-Origin': allowedOrigin,
|
|
35
|
-
'Access-Control-Allow-Methods': 'GET, POST, PATCH, OPTIONS',
|
|
35
|
+
'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE, OPTIONS',
|
|
36
36
|
'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization',
|
|
37
37
|
'Vary': 'Origin',
|
|
38
38
|
};
|
|
@@ -431,28 +431,37 @@ async function handleTransitionBusinessTask(req, res, ctx, taskId) {
|
|
|
431
431
|
const taskService = loadHBOService('lib/business-task-service.js');
|
|
432
432
|
if (taskService && typeof taskService.transition === 'function') {
|
|
433
433
|
try {
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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 });
|
|
456
465
|
} catch (e) {
|
|
457
466
|
jsonErr(res, 500, `Transition BusinessTask failed: ${safeErrMsg(e)}`);
|
|
458
467
|
}
|
|
@@ -462,34 +471,206 @@ async function handleTransitionBusinessTask(req, res, ctx, taskId) {
|
|
|
462
471
|
|
|
463
472
|
async function handleGetBusinessGoals(req, res, ctx) {
|
|
464
473
|
const { mgQuery, cid } = ctx;
|
|
465
|
-
|
|
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)
|
|
466
505
|
try {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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}`);
|
|
490
551
|
}
|
|
491
552
|
}
|
|
492
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
|
+
// Fire-and-forget: trigger tickGoalDecompose so the Harada cascade begins immediately
|
|
585
|
+
// (normally runs every 5th tick = up to 150s; injected hook shortens this to ~0ms).
|
|
586
|
+
if (_triggerGoalDecompose) {
|
|
587
|
+
setImmediate(() => {
|
|
588
|
+
try { _triggerGoalDecompose(cid); } catch (_tgdErr) {}
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
jsonOk(res, { goal: { id: goalId, companyId: cid, title, description, level, status: 'active', parentId } }, 201);
|
|
592
|
+
} catch (e) {
|
|
593
|
+
jsonErr(res, 500, `Create BusinessGoal failed: ${safeErrMsg(e)}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function handleUpdateBusinessGoal(req, res, ctx, goalId) {
|
|
598
|
+
const { mgQuery, cid } = ctx;
|
|
599
|
+
if (!mgQuery) { jsonErr(res, 503, 'Memgraph not connected'); return; }
|
|
600
|
+
if (!goalId) { jsonErr(res, 400, 'Missing goal ID'); return; }
|
|
601
|
+
try {
|
|
602
|
+
const body = await readBody(req);
|
|
603
|
+
if (!assertValidBody(body, res)) return;
|
|
604
|
+
const title = body.title !== undefined ? String(body.title).trim() : null;
|
|
605
|
+
const description = body.description !== undefined ? String(body.description) : null;
|
|
606
|
+
const level = body.level !== undefined ? String(body.level) : null;
|
|
607
|
+
const status = body.status !== undefined ? String(body.status) : null;
|
|
608
|
+
const parentId = body.parentId !== undefined ? (body.parentId ? String(body.parentId) : null) : undefined;
|
|
609
|
+
|
|
610
|
+
if (title === '' || (title === null && description === null && level === null && status === null && parentId === undefined)) {
|
|
611
|
+
jsonErr(res, 400, 'At least one of title, description, level, status, parentId must be provided');
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const result = await mgQuery(
|
|
616
|
+
`MATCH (g:BusinessGoal {id: $id, companyId: $cid})
|
|
617
|
+
SET g.title = COALESCE($title, g.title),
|
|
618
|
+
g.description = CASE WHEN $hasDescription THEN $description ELSE g.description END,
|
|
619
|
+
g.level = COALESCE($level, g.level),
|
|
620
|
+
g.status = COALESCE($status, g.status),
|
|
621
|
+
g.parentId = CASE WHEN $hasParentId THEN $parentId ELSE g.parentId END,
|
|
622
|
+
g.parentGoalId = CASE WHEN $hasParentId THEN $parentId ELSE g.parentGoalId END,
|
|
623
|
+
g.updatedAt = datetime()
|
|
624
|
+
RETURN g.id AS id, g.title AS title, g.description AS description,
|
|
625
|
+
g.level AS level, g.status AS status, g.parentId AS parentId`,
|
|
626
|
+
{
|
|
627
|
+
id: goalId, cid, title, description, level, status,
|
|
628
|
+
parentId: parentId === undefined ? null : parentId,
|
|
629
|
+
hasDescription: body.description !== undefined,
|
|
630
|
+
hasParentId: parentId !== undefined,
|
|
631
|
+
}
|
|
632
|
+
);
|
|
633
|
+
const rows = parseRows(result);
|
|
634
|
+
if (rows.length === 0) { jsonErr(res, 404, `BusinessGoal ${goalId} not found`); return; }
|
|
635
|
+
const goal = rowToObj(rows[0], ['id', 'title', 'description', 'level', 'status', 'parentId']);
|
|
636
|
+
// H-3 fix: only pass fields that were explicitly provided in the PATCH body to updateGoal.
|
|
637
|
+
// rowToObj fills missing Memgraph fields with null — passing null for e.g. title would
|
|
638
|
+
// overwrite the existing SQLite title with null on a status-only update.
|
|
639
|
+
const storeUpdate = { id: goalId };
|
|
640
|
+
if (title !== null) storeUpdate.title = goal.title;
|
|
641
|
+
if (description !== null) storeUpdate.description = goal.description;
|
|
642
|
+
if (level !== null) storeUpdate.level = goal.level;
|
|
643
|
+
if (status !== null) storeUpdate.status = goal.status;
|
|
644
|
+
if (parentId !== undefined) storeUpdate.parentId = goal.parentId;
|
|
645
|
+
try { hboStore.updateGoal?.(goalId, cid, storeUpdate); } catch (storeErr) { process.stderr.write(`[hbo] goal mirror failed: ${storeErr.message}\n`); }
|
|
646
|
+
try { ctx._bc?.({ type: 'goal.updated', companyId: cid, goalId }); } catch (_) {}
|
|
647
|
+
jsonOk(res, { goal });
|
|
648
|
+
} catch (e) {
|
|
649
|
+
jsonErr(res, 500, `Update BusinessGoal failed: ${safeErrMsg(e)}`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function handleDeleteBusinessGoal(req, res, ctx, goalId) {
|
|
654
|
+
const { mgQuery, cid } = ctx;
|
|
655
|
+
if (!mgQuery) { jsonErr(res, 503, 'Memgraph not connected'); return; }
|
|
656
|
+
if (!goalId) { jsonErr(res, 400, 'Missing goal ID'); return; }
|
|
657
|
+
try {
|
|
658
|
+
const result = await mgQuery(
|
|
659
|
+
`MATCH (g:BusinessGoal {id: $id, companyId: $cid})
|
|
660
|
+
WITH g, g.id AS deletedId
|
|
661
|
+
DETACH DELETE g
|
|
662
|
+
RETURN deletedId AS id`,
|
|
663
|
+
{ id: goalId, cid }
|
|
664
|
+
);
|
|
665
|
+
if (parseRows(result).length === 0) { jsonErr(res, 404, `BusinessGoal ${goalId} not found`); return; }
|
|
666
|
+
try { hboStore.deleteGoal?.(goalId, cid); } catch (storeErr) { process.stderr.write(`[hbo] goal mirror failed: ${storeErr.message}\n`); }
|
|
667
|
+
try { ctx._bc?.({ type: 'goal.deleted', companyId: cid, goalId }); } catch (_) {}
|
|
668
|
+
jsonOk(res, { deleted: true, goalId });
|
|
669
|
+
} catch (e) {
|
|
670
|
+
jsonErr(res, 500, `Delete BusinessGoal failed: ${safeErrMsg(e)}`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
493
674
|
// ── Warm signals handler ──────────────────────────────────────────────────────
|
|
494
675
|
|
|
495
676
|
async function handleGetWarmSignals(req, res, ctx) {
|
|
@@ -2181,9 +2362,11 @@ async function handleGetHboApprovals(req, res, ctx) {
|
|
|
2181
2362
|
const type = url.searchParams.get('type') || null;
|
|
2182
2363
|
|
|
2183
2364
|
// GP3-B2: expanded to include strategy_proposal and harada_strategy_review types
|
|
2184
|
-
let cypher = `MATCH (a:Approval {companyId: $cid}) WHERE a.type IN ['question','send_approval','strategy_proposal','harada_strategy_review','approval','domain_purchase','sender_identity_config','dns_records_review','email_domain_setup','harada_l2_review','harada_l3_review','harada_question']`;
|
|
2365
|
+
let cypher = `MATCH (a:Approval {companyId: $cid}) WHERE a.type IN ['question','send_approval','strategy_proposal','harada_strategy_review','approval','domain_purchase','sender_identity_config','dns_records_review','email_domain_setup','harada_l2_review','harada_l3_review','harada_question','plan_change_review']`;
|
|
2185
2366
|
const params = { cid: ctx.cid };
|
|
2186
|
-
|
|
2367
|
+
// F-03: 'pending' filter also includes 'revision_requested' so operators see revision_requested items in the Pending tab
|
|
2368
|
+
if (status === 'pending') { cypher += " AND a.status IN ['pending', 'revision_requested']"; }
|
|
2369
|
+
else if (status) { cypher += ' AND a.status = $status'; params.status = status; }
|
|
2187
2370
|
if (type) { cypher += ' AND a.type = $type'; params.type = type; }
|
|
2188
2371
|
cypher += ' RETURN a ORDER BY a.createdAt DESC LIMIT toInteger(50)';
|
|
2189
2372
|
|
|
@@ -2210,6 +2393,8 @@ async function handleGetHboApprovals(req, res, ctx) {
|
|
|
2210
2393
|
department: p?.department ?? null,
|
|
2211
2394
|
templateKey: p?.templateKey ?? null,
|
|
2212
2395
|
inferredAnswer: p?.inferredAnswer ?? null,
|
|
2396
|
+
decisionNote: p?.decisionNote ?? null,
|
|
2397
|
+
followUpTaskId: p?.followUpTaskId ?? null,
|
|
2213
2398
|
};
|
|
2214
2399
|
});
|
|
2215
2400
|
jsonOk(res, { approvals, count: approvals.length });
|
|
@@ -2218,6 +2403,28 @@ async function handleGetHboApprovals(req, res, ctx) {
|
|
|
2218
2403
|
}
|
|
2219
2404
|
}
|
|
2220
2405
|
|
|
2406
|
+
// P9A: GET /api/hbo/blocked-work — tasks stopped due to a blocker
|
|
2407
|
+
async function handleGetBlockedWork(req, res, ctx) {
|
|
2408
|
+
try {
|
|
2409
|
+
if (!ctx.mgQuery) { jsonErr(res, 503, 'Memgraph not connected'); return; }
|
|
2410
|
+
const url = new URL(req.url, 'http://localhost');
|
|
2411
|
+
const cid = ctx.cid || url.searchParams.get('companyId') || url.searchParams.get('cid') || '';
|
|
2412
|
+
const r = await ctx.mgQuery(
|
|
2413
|
+
`MATCH (t:Task) WHERE t.blockedReason IS NOT NULL AND t.status = 'blocked' AND t.companyId = $cid
|
|
2414
|
+
RETURN t.id AS id, t.title AS title, t.blockedReason AS blockedReason, t.blockedAt AS blockedAt
|
|
2415
|
+
ORDER BY t.blockedAt DESC`,
|
|
2416
|
+
{ cid }
|
|
2417
|
+
).catch(() => null);
|
|
2418
|
+
const blockedWork = (r?.rows ?? []).map(row => {
|
|
2419
|
+
if (Array.isArray(row)) return { id: row[0], title: row[1], blockedReason: row[2], blockedAt: row[3] };
|
|
2420
|
+
return { id: row['id'], title: row['title'], blockedReason: row['blockedReason'], blockedAt: row['blockedAt'] };
|
|
2421
|
+
});
|
|
2422
|
+
jsonOk(res, { blockedWork, count: blockedWork.length });
|
|
2423
|
+
} catch (e) {
|
|
2424
|
+
jsonErr(res, 500, `Get blocked-work failed: ${safeErrMsg(e)}`);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2221
2428
|
async function handleHboLaunch(req, res, ctx) {
|
|
2222
2429
|
const { mgQuery } = ctx;
|
|
2223
2430
|
if (!mgQuery) { jsonErr(res, 503, 'Memgraph not connected'); return; }
|
|
@@ -2726,9 +2933,90 @@ async function handleGetCommandCenter(req, res, ctx) {
|
|
|
2726
2933
|
}
|
|
2727
2934
|
}
|
|
2728
2935
|
|
|
2936
|
+
// ── G-01: Budget Policy CRUD Handlers ────────────────────────────────────────
|
|
2937
|
+
|
|
2938
|
+
async function handleCreateBudgetPolicy(req, res, ctx) {
|
|
2939
|
+
try {
|
|
2940
|
+
if (!ctx.mgQuery) { jsonErr(res, 503, 'Memgraph not connected'); return; }
|
|
2941
|
+
const body = await readBody(req);
|
|
2942
|
+
if (!assertValidBody(body, res)) return;
|
|
2943
|
+
const { agentId, limitUSD, period, companyId: bodyCompanyId, id: clientId } = body || {};
|
|
2944
|
+
if (!agentId || limitUSD === undefined || !period) {
|
|
2945
|
+
jsonErr(res, 400, 'agentId, limitUSD, and period are required'); return;
|
|
2946
|
+
}
|
|
2947
|
+
// Use client-supplied id (from SQLite saved record) so delete path matches; fall back to generated key.
|
|
2948
|
+
const cid = bodyCompanyId || ctx.cid;
|
|
2949
|
+
const id = clientId || `bp:${cid}:${agentId}:${Date.now()}`;
|
|
2950
|
+
await ctx.mgQuery(
|
|
2951
|
+
`MERGE (bp:BudgetPolicy {id: $id})
|
|
2952
|
+
ON CREATE SET bp.companyId = $cid, bp.agentId = $agentId,
|
|
2953
|
+
bp.limitUSD = $limitUSD, bp.period = $period,
|
|
2954
|
+
bp.limitCents = toInteger($limitUSD * 100),
|
|
2955
|
+
bp.spentCents = 0, bp.createdAt = datetime()
|
|
2956
|
+
ON MATCH SET bp.limitUSD = $limitUSD, bp.period = $period,
|
|
2957
|
+
bp.limitCents = toInteger($limitUSD * 100)`,
|
|
2958
|
+
{ id, cid, agentId: String(agentId), limitUSD: Number(limitUSD), period: String(period) }
|
|
2959
|
+
);
|
|
2960
|
+
jsonOk(res, { policy: { id, agentId, limitUSD: Number(limitUSD), period } });
|
|
2961
|
+
} catch (e) { jsonErr(res, 500, `Create budget policy failed: ${safeErrMsg(e)}`); }
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
async function handleUpdateBudgetPolicy(req, res, ctx, policyId) {
|
|
2965
|
+
try {
|
|
2966
|
+
if (!ctx.mgQuery) { jsonErr(res, 503, 'Memgraph not connected'); return; }
|
|
2967
|
+
if (!policyId) { jsonErr(res, 400, 'Missing policy ID'); return; }
|
|
2968
|
+
const body = await readBody(req);
|
|
2969
|
+
if (!assertValidBody(body, res)) return;
|
|
2970
|
+
const setClauses = [];
|
|
2971
|
+
const params = { id: policyId, cid: ctx.cid };
|
|
2972
|
+
if (body.limitUSD !== undefined) { setClauses.push('bp.limitUSD = $limitUSD, bp.limitCents = toInteger($limitUSD * 100)'); params.limitUSD = Number(body.limitUSD); }
|
|
2973
|
+
if (body.period !== undefined) { setClauses.push('bp.period = $period'); params.period = String(body.period); }
|
|
2974
|
+
if (body.agentId !== undefined) { setClauses.push('bp.agentId = $agentId'); params.agentId = String(body.agentId); }
|
|
2975
|
+
if (setClauses.length === 0) { jsonErr(res, 400, 'No fields to update'); return; }
|
|
2976
|
+
const r = await ctx.mgQuery(
|
|
2977
|
+
`MATCH (bp:BudgetPolicy {id: $id, companyId: $cid}) SET ${setClauses.join(', ')}, bp.updatedAt = datetime() RETURN bp.id`,
|
|
2978
|
+
params
|
|
2979
|
+
);
|
|
2980
|
+
if (!(r?.rows?.length)) { jsonErr(res, 404, `Budget policy not found: ${policyId}`); return; }
|
|
2981
|
+
jsonOk(res, { updated: true, policyId });
|
|
2982
|
+
} catch (e) { jsonErr(res, 500, `Update budget policy failed: ${safeErrMsg(e)}`); }
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
async function handleDeleteBudgetPolicy(req, res, ctx, policyId) {
|
|
2986
|
+
try {
|
|
2987
|
+
if (!ctx.mgQuery) { jsonErr(res, 503, 'Memgraph not connected'); return; }
|
|
2988
|
+
if (!policyId) { jsonErr(res, 400, 'Missing policy ID'); return; }
|
|
2989
|
+
await ctx.mgQuery(
|
|
2990
|
+
`MATCH (bp:BudgetPolicy {id: $id, companyId: $cid}) DETACH DELETE bp`,
|
|
2991
|
+
{ id: policyId, cid: ctx.cid }
|
|
2992
|
+
);
|
|
2993
|
+
jsonOk(res, { deleted: true, policyId });
|
|
2994
|
+
} catch (e) { jsonErr(res, 500, `Delete budget policy failed: ${safeErrMsg(e)}`); }
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
async function handleGetBudgetIncidents(req, res, ctx) {
|
|
2998
|
+
try {
|
|
2999
|
+
if (!ctx.mgQuery) { jsonErr(res, 503, 'Memgraph not connected'); return; }
|
|
3000
|
+
const r = await ctx.mgQuery(
|
|
3001
|
+
`MATCH (bi:BudgetIncident {companyId: $cid}) RETURN bi ORDER BY bi.createdAt DESC LIMIT 50`,
|
|
3002
|
+
{ cid: ctx.cid }
|
|
3003
|
+
).catch(() => null);
|
|
3004
|
+
const incidents = (r?.rows ?? []).map(row => {
|
|
3005
|
+
const node = Array.isArray(row) ? row[0] : (row['bi'] || row);
|
|
3006
|
+
const p = node?.properties || node;
|
|
3007
|
+
return { id: p?.id, agentId: p?.agentId, limitUSD: p?.limitUSD, spentUSD: p?.spentUSD,
|
|
3008
|
+
period: p?.period, resolvedAt: p?.resolvedAt ?? null, createdAt: p?.createdAt ?? null };
|
|
3009
|
+
});
|
|
3010
|
+
jsonOk(res, { incidents, count: incidents.length });
|
|
3011
|
+
} catch (e) { jsonErr(res, 500, `Get budget incidents failed: ${safeErrMsg(e)}`); }
|
|
3012
|
+
}
|
|
3013
|
+
|
|
2729
3014
|
module.exports = function createHBORouter(handlers) {
|
|
2730
|
-
const { broadcast, mgQuery: _mgQueryInit } = handlers || {};
|
|
3015
|
+
const { broadcast, mgQuery: _mgQueryInit, triggerGoalDecompose } = handlers || {};
|
|
2731
3016
|
const _bc = typeof broadcast === 'function' ? broadcast : () => {};
|
|
3017
|
+
// Optional hook: after a BusinessGoal is created, caller can fire-and-forget tickGoalDecompose
|
|
3018
|
+
// so the Harada cascade starts on the next event-loop tick rather than waiting up to 150s.
|
|
3019
|
+
const _triggerGoalDecompose = typeof triggerGoalDecompose === 'function' ? triggerGoalDecompose : null;
|
|
2732
3020
|
return async function hboRoute(req, res, ctx, pathname, method) {
|
|
2733
3021
|
// R3-C10: Set module-level request reference so jsonOk/jsonErr can include CORS headers
|
|
2734
3022
|
// at all 155 call sites without requiring changes to each handler.
|
|
@@ -2744,6 +3032,14 @@ module.exports = function createHBORouter(handlers) {
|
|
|
2744
3032
|
return false;
|
|
2745
3033
|
}
|
|
2746
3034
|
|
|
3035
|
+
// H-2 fix: Handle OPTIONS preflight for all HBO routes (including DELETE /api/hbo/goals/:id).
|
|
3036
|
+
// Must be BEFORE auth check — browsers never send credentials on preflight requests.
|
|
3037
|
+
if (method === 'OPTIONS') {
|
|
3038
|
+
res.writeHead(204, getCorsHeaders(req));
|
|
3039
|
+
res.end();
|
|
3040
|
+
return true;
|
|
3041
|
+
}
|
|
3042
|
+
|
|
2747
3043
|
// Auth check for all HBO routes
|
|
2748
3044
|
const authHeader = (req.headers || {})['authorization'];
|
|
2749
3045
|
const expected = ctx.apiToken;
|
|
@@ -2815,12 +3111,110 @@ module.exports = function createHBORouter(handlers) {
|
|
|
2815
3111
|
return true;
|
|
2816
3112
|
}
|
|
2817
3113
|
|
|
3114
|
+
// P8A-03: GET /api/hbo/tasks/:id — full task detail including originKind, approvalId, etc.
|
|
3115
|
+
// Must be matched BEFORE the PATCH /:id pattern below.
|
|
3116
|
+
const taskGetMatch = pathname.match(/^\/api\/hbo\/tasks\/([^/]+)$/);
|
|
3117
|
+
if (method === 'GET' && taskGetMatch) {
|
|
3118
|
+
const taskId = decodeURIComponent(taskGetMatch[1]);
|
|
3119
|
+
const { mgQuery, cid } = ctx;
|
|
3120
|
+
// E2 fix: prefer ?companyId query param so multi-company operator's active company
|
|
3121
|
+
// is used. Fall back to ctx.cid (session default) for single-company deployments.
|
|
3122
|
+
// Note: raw Node.js http.IncomingMessage has no .query property — must use URL.searchParams.
|
|
3123
|
+
const queryCid = new URL(req.url, 'http://localhost').searchParams.get('companyId') || cid;
|
|
3124
|
+
if (!mgQuery) {
|
|
3125
|
+
// SQL parity: fall back to SQLite when Memgraph unavailable
|
|
3126
|
+
try {
|
|
3127
|
+
const hboStore = require('../../lib/hbo-core-store');
|
|
3128
|
+
const t = hboStore.getTask ? hboStore.getTask(taskId, queryCid) : null;
|
|
3129
|
+
if (!t) { jsonErr(res, 404, 'Task not found'); return true; }
|
|
3130
|
+
jsonOk(res, { task: t, _source: 'sqlite' });
|
|
3131
|
+
} catch (storeErr) {
|
|
3132
|
+
jsonErr(res, 503, `Task unavailable: Memgraph not connected`);
|
|
3133
|
+
}
|
|
3134
|
+
return true;
|
|
3135
|
+
}
|
|
3136
|
+
try {
|
|
3137
|
+
// Return full node — includes all properties: originKind, approvalId, pillarId,
|
|
3138
|
+
// executionStateJson, executionPolicyJson, workspacePath, etc.
|
|
3139
|
+
const result = await mgQuery('MATCH (t:Task {id: $id, companyId: $cid}) RETURN t', { id: taskId, cid: queryCid });
|
|
3140
|
+
if (result.rows && result.rows.length > 0) {
|
|
3141
|
+
const t = result.rows[0][0].properties || result.rows[0][0];
|
|
3142
|
+
jsonOk(res, { task: t });
|
|
3143
|
+
} else {
|
|
3144
|
+
jsonErr(res, 404, 'Task not found');
|
|
3145
|
+
}
|
|
3146
|
+
} catch (e) {
|
|
3147
|
+
jsonErr(res, 500, `Task query failed: ${safeErrMsg(e)}`);
|
|
3148
|
+
}
|
|
3149
|
+
return true;
|
|
3150
|
+
}
|
|
3151
|
+
|
|
2818
3152
|
// POST /api/hbo/tasks
|
|
2819
3153
|
if (method === 'POST' && pathname === '/api/hbo/tasks') {
|
|
2820
3154
|
await handleCreateBusinessTask(req, res, ctx);
|
|
2821
3155
|
return true;
|
|
2822
3156
|
}
|
|
2823
3157
|
|
|
3158
|
+
// POST /api/hbo/tasks/:id/interpretation/refresh — P_EXPLAIN-04
|
|
3159
|
+
const taskInterpRefreshMatch = pathname.match(/^\/api\/hbo\/tasks\/([^/]+)\/interpretation\/refresh$/);
|
|
3160
|
+
if (method === 'POST' && taskInterpRefreshMatch) {
|
|
3161
|
+
const taskId = decodeURIComponent(taskInterpRefreshMatch[1]);
|
|
3162
|
+
const { mgQuery } = ctx;
|
|
3163
|
+
if (!mgQuery) { jsonResponse(res, 503, { error: 'Memgraph not connected' }); return true; }
|
|
3164
|
+
const cid = ctx.cid;
|
|
3165
|
+
try {
|
|
3166
|
+
const { createInterpretationExplanation } = require('../lib/interpretation-engine.js');
|
|
3167
|
+
const { rawWrite: _interpRw } = require('../lib/safe-memgraph.js');
|
|
3168
|
+
await createInterpretationExplanation(taskId, cid, _interpRw);
|
|
3169
|
+
jsonResponse(res, 200, { ok: true, taskId });
|
|
3170
|
+
} catch (err) {
|
|
3171
|
+
jsonResponse(res, 500, { error: `Interpretation refresh failed: ${err.message}` });
|
|
3172
|
+
}
|
|
3173
|
+
return true;
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
// E-02: GET /api/hbo/tasks/:taskId/enrichment — hill chart position + recent PDSA cycles
|
|
3177
|
+
// Returns hillChart: { id, position, phase, updatedAt } | null
|
|
3178
|
+
// pdsaCycles: [...] (up to 5 most recent, ordered by createdAt DESC)
|
|
3179
|
+
const taskEnrichmentMatch = pathname.match(/^\/api\/hbo\/tasks\/([^/]+)\/enrichment$/);
|
|
3180
|
+
if (method === 'GET' && taskEnrichmentMatch) {
|
|
3181
|
+
const taskId = decodeURIComponent(taskEnrichmentMatch[1]);
|
|
3182
|
+
const { mgQuery, cid } = ctx;
|
|
3183
|
+
const queryCid = new URL(req.url, 'http://localhost').searchParams.get('companyId') || cid;
|
|
3184
|
+
if (!mgQuery) {
|
|
3185
|
+
jsonResponse(res, 503, { error: 'Memgraph not connected' });
|
|
3186
|
+
return true;
|
|
3187
|
+
}
|
|
3188
|
+
try {
|
|
3189
|
+
const [hillResult, pdsaResult] = await Promise.all([
|
|
3190
|
+
mgQuery(
|
|
3191
|
+
`MATCH (t:Task {id: $taskId, companyId: $cid})
|
|
3192
|
+
OPTIONAL MATCH (t)-[:HAS_HILL_CHART]->(h:HillChart)
|
|
3193
|
+
RETURN h`,
|
|
3194
|
+
{ taskId, cid: queryCid }
|
|
3195
|
+
),
|
|
3196
|
+
mgQuery(
|
|
3197
|
+
`MATCH (t:Task {id: $taskId, companyId: $cid})
|
|
3198
|
+
OPTIONAL MATCH (t)-[:HAS_PDSA]->(p:PDSACycle)
|
|
3199
|
+
RETURN p ORDER BY p.createdAt DESC LIMIT 5`,
|
|
3200
|
+
{ taskId, cid: queryCid }
|
|
3201
|
+
),
|
|
3202
|
+
]);
|
|
3203
|
+
const hRow = hillResult.rows && hillResult.rows[0] ? hillResult.rows[0][0] : null;
|
|
3204
|
+
const hillChart = hRow
|
|
3205
|
+
? (hRow.properties ?? hRow)
|
|
3206
|
+
: null;
|
|
3207
|
+
const pdsaCycles = (pdsaResult.rows || [])
|
|
3208
|
+
.map((r) => r[0])
|
|
3209
|
+
.filter(Boolean)
|
|
3210
|
+
.map((p) => p.properties ?? p);
|
|
3211
|
+
jsonOk(res, { hillChart, pdsaCycles });
|
|
3212
|
+
} catch (e) {
|
|
3213
|
+
jsonErr(res, 500, `Enrichment query failed: ${safeErrMsg(e)}`);
|
|
3214
|
+
}
|
|
3215
|
+
return true;
|
|
3216
|
+
}
|
|
3217
|
+
|
|
2824
3218
|
// PATCH /api/hbo/tasks/:id
|
|
2825
3219
|
const taskPatchMatch = pathname.match(/^\/api\/hbo\/tasks\/(.+)$/);
|
|
2826
3220
|
if (method === 'PATCH' && taskPatchMatch) {
|
|
@@ -2834,6 +3228,30 @@ module.exports = function createHBORouter(handlers) {
|
|
|
2834
3228
|
return true;
|
|
2835
3229
|
}
|
|
2836
3230
|
|
|
3231
|
+
// GET /api/hbo/goals/:id — must be checked before POST/PATCH/DELETE goalMatch
|
|
3232
|
+
const goalIdOnlyMatch = pathname.match(/^\/api\/hbo\/goals\/([^/]+)$/);
|
|
3233
|
+
if (method === 'GET' && goalIdOnlyMatch) {
|
|
3234
|
+
await handleGetBusinessGoal(req, res, ctx, decodeURIComponent(goalIdOnlyMatch[1]));
|
|
3235
|
+
return true;
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
// POST /api/hbo/goals
|
|
3239
|
+
if (method === 'POST' && pathname === '/api/hbo/goals') {
|
|
3240
|
+
await handleCreateBusinessGoal(req, res, ctx);
|
|
3241
|
+
return true;
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
// PATCH/DELETE /api/hbo/goals/:id
|
|
3245
|
+
const goalMatch = pathname.match(/^\/api\/hbo\/goals\/(.+)$/);
|
|
3246
|
+
if (method === 'PATCH' && goalMatch) {
|
|
3247
|
+
await handleUpdateBusinessGoal(req, res, ctx, decodeURIComponent(goalMatch[1]));
|
|
3248
|
+
return true;
|
|
3249
|
+
}
|
|
3250
|
+
if (method === 'DELETE' && goalMatch) {
|
|
3251
|
+
await handleDeleteBusinessGoal(req, res, ctx, decodeURIComponent(goalMatch[1]));
|
|
3252
|
+
return true;
|
|
3253
|
+
}
|
|
3254
|
+
|
|
2837
3255
|
// GET /api/hbo/triage-summary
|
|
2838
3256
|
if (method === 'GET' && pathname === '/api/hbo/triage-summary') {
|
|
2839
3257
|
await handleGetTriageSummary(req, res, ctx);
|
|
@@ -2896,6 +3314,127 @@ module.exports = function createHBORouter(handlers) {
|
|
|
2896
3314
|
return true;
|
|
2897
3315
|
}
|
|
2898
3316
|
|
|
3317
|
+
// ── T3-01: POST /api/hbo/pillar/:id/l2content — L2 research agent submits strategy ──────
|
|
3318
|
+
const pillarL2ContentMatch = pathname.match(/^\/api\/hbo\/pillar\/([^/]+)\/l2content$/);
|
|
3319
|
+
if (method === 'POST' && pillarL2ContentMatch) {
|
|
3320
|
+
const pillarId = decodeURIComponent(pillarL2ContentMatch[1]);
|
|
3321
|
+
const cid = ctx.cid;
|
|
3322
|
+
const mgQuery = ctx.mgQuery;
|
|
3323
|
+
if (!mgQuery) { jsonErr(res, 503, 'Memgraph not connected'); return true; }
|
|
3324
|
+
try {
|
|
3325
|
+
const body = await readBody(req);
|
|
3326
|
+
if (!assertValidBody(body, res)) return true;
|
|
3327
|
+
const { l2Strategy, l2Content } = body || {};
|
|
3328
|
+
if (!l2Strategy && !l2Content) { jsonErr(res, 400, 'l2Strategy or l2Content required'); return true; }
|
|
3329
|
+
const r = await mgQuery(
|
|
3330
|
+
`MATCH (gp:GoalPillar {id: $pillarId, companyId: $cid})
|
|
3331
|
+
SET gp.l2Strategy = COALESCE($l2Strategy, gp.l2Strategy),
|
|
3332
|
+
gp.l2Content = COALESCE($l2Content, gp.l2Content),
|
|
3333
|
+
gp.l2ReviewStatus = 'pending_review',
|
|
3334
|
+
gp.updatedAt = datetime()
|
|
3335
|
+
RETURN gp.id AS id`,
|
|
3336
|
+
{ pillarId, cid, l2Strategy: l2Strategy ? String(l2Strategy) : null, l2Content: l2Content ? String(l2Content) : null }
|
|
3337
|
+
);
|
|
3338
|
+
if (!parseRows(r).length) { jsonErr(res, 404, `GoalPillar ${pillarId} not found`); return true; }
|
|
3339
|
+
jsonOk(res, { ok: true, pillarId, l2ReviewStatus: 'pending_review' });
|
|
3340
|
+
} catch (e) { jsonErr(res, 500, `l2content submit failed: ${safeErrMsg(e)}`); }
|
|
3341
|
+
return true;
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
// ── T3-02: POST /api/hbo/pillar/:id/l2review — CEO agent reviews L2 content ────────────
|
|
3345
|
+
const pillarL2ReviewMatch = pathname.match(/^\/api\/hbo\/pillar\/([^/]+)\/l2review$/);
|
|
3346
|
+
if (method === 'POST' && pillarL2ReviewMatch) {
|
|
3347
|
+
const pillarId = decodeURIComponent(pillarL2ReviewMatch[1]);
|
|
3348
|
+
const cid = ctx.cid;
|
|
3349
|
+
const mgQuery = ctx.mgQuery;
|
|
3350
|
+
if (!mgQuery) { jsonErr(res, 503, 'Memgraph not connected'); return true; }
|
|
3351
|
+
try {
|
|
3352
|
+
const body = await readBody(req);
|
|
3353
|
+
if (!assertValidBody(body, res)) return true;
|
|
3354
|
+
const { verdict, reviewCritique, goalId, agentId } = body || {};
|
|
3355
|
+
if (!verdict || !['pass', 'fail'].includes(verdict)) { jsonErr(res, 400, 'verdict must be "pass" or "fail"'); return true; }
|
|
3356
|
+
if (verdict === 'pass') {
|
|
3357
|
+
await mgQuery(
|
|
3358
|
+
`MATCH (gp:GoalPillar {id: $pillarId, companyId: $cid})
|
|
3359
|
+
SET gp.l2ReviewStatus = 'approved', gp.l2ReviewedAt = datetime()
|
|
3360
|
+
RETURN gp.id`,
|
|
3361
|
+
{ pillarId, cid }
|
|
3362
|
+
);
|
|
3363
|
+
jsonOk(res, { ok: true, pillarId, verdict: 'pass' });
|
|
3364
|
+
} else {
|
|
3365
|
+
// FAIL: increment cycle count, re-dispatch L2 research with critique
|
|
3366
|
+
const cycleResult = await mgQuery(
|
|
3367
|
+
`MATCH (gp:GoalPillar {id: $pillarId, companyId: $cid})
|
|
3368
|
+
SET gp.l2ReviewStatus = 'revision_needed',
|
|
3369
|
+
gp.l2ReviewCycles = COALESCE(toInteger(gp.l2ReviewCycles), 0) + 1
|
|
3370
|
+
RETURN gp.l2ReviewCycles AS cycles, gp.goalId AS goalId`,
|
|
3371
|
+
{ pillarId, cid }
|
|
3372
|
+
);
|
|
3373
|
+
const cycleRow = parseRows(cycleResult)[0];
|
|
3374
|
+
const resolvedGoalId = goalId || (cycleRow && (cycleRow['cycles'] !== undefined ? null : cycleRow['goalId'])) || null;
|
|
3375
|
+
const cycles = cycleRow ? Number(cycleRow['cycles'] ?? 1) : 1;
|
|
3376
|
+
if (cycles < 3 && resolvedGoalId && agentId) {
|
|
3377
|
+
try {
|
|
3378
|
+
const { CascadeResearchDispatcher } = require('../lib/harada/cascade-research-dispatcher');
|
|
3379
|
+
const crd = new CascadeResearchDispatcher(mgQuery, cid);
|
|
3380
|
+
await crd.dispatchL2Research(pillarId, resolvedGoalId, cid, agentId, reviewCritique || '');
|
|
3381
|
+
} catch (dispErr) {
|
|
3382
|
+
process.stderr.write(`[hbo] l2review: re-dispatch failed: ${safeErrMsg(dispErr)}\n`);
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
jsonOk(res, { ok: true, pillarId, verdict: 'fail', cycles });
|
|
3386
|
+
}
|
|
3387
|
+
} catch (e) { jsonErr(res, 500, `l2review failed: ${safeErrMsg(e)}`); }
|
|
3388
|
+
return true;
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
// ── T3-03: POST /api/hbo/action-cell/:id/l3content — L3 research agent submits ──────────
|
|
3392
|
+
const cellL3ContentMatch = pathname.match(/^\/api\/hbo\/action-cell\/([^/]+)\/l3content$/);
|
|
3393
|
+
if (method === 'POST' && cellL3ContentMatch) {
|
|
3394
|
+
const cellId = decodeURIComponent(cellL3ContentMatch[1]);
|
|
3395
|
+
const cid = ctx.cid;
|
|
3396
|
+
const mgQuery = ctx.mgQuery;
|
|
3397
|
+
if (!mgQuery) { jsonErr(res, 503, 'Memgraph not connected'); return true; }
|
|
3398
|
+
try {
|
|
3399
|
+
const body = await readBody(req);
|
|
3400
|
+
if (!assertValidBody(body, res)) return true;
|
|
3401
|
+
const { l3Plan, l3Content } = body || {};
|
|
3402
|
+
if (!l3Plan && !l3Content) { jsonErr(res, 400, 'l3Plan or l3Content required'); return true; }
|
|
3403
|
+
const r = await mgQuery(
|
|
3404
|
+
`MATCH (ac:ActionCell {id: $cellId, companyId: $cid})
|
|
3405
|
+
SET ac.l3Plan = COALESCE($l3Plan, ac.l3Plan),
|
|
3406
|
+
ac.l3Content = COALESCE($l3Content, ac.l3Content),
|
|
3407
|
+
ac.l3ReviewStatus = 'pending_review',
|
|
3408
|
+
ac.updatedAt = datetime()
|
|
3409
|
+
RETURN ac.id AS id`,
|
|
3410
|
+
{ cellId, cid, l3Plan: l3Plan ? String(l3Plan) : null, l3Content: l3Content ? String(l3Content) : null }
|
|
3411
|
+
);
|
|
3412
|
+
if (!parseRows(r).length) { jsonErr(res, 404, `ActionCell ${cellId} not found`); return true; }
|
|
3413
|
+
jsonOk(res, { ok: true, cellId, l3ReviewStatus: 'pending_review' });
|
|
3414
|
+
} catch (e) { jsonErr(res, 500, `l3content submit failed: ${safeErrMsg(e)}`); }
|
|
3415
|
+
return true;
|
|
3416
|
+
}
|
|
3417
|
+
|
|
3418
|
+
// ── T3-03b: POST /api/hbo/action-cell/:id/l3review — dept head reviews L3 content ───────
|
|
3419
|
+
const cellL3ReviewMatch = pathname.match(/^\/api\/hbo\/action-cell\/([^/]+)\/l3review$/);
|
|
3420
|
+
if (method === 'POST' && cellL3ReviewMatch) {
|
|
3421
|
+
const cellId = decodeURIComponent(cellL3ReviewMatch[1]);
|
|
3422
|
+
const cid = ctx.cid;
|
|
3423
|
+
const mgQuery = ctx.mgQuery;
|
|
3424
|
+
if (!mgQuery) { jsonErr(res, 503, 'Memgraph not connected'); return true; }
|
|
3425
|
+
try {
|
|
3426
|
+
const body = await readBody(req);
|
|
3427
|
+
if (!assertValidBody(body, res)) return true;
|
|
3428
|
+
const { verdict, reviewCritique } = body || {};
|
|
3429
|
+
if (!verdict || !['pass', 'fail'].includes(verdict)) { jsonErr(res, 400, 'verdict must be "pass" or "fail"'); return true; }
|
|
3430
|
+
const { CascadeJudge } = require('../lib/harada/cascade-judge');
|
|
3431
|
+
const judge = new CascadeJudge(mgQuery, cid);
|
|
3432
|
+
const result = await judge.judgeL3(cellId);
|
|
3433
|
+
jsonOk(res, { ok: true, cellId, verdict: result.verdict, critique: result.critique });
|
|
3434
|
+
} catch (e) { jsonErr(res, 500, `l3review failed: ${safeErrMsg(e)}`); }
|
|
3435
|
+
return true;
|
|
3436
|
+
}
|
|
3437
|
+
|
|
2899
3438
|
// GET /api/hbo/metrics — ProcessMetrics and ControlChartSignals
|
|
2900
3439
|
if (method === 'GET' && pathname === '/api/hbo/metrics') {
|
|
2901
3440
|
await handleGetMetrics(req, res, ctx);
|
|
@@ -3053,11 +3592,17 @@ module.exports = function createHBORouter(handlers) {
|
|
|
3053
3592
|
return true;
|
|
3054
3593
|
}
|
|
3055
3594
|
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3595
|
+
// GET /api/hbo/blocked-work — P9A: tasks stopped due to a blocker
|
|
3596
|
+
if (method === 'GET' && pathname === '/api/hbo/blocked-work') {
|
|
3597
|
+
await handleGetBlockedWork(req, res, ctx);
|
|
3598
|
+
return true;
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
// GET /api/hbo/command-center — aggregated snapshot for dashboard
|
|
3602
|
+
if (method === 'GET' && pathname === '/api/hbo/command-center') {
|
|
3603
|
+
await handleGetCommandCenter(req, res, ctx);
|
|
3604
|
+
return true;
|
|
3605
|
+
}
|
|
3061
3606
|
|
|
3062
3607
|
// GET /api/hbo/decisions/timeline — unified historical decisions timeline
|
|
3063
3608
|
if (method === 'GET' && pathname === '/api/hbo/decisions/timeline') {
|
|
@@ -3065,6 +3610,21 @@ module.exports = function createHBORouter(handlers) {
|
|
|
3065
3610
|
return true;
|
|
3066
3611
|
}
|
|
3067
3612
|
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3613
|
+
// G-01: Budget Policy CRUD
|
|
3614
|
+
if (method === 'POST' && pathname === '/api/budget-policies') {
|
|
3615
|
+
await handleCreateBudgetPolicy(req, res, ctx);
|
|
3616
|
+
return true;
|
|
3617
|
+
}
|
|
3618
|
+
const budgetPolicyMatch = pathname.match(/^\/api\/budget-policies\/([^/]+)$/);
|
|
3619
|
+
if (budgetPolicyMatch) {
|
|
3620
|
+
if (method === 'PATCH') { await handleUpdateBudgetPolicy(req, res, ctx, budgetPolicyMatch[1]); return true; }
|
|
3621
|
+
if (method === 'DELETE') { await handleDeleteBudgetPolicy(req, res, ctx, budgetPolicyMatch[1]); return true; }
|
|
3622
|
+
}
|
|
3623
|
+
if (method === 'GET' && pathname === '/api/budget-incidents') {
|
|
3624
|
+
await handleGetBudgetIncidents(req, res, ctx);
|
|
3625
|
+
return true;
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
return false;
|
|
3629
|
+
};
|
|
3630
|
+
};
|