@unerr-ai/unerr 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/cli.js +37236 -35793
- package/package.json +6 -1
- package/dist/behaviors/agent-llm-bridge.js +0 -166
- package/dist/behaviors/architecture-guard.js +0 -256
- package/dist/behaviors/auto-doc.js +0 -247
- package/dist/behaviors/cascade-guard.js +0 -289
- package/dist/behaviors/change-narrative.js +0 -270
- package/dist/behaviors/convention-drift.js +0 -290
- package/dist/behaviors/framework.js +0 -235
- package/dist/behaviors/guard-formatter.js +0 -44
- package/dist/behaviors/incomplete-work.js +0 -270
- package/dist/behaviors/loop-breaker.js +0 -300
- package/dist/behaviors/session-continuity.js +0 -208
- package/dist/commands/branches.js +0 -97
- package/dist/commands/check-commit.js +0 -225
- package/dist/commands/compress-output.js +0 -64
- package/dist/commands/config-verify.js +0 -243
- package/dist/commands/daemon.js +0 -905
- package/dist/commands/dashboard.js +0 -52
- package/dist/commands/debug.js +0 -200
- package/dist/commands/enrich.js +0 -184
- package/dist/commands/exec.js +0 -233
- package/dist/commands/gain.js +0 -156
- package/dist/commands/hook.js +0 -88
- package/dist/commands/index.js +0 -88
- package/dist/commands/init.js +0 -74
- package/dist/commands/install.js +0 -505
- package/dist/commands/learn.js +0 -116
- package/dist/commands/manifest.js +0 -193
- package/dist/commands/rewind.js +0 -103
- package/dist/commands/serve.js +0 -19
- package/dist/commands/setup-wizard.js +0 -414
- package/dist/commands/skills.js +0 -64
- package/dist/commands/stats.js +0 -20
- package/dist/commands/status.js +0 -654
- package/dist/commands/timeline.js +0 -139
- package/dist/commands/uninstall.js +0 -230
- package/dist/components/App.js +0 -109
- package/dist/components/Banner.js +0 -12
- package/dist/components/ConfirmPrompt.js +0 -25
- package/dist/components/DriftSummary.js +0 -23
- package/dist/components/GradeBadge.js +0 -15
- package/dist/components/HealthCard.js +0 -18
- package/dist/components/InkSpinner.js +0 -22
- package/dist/components/InputBox.js +0 -17
- package/dist/components/KeyValue.js +0 -13
- package/dist/components/MessageList.js +0 -14
- package/dist/components/ProgressBar.js +0 -26
- package/dist/components/Section.js +0 -16
- package/dist/components/SessionSummaryCard.js +0 -73
- package/dist/components/StartupDisplay.js +0 -24
- package/dist/components/StatusDashboard.js +0 -57
- package/dist/components/StatusLine.js +0 -8
- package/dist/components/StepLine.js +0 -22
- package/dist/components/Theme.js +0 -20
- package/dist/components/ToolProgress.js +0 -8
- package/dist/components/ViolationList.js +0 -21
- package/dist/components/render.js +0 -13
- package/dist/config/agent-registry.js +0 -237
- package/dist/config/claude-settings-hooks.js +0 -304
- package/dist/config/hook-installer.js +0 -65
- package/dist/config/instruction-writer.js +0 -388
- package/dist/config/mcp-config-writer.js +0 -266
- package/dist/config/settings.js +0 -174
- package/dist/config/tool-detector.js +0 -42
- package/dist/config/value-surfacing.js +0 -119
- package/dist/core/context-assembly.js +0 -108
- package/dist/core/conversation.js +0 -33
- package/dist/core/local-chat-provider.js +0 -475
- package/dist/core/provider-factory.js +0 -55
- package/dist/core/providers.js +0 -90
- package/dist/core/query-engine.js +0 -174
- package/dist/daemon/api.js +0 -312
- package/dist/daemon/autostart.js +0 -119
- package/dist/daemon/bootstrap.js +0 -39
- package/dist/daemon/client.js +0 -164
- package/dist/daemon/detect-ci.js +0 -81
- package/dist/daemon/platform-linux.js +0 -146
- package/dist/daemon/platform-macos.js +0 -134
- package/dist/daemon/platform-windows.js +0 -116
- package/dist/daemon/process-manager.js +0 -299
- package/dist/daemon/protocol.js +0 -23
- package/dist/daemon/registry.js +0 -270
- package/dist/daemon/settings-schema.js +0 -72
- package/dist/daemon/system-health.js +0 -134
- package/dist/daemon/version-checker.js +0 -262
- package/dist/daemon/warm-start.js +0 -223
- package/dist/entrypoints/cli.js +0 -1043
- package/dist/entrypoints/daemon.js +0 -380
- package/dist/entrypoints/repl.js +0 -147
- package/dist/hooks/adapters/claude-code.js +0 -90
- package/dist/hooks/adapters/cline.js +0 -100
- package/dist/hooks/adapters/cursor.js +0 -98
- package/dist/hooks/hook-dedup.js +0 -79
- package/dist/hooks/hook-runner.js +0 -113
- package/dist/hooks/navigation-hooks.js +0 -175
- package/dist/hooks/prompt-hooks.js +0 -63
- package/dist/hooks/shell-hooks.js +0 -47
- package/dist/ignore.js +0 -111
- package/dist/intelligence/approach-suggester.js +0 -61
- package/dist/intelligence/ast-extractor.js +0 -2615
- package/dist/intelligence/ast-worker.js +0 -34
- package/dist/intelligence/background-indexer.js +0 -121
- package/dist/intelligence/blast-radius.js +0 -200
- package/dist/intelligence/community-detection.js +0 -691
- package/dist/intelligence/community-detector.js +0 -184
- package/dist/intelligence/computation-scheduler.js +0 -75
- package/dist/intelligence/confidence-propagation.js +0 -47
- package/dist/intelligence/convention-detector.js +0 -242
- package/dist/intelligence/convention-learner.js +0 -205
- package/dist/intelligence/convention-matcher.js +0 -205
- package/dist/intelligence/cozo-schema.js +0 -376
- package/dist/intelligence/decision-point-detector.js +0 -90
- package/dist/intelligence/deep-dive-tools.js +0 -586
- package/dist/intelligence/durability-scorer.js +0 -84
- package/dist/intelligence/exploration-cost.js +0 -204
- package/dist/intelligence/exploration-pattern-tracker.js +0 -61
- package/dist/intelligence/fact-generator.js +0 -322
- package/dist/intelligence/facts-schema.js +0 -90
- package/dist/intelligence/file-intelligence.js +0 -59
- package/dist/intelligence/graph-holder.js +0 -220
- package/dist/intelligence/graph-temporal-joiner.js +0 -238
- package/dist/intelligence/health-grade.js +0 -423
- package/dist/intelligence/health-grader.js +0 -200
- package/dist/intelligence/health-map-data.js +0 -259
- package/dist/intelligence/import-symbols.js +0 -136
- package/dist/intelligence/incremental-indexer.js +0 -658
- package/dist/intelligence/indexer/centrality.js +0 -62
- package/dist/intelligence/indexer/cfg-context.js +0 -95
- package/dist/intelligence/indexer/confidence.js +0 -34
- package/dist/intelligence/indexer/cross-file-resolver.js +0 -104
- package/dist/intelligence/indexer/edge-repair.js +0 -89
- package/dist/intelligence/indexer/entity-key.js +0 -17
- package/dist/intelligence/indexer/export-map.js +0 -132
- package/dist/intelligence/indexer/git-cochange.js +0 -128
- package/dist/intelligence/indexer/graph-patch.js +0 -147
- package/dist/intelligence/indexer/incremental.js +0 -78
- package/dist/intelligence/indexer/ingest.js +0 -160
- package/dist/intelligence/indexer/language-detect.js +0 -226
- package/dist/intelligence/indexer/metadata.js +0 -63
- package/dist/intelligence/indexer/mutation-tracker.js +0 -79
- package/dist/intelligence/indexer/orchestrator.js +0 -155
- package/dist/intelligence/indexer/plugin-interface.js +0 -31
- package/dist/intelligence/indexer/plugins/csharp.js +0 -440
- package/dist/intelligence/indexer/plugins/go.js +0 -335
- package/dist/intelligence/indexer/plugins/java.js +0 -370
- package/dist/intelligence/indexer/plugins/python.js +0 -358
- package/dist/intelligence/indexer/plugins/regex-fallback.js +0 -82
- package/dist/intelligence/indexer/plugins/ruby.js +0 -290
- package/dist/intelligence/indexer/plugins/rust.js +0 -484
- package/dist/intelligence/indexer/plugins/tier2-generic.js +0 -310
- package/dist/intelligence/indexer/plugins/typescript.js +0 -456
- package/dist/intelligence/indexer/resource-monitor.js +0 -93
- package/dist/intelligence/indexer/scip/decoder.js +0 -253
- package/dist/intelligence/indexer/scip/detector.js +0 -232
- package/dist/intelligence/indexer/scip/downloader.js +0 -427
- package/dist/intelligence/indexer/scip/fallback.js +0 -34
- package/dist/intelligence/indexer/scip/merger.js +0 -109
- package/dist/intelligence/indexer/scip/orchestrator.js +0 -433
- package/dist/intelligence/indexer/scip/runner.js +0 -98
- package/dist/intelligence/indexer/snapshot.js +0 -66
- package/dist/intelligence/indexer/test-detector.js +0 -196
- package/dist/intelligence/indexer/watch-integration.js +0 -61
- package/dist/intelligence/indexer/worker.js +0 -85
- package/dist/intelligence/local-convention-detector.js +0 -437
- package/dist/intelligence/local-embeddings.js +0 -190
- package/dist/intelligence/local-graph.js +0 -1946
- package/dist/intelligence/local-indexer.js +0 -1575
- package/dist/intelligence/local-llm.js +0 -163
- package/dist/intelligence/local-rule-generator.js +0 -154
- package/dist/intelligence/local-snapshot.js +0 -213
- package/dist/intelligence/negative-knowledge.js +0 -103
- package/dist/intelligence/persistent-db.js +0 -85
- package/dist/intelligence/query-router.js +0 -2556
- package/dist/intelligence/risk-classifier.js +0 -116
- package/dist/intelligence/rule-evaluator.js +0 -380
- package/dist/intelligence/rule-generator.js +0 -49
- package/dist/intelligence/search-index.js +0 -173
- package/dist/intelligence/semantic/docstring-extractor.js +0 -67
- package/dist/intelligence/semantic/embedding-store.js +0 -52
- package/dist/intelligence/semantic/enrichment-orchestrator.js +0 -48
- package/dist/intelligence/semantic/git-message-miner.js +0 -114
- package/dist/intelligence/semantic/identifier-tokenizer.js +0 -51
- package/dist/intelligence/semantic/node2vec-embeddings.js +0 -71
- package/dist/intelligence/semantic/node2vec-walks.js +0 -103
- package/dist/intelligence/semantic/path-domain-inference.js +0 -112
- package/dist/intelligence/semantic/similarity-engine.js +0 -60
- package/dist/intelligence/semantic/tfidf-vectors.js +0 -88
- package/dist/intelligence/session-brief-builder.js +0 -159
- package/dist/intelligence/session-context.js +0 -221
- package/dist/intelligence/session-health-monitor.js +0 -211
- package/dist/intelligence/session-narrative.js +0 -197
- package/dist/intelligence/session-pattern-analyzer.js +0 -218
- package/dist/intelligence/signal-scorer.js +0 -390
- package/dist/intelligence/signal-show-store.js +0 -182
- package/dist/intelligence/smart-truncate.js +0 -158
- package/dist/intelligence/subgraph-cache.js +0 -88
- package/dist/intelligence/temporal-facts.js +0 -494
- package/dist/intelligence/token-estimator.js +0 -100
- package/dist/intelligence/tool-injector.js +0 -87
- package/dist/intelligence/tree-sitter-loader.js +0 -71
- package/dist/intelligence/worker-pool.js +0 -116
- package/dist/proxy/arg-validator.js +0 -79
- package/dist/proxy/auto-bootstrap.js +0 -167
- package/dist/proxy/bridge.js +0 -147
- package/dist/proxy/budget-enforcer.js +0 -70
- package/dist/proxy/compression-quality-monitor.js +0 -160
- package/dist/proxy/compression-stats.js +0 -51
- package/dist/proxy/context-rot-detector.js +0 -137
- package/dist/proxy/drift-detector.js +0 -139
- package/dist/proxy/efficiency-tracker.js +0 -79
- package/dist/proxy/fact-ranking.js +0 -154
- package/dist/proxy/format-encoder.js +0 -266
- package/dist/proxy/http-transport.js +0 -90
- package/dist/proxy/lifecycle-actor.js +0 -55
- package/dist/proxy/lifecycle-machine.js +0 -187
- package/dist/proxy/log-tailer.js +0 -265
- package/dist/proxy/model-pricing.js +0 -98
- package/dist/proxy/network-firewall.js +0 -141
- package/dist/proxy/nudge-state.js +0 -93
- package/dist/proxy/output-compressor.js +0 -185
- package/dist/proxy/pid-lock.js +0 -291
- package/dist/proxy/proxy-context.js +0 -11
- package/dist/proxy/proxy.js +0 -2633
- package/dist/proxy/response-enrichment.js +0 -32
- package/dist/proxy/response-envelope.js +0 -313
- package/dist/proxy/session-dedup.js +0 -82
- package/dist/proxy/session-legend.js +0 -30
- package/dist/proxy/session-persistence.js +0 -210
- package/dist/proxy/session-resume.js +0 -94
- package/dist/proxy/session-stats.js +0 -513
- package/dist/proxy/shell-classifier.js +0 -1346
- package/dist/proxy/shell-compression-log.js +0 -93
- package/dist/proxy/shell-compressor.js +0 -390
- package/dist/proxy/shell-graph-boost.js +0 -202
- package/dist/proxy/shell-monitor-map.js +0 -18
- package/dist/proxy/shell-stats.js +0 -54
- package/dist/proxy/shell-strategies/cloud.js +0 -215
- package/dist/proxy/shell-strategies/diff.js +0 -159
- package/dist/proxy/shell-strategies/error-diagnostic.js +0 -796
- package/dist/proxy/shell-strategies/filter-dsl.js +0 -358
- package/dist/proxy/shell-strategies/git-status.js +0 -177
- package/dist/proxy/shell-strategies/key-value.js +0 -193
- package/dist/proxy/shell-strategies/log-text.js +0 -154
- package/dist/proxy/shell-strategies/omni.js +0 -188
- package/dist/proxy/shell-strategies/progress.js +0 -55
- package/dist/proxy/shell-strategies/redact.js +0 -76
- package/dist/proxy/shell-strategies/structured.js +0 -241
- package/dist/proxy/shell-strategies/tabular.js +0 -243
- package/dist/proxy/shell-strategies/test-results-types.js +0 -13
- package/dist/proxy/shell-strategies/test-results.js +0 -784
- package/dist/proxy/shell-strategies/tree-paths.js +0 -144
- package/dist/proxy/shell-strategies/yaml.js +0 -182
- package/dist/proxy/shell-tee.js +0 -111
- package/dist/proxy/signal-dedup.js +0 -171
- package/dist/proxy/startup-renderer.js +0 -158
- package/dist/proxy/task-token-display.js +0 -38
- package/dist/proxy/token-counter.js +0 -61
- package/dist/proxy/tool-clusters.js +0 -273
- package/dist/proxy/tool-definitions.js +0 -525
- package/dist/proxy/transport-mux.js +0 -229
- package/dist/proxy/wire-cap.js +0 -268
- package/dist/rules/developer.mozilla.org.json +0 -9
- package/dist/rules/github.com.json +0 -21
- package/dist/schemas/api/skills.js +0 -19
- package/dist/schemas/common/errors.js +0 -7
- package/dist/schemas/common/headers.js +0 -5
- package/dist/schemas/entities/edge.js +0 -25
- package/dist/schemas/entities/entity.js +0 -22
- package/dist/schemas/entities/rule.js +0 -18
- package/dist/schemas/index.js +0 -14
- package/dist/server/event-bus.js +0 -59
- package/dist/server/http.js +0 -156
- package/dist/server/middleware.js +0 -70
- package/dist/server/routes/drift.js +0 -97
- package/dist/server/routes/intelligence.js +0 -1217
- package/dist/server/routes/reasoning-quality.js +0 -444
- package/dist/server/routes/session.js +0 -86
- package/dist/server/routes/stream.js +0 -120
- package/dist/server/routes/system.js +0 -73
- package/dist/server/routes/temporal.js +0 -170
- package/dist/server/routes/timeline.js +0 -232
- package/dist/server/routes/token-flow.js +0 -403
- package/dist/skills/effectiveness-tracker.js +0 -93
- package/dist/skills/local-pack.js +0 -380
- package/dist/skills/resolver.js +0 -495
- package/dist/state-detector.js +0 -83
- package/dist/timeline/intent-detector.js +0 -263
- package/dist/timeline/loop-miner.js +0 -140
- package/dist/timeline/open-threads.js +0 -49
- package/dist/timeline/signal-reinforcer.js +0 -62
- package/dist/timeline/timeline-bootstrap.js +0 -151
- package/dist/timeline/timeline-store.js +0 -618
- package/dist/tools/coding/bash.js +0 -49
- package/dist/tools/coding/file-edit.js +0 -72
- package/dist/tools/coding/file-outline.js +0 -227
- package/dist/tools/coding/file-read-protocol.js +0 -425
- package/dist/tools/coding/file-read.js +0 -35
- package/dist/tools/coding/file-write.js +0 -43
- package/dist/tools/coding/glob-tool.js +0 -109
- package/dist/tools/coding/grep.js +0 -162
- package/dist/tools/coding/index.js +0 -27
- package/dist/tools/intelligence/index.js +0 -269
- package/dist/tools/intelligence/record-fact.js +0 -48
- package/dist/tools/intelligence/timeline-markers.js +0 -130
- package/dist/tools/registry.js +0 -47
- package/dist/tools/types.js +0 -8
- package/dist/tracking/auto-snapshot-triggers.js +0 -246
- package/dist/tracking/branch-context.js +0 -115
- package/dist/tracking/branch-snapshot.js +0 -217
- package/dist/tracking/causal-bridge.js +0 -317
- package/dist/tracking/circuit-breaker.js +0 -147
- package/dist/tracking/commit-watcher.js +0 -114
- package/dist/tracking/context-ledger.js +0 -119
- package/dist/tracking/correction-detector.js +0 -324
- package/dist/tracking/drift-tracker.js +0 -874
- package/dist/tracking/durability-tracker.js +0 -94
- package/dist/tracking/entity-rewind.js +0 -200
- package/dist/tracking/file-hash-state.js +0 -114
- package/dist/tracking/git-attribution.js +0 -132
- package/dist/tracking/git-trailers.js +0 -171
- package/dist/tracking/intelligence-counter.js +0 -46
- package/dist/tracking/intent-correlator.js +0 -202
- package/dist/tracking/intent-encoder.js +0 -52
- package/dist/tracking/intent-token-tracker.js +0 -159
- package/dist/tracking/ledger-archiver.js +0 -94
- package/dist/tracking/ledger-chains.js +0 -245
- package/dist/tracking/metrics-store.js +0 -361
- package/dist/tracking/native-watcher.js +0 -131
- package/dist/tracking/offline-rewind.js +0 -295
- package/dist/tracking/pending-violations.js +0 -74
- package/dist/tracking/persistence-effectiveness.js +0 -167
- package/dist/tracking/prompt-durability.js +0 -202
- package/dist/tracking/quality-signals.js +0 -213
- package/dist/tracking/redactor.js +0 -73
- package/dist/tracking/rewind-engine.js +0 -161
- package/dist/tracking/session-history.js +0 -128
- package/dist/tracking/session-receipt.js +0 -88
- package/dist/tracking/session-summary-writer.js +0 -157
- package/dist/tracking/shadow-ledger.js +0 -321
- package/dist/tracking/stash-manager.js +0 -258
- package/dist/tracking/timeline-fork.js +0 -213
- package/dist/tracking/timeline.js +0 -69
- package/dist/tracking/token-flow.js +0 -276
- package/dist/tracking/turn-segmenter.js +0 -122
- package/dist/tracking/weekly-accumulator.js +0 -179
- package/dist/tracking/working-snapshots.js +0 -188
- package/dist/tracking/workspace-manifest.js +0 -176
- package/dist/transport/http.js +0 -102
- package/dist/utils/counterfactual.js +0 -65
- package/dist/utils/deep-link.js +0 -34
- package/dist/utils/detect.js +0 -193
- package/dist/utils/exec.js +0 -73
- package/dist/utils/file-logger.js +0 -87
- package/dist/utils/format-error.js +0 -29
- package/dist/utils/git.js +0 -181
- package/dist/utils/log.js +0 -57
- package/dist/utils/logger.js +0 -35
- package/dist/utils/mcp-content-json.js +0 -8
- package/dist/utils/session-logger.js +0 -154
- package/dist/utils/startup-log.js +0 -512
- package/dist/utils/ui.js +0 -56
package/dist/proxy/proxy.js
DELETED
|
@@ -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
|
-
}
|