@unerr-ai/unerr 0.2.1 → 0.2.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 (363) hide show
  1. package/README.md +36 -45
  2. package/dist/cli.js +37443 -36022
  3. package/package.json +2 -1
  4. package/dist/behaviors/agent-llm-bridge.js +0 -166
  5. package/dist/behaviors/architecture-guard.js +0 -256
  6. package/dist/behaviors/auto-doc.js +0 -247
  7. package/dist/behaviors/cascade-guard.js +0 -289
  8. package/dist/behaviors/change-narrative.js +0 -270
  9. package/dist/behaviors/convention-drift.js +0 -290
  10. package/dist/behaviors/framework.js +0 -235
  11. package/dist/behaviors/guard-formatter.js +0 -44
  12. package/dist/behaviors/incomplete-work.js +0 -270
  13. package/dist/behaviors/loop-breaker.js +0 -300
  14. package/dist/behaviors/session-continuity.js +0 -208
  15. package/dist/commands/branches.js +0 -97
  16. package/dist/commands/check-commit.js +0 -225
  17. package/dist/commands/compress-output.js +0 -64
  18. package/dist/commands/config-verify.js +0 -243
  19. package/dist/commands/daemon.js +0 -905
  20. package/dist/commands/dashboard.js +0 -52
  21. package/dist/commands/debug.js +0 -200
  22. package/dist/commands/enrich.js +0 -184
  23. package/dist/commands/exec.js +0 -233
  24. package/dist/commands/gain.js +0 -156
  25. package/dist/commands/hook.js +0 -88
  26. package/dist/commands/index.js +0 -88
  27. package/dist/commands/init.js +0 -74
  28. package/dist/commands/install.js +0 -505
  29. package/dist/commands/learn.js +0 -116
  30. package/dist/commands/manifest.js +0 -193
  31. package/dist/commands/rewind.js +0 -103
  32. package/dist/commands/serve.js +0 -19
  33. package/dist/commands/setup-wizard.js +0 -414
  34. package/dist/commands/skills.js +0 -64
  35. package/dist/commands/stats.js +0 -20
  36. package/dist/commands/status.js +0 -654
  37. package/dist/commands/timeline.js +0 -139
  38. package/dist/commands/uninstall.js +0 -230
  39. package/dist/components/App.js +0 -109
  40. package/dist/components/Banner.js +0 -12
  41. package/dist/components/ConfirmPrompt.js +0 -25
  42. package/dist/components/DriftSummary.js +0 -23
  43. package/dist/components/GradeBadge.js +0 -15
  44. package/dist/components/HealthCard.js +0 -18
  45. package/dist/components/InkSpinner.js +0 -22
  46. package/dist/components/InputBox.js +0 -17
  47. package/dist/components/KeyValue.js +0 -13
  48. package/dist/components/MessageList.js +0 -14
  49. package/dist/components/ProgressBar.js +0 -26
  50. package/dist/components/Section.js +0 -16
  51. package/dist/components/SessionSummaryCard.js +0 -73
  52. package/dist/components/StartupDisplay.js +0 -24
  53. package/dist/components/StatusDashboard.js +0 -57
  54. package/dist/components/StatusLine.js +0 -8
  55. package/dist/components/StepLine.js +0 -22
  56. package/dist/components/Theme.js +0 -20
  57. package/dist/components/ToolProgress.js +0 -8
  58. package/dist/components/ViolationList.js +0 -21
  59. package/dist/components/render.js +0 -13
  60. package/dist/config/agent-registry.js +0 -237
  61. package/dist/config/claude-settings-hooks.js +0 -304
  62. package/dist/config/hook-installer.js +0 -65
  63. package/dist/config/instruction-writer.js +0 -388
  64. package/dist/config/mcp-config-writer.js +0 -266
  65. package/dist/config/settings.js +0 -174
  66. package/dist/config/tool-detector.js +0 -42
  67. package/dist/config/value-surfacing.js +0 -119
  68. package/dist/core/context-assembly.js +0 -108
  69. package/dist/core/conversation.js +0 -33
  70. package/dist/core/local-chat-provider.js +0 -475
  71. package/dist/core/provider-factory.js +0 -55
  72. package/dist/core/providers.js +0 -90
  73. package/dist/core/query-engine.js +0 -174
  74. package/dist/daemon/api.js +0 -312
  75. package/dist/daemon/autostart.js +0 -119
  76. package/dist/daemon/bootstrap.js +0 -39
  77. package/dist/daemon/client.js +0 -164
  78. package/dist/daemon/detect-ci.js +0 -81
  79. package/dist/daemon/platform-linux.js +0 -146
  80. package/dist/daemon/platform-macos.js +0 -134
  81. package/dist/daemon/platform-windows.js +0 -116
  82. package/dist/daemon/process-manager.js +0 -299
  83. package/dist/daemon/protocol.js +0 -23
  84. package/dist/daemon/registry.js +0 -270
  85. package/dist/daemon/settings-schema.js +0 -72
  86. package/dist/daemon/system-health.js +0 -134
  87. package/dist/daemon/version-checker.js +0 -262
  88. package/dist/daemon/warm-start.js +0 -223
  89. package/dist/entrypoints/cli.js +0 -1043
  90. package/dist/entrypoints/daemon.js +0 -380
  91. package/dist/entrypoints/repl.js +0 -147
  92. package/dist/hooks/adapters/claude-code.js +0 -90
  93. package/dist/hooks/adapters/cline.js +0 -100
  94. package/dist/hooks/adapters/cursor.js +0 -98
  95. package/dist/hooks/hook-dedup.js +0 -79
  96. package/dist/hooks/hook-runner.js +0 -113
  97. package/dist/hooks/navigation-hooks.js +0 -175
  98. package/dist/hooks/prompt-hooks.js +0 -63
  99. package/dist/hooks/shell-hooks.js +0 -47
  100. package/dist/ignore.js +0 -111
  101. package/dist/intelligence/approach-suggester.js +0 -61
  102. package/dist/intelligence/ast-extractor.js +0 -2615
  103. package/dist/intelligence/ast-worker.js +0 -34
  104. package/dist/intelligence/background-indexer.js +0 -121
  105. package/dist/intelligence/blast-radius.js +0 -200
  106. package/dist/intelligence/community-detection.js +0 -691
  107. package/dist/intelligence/community-detector.js +0 -184
  108. package/dist/intelligence/computation-scheduler.js +0 -75
  109. package/dist/intelligence/confidence-propagation.js +0 -47
  110. package/dist/intelligence/convention-detector.js +0 -242
  111. package/dist/intelligence/convention-learner.js +0 -205
  112. package/dist/intelligence/convention-matcher.js +0 -205
  113. package/dist/intelligence/cozo-schema.js +0 -376
  114. package/dist/intelligence/decision-point-detector.js +0 -90
  115. package/dist/intelligence/deep-dive-tools.js +0 -586
  116. package/dist/intelligence/durability-scorer.js +0 -84
  117. package/dist/intelligence/exploration-cost.js +0 -204
  118. package/dist/intelligence/exploration-pattern-tracker.js +0 -61
  119. package/dist/intelligence/fact-generator.js +0 -322
  120. package/dist/intelligence/facts-schema.js +0 -90
  121. package/dist/intelligence/file-intelligence.js +0 -59
  122. package/dist/intelligence/graph-holder.js +0 -220
  123. package/dist/intelligence/graph-temporal-joiner.js +0 -238
  124. package/dist/intelligence/health-grade.js +0 -423
  125. package/dist/intelligence/health-grader.js +0 -200
  126. package/dist/intelligence/health-map-data.js +0 -259
  127. package/dist/intelligence/import-symbols.js +0 -136
  128. package/dist/intelligence/incremental-indexer.js +0 -658
  129. package/dist/intelligence/indexer/centrality.js +0 -62
  130. package/dist/intelligence/indexer/cfg-context.js +0 -95
  131. package/dist/intelligence/indexer/confidence.js +0 -34
  132. package/dist/intelligence/indexer/cross-file-resolver.js +0 -104
  133. package/dist/intelligence/indexer/edge-repair.js +0 -89
  134. package/dist/intelligence/indexer/entity-key.js +0 -17
  135. package/dist/intelligence/indexer/export-map.js +0 -132
  136. package/dist/intelligence/indexer/git-cochange.js +0 -128
  137. package/dist/intelligence/indexer/graph-patch.js +0 -147
  138. package/dist/intelligence/indexer/incremental.js +0 -78
  139. package/dist/intelligence/indexer/ingest.js +0 -160
  140. package/dist/intelligence/indexer/language-detect.js +0 -226
  141. package/dist/intelligence/indexer/metadata.js +0 -63
  142. package/dist/intelligence/indexer/mutation-tracker.js +0 -79
  143. package/dist/intelligence/indexer/orchestrator.js +0 -155
  144. package/dist/intelligence/indexer/plugin-interface.js +0 -31
  145. package/dist/intelligence/indexer/plugins/csharp.js +0 -440
  146. package/dist/intelligence/indexer/plugins/go.js +0 -335
  147. package/dist/intelligence/indexer/plugins/java.js +0 -370
  148. package/dist/intelligence/indexer/plugins/python.js +0 -358
  149. package/dist/intelligence/indexer/plugins/regex-fallback.js +0 -82
  150. package/dist/intelligence/indexer/plugins/ruby.js +0 -290
  151. package/dist/intelligence/indexer/plugins/rust.js +0 -484
  152. package/dist/intelligence/indexer/plugins/tier2-generic.js +0 -310
  153. package/dist/intelligence/indexer/plugins/typescript.js +0 -456
  154. package/dist/intelligence/indexer/resource-monitor.js +0 -93
  155. package/dist/intelligence/indexer/scip/decoder.js +0 -253
  156. package/dist/intelligence/indexer/scip/detector.js +0 -232
  157. package/dist/intelligence/indexer/scip/downloader.js +0 -427
  158. package/dist/intelligence/indexer/scip/fallback.js +0 -34
  159. package/dist/intelligence/indexer/scip/merger.js +0 -109
  160. package/dist/intelligence/indexer/scip/orchestrator.js +0 -433
  161. package/dist/intelligence/indexer/scip/runner.js +0 -98
  162. package/dist/intelligence/indexer/snapshot.js +0 -66
  163. package/dist/intelligence/indexer/test-detector.js +0 -196
  164. package/dist/intelligence/indexer/watch-integration.js +0 -61
  165. package/dist/intelligence/indexer/worker.js +0 -85
  166. package/dist/intelligence/local-convention-detector.js +0 -437
  167. package/dist/intelligence/local-embeddings.js +0 -190
  168. package/dist/intelligence/local-graph.js +0 -1946
  169. package/dist/intelligence/local-indexer.js +0 -1575
  170. package/dist/intelligence/local-llm.js +0 -163
  171. package/dist/intelligence/local-rule-generator.js +0 -154
  172. package/dist/intelligence/local-snapshot.js +0 -213
  173. package/dist/intelligence/negative-knowledge.js +0 -103
  174. package/dist/intelligence/persistent-db.js +0 -85
  175. package/dist/intelligence/query-router.js +0 -2556
  176. package/dist/intelligence/risk-classifier.js +0 -116
  177. package/dist/intelligence/rule-evaluator.js +0 -380
  178. package/dist/intelligence/rule-generator.js +0 -49
  179. package/dist/intelligence/search-index.js +0 -173
  180. package/dist/intelligence/semantic/docstring-extractor.js +0 -67
  181. package/dist/intelligence/semantic/embedding-store.js +0 -52
  182. package/dist/intelligence/semantic/enrichment-orchestrator.js +0 -48
  183. package/dist/intelligence/semantic/git-message-miner.js +0 -114
  184. package/dist/intelligence/semantic/identifier-tokenizer.js +0 -51
  185. package/dist/intelligence/semantic/node2vec-embeddings.js +0 -71
  186. package/dist/intelligence/semantic/node2vec-walks.js +0 -103
  187. package/dist/intelligence/semantic/path-domain-inference.js +0 -112
  188. package/dist/intelligence/semantic/similarity-engine.js +0 -60
  189. package/dist/intelligence/semantic/tfidf-vectors.js +0 -88
  190. package/dist/intelligence/session-brief-builder.js +0 -159
  191. package/dist/intelligence/session-context.js +0 -221
  192. package/dist/intelligence/session-health-monitor.js +0 -211
  193. package/dist/intelligence/session-narrative.js +0 -197
  194. package/dist/intelligence/session-pattern-analyzer.js +0 -218
  195. package/dist/intelligence/signal-scorer.js +0 -390
  196. package/dist/intelligence/signal-show-store.js +0 -182
  197. package/dist/intelligence/smart-truncate.js +0 -158
  198. package/dist/intelligence/subgraph-cache.js +0 -88
  199. package/dist/intelligence/temporal-facts.js +0 -494
  200. package/dist/intelligence/token-estimator.js +0 -100
  201. package/dist/intelligence/tool-injector.js +0 -87
  202. package/dist/intelligence/tree-sitter-loader.js +0 -71
  203. package/dist/intelligence/worker-pool.js +0 -116
  204. package/dist/proxy/arg-validator.js +0 -79
  205. package/dist/proxy/auto-bootstrap.js +0 -167
  206. package/dist/proxy/bridge.js +0 -147
  207. package/dist/proxy/budget-enforcer.js +0 -70
  208. package/dist/proxy/compression-quality-monitor.js +0 -160
  209. package/dist/proxy/compression-stats.js +0 -51
  210. package/dist/proxy/context-rot-detector.js +0 -137
  211. package/dist/proxy/drift-detector.js +0 -139
  212. package/dist/proxy/efficiency-tracker.js +0 -79
  213. package/dist/proxy/fact-ranking.js +0 -154
  214. package/dist/proxy/format-encoder.js +0 -266
  215. package/dist/proxy/http-transport.js +0 -90
  216. package/dist/proxy/lifecycle-actor.js +0 -55
  217. package/dist/proxy/lifecycle-machine.js +0 -187
  218. package/dist/proxy/log-tailer.js +0 -265
  219. package/dist/proxy/model-pricing.js +0 -98
  220. package/dist/proxy/network-firewall.js +0 -141
  221. package/dist/proxy/nudge-state.js +0 -93
  222. package/dist/proxy/output-compressor.js +0 -185
  223. package/dist/proxy/pid-lock.js +0 -291
  224. package/dist/proxy/proxy-context.js +0 -11
  225. package/dist/proxy/proxy.js +0 -2633
  226. package/dist/proxy/response-enrichment.js +0 -32
  227. package/dist/proxy/response-envelope.js +0 -313
  228. package/dist/proxy/session-dedup.js +0 -82
  229. package/dist/proxy/session-legend.js +0 -30
  230. package/dist/proxy/session-persistence.js +0 -210
  231. package/dist/proxy/session-resume.js +0 -94
  232. package/dist/proxy/session-stats.js +0 -513
  233. package/dist/proxy/shell-classifier.js +0 -1346
  234. package/dist/proxy/shell-compression-log.js +0 -93
  235. package/dist/proxy/shell-compressor.js +0 -390
  236. package/dist/proxy/shell-graph-boost.js +0 -202
  237. package/dist/proxy/shell-monitor-map.js +0 -18
  238. package/dist/proxy/shell-stats.js +0 -54
  239. package/dist/proxy/shell-strategies/cloud.js +0 -215
  240. package/dist/proxy/shell-strategies/diff.js +0 -159
  241. package/dist/proxy/shell-strategies/error-diagnostic.js +0 -796
  242. package/dist/proxy/shell-strategies/filter-dsl.js +0 -358
  243. package/dist/proxy/shell-strategies/git-status.js +0 -177
  244. package/dist/proxy/shell-strategies/key-value.js +0 -193
  245. package/dist/proxy/shell-strategies/log-text.js +0 -154
  246. package/dist/proxy/shell-strategies/omni.js +0 -188
  247. package/dist/proxy/shell-strategies/progress.js +0 -55
  248. package/dist/proxy/shell-strategies/redact.js +0 -76
  249. package/dist/proxy/shell-strategies/structured.js +0 -241
  250. package/dist/proxy/shell-strategies/tabular.js +0 -243
  251. package/dist/proxy/shell-strategies/test-results-types.js +0 -13
  252. package/dist/proxy/shell-strategies/test-results.js +0 -784
  253. package/dist/proxy/shell-strategies/tree-paths.js +0 -144
  254. package/dist/proxy/shell-strategies/yaml.js +0 -182
  255. package/dist/proxy/shell-tee.js +0 -111
  256. package/dist/proxy/signal-dedup.js +0 -171
  257. package/dist/proxy/startup-renderer.js +0 -158
  258. package/dist/proxy/task-token-display.js +0 -38
  259. package/dist/proxy/token-counter.js +0 -61
  260. package/dist/proxy/tool-clusters.js +0 -273
  261. package/dist/proxy/tool-definitions.js +0 -525
  262. package/dist/proxy/transport-mux.js +0 -229
  263. package/dist/proxy/wire-cap.js +0 -268
  264. package/dist/rules/developer.mozilla.org.json +0 -9
  265. package/dist/rules/github.com.json +0 -21
  266. package/dist/schemas/api/skills.js +0 -19
  267. package/dist/schemas/common/errors.js +0 -7
  268. package/dist/schemas/common/headers.js +0 -5
  269. package/dist/schemas/entities/edge.js +0 -25
  270. package/dist/schemas/entities/entity.js +0 -22
  271. package/dist/schemas/entities/rule.js +0 -18
  272. package/dist/schemas/index.js +0 -14
  273. package/dist/server/event-bus.js +0 -59
  274. package/dist/server/http.js +0 -156
  275. package/dist/server/middleware.js +0 -70
  276. package/dist/server/routes/drift.js +0 -97
  277. package/dist/server/routes/intelligence.js +0 -1217
  278. package/dist/server/routes/reasoning-quality.js +0 -444
  279. package/dist/server/routes/session.js +0 -86
  280. package/dist/server/routes/stream.js +0 -120
  281. package/dist/server/routes/system.js +0 -73
  282. package/dist/server/routes/temporal.js +0 -170
  283. package/dist/server/routes/timeline.js +0 -232
  284. package/dist/server/routes/token-flow.js +0 -403
  285. package/dist/skills/effectiveness-tracker.js +0 -93
  286. package/dist/skills/local-pack.js +0 -380
  287. package/dist/skills/resolver.js +0 -495
  288. package/dist/state-detector.js +0 -83
  289. package/dist/timeline/intent-detector.js +0 -263
  290. package/dist/timeline/loop-miner.js +0 -140
  291. package/dist/timeline/open-threads.js +0 -49
  292. package/dist/timeline/signal-reinforcer.js +0 -62
  293. package/dist/timeline/timeline-bootstrap.js +0 -151
  294. package/dist/timeline/timeline-store.js +0 -618
  295. package/dist/tools/coding/bash.js +0 -49
  296. package/dist/tools/coding/file-edit.js +0 -72
  297. package/dist/tools/coding/file-outline.js +0 -227
  298. package/dist/tools/coding/file-read-protocol.js +0 -425
  299. package/dist/tools/coding/file-read.js +0 -35
  300. package/dist/tools/coding/file-write.js +0 -43
  301. package/dist/tools/coding/glob-tool.js +0 -109
  302. package/dist/tools/coding/grep.js +0 -162
  303. package/dist/tools/coding/index.js +0 -27
  304. package/dist/tools/intelligence/index.js +0 -269
  305. package/dist/tools/intelligence/record-fact.js +0 -48
  306. package/dist/tools/intelligence/timeline-markers.js +0 -130
  307. package/dist/tools/registry.js +0 -47
  308. package/dist/tools/types.js +0 -8
  309. package/dist/tracking/auto-snapshot-triggers.js +0 -246
  310. package/dist/tracking/branch-context.js +0 -115
  311. package/dist/tracking/branch-snapshot.js +0 -217
  312. package/dist/tracking/causal-bridge.js +0 -317
  313. package/dist/tracking/circuit-breaker.js +0 -147
  314. package/dist/tracking/commit-watcher.js +0 -114
  315. package/dist/tracking/context-ledger.js +0 -119
  316. package/dist/tracking/correction-detector.js +0 -324
  317. package/dist/tracking/drift-tracker.js +0 -874
  318. package/dist/tracking/durability-tracker.js +0 -94
  319. package/dist/tracking/entity-rewind.js +0 -200
  320. package/dist/tracking/file-hash-state.js +0 -114
  321. package/dist/tracking/git-attribution.js +0 -132
  322. package/dist/tracking/git-trailers.js +0 -171
  323. package/dist/tracking/intelligence-counter.js +0 -46
  324. package/dist/tracking/intent-correlator.js +0 -202
  325. package/dist/tracking/intent-encoder.js +0 -52
  326. package/dist/tracking/intent-token-tracker.js +0 -159
  327. package/dist/tracking/ledger-archiver.js +0 -94
  328. package/dist/tracking/ledger-chains.js +0 -245
  329. package/dist/tracking/metrics-store.js +0 -361
  330. package/dist/tracking/native-watcher.js +0 -131
  331. package/dist/tracking/offline-rewind.js +0 -295
  332. package/dist/tracking/pending-violations.js +0 -74
  333. package/dist/tracking/persistence-effectiveness.js +0 -167
  334. package/dist/tracking/prompt-durability.js +0 -202
  335. package/dist/tracking/quality-signals.js +0 -213
  336. package/dist/tracking/redactor.js +0 -73
  337. package/dist/tracking/rewind-engine.js +0 -161
  338. package/dist/tracking/session-history.js +0 -128
  339. package/dist/tracking/session-receipt.js +0 -88
  340. package/dist/tracking/session-summary-writer.js +0 -157
  341. package/dist/tracking/shadow-ledger.js +0 -321
  342. package/dist/tracking/stash-manager.js +0 -258
  343. package/dist/tracking/timeline-fork.js +0 -213
  344. package/dist/tracking/timeline.js +0 -69
  345. package/dist/tracking/token-flow.js +0 -276
  346. package/dist/tracking/turn-segmenter.js +0 -122
  347. package/dist/tracking/weekly-accumulator.js +0 -179
  348. package/dist/tracking/working-snapshots.js +0 -188
  349. package/dist/tracking/workspace-manifest.js +0 -176
  350. package/dist/transport/http.js +0 -102
  351. package/dist/utils/counterfactual.js +0 -65
  352. package/dist/utils/deep-link.js +0 -34
  353. package/dist/utils/detect.js +0 -193
  354. package/dist/utils/exec.js +0 -73
  355. package/dist/utils/file-logger.js +0 -87
  356. package/dist/utils/format-error.js +0 -29
  357. package/dist/utils/git.js +0 -181
  358. package/dist/utils/log.js +0 -57
  359. package/dist/utils/logger.js +0 -35
  360. package/dist/utils/mcp-content-json.js +0 -8
  361. package/dist/utils/session-logger.js +0 -154
  362. package/dist/utils/startup-log.js +0 -512
  363. package/dist/utils/ui.js +0 -56
