@cgh567/agent 2.4.1 → 2.4.3

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 (169) hide show
  1. package/bin/helios +0 -0
  2. package/bin/helios-rpc-node-wrapper.cjs +0 -0
  3. package/bin/helios-rpc-wrapper.sh +0 -0
  4. package/daemon/adapters/helios-rpc-adapter.js +47 -25
  5. package/daemon/adapters/tui_wakeup.js +8 -0
  6. package/daemon/config/com.familiar.helios-daemon.plist +5 -0
  7. package/daemon/config/helios-daemon.service +4 -0
  8. package/daemon/context-enrichment.js +59 -21
  9. package/daemon/daemon-manager.js +1 -1
  10. package/daemon/db/email-infrastructure-migrate.js +192 -0
  11. package/daemon/db/hbo-core-migrate.js +189 -0
  12. package/daemon/helios-api.js +723 -57
  13. package/daemon/helios-company-daemon.js +616 -134
  14. package/daemon/lib/harada/cascade-judge.js +12 -50
  15. package/daemon/lib/harada/mandala.js +20 -0
  16. package/daemon/lib/harada/pillar-dispatcher.js +1 -1
  17. package/daemon/lib/harada/project-factory.js +7 -2
  18. package/daemon/lib/hbo-bridge.js +32 -13
  19. package/daemon/lib/hed-engine.js +10 -292
  20. package/daemon/lib/helios-hitl-host.js +15 -2
  21. package/daemon/lib/hitl-interaction-service.js +0 -0
  22. package/daemon/lib/memgraph-verify.js +38 -33
  23. package/daemon/lib/project-drift-detector.js +7 -17
  24. package/daemon/lib/project-semantic-updater.js +1 -14
  25. package/daemon/lib/task-completion-processor.js +11 -0
  26. package/daemon/lib/wizard-engine.js +57 -6
  27. package/daemon/routes/channels.js +10 -5
  28. package/daemon/routes/harada-map.js +11 -48
  29. package/daemon/routes/hbo.js +342 -75
  30. package/daemon/routes/hitl.js +0 -0
  31. package/daemon/routes/project.js +194 -62
  32. package/daemon/routes/routines.js +14 -0
  33. package/daemon/routes/tasks.js +15 -1
  34. package/daemon/routes/wizard.js +11 -4
  35. package/daemon/schema-apply.js +174 -0
  36. package/daemon/schema-definitions.js +423 -0
  37. package/daemon/schema-migrations-hbo.js +10 -0
  38. package/daemon/schema-migrations-hed.js +18 -0
  39. package/daemon/schema-migrations-hitl.js +0 -0
  40. package/daemon/schema-migrations-proj.js +131 -0
  41. package/extensions/001-tool-output-cap.ts +0 -0
  42. package/extensions/context-compaction.ts +45 -26
  43. package/extensions/cortex/activation-bridge.ts +5 -0
  44. package/extensions/cortex/learn.ts +26 -0
  45. package/extensions/cortex/wal-replay.ts +91 -0
  46. package/extensions/email/backfill.ts +0 -0
  47. package/extensions/helios-governance/analysis/ambiguity.ts +0 -0
  48. package/extensions/helios-governance/analysis/compliance.ts +0 -0
  49. package/extensions/helios-governance/analysis/long-task-detector.ts +0 -0
  50. package/extensions/helios-governance/analysis/output-contract.ts +0 -0
  51. package/extensions/helios-governance/analysis/patterns.ts +0 -0
  52. package/extensions/helios-governance/analysis/preflight.ts +0 -0
  53. package/extensions/helios-governance/analysis/recurring-violations.ts +0 -0
  54. package/extensions/helios-governance/analysis/task-classification.ts +0 -0
  55. package/extensions/helios-governance/analysis/task-intent.ts +0 -0
  56. package/extensions/helios-governance/gates/high-impact.ts +1 -1
  57. package/extensions/helios-governance/handlers/_jiti-require.ts +15 -8
  58. package/extensions/helios-governance/handlers/proxy-test-detector.ts +0 -0
  59. package/extensions/hema-dispatch-v3/graph-memory.ts +10 -0
  60. package/extensions/hema-dispatch-v3/index.ts +72 -47
  61. package/extensions/lib/elo-engine.js +0 -0
  62. package/extensions/lib/elo-engine.test.js +0 -0
  63. package/extensions/memgraph-autostart.ts +13 -0
  64. package/extensions/neuroplastic-eval.ts +0 -0
  65. package/extensions/shadow-loop/index.ts +0 -0
  66. package/extensions/warm-tick/warm-tick-maintenance.ts +8 -0
  67. package/lib/__tests__/hbo-core-store.test.js +238 -0
  68. package/lib/brain-v2-budget.js +0 -0
  69. package/lib/brain-v2-circuit-breaker.js +0 -0
  70. package/lib/brain-v2.js +0 -0
  71. package/lib/broker/adaptive-throttle.js +0 -0
  72. package/lib/broker/batch-coalescer.js +0 -0
  73. package/lib/broker/bulkhead.js +0 -0
  74. package/lib/broker/channel-registry.js +0 -0
  75. package/lib/broker/circuit-breaker.js +0 -0
  76. package/lib/broker/evidence-cache.js +0 -0
  77. package/lib/broker/health-monitor.js +0 -0
  78. package/lib/broker/mage-queue.js +0 -0
  79. package/lib/broker/priority-queue.js +0 -0
  80. package/lib/broker/server.js.bak-error2-fix +0 -0
  81. package/lib/broker/session-registry.js +0 -0
  82. package/lib/broker/singleton-timers.js +0 -0
  83. package/lib/broker/types.d.ts +0 -0
  84. package/lib/broker/vegas-limit.js +0 -0
  85. package/lib/compression/dist/ccr-store.js +74 -0
  86. package/lib/compression/dist/content-router.js +115 -0
  87. package/lib/compression/dist/pipeline.js +113 -0
  88. package/lib/compression/dist/server.js +265 -0
  89. package/lib/compression/dist/smart-crusher.js +251 -0
  90. package/lib/context-budget.ts +0 -0
  91. package/lib/context-firewall.js +0 -0
  92. package/lib/crm/integration/triage-bridge.js +0 -0
  93. package/lib/email-utils.ts +0 -0
  94. package/lib/eval/__tests__/preflight-checker.test.ts +0 -0
  95. package/lib/eval/__tests__/task-instruction-parser.test.ts +0 -0
  96. package/lib/eval/__tests__/verifier-runner.test.ts +0 -0
  97. package/lib/eval/index.ts +0 -0
  98. package/lib/eval/preflight-checker.ts +0 -0
  99. package/lib/eval/task-domain-classifier.ts +0 -0
  100. package/lib/eval/task-instruction-parser.ts +0 -0
  101. package/lib/eval/verifier-runner.ts +0 -0
  102. package/lib/event-bus.d.ts +0 -0
  103. package/lib/event-bus.mts +1 -1
  104. package/lib/governance-context-selector.ts +0 -0
  105. package/lib/graph/generate-extension-embeddings.js +0 -0
  106. package/lib/graph/generate-static-embeddings.js +0 -0
  107. package/lib/graph/lib/utils.js +1 -1
  108. package/lib/graph-audit.d.ts +0 -0
  109. package/lib/graph-availability.js +62 -0
  110. package/lib/hbo-core-store.compiled.js +834 -0
  111. package/lib/hbo-core-store.js +124 -0
  112. package/lib/hbo-core-store.ts +908 -0
  113. package/lib/mesh-circuit-breaker.js +0 -0
  114. package/lib/mission-loop/lesson-extractor.ts +0 -0
  115. package/lib/mission-loop/mental-model-scorer.ts +0 -0
  116. package/lib/mission-loop/occ-detector.ts +0 -0
  117. package/lib/mission-loop/query-variants.ts +0 -0
  118. package/lib/mission-loop/verifier-check.ts +0 -0
  119. package/lib/skill-reference-builder.ts +0 -0
  120. package/lib/telemetry/token-breakdown.ts +0 -0
  121. package/lib/tool-compressor.ts +0 -0
  122. package/lib/triage-core/classifier.ts +3 -2
  123. package/lib/triage-core/graph/schema.cypher +10 -0
  124. package/lib/triage-core/legal-routing.ts +0 -0
  125. package/lib/triage-core/mental-model/dunbar-classifier.ts +0 -0
  126. package/lib/triage-core/mental-model/enrich-all.ts +0 -0
  127. package/lib/triage-core/mental-model/identity-resolver.ts +0 -0
  128. package/lib/triage-core/mental-model/key-facts.ts +1 -2
  129. package/lib/triage-core/mental-model/model-assembler.ts +0 -0
  130. package/lib/triage-core/orchestrator.ts +4 -11
  131. package/lib/triage-core/orchestrator.ts.bak-r005-r006-r008 +0 -0
  132. package/package.json +18 -8
  133. package/skills/helios-business-operator/services/signals/upwork-signals.js +0 -0
  134. package/skills/talisman-ceo/SKILL.md +23 -25
  135. package/skills/talisman-comms/SKILL.md +5 -5
  136. package/skills/talisman-engineering/SKILL.md +5 -5
  137. package/skills/talisman-finance/SKILL.md +10 -8
  138. package/skills/talisman-marketing/SKILL.md +10 -10
  139. package/skills/talisman-sales/SKILL.md +12 -15
  140. package/skills/talisman-support/SKILL.md +5 -5
  141. package/agents/business/talisman-ceo.md +0 -183
  142. package/agents/business/talisman-comms.md +0 -257
  143. package/agents/business/talisman-cto.md +0 -153
  144. package/agents/business/talisman-finance.md +0 -246
  145. package/agents/business/talisman-marketing.md +0 -240
  146. package/agents/business/talisman-sales.md +0 -242
  147. package/agents/business/talisman-support.md +0 -236
  148. package/daemon/lib/approval-expiry.js +0 -162
  149. package/daemon/lib/blast-radius-analyzer.js +0 -75
  150. package/daemon/lib/domain-bootstrap-orchestrator.js +0 -267
  151. package/daemon/lib/forensic-log.js +0 -113
  152. package/daemon/lib/goal-research-pipeline.js +0 -644
  153. package/daemon/lib/harada/cascade-research-dispatcher.js +0 -261
  154. package/daemon/lib/headroom-middleware.js +0 -167
  155. package/daemon/lib/headroom-proxy-manager.js +0 -623
  156. package/daemon/lib/mental-model-cache.js +0 -96
  157. package/daemon/lib/project-factory.js +0 -47
  158. package/daemon/lib/session-log-reader.js +0 -93
  159. package/daemon/routes/hed.js +0 -133
  160. package/lib/graph/learning/headroom-learn-bridge.js +0 -215
  161. package/skills/helios-bookkeeping/SKILL.md +0 -321
  162. package/skills/helios-briefer/SKILL.md +0 -44
  163. package/skills/helios-client-relations/SKILL.md +0 -322
  164. package/skills/helios-personal-triager/SKILL.md +0 -45
  165. package/skills/helios-recruitment/SKILL.md +0 -317
  166. package/skills/helios-relationship-nudger/SKILL.md +0 -77
  167. package/skills/helios-researcher/SKILL.md +0 -44
  168. package/skills/helios-scheduler/SKILL.md +0 -58
  169. package/skills/helios-tax-analyst/SKILL.md +0 -280
