@cgh567/agent 2.4.3 → 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.
Files changed (140) hide show
  1. package/agents/business/talisman-ceo.md +183 -0
  2. package/agents/business/talisman-comms.md +257 -0
  3. package/agents/business/talisman-cto.md +153 -0
  4. package/agents/business/talisman-finance.md +246 -0
  5. package/agents/business/talisman-marketing.md +240 -0
  6. package/agents/business/talisman-sales.md +242 -0
  7. package/agents/business/talisman-support.md +236 -0
  8. package/bin/helios-rpc.js +19 -0
  9. package/daemon/adapters/helios-rpc-adapter.js +5 -12
  10. package/daemon/context-enrichment.js +27 -0
  11. package/daemon/helios-api.js +290 -45
  12. package/daemon/helios-company-daemon.js +160 -50
  13. package/daemon/lib/blast-radius-analyzer.js +75 -0
  14. package/daemon/lib/domain-bootstrap-orchestrator.js +267 -0
  15. package/daemon/lib/forensic-log.js +113 -0
  16. package/daemon/lib/goal-research-pipeline.js +644 -0
  17. package/daemon/lib/harada/cascade-judge.js +84 -1
  18. package/daemon/lib/harada/cascade-research-dispatcher.js +282 -0
  19. package/daemon/lib/harada/pillar-dispatcher.js +23 -2
  20. package/daemon/lib/hbo-bridge.js +73 -5
  21. package/daemon/lib/headroom-middleware.js +129 -0
  22. package/daemon/lib/headroom-proxy-manager.js +309 -0
  23. package/daemon/lib/intelligence/department-page-generator.js +46 -1
  24. package/daemon/lib/interpretation-engine.js +92 -0
  25. package/daemon/lib/mental-model-cache.js +96 -0
  26. package/daemon/lib/project-factory.js +47 -0
  27. package/daemon/lib/session-log-reader.js +93 -0
  28. package/daemon/lib/standard-work-bootstrap.js +87 -1
  29. package/daemon/lib/task-completion-processor.js +12 -0
  30. package/daemon/package.json +2 -1
  31. package/daemon/routes/agents.js +51 -6
  32. package/daemon/routes/channels.js +116 -2
  33. package/daemon/routes/crm.js +85 -0
  34. package/daemon/routes/dashboard.js +62 -16
  35. package/daemon/routes/dept.js +10 -1
  36. package/daemon/routes/email-triage.js +19 -10
  37. package/daemon/routes/hbo.js +367 -13
  38. package/daemon/routes/hed.js +133 -0
  39. package/daemon/routes/inbox.js +397 -8
  40. package/daemon/routes/project.js +392 -9
  41. package/daemon/schema-definitions.js +10 -0
  42. package/daemon/schema-migrations-hbo.js +10 -0
  43. package/daemon/schema-migrations-proj.js +22 -0
  44. package/extensions/__tests__/codebase-index.test.ts +73 -0
  45. package/extensions/__tests__/extension-command-registration.test.ts +35 -0
  46. package/extensions/__tests__/git-push-guard.test.ts +68 -0
  47. package/extensions/context-compaction.ts +104 -76
  48. package/extensions/cortex/__tests__/cortex-core.test.ts +100 -0
  49. package/extensions/email/actions/draft-response.ts +21 -1
  50. package/extensions/email/auth/accounts.ts +5 -11
  51. package/extensions/email/auth/inbox-dog.ts +5 -2
  52. package/extensions/email/backfill.ts +20 -13
  53. package/extensions/email/providers/gmail.ts +164 -0
  54. package/extensions/email/providers/google-calendar.ts +34 -5
  55. package/extensions/helios-browser/__tests__/browser-routing.test.ts +57 -0
  56. package/extensions/helios-browser/backends/playwright.ts +3 -1
  57. package/extensions/helios-governance/__tests__/governance-gates.test.ts +40 -0
  58. package/extensions/helios-governance/__tests__/tournament-consumer.test.js +66 -0
  59. package/extensions/hema-dispatch-v3/headroom-compress.ts +103 -0
  60. package/extensions/hema-dispatch-v3/index.ts +33 -65
  61. package/extensions/interview/__tests__/server.test.ts +117 -0
  62. package/extensions/lib/helios-root.cjs +46 -0
  63. package/extensions/subagent-mesh/__tests__/handlers.test.ts +98 -0
  64. package/extensions/warm-tick/warm-tick-maintenance.ts +156 -0
  65. package/lib/__tests__/bulk-ingest.live.test.ts +66 -0
  66. package/lib/__tests__/crash-fixes.test.ts +49 -0
  67. package/lib/__tests__/maintenance-mission-wiring.test.ts +35 -0
  68. package/lib/broker/__tests__/jit-subscription.test.js +44 -1
  69. package/lib/broker/__tests__/lifecycle-channels.test.js +25 -1
  70. package/lib/compression/__tests__/ccr-store.test.js +138 -0
  71. package/lib/compression/__tests__/pipeline.test.js +280 -0
  72. package/lib/compression/__tests__/smart-crusher.test.js +242 -0
  73. package/lib/compression/dist/server.js +34 -1
  74. package/lib/compression/dist/start-server.js +77 -0
  75. package/lib/graph/learning/headroom-learn-bridge.js +175 -0
  76. package/lib/hbo-core-store.ts +71 -0
  77. package/lib/mission-loop/__tests__/research-handler.test.ts +143 -0
  78. package/lib/skill-sync.js +6 -1
  79. package/lib/startup-integrity.js +9 -2
  80. package/lib/triage-core/__tests__/classifier-fixture.test.ts +254 -0
  81. package/lib/triage-core/__tests__/classifier-post-norm.test.ts +1 -1
  82. package/lib/triage-core/__tests__/classifier.test.ts +45 -7
  83. package/lib/triage-core/__tests__/correction-detector.test.ts +36 -0
  84. package/lib/triage-core/__tests__/d6-dunbar-boost.test.ts +5 -5
  85. package/lib/triage-core/__tests__/orchestrator-pipeline.test.ts +107 -0
  86. package/lib/triage-core/__tests__/orchestrator.test.ts +113 -1
  87. package/lib/triage-core/__tests__/signals.test.ts +357 -0
  88. package/lib/triage-core/__tests__/sql-parity.test.ts +216 -0
  89. package/lib/triage-core/backfill-cost-estimator.ts +91 -0
  90. package/lib/triage-core/backfill-orchestrator.ts +119 -0
  91. package/lib/triage-core/classifier.ts +38 -6
  92. package/lib/triage-core/cos/cross-channel-escalation.ts +2 -3
  93. package/lib/triage-core/cos/response-debt.ts +2 -2
  94. package/lib/triage-core/graph/__tests__/batch-persistence.test.ts +283 -0
  95. package/lib/triage-core/graph/batch-persistence.ts +66 -2
  96. package/lib/triage-core/graph/betweenness-worker.js +75 -0
  97. package/lib/triage-core/graph/graph-rank-sql.ts +67 -0
  98. package/lib/triage-core/graph/persistence.ts +1 -1
  99. package/lib/triage-core/graph/schema-v2.ts +2 -0
  100. package/lib/triage-core/graph/schema.cypher +1 -0
  101. package/lib/triage-core/graph/triage-query.ts +1 -1
  102. package/lib/triage-core/learning.ts +15 -20
  103. package/lib/triage-core/mental-model/bedrock-config.ts +78 -0
  104. package/lib/triage-core/mental-model/cos-integration.ts +1 -1
  105. package/lib/triage-core/mental-model/entity-extractor.ts +51 -4
  106. package/lib/triage-core/mental-model/identity-resolver.ts +5 -5
  107. package/lib/triage-core/mental-model/model-assembler-sql.ts +200 -0
  108. package/lib/triage-core/mental-model/model-assembler.ts +16 -3
  109. package/lib/triage-core/orchestrator.ts +4 -4
  110. package/lib/triage-core/scheduled-sends.ts +39 -2
  111. package/lib/triage-core/signals/comms-style.ts +1 -1
  112. package/lib/triage-core/signals/cross-channel-escalation.ts +2 -2
  113. package/lib/triage-core/signals/favee-type.ts +6 -1
  114. package/lib/triage-core/signals/goal-relevance.ts +31 -2
  115. package/lib/triage-core/signals/personal-importance.ts +1 -1
  116. package/lib/triage-core/signals/referral-chain.ts +0 -1
  117. package/lib/triage-core/signals/relationship-decay.ts +4 -0
  118. package/lib/triage-core/signals/relationship-health.ts +6 -1
  119. package/lib/triage-core/signals/trajectory-signal.ts +38 -3
  120. package/lib/triage-core/tournament-runner.js +11 -1
  121. package/lib/triage-core/triage-llm-factory.ts +110 -0
  122. package/lib/triage-core/triage-local-llm.ts +145 -0
  123. package/lib/triage-core/triage-sql-store.ts +337 -0
  124. package/lib/triage-core/types.ts +2 -2
  125. package/lib/unified-graph.atomic.test.ts +52 -0
  126. package/lib/unified-graph.failure-categories.test.ts +55 -0
  127. package/package.json +10 -3
  128. package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
  129. package/prebuilds/linux-x64/better_sqlite3.node +0 -0
  130. package/prebuilds/win32-x64/better_sqlite3.node +0 -0
  131. package/skills/helios-bookkeeping/SKILL.md +321 -0
  132. package/skills/helios-briefer/SKILL.md +44 -0
  133. package/skills/helios-client-relations/SKILL.md +322 -0
  134. package/skills/helios-personal-triager/SKILL.md +45 -0
  135. package/skills/helios-recruitment/SKILL.md +317 -0
  136. package/skills/helios-relationship-nudger/SKILL.md +77 -0
  137. package/skills/helios-researcher/SKILL.md +44 -0
  138. package/skills/helios-scheduler/SKILL.md +58 -0
  139. package/skills/helios-tax-analyst/SKILL.md +280 -0
  140. 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('debug', `Coalescing routine ${routine['r.name']} — queued follow-up`);
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
- // This fires BEFORE the normal single-task creation to batch missed runs first.
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
- const lastRunMs = lastRunStr ? Date.parse(lastRunStr) : 0;
1140
- while (checkDate && missedCount < catchUpCap) {
1141
- if (checkDate.getTime() <= lastRunMs) break;
1142
- missedCount++;
1143
- checkDate = cron.previousRun ? cron.previousRun() : null;
1144
- }
1145
- cron.stop();
1146
- for (let i = 0; i < missedCount; i++) {
1147
- const catchUpTaskId = `task:routine:${routineId}:catchup:${i}:${randomUUID()}`;
1148
- const catchUpRunId = requireRunId(`run:${routineId}:catchup:${i}:${randomUUID()}`, 'RoutineEvaluator.catchup');
1149
- await this.mg(
1150
- `MERGE (t:Task {id: $taskId}) SET t.title = $title, t.status = 'todo', t.assigneeAgentId = $agentId, t.companyId = $companyId, t.originKind = 'routine_catchup', t.progressPropagated = false, t.createdAt = datetime()`,
1151
- { taskId: catchUpTaskId, title: `Routine catch-up: ${routine['r.name']}`, agentId: routineAgentId, companyId: this.companyId }
1152
- ).catch(e => log('warn', `RoutineEvaluator: catch-up task create failed: ${e.message}`));
1153
- await this.mg(
1154
- `MERGE (rr:RoutineRun {id: $runId}) SET rr.routineId = $routineId, rr.status = 'queued', rr.linkedTaskId = $taskId, rr.companyId = $companyId, rr.queuedAt = datetime()`,
1155
- { runId: catchUpRunId, routineId, taskId: catchUpTaskId, companyId: this.companyId }
1156
- ).catch(e => log('warn', `RoutineEvaluator: catch-up run create failed: ${e.message}`));
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: null,
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 pi binary: prefer helios-agent local node_modules/.bin/pi to avoid
2319
- // ENOENT in non-login shells where NVM is not sourced (crash vector confirmed).
2320
- // On Windows, use pi.cmd wrapper from node_modules/.bin/.
2321
- // Fallback chain: local node_modules ~/.local/bin/pi (Unix only) 'pi' (last resort).
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 localPiBin = path.join(__dirname, '..', 'node_modules', '.bin', _piBinName);
2324
- const piCmd = fs.existsSync(localPiBin) ? localPiBin
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,6 +2420,7 @@ 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.
@@ -2352,11 +2430,12 @@ class AgentDispatcher {
2352
2430
 
2353
2431
  log('info', `Dispatching task ${taskId} to agent ${agentId} with skill ${skill}`, { taskId, agentId, skill });
2354
2432
 
2355
- const child = spawnFn(piCmd, piArgs, {
2433
+ const child = spawnFn(spawnExe, spawnArgs, {
2356
2434
  detached: true,
2357
2435
  stdio: ['ignore', 'ignore', 'pipe'],
2358
2436
  env: spawnEnv,
2359
2437
  windowsHide: true,
2438
+ shell: false,
2360
2439
  });
2361
2440
 
2362
2441
  // Collect stderr chunks for crash log (64KB cap to prevent OOM from chatty workers)
@@ -2568,8 +2647,8 @@ class ActivityLogger {
2568
2647
  for (const event of batch) {
2569
2648
  try {
2570
2649
  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 ?? {}) }
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 ?? {}) }
2573
2652
  );
2574
2653
  successCount++;
2575
2654
  } catch (e) {
@@ -2679,7 +2758,7 @@ class CostEventSyncer {
2679
2758
  let db;
2680
2759
  try {
2681
2760
  db = new Database(this._dbPath, { readonly: true });
2682
- db.pragma('busy_timeout = 100'); // 100ms max wait if DB is locked
2761
+ db.pragma('busy_timeout = 5000'); // 5000ms max wait if DB is locked
2683
2762
  } catch (err) {
2684
2763
  log('warn', `CostEventSyncer: failed to open SQLite DB at ${this._dbPath}: ${err.message}`);
2685
2764
  return [];
@@ -3153,7 +3232,7 @@ class ApprovalWatcher {
3153
3232
  approved = await this.mg(
3154
3233
  `MATCH (a:Approval {companyId: $cid, status: 'approved'})
3155
3234
  WHERE a.followUpTaskCreated IS NULL OR a.followUpTaskCreated = false
3156
- 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`,
3157
3236
  { cid: this.companyId }
3158
3237
  );
3159
3238
  } catch (mgErr) {
@@ -3162,8 +3241,8 @@ class ApprovalWatcher {
3162
3241
  const storeApprovals = hboStore.getApprovalsByCompanyStatus(this.companyId, 'approved')
3163
3242
  .filter(a => !a.followUpTaskCreated);
3164
3243
  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'],
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'],
3167
3246
  };
3168
3247
  log('info', `ApprovalWatcher: using SQLite fallback for approval lookup (Memgraph unavailable): ${mgErr.message}`);
3169
3248
  }
@@ -3210,6 +3289,28 @@ class ApprovalWatcher {
3210
3289
  ).catch(e => log('warn', `ApprovalWatcher: strategy approve projection failed: ${e.message}`)));
3211
3290
  log('info', `ApprovalWatcher: strategy ${strategyId} approved`);
3212
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
+ }
3213
3314
  }
3214
3315
 
3215
3316
  // SQLite-first task create (P2-5)
@@ -3228,11 +3329,13 @@ class ApprovalWatcher {
3228
3329
  { taskId, title: `Approval resolved: ${title}. Execute the approved plan.`, agentId: requestedBy, cid: this.companyId, approvalId }
3229
3330
  ).catch(e => log('warn', `[daemon] Memgraph Task projection failed (non-fatal): ${e.message}`)));