@@ -1,2633 +0,0 @@
1
- /**
2
- * Unified Proxy Loop — the heart of the Local-First Intelligence Proxy.
3
- *
4
- * Combines serve (MCP server), index (auto-index on startup), and watch (file watcher)
5
- * into a single long-lived process.
6
- *
7
- * Boot sequence:
8
- * 1. PID lock → single-instance enforcement
9
- * 2. Graph bootstrap → load CozoDB, pull if missing/stale
10
- * 3. MCP server → stdio transport, 13 tools registered
11
- * 4. Session stats → in-memory counters, print on shutdown
12
- *
13
- * CRITICAL: All logging goes to stderr. stdout is reserved for MCP JSON-RPC.
14
- */
15
- import { existsSync, writeFileSync as fsWriteFileSync, mkdirSync, readFileSync, readdirSync, } from "node:fs";
16
- import { join } from "node:path";
17
- import { aliasAndValidate } from "./arg-validator.js";
18
- import { PidLock } from "./pid-lock.js";
19
- import { createSessionStats, detectSessionResume, formatLocalModeSessionStats, recordBlastRadius, recordChokepointWarning, recordCircularDep, recordCommunityContext, recordCorrectionInjection, recordDeadCodeReference, recordGraphQuery, recordIndexingResult, recordLatency, recordLatencyAdvantage, recordRiskWarning, recordSignaturePreservation, recordToolCall, recordViolation, } from "./session-stats.js";
20
- import { StartupRenderer } from "./startup-renderer.js";
21
- import { ToolUsageTracker, reorderToolsByCluster } from "./tool-clusters.js";
22
- import { TOOL_DEFINITIONS } from "./tool-definitions.js";
23
- import { installFileLogger } from "../utils/file-logger.js";
24
- import { formatUnknownError } from "../utils/format-error.js";
25
- import { stringifyMcpToolJson } from "../utils/mcp-content-json.js";
26
- import { startupLog } from "../utils/startup-log.js";
27
- import { createLifecycleActor, } from "./lifecycle-actor.js";
28
- /** stderr-only logger. stdout is MCP territory. */
29
- const log = {
30
- info: (msg) => process.stderr.write(`[unerr] ${msg}\n`),
31
- warn: (msg) => process.stderr.write(`[unerr] WARN: ${msg}\n`),
32
- error: (msg) => process.stderr.write(`[unerr] ERROR: ${msg}\n`),
33
- };
34
- let proxyFactStore = undefined; // undefined = not yet initialized
35
- let proxyShowStore = null;
36
- async function getProxyFactStore(unerrDir) {
37
- if (proxyFactStore !== undefined)
38
- return proxyFactStore;
39
- try {
40
- const { TemporalFactStore } = await import("../intelligence/temporal-facts.js");
41
- const cwd = join(unerrDir, "..");
42
- proxyFactStore = await TemporalFactStore.create(cwd);
43
- return proxyFactStore;
44
- }
45
- catch {
46
- proxyFactStore = null;
47
- return null;
48
- }
49
- }
50
- async function handleRecordFactProxy(args, unerrDir, shadowLedger, effectiveness) {
51
- const factStore = await getProxyFactStore(unerrDir);
52
- if (!factStore) {
53
- return {
54
- content: [
55
- {
56
- type: "text",
57
- text: JSON.stringify({
58
- error: "Fact store not available. Ensure .unerr/ directory exists.",
59
- }),
60
- },
61
- ],
62
- };
63
- }
64
- try {
65
- const { executeRecordFact } = await import("../tools/intelligence/record-fact.js");
66
- const result = await executeRecordFact(args, factStore, shadowLedger.getSessionId());
67
- if (effectiveness) {
68
- effectiveness.tracker.recordSignalFired({
69
- kind: "fact_recorded",
70
- signal_id: result.fact_id,
71
- entity_key: args.subject ?? null,
72
- turn: effectiveness.turn,
73
- });
74
- }
75
- shadowLedger.record("record_fact", args, { fact_id: result.fact_id }, "unknown", "");
76
- return {
77
- content: [{ type: "text", text: JSON.stringify(result) }],
78
- };
79
- }
80
- catch (err) {
81
- const errMsg = err instanceof Error ? err.message : String(err);
82
- // isError:true is the only channel MCP clients (Claude Code, Cursor)
83
- // surface as a failed tool call in the agent's conversation. Without
84
- // it, an error body looks like a normal successful response and the
85
- // agent reads it as data.
86
- process.stderr.write(`[unerr] record_fact failed: ${errMsg}\n`);
87
- return {
88
- content: [{ type: "text", text: JSON.stringify({ error: errMsg }) }],
89
- isError: true,
90
- };
91
- }
92
- }
93
- async function handleRecallFactsProxy(args, unerrDir, effectiveness) {
94
- const factStore = await getProxyFactStore(unerrDir);
95
- if (!factStore) {
96
- return {
97
- content: [
98
- {
99
- type: "text",
100
- text: JSON.stringify({
101
- facts: [],
102
- message: "Fact store not available",
103
- }),
104
- },
105
- ],
106
- };
107
- }
108
- try {
109
- const scope = args.scope;
110
- const factType = args.fact_type ?? "all";
111
- const minConfidence = args.min_confidence ?? 0.3;
112
- const rotationMode = args.rotation ?? "decay";
113
- const { applyDiversityQuota, rankFactsWithRotation, resolveFactLimit } = await import("./fact-ranking.js");
114
- const requestedLimit = resolveFactLimit(args.limit);
115
- let facts;
116
- if (factType === "negative") {
117
- facts = await factStore.recallNegative(minConfidence);
118
- }
119
- else {
120
- facts = await factStore.recallByScope(scope, minConfidence);
121
- if (factType !== "all") {
122
- facts = facts.filter((f) => f.fact_type === factType);
123
- }
124
- }
125
- const useRotation = rotationMode !== "none" && proxyShowStore !== null;
126
- const ranked = rankFactsWithRotation(facts, {
127
- getShowCount: useRotation
128
- ? (id) => proxyShowStore?.getEffectiveShowCount(id) ?? 0
129
- : undefined,
130
- getLastShownMs: useRotation
131
- ? (id) => proxyShowStore?.getLastShownMs(id) ?? 0
132
- : undefined,
133
- });
134
- const total = ranked.length;
135
- const sliced = applyDiversityQuota(ranked, requestedLimit);
136
- // (Removed: rotation-impact counter only used by the dropped ur|rot prefix.)
137
- if (proxyShowStore) {
138
- for (const f of sliced) {
139
- proxyShowStore.recordShown(f.fact_id, scope ?? "");
140
- }
141
- }
142
- if (effectiveness) {
143
- for (const f of sliced) {
144
- effectiveness.tracker.recordSignalFired({
145
- kind: f.fact_type === "negative" ? "negative_warned" : "fact_recalled",
146
- signal_id: f.fact_id,
147
- entity_key: f.subject ?? null,
148
- turn: effectiveness.turn,
149
- });
150
- }
151
- }
152
- const response = sliced.map((f) => ({
153
- fact_id: f.fact_id,
154
- type: f.fact_type,
155
- content: f.content,
156
- confidence: Math.round(f.effective_confidence * 100) / 100,
157
- subject: f.subject,
158
- source: f.source,
159
- reinforced: f.reinforcement_count,
160
- }));
161
- const body = {
162
- facts: response,
163
- total,
164
- returned: response.length,
165
- };
166
- if (total > response.length) {
167
- body.more_available = total - response.length;
168
- }
169
- // Table row #23 CUT-FLUFF — `ur|rot` was pure internal debug
170
- // ("N facts deprioritized — rotation surfaced fresh picks"). The agent
171
- // could not act on it; rotation is a server-side concept. Body already
172
- // contains the rotated picks; no prefix needed.
173
- return {
174
- content: [{ type: "text", text: JSON.stringify(body) }],
175
- };
176
- }
177
- catch (err) {
178
- const errMsg = err instanceof Error ? err.message : String(err);
179
- process.stderr.write(`[unerr] recall_facts failed: ${errMsg}\n`);
180
- return {
181
- content: [
182
- { type: "text", text: JSON.stringify({ facts: [], error: errMsg }) },
183
- ],
184
- isError: true,
185
- };
186
- }
187
- }
188
- /**
189
- * Start the unified proxy loop. This is the main entry point.
190
- * Returns a cleanup function for testing.
191
- */
192
- /**
193
- * Migrate agent permission config: remove "Read" from deny list.
194
- * Read must remain available for the Edit workflow.
195
- */
196
- function migrateAgentPermissions(cwd) {
197
- try {
198
- const settingsPath = join(cwd, ".claude", "settings.json");
199
- if (!existsSync(settingsPath))
200
- return;
201
- const raw = readFileSync(settingsPath, "utf-8");
202
- const settings = JSON.parse(raw);
203
- const deny = settings?.permissions?.deny;
204
- if (!Array.isArray(deny))
205
- return;
206
- const readIdx = deny.indexOf("Read");
207
- if (readIdx < 0)
208
- return;
209
- deny.splice(readIdx, 1);
210
- fsWriteFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
211
- process.stderr.write("[unerr] Migrated permissions: removed Read from deny list (required for Edit workflow)\n");
212
- }
213
- catch {
214
- // Non-critical — settings migration is best-effort
215
- }
216
- }
217
- export async function startProxy(opts = {}) {
218
- // Mirror stderr to a rotating .log so crash traces during startup land on
219
- // disk even when the process is detached (DM-3 auto-spawn).
220
- installFileLogger({
221
- filePath: join(process.cwd(), ".unerr", "logs", "unerr.log"),
222
- maxBytes: 5_000_000,
223
- keep: 5,
224
- });
225
- const stats = createSessionStats(true);
226
- const startup = new StartupRenderer();
227
- if (!opts.daemonChild) {
228
- startup.mount();
229
- }
230
- // Migrate agent permissions on every startup (idempotent)
231
- migrateAgentPermissions(process.cwd());
232
- const lifecycle = createLifecycleActor(process.cwd());
233
- lifecycle.send({ type: "START_DETECT" });
234
- startup.setLocalMode(true);
235
- // ── Step 1: PID Lock ─────────────────────────────────────────────
236
- const stateDir = join(process.cwd(), ".unerr", "state");
237
- if (!existsSync(stateDir)) {
238
- mkdirSync(stateDir, { recursive: true });
239
- }
240
- const pidLock = new PidLock(stateDir);
241
- const lockResult = await pidLock.acquire();
242
- if (!lockResult.acquired) {
243
- log.info(`Proxy already running (PID ${lockResult.existingPid}). Secondary IDEs can connect via UDS at .unerr/state/proxy.sock`);
244
- process.exit(0);
245
- }
246
- if (lockResult.outcome === "stale_recovered") {
247
- log.warn("Recovered from stale PID file (previous proxy crashed)");
248
- }
249
- startupLog.header();
250
- startupLog.step(`PID ${process.pid} ${startupLog.fmt.muted(`· health localhost:${lockResult.healthPort}`)}`);
251
- // ── Step 1b: Session Resume Detection ────────────────────────────
252
- const ledgerDir = join(process.cwd(), ".unerr", "ledger");
253
- const previousSession = detectSessionResume(stateDir, ledgerDir);
254
- if (previousSession) {
255
- stats.isResumedSession = true;
256
- stats.previousSession = previousSession;
257
- const prevTotal = previousSession.toolCallsLocal;
258
- startupLog.sessionResumed(prevTotal, previousSession.durationMinutes);
259
- }
260
- // Track proxy capability — may degrade to PARSE if CozoDB unavailable
261
- let proxyMode = "local";
262
- let proxyModeReason = "All intelligence runs locally";
263
- // ── Step 2: Discover repos ───────────────────────────────────────
264
- startup.addStep("Repository", "active");
265
- let repoIds = [];
266
- if (opts.repoId) {
267
- repoIds = [opts.repoId];
268
- }
269
- else {
270
- // Auto-detect from local .unerr/config.json
271
- const configPath = join(process.cwd(), ".unerr", "config.json");
272
- if (existsSync(configPath)) {
273
- try {
274
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
275
- if (config.repoId)
276
- repoIds = [config.repoId];
277
- }
278
- catch {
279
- /* fallthrough to manifest discovery */
280
- }
281
- }
282
- // Fallback: discover from manifests
283
- const manifestsDir = join(process.cwd(), ".unerr", "manifests");
284
- if (repoIds.length === 0 && existsSync(manifestsDir)) {
285
- repoIds = readdirSync(manifestsDir)
286
- .filter((f) => f.endsWith(".json"))
287
- .map((f) => f.replace(".json", ""));
288
- }
289
- }
290
- // ── Step 3b: Auto-Bootstrap if no repos found ───────────────────
291
- if (repoIds.length === 0) {
292
- // Generate a deterministic local repoId from git remote or dir name
293
- const { createHash } = await import("node:crypto");
294
- let repoIdentifier = process.cwd();
295
- try {
296
- const { getRemoteUrl } = await import("../utils/git.js");
297
- const remote = await getRemoteUrl(process.cwd());
298
- if (remote)
299
- repoIdentifier = remote;
300
- }
301
- catch {
302
- // No git remote — use cwd
303
- }
304
- const localRepoId = createHash("sha256")
305
- .update(repoIdentifier)
306
- .digest("hex")
307
- .slice(0, 12);
308
- repoIds = [localRepoId];
309
- startupLog.done(`Repository ${startupLog.fmt.cyan(localRepoId)} ${startupLog.fmt.muted(`(from ${repoIdentifier === process.cwd() ? "directory" : "git remote"})`)}`);
310
- }
311
- // Update repository step
312
- if (repoIds.length > 0) {
313
- startup.updateStep("Repository", "done", repoIds[0] ?? "");
314
- }
315
- else {
316
- startup.updateStep("Repository", "error", "No repo found");
317
- }
318
- lifecycle.send({
319
- type: "DETECT_COMPLETE",
320
- needsSetup: repoIds.length === 0,
321
- repoId: repoIds[0],
322
- });
323
- // ── Step 4: Graph Bootstrap (skipped in PARSE mode) ──────────────
324
- startup.addStep("Graph loaded", proxyMode === "parse" ? "pending" : "active");
325
- let localGraph = null;
326
- let parseIndex = null;
327
- // L11: Background indexing flag — hoisted for access after MCP server.connect()
328
- let needsBackgroundIndex = false;
329
- if (proxyMode !== "parse") {
330
- const projectRoot = process.cwd();
331
- try {
332
- const { openPersistentDb } = await import("../intelligence/persistent-db.js");
333
- const { db, isNew, dbPath } = await openPersistentDb(projectRoot);
334
- const { CozoGraphStore } = await import("../intelligence/local-graph.js");
335
- const graphStart = Date.now();
336
- localGraph = await CozoGraphStore.create(db);
337
- const graphOpenMs = Date.now() - graphStart;
338
- if (!isNew && (await localGraph.isPopulated())) {
339
- // ── Persistent DB: graph already fully populated ──────────────
340
- // All entities, edges, communities, conventions, rules survive across restarts.
341
- // No snapshot loading, no re-detection — instant availability.
342
- const projStats = await localGraph.getLocalProjectStats();
343
- const rules = await localGraph.getRules();
344
- const ruleCount = rules?.length ?? 0;
345
- const communityResult = await localGraph.db.run("?[count(id)] := *communities[id, _, _, _]");
346
- const communityCount = communityResult.rows[0]?.[0] ?? 0;
347
- const patternResult = await localGraph.db.run("?[count(key)] := *patterns[key, _, _, _, _, _, _]");
348
- const patternCount = patternResult.rows[0]?.[0] ?? 0;
349
- startup.setLocalIndexStats({
350
- fileCount: projStats.fileCount,
351
- entityCount: projStats.entityCount,
352
- edgeCount: projStats.edgeCount,
353
- indexingTimeMs: graphOpenMs,
354
- communityCount,
355
- conventionCount: patternCount,
356
- ruleCount,
357
- });
358
- const hottest = projStats.topFiles?.[0];
359
- startupLog.graphLoaded({
360
- entities: projStats.entityCount,
361
- edges: projStats.edgeCount,
362
- files: projStats.fileCount,
363
- communities: communityCount,
364
- patterns: patternCount,
365
- rules: ruleCount,
366
- ms: graphOpenMs,
367
- hottestFile: hottest?.filePath,
368
- hottestCount: hottest?.entityCount,
369
- });
370
- startupLog.perf(`${startupLog.fmt.cyan("Persistent graph")} ${startupLog.fmt.muted("— zero recomputation, all intelligence preserved")}`);
371
- // Always reindex on startup to ensure graph data (end_line, etc.) is fresh
372
- needsBackgroundIndex = true;
373
- startupLog.step(`${startupLog.fmt.muted("Background reindex will refresh graph data after MCP ready")}`);
374
- }
375
- else {
376
- // ── Fresh DB or empty: needs initial indexing ─────────────────
377
- // Check for existing msgpack snapshot to migrate from
378
- const { loadLocalSnapshot } = await import("../intelligence/local-snapshot.js");
379
- const snapshotStart = Date.now();
380
- const migrated = await loadLocalSnapshot(projectRoot, localGraph);
381
- if (migrated) {
382
- // One-time migration: snapshot → persistent DB
383
- const snapshotMs = Date.now() - snapshotStart;
384
- const { buildSearchIndex } = await import("../intelligence/search-index.js");
385
- await buildSearchIndex(localGraph.db);
386
- const { runCommunityDetection, runConventionDetection } = await import("../intelligence/local-indexer.js");
387
- const communityCount = await runCommunityDetection(localGraph);
388
- const { patternCount, ruleCount } = await runConventionDetection(localGraph, repoIds[0]);
389
- const projStats = await localGraph.getLocalProjectStats();
390
- startup.setLocalIndexStats({
391
- fileCount: projStats.fileCount,
392
- entityCount: projStats.entityCount,
393
- edgeCount: projStats.edgeCount,
394
- indexingTimeMs: snapshotMs,
395
- communityCount,
396
- conventionCount: patternCount,
397
- ruleCount,
398
- });
399
- const hottest = projStats.topFiles?.[0];
400
- startupLog.graphLoaded({
401
- entities: projStats.entityCount,
402
- edges: projStats.edgeCount,
403
- files: projStats.fileCount,
404
- communities: communityCount,
405
- patterns: patternCount,
406
- rules: ruleCount,
407
- ms: snapshotMs,
408
- hottestFile: hottest?.filePath,
409
- hottestCount: hottest?.entityCount,
410
- });
411
- startupLog.done(`Migrated snapshot to persistent graph ${startupLog.fmt.muted(`→ ${dbPath}`)}`);
412
- // Layer 9: Generate temporal facts from conventions after snapshot migration
413
- try {
414
- const migrationUnerrDir = join(process.cwd(), ".unerr");
415
- const factStoreForMigration = await getProxyFactStore(migrationUnerrDir);
416
- if (factStoreForMigration) {
417
- const { detectLocalConventions } = await import("../intelligence/local-convention-detector.js");
418
- const { generateFromConventions, runFactGenerationPipeline } = await import("../intelligence/fact-generator.js");
419
- const detection = await detectLocalConventions(localGraph.db);
420
- if (detection.conventions.length > 0) {
421
- const convResult = await generateFromConventions(factStoreForMigration, detection.conventions);
422
- if (convResult.created > 0 || convResult.reinforced > 0) {
423
- log.info(`Fact generator: ${convResult.created} convention facts created, ${convResult.reinforced} reinforced`);
424
- }
425
- }
426
- const pipelineResults = await runFactGenerationPipeline(factStoreForMigration, migrationUnerrDir);
427
- for (const r of pipelineResults) {
428
- if (r.created > 0 || r.reinforced > 0) {
429
- log.info(`Fact generator [${r.source}]: ${r.created} created, ${r.reinforced} reinforced`);
430
- }
431
- }
432
- }
433
- }
434
- catch {
435
- // Non-critical — fact generation failure doesn't block startup
436
- }
437
- }
438
- else {
439
- // No snapshot available — full index needed
440
- needsBackgroundIndex = true;
441
- startupLog.step(`${startupLog.fmt.muted("First run — full index will start after MCP ready")}`);
442
- }
443
- }
444
- }
445
- catch (err) {
446
- const errMsg = err instanceof Error
447
- ? err.message
448
- : typeof err === "object" && err !== null
449
- ? JSON.stringify(err)
450
- : String(err);
451
- log.warn(`Failed to open persistent graph: ${errMsg}. Falling back to PARSE mode.`);
452
- proxyMode = "parse";
453
- proxyModeReason = "CozoDB unavailable. Running in parse-only mode.";
454
- }
455
- }
456
- // Health grade computation — deferred to after MCP ready (Task 6.3)
457
- let healthResult = null;
458
- if (localGraph && proxyMode !== "parse") {
459
- startup.updateStep("Graph loaded", "done");
460
- lifecycle.send({ type: "GRAPH_LOADED" });
461
- }
462
- else if (proxyMode === "parse") {
463
- startup.updateStep("Graph loaded", "pending", "PARSE mode");
464
- }
465
- // PARSE mode: create empty index now — populate asynchronously after MCP ready (Task 6.3)
466
- if (proxyMode === "parse") {
467
- const { ParseModeIndex } = await import("./auto-bootstrap.js");
468
- parseIndex = new ParseModeIndex();
469
- }
470
- // ── Step 5: Router ─────────────────────────────────────────────
471
- let ruleEvaluator;
472
- if (localGraph) {
473
- try {
474
- const ruleEvalModule = await import("../intelligence/rule-evaluator.js");
475
- ruleEvaluator = ruleEvalModule.evaluateRules;
476
- }
477
- catch {
478
- // Rule evaluator not available — check_rules will be unavailable
479
- }
480
- }
481
- // In PARSE mode, create a minimal graph stub that delegates to ParseModeIndex
482
- const graphForRouter = localGraph ?? (await createParseGraphStub(parseIndex));
483
- const { QueryRouter } = await import("../intelligence/query-router.js");
484
- const router = new QueryRouter(graphForRouter, ruleEvaluator);
485
- router.setMode(proxyMode, proxyModeReason);
486
- // Layer 7: Wire event bus for dashboard SSE transport
487
- const { eventBus } = await import("../server/event-bus.js");
488
- router.setEventBus(eventBus);
489
- // Sprint 2: Wire session events for value counter (Task 2.7)
490
- router.setSessionEvents(stats.events);
491
- // Sprint S1: Wire output compression & quality loop
492
- const { createSessionDedup } = await import("./session-dedup.js");
493
- const { createCompressionQualityMonitor } = await import("./compression-quality-monitor.js");
494
- const sessionDedup = createSessionDedup();
495
- const compressionMonitor = createCompressionQualityMonitor();
496
- router.setSessionDedup(sessionDedup);
497
- router.setCompressionMonitor(compressionMonitor);
498
- // Sprint S2: Wire session health monitor & exploration cost accumulator
499
- const { createSessionHealthMonitor } = await import("../intelligence/session-health-monitor.js");
500
- const { createExplorationAccumulator } = await import("../intelligence/exploration-cost.js");
501
- const healthMonitor = createSessionHealthMonitor();
502
- const explorationAccumulator = createExplorationAccumulator();
503
- router.setHealthMonitor(healthMonitor);
504
- router.setExplorationAccumulator(explorationAccumulator);
505
- // Sprint S3: Wire context rot detector
506
- const { createContextRotDetector } = await import("./context-rot-detector.js");
507
- const contextRotDetector = createContextRotDetector();
508
- router.setContextRotDetector(contextRotDetector);
509
- // Sprint S4: Wire token accounting & visibility
510
- const { createTokenCounter } = await import("./token-counter.js");
511
- const { createEfficiencyTracker } = await import("./efficiency-tracker.js");
512
- const tokenCounter = createTokenCounter({ emitEveryN: 5 });
513
- // EfficiencyTracker created with placeholder — upgraded to TokenFlow-backed after tokenFlowWriter init
514
- let efficiencyTracker = createEfficiencyTracker();
515
- router.setTokenCounter(tokenCounter);
516
- router.setEfficiencyTracker(efficiencyTracker);
517
- // Sprint 1.2: Wire fact store for _context injection
518
- const proxyFactStore = await getProxyFactStore(join(process.cwd(), ".unerr"));
519
- if (proxyFactStore) {
520
- router.setFactStore(proxyFactStore);
521
- }
522
- // Sprint 2: Health info wired in deferred init (Task 6.3)
523
- // ── Sprint S7: Persistent Context Wiring ────────────────────��───────
524
- // S7.2 + S7.3 + S7.4: Session resume with causal-bridge enrichment
525
- if (previousSession) {
526
- try {
527
- const { generateSessionResume } = await import("../proxy/session-resume.js");
528
- const { ShadowLedger: ResumeLedger } = await import("../tracking/shadow-ledger.js");
529
- const resumeLedger = new ResumeLedger(join(process.cwd(), ".unerr"));
530
- const ledgerEntries = resumeLedger.getRecentEntries(50);
531
- const resumeCtx = generateSessionResume(ledgerEntries);
532
- if (resumeCtx) {
533
- router.setSessionResumeContext({
534
- summary: resumeCtx.summary,
535
- filesModified: resumeCtx.filesModified,
536
- incompleteEntities: resumeCtx.incompleteEntities,
537
- });
538
- log.info(`Session resume context prepared (${resumeCtx.filesModified.length} files, ${resumeCtx.incompleteEntities.length} incomplete)`);
539
- }
540
- }
541
- catch {
542
- // Non-critical — session resume enrichment is best-effort
543
- }
544
- }
545
- // S7.5 + S7.8: Durability scorer — compute scores from ledger history
546
- try {
547
- const { createDurabilityScorer } = await import("../intelligence/durability-scorer.js");
548
- const durabilityScorer = createDurabilityScorer();
549
- const { ShadowLedger: DurLedger } = await import("../tracking/shadow-ledger.js");
550
- const durLedger = new DurLedger(join(process.cwd(), ".unerr"));
551
- const durEntries = durLedger.getRecentEntries(200);
552
- if (durEntries.length > 0) {
553
- durabilityScorer.computeScores(durEntries);
554
- router.setDurabilityScorer(durabilityScorer);
555
- const unstable = durabilityScorer.getTopUnstable(5);
556
- if (unstable.length > 0) {
557
- log.info(`Durability scorer active (${unstable.length} fragile entities tracked)`);
558
- }
559
- }
560
- }
561
- catch {
562
- // Non-critical — durability scoring is best-effort
563
- }
564
- // ── Sprint S8: Value Surfacing ──────────────────────────────────────
565
- // S8.1 + S8.5: Wire value guard (fires once per session when savings exceed threshold)
566
- const { createValueGuard } = await import("../config/value-surfacing.js");
567
- const valueGuard = createValueGuard();
568
- router.setValueGuard(valueGuard);
569
- // S7.6: Negative knowledge — load anti-patterns for injection
570
- try {
571
- const { detectInstableEntities } = await import("../intelligence/negative-knowledge.js");
572
- const { ShadowLedger: NkLedger } = await import("../tracking/shadow-ledger.js");
573
- const nkLedger = new NkLedger(join(process.cwd(), ".unerr"));
574
- const nkEntries = nkLedger.getRecentEntries(200);
575
- if (nkEntries.length > 0) {
576
- const antiPatterns = detectInstableEntities(nkEntries);
577
- if (antiPatterns.length > 0) {
578
- router.setAntiPatterns(antiPatterns.map((p) => ({
579
- entityKey: p.entityKey,
580
- pattern: p.pattern,
581
- reason: p.reason,
582
- })));
583
- log.info(`Negative knowledge loaded (${antiPatterns.length} anti-patterns)`);
584
- }
585
- }
586
- }
587
- catch {
588
- // Non-critical — negative knowledge is best-effort
589
- }
590
- // ── Q.1: Causal Bridge — entity history from prompt→commit→survival ──
591
- try {
592
- const { CausalBridge } = await import("../tracking/causal-bridge.js");
593
- const causalBridge = new CausalBridge(join(process.cwd(), ".unerr"), process.cwd());
594
- router.setCausalBridge(causalBridge);
595
- log.info("Causal bridge wired (entity interaction history active)");
596
- }
597
- catch {
598
- // Non-critical — causal bridge is best-effort
599
- }
600
- // ── Q.3: Convention Learner — cross-session correction learning ──
601
- try {
602
- const { learnConventions } = await import("../intelligence/convention-learner.js");
603
- const { ShadowLedger: ConvLedger } = await import("../tracking/shadow-ledger.js");
604
- const convLedger = new ConvLedger(join(process.cwd(), ".unerr"));
605
- const convEntries = convLedger.getRecentEntries(100);
606
- if (convEntries.length > 0) {
607
- const learned = learnConventions(convEntries);
608
- if (learned.length > 0) {
609
- router.setLearnedConventions(learned.map((c) => ({
610
- id: c.id,
611
- name: c.name,
612
- pattern: c.pattern,
613
- confidence: c.confidence,
614
- })));
615
- log.info(`Convention learner: ${learned.length} patterns detected from corrections`);
616
- }
617
- }
618
- }
619
- catch {
620
- // Non-critical — convention learning is best-effort
621
- }
622
- // ── Q.1: Prompt Durability Profiles — strategy recommendations ──
623
- try {
624
- const { computePromptDurabilityProfiles } = await import("../tracking/prompt-durability.js");
625
- const { ShadowLedger: DurProfLedger } = await import("../tracking/shadow-ledger.js");
626
- const durProfLedger = new DurProfLedger(join(process.cwd(), ".unerr"));
627
- const durProfEntries = durProfLedger.getRecentEntries(200);
628
- if (durProfEntries.length > 0) {
629
- const profiles = computePromptDurabilityProfiles(durProfEntries);
630
- if (profiles.length > 0) {
631
- router.setPromptDurabilityProfiles(profiles.map((p) => ({
632
- actionType: p.actionType,
633
- targetRisk: p.targetRisk,
634
- durability: p.durability,
635
- recommendation: p.recommendation,
636
- })));
637
- log.info(`Prompt durability: ${profiles.length} profiles computed`);
638
- }
639
- }
640
- }
641
- catch {
642
- // Non-critical — prompt durability is best-effort
643
- }
644
- // ── Cross-Session Context Ledger — prevents re-delivering context ──
645
- try {
646
- const { createContextLedger } = await import("../tracking/context-ledger.js");
647
- const contextLedger = createContextLedger(join(process.cwd(), ".unerr"));
648
- contextLedger.load();
649
- contextLedger.prune();
650
- router.setContextLedger(contextLedger);
651
- log.info(`Context ledger loaded (${contextLedger.getDeliveredCount()} prior deliveries)`);
652
- }
653
- catch {
654
- // Non-critical — cross-session dedup is best-effort
655
- }
656
- // ── Intent Token Tracker — groups tool calls by intent ──
657
- try {
658
- const { createIntentTokenTracker } = await import("../tracking/intent-token-tracker.js");
659
- const { eventBus: intentEventBus } = await import("../server/event-bus.js");
660
- const intentTracker = createIntentTokenTracker({
661
- dashboardSink: (payload) => {
662
- intentEventBus.emit("intent", payload);
663
- },
664
- });
665
- router.setIntentTracker(intentTracker);
666
- log.info("Intent token tracker active");
667
- }
668
- catch {
669
- // Non-critical — intent tracking is best-effort
670
- }
671
- // ── Step 5c (L12.4): Skill Self-Healing on Boot ─────────────────
672
- // Check IDE skill directory — reinstall from cascade if empty.
673
- try {
674
- const { detectIde } = await import("../utils/detect.js");
675
- const { ensureSkillsPresent } = await import("../skills/resolver.js");
676
- const ide = await detectIde(process.cwd());
677
- const skillsInstalled = await ensureSkillsPresent({
678
- ide,
679
- cwd: process.cwd(),
680
- });
681
- if (skillsInstalled > 0) {
682
- log.info(`Self-healed ${skillsInstalled} skills for ${ide}`);
683
- }
684
- }
685
- catch (err) {
686
- log.warn(`Skill self-healing failed: ${err instanceof Error ? err.message : String(err)}`);
687
- }
688
- // ── Step 7: MCP Server (stdio) ───────────────────────────────────
689
- const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
690
- const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
691
- const { ListToolsRequestSchema, CallToolRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
692
- const server = new Server({ name: "unerr-local", version: "0.1.0" }, { capabilities: { tools: {} } });
693
- // Tool definitions imported from shared tool-definitions.ts (single source of truth)
694
- const toolDefinitions = [...TOOL_DEFINITIONS];
695
- // S7: Tool usage tracker for semantic cluster reordering
696
- const toolUsageTracker = new ToolUsageTracker();
697
- let cachedInjectedTools = null;
698
- let cachedBlockRuleKeys = new Set();
699
- let cachedDeepDiveState = "none";
700
- /**
701
- * Boundary tool-call validator. Looks up the tool's schema (base +
702
- * deep-dive) and runs alias normalization + required-field enforcement
703
- * via arg-validator. Returns a structured failure on missing/invalid
704
- * args; null on success. Must be invoked from BOTH stdio and UDS
705
- * dispatch paths (see comment at the stdio handler).
706
- */
707
- const runBoundaryValidation = (toolName, toolArgs) => {
708
- let def = toolDefinitions.find((t) => t.name === toolName);
709
- if (!def) {
710
- try {
711
- // biome-ignore format: typeof import() must stay on one line for TS parsing
712
- const { DEEP_DIVE_TOOL_DEFINITIONS } = require("../intelligence/deep-dive-tools.js");
713
- def = DEEP_DIVE_TOOL_DEFINITIONS.find((t) => t.name === toolName);
714
- }
715
- catch {
716
- /* deep-dive not available — base validation is enough */
717
- }
718
- }
719
- if (!def)
720
- return null;
721
- return aliasAndValidate(def, toolArgs);
722
- };
723
- async function getInjectedTools() {
724
- if (!localGraph)
725
- return toolDefinitions;
726
- try {
727
- // biome-ignore format: typeof import() must stay on one line for TS parsing
728
- const { injectRuleContext, needsRefresh, getBlockRules } = require("../intelligence/tool-injector.js");
729
- // biome-ignore format: typeof import() must stay on one line for TS parsing
730
- const { DEEP_DIVE_TOOL_DEFINITIONS, NAVIGATION_TOOL_NAMES } = require("../intelligence/deep-dive-tools.js");
731
- const currentDeepDiveState = await localGraph.getDeepDiveProjectState();
732
- const deepDiveStateChanged = currentDeepDiveState !== cachedDeepDiveState;
733
- if (cachedInjectedTools &&
734
- !(await needsRefresh(localGraph, cachedBlockRuleKeys)) &&
735
- !deepDiveStateChanged) {
736
- return cachedInjectedTools;
737
- }
738
- // Build base tools + conditionally include deep dive tools
739
- let baseTools = [...toolDefinitions];
740
- if (currentDeepDiveState === "approved") {
741
- // Post-approval: 4 navigation tools
742
- const navTools = DEEP_DIVE_TOOL_DEFINITIONS.filter((t) => NAVIGATION_TOOL_NAMES.includes(t.name));
743
- baseTools = [...baseTools, ...navTools];
744
- }
745
- else if (currentDeepDiveState === "building") {
746
- // Implementation phase: all 8 tools
747
- baseTools = [
748
- ...baseTools,
749
- ...DEEP_DIVE_TOOL_DEFINITIONS,
750
- ];
751
- }
752
- // "none" and "pre_approval": no deep dive tools
753
- cachedInjectedTools = (await injectRuleContext(baseTools, localGraph));
754
- cachedBlockRuleKeys = new Set((await getBlockRules(localGraph)).map((r) => r.key));
755
- cachedDeepDiveState = currentDeepDiveState;
756
- return cachedInjectedTools;
757
- }
758
- catch {
759
- return toolDefinitions;
760
- }
761
- }
762
- // S7: Reorder tools by semantic cluster priority based on recent usage
763
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
764
- tools: reorderToolsByCluster(await getInjectedTools(), toolUsageTracker),
765
- }));
766
- // ── Step 7a: Shadow Ledger + Intent Correlator ─────────────────
767
- const { ShadowLedger } = await import("../tracking/shadow-ledger.js");
768
- const { IntentCorrelator } = await import("../tracking/intent-correlator.js");
769
- const unerrDirForLedger = join(process.cwd(), ".unerr");
770
- const shadowLedger = new ShadowLedger(unerrDirForLedger);
771
- const intentCorrelator = new IntentCorrelator(unerrDirForLedger);
772
- log.info(`Shadow ledger active (session ${shadowLedger.getSessionId().slice(0, 8)})`);
773
- // ST-1c: Timeline subsystem (kill-switch UNERR_TIMELINE_V2=0). Additive,
774
- // never touches existing facts.db / graph.db code paths.
775
- const { startTimelineBootstrap } = await import("../timeline/timeline-bootstrap.js");
776
- const timelineHandle = await startTimelineBootstrap({
777
- projectRoot: process.cwd(),
778
- ledger: shadowLedger,
779
- log: (level, msg) => level === "warn" ? log.warn(msg) : log.info(`[timeline] ${msg}`),
780
- // UX-2: resolve agent name lazily — `agentNameByClient` is populated later
781
- // by the UDS initialize handler, and `server.getClientVersion()` resolves
782
- // once the stdio client completes its MCP handshake.
783
- getAgentName: () => server.getClientVersion?.()?.name ?? undefined,
784
- });
785
- // ST-4: Nightly intent-stitch job (runs at idle, never blocking). Additive,
786
- // separate from the existing pruneDecayed cron on facts.db.
787
- let timelineIntentStitchInterval = null;
788
- let timelineSignalPruneInterval = null;
789
- if (timelineHandle) {
790
- const { runIntentStitch } = await import("../timeline/intent-detector.js");
791
- const { pruneStaleSignals } = await import("../timeline/signal-reinforcer.js");
792
- const stitchPeriodMs = 60 * 60_000; // hourly is fine; nightly would skip many windows
793
- timelineIntentStitchInterval = setInterval(() => {
794
- runIntentStitch(timelineHandle.store).catch((err) => {
795
- log.warn(`Timeline intent-stitch failed: ${err instanceof Error ? err.message : String(err)}`);
796
- });
797
- }, stitchPeriodMs);
798
- // Best-effort initial run so first session has an intent attached.
799
- runIntentStitch(timelineHandle.store).catch(() => { });
800
- // ST-5: Hourly signal prune. Operates only on timeline.db.derived_signals;
801
- // Layer 9 pruneDecayed on facts.db is untouched.
802
- const prunePeriodMs = 60 * 60_000;
803
- timelineSignalPruneInterval = setInterval(() => {
804
- pruneStaleSignals(timelineHandle.store).catch((err) => {
805
- log.warn(`Timeline signal prune failed: ${err instanceof Error ? err.message : String(err)}`);
806
- });
807
- }, prunePeriodMs);
808
- }
809
- // ST-6: Daily shadow-ledger archive. Splits shadow.jsonl into
810
- // recent (<7 days) + gzipped archive. Pure I/O on the ledger directory;
811
- // no other subsystem touched.
812
- const { archiveShadowLedger } = await import("../tracking/ledger-archiver.js");
813
- const archiveIntervalMs = 24 * 60 * 60_000;
814
- const ledgerArchiveInterval = setInterval(() => {
815
- try {
816
- const r = archiveShadowLedger(unerrDirForLedger);
817
- if (r.archived > 0) {
818
- log.info(`Ledger archive: rotated ${r.archived} entries → ${r.archivePath ?? "?"}`);
819
- }
820
- }
821
- catch (err) {
822
- log.warn(`Ledger archive failed: ${err instanceof Error ? err.message : String(err)}`);
823
- }
824
- }, archiveIntervalMs);
825
- // Fire once on boot so existing >7d entries get archived without waiting.
826
- try {
827
- archiveShadowLedger(unerrDirForLedger);
828
- }
829
- catch {
830
- /* best-effort on boot */
831
- }
832
- // Sprint 4: Initialize narrative capture (daemon mode)
833
- let narrativeCapture = null;
834
- if (proxyFactStore) {
835
- try {
836
- const { SessionNarrativeCapture } = await import("../intelligence/session-narrative.js");
837
- narrativeCapture = new SessionNarrativeCapture(proxyFactStore, shadowLedger);
838
- }
839
- catch {
840
- // Non-critical
841
- }
842
- }
843
- // Persistent rotation store — facts.db `signal_shows` relation. Survives
844
- // restart and coordinates show-counts across parallel `unerr --mcp` sessions
845
- // in the same repo (per-session rows so writes never contend).
846
- if (proxyFactStore) {
847
- try {
848
- const { SignalShowStore } = await import("../intelligence/signal-show-store.js");
849
- proxyShowStore = new SignalShowStore(proxyFactStore.getDb(), shadowLedger.getSessionId());
850
- await proxyShowStore.start();
851
- router.setSignalShowStore(proxyShowStore);
852
- process.once("beforeExit", () => {
853
- proxyShowStore?.close().catch(() => { });
854
- });
855
- }
856
- catch (err) {
857
- log.warn(`Signal show store init failed (rotation degrades to in-memory): ${err instanceof Error ? err.message : "unknown"}`);
858
- }
859
- }
860
- // Sprint 5: Pattern analysis call counter (periodic trigger every 20 calls)
861
- let patternAnalysisCallCount = 0;
862
- // ── Layer 10: Token Flow Writer — unified savings attribution ────
863
- const { TokenFlowWriter } = await import("../tracking/token-flow.js");
864
- const tokenFlowWriter = new TokenFlowWriter(unerrDirForLedger, shadowLedger.getSessionId());
865
- process.env.UNERR_SESSION_ID = shadowLedger.getSessionId();
866
- // RC3 fix: Write session ID to file for exec processes
867
- try {
868
- const { writeFileSync } = await import("node:fs");
869
- writeFileSync(join(unerrDirForLedger, "state", "session.id"), shadowLedger.getSessionId(), "utf-8");
870
- }
871
- catch {
872
- /* best effort */
873
- }
874
- router.setTokenFlow(tokenFlowWriter);
875
- efficiencyTracker = createEfficiencyTracker(tokenFlowWriter);
876
- router.setEfficiencyTracker(efficiencyTracker);
877
- // Persistent memory effectiveness tracker — emits verdict events when
878
- // fact/convention/resume injections close their observation window.
879
- const { PersistenceEffectivenessTracker } = await import("../tracking/persistence-effectiveness.js");
880
- const effectivenessTracker = new PersistenceEffectivenessTracker(tokenFlowWriter);
881
- router.setEffectivenessTracker(effectivenessTracker);
882
- // Close windows on every turn boundary so verdicts land near-real-time.
883
- shadowLedger.getTurnSegmenter().onTurnClose(() => {
884
- effectivenessTracker.closeWindow(router.sessionContext.getToolCallCount());
885
- });
886
- // ── Sprint 10: Working Snapshots, Circuit Breaker, Quality Signals ──
887
- const { WorkingSnapshotStore } = await import("../tracking/working-snapshots.js");
888
- const workingSnapshotStore = new WorkingSnapshotStore(unerrDirForLedger);
889
- const { LedgerCircuitBreaker } = await import("../tracking/circuit-breaker.js");
890
- const circuitBreaker = new LedgerCircuitBreaker();
891
- // S9.1: Wire circuit breaker into query router for loop detection
892
- router.setCircuitBreaker(circuitBreaker);
893
- const { QualitySignalTracker } = await import("../tracking/quality-signals.js");
894
- const qualitySignalTracker = new QualitySignalTracker(unerrDirForLedger);
895
- let resumeMetaEmitted = false;
896
- // ── Layer 4: Behavior Engine (BA-1 + BA-2 + BA-3) ──────────────
897
- const { BehaviorDispatcher } = await import("../behaviors/framework.js");
898
- const { LoopCircuitBreaker } = await import("../behaviors/loop-breaker.js");
899
- const { SessionContinuityBehavior } = await import("../behaviors/session-continuity.js");
900
- const { CascadeConsistencyGuard } = await import("../behaviors/cascade-guard.js");
901
- const { IncompleteWorkDetector } = await import("../behaviors/incomplete-work.js");
902
- const { ConventionDriftPrevention } = await import("../behaviors/convention-drift.js");
903
- const { AutoDocBehavior } = await import("../behaviors/auto-doc.js");
904
- const { ArchitectureBoundaryGuard } = await import("../behaviors/architecture-guard.js");
905
- const { ChangeNarrativeBehavior } = await import("../behaviors/change-narrative.js");
906
- const behaviorDispatcher = new BehaviorDispatcher();
907
- const loopBreaker = new LoopCircuitBreaker();
908
- behaviorDispatcher.register(loopBreaker);
909
- const sessionContinuity = new SessionContinuityBehavior();
910
- sessionContinuity.attachLedger(shadowLedger);
911
- behaviorDispatcher.register(sessionContinuity);
912
- const cascadeGuard = new CascadeConsistencyGuard();
913
- if (localGraph)
914
- cascadeGuard.attachGraph(localGraph);
915
- behaviorDispatcher.register(cascadeGuard);
916
- const incompleteWork = new IncompleteWorkDetector();
917
- incompleteWork.attachLedger(shadowLedger);
918
- incompleteWork.attachCascadeGuard(cascadeGuard);
919
- incompleteWork.setUnerrDir(unerrDirForLedger);
920
- if (localGraph)
921
- incompleteWork.attachGraph(localGraph);
922
- behaviorDispatcher.register(incompleteWork);
923
- const conventionDrift = new ConventionDriftPrevention();
924
- if (localGraph)
925
- conventionDrift.attachGraph(localGraph);
926
- behaviorDispatcher.register(conventionDrift);
927
- const autoDoc = new AutoDocBehavior();
928
- if (localGraph)
929
- autoDoc.attachGraph(localGraph);
930
- behaviorDispatcher.register(autoDoc);
931
- const architectureGuard = new ArchitectureBoundaryGuard();
932
- if (localGraph)
933
- architectureGuard.attachGraph(localGraph);
934
- behaviorDispatcher.register(architectureGuard);
935
- const changeNarrative = new ChangeNarrativeBehavior();
936
- changeNarrative.attachBehaviors({
937
- cascadeGuard,
938
- conventionDrift,
939
- incompleteWork,
940
- architectureGuard,
941
- loopBreaker,
942
- autoDoc,
943
- });
944
- behaviorDispatcher.register(changeNarrative);
945
- log.info(`Behavior engine active (${behaviorDispatcher.getRegisteredBehaviors().length} behaviors registered)`);
946
- // Task 6.3: Deferred initialization tracking
947
- let deferredInitComplete = false;
948
- let branchContext = null;
949
- // ══════════════════════════════════════════════════════════════════
950
- // Stdio tools/call handler (primary MCP client connected directly)
951
- //
952
- // IMPORTANT: Any tool intercepted here (before router.execute()) MUST
953
- // also be intercepted in the UDS handler below (transportMux.setHandler).
954
- // Bridged IDE clients via `unerr --mcp` hit the UDS handler, not this one.
955
- // Forgetting to mirror dispatch causes "Unknown tool" errors for bridged clients.
956
- // ══════════════════════════════════════════════════════════════════
957
- server.setRequestHandler(CallToolRequestSchema,
958
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
959
- (async (request) => {
960
- const { name, arguments: args = {} } = request.params;
961
- // ── Boundary validation: alias normalization + required-field check ──
962
- // Centralized in arg-validator so every tool with a schema-level
963
- // `required: [...]` is enforced uniformly. Catches the silent-failure
964
- // pattern where missing/aliased params reached handlers, ran queries
965
- // with undefined filters, and returned empty results that agents
966
- // mistook for "graph has no data" — driving drift to grep fallback.
967
- const validationFailure = runBoundaryValidation(name, args);
968
- if (validationFailure) {
969
- // Boundary failures (missing required args, type mismatches) are real
970
- // errors — flag with isError:true so MCP clients surface them in the
971
- // agent conversation instead of treating the message as a normal
972
- // tool result body.
973
- process.stderr.write(`[unerr] tools/call validation failed for ${name}: ${JSON.stringify(validationFailure)}\n`);
974
- return {
975
- content: [{ type: "text", text: JSON.stringify(validationFailure) }],
976
- isError: true,
977
- };
978
- }
979
- // S7: Track tool usage for semantic cluster reordering
980
- toolUsageTracker.record(name);
981
- // ── Layer 4: Pre-tool-use behavioral hooks ──
982
- const behaviorCtx = {
983
- toolName: name,
984
- args,
985
- sessionId: shadowLedger.getSessionId(),
986
- entityKey: args.key ?? args.entity ?? undefined,
987
- filePath: args.path ??
988
- args.file_path ??
989
- args.file ??
990
- undefined,
991
- };
992
- const preOutput = await behaviorDispatcher.firePreToolUse(behaviorCtx);
993
- if (preOutput?.halt) {
994
- // Layer 10: Record behavior automation savings (prevented a full tool call)
995
- if (tokenFlowWriter) {
996
- const haltContent = preOutput._context
997
- ? JSON.stringify(preOutput._context)
998
- : "";
999
- const avoidedTokens = 3200;
1000
- const deliveredTokens = Math.ceil(haltContent.length / 4);
1001
- tokenFlowWriter.record({
1002
- session_id: tokenFlowWriter.sessionId,
1003
- turn: stats.toolCallsLocal + 1,
1004
- mechanism: "behavior_automation",
1005
- tool: name,
1006
- tokens_without: avoidedTokens,
1007
- tokens_with: deliveredTokens,
1008
- tokens_saved: Math.max(0, avoidedTokens - deliveredTokens),
1009
- detail: { behavior: preOutput.behaviorId, action: "halted" },
1010
- });
1011
- }
1012
- return {
1013
- content: [
1014
- {
1015
- type: "text",
1016
- text: stringifyMcpToolJson(preOutput._context),
1017
- },
1018
- ],
1019
- _meta: { format: "json", ...(preOutput._meta ?? {}) },
1020
- ...(preOutput._context ? { _context: preOutput._context } : {}),
1021
- };
1022
- }
1023
- // Shadow ledger tools disabled — not exposed in tool definitions
1024
- // (unerr_mark_working, unerr_revert_to_working_state, unerr_get_timeline handlers removed)
1025
- // ── ST-2: Session-narrative marker tools ──
1026
- {
1027
- const { isMarkerTool, handleMarkerCall } = await import("../tools/intelligence/timeline-markers.js");
1028
- if (isMarkerTool(name)) {
1029
- if (!timelineHandle) {
1030
- process.stderr.write(`[unerr] ${name} called but timeline subsystem is disabled\n`);
1031
- return {
1032
- content: [
1033
- {
1034
- type: "text",
1035
- text: JSON.stringify({
1036
- error: "marker tools require timeline subsystem (UNERR_TIMELINE_V2!=0)",
1037
- }),
1038
- },
1039
- ],
1040
- isError: true,
1041
- };
1042
- }
1043
- let branchVal = "main";
1044
- let headShaVal = "";
1045
- try {
1046
- const { getCurrentBranch, getHeadSha } = await import("../utils/git.js");
1047
- branchVal = (await getCurrentBranch(process.cwd())) ?? branchVal;
1048
- headShaVal = (await getHeadSha(process.cwd())) ?? "";
1049
- }
1050
- catch {
1051
- /* defaults */
1052
- }
1053
- return handleMarkerCall(name, args, {
1054
- ledger: shadowLedger,
1055
- store: timelineHandle.store,
1056
- branch: branchVal,
1057
- headSha: headShaVal,
1058
- });
1059
- }
1060
- }
1061
- // ── Layer 9: record_fact + recall_facts ──
1062
- if (name === "record_fact" || name === "recall_facts") {
1063
- const factResult = name === "record_fact"
1064
- ? await handleRecordFactProxy(args, unerrDirForLedger, shadowLedger, {
1065
- tracker: effectivenessTracker,
1066
- turn: router.sessionContext.getToolCallCount(),
1067
- })
1068
- : await handleRecallFactsProxy(args, unerrDirForLedger, {
1069
- tracker: effectivenessTracker,
1070
- turn: router.sessionContext.getToolCallCount(),
1071
- });
1072
- const { applyWireCap: applyWireCapFact } = await import("./wire-cap.js");
1073
- const rawText = factResult.content?.[0]?.text;
1074
- let parsed = null;
1075
- if (rawText) {
1076
- try {
1077
- parsed = JSON.parse(rawText);
1078
- }
1079
- catch {
1080
- /* non-JSON, skip cap */
1081
- }
1082
- }
1083
- if (parsed) {
1084
- const { body: cappedBody, pageHint } = applyWireCapFact(name, parsed, args);
1085
- const pageBlock = pageHint ? `${pageHint}\n\n` : "";
1086
- // Forward isError so error responses from the fact handler reach
1087
- // the agent as failed tool calls, not as opaque JSON bodies.
1088
- return {
1089
- content: [
1090
- {
1091
- type: "text",
1092
- text: pageBlock + stringifyMcpToolJson(cappedBody),
1093
- },
1094
- ],
1095
- ...(factResult.isError ? { isError: true } : {}),
1096
- };
1097
- }
1098
- return factResult;
1099
- }
1100
- // Sprint 11: Deep Dive MCP tools — handle locally
1101
- if (localGraph) {
1102
- const { handleDeepDiveTool } = await import("../intelligence/deep-dive-tools.js");
1103
- const deepDiveResult = await handleDeepDiveTool(name, args, localGraph);
1104
- if (deepDiveResult) {
1105
- recordToolCall(stats);
1106
- recordLatency(stats.latency, 0);
1107
- pidLock.recordToolCall();
1108
- if (stats.localMode)
1109
- recordGraphQuery(stats.localMode, name);
1110
- const branch = branchContext?.currentBranch ?? "unknown";
1111
- const headSha = branchContext?.headSha ?? "";
1112
- shadowLedger.record(name, args, { tool: name, source: "local" }, branch, headSha);
1113
- return deepDiveResult;
1114
- }
1115
- }
1116
- // MCP tools: Layer 6 wire formats (columnar / json) are applied inside QueryRouter.execute.
1117
- // Wrap in try/catch so any throw lands as isError:true on the wire
1118
- // instead of the SDK's generic JSON-RPC error, which some clients
1119
- // surface less prominently than a tool-level error.
1120
- let result;
1121
- try {
1122
- result = await router.execute(name, args);
1123
- }
1124
- catch (err) {
1125
- const errMsg = err instanceof Error ? err.message : String(err);
1126
- process.stderr.write(`[unerr] router.execute(${name}) threw: ${errMsg}\n`);
1127
- return {
1128
- content: [
1129
- {
1130
- type: "text",
1131
- text: JSON.stringify({ error: errMsg, tool: name }),
1132
- },
1133
- ],
1134
- isError: true,
1135
- };
1136
- }
1137
- // Track session stats + latency
1138
- recordToolCall(stats);
1139
- recordLatency(stats.latency, result._meta.latency_ms);
1140
- pidLock.recordToolCall();
1141
- // Local Mode: track per-tool graph query counts
1142
- if (stats.localMode && result._meta.source === "local") {
1143
- recordGraphQuery(stats.localMode, name);
1144
- }
1145
- // Local Mode: track blast radius computations
1146
- if (stats.localMode && result._meta.entity_risk) {
1147
- recordBlastRadius(stats.localMode);
1148
- }
1149
- // Local Mode: track community context injections
1150
- if (stats.localMode && result._meta.community) {
1151
- recordCommunityContext(stats.localMode);
1152
- }
1153
- // Local Mode: accumulate latency advantage vs remote baseline (200ms baseline)
1154
- if (stats.localMode && result._meta.source === "local") {
1155
- recordLatencyAdvantage(stats.localMode, Math.max(0, 200 - result._meta.latency_ms));
1156
- }
1157
- if (result._meta.entity_risk?.risk_level === "high") {
1158
- recordRiskWarning(stats);
1159
- // Track chokepoint warning when blast radius is high
1160
- if ((result._meta.entity_risk?.fan_in ?? 0) > 10) {
1161
- recordChokepointWarning(stats);
1162
- }
1163
- }
1164
- // Track dead code references (fan_in=0 entities)
1165
- if (result._meta.entity_risk?.fan_in === 0) {
1166
- recordDeadCodeReference(stats);
1167
- }
1168
- // Track convention violations from check_rules results
1169
- if (name === "check_rules" && result.content != null) {
1170
- const checkResult = result.content;
1171
- const viols = checkResult.violations;
1172
- if (viols && viols.length > 0) {
1173
- void import("../server/event-bus.js").then(({ eventBus }) => {
1174
- eventBus.emit("violation", {
1175
- source: "check_rules",
1176
- count: viols.length,
1177
- rules: viols.slice(0, 24).map((v) => v.ruleKey),
1178
- });
1179
- });
1180
- for (let i = 0; i < viols.length; i++) {
1181
- recordViolation(stats);
1182
- }
1183
- }
1184
- }
1185
- // Track circular dependency detection from import analysis
1186
- if (name === "get_imports" && result.content != null) {
1187
- const imports = result.content;
1188
- if (Array.isArray(imports)) {
1189
- // Detect circular: file A imports B and B imports A
1190
- const importedFiles = new Set(imports.map((e) => e.imported_file));
1191
- const filePath = args?.file_path;
1192
- if (filePath) {
1193
- // Check if any imported file also imports this file
1194
- for (const target of importedFiles) {
1195
- if (target === filePath) {
1196
- recordCircularDep(stats);
1197
- break;
1198
- }
1199
- }
1200
- }
1201
- }
1202
- }
1203
- // Track signature preservation when drift shows modified entities
1204
- if (result._meta.drift?.entityStatus === "modified") {
1205
- recordSignaturePreservation(stats);
1206
- }
1207
- // Record in Shadow Ledger
1208
- const branch = branchContext?.currentBranch ?? "unknown";
1209
- const headSha = branchContext?.headSha ?? "";
1210
- const resultSummary = {
1211
- source: result._meta.source,
1212
- found: result.content != null,
1213
- };
1214
- if (Array.isArray(result.content)) {
1215
- resultSummary.count = result.content.length;
1216
- }
1217
- shadowLedger.record(name, args, resultSummary, branch, headSha);
1218
- // Sprint 4: Capture edit narratives as episodic facts
1219
- const NARRATIVE_EDIT_TOOLS = new Set([
1220
- "file_write",
1221
- "write_file",
1222
- "edit_file",
1223
- "str_replace_editor",
1224
- "Write",
1225
- "Edit",
1226
- ]);
1227
- if (narrativeCapture && NARRATIVE_EDIT_TOOLS.has(name)) {
1228
- const recentEntries = shadowLedger.getRecentEntries(10);
1229
- const lastEntry = recentEntries[recentEntries.length - 1];
1230
- if (lastEntry) {
1231
- setImmediate(() => narrativeCapture?.captureEditNarrative(lastEntry).catch(() => { }));
1232
- }
1233
- }
1234
- // Sprint 5: Periodic pattern analysis (every 20 tool calls)
1235
- patternAnalysisCallCount++;
1236
- if (patternAnalysisCallCount % 20 === 0 &&
1237
- proxyFactStore &&
1238
- shadowLedger) {
1239
- setImmediate(async () => {
1240
- try {
1241
- const { analyzeSessionPatterns } = await import("../intelligence/session-pattern-analyzer.js");
1242
- const entries = shadowLedger?.getRecentEntries(20);
1243
- await analyzeSessionPatterns({
1244
- ledgerEntries: entries,
1245
- factStore: proxyFactStore,
1246
- sessionId: shadowLedger?.getSessionId(),
1247
- });
1248
- }
1249
- catch {
1250
- /* non-critical */
1251
- }
1252
- });
1253
- }
1254
- // S7.1: Auto-snapshot trigger evaluation (post-tool-call)
1255
- try {
1256
- const { shouldAutoSnapshot } = await import("../tracking/auto-snapshot-triggers.js");
1257
- const fanInThreshold = result._meta.entity_risk?.fan_in ?? 0;
1258
- if (shouldAutoSnapshot(name, args, resultSummary, fanInThreshold > 8 ? fanInThreshold : undefined)) {
1259
- const snapshotBranch = branchContext?.currentBranch ?? "unknown";
1260
- const snapshotSha = branchContext?.headSha ?? "";
1261
- workingSnapshotStore.create({
1262
- commitSha: snapshotSha,
1263
- reason: `auto: ${name}`,
1264
- branch: snapshotBranch,
1265
- timelineBranch: workingSnapshotStore.getTimelineBranch(),
1266
- sessionId: shadowLedger.getSessionId(),
1267
- });
1268
- }
1269
- }
1270
- catch {
1271
- // Auto-snapshot is non-critical
1272
- }
1273
- // Task 6.3: Flag partial initialization on early responses
1274
- const meta = { ...result._meta };
1275
- if (!deferredInitComplete) {
1276
- meta.initialization = "partial";
1277
- }
1278
- // Inject session_resumed on first MCP response after resume
1279
- if (stats.isResumedSession && !resumeMetaEmitted) {
1280
- meta.session_resumed = true;
1281
- if (stats.previousSession) {
1282
- const prev = stats.previousSession;
1283
- meta.previous_session = {
1284
- tool_calls: prev.toolCallsLocal,
1285
- duration_minutes: prev.durationMinutes,
1286
- };
1287
- }
1288
- effectivenessTracker.recordSignalFired({
1289
- kind: "resume_injected",
1290
- signal_id: shadowLedger.getSessionId(),
1291
- entity_key: null,
1292
- turn: router.sessionContext.getToolCallCount(),
1293
- });
1294
- resumeMetaEmitted = true;
1295
- }
1296
- // ── Layer 4: Post-tool-use behavioral hooks ──
1297
- const postCtx = {
1298
- ...behaviorCtx,
1299
- result: result,
1300
- };
1301
- const postOutput = await behaviorDispatcher.firePostToolUse(postCtx);
1302
- let contextPayload = result._context ?? {};
1303
- if (postOutput?._context) {
1304
- contextPayload = { ...contextPayload, ...postOutput._context };
1305
- }
1306
- if (postOutput?._meta) {
1307
- Object.assign(meta, postOutput._meta);
1308
- }
1309
- // Tier-3: clients filter `_meta`/`_context`. Migrate anti-drift signals
1310
- // into body text. Wire-cap already ran inside QueryRouter (pre-format)
1311
- // and stashed page hint on meta._unerr_page_hint — consume it here.
1312
- const { buildSignalPrefix } = await import("./response-envelope.js");
1313
- const entityKey = args.entity_key ??
1314
- args.entity ??
1315
- args.key ??
1316
- args.name ??
1317
- args.file_path ??
1318
- null;
1319
- const signalFooter = buildSignalPrefix(meta, contextPayload, entityKey);
1320
- const pageHint = meta._unerr_page_hint;
1321
- const bodyText = typeof result.content === "string"
1322
- ? result.content
1323
- : stringifyMcpToolJson(result.content);
1324
- const pageBlock = pageHint ? `\n${pageHint}` : "";
1325
- const footerBlock = signalFooter ? `\n${signalFooter.trimEnd()}` : "";
1326
- const bodyEnd = bodyText.endsWith("\n") ? "" : "\n";
1327
- // Final assembly: data → page-hint → signal footer (signals trail).
1328
- const finalText = bodyText + bodyEnd + pageBlock + footerBlock;
1329
- return {
1330
- content: [{ type: "text", text: finalText }],
1331
- };
1332
- }));
1333
- const transport = new StdioServerTransport();
1334
- await server.connect(transport);
1335
- lifecycle.send({ type: "INDEX_COMPLETE" });
1336
- lifecycle.send({ type: "MCP_READY" });
1337
- // ── Step 7a-2: UDS Transport for Multi-Client (Task 7.2) ──────
1338
- const { TransportMux } = await import("./transport-mux.js");
1339
- const sockPath = join(stateDir, "proxy.sock");
1340
- const transportMux = new TransportMux(sockPath);
1341
- /** Map clientId → agent name (captured from MCP initialize handshake) */
1342
- const agentNameByClient = new Map();
1343
- // Sprint 10.5: Add custom HTTP handler for /commit-context (git trailer injection)
1344
- transportMux.setCustomHttpHandler("/commit-context", (_url) => {
1345
- // biome-ignore format: keep import() type on one line for TS compat
1346
- const { getCommitTrailers } = require("../tracking/git-trailers.js");
1347
- const branch = branchContext?.currentBranch ?? "unknown";
1348
- const timelineBranch = workingSnapshotStore.getTimelineBranch();
1349
- const trailers = getCommitTrailers(shadowLedger, timelineBranch, branch);
1350
- return JSON.stringify(trailers);
1351
- });
1352
- transportMux.setHandler(async (clientId, message) => {
1353
- // MCP protocol: handle initialize handshake for bridged clients
1354
- if (message.method === "initialize") {
1355
- // Capture agent name from clientInfo (e.g. "claude-code", "cursor")
1356
- const clientName = message.params?.clientInfo?.name;
1357
- if (clientName) {
1358
- agentNameByClient.set(clientId, clientName);
1359
- }
1360
- return {
1361
- jsonrpc: "2.0",
1362
- id: message.id,
1363
- result: {
1364
- protocolVersion: "2024-11-05",
1365
- capabilities: { tools: {} },
1366
- serverInfo: { name: "unerr-local", version: "0.1.0" },
1367
- },
1368
- };
1369
- }
1370
- // MCP protocol: acknowledge initialized notification
1371
- if (message.method === "notifications/initialized") {
1372
- // Notifications don't get responses, but we need to not error
1373
- return { jsonrpc: "2.0" };
1374
- }
1375
- if (message.method === "tools/list") {
1376
- return {
1377
- jsonrpc: "2.0",
1378
- result: { tools: await getInjectedTools() },
1379
- };
1380
- }
1381
- // ══════════════════════════════════════════════════════════════════
1382
- // UDS tools/call handler
1383
- //
1384
- // CRITICAL ARCHITECTURE NOTE: This handler MUST mirror the stdio
1385
- // handler's dispatch chain (see CallToolRequestSchema handler above).
1386
- //
1387
- // When `unerr --mcp` detects a running proxy, it bridges stdin/stdout
1388
- // to this UDS socket via bridge.ts. Tool calls from bridged IDEs
1389
- // arrive HERE, not at the stdio handler. Any tool intercepted before
1390
- // router.execute() in the stdio handler MUST also be intercepted here,
1391
- // otherwise it hits QueryRouter which returns "Unknown tool" because
1392
- // these tools are NOT in the LOCAL_TOOLS set.
1393
- //
1394
- // Tools that need interception (not in QueryRouter.LOCAL_TOOLS):
1395
- // - record_fact, recall_facts (Layer 9: temporal fact store)
1396
- // - unerr_mark_working (Shadow ledger: working snapshots)
1397
- // - unerr_revert_to_working_state (Shadow ledger: revert)
1398
- // - unerr_get_timeline (Shadow ledger: timeline view)
1399
- // - Deep dive tools (Sprint 11: handled by handleDeepDiveTool)
1400
- //
1401
- // When adding new tools to TOOL_DEFINITIONS, ensure they are ALSO
1402
- // dispatched here if they are not handled by QueryRouter.executeLocal().
1403
- // ══════════════════════════════════════════════════════════════════
1404
- if (message.method === "tools/call") {
1405
- const params = message.params;
1406
- if (!params?.name) {
1407
- return {
1408
- jsonrpc: "2.0",
1409
- error: { code: -32602, message: "Missing tool name" },
1410
- };
1411
- }
1412
- const { name, arguments: toolArgs = {} } = params;
1413
- // ── Boundary validation (mirrors stdio handler) ──
1414
- // Bridged IDE clients via `unerr --mcp` hit THIS handler, not the
1415
- // stdio one. Forgetting to mirror lets the silent-failure pattern
1416
- // resurface for every IDE user. See arg-validator + the stdio dup.
1417
- const udsValidationFailure = runBoundaryValidation(name, toolArgs);
1418
- if (udsValidationFailure) {
1419
- process.stderr.write(`[unerr] tools/call validation failed for ${name} (uds): ${JSON.stringify(udsValidationFailure)}\n`);
1420
- return {
1421
- jsonrpc: "2.0",
1422
- result: {
1423
- content: [
1424
- { type: "text", text: JSON.stringify(udsValidationFailure) },
1425
- ],
1426
- isError: true,
1427
- },
1428
- };
1429
- }
1430
- // Track tool usage for semantic cluster reordering (mirrors stdio handler)
1431
- toolUsageTracker.record(name);
1432
- // ── ST-2: Session-narrative marker tools (UDS path) ──
1433
- {
1434
- const { isMarkerTool, handleMarkerCall } = await import("../tools/intelligence/timeline-markers.js");
1435
- if (isMarkerTool(name)) {
1436
- if (!timelineHandle) {
1437
- process.stderr.write(`[unerr] ${name} called but timeline subsystem is disabled (uds)\n`);
1438
- return {
1439
- jsonrpc: "2.0",
1440
- result: {
1441
- content: [
1442
- {
1443
- type: "text",
1444
- text: JSON.stringify({
1445
- error: "marker tools require timeline subsystem (UNERR_TIMELINE_V2!=0)",
1446
- }),
1447
- },
1448
- ],
1449
- isError: true,
1450
- },
1451
- };
1452
- }
1453
- let branchVal = "main";
1454
- let headShaVal = "";
1455
- try {
1456
- const { getCurrentBranch, getHeadSha } = await import("../utils/git.js");
1457
- branchVal = (await getCurrentBranch(process.cwd())) ?? branchVal;
1458
- headShaVal = (await getHeadSha(process.cwd())) ?? "";
1459
- }
1460
- catch {
1461
- /* defaults */
1462
- }
1463
- const markerRes = await handleMarkerCall(name, toolArgs, {
1464
- ledger: shadowLedger,
1465
- store: timelineHandle.store,
1466
- branch: branchVal,
1467
- headSha: headShaVal,
1468
- });
1469
- return { jsonrpc: "2.0", result: markerRes };
1470
- }
1471
- }
1472
- // ── Layer 9: record_fact + recall_facts (independent of graph) ──
1473
- if (name === "record_fact" || name === "recall_facts") {
1474
- const factResult = name === "record_fact"
1475
- ? await handleRecordFactProxy(toolArgs, unerrDirForLedger, shadowLedger, {
1476
- tracker: effectivenessTracker,
1477
- turn: router.sessionContext.getToolCallCount(),
1478
- })
1479
- : await handleRecallFactsProxy(toolArgs, unerrDirForLedger, {
1480
- tracker: effectivenessTracker,
1481
- turn: router.sessionContext.getToolCallCount(),
1482
- });
1483
- // Apply universal pagination cap so recall_facts surfaces page hints
1484
- // when more facts are available beyond what the handler returned.
1485
- const { applyWireCap: applyWireCapFact } = await import("./wire-cap.js");
1486
- const rawText = factResult.content?.[0]?.text;
1487
- let parsed = null;
1488
- if (rawText) {
1489
- try {
1490
- parsed = JSON.parse(rawText);
1491
- }
1492
- catch {
1493
- /* non-JSON, skip cap */
1494
- }
1495
- }
1496
- if (parsed) {
1497
- const { body: cappedBody, pageHint } = applyWireCapFact(name, parsed, toolArgs);
1498
- const pageBlock = pageHint ? `${pageHint}\n\n` : "";
1499
- // Forward isError so error responses from the fact handler reach
1500
- // the agent as failed tool calls (UDS path mirrors stdio).
1501
- return {
1502
- jsonrpc: "2.0",
1503
- result: {
1504
- content: [
1505
- {
1506
- type: "text",
1507
- text: pageBlock + stringifyMcpToolJson(cappedBody),
1508
- },
1509
- ],
1510
- ...(factResult.isError ? { isError: true } : {}),
1511
- },
1512
- };
1513
- }
1514
- return { jsonrpc: "2.0", result: factResult };
1515
- }
1516
- // Shadow ledger tools disabled — not exposed in tool definitions
1517
- // (unerr_mark_working, unerr_revert_to_working_state, unerr_get_timeline handlers removed)
1518
- // ── Sprint 11: Deep Dive MCP tools (handled outside QueryRouter) ──
1519
- if (localGraph) {
1520
- const { handleDeepDiveTool } = await import("../intelligence/deep-dive-tools.js");
1521
- const deepDiveResult = await handleDeepDiveTool(name, toolArgs, localGraph);
1522
- if (deepDiveResult) {
1523
- recordToolCall(stats);
1524
- recordLatency(stats.latency, 0);
1525
- pidLock.recordToolCall();
1526
- if (stats.localMode)
1527
- recordGraphQuery(stats.localMode, name);
1528
- const branch = branchContext?.currentBranch ?? "unknown";
1529
- const headSha = branchContext?.headSha ?? "";
1530
- shadowLedger.record(name, toolArgs, { tool: name, source: "local", client: clientId }, branch, headSha);
1531
- return { jsonrpc: "2.0", result: deepDiveResult };
1532
- }
1533
- }
1534
- // ── All remaining tools: QueryRouter.execute (graph-backed) ──
1535
- // Tools in QueryRouter.LOCAL_TOOLS: get_entity, get_file, get_references,
1536
- // get_imports, search_code, get_rules, get_business_context, get_conventions,
1537
- // file_read, file_outline, and deep dive blueprint tools.
1538
- // Wrap in try/catch so any throw lands as isError:true (UDS mirror of stdio).
1539
- let result;
1540
- try {
1541
- result = await router.execute(name, toolArgs);
1542
- }
1543
- catch (err) {
1544
- const errMsg = err instanceof Error ? err.message : String(err);
1545
- process.stderr.write(`[unerr] router.execute(${name}) threw (uds): ${errMsg}\n`);
1546
- return {
1547
- jsonrpc: "2.0",
1548
- result: {
1549
- content: [
1550
- {
1551
- type: "text",
1552
- text: JSON.stringify({ error: errMsg, tool: name }),
1553
- },
1554
- ],
1555
- isError: true,
1556
- },
1557
- };
1558
- }
1559
- // Track stats from UDS clients the same way as stdio clients
1560
- recordToolCall(stats);
1561
- recordLatency(stats.latency, result._meta.latency_ms);
1562
- pidLock.recordToolCall();
1563
- if (stats.localMode && result._meta.source === "local") {
1564
- recordGraphQuery(stats.localMode, name);
1565
- }
1566
- if (stats.localMode && result._meta.entity_risk) {
1567
- recordBlastRadius(stats.localMode);
1568
- }
1569
- if (stats.localMode && result._meta.source === "local") {
1570
- recordLatencyAdvantage(stats.localMode, Math.max(0, 200 - result._meta.latency_ms));
1571
- }
1572
- // Record in Shadow Ledger with client-specific session context
1573
- const branch = branchContext?.currentBranch ?? "unknown";
1574
- const headSha = branchContext?.headSha ?? "";
1575
- shadowLedger.record(name, toolArgs, {
1576
- source: result._meta.source,
1577
- found: result.content != null,
1578
- client: clientId,
1579
- }, branch, headSha);
1580
- // Tier-3: _meta/_context stripped. Wire-cap ran in QueryRouter and
1581
- // stashed any pageHint on meta._unerr_page_hint — consume it here.
1582
- const { buildSignalPrefix: buildSignalPrefix2 } = await import("./response-envelope.js");
1583
- const entityKey2 = toolArgs.entity_key ??
1584
- toolArgs.entity ??
1585
- toolArgs.key ??
1586
- toolArgs.name ??
1587
- toolArgs.file_path ??
1588
- null;
1589
- const signalFooter2 = buildSignalPrefix2(result._meta, result._context, entityKey2);
1590
- const pageHint2 = result._meta
1591
- ._unerr_page_hint;
1592
- const bodyText2 = typeof result.content === "string"
1593
- ? result.content
1594
- : stringifyMcpToolJson(result.content);
1595
- const pageBlock2 = pageHint2 ? `\n${pageHint2}` : "";
1596
- const footerBlock2 = signalFooter2 ? `\n${signalFooter2.trimEnd()}` : "";
1597
- const bodyEnd2 = bodyText2.endsWith("\n") ? "" : "\n";
1598
- return {
1599
- jsonrpc: "2.0",
1600
- result: {
1601
- content: [
1602
- {
1603
- type: "text",
1604
- text: bodyText2 + bodyEnd2 + pageBlock2 + footerBlock2,
1605
- },
1606
- ],
1607
- },
1608
- };
1609
- }
1610
- return {
1611
- jsonrpc: "2.0",
1612
- error: { code: -32601, message: `Method not found: ${message.method}` },
1613
- };
1614
- });
1615
- transportMux.start();
1616
- // ── Step 7a-3: HTTP Transport (Task 5.3) ────────────────────────
1617
- let httpTransportHandle = null;
1618
- if (opts.httpPort && opts.httpPort > 0) {
1619
- try {
1620
- const { startHttpTransport } = await import("./http-transport.js");
1621
- httpTransportHandle = await startHttpTransport({
1622
- port: opts.httpPort,
1623
- mcpServer: server,
1624
- log: log.info,
1625
- });
1626
- log.info(`HTTP transport ready on port ${httpTransportHandle.port}`);
1627
- }
1628
- catch (err) {
1629
- log.warn(`HTTP transport failed to start: ${err instanceof Error ? err.message : String(err)}`);
1630
- }
1631
- }
1632
- // ── Step 7b: Branch Context + Drift Tracker ────────────────────
1633
- const { computeBranchContextAsync, getCurrentBranch, startBranchPoller } = await import("../tracking/branch-context.js");
1634
- branchContext = await computeBranchContextAsync();
1635
- router.setBranchContext(branchContext);
1636
- let _driftTracker = null;
1637
- let stopBranchPoller = null;
1638
- // L11.4: DriftTracker initialization extracted into a function.
1639
- // Called immediately when snapshot is loaded, or deferred to onComplete when background indexing.
1640
- async function initDriftTracker() {
1641
- if (!localGraph || repoIds.length === 0)
1642
- return;
1643
- try {
1644
- const { DriftTracker } = await import("../tracking/drift-tracker.js");
1645
- const { FileHashManager } = await import("../tracking/file-hash-state.js");
1646
- const unerrDir = join(process.cwd(), ".unerr");
1647
- const fileHashManager = new FileHashManager(unerrDir);
1648
- _driftTracker = new DriftTracker({ projectRoot: process.cwd(), repoId: repoIds[0], unerrDir }, localGraph, fileHashManager);
1649
- // L9.4: Wire DriftTracker into QueryRouter for sync_local_diff overlay writes
1650
- router.setDriftTracker(_driftTracker);
1651
- const { eventBus } = await import("../server/event-bus.js");
1652
- _driftTracker.setDriftEventSink((payload) => {
1653
- eventBus.emit("drift", payload);
1654
- });
1655
- // L2.5: Swap-on-idle graph rebuild via GraphHolder.
1656
- // DriftTracker notifies GraphHolder of file changes → idle timer → full rebuild
1657
- // into a fresh CozoDB instance → atomic swap to all consumers.
1658
- const { GraphHolder } = await import("../intelligence/graph-holder.js");
1659
- const { indexLocalProject } = await import("../intelligence/local-indexer.js");
1660
- const repoId = repoIds[0];
1661
- const cwd = process.cwd();
1662
- const graphHolder = new GraphHolder(localGraph);
1663
- // Factory: reindexes into the existing persistent graph.
1664
- // CozoDB :put is upsert — data stays queryable during rebuild.
1665
- // Orphan cleanup at end of indexLocalProject removes stale entities.
1666
- graphHolder.setRebuildFactory(async () => {
1667
- const result = await indexLocalProject(cwd, localGraph, repoId);
1668
- return { graph: localGraph, result };
1669
- });
1670
- // Incremental factory — processes only changed files, no full reindex.
1671
- const { indexFilesIncremental } = await import("../intelligence/incremental-indexer.js");
1672
- graphHolder.setIncrementalFactory(async (changedFiles) => {
1673
- return indexFilesIncremental(cwd, changedFiles, localGraph, repoId);
1674
- });
1675
- // Swap callbacks — propagate new graph to all consumers
1676
- graphHolder.onSwap((newGraph) => {
1677
- router.swapGraph(newGraph);
1678
- });
1679
- graphHolder.onSwap((newGraph) => {
1680
- _driftTracker?.swapGraph(newGraph);
1681
- });
1682
- // Behaviors
1683
- graphHolder.onSwap((newGraph) => {
1684
- cascadeGuard.attachGraph(newGraph);
1685
- incompleteWork.attachGraph(newGraph);
1686
- conventionDrift.attachGraph(newGraph);
1687
- autoDoc.attachGraph(newGraph);
1688
- architectureGuard.attachGraph(newGraph);
1689
- });
1690
- // NOTE: DriftTracker → GraphHolder notification intentionally NOT wired.
1691
- // The NativeWatcher below directly notifies GraphHolder with file paths,
1692
- // so a DriftTracker bridge would cause double-fire (duplicate incremental runs).
1693
- // Wire NativeWatcher to detect file changes (both LLM tool writes and user edits).
1694
- // Feeds into DriftTracker (overlay updates) + GraphHolder (idle timer for rebuild).
1695
- const { createNativeWatcher } = await import("../tracking/native-watcher.js");
1696
- const { filterIndexableEvents } = await import("../intelligence/indexer/watch-integration.js");
1697
- const nativeWatcher = createNativeWatcher({
1698
- projectRoot: cwd,
1699
- debounceMs: 100,
1700
- onEvents: (events) => {
1701
- const indexable = filterIndexableEvents(events);
1702
- if (indexable.length === 0)
1703
- return;
1704
- // Notify GraphHolder of file change (resets idle timer, tracks paths for incremental)
1705
- graphHolder.notifyFileChange(indexable);
1706
- // Feed into DriftTracker for overlay updates
1707
- const headSha = branchContext?.headSha ?? "unknown";
1708
- _driftTracker
1709
- ?.processFiles(indexable, headSha)
1710
- .catch((err) => {
1711
- process.stderr.write(`⚠ [watcher] Drift processing failed: ${formatUnknownError(err)}\n`);
1712
- });
1713
- },
1714
- });
1715
- nativeWatcher.start().catch((err) => {
1716
- process.stderr.write(`⚠ [watcher] File watcher failed to start: ${err instanceof Error ? err.message : String(err)}\n`);
1717
- });
1718
- // Initialize branch snapshot manager (Task 6.2)
1719
- const branchSnapshots = _driftTracker.initBranchSnapshots();
1720
- // GC snapshots for deleted branches on startup
1721
- branchSnapshots.garbageCollect();
1722
- // Start branch poller — save/restore overlay on switch
1723
- let _previousBranch = getCurrentBranch() ?? "unknown";
1724
- stopBranchPoller = startBranchPoller((newBranch, newContext) => {
1725
- log.info(`Branch switch detected: ${_previousBranch} → ${newBranch}`);
1726
- router.setBranchContext(newContext);
1727
- const prev = _previousBranch;
1728
- _previousBranch = newBranch;
1729
- _driftTracker
1730
- ?.onBranchSwitch([], newContext.headSha, prev, newBranch)
1731
- .catch((err) => {
1732
- log.warn(`Branch switch drift failed: ${err instanceof Error ? err.message : String(err)}`);
1733
- });
1734
- });
1735
- }
1736
- catch (err) {
1737
- log.warn(`Drift tracker not available: ${err instanceof Error ? err.message : String(err)}`);
1738
- }
1739
- }
1740
- // ── Step 7b-2: Background Indexing + ora Spinner (L11.1/L11.3) ──
1741
- if (needsBackgroundIndex && localGraph) {
1742
- const { BackgroundIndexer } = await import("../intelligence/background-indexer.js");
1743
- const bgIndexer = new BackgroundIndexer();
1744
- // Wire into router for partial graph responses (L11.2)
1745
- router.setBackgroundIndexer(bgIndexer);
1746
- // ora spinner on stderr — never touches stdout (MCP JSON-RPC only)
1747
- const ora = (await import("ora")).default;
1748
- const spinner = ora({
1749
- text: "Indexing project...",
1750
- stream: process.stderr,
1751
- }).start();
1752
- const localRepoId = repoIds[0];
1753
- bgIndexer.start(process.cwd(), localGraph, localRepoId,
1754
- // onComplete
1755
- async (result) => {
1756
- // L4.1: Record indexing stats for Local Mode proof
1757
- if (stats.localMode) {
1758
- recordIndexingResult(stats.localMode, result);
1759
- }
1760
- spinner.succeed("Deep index complete");
1761
- startupLog.graphLoaded({
1762
- entities: result.entityCount,
1763
- edges: result.edgeCount,
1764
- files: result.fileCount,
1765
- communities: result.communityCount,
1766
- patterns: 0,
1767
- rules: 0,
1768
- ms: result.elapsedMs,
1769
- });
1770
- // Compute health grade now that the graph is populated
1771
- try {
1772
- const { computeHealthGrade } = await import("../intelligence/health-grade.js");
1773
- healthResult = await computeHealthGrade(localGraph.db);
1774
- if (healthResult) {
1775
- router.setHealthInfo(healthResult.grade, {
1776
- entities: healthResult.totalEntities,
1777
- edges: healthResult.totalEdges,
1778
- rules: healthResult.totalRules,
1779
- });
1780
- // startupLog.healthCard(healthResult); // Disabled until health metrics verified against drift state
1781
- }
1782
- }
1783
- catch (err) {
1784
- log.warn(`Health grade failed: ${err instanceof Error ? err.message : String(err)}`);
1785
- }
1786
- // Show MCP connection card with config snippet for manual agent setup
1787
- try {
1788
- const { AGENT_REGISTRY } = await import("../config/agent-registry.js");
1789
- const fs = await import("node:fs");
1790
- const pathMod = await import("node:path");
1791
- const projectDir = process.cwd();
1792
- const configured = AGENT_REGISTRY.filter((a) => fs.existsSync(pathMod.join(projectDir, a.projectConfigPath))).map((a) => a.name);
1793
- startupLog.mcpConnectionCard(configured, projectDir);
1794
- }
1795
- catch (err) {
1796
- log.warn(`MCP connection card failed: ${err instanceof Error ? err.message : String(err)}`);
1797
- }
1798
- // L11.4: Start DriftTracker ONLY after initial indexing completes (TL-31)
1799
- initDriftTracker().catch((err) => {
1800
- log.warn(`Post-index DriftTracker init failed: ${err instanceof Error ? err.message : String(err)}`);
1801
- });
1802
- // Layer 9: Generate temporal facts from detected conventions after reindex
1803
- try {
1804
- const factStoreForGen = await getProxyFactStore(unerrDirForLedger);
1805
- if (factStoreForGen) {
1806
- const { detectLocalConventions } = await import("../intelligence/local-convention-detector.js");
1807
- const { generateFromConventions } = await import("../intelligence/fact-generator.js");
1808
- const detection = await detectLocalConventions(localGraph.db);
1809
- if (detection.conventions.length > 0) {
1810
- const convResult = await generateFromConventions(factStoreForGen, detection.conventions);
1811
- if (convResult.created > 0 || convResult.reinforced > 0) {
1812
- log.info(`Fact generator: ${convResult.created} convention facts created, ${convResult.reinforced} reinforced`);
1813
- }
1814
- }
1815
- // Also run session analysis pipeline
1816
- const { runFactGenerationPipeline } = await import("../intelligence/fact-generator.js");
1817
- const pipelineResults = await runFactGenerationPipeline(factStoreForGen, unerrDirForLedger);
1818
- for (const r of pipelineResults) {
1819
- if (r.created > 0 || r.reinforced > 0) {
1820
- log.info(`Fact generator [${r.source}]: ${r.created} created, ${r.reinforced} reinforced`);
1821
- }
1822
- }
1823
- }
1824
- }
1825
- catch {
1826
- // Non-critical — fact generation failure doesn't block operation
1827
- }
1828
- },
1829
- // onError
1830
- (err) => {
1831
- spinner.fail(`Indexing failed: ${err.message}`);
1832
- process.stderr.write(" MCP continues with partial graph. Run 'unerr' again to retry.\n");
1833
- });
1834
- // Update spinner with progress every 200ms
1835
- const progressInterval = setInterval(() => {
1836
- if (!bgIndexer.isIndexing()) {
1837
- clearInterval(progressInterval);
1838
- return;
1839
- }
1840
- const p = bgIndexer.getProgress();
1841
- const shortFile = p.currentFile
1842
- ? p.currentFile.length > 40
1843
- ? `...${p.currentFile.slice(-37)}`
1844
- : p.currentFile
1845
- : "";
1846
- spinner.text = `${p.phase}: ${p.processed}/${p.total} (${p.pct}%) ${shortFile}`;
1847
- }, 200);
1848
- }
1849
- else {
1850
- // No background indexing needed — start DriftTracker immediately
1851
- await initDriftTracker();
1852
- }
1853
- // ── Step 7c: Commit Watcher + Manifest ───────────────────────────
1854
- const { WorkspaceManifest } = await import("../tracking/workspace-manifest.js");
1855
- const workspaceManifest = repoIds[0]
1856
- ? new WorkspaceManifest(join(process.cwd(), ".unerr"), repoIds[0], shadowLedger.getSessionId())
1857
- : null;
1858
- const { CommitWatcher } = await import("../tracking/commit-watcher.js");
1859
- const commitWatcher = new CommitWatcher(intentCorrelator, {
1860
- cwd: process.cwd(),
1861
- sessionId: shadowLedger.getSessionId(),
1862
- onCommit: (_sha, _files, associated) => {
1863
- if (associated > 0) {
1864
- // Record attributions in manifest for committed correlations
1865
- if (workspaceManifest) {
1866
- const committed = intentCorrelator.getCommittedUnflushed();
1867
- const branch = branchContext?.currentBranch ?? "unknown";
1868
- for (const correlation of committed) {
1869
- workspaceManifest.recordAttribution(correlation, branch);
1870
- }
1871
- }
1872
- }
1873
- },
1874
- });
1875
- // Set branch context + drift summary for git note encoding (Task 8.1)
1876
- if (branchContext) {
1877
- commitWatcher.setBranchContext(branchContext);
1878
- }
1879
- if (localGraph) {
1880
- commitWatcher.setDriftSummaryFn(async () => {
1881
- const s = await localGraph.getDriftSummary();
1882
- return { added: s.added, modified: s.modified, deleted: s.deleted };
1883
- });
1884
- }
1885
- commitWatcher.start();
1886
- if (proxyMode === "parse") {
1887
- const parseStats = parseIndex?.getStats();
1888
- startup.addStep("MCP ready", "done", `PARSE mode (${parseStats?.entityCount ?? 0} entities)`);
1889
- log.info(`MCP server running on stdio — PARSE mode (${parseStats?.entityCount ?? 0} entities from ${parseStats?.fileCount ?? 0} files)`);
1890
- }
1891
- else {
1892
- const localToolCount = 14;
1893
- const rules = localGraph?.hasRules() ? await localGraph.getRules() : null;
1894
- const ruleInfo = rules ? ` (${rules.length} rules loaded)` : "";
1895
- startup.addStep("MCP ready", "done", `${localToolCount} local tools ready`);
1896
- startup.setToolCount(localToolCount);
1897
- startupLog.toolsReady(localToolCount, rules?.length ?? 0);
1898
- startupLog.ready(localToolCount, proxyMode);
1899
- }
1900
- // Finalize startup display (Act 3)
1901
- startup.setReady(proxyMode);
1902
- startup.unmount();
1903
- // ── Task 6.3: Deferred Initialization ─────────────────────────────
1904
- // These run after MCP is already serving — first few tool calls may
1905
- // lack health/PARSE data, which is acceptable (flagged via _meta.initialization).
1906
- // Deferred: Health grade computation (non-PARSE mode)
1907
- // Skip if background index is running — graph is empty until indexing completes.
1908
- // In that case, health grade is computed in the bgIndexer onComplete callback.
1909
- if (localGraph && proxyMode !== "parse" && !needsBackgroundIndex) {
1910
- try {
1911
- const { computeHealthGrade } = await import("../intelligence/health-grade.js");
1912
- healthResult = await computeHealthGrade(localGraph.db);
1913
- if (healthResult) {
1914
- router.setHealthInfo(healthResult.grade, {
1915
- entities: healthResult.totalEntities,
1916
- edges: healthResult.totalEdges,
1917
- rules: healthResult.totalRules,
1918
- });
1919
- // startupLog.healthCard(healthResult); // Disabled until health metrics verified against drift state
1920
- }
1921
- }
1922
- catch (err) {
1923
- log.warn(`Health grade failed: ${err instanceof Error ? err.message : String(err)}`);
1924
- }
1925
- // Show MCP connection card on resume (matches first-run path)
1926
- try {
1927
- const { AGENT_REGISTRY } = await import("../config/agent-registry.js");
1928
- const fs = await import("node:fs");
1929
- const pathMod = await import("node:path");
1930
- const projectDir = process.cwd();
1931
- const configured = AGENT_REGISTRY.filter((a) => fs.existsSync(pathMod.join(projectDir, a.projectConfigPath))).map((a) => a.name);
1932
- startupLog.mcpConnectionCard(configured, projectDir);
1933
- }
1934
- catch (err) {
1935
- log.warn(`MCP connection card failed: ${err instanceof Error ? err.message : String(err)}`);
1936
- }
1937
- }
1938
- // Deferred: PARSE mode entity indexing
1939
- if (proxyMode === "parse" && parseIndex) {
1940
- try {
1941
- const { extractEntitiesFromSource } = await import("./auto-bootstrap.js");
1942
- const allFiles = readdirSync(process.cwd(), {
1943
- recursive: true,
1944
- encoding: "utf-8",
1945
- });
1946
- const sourceFiles = allFiles.filter((f) => {
1947
- if (!f.match(/\.(ts|tsx|js|jsx)$/))
1948
- return false;
1949
- if (f.includes("node_modules"))
1950
- return false;
1951
- if (f.startsWith("dist/") ||
1952
- f.startsWith(".git/") ||
1953
- f.includes("/dist/"))
1954
- return false;
1955
- if (f.includes("coverage/"))
1956
- return false;
1957
- return true;
1958
- });
1959
- for (const file of sourceFiles.slice(0, 500)) {
1960
- try {
1961
- const content = readFileSync(join(process.cwd(), file), "utf-8");
1962
- const entities = extractEntitiesFromSource(file, content);
1963
- parseIndex.addEntities(entities);
1964
- }
1965
- catch {
1966
- /* skip unreadable files */
1967
- }
1968
- }
1969
- const indexStats = parseIndex.getStats();
1970
- log.info(`PARSE index: ${indexStats.entityCount} entities from ${indexStats.fileCount} files`);
1971
- }
1972
- catch (err) {
1973
- log.warn(`PARSE indexing failed: ${err instanceof Error ? err.message : String(err)}`);
1974
- }
1975
- }
1976
- // Task 6.5: Pre-load tree-sitter WASM grammars (non-blocking)
1977
- try {
1978
- const { preloadGrammars } = await import("../intelligence/ast-extractor.js");
1979
- await preloadGrammars();
1980
- log.info("Tree-sitter WASM grammars pre-loaded");
1981
- }
1982
- catch (err) {
1983
- log.warn(`Tree-sitter grammar pre-load failed (regex fallback active): ${err instanceof Error ? err.message : String(err)}`);
1984
- }
1985
- deferredInitComplete = true;
1986
- log.info("Deferred initialization complete");
1987
- // ── Log Tailer: relay logs from exec/--mcp child processes ────────
1988
- const { startLogTailer } = await import("./log-tailer.js");
1989
- const logTailer = startLogTailer(process.cwd(), {
1990
- // RC4 fix: Ingest child process token-flow events into proxy's writer
1991
- // so they appear in SSE streams and in-memory aggregations
1992
- onTokenFlowEvent: (entry) => {
1993
- if (tokenFlowWriter) {
1994
- try {
1995
- tokenFlowWriter.ingestExternal(entry);
1996
- }
1997
- catch {
1998
- /* best effort */
1999
- }
2000
- }
2001
- },
2002
- });
2003
- // ── Step 7c-2: Auto-configure git notes push (Task 8.7) ──────────
2004
- try {
2005
- const { gitQuery: gitQ, gitExec: gitE } = await import("../utils/exec.js");
2006
- const notesPush = (await gitQ(["config", "--local", "--get-all", "notes.push"], process.cwd())) ?? "";
2007
- if (!notesPush.includes("refs/notes/unerr")) {
2008
- await gitE(["config", "--local", "--add", "notes.push", "refs/notes/unerr"], { cwd: process.cwd() });
2009
- log.info("Auto-configured git notes push for intent tracking");
2010
- }
2011
- }
2012
- catch {
2013
- // Non-critical — notes just won't auto-push
2014
- }
2015
- // ── Step 7c-2b: Git Trailer Hook (Sprint 10.5) ───────────────────
2016
- try {
2017
- const { installPrepareCommitMsgHook } = await import("../tracking/git-trailers.js");
2018
- installPrepareCommitMsgHook(process.cwd());
2019
- }
2020
- catch {
2021
- // Non-critical
2022
- }
2023
- // ── Step 7d: Periodic stats snapshot (for `unerr status`) ─────
2024
- const { writeFileSync: writeStatsFile } = await import("node:fs");
2025
- const { computePercentiles } = await import("./session-stats.js"); // same dir
2026
- const statsSnapshotPath = join(stateDir, "session_stats.json");
2027
- const statsSnapshotInterval = setInterval(() => {
2028
- try {
2029
- const total = stats.toolCallsLocal;
2030
- if (total === 0)
2031
- return;
2032
- const localP = computePercentiles(stats.latency.localSamples, stats.latency.localTotalSamples);
2033
- const snapshot = {
2034
- pid: process.pid,
2035
- sessionStartedAt: stats.sessionStartedAt,
2036
- toolCallsLocal: stats.toolCallsLocal,
2037
- violationsCaught: stats.violationsCaught,
2038
- riskWarningsIssued: stats.riskWarningsIssued,
2039
- latency: {
2040
- local: localP
2041
- ? {
2042
- p50: localP.p50,
2043
- p95: localP.p95,
2044
- p99: localP.p99,
2045
- count: localP.count,
2046
- }
2047
- : null,
2048
- },
2049
- updatedAt: new Date().toISOString(),
2050
- };
2051
- writeStatsFile(statsSnapshotPath, JSON.stringify(snapshot, null, 2), "utf-8");
2052
- }
2053
- catch {
2054
- /* non-critical */
2055
- }
2056
- }, 10_000); // every 10s
2057
- // ── Step 7d: Layer 7 Dashboard HTTP Server ──────────────────────
2058
- // Non-blocking: runs after MCP is ready, failure doesn't affect proxy.
2059
- let dashboardHandle = null;
2060
- try {
2061
- const { startDashboardServer } = await import("../server/http.js");
2062
- const { detectIde: detectIdeDashboard } = await import("../utils/detect.js");
2063
- const ideType = await detectIdeDashboard(process.cwd());
2064
- const unerrDirForApi = join(process.cwd(), ".unerr");
2065
- dashboardHandle = await startDashboardServer({
2066
- system: {
2067
- stats,
2068
- cwd: process.cwd(),
2069
- dashboardPort: 0, // Resolved during port scan
2070
- startedAt: stats.sessionStartedAt,
2071
- ide: ideType,
2072
- getGraphStats: async () => {
2073
- if (!localGraph)
2074
- return { entities: 0, edges: 0, rules: 0 };
2075
- const projectStats = await localGraph.getLocalProjectStats();
2076
- return {
2077
- entities: projectStats.entityCount,
2078
- edges: projectStats.edgeCount,
2079
- rules: projectStats.ruleCount,
2080
- };
2081
- },
2082
- },
2083
- intelligence: {
2084
- localGraph,
2085
- cwd: process.cwd(),
2086
- unerrDir: unerrDirForApi,
2087
- getRecentLedgerEntries: (limit) => shadowLedger.getRecentEntries(limit),
2088
- getHealthGrade: async () => {
2089
- if (healthResult)
2090
- return healthResult;
2091
- if (!localGraph || proxyMode === "parse")
2092
- return null;
2093
- try {
2094
- const { computeHealthGrade } = await import("../intelligence/health-grade.js");
2095
- return await computeHealthGrade(localGraph.db);
2096
- }
2097
- catch {
2098
- return null;
2099
- }
2100
- },
2101
- getSignalStats: () => router.getSignalStats(),
2102
- },
2103
- session: {
2104
- stats,
2105
- getEfficiencySnapshot: () => router.getEfficiencySnapshot(),
2106
- getIntentGroups: () => router.getIntentGroups(),
2107
- getRecentLedgerEntries: (limit) => shadowLedger.getRecentEntries(limit),
2108
- },
2109
- stream: { stats },
2110
- stateDir,
2111
- apiOnly: !!opts.daemonChild,
2112
- tokenFlow: {
2113
- unerrDir: unerrDirForApi,
2114
- getTokenFlowWriter: () => tokenFlowWriter,
2115
- getAgentName: (_sessionId) => {
2116
- // Return most recent connected agent name (proxy has one active session)
2117
- const last = [...agentNameByClient.values()].pop();
2118
- return last ?? server.getClientVersion?.()?.name ?? undefined;
2119
- },
2120
- },
2121
- reasoningQuality: {
2122
- unerrDir: unerrDirForApi,
2123
- getTokenFlowWriter: () => tokenFlowWriter,
2124
- getAgentName: (_sessionId) => {
2125
- const last = [...agentNameByClient.values()].pop();
2126
- return last ?? server.getClientVersion?.()?.name ?? undefined;
2127
- },
2128
- },
2129
- timeline: timelineHandle
2130
- ? {
2131
- store: timelineHandle.store,
2132
- getRecentLedgerEntries: (limit) => shadowLedger.getRecentEntries(limit),
2133
- }
2134
- : undefined,
2135
- temporal: await (async () => {
2136
- try {
2137
- const { TemporalFactStore } = await import("../intelligence/temporal-facts.js");
2138
- const { readdirSync, readFileSync } = await import("node:fs");
2139
- const factStore = await TemporalFactStore.create(process.cwd());
2140
- return {
2141
- factStore,
2142
- loadRecentSessions: (limit) => {
2143
- try {
2144
- const sessDir = join(unerrDirForApi, "sessions");
2145
- const files = readdirSync(sessDir)
2146
- .filter((f) => f.endsWith(".jsonl"))
2147
- .sort()
2148
- .slice(-limit);
2149
- return files.map((f) => {
2150
- const content = readFileSync(join(sessDir, f), "utf-8")
2151
- .trim()
2152
- .split("\n")
2153
- .pop();
2154
- return JSON.parse(content);
2155
- });
2156
- }
2157
- catch {
2158
- return [];
2159
- }
2160
- },
2161
- emitEvent: (_type, _data) => {
2162
- // SSE event bus — wired to dashboard EventSource
2163
- },
2164
- };
2165
- }
2166
- catch {
2167
- return undefined;
2168
- }
2169
- })(),
2170
- });
2171
- if (dashboardHandle) {
2172
- startupLog.dashboardReady(`http://127.0.0.1:${dashboardHandle.port}`);
2173
- }
2174
- }
2175
- catch (err) {
2176
- log.warn(`Dashboard server failed: ${err instanceof Error ? err.message : String(err)}`);
2177
- }
2178
- // ── Step 8: Graceful Shutdown ────────────────────────────────────
2179
- // Once-guard: shutdown can fire from SIGINT, SIGTERM, and direct callers.
2180
- // We want exactly one full pass — subsequent calls return the same promise.
2181
- let shutdownPromise = null;
2182
- const shutdown = async () => {
2183
- if (shutdownPromise)
2184
- return shutdownPromise;
2185
- shutdownPromise = (async () => {
2186
- lifecycle.send({ type: "SHUTDOWN" });
2187
- lifecycle.stop();
2188
- logTailer.close();
2189
- // Layer 4: Fire session-end behaviors
2190
- behaviorDispatcher
2191
- .fireSessionEnd({
2192
- toolName: "__session_end__",
2193
- args: {},
2194
- sessionId: shadowLedger.getSessionId(),
2195
- })
2196
- .catch(() => { });
2197
- // Close any still-open persistent-memory windows so their verdicts
2198
- // land in the session summary instead of being dropped.
2199
- try {
2200
- effectivenessTracker.closeAll(router.sessionContext.getToolCallCount());
2201
- }
2202
- catch {
2203
- /* best-effort — tracker errors must never block shutdown */
2204
- }
2205
- // Persist session stats via unified weekly accumulator (S8.4)
2206
- const total = stats.toolCallsLocal;
2207
- if (total > 0) {
2208
- // Layer 10: Compute token flow summary for persistence + receipt
2209
- let tokenFlowSummary = null;
2210
- let mechanismBreakdown;
2211
- if (tokenFlowWriter) {
2212
- try {
2213
- const { aggregateSession: aggSession } = require("../tracking/token-flow.js");
2214
- tokenFlowSummary = aggSession(tokenFlowWriter.getSessionEvents(), tokenFlowWriter.sessionId);
2215
- if (Object.keys(tokenFlowSummary.by_mechanism).length > 0) {
2216
- mechanismBreakdown = {};
2217
- for (const [mech, data] of Object.entries(tokenFlowSummary.by_mechanism)) {
2218
- mechanismBreakdown[mech] = data.tokens_saved;
2219
- }
2220
- }
2221
- }
2222
- catch {
2223
- /* non-critical */
2224
- }
2225
- }
2226
- // biome-ignore format: typeof import() must stay single-line for TS
2227
- const { accumulateSession } = require("../tracking/weekly-accumulator.js");
2228
- const { computePercentiles } = require("./session-stats.js");
2229
- const localPercentiles = computePercentiles(stats.latency.localSamples, stats.latency.localTotalSamples);
2230
- const effSnap = router.getEfficiencySnapshot();
2231
- const unifiedStats = accumulateSession({
2232
- tokensSaved: tokenFlowSummary?.total_tokens_saved ??
2233
- stats.localMode?.tokensSavedByTruncation ??
2234
- stats.estimatedTokensSaved,
2235
- dollarsSaved: router.getSessionDollarsSaved(),
2236
- toolCalls: stats.toolCallsLocal,
2237
- violationsCaught: stats.violationsCaught,
2238
- chokepointWarnings: stats.events.chokepointWarningsIssued,
2239
- correctionsApplied: stats.localMode?.correctionPatternsInjected ?? 0,
2240
- blastRadiusComputed: stats.localMode?.blastRadiusComputations ?? 0,
2241
- efficiency: tokenFlowSummary?.efficiency_pct ?? effSnap?.efficiency ?? 0,
2242
- latencyP50: localPercentiles?.p50 ?? 0,
2243
- tokensByMechanism: mechanismBreakdown,
2244
- });
2245
- // Layer 10: Persist session history with token flow summary
2246
- if (tokenFlowSummary && tokenFlowSummary.total_tokens_saved > 0) {
2247
- try {
2248
- const { appendSessionHistory } = require("../tracking/session-history.js");
2249
- const topMech = Object.entries(tokenFlowSummary.by_mechanism).sort(([, a], [, b]) => b.tokens_saved - a.tokens_saved)[0];
2250
- appendSessionHistory(join(process.cwd(), ".unerr"), {
2251
- sessionId: shadowLedger.getSessionId(),
2252
- startedAt: new Date(stats.sessionStartedAt).toISOString(),
2253
- endedAt: new Date().toISOString(),
2254
- durationMs: Date.now() - stats.sessionStartedAt,
2255
- toolCalls: stats.toolCallsLocal,
2256
- tokensSaved: tokenFlowSummary.total_tokens_saved,
2257
- tokensProcessed: tokenFlowSummary.total_tokens_without,
2258
- efficiency: tokenFlowSummary.efficiency_pct,
2259
- dollarsSaved: router.getSessionDollarsSaved(),
2260
- modelId: "unknown",
2261
- entityCount: 0,
2262
- agentName: agentNameByClient.values().next().value ??
2263
- server.getClientVersion?.()?.name ??
2264
- undefined,
2265
- tokenFlowSummary: {
2266
- by_mechanism: Object.fromEntries(Object.entries(tokenFlowSummary.by_mechanism).map(([k, v]) => [
2267
- k,
2268
- {
2269
- tokens_saved: v.tokens_saved,
2270
- event_count: v.event_count,
2271
- },
2272
- ])),
2273
- top_mechanism: topMech?.[0] ?? "none",
2274
- efficiency_pct: tokenFlowSummary.efficiency_pct,
2275
- total_tokens_saved: tokenFlowSummary.total_tokens_saved,
2276
- total_tokens_delivered: tokenFlowSummary.total_tokens_with,
2277
- },
2278
- });
2279
- }
2280
- catch {
2281
- /* non-critical */
2282
- }
2283
- }
2284
- // Layer 10: Print session receipt
2285
- if (tokenFlowSummary && tokenFlowSummary.total_tokens_saved > 0) {
2286
- try {
2287
- const { printSessionReceipt } = require("../tracking/session-receipt.js");
2288
- printSessionReceipt({
2289
- summary: tokenFlowSummary,
2290
- durationMs: Date.now() - stats.sessionStartedAt,
2291
- toolCalls: stats.toolCallsLocal,
2292
- weeklyTokensSaved: unifiedStats.weekly.tokensSaved,
2293
- weeklySessions: unifiedStats.weekly.sessions,
2294
- });
2295
- }
2296
- catch {
2297
- /* non-critical */
2298
- }
2299
- }
2300
- // Build CumulativeLocalStats shape for SessionSummaryCard backwards compat
2301
- const cumulativeLocal = {
2302
- weekStartDate: unifiedStats.weekly.weekStart,
2303
- totalSessions: unifiedStats.weekly.sessions,
2304
- totalToolCalls: unifiedStats.weekly.toolCalls,
2305
- totalTokensSaved: unifiedStats.weekly.tokensSaved,
2306
- totalViolationsCaught: unifiedStats.weekly.violationsCaught,
2307
- totalCorrectionsApplied: unifiedStats.weekly.correctionsApplied,
2308
- totalFilesIndexed: 0,
2309
- totalSemanticSearches: 0,
2310
- avgLatencyP50: unifiedStats.weekly.avgLatencyP50,
2311
- };
2312
- // S8.6: Build scorecard for session summary display
2313
- // biome-ignore format: typeof import() must stay single-line for TS
2314
- const { formatScorecard, formatCounterfactual } = require("../config/value-surfacing.js");
2315
- const tokensSaved = stats.localMode?.tokensSavedByTruncation ??
2316
- stats.estimatedTokensSaved;
2317
- const dollarsSaved = router.getSessionDollarsSaved();
2318
- const durationMs = Date.now() - stats.sessionStartedAt;
2319
- const scorecardData = formatScorecard({
2320
- toolCalls: stats.toolCallsLocal,
2321
- tokensSaved,
2322
- dollarsSaved,
2323
- efficiency: effSnap?.efficiency ?? 0,
2324
- durationMs,
2325
- blastRadiusComputed: stats.localMode?.blastRadiusComputations ?? 0,
2326
- conventionsInjected: stats.localMode?.communityContextsInjected ?? 0,
2327
- outputsCompressed: stats.localMode?.truncatedResponses ?? 0,
2328
- correctionsApplied: stats.localMode?.correctionPatternsInjected ?? 0,
2329
- wrongApproachesPrevented: 0,
2330
- });
2331
- // S8.7: Counterfactual explanation
2332
- const tokensWithout = tokensSaved > 0 ? Math.round(tokensSaved / 0.65) : 0;
2333
- const counterfactualStr = tokensWithout > 0
2334
- ? formatCounterfactual(tokensWithout, tokensWithout - tokensSaved)
2335
- : undefined;
2336
- try {
2337
- const React = require("react");
2338
- const { SessionSummaryCard } = require("../components/SessionSummaryCard.js");
2339
- const { ThemeProvider } = require("../components/Theme.js");
2340
- const { renderToStderr } = require("../components/render.js");
2341
- const el = React.createElement(ThemeProvider, null, React.createElement(SessionSummaryCard, {
2342
- stats,
2343
- cumulativeLocal,
2344
- scorecard: {
2345
- efficiency: scorecardData.efficiency,
2346
- dollarsSaved: scorecardData.dollarsSaved,
2347
- tokensSaved: scorecardData.tokensSaved,
2348
- counterfactual: counterfactualStr,
2349
- },
2350
- }));
2351
- const inst = renderToStderr(el);
2352
- inst.unmount();
2353
- }
2354
- catch {
2355
- // Fallback to plain text if Ink rendering fails
2356
- const localSummary = formatLocalModeSessionStats(stats);
2357
- if (localSummary)
2358
- process.stderr.write(localSummary);
2359
- }
2360
- }
2361
- // Print drift summary if any
2362
- if (localGraph) {
2363
- try {
2364
- const driftSummary = await localGraph.getDriftSummary();
2365
- if (driftSummary.total > 0) {
2366
- process.stderr.write(`[unerr] Drift: ${driftSummary.added} added, ${driftSummary.modified} modified, ${driftSummary.deleted} deleted\n`);
2367
- }
2368
- }
2369
- catch {
2370
- /* non-critical */
2371
- }
2372
- }
2373
- // Sprint 10.7: Persist quality signals
2374
- qualitySignalTracker.save();
2375
- // Flush shadow ledger + print ledger summary
2376
- shadowLedger.flush();
2377
- const ledgerStats = shadowLedger.getStats();
2378
- if (ledgerStats.totalEntries > 0) {
2379
- const pendingCorrelations = intentCorrelator.getPendingCount();
2380
- process.stderr.write(`[unerr] Ledger: ${ledgerStats.totalEntries} entries, ${ledgerStats.bufferSize} buffered, ${pendingCorrelations} pending correlations\n`);
2381
- }
2382
- // Sprint 4: Capture session narrative summary (works in both modes)
2383
- if (narrativeCapture) {
2384
- const sessionId = shadowLedger.getSessionId();
2385
- narrativeCapture
2386
- .captureSessionSummary(sessionId)
2387
- .then((result) => {
2388
- if (result.filesModified.length > 0) {
2389
- process.stderr.write(`[unerr] Session narrative: ${result.narratives.length} edits across ${result.filesModified.length} file(s)\n`);
2390
- }
2391
- })
2392
- .catch(() => { });
2393
- }
2394
- // Sprint 5: Run session pattern analyzer at shutdown
2395
- if (proxyFactStore && ledgerStats.totalEntries > 0) {
2396
- try {
2397
- const { analyzeSessionPatterns } = require("../intelligence/session-pattern-analyzer.js");
2398
- const entries = shadowLedger.getRecentEntries(100);
2399
- analyzeSessionPatterns({
2400
- ledgerEntries: entries,
2401
- factStore: proxyFactStore,
2402
- sessionId: shadowLedger.getSessionId(),
2403
- })
2404
- .then((analysisResult) => {
2405
- if (analysisResult.factsCreated > 0 ||
2406
- analysisResult.factsReinforced > 0) {
2407
- process.stderr.write(`[unerr] Session analysis: ${analysisResult.factsCreated} facts learned, ${analysisResult.factsReinforced} reinforced\n`);
2408
- }
2409
- })
2410
- .catch(() => { });
2411
- }
2412
- catch {
2413
- // Pattern analysis is non-critical
2414
- }
2415
- }
2416
- // Leapfrog Sprint B: Run correction detector on this session's ledger entries
2417
- if (localGraph && ledgerStats.totalEntries > 0) {
2418
- try {
2419
- // eslint-disable-next-line @typescript-eslint/no-require-imports
2420
- const correctionModule = require("../tracking/correction-detector.js");
2421
- const detectCorrections = correctionModule.detectCorrections;
2422
- const ledgerPath = join(process.cwd(), ".unerr", "ledger", "shadow.jsonl");
2423
- const patterns = detectCorrections(ledgerPath, { since_days: 1 });
2424
- if (patterns.length > 0) {
2425
- localGraph.persistCorrections(patterns);
2426
- // L4.1: Track correction patterns injected
2427
- if (stats.localMode) {
2428
- for (let i = 0; i < patterns.length; i++) {
2429
- recordCorrectionInjection(stats.localMode);
2430
- }
2431
- }
2432
- process.stderr.write(`[unerr] Learned ${patterns.length} correction pattern${patterns.length !== 1 ? "s" : ""} from this session\n`);
2433
- // Layer 9: Generate negative knowledge facts from corrections
2434
- try {
2435
- const factStoreForShutdown = await getProxyFactStore(unerrDirForLedger);
2436
- if (factStoreForShutdown) {
2437
- const { generateFromNegativeKnowledge } = await import("../intelligence/fact-generator.js");
2438
- const corrections = patterns.map((p, i) => ({
2439
- id: `correction-${shadowLedger.getSessionId()}-${i}`,
2440
- entityKey: p.entity_key,
2441
- pattern: p.error_type,
2442
- reason: p.correction_summary,
2443
- detectedAt: p.last_seen,
2444
- rewindEntryId: shadowLedger.getSessionId(),
2445
- confidence: p.confidence,
2446
- }));
2447
- const negResult = await generateFromNegativeKnowledge(factStoreForShutdown, corrections);
2448
- if (negResult.created > 0) {
2449
- process.stderr.write(`[unerr] Fact generator: ${negResult.created} negative knowledge facts created\n`);
2450
- }
2451
- }
2452
- }
2453
- catch {
2454
- // Non-critical — fact generation doesn't block shutdown
2455
- }
2456
- }
2457
- }
2458
- catch {
2459
- // Correction detection is non-critical — don't block shutdown
2460
- }
2461
- }
2462
- // Layer 9: Run session analysis fact generation on shutdown
2463
- try {
2464
- const factStoreForSession = await getProxyFactStore(unerrDirForLedger);
2465
- if (factStoreForSession) {
2466
- const { runFactGenerationPipeline } = await import("../intelligence/fact-generator.js");
2467
- const pipelineResults = await runFactGenerationPipeline(factStoreForSession, unerrDirForLedger);
2468
- for (const r of pipelineResults) {
2469
- if (r.created > 0 || r.reinforced > 0) {
2470
- process.stderr.write(`[unerr] Fact generator [${r.source}]: ${r.created} created, ${r.reinforced} reinforced\n`);
2471
- }
2472
- }
2473
- }
2474
- }
2475
- catch {
2476
- // Non-critical — fact generation doesn't block shutdown
2477
- }
2478
- // Record orphaned intents (pending correlations that never got committed)
2479
- if (workspaceManifest) {
2480
- const orphans = intentCorrelator.getPending();
2481
- if (orphans.length > 0) {
2482
- workspaceManifest.recordOrphanedIntents(orphans.map((c) => ({
2483
- rootIntentId: c.rootIntentId,
2484
- prompt: c.prompt,
2485
- toolChain: c.toolChain,
2486
- files: c.files,
2487
- createdAt: c.createdAt,
2488
- })));
2489
- }
2490
- const mStats = workspaceManifest.getStats();
2491
- if (mStats.total > 0 || mStats.orphanedIntents > 0) {
2492
- process.stderr.write(`[unerr] Manifest: ${mStats.total} attributions (${mStats.unflushed} unflushed, ${mStats.orphanedIntents} orphaned)\n`);
2493
- }
2494
- }
2495
- // Cleanup
2496
- clearInterval(statsSnapshotInterval);
2497
- commitWatcher.stop();
2498
- stopBranchPoller?.();
2499
- // Task 7.2: Stop UDS transport (cleans up socket file)
2500
- transportMux.stop();
2501
- // Task 5.3: Stop HTTP transport
2502
- httpTransportHandle?.close();
2503
- // Layer 7: Stop dashboard server
2504
- dashboardHandle?.close();
2505
- // Remove stats snapshot file
2506
- try {
2507
- const { unlinkSync } = require("node:fs");
2508
- unlinkSync(statsSnapshotPath);
2509
- }
2510
- catch {
2511
- /* ignore */
2512
- }
2513
- // Release persistent CozoDB native handles
2514
- if (localGraph?.db.close) {
2515
- localGraph.db.close();
2516
- }
2517
- // Release SQLite metrics handle(s).
2518
- try {
2519
- const { closeAllMetricsStores } = require("../tracking/metrics-store.js");
2520
- closeAllMetricsStores();
2521
- }
2522
- catch {
2523
- /* metrics store may not have been opened this session */
2524
- }
2525
- // ST-4: Stop intent-stitch interval before releasing the store handle.
2526
- if (timelineIntentStitchInterval) {
2527
- clearInterval(timelineIntentStitchInterval);
2528
- }
2529
- // ST-5: Stop signal prune interval.
2530
- if (timelineSignalPruneInterval) {
2531
- clearInterval(timelineSignalPruneInterval);
2532
- }
2533
- // ST-6: Stop daily ledger-archive interval.
2534
- clearInterval(ledgerArchiveInterval);
2535
- // ST-1c: Release timeline subsystem (no-op if disabled or never started)
2536
- timelineHandle?.stop();
2537
- pidLock.release();
2538
- log.info("Proxy stopped.");
2539
- })();
2540
- return shutdownPromise;
2541
- };
2542
- process.on("SIGINT", () => {
2543
- void shutdown().then(() => process.exit(0));
2544
- });
2545
- process.on("SIGTERM", () => {
2546
- void shutdown().then(() => process.exit(0));
2547
- });
2548
- return { shutdown, stats };
2549
- }
2550
- // ── Internal Helpers ──────────────────────────────────────────────────
2551
- /**
2552
- * Create a minimal CozoGraphStore-compatible stub for PARSE mode.
2553
- * Delegates entity lookups to the ParseModeIndex.
2554
- */
2555
- async function createParseGraphStub(index) {
2556
- const noop = async () => [];
2557
- const noopVoid = async () => { };
2558
- return {
2559
- getEntity: async (key) => {
2560
- const e = await index.getEntity(key);
2561
- if (!e)
2562
- return null;
2563
- return {
2564
- key: e.key,
2565
- kind: e.kind,
2566
- name: e.name,
2567
- file_path: e.file_path,
2568
- start_line: e.line_start,
2569
- signature: e.signature,
2570
- body: "",
2571
- fan_in: 0,
2572
- fan_out: 0,
2573
- risk_level: "normal",
2574
- };
2575
- },
2576
- getCallersOf: noop,
2577
- getCalleesOf: noop,
2578
- getEntitiesByFile: async (fp) => (await index.getEntitiesByFile(fp)).map((e) => ({
2579
- key: e.key,
2580
- kind: e.kind,
2581
- name: e.name,
2582
- file_path: e.file_path,
2583
- start_line: e.line_start,
2584
- signature: e.signature,
2585
- body: "",
2586
- fan_in: 0,
2587
- fan_out: 0,
2588
- risk_level: "normal",
2589
- })),
2590
- searchEntities: async (q, limit) => index.search(q, limit).map((e) => ({
2591
- key: e.key,
2592
- kind: e.kind,
2593
- name: e.name,
2594
- file_path: e.file_path,
2595
- start_line: e.line_start,
2596
- signature: e.signature,
2597
- body: "",
2598
- fan_in: 0,
2599
- fan_out: 0,
2600
- risk_level: "normal",
2601
- })),
2602
- getImports: noop,
2603
- hasRules: () => false,
2604
- getRules: noop,
2605
- getPatterns: noop,
2606
- hasJustifications: () => false,
2607
- getBusinessContext: async () => null,
2608
- getConventions: noop,
2609
- getDriftEntitiesForFile: noop,
2610
- upsertDriftEntity: noopVoid,
2611
- removeDriftEntity: noopVoid,
2612
- clearDriftOverlay: noopVoid,
2613
- getDriftSummary: async () => ({
2614
- added: 0,
2615
- modified: 0,
2616
- deleted: 0,
2617
- total: 0,
2618
- }),
2619
- healthCheck: () => ({ status: "parse_mode", latencyMs: 0 }),
2620
- isLoaded: () => true,
2621
- loadSnapshot: noopVoid,
2622
- loadRules: noopVoid,
2623
- loadPatterns: noopVoid,
2624
- loadJustifications: noopVoid,
2625
- applyDelta: () => ({
2626
- applied: 0,
2627
- deleted: 0,
2628
- edges: 0,
2629
- justifications: 0,
2630
- overlayExpired: 0,
2631
- }),
2632
- };
2633
- }