@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
|
@@ -20,12 +20,14 @@ if (!process.env.TZ) { process.env.TZ = 'UTC'; }
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
const path = require('path');
|
|
23
|
-
const fs = require('fs');
|
|
24
|
-
const os = require('os');
|
|
25
|
-
const {
|
|
26
|
-
const {
|
|
27
|
-
const {
|
|
28
|
-
const
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const os = require('os');
|
|
25
|
+
const { execFile } = require('child_process');
|
|
26
|
+
const { performance } = require('perf_hooks');
|
|
27
|
+
const { randomUUID } = require('crypto');
|
|
28
|
+
const { buildContextBrief } = require('./context-enrichment');
|
|
29
|
+
const hboStore = require('../lib/hbo-core-store');
|
|
30
|
+
const graphWal = require('../lib/graph-wal');
|
|
29
31
|
const { runMigrations } = require('./schema-migrations');
|
|
30
32
|
const { runHBOMigrations } = require('./schema-migrations-hbo');
|
|
31
33
|
const { runHaradaMigrations } = require('./schema-migrations-harada');
|
|
@@ -727,7 +729,7 @@ async function executeQueueAction(item) {
|
|
|
727
729
|
const cid = (item.payload && item.payload.companyId) || 'default';
|
|
728
730
|
|
|
729
731
|
// SQLite-first write (P2-2)
|
|
730
|
-
hboStore.createTask({
|
|
732
|
+
try { hboStore.createTask({
|
|
731
733
|
id: taskId,
|
|
732
734
|
companyId: cid,
|
|
733
735
|
title,
|
|
@@ -739,7 +741,7 @@ async function executeQueueAction(item) {
|
|
|
739
741
|
sourceChannel: item.channel,
|
|
740
742
|
progressPropagated: false,
|
|
741
743
|
createdAt: Date.now(),
|
|
742
|
-
});
|
|
744
|
+
}); } catch (_storeErr) { /* non-fatal: SQLite store unavailable */ }
|
|
743
745
|
// Non-blocking Memgraph projection (fire-and-forget)
|
|
744
746
|
setImmediate(() => mg.safeWrite(`
|
|
745
747
|
CREATE (t:Task {
|
|
@@ -1051,6 +1053,46 @@ class RoutineEvaluator {
|
|
|
1051
1053
|
}
|
|
1052
1054
|
|
|
1053
1055
|
async evaluate() {
|
|
1056
|
+
// HIGH-1 fix: promote any queued_coalesced RoutineRun nodes whose active run has now
|
|
1057
|
+
// completed. This runs BEFORE the due-routines query so promoted runs are visible immediately.
|
|
1058
|
+
try {
|
|
1059
|
+
const coalescedResult = await this.mg(
|
|
1060
|
+
`MATCH (rr:RoutineRun {companyId: $cid, status: 'queued_coalesced'})
|
|
1061
|
+
WHERE NOT EXISTS {
|
|
1062
|
+
MATCH (active:RoutineRun {routineId: rr.routineId, companyId: $cid})
|
|
1063
|
+
WHERE active.status IN ['queued', 'running']
|
|
1064
|
+
AND active.id <> rr.id
|
|
1065
|
+
}
|
|
1066
|
+
RETURN rr.id AS runId, rr.routineId AS routineId`,
|
|
1067
|
+
{ cid: this.companyId }
|
|
1068
|
+
);
|
|
1069
|
+
const coalescedRows = coalescedResult?.rows ?? [];
|
|
1070
|
+
for (const row of coalescedRows) {
|
|
1071
|
+
const coalescedRunId = row[0] ?? row['runId'];
|
|
1072
|
+
const coalescedRoutineId = row[1] ?? row['routineId'];
|
|
1073
|
+
if (!coalescedRunId || !coalescedRoutineId) continue;
|
|
1074
|
+
// Fetch routine details to create the task
|
|
1075
|
+
const routineResult = await this.mg(
|
|
1076
|
+
`MATCH (r:Routine {id: $rid, companyId: $cid}) RETURN r.name, r.agentId`,
|
|
1077
|
+
{ rid: coalescedRoutineId, cid: this.companyId }
|
|
1078
|
+
);
|
|
1079
|
+
const routineRows = routineResult?.rows ?? [];
|
|
1080
|
+
if (routineRows.length === 0) continue;
|
|
1081
|
+
const routineName = routineRows[0][0] ?? routineRows[0]['r.name'] ?? coalescedRoutineId;
|
|
1082
|
+
const routineAgentId = routineRows[0][1] ?? routineRows[0]['r.agentId'];
|
|
1083
|
+
if (!routineAgentId) continue;
|
|
1084
|
+
const promotedTaskId = `task:routine:${coalescedRoutineId}:coalesced:${randomUUID()}`;
|
|
1085
|
+
await this.mg(
|
|
1086
|
+
`MERGE (t:Task {id: $taskId}) SET t.title = $title, t.status = 'todo', t.assigneeAgentId = $agentId, t.companyId = $companyId, t.originKind = 'routine', t.progressPropagated = false, t.createdAt = datetime()
|
|
1087
|
+
WITH t MATCH (rr:RoutineRun {id: $runId}) SET rr.status = 'queued', rr.linkedTaskId = $taskId`,
|
|
1088
|
+
{ taskId: promotedTaskId, title: `Routine: ${routineName}`, agentId: routineAgentId, companyId: this.companyId, runId: coalescedRunId }
|
|
1089
|
+
).catch(e => log('warn', `RoutineEvaluator: coalesced promotion failed for ${coalescedRoutineId}: ${e.message}`));
|
|
1090
|
+
log('info', `RoutineEvaluator: promoted coalesced run ${coalescedRunId} → Task ${promotedTaskId}`);
|
|
1091
|
+
}
|
|
1092
|
+
} catch (coalescedErr) {
|
|
1093
|
+
log('warn', `RoutineEvaluator: coalesced promotion check failed: ${coalescedErr.message}`);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1054
1096
|
let dueRoutines;
|
|
1055
1097
|
try {
|
|
1056
1098
|
// SC-26: ISO timestamp format is correct — converts .000Z suffix to +00:00 for
|
|
@@ -1058,7 +1100,7 @@ class RoutineEvaluator {
|
|
|
1058
1100
|
// milliseconds-and-Z suffix produced by toISOString(). No change needed.
|
|
1059
1101
|
const now = new Date().toISOString().replace(/\.\d{3}Z$/, '+00:00');
|
|
1060
1102
|
dueRoutines = await this.mg(
|
|
1061
|
-
`MATCH (r:Routine {companyId: $companyId}) WHERE r.status = 'active' AND r.nextRunAt <= datetime($now) RETURN r.id, r.name, r.cronExpr, r.agentId, r.companyId, r.concurrencyPolicy, r.timezone`,
|
|
1103
|
+
`MATCH (r:Routine {companyId: $companyId}) WHERE r.status = 'active' AND r.nextRunAt <= datetime($now) RETURN r.id, r.name, r.cronExpr, r.agentId, r.companyId, r.concurrencyPolicy, r.timezone, r.catchUpCap, r.catchUpPolicy`,
|
|
1062
1104
|
{ now, companyId: this.companyId }
|
|
1063
1105
|
);
|
|
1064
1106
|
} catch (err) {
|
|
@@ -1086,11 +1128,81 @@ class RoutineEvaluator {
|
|
|
1086
1128
|
}
|
|
1087
1129
|
}
|
|
1088
1130
|
|
|
1131
|
+
// P5-03: coalesce_if_active — skip creating a full run but queue one follow-up RoutineRun
|
|
1132
|
+
if (routine['r.concurrencyPolicy'] === 'coalesce_if_active') {
|
|
1133
|
+
const active = await this.mg(
|
|
1134
|
+
`MATCH (rr:RoutineRun {routineId: $rid}) WHERE rr.status IN ['queued', 'running'] RETURN count(rr) as cnt`,
|
|
1135
|
+
{ rid: routineId }
|
|
1136
|
+
);
|
|
1137
|
+
const activeCount = active?.rows?.[0]?.[0] ?? 0;
|
|
1138
|
+
if (activeCount > 0) {
|
|
1139
|
+
// Check if a coalesced follow-up already exists (prevent duplicate queuing)
|
|
1140
|
+
const queuedFollowUp = await this.mg(
|
|
1141
|
+
`MATCH (rr:RoutineRun {routineId: $rid, status: 'queued_coalesced'}) RETURN count(rr) as cnt`,
|
|
1142
|
+
{ rid: routineId }
|
|
1143
|
+
);
|
|
1144
|
+
if ((queuedFollowUp?.rows?.[0]?.[0] ?? 0) === 0) {
|
|
1145
|
+
const followUpRunId = requireRunId(`run:${routineId}:coalesced:${randomUUID()}`, 'RoutineEvaluator.coalesce');
|
|
1146
|
+
await this.mg(
|
|
1147
|
+
`MERGE (rr:RoutineRun {id: $runId}) SET rr.routineId = $routineId, rr.status = 'queued_coalesced', rr.companyId = $companyId, rr.queuedAt = datetime()`,
|
|
1148
|
+
{ runId: followUpRunId, routineId, companyId: this.companyId }
|
|
1149
|
+
).catch(e => log('warn', `RoutineEvaluator: coalesce follow-up failed: ${e.message}`));
|
|
1150
|
+
}
|
|
1151
|
+
log('info', `[RoutineEvaluator] coalescing — prior run still active for routine ${routineId}`);
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1089
1156
|
const routineAgentId = routine['r.agentId'];
|
|
1090
1157
|
if (!routineAgentId) {
|
|
1091
1158
|
log('warn', `RoutineEvaluator: routine ${routineId} has no agentId — skipping task creation`);
|
|
1092
1159
|
continue;
|
|
1093
1160
|
}
|
|
1161
|
+
|
|
1162
|
+
// P5-04: catchUpCap — enqueue missed windows up to cap
|
|
1163
|
+
// CRIT-1 fix: previousRun() on a fresh Cron instance always returns null because
|
|
1164
|
+
// _states.previousRun is only set when the callback fires. Use forward iteration
|
|
1165
|
+
// from lastRunAt instead: call nextRun() repeatedly from lastRunAt until we reach now.
|
|
1166
|
+
const catchUpPolicy = routine['r.catchUpPolicy'];
|
|
1167
|
+
const catchUpCap = parseInt(routine['r.catchUpCap'] ?? '0', 10) || 0;
|
|
1168
|
+
if (catchUpPolicy === 'enqueue_missed_with_cap' && catchUpCap > 0) {
|
|
1169
|
+
try {
|
|
1170
|
+
const { Cron } = require('croner');
|
|
1171
|
+
const lastRunStr = routine['r.lastRunAt'];
|
|
1172
|
+
if (lastRunStr) {
|
|
1173
|
+
const lastRunMs = Date.parse(lastRunStr);
|
|
1174
|
+
const nowMs = Date.now();
|
|
1175
|
+
let missedCount = 0;
|
|
1176
|
+
// Walk forward from lastRunAt, counting how many scheduled windows occurred before now
|
|
1177
|
+
let probe = new Cron(routine['r.cronExpr'], { timezone: routine['r.timezone'], startAt: new Date(lastRunMs) });
|
|
1178
|
+
let next = probe.nextRun();
|
|
1179
|
+
probe.stop();
|
|
1180
|
+
while (next && next.getTime() < nowMs && missedCount < catchUpCap) {
|
|
1181
|
+
missedCount++;
|
|
1182
|
+
// Advance probe to count next window after this one
|
|
1183
|
+
probe = new Cron(routine['r.cronExpr'], { timezone: routine['r.timezone'], startAt: next });
|
|
1184
|
+
next = probe.nextRun();
|
|
1185
|
+
probe.stop();
|
|
1186
|
+
}
|
|
1187
|
+
for (let i = 0; i < missedCount; i++) {
|
|
1188
|
+
const catchUpTaskId = `task:routine:${routineId}:catchup:${i}:${randomUUID()}`;
|
|
1189
|
+
const catchUpRunId = requireRunId(`run:${routineId}:catchup:${i}:${randomUUID()}`, 'RoutineEvaluator.catchup');
|
|
1190
|
+
await this.mg(
|
|
1191
|
+
`MERGE (t:Task {id: $taskId}) SET t.title = $title, t.status = 'todo', t.assigneeAgentId = $agentId, t.companyId = $companyId, t.originKind = 'routine', t.progressPropagated = false, t.createdAt = datetime()`,
|
|
1192
|
+
{ taskId: catchUpTaskId, title: `Routine: ${routine['r.name']}`, agentId: routineAgentId, companyId: this.companyId }
|
|
1193
|
+
).catch(e => log('warn', `RoutineEvaluator: catch-up task create failed: ${e.message}`));
|
|
1194
|
+
await this.mg(
|
|
1195
|
+
`MERGE (rr:RoutineRun {id: $runId}) SET rr.routineId = $routineId, rr.status = 'queued', rr.linkedTaskId = $taskId, rr.companyId = $companyId, rr.queuedAt = datetime()`,
|
|
1196
|
+
{ runId: catchUpRunId, routineId, taskId: catchUpTaskId, companyId: this.companyId }
|
|
1197
|
+
).catch(e => log('warn', `RoutineEvaluator: catch-up run create failed: ${e.message}`));
|
|
1198
|
+
}
|
|
1199
|
+
if (missedCount > 0) log('info', `[RoutineEvaluator] enqueue_missed_with_cap: queued ${missedCount} catch-up tasks for routine ${routineId} (${routine['r.name']})`);
|
|
1200
|
+
}
|
|
1201
|
+
} catch (catchUpErr) {
|
|
1202
|
+
log('warn', `RoutineEvaluator: catchUpCap logic failed for ${routineId}: ${catchUpErr.message}`);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1094
1206
|
const taskId = `task:routine:${routineId}:${randomUUID()}`;
|
|
1095
1207
|
await criticalOp(
|
|
1096
1208
|
() => this.mg(
|
|
@@ -1926,6 +2038,26 @@ class AgentDispatcher {
|
|
|
1926
2038
|
log('debug', `[${taskId}] context-brief skipped for adapter path: ${_cbe.message}`);
|
|
1927
2039
|
}
|
|
1928
2040
|
|
|
2041
|
+
// P6-06b: read workspacePath from Task node — desktop sets this after acquiring
|
|
2042
|
+
// a SessionWorktree for isolated execution (workspaceMode='isolated').
|
|
2043
|
+
// Non-fatal: if unavailable, falls back to null (no worktree isolation).
|
|
2044
|
+
// HIGH-2 fix: include companyId in MATCH to enforce multi-tenant isolation
|
|
2045
|
+
// MEDIUM-4 fix: skip the extra Memgraph round-trip for tasks that cannot have a
|
|
2046
|
+
// workspace (only 'workspace' and 'crew' originKinds support isolated worktrees)
|
|
2047
|
+
let _taskWorkspacePath = null;
|
|
2048
|
+
if (originKind === 'workspace' || originKind === 'crew') {
|
|
2049
|
+
try {
|
|
2050
|
+
const _wpResult = await this.mg(
|
|
2051
|
+
`MATCH (t:Task {id: $taskId, companyId: $cid}) RETURN t.workspacePath AS wp`,
|
|
2052
|
+
{ taskId, cid: this.companyId }
|
|
2053
|
+
);
|
|
2054
|
+
const _wpRows = _wpResult?.rows ?? [];
|
|
2055
|
+
_taskWorkspacePath = _wpRows[0]?.[0] ?? _wpRows[0]?.['wp'] ?? null;
|
|
2056
|
+
} catch (_wpe) {
|
|
2057
|
+
log('debug', `[${taskId}] workspacePath read skipped: ${_wpe.message}`);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
|
|
1929
2061
|
const adapterContext = {
|
|
1930
2062
|
runId: `run:${taskId}:${randomUUID()}`,
|
|
1931
2063
|
agentId,
|
|
@@ -1934,7 +2066,7 @@ class AgentDispatcher {
|
|
|
1934
2066
|
instructions: body ? `Execute task: ${title} (ID: ${taskId})\n\n${body}` : `Execute task: ${title} (ID: ${taskId})`,
|
|
1935
2067
|
skills: [],
|
|
1936
2068
|
budget: null,
|
|
1937
|
-
workspacePath:
|
|
2069
|
+
workspacePath: _taskWorkspacePath,
|
|
1938
2070
|
envVars: { HELIOS_TASK_ID: taskId, HELIOS_AGENT_ID: agentId },
|
|
1939
2071
|
onLog: (event) => log('debug', `[${taskId}] ${event.stream}: ${event.chunk}`),
|
|
1940
2072
|
onMeta: (meta) => log('debug', `[${taskId}] meta: ${meta.type}`, meta.data),
|
|
@@ -2239,22 +2371,37 @@ class AgentDispatcher {
|
|
|
2239
2371
|
}
|
|
2240
2372
|
|
|
2241
2373
|
// Fallback: direct pi spawn (only when heliosConfig.apiUrl is not set)
|
|
2374
|
+
// Spawns process.execPath (node.exe) + the @cgh567/cli JS entrypoint directly.
|
|
2375
|
+
// This avoids spawning pi.cmd (.cmd shim) which causes Windows to create a
|
|
2376
|
+
// conhost.exe console host, producing the visible terminal window flash.
|
|
2377
|
+
// process.execPath is a native .exe on all platforms — no .cmd wrapper, no conhost.
|
|
2242
2378
|
if (!heliosConfig?.apiUrl) {
|
|
2243
2379
|
|
|
2244
2380
|
const { spawn } = require('child_process');
|
|
2245
2381
|
const spawnFn = this._spawnFn ?? spawn;
|
|
2246
2382
|
const piArgs = ['--skill', skill, '--prompt', `Execute task: ${title} (ID: ${taskId})`];
|
|
2247
2383
|
|
|
2248
|
-
// Resolve
|
|
2249
|
-
//
|
|
2250
|
-
//
|
|
2251
|
-
//
|
|
2384
|
+
// Resolve the actual JS entrypoint of pi from node_modules.
|
|
2385
|
+
// On Windows, pi.cmd is a .cmd shim — spawning it via Node creates a conhost.exe
|
|
2386
|
+
// console host window even with windowsHide:true (CREATE_NO_WINDOW applies to the
|
|
2387
|
+
// spawned process, not the conhost the kernel creates for .cmd files).
|
|
2388
|
+
// Fix: resolve @cgh567/cli/dist/cli.js and spawn via process.execPath (node.exe)
|
|
2389
|
+
// directly. This is cross-platform: process.execPath is the node binary on all OSes.
|
|
2390
|
+
const _piCliJs = path.join(__dirname, '..', 'node_modules', '@cgh567', 'cli', 'dist', 'cli.js');
|
|
2391
|
+
const _piCliExists = fs.existsSync(_piCliJs);
|
|
2392
|
+
|
|
2393
|
+
// Spawn args: if JS entrypoint found, use process.execPath + [cli.js, ...piArgs].
|
|
2394
|
+
// Fallback (cli.js not found): resolve pi binary the old way and pass piArgs directly.
|
|
2395
|
+
// The fallback preserves original behaviour on systems without the expected layout.
|
|
2252
2396
|
const _piBinName = process.platform === 'win32' ? 'pi.cmd' : 'pi';
|
|
2253
|
-
const
|
|
2254
|
-
const
|
|
2397
|
+
const _localPiBin = path.join(__dirname, '..', 'node_modules', '.bin', _piBinName);
|
|
2398
|
+
const _piFallbackCmd = fs.existsSync(_localPiBin) ? _localPiBin
|
|
2255
2399
|
: (process.platform !== 'win32' && fs.existsSync(`${os.homedir()}/.local/bin/pi`)) ? `${os.homedir()}/.local/bin/pi`
|
|
2256
2400
|
: 'pi';
|
|
2257
2401
|
|
|
2402
|
+
const spawnExe = _piCliExists ? process.execPath : _piFallbackCmd;
|
|
2403
|
+
const spawnArgs = _piCliExists ? [_piCliJs, ...piArgs] : piArgs;
|
|
2404
|
+
|
|
2258
2405
|
// E-07: Build complete spawn env — PI_SUBAGENT_DEPTH, PI_AGENT, HELIOS_COMPANY_ID
|
|
2259
2406
|
// must always be injected so workers know they are subagents and governance
|
|
2260
2407
|
// gates (H71, STRICT_EDIT_BLOCK) do not block them at startup.
|
|
@@ -2273,6 +2420,7 @@ class AgentDispatcher {
|
|
|
2273
2420
|
HELIOS_TASK_ID: taskId,
|
|
2274
2421
|
HELIOS_AGENT_ID: agentId,
|
|
2275
2422
|
HELIOS_COMPANY_ID: String(this.companyId),
|
|
2423
|
+
HELIOS_HEADLESS: '1', // Enable browser auto-run mode for research agents (safety.ts:55)
|
|
2276
2424
|
};
|
|
2277
2425
|
|
|
2278
2426
|
// E-07: Capture stderr to crash log so silent failures become visible.
|
|
@@ -2282,11 +2430,12 @@ class AgentDispatcher {
|
|
|
2282
2430
|
|
|
2283
2431
|
log('info', `Dispatching task ${taskId} to agent ${agentId} with skill ${skill}`, { taskId, agentId, skill });
|
|
2284
2432
|
|
|
2285
|
-
const child = spawnFn(
|
|
2433
|
+
const child = spawnFn(spawnExe, spawnArgs, {
|
|
2286
2434
|
detached: true,
|
|
2287
2435
|
stdio: ['ignore', 'ignore', 'pipe'],
|
|
2288
2436
|
env: spawnEnv,
|
|
2289
2437
|
windowsHide: true,
|
|
2438
|
+
shell: false,
|
|
2290
2439
|
});
|
|
2291
2440
|
|
|
2292
2441
|
// Collect stderr chunks for crash log (64KB cap to prevent OOM from chatty workers)
|
|
@@ -2498,8 +2647,8 @@ class ActivityLogger {
|
|
|
2498
2647
|
for (const event of batch) {
|
|
2499
2648
|
try {
|
|
2500
2649
|
await this.mg(
|
|
2501
|
-
`MERGE (ae:ActivityEvent {id: $id}) SET ae.action = $action, ae.actor = $actor, ae.entityId = $entityId, ae.companyId = $companyId, ae.createdAt = datetime(), ae.meta = $meta`,
|
|
2502
|
-
{ id: event.id, action: event.action, actor: event.actor ?? 'daemon', entityId: event.entityId ?? '', companyId: event.companyId ?? this._companyId, meta: JSON.stringify(event.meta ?? {}) }
|
|
2650
|
+
`MERGE (ae:ActivityEvent {id: $id}) SET ae.action = $action, ae.actor = $actor, ae.entityId = $entityId, ae.companyId = $companyId, ae.outcome = $outcome, ae.createdAt = datetime(), ae.meta = $meta`,
|
|
2651
|
+
{ id: event.id, action: event.action, actor: event.actor ?? 'daemon', entityId: event.entityId ?? '', companyId: event.companyId ?? this._companyId, outcome: event.outcome ?? null, meta: JSON.stringify(event.meta ?? {}) }
|
|
2503
2652
|
);
|
|
2504
2653
|
successCount++;
|
|
2505
2654
|
} catch (e) {
|
|
@@ -2609,7 +2758,7 @@ class CostEventSyncer {
|
|
|
2609
2758
|
let db;
|
|
2610
2759
|
try {
|
|
2611
2760
|
db = new Database(this._dbPath, { readonly: true });
|
|
2612
|
-
db.pragma('busy_timeout =
|
|
2761
|
+
db.pragma('busy_timeout = 5000'); // 5000ms max wait if DB is locked
|
|
2613
2762
|
} catch (err) {
|
|
2614
2763
|
log('warn', `CostEventSyncer: failed to open SQLite DB at ${this._dbPath}: ${err.message}`);
|
|
2615
2764
|
return [];
|
|
@@ -3083,7 +3232,7 @@ class ApprovalWatcher {
|
|
|
3083
3232
|
approved = await this.mg(
|
|
3084
3233
|
`MATCH (a:Approval {companyId: $cid, status: 'approved'})
|
|
3085
3234
|
WHERE a.followUpTaskCreated IS NULL OR a.followUpTaskCreated = false
|
|
3086
|
-
RETURN a.id, a.title, a.requestedBy, a.type, a.strategyId`,
|
|
3235
|
+
RETURN a.id, a.title, a.requestedBy, a.type, a.strategyId, a.sourceTaskId, a.goalId`,
|
|
3087
3236
|
{ cid: this.companyId }
|
|
3088
3237
|
);
|
|
3089
3238
|
} catch (mgErr) {
|
|
@@ -3092,8 +3241,8 @@ class ApprovalWatcher {
|
|
|
3092
3241
|
const storeApprovals = hboStore.getApprovalsByCompanyStatus(this.companyId, 'approved')
|
|
3093
3242
|
.filter(a => !a.followUpTaskCreated);
|
|
3094
3243
|
approved = {
|
|
3095
|
-
rows: storeApprovals.map(a => [a.id, a.title, a.requestedBy, a.type, a.strategyId ?? null]),
|
|
3096
|
-
keys: ['a.id', 'a.title', 'a.requestedBy', 'a.type', 'a.strategyId'],
|
|
3244
|
+
rows: storeApprovals.map(a => [a.id, a.title, a.requestedBy, a.type, a.strategyId ?? null, a.sourceTaskId ?? null, a.goalId ?? null]),
|
|
3245
|
+
keys: ['a.id', 'a.title', 'a.requestedBy', 'a.type', 'a.strategyId', 'a.sourceTaskId', 'a.goalId'],
|
|
3097
3246
|
};
|
|
3098
3247
|
log('info', `ApprovalWatcher: using SQLite fallback for approval lookup (Memgraph unavailable): ${mgErr.message}`);
|
|
3099
3248
|
}
|
|
@@ -3140,6 +3289,28 @@ class ApprovalWatcher {
|
|
|
3140
3289
|
).catch(e => log('warn', `ApprovalWatcher: strategy approve projection failed: ${e.message}`)));
|
|
3141
3290
|
log('info', `ApprovalWatcher: strategy ${strategyId} approved`);
|
|
3142
3291
|
}
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
// T3-05: harada_strategy_review approved → dispatch L2 research task for this pillar
|
|
3295
|
+
if (approvalType === 'harada_strategy_review') {
|
|
3296
|
+
const pillarId = approval['a.sourceTaskId'];
|
|
3297
|
+
const goalId = approval['a.goalId'];
|
|
3298
|
+
const l2AgentId = approval['a.requestedBy'] || requestedBy;
|
|
3299
|
+
if (pillarId && goalId) {
|
|
3300
|
+
setImmediate(() => {
|
|
3301
|
+
try {
|
|
3302
|
+
const { CascadeResearchDispatcher } = require('./lib/harada/cascade-research-dispatcher');
|
|
3303
|
+
const crd = new CascadeResearchDispatcher(this.mg.bind(this), this.companyId);
|
|
3304
|
+
crd.dispatchL2Research(pillarId, goalId, this.companyId, l2AgentId)
|
|
3305
|
+
.then(() => log('info', `ApprovalWatcher: L2 research dispatched for pillar ${pillarId}`))
|
|
3306
|
+
.catch(e => log('warn', `ApprovalWatcher: L2 dispatch failed for pillar ${pillarId}: ${e.message}`));
|
|
3307
|
+
} catch (dispErr) {
|
|
3308
|
+
log('warn', `ApprovalWatcher: CascadeResearchDispatcher load failed: ${dispErr.message}`);
|
|
3309
|
+
}
|
|
3310
|
+
});
|
|
3311
|
+
} else {
|
|
3312
|
+
log('warn', `ApprovalWatcher: harada_strategy_review approved but missing pillarId (${pillarId}) or goalId (${goalId})`);
|
|
3313
|
+
}
|
|
3143
3314
|
}
|
|
3144
3315
|
|
|
3145
3316
|
// SQLite-first task create (P2-5)
|
|
@@ -3158,11 +3329,13 @@ class ApprovalWatcher {
|
|
|
3158
3329
|
{ taskId, title: `Approval resolved: ${title}. Execute the approved plan.`, agentId: requestedBy, cid: this.companyId, approvalId }
|
|
3159
3330
|
).catch(e => log('warn', `[daemon] Memgraph Task projection failed (non-fatal): ${e.message}`)));
|
|
3160
3331
|
// SQLite-first approval update (P2-5)
|
|
3161
|
-
|
|
3332
|
+
// P8D-01: also write followUpTaskId so "View Task →" link in ApprovalCard works
|
|
3333
|
+
try { hboStore.updateApproval(approvalId, this.companyId, { followUpTaskCreated: true, followUpTaskId: taskId }); } catch (_) {}
|
|
3162
3334
|
// Non-blocking Memgraph projection (fire-and-forget)
|
|
3335
|
+
// P8D-01: SET followUpTaskId so GET /api/approvals returns it for navigation
|
|
3163
3336
|
setImmediate(() => this.mg(
|
|
3164
|
-
`MATCH (a:Approval {id: $approvalId}) SET a.followUpTaskCreated = true`,
|
|
3165
|
-
{ approvalId }
|
|
3337
|
+
`MATCH (a:Approval {id: $approvalId}) SET a.followUpTaskCreated = true, a.followUpTaskId = $taskId`,
|
|
3338
|
+
{ approvalId, taskId }
|
|
3166
3339
|
).catch(e => log('warn', `[daemon] Memgraph Approval projection failed (non-fatal): ${e.message}`)));
|
|
3167
3340
|
log('info', `ApprovalWatcher: created follow-up task ${taskId} for approval ${approvalId}`);
|
|
3168
3341
|
} catch (err) {
|
|
@@ -3224,12 +3397,13 @@ function buildForCompany(companyId, mgQueryAsync, opts) {
|
|
|
3224
3397
|
try { mods.taskCompletionWatchdog = new TaskCompletionWatchdog(mgQueryAsync, cid, _daemonConfig.taskTimeoutMs ?? 1800000); }
|
|
3225
3398
|
catch (e) { log('warn', `[module-factory] TaskCompletionWatchdog init failed for ${cid}: ${e.message}`); mods.taskCompletionWatchdog = null; }
|
|
3226
3399
|
|
|
3227
|
-
try { mods.runCompletionPoller = new RunCompletionPoller(mgQueryAsync, cid, typeof broadcast === 'function' ? (...args) => broadcast(...args) : null, new TaskCompletionProcessor({ mgQuery: mgQueryAsync })); }
|
|
3228
|
-
catch (e) { log('warn', `[module-factory] RunCompletionPoller init failed for ${cid}: ${e.message}`); mods.runCompletionPoller = null; }
|
|
3229
|
-
|
|
3230
3400
|
try { mods.activityLogger = new ActivityLogger(mgQueryAsync, cid); }
|
|
3231
3401
|
catch (e) { log('warn', `[module-factory] ActivityLogger init failed for ${cid}: ${e.message}`); mods.activityLogger = null; }
|
|
3232
3402
|
|
|
3403
|
+
// H-01: activityLogger injected so TaskCompletionProcessor can record task.complete events
|
|
3404
|
+
try { mods.runCompletionPoller = new RunCompletionPoller(mgQueryAsync, cid, typeof broadcast === 'function' ? (...args) => broadcast(...args) : null, new TaskCompletionProcessor({ mgQuery: mgQueryAsync, activityLogger: mods.activityLogger })); }
|
|
3405
|
+
catch (e) { log('warn', `[module-factory] RunCompletionPoller init failed for ${cid}: ${e.message}`); mods.runCompletionPoller = null; }
|
|
3406
|
+
|
|
3233
3407
|
try { mods.approvalWatcher = new ApprovalWatcher(mgQueryAsync, cid); }
|
|
3234
3408
|
catch (e) { log('warn', `[module-factory] ApprovalWatcher init failed for ${cid}: ${e.message}`); mods.approvalWatcher = null; }
|
|
3235
3409
|
|
|
@@ -3288,6 +3462,8 @@ function buildForCompany(companyId, mgQueryAsync, opts) {
|
|
|
3288
3462
|
mods.cascadeReviewTrigger = safeNew('CascadeReviewTrigger', './lib/harada/cascade-review', 'CascadeReviewTrigger', mgQueryAsync, cid);
|
|
3289
3463
|
mods.beliefCeilingAssessment = safeNew('BeliefCeilingAssessment', './lib/harada/belief-ceiling', 'BeliefCeilingAssessment', mgQueryAsync, cid);
|
|
3290
3464
|
mods.sacrificeDeclaration = safeNew('SacrificeDeclaration', './lib/harada/sacrifice-declaration', 'SacrificeDeclaration', mgQueryAsync, cid);
|
|
3465
|
+
// T3-04: CascadeJudge auto-reviews L2/L3 submissions — called every 5 ticks
|
|
3466
|
+
mods.cascadeJudge = safeNew('CascadeJudge', './lib/harada/cascade-judge', 'CascadeJudge', mgQueryAsync, cid);
|
|
3291
3467
|
|
|
3292
3468
|
try {
|
|
3293
3469
|
// MirrorPatternScan runs on a P7D (weekly) wall-clock schedule via WallClockScheduler
|
|
@@ -4030,6 +4206,9 @@ class HeliosCompanyDaemon {
|
|
|
4030
4206
|
if (this._tickCount % 5 === 1) {
|
|
4031
4207
|
await this._runModule('hbo_goal_decompose', () => this._forAllCompanies((cid, m) => m.hboBridge?.tickGoalDecompose()));
|
|
4032
4208
|
await this._runModule('hbo_goal_sync', () => this._forAllCompanies((cid, m) => m.hboBridge?.tickGoalSync()));
|
|
4209
|
+
// T3-04: Auto-review L2/L3 submissions every 5 ticks
|
|
4210
|
+
await this._runModule('CascadeJudgeL2', () => this._forAllCompanies((cid, m) => m.cascadeJudge?.judgeReadyPillars()));
|
|
4211
|
+
await this._runModule('CascadeJudgeL3', () => this._forAllCompanies((cid, m) => m.cascadeJudge?.judgeReadyActionCells()));
|
|
4033
4212
|
await this._runModule('PDSACompletion', () => this._forAllCompanies((cid, m) => m.pdsaCompletion?.process()));
|
|
4034
4213
|
await this._runModule('ProgressPropagation', () => this._forAllCompanies((cid, m) => m.progressPropagation?.propagate()));
|
|
4035
4214
|
await this._runModule('CatchballProcessing', () => this._forAllCompanies((cid, m) => m.catchballProcessing?.process()));
|
|
@@ -4330,10 +4509,30 @@ class HeliosCompanyDaemon {
|
|
|
4330
4509
|
// CRITICAL-3 fix: wait for Memgraph to be reachable before running migrations.
|
|
4331
4510
|
// Without this, PM2 can restart the daemon before Memgraph is up post-OOM,
|
|
4332
4511
|
// and all migrations + MAGE backfill silently fail, leaving the graph in a partial state.
|
|
4333
|
-
await this._waitForMemgraph();
|
|
4334
|
-
await this._connectMemgraph();
|
|
4335
|
-
|
|
4336
|
-
|
|
4512
|
+
await this._waitForMemgraph();
|
|
4513
|
+
await this._connectMemgraph();
|
|
4514
|
+
|
|
4515
|
+
try {
|
|
4516
|
+
const replay = await graphWal.replayPending(this._mgQueryAsync.bind(this));
|
|
4517
|
+
log('info', 'WAL replay complete', replay);
|
|
4518
|
+
} catch (err) {
|
|
4519
|
+
log('error', 'WAL replay failed', { error: err.message });
|
|
4520
|
+
}
|
|
4521
|
+
|
|
4522
|
+
const migrateScript = path.join(DAEMON_DIR, 'db', 'hbo-core-migrate.js');
|
|
4523
|
+
execFile(process.execPath, ['--experimental-sqlite', migrateScript], {
|
|
4524
|
+
cwd: HELIOS_ROOT,
|
|
4525
|
+
timeout: 60_000,
|
|
4526
|
+
env: process.env,
|
|
4527
|
+
}, (err, stdout, stderr) => {
|
|
4528
|
+
if (err) {
|
|
4529
|
+
log('warn', 'hbo-core reconcile failed', { error: err.message, stderr: String(stderr || '').slice(0, 1000) });
|
|
4530
|
+
return;
|
|
4531
|
+
}
|
|
4532
|
+
log('info', 'hbo-core reconcile complete', { stdout: String(stdout || '').slice(0, 1000) });
|
|
4533
|
+
});
|
|
4534
|
+
|
|
4535
|
+
// ── TZ sanity check ─────────────────────────────────────────────────────────
|
|
4337
4536
|
if (!process.env.TZ || process.env.TZ !== 'UTC') {
|
|
4338
4537
|
log('warn', `[startup] TZ=${process.env.TZ} — recommend TZ=UTC for consistent datetime comparisons. See AGENTS.md.`);
|
|
4339
4538
|
}
|
|
@@ -5552,6 +5751,7 @@ if (require.main === module) {
|
|
|
5552
5751
|
getAgents: () => daemon._agents || [],
|
|
5553
5752
|
getDaemonHealth: () => ({ pid: process.pid, uptime: process.uptime(), tickCount: daemon._tickCount }),
|
|
5554
5753
|
daemon,
|
|
5754
|
+
activityLogger: daemon._activityLogger ?? null, // H-01: approval event recording in helios-api.js approve/reject handlers
|
|
5555
5755
|
broadcast, // Phase 4/5: required for project SSE events (project:understanding:ready etc.)
|
|
5556
5756
|
companies: _allCompanyConfigs.map(cfg => ({
|
|
5557
5757
|
id: cfg.company?.id || cfg.companyName || 'unknown',
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFile } = require('child_process');
|
|
4
|
+
const { promisify } = require('util');
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* BlastRadiusAnalyzer — measures the impact of a HED operation's execution.
|
|
9
|
+
* Compares declared target vs actual changes (git diff + Memgraph diff).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
class BlastRadiusAnalyzer {
|
|
13
|
+
constructor(mg, repoPath) {
|
|
14
|
+
this._mg = mg;
|
|
15
|
+
this._repoPath = repoPath || process.cwd();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async analyze(opId, operation, executionLockedAt, completedAt) {
|
|
19
|
+
const [filesChanged, nodesAffected] = await Promise.allSettled([
|
|
20
|
+
this._getChangedFiles(operation.target),
|
|
21
|
+
this._getChangedNodes(executionLockedAt, completedAt)
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const changedFiles = filesChanged.status === 'fulfilled' ? filesChanged.value : [];
|
|
25
|
+
const changedNodes = nodesAffected.status === 'fulfilled' ? nodesAffected.value : [];
|
|
26
|
+
|
|
27
|
+
const outsideTarget = changedFiles.filter(f => !f.includes(operation.target || ''));
|
|
28
|
+
const severity = this._computeSeverity(outsideTarget, changedNodes);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
filesChanged: outsideTarget,
|
|
32
|
+
allFilesChanged: changedFiles,
|
|
33
|
+
nodesAffected: changedNodes,
|
|
34
|
+
severity,
|
|
35
|
+
opId
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async _getChangedFiles(declaredTarget) {
|
|
40
|
+
try {
|
|
41
|
+
const { stdout } = await execFileAsync('git', ['diff', '--name-only', 'HEAD~1'], {
|
|
42
|
+
cwd: this._repoPath,
|
|
43
|
+
windowsHide: true
|
|
44
|
+
});
|
|
45
|
+
return stdout.trim().split('\n').filter(Boolean);
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async _getChangedNodes(executionLockedAt, completedAt) {
|
|
52
|
+
if (!executionLockedAt || !completedAt) return [];
|
|
53
|
+
try {
|
|
54
|
+
const rows = await this._mg(
|
|
55
|
+
`MATCH (n) WHERE n.updatedAt >= $from AND n.updatedAt <= $to
|
|
56
|
+
RETURN DISTINCT labels(n)[0] AS label, count(n) AS count LIMIT 20`,
|
|
57
|
+
{ from: executionLockedAt, to: completedAt }
|
|
58
|
+
);
|
|
59
|
+
return (rows || []).map(r => `${r.label}(${r.count})`);
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_computeSeverity(outsideFiles, changedNodes) {
|
|
66
|
+
const score = outsideFiles.length * 2 + changedNodes.length;
|
|
67
|
+
if (score === 0) return 'none';
|
|
68
|
+
if (score <= 2) return 'low';
|
|
69
|
+
if (score <= 6) return 'medium';
|
|
70
|
+
if (score <= 12) return 'high';
|
|
71
|
+
return 'critical';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { BlastRadiusAnalyzer };
|