@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.
- 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-wrapper.sh +4 -1
- package/bin/helios-rpc.js +19 -0
- package/daemon/adapters/helios-rpc-adapter.js +5 -12
- package/daemon/context-enrichment.js +27 -0
- package/daemon/helios-api.js +310 -58
- package/daemon/helios-company-daemon.js +179 -53
- 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 +73 -5
- package/daemon/lib/headroom-middleware.js +129 -0
- package/daemon/lib/headroom-proxy-manager.js +319 -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 +12 -0
- 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 +367 -13
- package/daemon/routes/hed.js +133 -0
- package/daemon/routes/inbox.js +466 -10
- package/daemon/routes/project.js +392 -9
- package/daemon/schema-definitions.js +10 -0
- package/daemon/schema-migrations-hbo.js +10 -0
- package/daemon/schema-migrations-proj.js +22 -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/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 +33 -65
- 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 +156 -0
- package/lib/__tests__/bulk-ingest.live.test.ts +66 -0
- package/lib/__tests__/crash-fixes.test.ts +49 -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/graph/learning/headroom-learn-bridge.js +175 -0
- package/lib/hbo-core-store.ts +71 -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 +38 -6
- 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 +1 -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/model-assembler-sql.ts +200 -0
- package/lib/triage-core/mental-model/model-assembler.ts +16 -3
- package/lib/triage-core/orchestrator.ts +4 -4
- 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 +10 -3
- 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/dept.js
CHANGED
|
@@ -149,7 +149,16 @@ module.exports = function createDeptRoute({ mgQuery, broadcast, semanticUpdater
|
|
|
149
149
|
pcp.rationale = $rationale,
|
|
150
150
|
pcp.status = 'pending',
|
|
151
151
|
pcp.updatedAt = datetime()`,
|
|
152
|
-
|
|
152
|
+
{ proposalId, cid, pillarId, dept, section, proposedChange: String(proposedChange), rationale: rationale ? String(rationale) : '', authorId }
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// P1-C: Create Approval node for plan_change_review so it surfaces in GET /api/hbo/approvals
|
|
156
|
+
await mgQuery(
|
|
157
|
+
`MERGE (a:Approval {id: $approvalId})
|
|
158
|
+
ON CREATE SET a.type = 'plan_change_review', a.companyId = $cid, a.status = 'pending',
|
|
159
|
+
a.proposalId = $proposalId, a.createdAt = datetime()
|
|
160
|
+
ON MATCH SET a.status = 'pending', a.proposalId = $proposalId`,
|
|
161
|
+
{ approvalId: `approval:pcp:${proposalId}`, proposalId, cid }
|
|
153
162
|
);
|
|
154
163
|
|
|
155
164
|
// Respond immediately — LLM analysis is async
|
|
@@ -16,14 +16,16 @@ const { spawn } = require('child_process');
|
|
|
16
16
|
const path = require('path');
|
|
17
17
|
|
|
18
18
|
const HOME = homedir();
|
|
19
|
-
const
|
|
19
|
+
const HELIOS_ROOT = process.env.HELIOS_ROOT
|
|
20
|
+
|| join(HOME, 'Desktop', 'Helios', 'helios-agent-main');
|
|
21
|
+
const TRIAGE_DATA = join(HELIOS_ROOT, 'data', 'email-triage');
|
|
20
22
|
const BRIEFING_PATH = join(TRIAGE_DATA, 'latest-briefing.json');
|
|
21
23
|
const INBOX_PATH = join(TRIAGE_DATA, 'latest-inbox.json');
|
|
22
24
|
const DRAFTS_PATH = join(TRIAGE_DATA, 'drafted-responses.json');
|
|
23
|
-
const DASHBOARD_PATH = join(
|
|
25
|
+
const DASHBOARD_PATH = join(HELIOS_ROOT, 'daemon', 'triage', 'dashboard.html');
|
|
24
26
|
|
|
25
27
|
// ─── Backfill Process Mutex (PID file) ──────────────────────────────────────
|
|
26
|
-
const BACKFILL_PID_FILE = path.join(
|
|
28
|
+
const BACKFILL_PID_FILE = path.join(HELIOS_ROOT, 'data', 'backfill.pid');
|
|
27
29
|
|
|
28
30
|
function isBackfillRunning() {
|
|
29
31
|
try {
|
|
@@ -64,14 +66,14 @@ function jsonResponse(res, status, data) {
|
|
|
64
66
|
*/
|
|
65
67
|
async function regenerateDashboard() {
|
|
66
68
|
return new Promise((resolve) => {
|
|
67
|
-
const updaterPath = join(
|
|
69
|
+
const updaterPath = join(HELIOS_ROOT, 'scripts', 'update-dashboard.js');
|
|
68
70
|
if (!existsSync(updaterPath)) {
|
|
69
71
|
console.warn('[email-triage] update-dashboard.js not found — skipping regen');
|
|
70
72
|
resolve();
|
|
71
73
|
return;
|
|
72
74
|
}
|
|
73
75
|
const child = spawn(process.execPath, [updaterPath], {
|
|
74
|
-
cwd:
|
|
76
|
+
cwd: HELIOS_ROOT,
|
|
75
77
|
env: { ...process.env },
|
|
76
78
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
77
79
|
windowsHide: true,
|
|
@@ -237,6 +239,7 @@ module.exports = function emailTriageRoute({ broadcast } = {}) {
|
|
|
237
239
|
// Parse optional body for config
|
|
238
240
|
let since = '30d';
|
|
239
241
|
let limit = 100;
|
|
242
|
+
let account = null;
|
|
240
243
|
try {
|
|
241
244
|
const chunks = [];
|
|
242
245
|
await new Promise((resolve, reject) => {
|
|
@@ -247,6 +250,7 @@ module.exports = function emailTriageRoute({ broadcast } = {}) {
|
|
|
247
250
|
const body = chunks.length > 0 ? JSON.parse(Buffer.concat(chunks).toString()) : {};
|
|
248
251
|
if (body.since) since = body.since;
|
|
249
252
|
if (body.limit) limit = parseInt(body.limit, 10) || 100;
|
|
253
|
+
if (body.account) account = body.account;
|
|
250
254
|
} catch (_) { /* use defaults */ }
|
|
251
255
|
|
|
252
256
|
// Set up SSE headers
|
|
@@ -268,10 +272,12 @@ module.exports = function emailTriageRoute({ broadcast } = {}) {
|
|
|
268
272
|
|
|
269
273
|
// Resolve the backfill script — prefer run-backfill-30d.ts (full triage + drafts)
|
|
270
274
|
const scriptCandidates = [
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
join(
|
|
275
|
+
process.env.HELIOS_ROOT
|
|
276
|
+
? join(process.env.HELIOS_ROOT, 'run-backfill-30d.ts')
|
|
277
|
+
: join(HOME, 'Desktop', 'Helios', 'helios-agent-main', 'run-backfill-30d.ts'), // W1-3: correct script (full 5-phase email triage + drafts)
|
|
278
|
+
join(HELIOS_ROOT, 'run-triage-backfill.ts'), // legacy name (does not exist, kept as fallback)
|
|
279
|
+
join(HELIOS_ROOT, 'extensions', 'email', 'backfill.ts'), // graph ingestion only
|
|
280
|
+
join(HELIOS_ROOT, 'scripts', 'run-backfill.ts'), // full graph backfill (wrong for email triage)
|
|
275
281
|
];
|
|
276
282
|
|
|
277
283
|
const scriptPath = scriptCandidates.find(p => existsSync(p));
|
|
@@ -293,17 +299,20 @@ module.exports = function emailTriageRoute({ broadcast } = {}) {
|
|
|
293
299
|
}
|
|
294
300
|
|
|
295
301
|
const args = [scriptPath, '--since', sinceDate, '--limit', limit];
|
|
302
|
+
if (account) args.push('--account', account);
|
|
296
303
|
sendSSE({ phase: 1, message: `Running: npx tsx ${path.basename(scriptPath)} --since ${sinceDate} --limit ${limit}` });
|
|
297
304
|
|
|
298
305
|
let child;
|
|
299
306
|
try {
|
|
300
307
|
// C6: use npx.cmd on Windows so spawn finds the correct executable
|
|
308
|
+
// shell:true required on Windows for .cmd files (EINVAL without it)
|
|
301
309
|
const npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
302
310
|
child = spawn(npxBin, ['tsx', ...args], {
|
|
303
|
-
cwd:
|
|
311
|
+
cwd: HELIOS_ROOT,
|
|
304
312
|
env: { ...process.env, HELIOS_SKIP_INTERNAL_DASHBOARD_REGEN: '1' },
|
|
305
313
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
306
314
|
windowsHide: true,
|
|
315
|
+
shell: process.platform === 'win32',
|
|
307
316
|
});
|
|
308
317
|
} catch (spawnErr) {
|
|
309
318
|
sendSSE({ phase: -1, message: `Spawn error: ${spawnErr.message}`, error: true, done: true });
|
package/daemon/routes/hbo.js
CHANGED
|
@@ -580,7 +580,14 @@ async function handleCreateBusinessGoal(req, res, ctx) {
|
|
|
580
580
|
} catch (storeErr) {
|
|
581
581
|
process.stderr.write(`[hbo] goal mirror failed: ${storeErr.message}\n`);
|
|
582
582
|
}
|
|
583
|
-
try { ctx._bc?.({ type: 'goal.created', companyId: cid, goalId }); } catch (_) {}
|
|
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
|
+
}
|
|
584
591
|
jsonOk(res, { goal: { id: goalId, companyId: cid, title, description, level, status: 'active', parentId } }, 201);
|
|
585
592
|
} catch (e) {
|
|
586
593
|
jsonErr(res, 500, `Create BusinessGoal failed: ${safeErrMsg(e)}`);
|
|
@@ -629,7 +636,7 @@ async function handleUpdateBusinessGoal(req, res, ctx, goalId) {
|
|
|
629
636
|
// H-3 fix: only pass fields that were explicitly provided in the PATCH body to updateGoal.
|
|
630
637
|
// rowToObj fills missing Memgraph fields with null — passing null for e.g. title would
|
|
631
638
|
// overwrite the existing SQLite title with null on a status-only update.
|
|
632
|
-
const storeUpdate
|
|
639
|
+
const storeUpdate = { id: goalId };
|
|
633
640
|
if (title !== null) storeUpdate.title = goal.title;
|
|
634
641
|
if (description !== null) storeUpdate.description = goal.description;
|
|
635
642
|
if (level !== null) storeUpdate.level = goal.level;
|
|
@@ -2355,9 +2362,11 @@ async function handleGetHboApprovals(req, res, ctx) {
|
|
|
2355
2362
|
const type = url.searchParams.get('type') || null;
|
|
2356
2363
|
|
|
2357
2364
|
// GP3-B2: expanded to include strategy_proposal and harada_strategy_review types
|
|
2358
|
-
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']`;
|
|
2359
2366
|
const params = { cid: ctx.cid };
|
|
2360
|
-
|
|
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; }
|
|
2361
2370
|
if (type) { cypher += ' AND a.type = $type'; params.type = type; }
|
|
2362
2371
|
cypher += ' RETURN a ORDER BY a.createdAt DESC LIMIT toInteger(50)';
|
|
2363
2372
|
|
|
@@ -2384,6 +2393,8 @@ async function handleGetHboApprovals(req, res, ctx) {
|
|
|
2384
2393
|
department: p?.department ?? null,
|
|
2385
2394
|
templateKey: p?.templateKey ?? null,
|
|
2386
2395
|
inferredAnswer: p?.inferredAnswer ?? null,
|
|
2396
|
+
decisionNote: p?.decisionNote ?? null,
|
|
2397
|
+
followUpTaskId: p?.followUpTaskId ?? null,
|
|
2387
2398
|
};
|
|
2388
2399
|
});
|
|
2389
2400
|
jsonOk(res, { approvals, count: approvals.length });
|
|
@@ -2392,6 +2403,28 @@ async function handleGetHboApprovals(req, res, ctx) {
|
|
|
2392
2403
|
}
|
|
2393
2404
|
}
|
|
2394
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
|
+
|
|
2395
2428
|
async function handleHboLaunch(req, res, ctx) {
|
|
2396
2429
|
const { mgQuery } = ctx;
|
|
2397
2430
|
if (!mgQuery) { jsonErr(res, 503, 'Memgraph not connected'); return; }
|
|
@@ -2900,9 +2933,90 @@ async function handleGetCommandCenter(req, res, ctx) {
|
|
|
2900
2933
|
}
|
|
2901
2934
|
}
|
|
2902
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
|
+
|
|
2903
3014
|
module.exports = function createHBORouter(handlers) {
|
|
2904
|
-
const { broadcast, mgQuery: _mgQueryInit } = handlers || {};
|
|
3015
|
+
const { broadcast, mgQuery: _mgQueryInit, triggerGoalDecompose } = handlers || {};
|
|
2905
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;
|
|
2906
3020
|
return async function hboRoute(req, res, ctx, pathname, method) {
|
|
2907
3021
|
// R3-C10: Set module-level request reference so jsonOk/jsonErr can include CORS headers
|
|
2908
3022
|
// at all 155 call sites without requiring changes to each handler.
|
|
@@ -2997,12 +3111,110 @@ module.exports = function createHBORouter(handlers) {
|
|
|
2997
3111
|
return true;
|
|
2998
3112
|
}
|
|
2999
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
|
+
|
|
3000
3152
|
// POST /api/hbo/tasks
|
|
3001
3153
|
if (method === 'POST' && pathname === '/api/hbo/tasks') {
|
|
3002
3154
|
await handleCreateBusinessTask(req, res, ctx);
|
|
3003
3155
|
return true;
|
|
3004
3156
|
}
|
|
3005
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
|
+
|
|
3006
3218
|
// PATCH /api/hbo/tasks/:id
|
|
3007
3219
|
const taskPatchMatch = pathname.match(/^\/api\/hbo\/tasks\/(.+)$/);
|
|
3008
3220
|
if (method === 'PATCH' && taskPatchMatch) {
|
|
@@ -3102,6 +3314,127 @@ module.exports = function createHBORouter(handlers) {
|
|
|
3102
3314
|
return true;
|
|
3103
3315
|
}
|
|
3104
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
|
+
|
|
3105
3438
|
// GET /api/hbo/metrics — ProcessMetrics and ControlChartSignals
|
|
3106
3439
|
if (method === 'GET' && pathname === '/api/hbo/metrics') {
|
|
3107
3440
|
await handleGetMetrics(req, res, ctx);
|
|
@@ -3259,11 +3592,17 @@ module.exports = function createHBORouter(handlers) {
|
|
|
3259
3592
|
return true;
|
|
3260
3593
|
}
|
|
3261
3594
|
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
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
|
+
}
|
|
3267
3606
|
|
|
3268
3607
|
// GET /api/hbo/decisions/timeline — unified historical decisions timeline
|
|
3269
3608
|
if (method === 'GET' && pathname === '/api/hbo/decisions/timeline') {
|
|
@@ -3271,6 +3610,21 @@ module.exports = function createHBORouter(handlers) {
|
|
|
3271
3610
|
return true;
|
|
3272
3611
|
}
|
|
3273
3612
|
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
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
|
+
};
|