@@ -107,35 +107,63 @@ function startSseKeepAlive(res) {
107
107
 
108
108
  // ── Route factory ─────────────────────────────────────────────────────────────
109
109
 
110
- module.exports = function createProjectRoute({ mgQuery, broadcast, invalidateContextCache, semanticUpdater, driftDetector } = {}) {
110
+ module.exports = function createProjectRoute({ mgQuery, broadcast, invalidateContextCache, semanticUpdater, driftDetector, hboStore } = {}) {
111
111
 
112
112
  // ── POST /api/project ─────────────────────────────────────────────────────
113
113
  async function handleCreate(req, res, ctx) {
114
114
  const body = await readBody(req);
115
115
  if (!assertBody(body, res, req)) return;
116
116
  // D3: ctx.cid is authoritative - prevents cross-company project injection
117
- const { pillarId, goalId, name } = body;
117
+ // P3-02: pillarId and goalId are now optional (defaults to null) so desktop
118
+ // users without Harada pillars can still create projects.
119
+ const { name } = body;
120
+ const pillarId = body.pillarId ? String(body.pillarId) : null;
121
+ const goalId = body.goalId ? String(body.goalId) : null;
118
122
  const companyId = ctx?.cid || body?.companyId || '';
119
- if (!companyId || !pillarId || !goalId || !name) {
120
- return jsonErr(res, 400, 'companyId, pillarId, goalId, name required', req);
123
+ if (!companyId || !name) {
124
+ return jsonErr(res, 400, 'companyId and name are required', req);
121
125
  }
122
126
  try {
123
- const slug = pillarId.replace(/[^a-z0-9]/gi, '-').toLowerCase().slice(0, 40);
124
- const projId = `proj:${companyId}:${slug}`;
127
+ // projId is deterministic: use caller-supplied id (H-3: preserves desktop ID for reconciliation),
128
+ // then pillarId-slug, then UUID slug (P3-02: pillarId optional)
129
+ const slug = pillarId
130
+ ? pillarId.replace(/[^a-z0-9]/gi, '-').toLowerCase().slice(0, 40)
131
+ : randomUUID().slice(0, 8);
132
+ const projId = body.id ? String(body.id) : `proj:${companyId}:${slug}`;
125
133
  const docId = `pdoc:${projId}:main`;
126
- await mgQuery(
127
- `MERGE (p:HeliosProject {id: $projId})
128
- ON CREATE SET p.companyId=$cid, p.goalId=$goalId, p.pillarId=$pillarId,
129
- p.name=$name, p.status='planning', p.phase='planning', p.createdAt=datetime()`,
130
- { projId, cid: companyId, goalId, pillarId, name }
131
- );
132
- await mgQuery(
133
- `MERGE (d:ProjectDocument {id: $docId})
134
- ON CREATE SET d.projectId=$projId, d.purpose='', d.approach='',
135
- d.successCriteria='[]', d.intentAnchor='', d.exclusions='[]',
136
- d.content='', d.version=toInteger(1), d.updatedAt=datetime()`,
137
- { docId, projId }
138
- );
134
+ if (mgQuery) {
135
+ await mgQuery(
136
+ `MERGE (p:HeliosProject {id: $projId})
137
+ ON CREATE SET p.companyId=$cid, p.goalId=$goalId, p.pillarId=$pillarId,
138
+ p.name=$name, p.status='planning', p.phase='planning', p.createdAt=datetime()`,
139
+ { projId, cid: companyId, goalId, pillarId, name }
140
+ );
141
+ // H-1 fix: SQLite write-through happens HERE (after first Memgraph write succeeds)
142
+ // so it executes even if the second Memgraph write (ProjectDocument) fails
143
+ if (hboStore?.upsertProject) {
144
+ try {
145
+ hboStore.upsertProject({ id: projId, companyId, name, pillarId, goalId, status: 'planning', phase: 'planning', createdAt: new Date().toISOString(), updated_at: Date.now() });
146
+ } catch (storeErr) {
147
+ process.stderr.write(`[project] hboStore.upsertProject failed (non-fatal): ${storeErr.message}\n`);
148
+ }
149
+ }
150
+ await mgQuery(
151
+ `MERGE (d:ProjectDocument {id: $docId})
152
+ ON CREATE SET d.projectId=$projId, d.purpose='', d.approach='',
153
+ d.successCriteria='[]', d.intentAnchor='', d.exclusions='[]',
154
+ d.content='', d.version=toInteger(1), d.updatedAt=datetime()`,
155
+ { docId, projId }
156
+ );
157
+ } else {
158
+ // Memgraph unavailable — write to SQLite only (C-2 / SQL-parity invariant)
159
+ if (hboStore?.upsertProject) {
160
+ try {
161
+ hboStore.upsertProject({ id: projId, companyId, name, pillarId, goalId, status: 'planning', phase: 'planning', createdAt: new Date().toISOString(), updated_at: Date.now() });
162
+ } catch (storeErr) {
163
+ process.stderr.write(`[project] hboStore.upsertProject (offline) failed (non-fatal): ${storeErr.message}\n`);
164
+ }
165
+ }
166
+ }
139
167
  jsonOk(res, { ok: true, projectId: projId }, req, 201);
140
168
  } catch (e) {
141
169
  jsonErr(res, 500, safeMsg(e), req);
@@ -148,41 +176,84 @@ module.exports = function createProjectRoute({ mgQuery, broadcast, invalidateCon
148
176
  const cid = url.searchParams.get('companyId') || ctx?.cid || '';
149
177
  const pillar = url.searchParams.get('pillarId') || '';
150
178
  const status = url.searchParams.get('status') || '';
179
+ // P3-SQL: try Memgraph first; fall back to SQLite when unavailable
180
+ if (mgQuery) {
181
+ try {
182
+ let cypher = `MATCH (p:HeliosProject)`;
183
+ const params = {};
184
+ const where = [];
185
+ if (cid) { where.push('p.companyId = $cid'); params.cid = cid; }
186
+ if (pillar) { where.push('p.pillarId = $pillar'); params.pillar = pillar; }
187
+ if (status) { where.push('p.status = $status'); params.status = status; }
188
+ if (where.length) cypher += ` WHERE ${where.join(' AND ')}`;
189
+ cypher += ` RETURN p.id, p.name, p.phase, p.status, p.pillarId, p.goalId, p.createdAt ORDER BY p.createdAt DESC LIMIT 100`;
190
+ const result = await mgQuery(cypher, params);
191
+ const keys = ['id','name','phase','status','pillarId','goalId','createdAt'];
192
+ const projects = parseRows(result).map(r => rowToObj(r, keys));
193
+ return jsonOk(res, { projects, count: projects.length }, req);
194
+ } catch (e) {
195
+ process.stderr.write(`[project] handleList Memgraph failed, falling back to SQLite: ${e.message}\n`);
196
+ }
197
+ }
198
+ // SQLite fallback
151
199
  try {
152
- let cypher = `MATCH (p:HeliosProject)`;
153
- const params = {};
154
- const where = [];
155
- if (cid) { where.push('p.companyId = $cid'); params.cid = cid; }
156
- if (pillar) { where.push('p.pillarId = $pillar'); params.pillar = pillar; }
157
- if (status) { where.push('p.status = $status'); params.status = status; }
158
- if (where.length) cypher += ` WHERE ${where.join(' AND ')}`;
159
- cypher += ` RETURN p.id, p.name, p.phase, p.status, p.pillarId, p.goalId, p.createdAt ORDER BY p.createdAt DESC LIMIT 100`;
160
- const result = await mgQuery(cypher, params);
161
- const keys = ['id','name','phase','status','pillarId','goalId','createdAt'];
162
- const projects = parseRows(result).map(r => rowToObj(r, keys));
163
- jsonOk(res, { projects, count: projects.length }, req);
164
- } catch (e) {
165
- jsonErr(res, 500, safeMsg(e), req);
200
+ const all = hboStore?.getProjectsByCompany ? hboStore.getProjectsByCompany(cid) : [];
201
+ let projects = all || [];
202
+ if (pillar) projects = projects.filter(p => p.pillarId === pillar);
203
+ if (status) projects = projects.filter(p => p.status === status);
204
+ return jsonOk(res, { projects, count: projects.length, _source: 'sqlite' }, req);
205
+ } catch (storeErr) {
206
+ return jsonErr(res, 503, `Projects unavailable: Memgraph not connected and SQLite fallback failed: ${storeErr.message}`, req);
166
207
  }
167
208
  }
168
209
 
169
210
  // ── GET /api/project/:id ──────────────────────────────────────────────────
170
211
  async function handleGet(req, res, ctx, projectId) {
212
+ // P3-SQL: Memgraph-first, SQLite fallback
213
+ if (mgQuery) {
214
+ try {
215
+ const projRes = await mgQuery(
216
+ `MATCH (p:HeliosProject {id: $id, companyId: $cid})
217
+ OPTIONAL MATCH (d:ProjectDocument {projectId: p.id})
218
+ RETURN p.id, p.name, p.phase, p.status, p.pillarId, p.goalId, p.createdAt,
219
+ d.purpose, d.approach, d.successCriteria, d.intentAnchor, d.exclusions, d.version, d.updatedAt, d.content`,
220
+ { id: projectId, cid: ctx.cid || ctx?.cid || '' }
221
+ );
222
+ const row = parseRows(projRes)[0];
223
+ if (!row) return jsonErr(res, 404, 'Project not found', req);
224
+ const keys = ['id','name','phase','status','pillarId','goalId','createdAt','purpose','approach','successCriteria','intentAnchor','exclusions','version','updatedAt','content'];
225
+ return jsonOk(res, rowToObj(row, keys), req);
226
+ } catch (e) {
227
+ process.stderr.write(`[project] handleGet Memgraph failed, falling back to SQLite: ${e.message}\n`);
228
+ }
229
+ }
230
+ // SQLite fallback
171
231
  try {
172
- // D1: Add companyId to prevent cross-company project exfiltration
173
- const projRes = await mgQuery(
174
- `MATCH (p:HeliosProject {id: $id, companyId: $cid})
175
- OPTIONAL MATCH (d:ProjectDocument {projectId: p.id})
176
- RETURN p.id, p.name, p.phase, p.status, p.pillarId, p.goalId, p.createdAt,
177
- d.purpose, d.approach, d.successCriteria, d.intentAnchor, d.exclusions, d.version, d.updatedAt, d.content`,
178
- { id: projectId, cid: ctx.cid || ctx?.cid || '' }
179
- );
180
- const row = parseRows(projRes)[0];
181
- if (!row) return jsonErr(res, 404, 'Project not found', req);
182
- const keys = ['id','name','phase','status','pillarId','goalId','createdAt','purpose','approach','successCriteria','intentAnchor','exclusions','version','updatedAt','content'];
183
- jsonOk(res, rowToObj(row, keys), req);
184
- } catch (e) {
185
- jsonErr(res, 500, safeMsg(e), req);
232
+ const p = hboStore?.getProject ? hboStore.getProject(projectId, ctx?.cid || '') : null;
233
+ if (!p) return jsonErr(res, 404, 'Project not found', req);
234
+ // M-2 fix: normalize response shape to match Memgraph path exactly
235
+ const doc = hboStore?.getProjectDocument ? hboStore.getProjectDocument(projectId) : null;
236
+ const createdAtMs = typeof p.created_at === 'number' ? p.created_at : (p.createdAt ? new Date(p.createdAt).getTime() : null);
237
+ return jsonOk(res, {
238
+ id: p.id,
239
+ name: p.name ?? null,
240
+ phase: p.phase ?? 'planning',
241
+ status: p.status ?? 'planning',
242
+ pillarId: p.pillarId ?? p.pillar_id ?? null,
243
+ goalId: p.goalId ?? p.goal_id ?? null,
244
+ createdAt: createdAtMs ? new Date(createdAtMs).toISOString() : null,
245
+ purpose: doc?.purpose ?? null,
246
+ approach: doc?.approach ?? null,
247
+ successCriteria: doc?.success_criteria ?? doc?.successCriteria ?? '[]',
248
+ intentAnchor: doc?.intent_anchor ?? doc?.intentAnchor ?? null,
249
+ exclusions: doc?.exclusions ?? '[]',
250
+ version: doc?.version ?? 1,
251
+ updatedAt: doc?.updated_at ? new Date(doc.updated_at).toISOString() : null,
252
+ content: doc?.content ?? null,
253
+ _source: 'sqlite',
254
+ }, req);
255
+ } catch (storeErr) {
256
+ return jsonErr(res, 503, `Project unavailable: ${storeErr.message}`, req);
186
257
  }
187
258
  }
188
259
 
@@ -202,6 +273,26 @@ module.exports = function createProjectRoute({ mgQuery, broadcast, invalidateCon
202
273
  const docId = `pdoc:${projectId}:main`;
203
274
  const companyId = bodyCompanyId || ctx?.cid || projectId.split(':')[1] || 'unknown';
204
275
 
276
+ // C-2 fix: SQLite-only path when Memgraph unavailable
277
+ if (!mgQuery) {
278
+ if (hboStore?.upsertProjectDocument) {
279
+ try {
280
+ const existing = hboStore.getProjectDocument ? hboStore.getProjectDocument(projectId) : null;
281
+ const update: Record<string, any> = {
282
+ id: docId, project_id: projectId, company_id: companyId,
283
+ ...(existing ? { purpose: existing.purpose, approach: existing.approach, intent_anchor: existing.intent_anchor, success_criteria: existing.success_criteria, exclusions: existing.exclusions, content: existing.content, version: existing.version ?? 1 } : {}),
284
+ [section === 'intentAnchor' ? 'intent_anchor' : section === 'successCriteria' ? 'success_criteria' : section]: String(value),
285
+ version: ((existing?.version ?? 0) + 1),
286
+ updated_at: Date.now(),
287
+ };
288
+ hboStore.upsertProjectDocument(update);
289
+ } catch (storeErr) {
290
+ process.stderr.write(`[project] hboStore.upsertProjectDocument (offline) failed: ${storeErr.message}\n`);
291
+ }
292
+ }
293
+ return jsonOk(res, { ok: true, section, _source: 'sqlite' }, req, 200);
294
+ }
295
+
205
296
  // ── content section: persist block tree + back-extract semantic fields ──
206
297
  // When the editor saves the full BlockNote block array, we:
207
298
  // 1. Store it as d.content (JSON string) — source of truth for the editor
@@ -260,6 +351,12 @@ module.exports = function createProjectRoute({ mgQuery, broadcast, invalidateCon
260
351
  { docId, content: String(value), purpose, approach, intentAnchor }
261
352
  );
262
353
  if (invalidateContextCache) invalidateContextCache(projectId);
354
+ // C-2: SQLite write-through for content section
355
+ if (hboStore?.upsertProjectDocument) {
356
+ try {
357
+ hboStore.upsertProjectDocument({ id: docId, project_id: projectId, company_id: companyId, content: String(value), purpose, approach, intent_anchor: intentAnchor, updated_at: Date.now() });
358
+ } catch (storeErr) { process.stderr.write(`[project] content write-through failed: ${storeErr.message}\n`); }
359
+ }
263
360
  return jsonOk(res, { ok: true, section: 'content' }, req, 200);
264
361
  }
265
362
 
@@ -296,6 +393,21 @@ module.exports = function createProjectRoute({ mgQuery, broadcast, invalidateCon
296
393
  // proposalId: internal LLM analysis task ID
297
394
  jsonOk(res, { ok: true, pduId, proposalId, message: 'Analyzing edit — check back shortly' }, req, 202);
298
395
 
396
+ // C-2: SQLite write-through for proposal/semantic edit sections
397
+ if (hboStore?.upsertProjectDocument) {
398
+ try {
399
+ const existing = hboStore.getProjectDocument ? hboStore.getProjectDocument(projectId) : null;
400
+ const colKey = section === 'intentAnchor' ? 'intent_anchor' : section === 'successCriteria' ? 'success_criteria' : section;
401
+ hboStore.upsertProjectDocument({
402
+ id: docId, project_id: projectId, company_id: companyId,
403
+ ...(existing ? { purpose: existing.purpose, approach: existing.approach, intent_anchor: existing.intent_anchor, success_criteria: existing.success_criteria, exclusions: existing.exclusions, content: existing.content, version: existing.version ?? 1 } : {}),
404
+ [colKey]: String(value),
405
+ version: ((existing?.version ?? 0) + 1),
406
+ updated_at: Date.now(),
407
+ });
408
+ } catch (storeErr) { process.stderr.write(`[project] proposal write-through failed: ${storeErr.message}\n`); }
409
+ }
410
+
299
411
  // Trigger semantic analysis asynchronously (non-blocking)
300
412
  if (semanticUpdater) {
301
413
  setImmediate(async () => {
@@ -480,21 +592,34 @@ module.exports = function createProjectRoute({ mgQuery, broadcast, invalidateCon
480
592
 
481
593
  // ── GET /api/project/:id/state ────────────────────────────────────────────
482
594
  async function handleGetState(req, res, ctx, projectId) {
595
+ // C-1 fix: guard against Memgraph unavailability — return zero-state from SQLite
596
+ if (!mgQuery) {
597
+ try {
598
+ const p = hboStore?.getProject ? hboStore.getProject(projectId, ctx?.cid || '') : null;
599
+ const currentPhase = p?.phase ?? p?.status ?? 'planning';
600
+ return jsonOk(res, { totalCells: 0, closedCells: 0, activeTasks: 0, openDriftSignals: 0, currentPhase, hillProgress: 0, _source: 'sqlite' }, req);
601
+ } catch (storeErr) {
602
+ return jsonOk(res, { totalCells: 0, closedCells: 0, activeTasks: 0, openDriftSignals: 0, currentPhase: 'planning', hillProgress: 0, _source: 'sqlite' }, req);
603
+ }
604
+ }
483
605
  try {
484
606
  const [cellRes, taskRes, driftRes, docRes] = await Promise.all([
485
607
  mgQuery(
608
+ // P3-05: OPTIONAL MATCH so projects without GoalPillars return hillProgress:0 not an error
609
+ // H-6 fix: WHERE gp IS NOT NULL prevents null pillarId matching unrelated ActionCells
486
610
  `MATCH (p:HeliosProject {id: $id})
487
- MATCH (gp:GoalPillar {id: p.pillarId})
488
- MATCH (cell:ActionCell {pillarId: gp.id}) RETURN count(cell) AS total,
611
+ OPTIONAL MATCH (gp:GoalPillar {id: p.pillarId})
612
+ OPTIONAL MATCH (cell:ActionCell) WHERE cell.pillarId = gp.id AND gp IS NOT NULL
613
+ RETURN count(cell) AS total,
489
614
  count(CASE WHEN cell.status='closed' THEN 1 END) AS closed`,
490
- { id: projectId, cid: ctx.cid || ctx?.cid || '' }
615
+ { id: projectId }
491
616
  ).catch(() => null),
492
617
  mgQuery(
493
618
  `MATCH (p:HeliosProject {id: $id})
494
619
  MATCH (t:Task {companyId: p.companyId}) WHERE t.task_metadata CONTAINS p.pillarId
495
620
  AND t.status IN ['todo','in_progress']
496
621
  RETURN count(t) AS active`,
497
- { id: projectId, cid: ctx.cid || ctx?.cid || '' }
622
+ { id: projectId }
498
623
  ).catch(() => null),
499
624
  mgQuery(
500
625
  `MATCH (sig:AnomalySignal) WHERE sig.id STARTS WITH $prefix AND sig.status='open'
@@ -503,7 +628,7 @@ module.exports = function createProjectRoute({ mgQuery, broadcast, invalidateCon
503
628
  ).catch(() => null),
504
629
  mgQuery(
505
630
  `MATCH (p:HeliosProject {id: $id}) RETURN p.phase`,
506
- { id: projectId, cid: ctx.cid || ctx?.cid || '' }
631
+ { id: projectId }
507
632
  ).catch(() => null),
508
633
  ]);
509
634
 
@@ -512,16 +637,22 @@ module.exports = function createProjectRoute({ mgQuery, broadcast, invalidateCon
512
637
  const driftRow = parseRows(driftRes)[0];
513
638
  const docRow = parseRows(docRes)[0];
514
639
 
515
- const totalCells = Number(cellRow?.[0] ?? cellRow?.['total'] ?? 0);
516
- const closedCells = Number(cellRow?.[1] ?? cellRow?.['closed'] ?? 0);
517
- const activeTasks = Number(taskRow?.[0] ?? taskRow?.['active'] ?? 0);
518
- const openDriftSignals = Number(driftRow?.[0] ?? driftRow?.['open'] ?? 0);
519
- const currentPhase = docRow?.[0] ?? docRow?.['phase'] ?? 'planning';
520
- const hillProgress = totalCells > 0 ? Math.round((closedCells / totalCells) * 100) : 0;
640
+ const totalCells = Number(cellRow?.[0] ?? cellRow?.['total'] ?? 0);
641
+ const closedCells = Number(cellRow?.[1] ?? cellRow?.['closed'] ?? 0);
642
+ const activeTasks = Number(taskRow?.[0] ?? taskRow?.['active'] ?? 0);
643
+ const openDriftSignals = Number(driftRow?.[0] ?? driftRow?.['open'] ?? 0);
644
+ const currentPhase = docRow?.[0] ?? docRow?.['phase'] ?? 'planning';
645
+ const hillProgress = totalCells > 0 ? Math.round((closedCells / totalCells) * 100) : 0;
521
646
 
522
647
  jsonOk(res, { totalCells, closedCells, activeTasks, openDriftSignals, currentPhase, hillProgress }, req);
523
648
  } catch (e) {
524
- jsonErr(res, 500, safeMsg(e), req);
649
+ // Memgraph query failed mid-flight — degrade to SQLite zero-state
650
+ try {
651
+ const p = hboStore?.getProject ? hboStore.getProject(projectId, ctx?.cid || '') : null;
652
+ jsonOk(res, { totalCells: 0, closedCells: 0, activeTasks: 0, openDriftSignals: 0, currentPhase: p?.phase ?? 'planning', hillProgress: 0, _source: 'sqlite' }, req);
653
+ } catch (_) {
654
+ jsonErr(res, 500, safeMsg(e), req);
655
+ }
525
656
  }
526
657
  }
527
658
 
@@ -551,13 +682,14 @@ module.exports = function createProjectRoute({ mgQuery, broadcast, invalidateCon
551
682
  const { answer, answeredBy } = body;
552
683
  if (!answer) return jsonErr(res, 400, 'answer required', req);
553
684
  try {
554
- await mgQuery(
685
+ const mgResult = await mgQuery(
555
686
  `MATCH (q:ProjectQuestion {id: $qid, projectId: $projId, companyId: $cid})
556
- SET q.answer=$answer, q.answeredBy=$by, q.answeredAt=datetime(), q.status='answered'`,
687
+ SET q.answer=$answer, q.answeredBy=$by, q.answeredAt=datetime(), q.status='answered'
688
+ RETURN count(q) AS updated`,
557
689
  { qid, projId: projectId, cid: ctx?.cid || "", answer: String(answer), by: answeredBy || null }
558
690
  );
559
691
  // D2: if no row matched, task not found for this company
560
- const _updated = taskResult?.rows?.[0]?.[0] ?? taskResult?.rows?.[0]?.updated ?? 0;
692
+ const _updated = mgResult?.rows?.[0]?.[0] ?? mgResult?.rows?.[0]?.updated ?? 0;
561
693
  if (!Number(_updated)) return jsonErr(res, 404, "Task not found for this company", req);
562
694
  jsonOk(res, { ok: true }, req);
563
695
  } catch (e) {
@@ -14,6 +14,8 @@
14
14
 
15
15
  module.exports = function createRoutinesRouter(handlers) {
16
16
  const {
17
+ handleGetRoutines,
18
+ handleCreateRoutine,
17
19
  handleGetRoutineTriggers,
18
20
  handleCreateRoutineTrigger,
19
21
  handleFireWebhookTrigger,
@@ -30,6 +32,18 @@ module.exports = function createRoutinesRouter(handlers) {
30
32
  return true;
31
33
  }
32
34
 
35
+ // P5-01: GET /api/routines — list routines for a company (must be before /:id routes)
36
+ if (method === 'GET' && pathname === '/api/routines') {
37
+ await handleGetRoutines(req, res, ctx);
38
+ return true;
39
+ }
40
+
41
+ // P5-02: POST /api/routines — create a new routine
42
+ if (method === 'POST' && pathname === '/api/routines') {
43
+ await handleCreateRoutine(req, res, ctx);
44
+ return true;
45
+ }
46
+
33
47
  // PATCH /api/routines/:id (edit routine → creates revision)
34
48
  const patchRoutineMatch = pathname.match(/^\/api\/routines\/([^/]+)$/);
35
49
  if (method === 'PATCH' && patchRoutineMatch) {
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const VALID_TASK_STATUSES = new Set(['todo', 'in_progress', 'done', 'failed', 'cancelled']);
3
+ // NOTE: Task status validation is handled in handleUpdateTask (helios-api.js VALID_STATUSES).
4
+ // The canonical valid statuses include: todo, in_progress, done, failed, cancelled, blocked, escalated, in_review.
4
5
 
5
6
  /**
6
7
  * routes/tasks.js — Task-related route dispatch
@@ -26,6 +27,7 @@ module.exports = function createTasksRouter(handlers) {
26
27
  handleGetTaskComments,
27
28
  handlePostTaskComment,
28
29
  handlePatchTask,
30
+ handlePatchTaskPolicy, // P4-07: PATCH /api/tasks/:id/policy
29
31
  } = handlers;
30
32
 
31
33
  return async function tasksRoute(req, res, ctx, pathname, method) {
@@ -89,6 +91,18 @@ module.exports = function createTasksRouter(handlers) {
89
91
  return true;
90
92
  }
91
93
 
94
+ // PATCH /api/tasks/:id/policy — P4-07: set executionPolicy on a task
95
+ const policyMatch = pathname.match(/^\/api\/tasks\/([^/]+)\/policy$/);
96
+ if (method === 'PATCH' && policyMatch) {
97
+ if (handlePatchTaskPolicy) {
98
+ await handlePatchTaskPolicy(req, res, ctx, decodeURIComponent(policyMatch[1]));
99
+ } else {
100
+ res.writeHead(501, { 'Content-Type': 'application/json' });
101
+ res.end(JSON.stringify({ error: 'Policy endpoint not configured' }));
102
+ }
103
+ return true;
104
+ }
105
+
92
106
  return false;
93
107
  };
94
108
  };
@@ -184,9 +184,16 @@ async function handlePostWizard(req, res, ctx) {
184
184
  }
185
185
  }
186
186
 
187
- // BUG-21 fix: fire tickGoalSync immediately after wizard completes so the
188
- // first AgentReadySignal dispatch happens within the current tick (~30s)
189
- // instead of waiting up to 2.5 min (5 ticks × 30s) for the next goal sync cycle.
187
+ // tickGoalDecompose (creates GoalPillar × 8) and wizard:pillars_ready broadcast are
188
+ // now handled in helios-company-daemon.js registerCompany(), which runs after the
189
+ // desktop calls POST /api/daemon/register-company. That is the correct point because:
190
+ // (a) _modulesByCompany is guaranteed to have this company's entry
191
+ // (b) daemon._broadcast is wired before setBroadcast() is called
192
+ // (c) No race with the setImmediate firing before registerCompany completes
193
+ //
194
+ // tickGoalSync bridges any BusinessTask→Task nodes created by the wizard (CEO task).
195
+ // It still runs here because it needs to fire as soon as possible after wizard:done,
196
+ // not after the desktop round-trip to /api/daemon/register-company.
190
197
  if (result.companyId && ctx.daemon?._modulesByCompany) {
191
198
  setImmediate(async () => {
192
199
  try {
@@ -194,7 +201,7 @@ async function handlePostWizard(req, res, ctx) {
194
201
  if (mods?.hboBridge?.tickGoalSync) {
195
202
  await mods.hboBridge.tickGoalSync();
196
203
  }
197
- } catch (_) { /* non-fatal — tickGoalSync will fire on next cycle */ }
204
+ } catch (_) { /* non-fatal */ }
198
205
  });
199
206
  }
200
207
 
@@ -0,0 +1,174 @@
1
+ 'use strict';
2
+ /**
3
+ * schema-apply.js — Converges the Memgraph graph to the canonical schema.
4
+ *
5
+ * Uses Memgraph MAGE's schema.assert() procedure to atomically create missing
6
+ * indexes and uniqueness constraints, keep existing correct ones, and optionally
7
+ * drop anything that doesn't belong (controlled by dropExisting flag).
8
+ *
9
+ * Composite indexes (multi-property) are applied via separate Cypher statements
10
+ * because schema.assert() only handles single-property label indexes.
11
+ *
12
+ * Called once during daemon startup, after Memgraph connection is established
13
+ * and before any tick processing begins.
14
+ *
15
+ * Usage:
16
+ * const { applySchema } = require('./schema-apply');
17
+ * await applySchema(mgQuery); // safe for rolling deployments
18
+ * await applySchema(mgQuery, true); // drop_existing — use on fresh installs
19
+ */
20
+
21
+ const { INDEXES, COMPOSITE_INDEXES, UNIQUE_CONSTRAINTS } = require('./schema-definitions');
22
+
23
+ /**
24
+ * @param {Function} mgQuery async (cypher, params?) => { rows, keys }
25
+ * @param {boolean} [dropExisting=false]
26
+ * false = safe for live deployments — adds missing, keeps existing, never drops.
27
+ * true = converge exactly — drops indexes/constraints not in the definition.
28
+ * Use on fresh installs or after intentional schema cleanup.
29
+ */
30
+ async function applySchema(mgQuery, dropExisting = false) {
31
+ const log = (msg) => console.log('[schema-apply]', msg);
32
+ const warn = (msg) => console.warn('[schema-apply] WARN:', msg);
33
+
34
+ // ── Step 1: schema.assert() for single-property indexes + unique constraints
35
+ log(`Applying schema via schema.assert() (drop_existing=${dropExisting})…`);
36
+
37
+ let assertResult;
38
+ try {
39
+ assertResult = await mgQuery(
40
+ `CALL schema.assert($indices, $unique, {}, $drop)
41
+ YIELD action, label, key, unique
42
+ RETURN action, label, key, unique`,
43
+ {
44
+ indices: INDEXES,
45
+ unique: UNIQUE_CONSTRAINTS,
46
+ drop: dropExisting,
47
+ }
48
+ );
49
+ } catch (err) {
50
+ // schema.assert() is a MAGE procedure — if MAGE is not installed or the
51
+ // version doesn't include the schema module, fall back gracefully to the
52
+ // per-statement legacy approach.
53
+ warn(`schema.assert() unavailable: ${err.message}`);
54
+ warn('Falling back to per-statement legacy migration.');
55
+ return _legacyFallback(mgQuery);
56
+ }
57
+
58
+ // ── Parse and log results ────────────────────────────────────────────────
59
+ const rows = assertResult?.rows ?? [];
60
+ const counts = { Created: 0, Kept: 0, Dropped: 0 };
61
+
62
+ for (const row of rows) {
63
+ // Memgraph returns rows as arrays or objects depending on driver config
64
+ const action = Array.isArray(row) ? row[0] : row.action;
65
+ const label = Array.isArray(row) ? row[1] : row.label;
66
+ const key = Array.isArray(row) ? row[2] : row.key;
67
+ const unique = Array.isArray(row) ? row[3] : row.unique;
68
+
69
+ const kind = unique ? 'constraint' : 'index';
70
+ if (action === 'Created') {
71
+ log(` ✓ Created ${kind}: :${label}(${key})`);
72
+ } else if (action === 'Dropped') {
73
+ log(` ✗ Dropped ${kind}: :${label}(${key})`);
74
+ }
75
+ // Kept entries are silent — too noisy otherwise
76
+ if (counts[action] !== undefined) counts[action]++;
77
+ }
78
+
79
+ log(`schema.assert() complete — Created: ${counts.Created}, Kept: ${counts.Kept}, Dropped: ${counts.Dropped}`);
80
+
81
+ // ── Step 2: Composite indexes ─────────────────────────────────────────────
82
+ // schema.assert() does not support composite indexes, so we create them
83
+ // individually. Each uses CREATE INDEX ON :Label(propA, propB) with a
84
+ // try/catch to silently skip "already exists" errors.
85
+ let compositeCreated = 0;
86
+ let compositeSkipped = 0;
87
+
88
+ for (const { label, props } of COMPOSITE_INDEXES) {
89
+ const propList = props.join(', ');
90
+ const cypher = `CREATE INDEX ON :${label}(${propList})`;
91
+ try {
92
+ await mgQuery(cypher);
93
+ log(` ✓ Created composite index: :${label}(${propList})`);
94
+ compositeCreated++;
95
+ } catch (err) {
96
+ // "already exists" is the expected no-op case
97
+ const msg = err.message ?? '';
98
+ if (/already exists/i.test(msg) || /constraint.*exists/i.test(msg) || /index.*exists/i.test(msg)) {
99
+ compositeSkipped++;
100
+ } else {
101
+ warn(`Composite index :${label}(${propList}) failed: ${msg}`);
102
+ }
103
+ }
104
+ }
105
+
106
+ if (compositeCreated > 0 || compositeSkipped > 0) {
107
+ log(`Composite indexes — Created: ${compositeCreated}, Already existed: ${compositeSkipped}`);
108
+ }
109
+
110
+ log('Schema application complete.');
111
+ }
112
+
113
+ // ── Legacy fallback ───────────────────────────────────────────────────────────
114
+ // Used when schema.assert() is unavailable (MAGE not installed or older version).
115
+ // Runs each constraint/index as individual CREATE statements with try/catch.
116
+ // This is the old seven-migration-file behaviour, condensed into one function.
117
+
118
+ async function _legacyFallback(mgQuery) {
119
+ const log = (msg) => console.log('[schema-apply/legacy]', msg);
120
+ const warn = (msg) => console.warn('[schema-apply/legacy] WARN:', msg);
121
+
122
+ log('Running legacy per-statement schema migration…');
123
+
124
+ let created = 0;
125
+ let skipped = 0;
126
+ let failed = 0;
127
+
128
+ async function exec(cypher) {
129
+ try {
130
+ await mgQuery(cypher);
131
+ created++;
132
+ } catch (err) {
133
+ const msg = err.message ?? '';
134
+ if (/already exists/i.test(msg) || /constraint.*exists/i.test(msg) || /index.*exists/i.test(msg)) {
135
+ skipped++;
136
+ } else {
137
+ warn(`${cypher} → ${msg}`);
138
+ failed++;
139
+ }
140
+ }
141
+ }
142
+
143
+ // Unique constraints
144
+ for (const [label, constraintGroups] of Object.entries(UNIQUE_CONSTRAINTS)) {
145
+ for (const props of constraintGroups) {
146
+ if (props.length === 1) {
147
+ await exec(`CREATE CONSTRAINT ON (n:${label}) ASSERT n.${props[0]} IS UNIQUE`);
148
+ } else {
149
+ // Composite unique — old syntax doesn't support this cleanly; use new syntax
150
+ await exec(`CREATE CONSTRAINT FOR (n:${label}) REQUIRE (${props.map(p => `n.${p}`).join(', ')}) IS UNIQUE`);
151
+ }
152
+ }
153
+ }
154
+
155
+ // Single-property indexes
156
+ for (const [label, props] of Object.entries(INDEXES)) {
157
+ for (const prop of props) {
158
+ if (prop === '') {
159
+ await exec(`CREATE INDEX ON :${label}`);
160
+ } else {
161
+ await exec(`CREATE INDEX ON :${label}(${prop})`);
162
+ }
163
+ }
164
+ }
165
+
166
+ // Composite indexes
167
+ for (const { label, props } of COMPOSITE_INDEXES) {
168
+ await exec(`CREATE INDEX ON :${label}(${props.join(', ')})`);
169
+ }
170
+
171
+ log(`Legacy migration complete — Created: ${created}, Skipped: ${skipped}, Failed: ${failed}`);
172
+ }
173
+
174
+ module.exports = { applySchema };