@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
|
@@ -1053,6 +1053,46 @@ class RoutineEvaluator {
|
|
|
1053
1053
|
}
|
|
1054
1054
|
|
|
1055
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
|
+
|
|
1056
1096
|
let dueRoutines;
|
|
1057
1097
|
try {
|
|
1058
1098
|
// SC-26: ISO timestamp format is correct — converts .000Z suffix to +00:00 for
|
|
@@ -1108,7 +1148,7 @@ class RoutineEvaluator {
|
|
|
1108
1148
|
{ runId: followUpRunId, routineId, companyId: this.companyId }
|
|
1109
1149
|
).catch(e => log('warn', `RoutineEvaluator: coalesce follow-up failed: ${e.message}`));
|
|
1110
1150
|
}
|
|
1111
|
-
log('
|
|
1151
|
+
log('info', `[RoutineEvaluator] coalescing — prior run still active for routine ${routineId}`);
|
|
1112
1152
|
continue;
|
|
1113
1153
|
}
|
|
1114
1154
|
}
|
|
@@ -1120,42 +1160,44 @@ class RoutineEvaluator {
|
|
|
1120
1160
|
}
|
|
1121
1161
|
|
|
1122
1162
|
// P5-04: catchUpCap — enqueue missed windows up to cap
|
|
1123
|
-
//
|
|
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.
|
|
1124
1166
|
const catchUpPolicy = routine['r.catchUpPolicy'];
|
|
1125
1167
|
const catchUpCap = parseInt(routine['r.catchUpCap'] ?? '0', 10) || 0;
|
|
1126
1168
|
if (catchUpPolicy === 'enqueue_missed_with_cap' && catchUpCap > 0) {
|
|
1127
1169
|
try {
|
|
1128
1170
|
const { Cron } = require('croner');
|
|
1129
|
-
const cron = new Cron(routine['r.cronExpr'], { timezone: routine['r.timezone'] });
|
|
1130
|
-
// Count missed windows: how many times cron fired between lastRunAt and now
|
|
1131
|
-
// Simple approximation: count backward from now until we hit lastRunAt or cap
|
|
1132
|
-
let missedCount = 0;
|
|
1133
|
-
const prev = cron.previousRun ? cron.previousRun() : null;
|
|
1134
|
-
// Since we don't have a full missed-window iterator here, use a conservative
|
|
1135
|
-
// estimate: check if at least one missed window exists and enqueue up to cap
|
|
1136
|
-
// by repeatedly calling previousRun. Max cap iterations.
|
|
1137
|
-
let checkDate = prev;
|
|
1138
1171
|
const lastRunStr = routine['r.lastRunAt'];
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
missedCount
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
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']})`);
|
|
1157
1200
|
}
|
|
1158
|
-
if (missedCount > 0) log('info', `RoutineEvaluator: catch-up ${missedCount} tasks for ${routine['r.name']}`);
|
|
1159
1201
|
} catch (catchUpErr) {
|
|
1160
1202
|
log('warn', `RoutineEvaluator: catchUpCap logic failed for ${routineId}: ${catchUpErr.message}`);
|
|
1161
1203
|
}
|
|
@@ -1996,6 +2038,26 @@ class AgentDispatcher {
|
|
|
1996
2038
|
log('debug', `[${taskId}] context-brief skipped for adapter path: ${_cbe.message}`);
|
|
1997
2039
|
}
|
|
1998
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
|
+
|
|
1999
2061
|
const adapterContext = {
|
|
2000
2062
|
runId: `run:${taskId}:${randomUUID()}`,
|
|
2001
2063
|
agentId,
|
|
@@ -2004,7 +2066,7 @@ class AgentDispatcher {
|
|
|
2004
2066
|
instructions: body ? `Execute task: ${title} (ID: ${taskId})\n\n${body}` : `Execute task: ${title} (ID: ${taskId})`,
|
|
2005
2067
|
skills: [],
|
|
2006
2068
|
budget: null,
|
|
2007
|
-
workspacePath:
|
|
2069
|
+
workspacePath: _taskWorkspacePath,
|
|
2008
2070
|
envVars: { HELIOS_TASK_ID: taskId, HELIOS_AGENT_ID: agentId },
|
|
2009
2071
|
onLog: (event) => log('debug', `[${taskId}] ${event.stream}: ${event.chunk}`),
|
|
2010
2072
|
onMeta: (meta) => log('debug', `[${taskId}] meta: ${meta.type}`, meta.data),
|
|
@@ -2309,22 +2371,37 @@ class AgentDispatcher {
|
|
|
2309
2371
|
}
|
|
2310
2372
|
|
|
2311
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.
|
|
2312
2378
|
if (!heliosConfig?.apiUrl) {
|
|
2313
2379
|
|
|
2314
2380
|
const { spawn } = require('child_process');
|
|
2315
2381
|
const spawnFn = this._spawnFn ?? spawn;
|
|
2316
2382
|
const piArgs = ['--skill', skill, '--prompt', `Execute task: ${title} (ID: ${taskId})`];
|
|
2317
2383
|
|
|
2318
|
-
// Resolve
|
|
2319
|
-
//
|
|
2320
|
-
//
|
|
2321
|
-
//
|
|
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.
|
|
2322
2396
|
const _piBinName = process.platform === 'win32' ? 'pi.cmd' : 'pi';
|
|
2323
|
-
const
|
|
2324
|
-
const
|
|
2397
|
+
const _localPiBin = path.join(__dirname, '..', 'node_modules', '.bin', _piBinName);
|
|
2398
|
+
const _piFallbackCmd = fs.existsSync(_localPiBin) ? _localPiBin
|
|
2325
2399
|
: (process.platform !== 'win32' && fs.existsSync(`${os.homedir()}/.local/bin/pi`)) ? `${os.homedir()}/.local/bin/pi`
|
|
2326
2400
|
: 'pi';
|
|
2327
2401
|
|
|
2402
|
+
const spawnExe = _piCliExists ? process.execPath : _piFallbackCmd;
|
|
2403
|
+
const spawnArgs = _piCliExists ? [_piCliJs, ...piArgs] : piArgs;
|
|
2404
|
+
|
|
2328
2405
|
// E-07: Build complete spawn env — PI_SUBAGENT_DEPTH, PI_AGENT, HELIOS_COMPANY_ID
|
|
2329
2406
|
// must always be injected so workers know they are subagents and governance
|
|
2330
2407
|
// gates (H71, STRICT_EDIT_BLOCK) do not block them at startup.
|
|
@@ -2343,23 +2420,26 @@ class AgentDispatcher {
|
|
|
2343
2420
|
HELIOS_TASK_ID: taskId,
|
|
2344
2421
|
HELIOS_AGENT_ID: agentId,
|
|
2345
2422
|
HELIOS_COMPANY_ID: String(this.companyId),
|
|
2423
|
+
HELIOS_HEADLESS: '1', // Enable browser auto-run mode for research agents (safety.ts:55)
|
|
2346
2424
|
};
|
|
2347
2425
|
|
|
2348
2426
|
// E-07: Capture stderr to crash log so silent failures become visible.
|
|
2349
|
-
// Use ['ignore', '
|
|
2427
|
+
// Use ['ignore', 'pipe', 'pipe'] — stdin ignored, stdout+stderr both captured for crash log.
|
|
2428
|
+
// Previously used ['ignore', 'ignore', 'pipe'] — pi process may write errors to stdout.
|
|
2350
2429
|
const transcriptsDir = path.join(__dirname, 'transcripts');
|
|
2351
2430
|
try { fs.mkdirSync(transcriptsDir, { recursive: true }); } catch (_) {}
|
|
2352
2431
|
|
|
2353
2432
|
log('info', `Dispatching task ${taskId} to agent ${agentId} with skill ${skill}`, { taskId, agentId, skill });
|
|
2354
2433
|
|
|
2355
|
-
const child = spawnFn(
|
|
2434
|
+
const child = spawnFn(spawnExe, spawnArgs, {
|
|
2356
2435
|
detached: true,
|
|
2357
|
-
stdio: ['ignore', '
|
|
2436
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
2358
2437
|
env: spawnEnv,
|
|
2359
2438
|
windowsHide: true,
|
|
2439
|
+
shell: false,
|
|
2360
2440
|
});
|
|
2361
2441
|
|
|
2362
|
-
// Collect stderr chunks for crash log (64KB cap to prevent OOM from chatty workers)
|
|
2442
|
+
// Collect stderr+stdout chunks for crash log (64KB cap per stream to prevent OOM from chatty workers)
|
|
2363
2443
|
const stderrChunks = [];
|
|
2364
2444
|
let _stderrBytes = 0;
|
|
2365
2445
|
const MAX_STDERR_BYTES = 64 * 1024;
|
|
@@ -2374,6 +2454,18 @@ class AgentDispatcher {
|
|
|
2374
2454
|
// (child.unref() alone is insufficient when a stderr pipe listener is attached)
|
|
2375
2455
|
child.stderr.unref();
|
|
2376
2456
|
}
|
|
2457
|
+
// Also capture stdout (pi process may write errors/traces to stdout)
|
|
2458
|
+
const stdoutChunks = [];
|
|
2459
|
+
let _stdoutBytes = 0;
|
|
2460
|
+
if (child.stdout) {
|
|
2461
|
+
child.stdout.on('data', (chunk) => {
|
|
2462
|
+
if (_stdoutBytes < MAX_STDERR_BYTES) {
|
|
2463
|
+
stdoutChunks.push(chunk);
|
|
2464
|
+
_stdoutBytes += chunk.length;
|
|
2465
|
+
}
|
|
2466
|
+
});
|
|
2467
|
+
child.stdout.unref();
|
|
2468
|
+
}
|
|
2377
2469
|
|
|
2378
2470
|
child.on('error', (err) => {
|
|
2379
2471
|
if (err.code === 'ENOENT') {
|
|
@@ -2386,6 +2478,7 @@ class AgentDispatcher {
|
|
|
2386
2478
|
// E-07: Write crash log when worker exits with non-zero code
|
|
2387
2479
|
if (code !== 0) {
|
|
2388
2480
|
const stderr = stderrChunks.length > 0 ? Buffer.concat(stderrChunks).toString('utf8') : '(no stderr captured)';
|
|
2481
|
+
const stdout = stdoutChunks.length > 0 ? Buffer.concat(stdoutChunks).toString('utf8') : '(no stdout captured)';
|
|
2389
2482
|
const crashLogPath = path.join(transcriptsDir, `${taskId.replace(/[^a-zA-Z0-9_-]/g, '_')}.crash.log`);
|
|
2390
2483
|
const crashContent = [
|
|
2391
2484
|
`=== CRASH LOG: task ${taskId} ===`,
|
|
@@ -2398,6 +2491,8 @@ class AgentDispatcher {
|
|
|
2398
2491
|
`HELIOS_COMPANY_ID: ${spawnEnv.HELIOS_COMPANY_ID}`,
|
|
2399
2492
|
`--- stderr ---`,
|
|
2400
2493
|
stderr,
|
|
2494
|
+
`--- stdout ---`,
|
|
2495
|
+
stdout,
|
|
2401
2496
|
].join('\n');
|
|
2402
2497
|
try {
|
|
2403
2498
|
fs.writeFileSync(crashLogPath, crashContent, 'utf8');
|
|
@@ -2568,8 +2663,8 @@ class ActivityLogger {
|
|
|
2568
2663
|
for (const event of batch) {
|
|
2569
2664
|
try {
|
|
2570
2665
|
await this.mg(
|
|
2571
|
-
`MERGE (ae:ActivityEvent {id: $id}) SET ae.action = $action, ae.actor = $actor, ae.entityId = $entityId, ae.companyId = $companyId, ae.createdAt = datetime(), ae.meta = $meta`,
|
|
2572
|
-
{ id: event.id, action: event.action, actor: event.actor ?? 'daemon', entityId: event.entityId ?? '', companyId: event.companyId ?? this._companyId, meta: JSON.stringify(event.meta ?? {}) }
|
|
2666
|
+
`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`,
|
|
2667
|
+
{ 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 ?? {}) }
|
|
2573
2668
|
);
|
|
2574
2669
|
successCount++;
|
|
2575
2670
|
} catch (e) {
|
|
@@ -2679,7 +2774,7 @@ class CostEventSyncer {
|
|
|
2679
2774
|
let db;
|
|
2680
2775
|
try {
|
|
2681
2776
|
db = new Database(this._dbPath, { readonly: true });
|
|
2682
|
-
db.pragma('busy_timeout =
|
|
2777
|
+
db.pragma('busy_timeout = 5000'); // 5000ms max wait if DB is locked
|
|
2683
2778
|
} catch (err) {
|
|
2684
2779
|
log('warn', `CostEventSyncer: failed to open SQLite DB at ${this._dbPath}: ${err.message}`);
|
|
2685
2780
|
return [];
|
|
@@ -3153,7 +3248,7 @@ class ApprovalWatcher {
|
|
|
3153
3248
|
approved = await this.mg(
|
|
3154
3249
|
`MATCH (a:Approval {companyId: $cid, status: 'approved'})
|
|
3155
3250
|
WHERE a.followUpTaskCreated IS NULL OR a.followUpTaskCreated = false
|
|
3156
|
-
RETURN a.id, a.title, a.requestedBy, a.type, a.strategyId`,
|
|
3251
|
+
RETURN a.id, a.title, a.requestedBy, a.type, a.strategyId, a.sourceTaskId, a.goalId`,
|
|
3157
3252
|
{ cid: this.companyId }
|
|
3158
3253
|
);
|
|
3159
3254
|
} catch (mgErr) {
|
|
@@ -3162,8 +3257,8 @@ class ApprovalWatcher {
|
|
|
3162
3257
|
const storeApprovals = hboStore.getApprovalsByCompanyStatus(this.companyId, 'approved')
|
|
3163
3258
|
.filter(a => !a.followUpTaskCreated);
|
|
3164
3259
|
approved = {
|
|
3165
|
-
rows: storeApprovals.map(a => [a.id, a.title, a.requestedBy, a.type, a.strategyId ?? null]),
|
|
3166
|
-
keys: ['a.id', 'a.title', 'a.requestedBy', 'a.type', 'a.strategyId'],
|
|
3260
|
+
rows: storeApprovals.map(a => [a.id, a.title, a.requestedBy, a.type, a.strategyId ?? null, a.sourceTaskId ?? null, a.goalId ?? null]),
|
|
3261
|
+
keys: ['a.id', 'a.title', 'a.requestedBy', 'a.type', 'a.strategyId', 'a.sourceTaskId', 'a.goalId'],
|
|
3167
3262
|
};
|
|
3168
3263
|
log('info', `ApprovalWatcher: using SQLite fallback for approval lookup (Memgraph unavailable): ${mgErr.message}`);
|
|
3169
3264
|
}
|
|
@@ -3210,6 +3305,28 @@ class ApprovalWatcher {
|
|
|
3210
3305
|
).catch(e => log('warn', `ApprovalWatcher: strategy approve projection failed: ${e.message}`)));
|
|
3211
3306
|
log('info', `ApprovalWatcher: strategy ${strategyId} approved`);
|
|
3212
3307
|
}
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
// T3-05: harada_strategy_review approved → dispatch L2 research task for this pillar
|
|
3311
|
+
if (approvalType === 'harada_strategy_review') {
|
|
3312
|
+
const pillarId = approval['a.sourceTaskId'];
|
|
3313
|
+
const goalId = approval['a.goalId'];
|
|
3314
|
+
const l2AgentId = approval['a.requestedBy'] || requestedBy;
|
|
3315
|
+
if (pillarId && goalId) {
|
|
3316
|
+
setImmediate(() => {
|
|
3317
|
+
try {
|
|
3318
|
+
const { CascadeResearchDispatcher } = require('./lib/harada/cascade-research-dispatcher');
|
|
3319
|
+
const crd = new CascadeResearchDispatcher(this.mg.bind(this), this.companyId);
|
|
3320
|
+
crd.dispatchL2Research(pillarId, goalId, this.companyId, l2AgentId)
|
|
3321
|
+
.then(() => log('info', `ApprovalWatcher: L2 research dispatched for pillar ${pillarId}`))
|
|
3322
|
+
.catch(e => log('warn', `ApprovalWatcher: L2 dispatch failed for pillar ${pillarId}: ${e.message}`));
|
|
3323
|
+
} catch (dispErr) {
|
|
3324
|
+
log('warn', `ApprovalWatcher: CascadeResearchDispatcher load failed: ${dispErr.message}`);
|
|
3325
|
+
}
|
|
3326
|
+
});
|
|
3327
|
+
} else {
|
|
3328
|
+
log('warn', `ApprovalWatcher: harada_strategy_review approved but missing pillarId (${pillarId}) or goalId (${goalId})`);
|
|
3329
|
+
}
|
|
3213
3330
|
}
|
|
3214
3331
|
|
|
3215
3332
|
// SQLite-first task create (P2-5)
|
|
@@ -3228,11 +3345,13 @@ class ApprovalWatcher {
|
|
|
3228
3345
|
{ taskId, title: `Approval resolved: ${title}. Execute the approved plan.`, agentId: requestedBy, cid: this.companyId, approvalId }
|
|
3229
3346
|
).catch(e => log('warn', `[daemon] Memgraph Task projection failed (non-fatal): ${e.message}`)));
|
|
3230
3347
|
// SQLite-first approval update (P2-5)
|
|
3231
|
-
|
|
3348
|
+
// P8D-01: also write followUpTaskId so "View Task →" link in ApprovalCard works
|
|
3349
|
+
try { hboStore.updateApproval(approvalId, this.companyId, { followUpTaskCreated: true, followUpTaskId: taskId }); } catch (_) {}
|
|
3232
3350
|
// Non-blocking Memgraph projection (fire-and-forget)
|
|
3351
|
+
// P8D-01: SET followUpTaskId so GET /api/approvals returns it for navigation
|
|
3233
3352
|
setImmediate(() => this.mg(
|
|
3234
|
-
`MATCH (a:Approval {id: $approvalId}) SET a.followUpTaskCreated = true`,
|
|
3235
|
-
{ approvalId }
|
|
3353
|
+
`MATCH (a:Approval {id: $approvalId}) SET a.followUpTaskCreated = true, a.followUpTaskId = $taskId`,
|
|
3354
|
+
{ approvalId, taskId }
|
|
3236
3355
|
).catch(e => log('warn', `[daemon] Memgraph Approval projection failed (non-fatal): ${e.message}`)));
|
|
3237
3356
|
log('info', `ApprovalWatcher: created follow-up task ${taskId} for approval ${approvalId}`);
|
|
3238
3357
|
} catch (err) {
|
|
@@ -3294,12 +3413,13 @@ function buildForCompany(companyId, mgQueryAsync, opts) {
|
|
|
3294
3413
|
try { mods.taskCompletionWatchdog = new TaskCompletionWatchdog(mgQueryAsync, cid, _daemonConfig.taskTimeoutMs ?? 1800000); }
|
|
3295
3414
|
catch (e) { log('warn', `[module-factory] TaskCompletionWatchdog init failed for ${cid}: ${e.message}`); mods.taskCompletionWatchdog = null; }
|
|
3296
3415
|
|
|
3297
|
-
try { mods.runCompletionPoller = new RunCompletionPoller(mgQueryAsync, cid, typeof broadcast === 'function' ? (...args) => broadcast(...args) : null, new TaskCompletionProcessor({ mgQuery: mgQueryAsync })); }
|
|
3298
|
-
catch (e) { log('warn', `[module-factory] RunCompletionPoller init failed for ${cid}: ${e.message}`); mods.runCompletionPoller = null; }
|
|
3299
|
-
|
|
3300
3416
|
try { mods.activityLogger = new ActivityLogger(mgQueryAsync, cid); }
|
|
3301
3417
|
catch (e) { log('warn', `[module-factory] ActivityLogger init failed for ${cid}: ${e.message}`); mods.activityLogger = null; }
|
|
3302
3418
|
|
|
3419
|
+
// H-01: activityLogger injected so TaskCompletionProcessor can record task.complete events
|
|
3420
|
+
try { mods.runCompletionPoller = new RunCompletionPoller(mgQueryAsync, cid, typeof broadcast === 'function' ? (...args) => broadcast(...args) : null, new TaskCompletionProcessor({ mgQuery: mgQueryAsync, activityLogger: mods.activityLogger })); }
|
|
3421
|
+
catch (e) { log('warn', `[module-factory] RunCompletionPoller init failed for ${cid}: ${e.message}`); mods.runCompletionPoller = null; }
|
|
3422
|
+
|
|
3303
3423
|
try { mods.approvalWatcher = new ApprovalWatcher(mgQueryAsync, cid); }
|
|
3304
3424
|
catch (e) { log('warn', `[module-factory] ApprovalWatcher init failed for ${cid}: ${e.message}`); mods.approvalWatcher = null; }
|
|
3305
3425
|
|
|
@@ -3358,6 +3478,8 @@ function buildForCompany(companyId, mgQueryAsync, opts) {
|
|
|
3358
3478
|
mods.cascadeReviewTrigger = safeNew('CascadeReviewTrigger', './lib/harada/cascade-review', 'CascadeReviewTrigger', mgQueryAsync, cid);
|
|
3359
3479
|
mods.beliefCeilingAssessment = safeNew('BeliefCeilingAssessment', './lib/harada/belief-ceiling', 'BeliefCeilingAssessment', mgQueryAsync, cid);
|
|
3360
3480
|
mods.sacrificeDeclaration = safeNew('SacrificeDeclaration', './lib/harada/sacrifice-declaration', 'SacrificeDeclaration', mgQueryAsync, cid);
|
|
3481
|
+
// T3-04: CascadeJudge auto-reviews L2/L3 submissions — called every 5 ticks
|
|
3482
|
+
mods.cascadeJudge = safeNew('CascadeJudge', './lib/harada/cascade-judge', 'CascadeJudge', mgQueryAsync, cid);
|
|
3361
3483
|
|
|
3362
3484
|
try {
|
|
3363
3485
|
// MirrorPatternScan runs on a P7D (weekly) wall-clock schedule via WallClockScheduler
|
|
@@ -4100,6 +4222,9 @@ class HeliosCompanyDaemon {
|
|
|
4100
4222
|
if (this._tickCount % 5 === 1) {
|
|
4101
4223
|
await this._runModule('hbo_goal_decompose', () => this._forAllCompanies((cid, m) => m.hboBridge?.tickGoalDecompose()));
|
|
4102
4224
|
await this._runModule('hbo_goal_sync', () => this._forAllCompanies((cid, m) => m.hboBridge?.tickGoalSync()));
|
|
4225
|
+
// T3-04: Auto-review L2/L3 submissions every 5 ticks
|
|
4226
|
+
await this._runModule('CascadeJudgeL2', () => this._forAllCompanies((cid, m) => m.cascadeJudge?.judgeReadyPillars()));
|
|
4227
|
+
await this._runModule('CascadeJudgeL3', () => this._forAllCompanies((cid, m) => m.cascadeJudge?.judgeReadyActionCells()));
|
|
4103
4228
|
await this._runModule('PDSACompletion', () => this._forAllCompanies((cid, m) => m.pdsaCompletion?.process()));
|
|
4104
4229
|
await this._runModule('ProgressPropagation', () => this._forAllCompanies((cid, m) => m.progressPropagation?.propagate()));
|
|
4105
4230
|
await this._runModule('CatchballProcessing', () => this._forAllCompanies((cid, m) => m.catchballProcessing?.process()));
|
|
@@ -5642,6 +5767,7 @@ if (require.main === module) {
|
|
|
5642
5767
|
getAgents: () => daemon._agents || [],
|
|
5643
5768
|
getDaemonHealth: () => ({ pid: process.pid, uptime: process.uptime(), tickCount: daemon._tickCount }),
|
|
5644
5769
|
daemon,
|
|
5770
|
+
activityLogger: daemon._activityLogger ?? null, // H-01: approval event recording in helios-api.js approve/reject handlers
|
|
5645
5771
|
broadcast, // Phase 4/5: required for project SSE events (project:understanding:ready etc.)
|
|
5646
5772
|
companies: _allCompanyConfigs.map(cfg => ({
|
|
5647
5773
|
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 };
|