3230
3331
  // SQLite-first approval update (P2-5)
3231
- try { hboStore.updateApproval(approvalId, this.companyId, { followUpTaskCreated: true }); } catch (_) {}
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 (_) {}
3232
3334
  // Non-blocking Memgraph projection (fire-and-forget)
3335
+ // P8D-01: SET followUpTaskId so GET /api/approvals returns it for navigation
3233
3336
  setImmediate(() => this.mg(
3234
- `MATCH (a:Approval {id: $approvalId}) SET a.followUpTaskCreated = true`,
3235
- { approvalId }
3337
+ `MATCH (a:Approval {id: $approvalId}) SET a.followUpTaskCreated = true, a.followUpTaskId = $taskId`,
3338
+ { approvalId, taskId }
3236
3339
  ).catch(e => log('warn', `[daemon] Memgraph Approval projection failed (non-fatal): ${e.message}`)));
3237
3340
  log('info', `ApprovalWatcher: created follow-up task ${taskId} for approval ${approvalId}`);
3238
3341
  } catch (err) {
@@ -3294,12 +3397,13 @@ function buildForCompany(companyId, mgQueryAsync, opts) {
3294
3397
  try { mods.taskCompletionWatchdog = new TaskCompletionWatchdog(mgQueryAsync, cid, _daemonConfig.taskTimeoutMs ?? 1800000); }
3295
3398
  catch (e) { log('warn', `[module-factory] TaskCompletionWatchdog init failed for ${cid}: ${e.message}`); mods.taskCompletionWatchdog = null; }
3296
3399
 
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
3400
  try { mods.activityLogger = new ActivityLogger(mgQueryAsync, cid); }
3301
3401
  catch (e) { log('warn', `[module-factory] ActivityLogger init failed for ${cid}: ${e.message}`); mods.activityLogger = null; }
3302
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
+
3303
3407
  try { mods.approvalWatcher = new ApprovalWatcher(mgQueryAsync, cid); }
3304
3408
  catch (e) { log('warn', `[module-factory] ApprovalWatcher init failed for ${cid}: ${e.message}`); mods.approvalWatcher = null; }
3305
3409
 
@@ -3358,6 +3462,8 @@ function buildForCompany(companyId, mgQueryAsync, opts) {
3358
3462
  mods.cascadeReviewTrigger = safeNew('CascadeReviewTrigger', './lib/harada/cascade-review', 'CascadeReviewTrigger', mgQueryAsync, cid);
3359
3463
  mods.beliefCeilingAssessment = safeNew('BeliefCeilingAssessment', './lib/harada/belief-ceiling', 'BeliefCeilingAssessment', mgQueryAsync, cid);
3360
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);
3361
3467
 
3362
3468
  try {
3363
3469
  // MirrorPatternScan runs on a P7D (weekly) wall-clock schedule via WallClockScheduler
@@ -4100,6 +4206,9 @@ class HeliosCompanyDaemon {
4100
4206
  if (this._tickCount % 5 === 1) {
4101
4207
  await this._runModule('hbo_goal_decompose', () => this._forAllCompanies((cid, m) => m.hboBridge?.tickGoalDecompose()));
4102
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()));
4103
4212
  await this._runModule('PDSACompletion', () => this._forAllCompanies((cid, m) => m.pdsaCompletion?.process()));
4104
4213
  await this._runModule('ProgressPropagation', () => this._forAllCompanies((cid, m) => m.progressPropagation?.propagate()));
4105
4214
  await this._runModule('CatchballProcessing', () => this._forAllCompanies((cid, m) => m.catchballProcessing?.process()));
@@ -5642,6 +5751,7 @@ if (require.main === module) {
5642
5751
  getAgents: () => daemon._agents || [],
5643
5752
  getDaemonHealth: () => ({ pid: process.pid, uptime: process.uptime(), tickCount: daemon._tickCount }),
5644
5753
  daemon,
5754
+ activityLogger: daemon._activityLogger ?? null, // H-01: approval event recording in helios-api.js approve/reject handlers
5645
5755
  broadcast, // Phase 4/5: required for project SSE events (project:understanding:ready etc.)
5646
5756
  companies: _allCompanyConfigs.map(cfg => ({
5647
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 };