@massu/core 1.15.3 → 1.15.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,28 +1,46 @@
1
1
  #!/usr/bin/env node
2
2
  import{createRequire as __cr}from"module";const require=__cr(import.meta.url);
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
3
7
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
4
8
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
5
9
  }) : x)(function(x) {
6
10
  if (typeof require !== "undefined") return require.apply(this, arguments);
7
11
  throw Error('Dynamic require of "' + x + '" is not supported');
8
12
  });
9
-
10
- // src/memory-db.ts
11
- import Database from "better-sqlite3";
12
- import { dirname as dirname2, basename } from "path";
13
- import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
14
-
15
- // src/config.ts
16
- import { resolve as resolve2, dirname } from "path";
17
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
18
- import { homedir as homedir2 } from "os";
19
- import { parse as parseYaml } from "yaml";
20
- import { z } from "zod";
13
+ var __esm = (fn, res, err) => function __init() {
14
+ if (err) throw err[0];
15
+ try {
16
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
17
+ } catch (e) {
18
+ throw err = [e], e;
19
+ }
20
+ };
21
+ var __export = (target, all) => {
22
+ for (var name in all)
23
+ __defProp(target, name, { get: all[name], enumerable: true });
24
+ };
25
+ var __copyProps = (to, from, except, desc) => {
26
+ if (from && typeof from === "object" || typeof from === "function") {
27
+ for (let key of __getOwnPropNames(from))
28
+ if (!__hasOwnProp.call(to, key) && key !== except)
29
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
30
+ }
31
+ return to;
32
+ };
33
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
34
 
22
35
  // src/lib/memory-path.ts
23
36
  function encodeMemoryDirName(projectRoot) {
24
37
  return projectRoot.replace(/\//g, "-");
25
38
  }
39
+ var init_memory_path = __esm({
40
+ "src/lib/memory-path.ts"() {
41
+ "use strict";
42
+ }
43
+ });
26
44
 
27
45
  // src/credentials.ts
28
46
  import { homedir } from "os";
@@ -35,9 +53,6 @@ import {
35
53
  rmSync,
36
54
  chmodSync
37
55
  } from "fs";
38
- var MASSU_ENV_API_KEY = "MASSU_API_KEY";
39
- var MASSU_ENV_CLOUD_ENDPOINT = "MASSU_CLOUD_ENDPOINT";
40
- var DEFAULT_CLOUD_ENDPOINT = "https://api.massu.ai/v1";
41
56
  function credentialsDir(home = homedir()) {
42
57
  return resolve(home, ".massu");
43
58
  }
@@ -85,391 +100,160 @@ function resolveEndpoint(opts = {}) {
85
100
  if (envEp.length > 0) return envEp;
86
101
  return DEFAULT_CLOUD_ENDPOINT;
87
102
  }
88
-
89
- // src/config.ts
90
- var DomainConfigSchema = z.object({
91
- name: z.string().default("Unknown"),
92
- routers: z.array(z.string()).default([]),
93
- pages: z.array(z.string()).default([]),
94
- tables: z.array(z.string()).default([]),
95
- allowedImportsFrom: z.array(z.string()).default([])
96
- });
97
- var PatternRuleConfigSchema = z.object({
98
- pattern: z.string().default("**"),
99
- rules: z.array(z.string()).default([]),
100
- language: z.string().optional()
101
- });
102
- var CostModelSchema = z.object({
103
- input_per_million: z.number(),
104
- output_per_million: z.number(),
105
- cache_read_per_million: z.number().optional(),
106
- cache_write_per_million: z.number().optional()
107
- });
108
- var AnalyticsConfigSchema = z.object({
109
- quality: z.object({
110
- weights: z.record(z.string(), z.number()).default({
111
- bug_found: -5,
112
- vr_failure: -10,
113
- incident: -20,
114
- cr_violation: -3,
115
- vr_pass: 2,
116
- clean_commit: 5,
117
- successful_verification: 3
118
- }),
119
- categories: z.array(z.string()).default(["security", "architecture", "coupling", "tests", "rule_compliance"])
120
- }).optional(),
121
- cost: z.object({
122
- models: z.record(z.string(), CostModelSchema).default({}),
123
- currency: z.string().default("USD")
124
- }).optional(),
125
- prompts: z.object({
126
- success_indicators: z.array(z.string()).default(["committed", "approved", "looks good", "perfect", "great", "thanks"]),
127
- failure_indicators: z.array(z.string()).default(["revert", "wrong", "that's not", "undo", "incorrect"]),
128
- max_turns_for_success: z.number().default(2)
129
- }).optional()
130
- }).optional();
131
- var CustomPatternSchema = z.object({
132
- pattern: z.string(),
133
- severity: z.string(),
134
- message: z.string()
135
- });
136
- var GovernanceConfigSchema = z.object({
137
- audit: z.object({
138
- formats: z.array(z.string()).default(["summary", "detailed", "soc2"]),
139
- retention_days: z.number().default(365),
140
- auto_log: z.record(z.string(), z.boolean()).default({
141
- code_changes: true,
142
- rule_enforcement: true,
143
- approvals: true,
144
- commits: true
145
- })
146
- }).optional(),
147
- validation: z.object({
148
- realtime: z.boolean().default(true),
149
- checks: z.record(z.string(), z.boolean()).default({
150
- rule_compliance: true,
151
- import_existence: true,
152
- naming_conventions: true
153
- }),
154
- custom_patterns: z.array(CustomPatternSchema).default([])
155
- }).optional(),
156
- adr: z.object({
157
- detection_phrases: z.array(z.string()).default(["chose", "decided", "switching to", "moving from", "going with"]),
158
- template: z.string().default("default"),
159
- storage: z.string().default("database"),
160
- output_dir: z.string().default("docs/adr")
161
- }).optional()
162
- }).optional();
163
- var SecurityPatternSchema = z.object({
164
- pattern: z.string(),
165
- severity: z.string(),
166
- category: z.string(),
167
- description: z.string()
168
- });
169
- var SecurityConfigSchema = z.object({
170
- patterns: z.array(SecurityPatternSchema).default([]),
171
- auto_score_on_edit: z.boolean().default(true),
172
- score_threshold_alert: z.number().default(50),
173
- severity_weights: z.record(z.string(), z.number()).optional(),
174
- restrictive_licenses: z.array(z.string()).optional(),
175
- dep_alternatives: z.record(z.string(), z.array(z.string())).optional(),
176
- dependencies: z.object({
177
- package_manager: z.string().default("npm"),
178
- blocked_packages: z.array(z.string()).default([]),
179
- preferred_packages: z.record(z.string(), z.string()).default({}),
180
- max_bundle_size_kb: z.number().default(500)
181
- }).optional()
182
- }).optional();
183
- var TeamConfigSchema = z.object({
184
- enabled: z.boolean().default(false),
185
- sync_backend: z.string().default("local"),
186
- developer_id: z.string().default("auto"),
187
- share_by_default: z.boolean().default(false),
188
- expertise_weights: z.object({
189
- session: z.number().default(20),
190
- observation: z.number().default(10)
191
- }).optional(),
192
- privacy: z.object({
193
- share_file_paths: z.boolean().default(true),
194
- share_code_snippets: z.boolean().default(false),
195
- share_observations: z.boolean().default(true)
196
- }).optional()
197
- }).optional();
198
- var RegressionConfigSchema = z.object({
199
- test_patterns: z.array(z.string()).default([
200
- "{dir}/__tests__/{name}.test.{ext}",
201
- "{dir}/{name}.spec.{ext}",
202
- "tests/{path}.test.{ext}"
203
- ]),
204
- test_runner: z.string().default("npm test"),
205
- health_thresholds: z.object({
206
- healthy: z.number().default(80),
207
- warning: z.number().default(50)
208
- }).optional()
209
- }).optional();
210
- var AutoLearningConfigSchema = z.object({
211
- enabled: z.boolean().default(true),
212
- incidentDir: z.string().default("docs/incidents"),
213
- memoryDir: z.string().default("memory"),
214
- memoryIndexFile: z.string().default("MEMORY.md"),
215
- enforcementHooksDir: z.string().default("scripts/hooks"),
216
- fixDetection: z.object({
217
- enabled: z.boolean().default(true),
218
- lookbackDays: z.number().default(7),
219
- signals: z.array(z.string()).default([
220
- "removed_broken_code",
221
- "added_error_handling",
222
- "method_name_correction",
223
- "auth_fix",
224
- "nil_handling_fix",
225
- "concurrency_fix",
226
- "async_pattern_fix",
227
- "added_missing_import"
228
- ])
229
- }).default({}),
230
- failureClassification: z.object({
231
- enabled: z.boolean().default(true),
232
- thresholds: z.object({
233
- known: z.number().default(5),
234
- similar: z.number().default(3)
235
- }).default({}),
236
- scoring: z.object({
237
- diffPatternWeight: z.number().default(3),
238
- filePatternWeight: z.number().default(2),
239
- promptKeywordWeight: z.number().default(2)
240
- }).default({})
241
- }).default({}),
242
- pipeline: z.object({
243
- requireIncidentReport: z.boolean().default(true),
244
- requirePreventionRule: z.boolean().default(true),
245
- requireEnforcement: z.boolean().default(true)
246
- }).default({}),
247
- // plan-v0.2-interactive-rule-approval P-D-008 / P-D-009: project-configured
248
- // custom destinations for the rule-candidate funnel. The classifier matches
249
- // a candidate to one of these entries when none of the framework
250
- // destinations (pattern-scanner / claude-md-cr / corrections-md) apply.
251
- customDestinations: z.array(z.object({
252
- name: z.string(),
253
- path: z.string(),
254
- triggerKeywords: z.array(z.string()).default([]),
255
- template: z.string()
256
- })).default([])
257
- }).optional();
258
- var CloudConfigSchema = z.object({
259
- enabled: z.boolean().default(false),
260
- apiKey: z.string().optional(),
261
- endpoint: z.string().optional(),
262
- sync: z.object({
263
- memory: z.boolean().default(true),
264
- analytics: z.boolean().default(true),
265
- audit: z.boolean().default(true)
266
- }).default({ memory: true, analytics: true, audit: true })
267
- }).optional();
268
- var ConventionsConfigSchema = z.object({
269
- claudeDirName: z.string().default(".claude").refine(
270
- (s) => !s.includes("..") && !s.startsWith("/"),
271
- { message: 'claudeDirName must not contain ".." or start with "/"' }
272
- ),
273
- sessionStatePath: z.string().default(".claude/session-state/CURRENT.md").refine(
274
- (s) => !s.includes("..") && !s.startsWith("/"),
275
- { message: 'sessionStatePath must not contain ".." or start with "/"' }
276
- ),
277
- sessionArchivePath: z.string().default(".claude/session-state/archive").refine(
278
- (s) => !s.includes("..") && !s.startsWith("/"),
279
- { message: 'sessionArchivePath must not contain ".." or start with "/"' }
280
- ),
281
- knowledgeCategories: z.array(z.string()).default([
282
- "patterns",
283
- "commands",
284
- "incidents",
285
- "reference",
286
- "protocols",
287
- "checklists",
288
- "playbooks",
289
- "critical",
290
- "scripts",
291
- "status",
292
- "templates",
293
- "loop-state",
294
- "session-state",
295
- "agents"
296
- ]),
297
- knowledgeSourceFiles: z.array(z.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
298
- excludePatterns: z.array(z.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
299
- }).optional();
300
- var PythonDomainConfigSchema = z.object({
301
- name: z.string(),
302
- packages: z.array(z.string()),
303
- allowed_imports_from: z.array(z.string()).default([])
103
+ var MASSU_ENV_API_KEY, MASSU_ENV_CLOUD_ENDPOINT, DEFAULT_CLOUD_ENDPOINT;
104
+ var init_credentials = __esm({
105
+ "src/credentials.ts"() {
106
+ "use strict";
107
+ MASSU_ENV_API_KEY = "MASSU_API_KEY";
108
+ MASSU_ENV_CLOUD_ENDPOINT = "MASSU_CLOUD_ENDPOINT";
109
+ DEFAULT_CLOUD_ENDPOINT = "https://api.massu.ai/v1";
110
+ }
304
111
  });
305
- var PythonConfigSchema = z.object({
306
- root: z.string(),
307
- alembic_dir: z.string().optional(),
308
- domains: z.array(PythonDomainConfigSchema).default([]),
309
- exclude_dirs: z.array(z.string()).default(["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"])
310
- }).optional();
311
- var PathsConfigSchema = z.object({
312
- source: z.string().default("src"),
313
- aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
314
- monorepo_roots: z.array(z.string()).optional(),
315
- routers: z.string().optional(),
316
- routerRoot: z.string().optional(),
317
- pages: z.string().optional(),
318
- middleware: z.string().optional(),
319
- schema: z.string().optional(),
320
- components: z.string().optional(),
321
- hooks: z.string().optional()
112
+
113
+ // src/config-memory-schema.ts
114
+ import { z } from "zod";
115
+ var MemoryConfigSchema;
116
+ var init_config_memory_schema = __esm({
117
+ "src/config-memory-schema.ts"() {
118
+ "use strict";
119
+ MemoryConfigSchema = z.object({
120
+ recall: z.object({
121
+ enabled: z.boolean().default(true),
122
+ maxTokens: z.number().int().positive().default(1200),
123
+ sources: z.array(
124
+ z.enum([
125
+ "observation",
126
+ "architecture_decision",
127
+ "knowledge_chunk",
128
+ "failure_class"
129
+ ])
130
+ ).default(["observation", "architecture_decision", "knowledge_chunk", "failure_class"]),
131
+ timeoutMs: z.number().int().positive().default(8e3),
132
+ limit: z.number().int().positive().default(8),
133
+ minScore: z.number().min(0).default(0),
134
+ // --- Semantic embedder (plan-living-memory-slice-2a-embedder, P3-001) ---
135
+ // embedEnabled: Tier-1 bundled WASM embedder on by default; false forces
136
+ // Tier-2 (FTS keyword-only) recall.
137
+ // embedEndpoint: optional Tier-0 OpenAI-compatible /v1/embeddings provider
138
+ // (Ollama, LM Studio, vLLM, or any hosted API). When set, embedding egresses ONLY to this
139
+ // operator-chosen endpoint; unset means zero egress (bundled model).
140
+ // embedModel: optional Tier-0 model name sent to that endpoint.
141
+ embedEnabled: z.boolean().default(true),
142
+ embedEndpoint: z.string().url().refine((s) => /^https?:\/\//i.test(s), {
143
+ message: "memory.recall.embedEndpoint must be an http(s) URL"
144
+ }).optional(),
145
+ embedModel: z.string().min(1).optional()
146
+ }).default({}),
147
+ // --- Contradiction / supersede gate (plan-living-memory-slice-2-temporal-model, P5-001) ---
148
+ // When a new high-value memory (decision/correction) is written, find
149
+ // semantically-related existing records and, if the new one contradicts an
150
+ // old one, supersede-don't-delete the old row. Fully fail-open + gated to a
151
+ // small set of high-value types so the hot capture path is untouched.
152
+ contradiction: z.object({
153
+ // Master switch. false → every write is a plain insert (prior behavior).
154
+ enabled: z.boolean().default(true),
155
+ // Optional Tier-0 external judge (OpenAI-compatible). null/unset → local
156
+ // heuristic only, ZERO egress. When set, candidate + related records egress
157
+ // ONLY to this operator-chosen endpoint; any error falls back to heuristic.
158
+ judgeEndpoint: z.string().url().refine((s) => /^https?:\/\//i.test(s), {
159
+ message: "memory.contradiction.judgeEndpoint must be an http(s) URL"
160
+ }).optional(),
161
+ // Cosine similarity above which a correction-flavored new record is treated
162
+ // as superseding a related existing record. Calibrated to all-MiniLM-L6-v2
163
+ // (same-topic contradictions ≈0.65–0.86; related-but-complementary ≈0.47).
164
+ similarityThreshold: z.number().min(0).max(1).default(0.6),
165
+ // Cosine similarity above which a new record is a near-duplicate (NOOP).
166
+ dedupThreshold: z.number().min(0).max(1).default(0.93),
167
+ // Observation types the gate runs for. High-volume 'file_change' is
168
+ // deliberately excluded so the hot path never triggers a hybridSearch.
169
+ gatedTypes: z.array(z.string()).default(["decision", "cr_violation", "failed_attempt"]),
170
+ // When true, superseded rows may still surface in recall with a
171
+ // "(superseded on <date> by #<id>)" annotation instead of being excluded.
172
+ annotateSuperseded: z.boolean().default(false),
173
+ // Time budget (ms) for the contradiction check; exceeded → fail-open (ADD).
174
+ budgetMs: z.number().int().positive().default(800)
175
+ }).default({}),
176
+ // --- Background consolidation, the "sleep-time" pass
177
+ // (plan-living-memory-slice-3-consolidation) ---
178
+ // Keeps memory sharp over years: dedupes, distills dying sessions into
179
+ // durable lessons, spots corrections you keep repeating, reweights by what
180
+ // actually gets used, and retires dead weight by EXPIRING it (never deleting).
181
+ //
182
+ // Runs with ZERO LLM and ZERO network by default — every stage is arithmetic
183
+ // plus the embedding model Massu already bundles. `llmEndpoint` is OPTIONAL
184
+ // and upgrades the prose of session summaries ONLY.
185
+ consolidation: z.object({
186
+ enabled: z.boolean().default(true),
187
+ // Bounded sweep at session end — the automatic path (no scheduler needed).
188
+ sessionSweepEnabled: z.boolean().default(true),
189
+ // OPTIONAL local/remote OpenAI-compatible chat endpoint. UNSET = zero egress.
190
+ // When set, session text egresses ONLY to this endpoint you chose.
191
+ // NOTE: the API key is NEVER configured here — it is read exclusively from
192
+ // the MASSU_MEMORY_LLM_API_KEY env var, so a key can never be committed.
193
+ llmEndpoint: z.string().url().refine((s) => /^https?:\/\//i.test(s), {
194
+ message: "memory.consolidation.llmEndpoint must be an http(s) URL"
195
+ }).optional(),
196
+ // Model NAME/ALIAS sent to llmEndpoint (e.g. "llama3.1:8b").
197
+ llmModel: z.string().optional(),
198
+ // Distill a session once its newest turn is older than this. Must stay
199
+ // INSIDE the 7-day conversation_turns prune window or the raw material is
200
+ // destroyed before it is ever summarized.
201
+ summarizeAfterDays: z.number().int().positive().default(5),
202
+ // Age past which an unprotected, never-retrieved, low-importance row may expire.
203
+ retentionDays: z.number().int().positive().default(90),
204
+ importanceFloor: z.number().int().min(1).max(5).default(2),
205
+ // Types that may NEVER expire, however old.
206
+ protectedTypes: z.array(z.string()).default(["decision", "cr_violation", "incident_near_miss"]),
207
+ // Days the retrieval counter must observe usage BEFORE any expiry is armed.
208
+ // The cold-start guard: on a fresh counter nothing has "ever been
209
+ // retrieved", so without this the first pass would gut the store.
210
+ usageWarmupDays: z.number().int().nonnegative().default(30),
211
+ // Per-pass decay on the windowed hit count, so usefulness must be sustained.
212
+ usageDecay: z.number().min(0).max(1).default(0.9),
213
+ // A record is reweighted at most once per this many days (idempotency).
214
+ reweightIntervalDays: z.number().int().positive().default(1),
215
+ // Recurrences (across >= 2 sessions) before a rule candidate is proposed.
216
+ promoteMinOccurrences: z.number().int().positive().default(3),
217
+ budgetMs: z.number().int().positive().default(3e3),
218
+ // Surface optional upgrades (e.g. "a local model was detected") in chat.
219
+ suggestUpgrades: z.boolean().default(true),
220
+ suggestIntervalDays: z.number().int().positive().default(30)
221
+ }).default({}),
222
+ // A-20 (Slice 4) — memory FILES: the mirror, and the one switch that decides whether
223
+ // Massu may ever WRITE into the user's memory directory.
224
+ //
225
+ // ⛔ `renderEnabled` DEFAULTS TO FALSE. 4B is the first capability in Massu's history
226
+ // that writes files into the place the user keeps their own hand-written prose. A new
227
+ // write capability that arrives switched-on in an `npm update` is a capability nobody
228
+ // consented to. The path is: the advisor OFFERS it in chat -> the user runs
229
+ // `massu memory render --dry-run` and sees exactly what WOULD be written -> the user
230
+ // turns it on. Three deliberate steps, none implicit. A drift-guard pins this default.
231
+ files: z.object({
232
+ // The lossless file<->store mirror (ingest side). Read-only; it writes nothing.
233
+ enabled: z.boolean().default(true),
234
+ // The ONLY flag here that grants a WRITE. Never auto-enable.
235
+ renderEnabled: z.boolean().default(false),
236
+ // Anti-spam: files Massu may render in ONE session.
237
+ renderMaxFilesPerSession: z.number().int().min(0).default(3),
238
+ // Only memories at/above this importance are worth a durable file.
239
+ renderMinImportance: z.number().int().min(1).max(5).default(4),
240
+ // The clearly-labelled MEMORY.md section Massu's pointers live under.
241
+ indexSection: z.string().default("Learned by Massu"),
242
+ // Hard bound on the managed MEMORY.md region. MEMORY.md is auto-loaded into EVERY
243
+ // turn of EVERY session, so an unbounded index is a permanent context tax — and the
244
+ // per-session cap bounds only the RATE, never the total.
245
+ indexMaxLines: z.number().int().min(1).default(50)
246
+ }).default({})
247
+ }).optional();
248
+ }
322
249
  });
323
- var LanguageFrameworkEntrySchema = z.object({
324
- framework: z.string().optional(),
325
- test_framework: z.string().optional(),
326
- test: z.string().optional(),
327
- runtime: z.string().optional(),
328
- orm: z.string().optional(),
329
- router: z.string().optional(),
330
- ui: z.string().optional()
331
- }).passthrough();
332
- var FrameworkConfigSchema = z.object({
333
- type: z.string().default("typescript"),
334
- primary: z.string().optional(),
335
- router: z.string().default("none"),
336
- orm: z.string().default("none"),
337
- ui: z.string().default("none"),
338
- languages: z.record(z.string(), LanguageFrameworkEntrySchema).optional()
339
- }).passthrough();
340
- var DetectedConfigSchema = z.object({}).passthrough().optional();
341
- var VerificationEntrySchema = z.object({
342
- type: z.string().optional(),
343
- test: z.string().optional(),
344
- syntax: z.string().optional(),
345
- lint: z.string().optional(),
346
- build: z.string().optional()
347
- }).passthrough();
348
- var VerificationConfigSchema = z.record(z.string(), VerificationEntrySchema).optional();
349
- var CanonicalPathsSchema = z.record(z.string(), z.string()).optional();
350
- var VerificationTypesSchema = z.record(z.string(), z.string()).optional();
351
- var DetectionRuleEntrySchema = z.object({
352
- signals: z.array(z.string()).default([]),
353
- priority: z.number().optional()
354
- }).passthrough();
355
- var DetectionConfigSchema = z.object({
356
- rules: z.record(
357
- z.string(),
358
- // language
359
- z.record(z.string(), DetectionRuleEntrySchema)
360
- // framework -> rule entry
361
- ).optional(),
362
- signal_weights: z.record(z.string(), z.number()).optional(),
363
- disable_builtin: z.boolean().optional()
364
- }).passthrough().optional();
365
- var WatchConfigSchema = z.object({
366
- debounce_ms: z.number().int().positive().default(3e3),
367
- storm_threshold: z.number().int().positive().default(50),
368
- deep_storm_threshold: z.number().int().positive().default(500),
369
- hard_timeout_ms: z.number().int().positive().default(3e5),
370
- scope: z.enum(["paths", "full"]).default("paths"),
371
- // Plan 3a hotfix 2026-05-02: refuse to start if the watch surface
372
- // exceeds this many files. Prevents the misconfig pattern where
373
- // `paths.source_dirs` includes `.` or otherwise expands to a 60K+
374
- // file tree, producing 30-100% steady CPU. Override via
375
- // `paths_full_root_opt_in: true` for users on small repos who genuinely
376
- // need root-level watching.
377
- max_watched_files: z.number().int().positive().default(1e4),
378
- paths_full_root_opt_in: z.boolean().default(false)
379
- }).passthrough().optional();
380
- var AdapterLocalPathSchema = z.string().refine((s) => !/^([A-Za-z]:[\\/]|[\\/])/.test(s), {
381
- message: "absolute paths are rejected; adapters.local entries must be relative to the massu.config.yaml directory"
382
- }).refine((s) => !s.split(/[\\/]/).includes(".."), {
383
- message: "parent-directory traversal (`..`) is rejected; adapters.local entries must stay inside the project tree"
384
- }).transform((s) => s.split(/[\\/]/).filter((part) => part !== "" && part !== ".").join("/"));
385
- var AdaptersConfigSchema = z.object({
386
- enabled: z.boolean().default(false),
387
- local: z.array(AdapterLocalPathSchema).default([])
388
- }).passthrough().optional();
389
- var TelemetryConfigSchema = z.object({
390
- adapters: z.boolean().default(false)
391
- }).passthrough().optional();
392
- var LSPConfigSchema = z.object({
393
- enabled: z.boolean().default(false),
394
- servers: z.array(z.object({
395
- language: z.string(),
396
- command: z.string(),
397
- // F-014 (closed 2026-05-06): explicit opt-in to spawn SUID/SGID
398
- // binaries. Default false — argv[0] with the SUID bit is rejected
399
- // unless this is true. Decision is auditable in the YAML.
400
- allow_setuid: z.boolean().default(false),
401
- // F-015 (closed 2026-05-06): per-server RSS budget (MB). Watchdog
402
- // SIGKILLs the server after sustained breach. Default 1024 MB.
403
- // Set to 0 to disable the watchdog for this server.
404
- max_rss_mb: z.number().int().nonnegative().default(1024)
405
- })).default([]),
406
- autoDetect: z.object({
407
- viaPortScan: z.boolean().default(false)
408
- }).optional()
409
- }).passthrough();
410
- var RawConfigSchema = z.object({
411
- schema_version: z.union([z.literal(1), z.literal(2)]).default(1),
412
- project: z.object({
413
- name: z.string().default("my-project"),
414
- root: z.string().default("auto")
415
- }).default({ name: "my-project", root: "auto" }),
416
- framework: FrameworkConfigSchema.default({
417
- type: "typescript",
418
- router: "none",
419
- orm: "none",
420
- ui: "none"
421
- }),
422
- paths: PathsConfigSchema.default({ source: "src", aliases: { "@": "src" } }),
423
- toolPrefix: z.string().default("massu"),
424
- dbAccessPattern: z.string().optional(),
425
- knownMismatches: z.record(z.string(), z.record(z.string(), z.string())).optional(),
426
- accessScopes: z.array(z.string()).optional(),
427
- domains: z.array(DomainConfigSchema).default([]),
428
- rules: z.array(PatternRuleConfigSchema).default([]),
429
- // P-M-036 (plan-stage-d-medium-sweep): customer-authored CR-style
430
- // governance rules. DISTINCT from `rules:` above (path-scoped lint hints
431
- // used by pattern-scanner). At config-refresh time these entries are
432
- // loaded into the `knowledge_rules` SQLite table with
433
- // `source = 'customer-config'` so `massu_knowledge_rule` and the
434
- // governance docs surface customer-defined rules alongside framework CRs.
435
- governance_rules: z.array(
436
- z.object({
437
- id: z.string().min(1, "governance_rules[].id is required"),
438
- title: z.string().min(1, "governance_rules[].title is required"),
439
- description: z.string().min(1, "governance_rules[].description is required"),
440
- vr_type: z.string().default("VR-CUSTOM"),
441
- reference_path: z.string().optional(),
442
- severity: z.enum(["critical", "high", "medium", "low", "info"]).default("medium")
443
- }).passthrough()
444
- ).default([]),
445
- analytics: AnalyticsConfigSchema,
446
- governance: GovernanceConfigSchema,
447
- security: SecurityConfigSchema,
448
- team: TeamConfigSchema,
449
- regression: RegressionConfigSchema,
450
- cloud: CloudConfigSchema,
451
- conventions: ConventionsConfigSchema,
452
- autoLearning: AutoLearningConfigSchema,
453
- python: PythonConfigSchema,
454
- // P2-004 / P2-005 / P2-006 / P2-008: v2 extensions (all optional)
455
- verification: VerificationConfigSchema,
456
- canonical_paths: CanonicalPathsSchema,
457
- verification_types: VerificationTypesSchema,
458
- detection: DetectionConfigSchema,
459
- // Plan #2: detector-owned per-language conventions (free-form passthrough)
460
- detected: DetectedConfigSchema,
461
- // Plan 3a: file-watcher daemon tunables
462
- watch: WatchConfigSchema,
463
- // Plan 3c: third-party adapter registry kill-switch + signing override + local-path opt-in.
464
- adapters: AdaptersConfigSchema,
465
- // Plan 3c: anonymous adapter-discovery telemetry opt-in (default off).
466
- telemetry: TelemetryConfigSchema,
467
- // Plan 3b Phase 4: optional LSP enrichment of AST adapter results.
468
- lsp: LSPConfigSchema.optional()
469
- }).passthrough();
470
- var _config = null;
471
- var _resolvedApiKeySource = "none";
472
- var _projectRoot = null;
250
+
251
+ // src/config.ts
252
+ import { resolve as resolve2, dirname } from "path";
253
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
254
+ import { homedir as homedir2 } from "os";
255
+ import { parse as parseYaml } from "yaml";
256
+ import { z as z2 } from "zod";
473
257
  function findProjectRoot() {
474
258
  const cwd = process.cwd();
475
259
  let dir = cwd;
@@ -569,6 +353,7 @@ Hint: run \`massu config refresh\` to regenerate a valid config or fix the liste
569
353
  team: parsed.team,
570
354
  regression: parsed.regression,
571
355
  cloud: parsed.cloud,
356
+ memory: parsed.memory,
572
357
  conventions: parsed.conventions,
573
358
  autoLearning: parsed.autoLearning,
574
359
  python: parsed.python,
@@ -633,20 +418,1246 @@ function getResolvedPaths() {
633
418
  settingsLocalPath: resolve2(root, claudeDirName, "settings.local.json")
634
419
  };
635
420
  }
421
+ var DomainConfigSchema, PatternRuleConfigSchema, CostModelSchema, AnalyticsConfigSchema, CustomPatternSchema, GovernanceConfigSchema, SecurityPatternSchema, SecurityConfigSchema, TeamConfigSchema, RegressionConfigSchema, AutoLearningConfigSchema, CloudConfigSchema, ConventionsConfigSchema, PythonDomainConfigSchema, PythonConfigSchema, PathsConfigSchema, LanguageFrameworkEntrySchema, FrameworkConfigSchema, DetectedConfigSchema, VerificationEntrySchema, VerificationConfigSchema, CanonicalPathsSchema, VerificationTypesSchema, DetectionRuleEntrySchema, DetectionConfigSchema, WatchConfigSchema, AdapterLocalPathSchema, AdaptersConfigSchema, TelemetryConfigSchema, LSPConfigSchema, RawConfigSchema, _config, _resolvedApiKeySource, _projectRoot;
422
+ var init_config = __esm({
423
+ "src/config.ts"() {
424
+ "use strict";
425
+ init_memory_path();
426
+ init_credentials();
427
+ init_config_memory_schema();
428
+ DomainConfigSchema = z2.object({
429
+ name: z2.string().default("Unknown"),
430
+ routers: z2.array(z2.string()).default([]),
431
+ pages: z2.array(z2.string()).default([]),
432
+ tables: z2.array(z2.string()).default([]),
433
+ allowedImportsFrom: z2.array(z2.string()).default([])
434
+ });
435
+ PatternRuleConfigSchema = z2.object({
436
+ pattern: z2.string().default("**"),
437
+ rules: z2.array(z2.string()).default([]),
438
+ language: z2.string().optional()
439
+ });
440
+ CostModelSchema = z2.object({
441
+ input_per_million: z2.number(),
442
+ output_per_million: z2.number(),
443
+ cache_read_per_million: z2.number().optional(),
444
+ cache_write_per_million: z2.number().optional()
445
+ });
446
+ AnalyticsConfigSchema = z2.object({
447
+ quality: z2.object({
448
+ weights: z2.record(z2.string(), z2.number()).default({
449
+ bug_found: -5,
450
+ vr_failure: -10,
451
+ incident: -20,
452
+ cr_violation: -3,
453
+ vr_pass: 2,
454
+ clean_commit: 5,
455
+ successful_verification: 3
456
+ }),
457
+ categories: z2.array(z2.string()).default(["security", "architecture", "coupling", "tests", "rule_compliance"])
458
+ }).optional(),
459
+ cost: z2.object({
460
+ models: z2.record(z2.string(), CostModelSchema).default({}),
461
+ currency: z2.string().default("USD")
462
+ }).optional(),
463
+ prompts: z2.object({
464
+ success_indicators: z2.array(z2.string()).default(["committed", "approved", "looks good", "perfect", "great", "thanks"]),
465
+ failure_indicators: z2.array(z2.string()).default(["revert", "wrong", "that's not", "undo", "incorrect"]),
466
+ max_turns_for_success: z2.number().default(2)
467
+ }).optional()
468
+ }).optional();
469
+ CustomPatternSchema = z2.object({
470
+ pattern: z2.string(),
471
+ severity: z2.string(),
472
+ message: z2.string()
473
+ });
474
+ GovernanceConfigSchema = z2.object({
475
+ audit: z2.object({
476
+ formats: z2.array(z2.string()).default(["summary", "detailed", "soc2"]),
477
+ retention_days: z2.number().default(365),
478
+ auto_log: z2.record(z2.string(), z2.boolean()).default({
479
+ code_changes: true,
480
+ rule_enforcement: true,
481
+ approvals: true,
482
+ commits: true
483
+ })
484
+ }).optional(),
485
+ validation: z2.object({
486
+ realtime: z2.boolean().default(true),
487
+ checks: z2.record(z2.string(), z2.boolean()).default({
488
+ rule_compliance: true,
489
+ import_existence: true,
490
+ naming_conventions: true
491
+ }),
492
+ custom_patterns: z2.array(CustomPatternSchema).default([])
493
+ }).optional(),
494
+ adr: z2.object({
495
+ detection_phrases: z2.array(z2.string()).default(["chose", "decided", "switching to", "moving from", "going with"]),
496
+ template: z2.string().default("default"),
497
+ storage: z2.string().default("database"),
498
+ output_dir: z2.string().default("docs/adr")
499
+ }).optional()
500
+ }).optional();
501
+ SecurityPatternSchema = z2.object({
502
+ pattern: z2.string(),
503
+ severity: z2.string(),
504
+ category: z2.string(),
505
+ description: z2.string()
506
+ });
507
+ SecurityConfigSchema = z2.object({
508
+ patterns: z2.array(SecurityPatternSchema).default([]),
509
+ auto_score_on_edit: z2.boolean().default(true),
510
+ score_threshold_alert: z2.number().default(50),
511
+ severity_weights: z2.record(z2.string(), z2.number()).optional(),
512
+ restrictive_licenses: z2.array(z2.string()).optional(),
513
+ dep_alternatives: z2.record(z2.string(), z2.array(z2.string())).optional(),
514
+ dependencies: z2.object({
515
+ package_manager: z2.string().default("npm"),
516
+ blocked_packages: z2.array(z2.string()).default([]),
517
+ preferred_packages: z2.record(z2.string(), z2.string()).default({}),
518
+ max_bundle_size_kb: z2.number().default(500)
519
+ }).optional()
520
+ }).optional();
521
+ TeamConfigSchema = z2.object({
522
+ enabled: z2.boolean().default(false),
523
+ sync_backend: z2.string().default("local"),
524
+ developer_id: z2.string().default("auto"),
525
+ share_by_default: z2.boolean().default(false),
526
+ expertise_weights: z2.object({
527
+ session: z2.number().default(20),
528
+ observation: z2.number().default(10)
529
+ }).optional(),
530
+ privacy: z2.object({
531
+ share_file_paths: z2.boolean().default(true),
532
+ share_code_snippets: z2.boolean().default(false),
533
+ share_observations: z2.boolean().default(true)
534
+ }).optional()
535
+ }).optional();
536
+ RegressionConfigSchema = z2.object({
537
+ test_patterns: z2.array(z2.string()).default([
538
+ "{dir}/__tests__/{name}.test.{ext}",
539
+ "{dir}/{name}.spec.{ext}",
540
+ "tests/{path}.test.{ext}"
541
+ ]),
542
+ test_runner: z2.string().default("npm test"),
543
+ health_thresholds: z2.object({
544
+ healthy: z2.number().default(80),
545
+ warning: z2.number().default(50)
546
+ }).optional()
547
+ }).optional();
548
+ AutoLearningConfigSchema = z2.object({
549
+ enabled: z2.boolean().default(true),
550
+ incidentDir: z2.string().default("docs/incidents"),
551
+ memoryDir: z2.string().default("memory"),
552
+ memoryIndexFile: z2.string().default("MEMORY.md"),
553
+ enforcementHooksDir: z2.string().default("scripts/hooks"),
554
+ fixDetection: z2.object({
555
+ enabled: z2.boolean().default(true),
556
+ lookbackDays: z2.number().default(7),
557
+ signals: z2.array(z2.string()).default([
558
+ "removed_broken_code",
559
+ "added_error_handling",
560
+ "method_name_correction",
561
+ "auth_fix",
562
+ "nil_handling_fix",
563
+ "concurrency_fix",
564
+ "async_pattern_fix",
565
+ "added_missing_import"
566
+ ])
567
+ }).default({}),
568
+ failureClassification: z2.object({
569
+ enabled: z2.boolean().default(true),
570
+ thresholds: z2.object({
571
+ known: z2.number().default(5),
572
+ similar: z2.number().default(3)
573
+ }).default({}),
574
+ scoring: z2.object({
575
+ diffPatternWeight: z2.number().default(3),
576
+ filePatternWeight: z2.number().default(2),
577
+ promptKeywordWeight: z2.number().default(2)
578
+ }).default({})
579
+ }).default({}),
580
+ pipeline: z2.object({
581
+ requireIncidentReport: z2.boolean().default(true),
582
+ requirePreventionRule: z2.boolean().default(true),
583
+ requireEnforcement: z2.boolean().default(true)
584
+ }).default({}),
585
+ // plan-v0.2-interactive-rule-approval P-D-008 / P-D-009: project-configured
586
+ // custom destinations for the rule-candidate funnel. The classifier matches
587
+ // a candidate to one of these entries when none of the framework
588
+ // destinations (pattern-scanner / claude-md-cr / corrections-md) apply.
589
+ customDestinations: z2.array(z2.object({
590
+ name: z2.string(),
591
+ path: z2.string(),
592
+ triggerKeywords: z2.array(z2.string()).default([]),
593
+ template: z2.string()
594
+ })).default([])
595
+ }).optional();
596
+ CloudConfigSchema = z2.object({
597
+ enabled: z2.boolean().default(false),
598
+ apiKey: z2.string().optional(),
599
+ endpoint: z2.string().optional(),
600
+ sync: z2.object({
601
+ memory: z2.boolean().default(true),
602
+ analytics: z2.boolean().default(true),
603
+ audit: z2.boolean().default(true)
604
+ }).default({ memory: true, analytics: true, audit: true })
605
+ }).optional();
606
+ ConventionsConfigSchema = z2.object({
607
+ claudeDirName: z2.string().default(".claude").refine(
608
+ (s) => !s.includes("..") && !s.startsWith("/"),
609
+ { message: 'claudeDirName must not contain ".." or start with "/"' }
610
+ ),
611
+ sessionStatePath: z2.string().default(".claude/session-state/CURRENT.md").refine(
612
+ (s) => !s.includes("..") && !s.startsWith("/"),
613
+ { message: 'sessionStatePath must not contain ".." or start with "/"' }
614
+ ),
615
+ sessionArchivePath: z2.string().default(".claude/session-state/archive").refine(
616
+ (s) => !s.includes("..") && !s.startsWith("/"),
617
+ { message: 'sessionArchivePath must not contain ".." or start with "/"' }
618
+ ),
619
+ knowledgeCategories: z2.array(z2.string()).default([
620
+ "patterns",
621
+ "commands",
622
+ "incidents",
623
+ "reference",
624
+ "protocols",
625
+ "checklists",
626
+ "playbooks",
627
+ "critical",
628
+ "scripts",
629
+ "status",
630
+ "templates",
631
+ "loop-state",
632
+ "session-state",
633
+ "agents"
634
+ ]),
635
+ knowledgeSourceFiles: z2.array(z2.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
636
+ excludePatterns: z2.array(z2.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
637
+ }).optional();
638
+ PythonDomainConfigSchema = z2.object({
639
+ name: z2.string(),
640
+ packages: z2.array(z2.string()),
641
+ allowed_imports_from: z2.array(z2.string()).default([])
642
+ });
643
+ PythonConfigSchema = z2.object({
644
+ root: z2.string(),
645
+ alembic_dir: z2.string().optional(),
646
+ domains: z2.array(PythonDomainConfigSchema).default([]),
647
+ exclude_dirs: z2.array(z2.string()).default(["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"])
648
+ }).optional();
649
+ PathsConfigSchema = z2.object({
650
+ source: z2.string().default("src"),
651
+ aliases: z2.record(z2.string(), z2.string()).default({ "@": "src" }),
652
+ monorepo_roots: z2.array(z2.string()).optional(),
653
+ routers: z2.string().optional(),
654
+ routerRoot: z2.string().optional(),
655
+ pages: z2.string().optional(),
656
+ middleware: z2.string().optional(),
657
+ schema: z2.string().optional(),
658
+ components: z2.string().optional(),
659
+ hooks: z2.string().optional()
660
+ });
661
+ LanguageFrameworkEntrySchema = z2.object({
662
+ framework: z2.string().optional(),
663
+ test_framework: z2.string().optional(),
664
+ test: z2.string().optional(),
665
+ runtime: z2.string().optional(),
666
+ orm: z2.string().optional(),
667
+ router: z2.string().optional(),
668
+ ui: z2.string().optional()
669
+ }).passthrough();
670
+ FrameworkConfigSchema = z2.object({
671
+ type: z2.string().default("typescript"),
672
+ primary: z2.string().optional(),
673
+ router: z2.string().default("none"),
674
+ orm: z2.string().default("none"),
675
+ ui: z2.string().default("none"),
676
+ languages: z2.record(z2.string(), LanguageFrameworkEntrySchema).optional()
677
+ }).passthrough();
678
+ DetectedConfigSchema = z2.object({}).passthrough().optional();
679
+ VerificationEntrySchema = z2.object({
680
+ type: z2.string().optional(),
681
+ test: z2.string().optional(),
682
+ syntax: z2.string().optional(),
683
+ lint: z2.string().optional(),
684
+ build: z2.string().optional()
685
+ }).passthrough();
686
+ VerificationConfigSchema = z2.record(z2.string(), VerificationEntrySchema).optional();
687
+ CanonicalPathsSchema = z2.record(z2.string(), z2.string()).optional();
688
+ VerificationTypesSchema = z2.record(z2.string(), z2.string()).optional();
689
+ DetectionRuleEntrySchema = z2.object({
690
+ signals: z2.array(z2.string()).default([]),
691
+ priority: z2.number().optional()
692
+ }).passthrough();
693
+ DetectionConfigSchema = z2.object({
694
+ rules: z2.record(
695
+ z2.string(),
696
+ // language
697
+ z2.record(z2.string(), DetectionRuleEntrySchema)
698
+ // framework -> rule entry
699
+ ).optional(),
700
+ signal_weights: z2.record(z2.string(), z2.number()).optional(),
701
+ disable_builtin: z2.boolean().optional()
702
+ }).passthrough().optional();
703
+ WatchConfigSchema = z2.object({
704
+ debounce_ms: z2.number().int().positive().default(3e3),
705
+ storm_threshold: z2.number().int().positive().default(50),
706
+ deep_storm_threshold: z2.number().int().positive().default(500),
707
+ hard_timeout_ms: z2.number().int().positive().default(3e5),
708
+ scope: z2.enum(["paths", "full"]).default("paths"),
709
+ // Plan 3a hotfix 2026-05-02: refuse to start if the watch surface
710
+ // exceeds this many files. Prevents the misconfig pattern where
711
+ // `paths.source_dirs` includes `.` or otherwise expands to a 60K+
712
+ // file tree, producing 30-100% steady CPU. Override via
713
+ // `paths_full_root_opt_in: true` for users on small repos who genuinely
714
+ // need root-level watching.
715
+ max_watched_files: z2.number().int().positive().default(1e4),
716
+ paths_full_root_opt_in: z2.boolean().default(false)
717
+ }).passthrough().optional();
718
+ AdapterLocalPathSchema = z2.string().refine((s) => !/^([A-Za-z]:[\\/]|[\\/])/.test(s), {
719
+ message: "absolute paths are rejected; adapters.local entries must be relative to the massu.config.yaml directory"
720
+ }).refine((s) => !s.split(/[\\/]/).includes(".."), {
721
+ message: "parent-directory traversal (`..`) is rejected; adapters.local entries must stay inside the project tree"
722
+ }).transform((s) => s.split(/[\\/]/).filter((part) => part !== "" && part !== ".").join("/"));
723
+ AdaptersConfigSchema = z2.object({
724
+ enabled: z2.boolean().default(false),
725
+ local: z2.array(AdapterLocalPathSchema).default([])
726
+ }).passthrough().optional();
727
+ TelemetryConfigSchema = z2.object({
728
+ adapters: z2.boolean().default(false)
729
+ }).passthrough().optional();
730
+ LSPConfigSchema = z2.object({
731
+ enabled: z2.boolean().default(false),
732
+ servers: z2.array(z2.object({
733
+ language: z2.string(),
734
+ command: z2.string(),
735
+ // F-014 (closed 2026-05-06): explicit opt-in to spawn SUID/SGID
736
+ // binaries. Default false — argv[0] with the SUID bit is rejected
737
+ // unless this is true. Decision is auditable in the YAML.
738
+ allow_setuid: z2.boolean().default(false),
739
+ // F-015 (closed 2026-05-06): per-server RSS budget (MB). Watchdog
740
+ // SIGKILLs the server after sustained breach. Default 1024 MB.
741
+ // Set to 0 to disable the watchdog for this server.
742
+ max_rss_mb: z2.number().int().nonnegative().default(1024)
743
+ })).default([]),
744
+ autoDetect: z2.object({
745
+ viaPortScan: z2.boolean().default(false)
746
+ }).optional()
747
+ }).passthrough();
748
+ RawConfigSchema = z2.object({
749
+ schema_version: z2.union([z2.literal(1), z2.literal(2)]).default(1),
750
+ project: z2.object({
751
+ name: z2.string().default("my-project"),
752
+ root: z2.string().default("auto")
753
+ }).default({ name: "my-project", root: "auto" }),
754
+ framework: FrameworkConfigSchema.default({
755
+ type: "typescript",
756
+ router: "none",
757
+ orm: "none",
758
+ ui: "none"
759
+ }),
760
+ paths: PathsConfigSchema.default({ source: "src", aliases: { "@": "src" } }),
761
+ toolPrefix: z2.string().default("massu"),
762
+ dbAccessPattern: z2.string().optional(),
763
+ knownMismatches: z2.record(z2.string(), z2.record(z2.string(), z2.string())).optional(),
764
+ accessScopes: z2.array(z2.string()).optional(),
765
+ domains: z2.array(DomainConfigSchema).default([]),
766
+ rules: z2.array(PatternRuleConfigSchema).default([]),
767
+ // P-M-036 (plan-stage-d-medium-sweep): customer-authored CR-style
768
+ // governance rules. DISTINCT from `rules:` above (path-scoped lint hints
769
+ // used by pattern-scanner). At config-refresh time these entries are
770
+ // loaded into the `knowledge_rules` SQLite table with
771
+ // `source = 'customer-config'` so `massu_knowledge_rule` and the
772
+ // governance docs surface customer-defined rules alongside framework CRs.
773
+ governance_rules: z2.array(
774
+ z2.object({
775
+ id: z2.string().min(1, "governance_rules[].id is required"),
776
+ title: z2.string().min(1, "governance_rules[].title is required"),
777
+ description: z2.string().min(1, "governance_rules[].description is required"),
778
+ vr_type: z2.string().default("VR-CUSTOM"),
779
+ reference_path: z2.string().optional(),
780
+ severity: z2.enum(["critical", "high", "medium", "low", "info"]).default("medium")
781
+ }).passthrough()
782
+ ).default([]),
783
+ analytics: AnalyticsConfigSchema,
784
+ governance: GovernanceConfigSchema,
785
+ security: SecurityConfigSchema,
786
+ team: TeamConfigSchema,
787
+ regression: RegressionConfigSchema,
788
+ cloud: CloudConfigSchema,
789
+ // plan-living-memory-slice-1 P6-002: automatic-recall tunables.
790
+ memory: MemoryConfigSchema,
791
+ conventions: ConventionsConfigSchema,
792
+ autoLearning: AutoLearningConfigSchema,
793
+ python: PythonConfigSchema,
794
+ // P2-004 / P2-005 / P2-006 / P2-008: v2 extensions (all optional)
795
+ verification: VerificationConfigSchema,
796
+ canonical_paths: CanonicalPathsSchema,
797
+ verification_types: VerificationTypesSchema,
798
+ detection: DetectionConfigSchema,
799
+ // Plan #2: detector-owned per-language conventions (free-form passthrough)
800
+ detected: DetectedConfigSchema,
801
+ // Plan 3a: file-watcher daemon tunables
802
+ watch: WatchConfigSchema,
803
+ // Plan 3c: third-party adapter registry kill-switch + signing override + local-path opt-in.
804
+ adapters: AdaptersConfigSchema,
805
+ // Plan 3c: anonymous adapter-discovery telemetry opt-in (default off).
806
+ telemetry: TelemetryConfigSchema,
807
+ // Plan 3b Phase 4: optional LSP enrichment of AST adapter results.
808
+ lsp: LSPConfigSchema.optional()
809
+ }).passthrough();
810
+ _config = null;
811
+ _resolvedApiKeySource = "none";
812
+ _projectRoot = null;
813
+ }
814
+ });
815
+
816
+ // src/memory-vector.ts
817
+ function float32ToBlob(vec) {
818
+ return Buffer.from(new Uint8Array(vec.buffer, vec.byteOffset, vec.byteLength));
819
+ }
820
+ function l2normalize(vec) {
821
+ let sumSq = 0;
822
+ for (let i = 0; i < vec.length; i++) sumSq += vec[i] * vec[i];
823
+ const norm = Math.sqrt(sumSq);
824
+ if (norm === 0 || !Number.isFinite(norm)) return vec;
825
+ const out = new Float32Array(vec.length);
826
+ for (let i = 0; i < vec.length; i++) out[i] = vec[i] / norm;
827
+ return out;
828
+ }
829
+ var init_memory_vector = __esm({
830
+ "src/memory-vector.ts"() {
831
+ "use strict";
832
+ }
833
+ });
834
+
835
+ // src/db-backup.ts
836
+ import Database from "better-sqlite3";
837
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync2 } from "fs";
838
+ import { resolve as resolve3, join, basename, dirname as dirname2 } from "path";
839
+ import { homedir as homedir3 } from "os";
840
+ function dbBackupsRoot(home = homedir3()) {
841
+ return resolve3(home, ".massu", "db-backups");
842
+ }
843
+ function projectBackupDir(projectRoot, home = homedir3()) {
844
+ const slug = basename(projectRoot).replace(/[^A-Za-z0-9._-]/g, "_") || "project";
845
+ return join(dbBackupsRoot(home), slug);
846
+ }
847
+ function backupStamp(nowMs) {
848
+ return new Date(nowMs).toISOString().replace(/[:.]/g, "-");
849
+ }
850
+ function listDbBackups(projectRoot, home = homedir3()) {
851
+ const dir = projectBackupDir(projectRoot, home);
852
+ if (!existsSync3(dir)) return [];
853
+ const out = [];
854
+ for (const f of readdirSync(dir)) {
855
+ if (!f.endsWith(".db")) continue;
856
+ const p = join(dir, f);
857
+ const st = statSync(p);
858
+ const db = f.replace(/-\d{4}-\d{2}-\d{2}T.*$/, "");
859
+ out.push({ db, path: p, bytes: st.size, mtimeMs: st.mtimeMs });
860
+ }
861
+ return out.sort((a, b) => b.mtimeMs - a.mtimeMs);
862
+ }
863
+ function hasFreshDbBackup(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
864
+ const name = basename(dbPath, ".db");
865
+ const backups = listDbBackups(projectRoot, home).filter((b) => b.db === name);
866
+ if (backups.length === 0) return false;
867
+ const newest = backups[0];
868
+ if (nowMs - newest.mtimeMs > FRESH_WINDOW_MS) return false;
869
+ if (existsSync3(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
870
+ return true;
871
+ }
872
+ function integrityOk(dbPath) {
873
+ let db = null;
874
+ try {
875
+ db = new Database(dbPath, { readonly: true });
876
+ const rows = db.pragma("integrity_check");
877
+ return rows.length === 1 && rows[0].integrity_check === "ok";
878
+ } catch {
879
+ return false;
880
+ } finally {
881
+ try {
882
+ db?.close();
883
+ } catch {
884
+ }
885
+ }
886
+ }
887
+ function backupDb(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
888
+ if (!existsSync3(dbPath)) {
889
+ throw new DbBackupError(`cannot back up a database that does not exist: ${dbPath}`);
890
+ }
891
+ const dir = projectBackupDir(projectRoot, home);
892
+ mkdirSync2(dir, { recursive: true });
893
+ const name = basename(dbPath, ".db");
894
+ const dest = join(dir, `${name}-${backupStamp(nowMs)}.db`);
895
+ let src = null;
896
+ try {
897
+ src = new Database(dbPath, { readonly: true });
898
+ src.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
899
+ } catch (err) {
900
+ throw new DbBackupError(
901
+ `VACUUM INTO failed for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
902
+ );
903
+ } finally {
904
+ try {
905
+ src?.close();
906
+ } catch {
907
+ }
908
+ }
909
+ if (!integrityOk(dest)) {
910
+ try {
911
+ unlinkSync(dest);
912
+ } catch {
913
+ }
914
+ throw new DbBackupError(
915
+ `the backup of ${dbPath} FAILED its own integrity check and was deleted. A corrupt backup is worse than none \u2014 you only discover it when you need it.`
916
+ );
917
+ }
918
+ pruneDbBackups(projectRoot, name, DEFAULT_RETENTION, home);
919
+ const st = statSync(dest);
920
+ return { db: name, path: dest, bytes: st.size, mtimeMs: st.mtimeMs };
921
+ }
922
+ function pruneDbBackups(projectRoot, dbName, keep = DEFAULT_RETENTION, home = homedir3()) {
923
+ const mine = listDbBackups(projectRoot, home).filter((b) => b.db === dbName);
924
+ let removed = 0;
925
+ for (const old of mine.slice(keep)) {
926
+ try {
927
+ unlinkSync(old.path);
928
+ removed++;
929
+ } catch {
930
+ }
931
+ }
932
+ return removed;
933
+ }
934
+ function backupBeforeSchemaChange(projectRoot, dbPath, onError, nowMs = Date.now(), home = homedir3()) {
935
+ try {
936
+ if (!existsSync3(dbPath)) return null;
937
+ if (hasFreshDbBackup(projectRoot, dbPath, nowMs, home)) return null;
938
+ return backupDb(projectRoot, dbPath, nowMs, home);
939
+ } catch (err) {
940
+ onError(err);
941
+ return null;
942
+ }
943
+ }
944
+ var DEFAULT_RETENTION, FRESH_WINDOW_MS, DbBackupError;
945
+ var init_db_backup = __esm({
946
+ "src/db-backup.ts"() {
947
+ "use strict";
948
+ DEFAULT_RETENTION = 5;
949
+ FRESH_WINDOW_MS = 24 * 60 * 60 * 1e3;
950
+ DbBackupError = class extends Error {
951
+ constructor(message) {
952
+ super(message);
953
+ this.name = "DbBackupError";
954
+ }
955
+ };
956
+ }
957
+ });
958
+
959
+ // src/hooks/lib/hook-failure-signal.ts
960
+ import { appendFileSync, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
961
+ import { join as join2, dirname as dirname3 } from "path";
962
+ function resolveFailureLogPath() {
963
+ const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
964
+ if (explicit) return explicit;
965
+ let dir = process.cwd();
966
+ for (let i = 0; i < 12; i++) {
967
+ if (existsSync4(join2(dir, ".massu")) || existsSync4(join2(dir, "massu.config.yaml"))) {
968
+ return join2(dir, ".massu", "hook-failures.jsonl");
969
+ }
970
+ const parent = dirname3(dir);
971
+ if (parent === dir) break;
972
+ dir = parent;
973
+ }
974
+ return join2(process.cwd(), ".massu", "hook-failures.jsonl");
975
+ }
976
+ function recordHookFailure(hook, error, context) {
977
+ const record = {
978
+ hook,
979
+ error: error instanceof Error ? error.message : String(error),
980
+ stack: error instanceof Error ? error.stack?.split("\n").slice(0, 6).join("\n") : void 0,
981
+ context,
982
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
983
+ };
984
+ let wroteSomething = false;
985
+ try {
986
+ const path = resolveFailureLogPath();
987
+ mkdirSync3(dirname3(path), { recursive: true });
988
+ appendFileSync(path, JSON.stringify(record) + "\n", "utf-8");
989
+ wroteSomething = true;
990
+ } catch {
991
+ }
992
+ try {
993
+ process.stderr.write(
994
+ `[massu] HOOK FAILURE in ${hook}: ${record.error}
995
+ [massu] This is a bug in Massu, not in your code. See .massu/hook-failures.jsonl
996
+ `
997
+ );
998
+ wroteSomething = true;
999
+ } catch {
1000
+ }
1001
+ try {
1002
+ recordHookHealthRow(record);
1003
+ } catch {
1004
+ }
1005
+ return wroteSomething;
1006
+ }
1007
+ function recordHookHealthRow(record) {
1008
+ const { getMemoryDb: getMemoryDb2 } = (init_memory_db(), __toCommonJS(memory_db_exports));
1009
+ const db = getMemoryDb2();
1010
+ try {
1011
+ db.prepare(
1012
+ `INSERT INTO hook_health (hook, error, context_json, occurred_at)
1013
+ VALUES (?, ?, ?, ?)`
1014
+ ).run(
1015
+ record.hook,
1016
+ record.error,
1017
+ record.context ? JSON.stringify(record.context) : null,
1018
+ record.timestamp
1019
+ );
1020
+ } finally {
1021
+ db.close();
1022
+ }
1023
+ }
1024
+ var init_hook_failure_signal = __esm({
1025
+ "src/hooks/lib/hook-failure-signal.ts"() {
1026
+ "use strict";
1027
+ }
1028
+ });
1029
+
1030
+ // src/memory-embedder-tokenizer.ts
1031
+ import { readFileSync as readFileSync3 } from "fs";
1032
+ function loadVocab(vocabPath) {
1033
+ const lines = readFileSync3(vocabPath, "utf-8").split("\n");
1034
+ const vocab = /* @__PURE__ */ new Map();
1035
+ for (let i = 0; i < lines.length; i++) {
1036
+ const tok = lines[i].replace(/\r$/, "");
1037
+ if (tok.length === 0 && i === lines.length - 1) continue;
1038
+ vocab.set(tok, i);
1039
+ }
1040
+ return vocab;
1041
+ }
1042
+ function isWhitespace(ch) {
1043
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") return true;
1044
+ const cp = ch.codePointAt(0);
1045
+ return cp === 160 || cp >= 8192 && cp <= 8202 || cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287 || cp === 12288 || cp === 65279;
1046
+ }
1047
+ function isControl(ch) {
1048
+ if (ch === " " || ch === "\n" || ch === "\r") return false;
1049
+ const cp = ch.codePointAt(0);
1050
+ return cp <= 31 || cp >= 127 && cp <= 159;
1051
+ }
1052
+ function isPunctuation(ch) {
1053
+ const cp = ch.codePointAt(0);
1054
+ if (cp >= 33 && cp <= 47 || cp >= 58 && cp <= 64 || cp >= 91 && cp <= 96 || cp >= 123 && cp <= 126)
1055
+ return true;
1056
+ return /\p{P}|\p{S}/u.test(ch);
1057
+ }
1058
+ function isCJK(cp) {
1059
+ return cp >= 19968 && cp <= 40959 || cp >= 13312 && cp <= 19903 || cp >= 131072 && cp <= 173791 || cp >= 173824 && cp <= 177983 || cp >= 177984 && cp <= 178207 || cp >= 178208 && cp <= 183983 || cp >= 63744 && cp <= 64255 || cp >= 194560 && cp <= 195103;
1060
+ }
1061
+ function stripAccents(text) {
1062
+ return text.normalize("NFD").replace(/\p{Mn}/gu, "");
1063
+ }
1064
+ function basicTokenize(text) {
1065
+ let cleaned = "";
1066
+ for (const ch of text) {
1067
+ const cp = ch.codePointAt(0);
1068
+ if (cp === 0 || cp === 65533 || isControl(ch)) continue;
1069
+ if (isWhitespace(ch)) {
1070
+ cleaned += " ";
1071
+ continue;
1072
+ }
1073
+ if (isCJK(cp)) {
1074
+ cleaned += " " + ch + " ";
1075
+ continue;
1076
+ }
1077
+ cleaned += ch;
1078
+ }
1079
+ const rawTokens = cleaned.split(/\s+/).filter(Boolean);
1080
+ const out = [];
1081
+ for (let tok of rawTokens) {
1082
+ tok = stripAccents(tok.toLowerCase());
1083
+ let cur = "";
1084
+ for (const ch of tok) {
1085
+ if (isPunctuation(ch)) {
1086
+ if (cur) {
1087
+ out.push(cur);
1088
+ cur = "";
1089
+ }
1090
+ out.push(ch);
1091
+ } else {
1092
+ cur += ch;
1093
+ }
1094
+ }
1095
+ if (cur) out.push(cur);
1096
+ }
1097
+ return out;
1098
+ }
1099
+ function wordpieceTokenize(token, vocab, maxChars = 100) {
1100
+ if (token.length > maxChars) return [UNK];
1101
+ const chars = Array.from(token);
1102
+ const subTokens = [];
1103
+ let start = 0;
1104
+ let bad = false;
1105
+ while (start < chars.length) {
1106
+ let end = chars.length;
1107
+ let curSub = null;
1108
+ while (start < end) {
1109
+ let substr = chars.slice(start, end).join("");
1110
+ if (start > 0) substr = "##" + substr;
1111
+ if (vocab.has(substr)) {
1112
+ curSub = substr;
1113
+ break;
1114
+ }
1115
+ end -= 1;
1116
+ }
1117
+ if (curSub === null) {
1118
+ bad = true;
1119
+ break;
1120
+ }
1121
+ subTokens.push(curSub);
1122
+ start = end;
1123
+ }
1124
+ return bad ? [UNK] : subTokens;
1125
+ }
1126
+ function encode(text, vocab, opts = {}) {
1127
+ const maxLen = opts.maxLen ?? 256;
1128
+ const basic = basicTokenize(text);
1129
+ const wpTokens = [];
1130
+ for (const t2 of basic) {
1131
+ for (const sub of wordpieceTokenize(t2, vocab)) wpTokens.push(sub);
1132
+ }
1133
+ const truncated = wpTokens.slice(0, maxLen - 2);
1134
+ const tokens = [CLS, ...truncated, SEP];
1135
+ const unkId = vocab.get(UNK) ?? 100;
1136
+ const input_ids = tokens.map((t2) => vocab.get(t2) ?? unkId);
1137
+ const attention_mask = tokens.map(() => 1);
1138
+ const token_type_ids = tokens.map(() => 0);
1139
+ return { tokens, input_ids, attention_mask, token_type_ids };
1140
+ }
1141
+ var UNK, CLS, SEP;
1142
+ var init_memory_embedder_tokenizer = __esm({
1143
+ "src/memory-embedder-tokenizer.ts"() {
1144
+ "use strict";
1145
+ UNK = "[UNK]";
1146
+ CLS = "[CLS]";
1147
+ SEP = "[SEP]";
1148
+ }
1149
+ });
1150
+
1151
+ // src/memory-embedder.ts
1152
+ import { fileURLToPath } from "url";
1153
+ import { dirname as dirname4, join as join3 } from "path";
1154
+ import { existsSync as existsSync5 } from "fs";
1155
+ import { createRequire } from "module";
1156
+ function getActiveEmbedModel() {
1157
+ return _activeModel;
1158
+ }
1159
+ function loadEmbedSettings() {
1160
+ try {
1161
+ const r = getConfig().memory?.recall;
1162
+ return {
1163
+ enabled: r?.embedEnabled ?? true,
1164
+ endpoint: r?.embedEndpoint,
1165
+ model: r?.embedModel
1166
+ };
1167
+ } catch {
1168
+ return { enabled: true };
1169
+ }
1170
+ }
1171
+ function embeddingsDisabled() {
1172
+ return process.env.MASSU_DISABLE_EMBEDDINGS === "1";
1173
+ }
1174
+ function resolveModelDir() {
1175
+ try {
1176
+ let dir = dirname4(fileURLToPath(import.meta.url));
1177
+ let root = null;
1178
+ for (let i = 0; i < 8; i++) {
1179
+ if (existsSync5(join3(dir, "package.json"))) {
1180
+ root = dir;
1181
+ break;
1182
+ }
1183
+ const parent = dirname4(dir);
1184
+ if (parent === dir) break;
1185
+ dir = parent;
1186
+ }
1187
+ if (!root) return null;
1188
+ for (const rel of ["dist/embedder", "assets/embedder"]) {
1189
+ const candidate = join3(root, rel);
1190
+ if (existsSync5(join3(candidate, "model_quantized.onnx"))) return candidate;
1191
+ }
1192
+ return null;
1193
+ } catch {
1194
+ return null;
1195
+ }
1196
+ }
1197
+ function resolveWasmDir() {
1198
+ try {
1199
+ const req = createRequire(import.meta.url);
1200
+ const main2 = req.resolve(ORT_PKG);
1201
+ return dirname4(main2);
1202
+ } catch {
1203
+ return null;
1204
+ }
1205
+ }
1206
+ async function getSession() {
1207
+ if (_sessionPromise) return _sessionPromise;
1208
+ _sessionPromise = (async () => {
1209
+ try {
1210
+ const modelDir = resolveModelDir();
1211
+ const wasmDir = resolveWasmDir();
1212
+ if (!modelDir || !wasmDir) return null;
1213
+ const ort = await import(
1214
+ /* @vite-ignore */
1215
+ ORT_PKG
1216
+ );
1217
+ ort.env.wasm.numThreads = 1;
1218
+ ort.env.wasm.proxy = false;
1219
+ ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
1220
+ _ortTensor = ort.Tensor;
1221
+ const modelPath = join3(modelDir, "model_quantized.onnx");
1222
+ const vocabPath = join3(modelDir, "vocab.txt");
1223
+ _vocab = loadVocab(vocabPath);
1224
+ const session = await ort.InferenceSession.create(modelPath, {
1225
+ executionProviders: ["wasm"],
1226
+ graphOptimizationLevel: "all"
1227
+ });
1228
+ return session;
1229
+ } catch {
1230
+ return null;
1231
+ }
1232
+ })();
1233
+ return _sessionPromise;
1234
+ }
1235
+ function meanPool(hidden, mask, seqLen, hiddenSize) {
1236
+ const out = new Float32Array(hiddenSize);
1237
+ let maskSum = 0;
1238
+ for (let t2 = 0; t2 < seqLen; t2++) {
1239
+ const m = mask[t2];
1240
+ if (m === 0) continue;
1241
+ maskSum += m;
1242
+ const base = t2 * hiddenSize;
1243
+ for (let h = 0; h < hiddenSize; h++) out[h] += hidden[base + h] * m;
1244
+ }
1245
+ const denom = Math.max(maskSum, 1e-9);
1246
+ for (let h = 0; h < hiddenSize; h++) out[h] /= denom;
1247
+ return out;
1248
+ }
1249
+ function chunkForEmbedding(text) {
1250
+ const t2 = text.trim();
1251
+ if (!t2) return [];
1252
+ if (t2.length <= CHUNK_TARGET_CHARS) return [t2];
1253
+ const chunks = [];
1254
+ let start = 0;
1255
+ while (start < t2.length && chunks.length < MAX_CHUNKS_PER_RECORD) {
1256
+ let end = Math.min(start + CHUNK_TARGET_CHARS, t2.length);
1257
+ if (end < t2.length) {
1258
+ const floor = start + Math.floor(CHUNK_TARGET_CHARS / 2);
1259
+ const slice = t2.slice(start, end);
1260
+ const para = slice.lastIndexOf("\n\n");
1261
+ const sent = Math.max(slice.lastIndexOf(". "), slice.lastIndexOf(".\n"));
1262
+ const space = slice.lastIndexOf(" ");
1263
+ for (const rel of [para, sent, space]) {
1264
+ const abs = start + rel;
1265
+ if (rel > 0 && abs > floor) {
1266
+ end = abs + 1;
1267
+ break;
1268
+ }
1269
+ }
1270
+ }
1271
+ const chunk = t2.slice(start, end).trim();
1272
+ if (chunk) chunks.push(chunk);
1273
+ if (end >= t2.length) break;
1274
+ start = Math.max(end - CHUNK_OVERLAP_CHARS, start + 1);
1275
+ }
1276
+ return chunks;
1277
+ }
1278
+ async function embedTier1(text) {
1279
+ try {
1280
+ const session = await getSession();
1281
+ if (!session || !_vocab || !_ortTensor) return null;
1282
+ const { input_ids, attention_mask, token_type_ids } = encode(text, _vocab);
1283
+ const seqLen = input_ids.length;
1284
+ const ids = BigInt64Array.from(input_ids.map((x) => BigInt(x)));
1285
+ const mask = BigInt64Array.from(attention_mask.map((x) => BigInt(x)));
1286
+ const types = BigInt64Array.from(token_type_ids.map((x) => BigInt(x)));
1287
+ const dims = [1, seqLen];
1288
+ const Tensor = _ortTensor;
1289
+ const feeds = {
1290
+ input_ids: new Tensor("int64", ids, dims),
1291
+ attention_mask: new Tensor("int64", mask, dims)
1292
+ };
1293
+ if (session.inputNames.includes("token_type_ids")) {
1294
+ feeds.token_type_ids = new Tensor("int64", types, dims);
1295
+ }
1296
+ const results = await session.run(feeds);
1297
+ const outName = session.outputNames.find((n) => results[n].dims.length === 3) ?? session.outputNames[0];
1298
+ const outTensor = results[outName];
1299
+ const hiddenSize = outTensor.dims[2];
1300
+ const pooled = meanPool(outTensor.data, attention_mask, seqLen, hiddenSize);
1301
+ const vec = l2normalize(pooled);
1302
+ if (vec.length !== EMBED_DIM) return null;
1303
+ _activeModel = { modelId: EMBED_MODEL_ID, dim: EMBED_DIM };
1304
+ return vec;
1305
+ } catch {
1306
+ return null;
1307
+ }
1308
+ }
1309
+ async function embedTier0(text, settings) {
1310
+ const endpoint = settings.endpoint;
1311
+ if (!endpoint) return null;
1312
+ const requestModel = settings.model || EMBED_MODEL_ID;
1313
+ const base = endpoint.replace(/\/+$/, "");
1314
+ const url = `${base}/v1/embeddings`;
1315
+ const controller = new AbortController();
1316
+ const timer = setTimeout(() => controller.abort(), TIER0_TIMEOUT_MS);
1317
+ try {
1318
+ const res = await fetch(url, {
1319
+ method: "POST",
1320
+ headers: { "content-type": "application/json" },
1321
+ body: JSON.stringify({ model: requestModel, input: text }),
1322
+ signal: controller.signal
1323
+ });
1324
+ if (!res.ok) return null;
1325
+ const body = await res.json();
1326
+ const raw = body?.data?.[0]?.embedding;
1327
+ if (!Array.isArray(raw) || raw.length === 0) return null;
1328
+ const vec = l2normalize(Float32Array.from(raw));
1329
+ _activeModel = { modelId: requestModel, dim: vec.length };
1330
+ return vec;
1331
+ } catch {
1332
+ return null;
1333
+ } finally {
1334
+ clearTimeout(timer);
1335
+ }
1336
+ }
1337
+ async function embedBatch(texts) {
1338
+ if (!texts || texts.length === 0) return [];
1339
+ if (embeddingsDisabled()) return texts.map(() => null);
1340
+ const settings = loadEmbedSettings();
1341
+ if (!settings.enabled) return texts.map(() => null);
1342
+ const results = [];
1343
+ for (const t2 of texts) {
1344
+ if (!t2 || !t2.trim()) {
1345
+ results.push(null);
1346
+ continue;
1347
+ }
1348
+ try {
1349
+ let vec = null;
1350
+ if (settings.endpoint) {
1351
+ vec = await embedTier0(t2, settings);
1352
+ }
1353
+ if (!vec) vec = await embedTier1(t2);
1354
+ results.push(vec);
1355
+ } catch {
1356
+ results.push(null);
1357
+ }
1358
+ }
1359
+ return results;
1360
+ }
1361
+ var EMBED_MODEL_ID, EMBED_DIM, ORT_PKG, _activeModel, _sessionPromise, _vocab, _ortTensor, CHUNK_TARGET_CHARS, CHUNK_OVERLAP_CHARS, MAX_CHUNKS_PER_RECORD, TIER0_TIMEOUT_MS;
1362
+ var init_memory_embedder = __esm({
1363
+ "src/memory-embedder.ts"() {
1364
+ "use strict";
1365
+ init_memory_vector();
1366
+ init_memory_embedder_tokenizer();
1367
+ init_config();
1368
+ EMBED_MODEL_ID = "all-MiniLM-L6-v2";
1369
+ EMBED_DIM = 384;
1370
+ ORT_PKG = "onnxruntime-web";
1371
+ _activeModel = null;
1372
+ _sessionPromise = null;
1373
+ _vocab = null;
1374
+ _ortTensor = null;
1375
+ CHUNK_TARGET_CHARS = 900;
1376
+ CHUNK_OVERLAP_CHARS = 120;
1377
+ MAX_CHUNKS_PER_RECORD = 24;
1378
+ TIER0_TIMEOUT_MS = 2e3;
1379
+ }
1380
+ });
1381
+
1382
+ // src/memory-embed-sweep.ts
1383
+ function readCursor(db, metaTable, key) {
1384
+ if (!KNOWN_META_TABLES.has(metaTable)) return 0;
1385
+ try {
1386
+ const row = db.prepare(`SELECT value FROM ${metaTable} WHERE key = ?`).get(key);
1387
+ const n = row ? Number(row.value) : 0;
1388
+ return Number.isFinite(n) && n >= 0 ? n : 0;
1389
+ } catch {
1390
+ return 0;
1391
+ }
1392
+ }
1393
+ function writeCursor(db, metaTable, key, value) {
1394
+ if (!KNOWN_META_TABLES.has(metaTable)) return;
1395
+ try {
1396
+ db.prepare(`INSERT OR REPLACE INTO ${metaTable} (key, value) VALUES (?, ?)`).run(
1397
+ key,
1398
+ String(value)
1399
+ );
1400
+ } catch {
1401
+ }
1402
+ }
1403
+ async function runEmbedSweep(db, cfg, opts = {}) {
1404
+ const batchSize = Math.max(1, opts.batchSize ?? 16);
1405
+ const limit = opts.limit ?? Infinity;
1406
+ const budgetMs = opts.budgetMs;
1407
+ const start = Date.now();
1408
+ const cursorKey = `embed_sweep_cursor_${cfg.sourceLabel}`;
1409
+ let embedded = 0;
1410
+ let scanned = 0;
1411
+ let active = getActiveEmbedModel();
1412
+ let cursor = readCursor(db, cfg.metaTable, cursorKey);
1413
+ try {
1414
+ while (true) {
1415
+ if (embedded >= limit) break;
1416
+ if (budgetMs !== void 0 && Date.now() - start >= budgetMs) break;
1417
+ const remaining = limit === Infinity ? batchSize : Math.min(batchSize, limit - embedded);
1418
+ const rows = cfg.selectMissing(db, cursor, active, Math.max(1, remaining));
1419
+ if (rows.length === 0) {
1420
+ writeCursor(db, cfg.metaTable, cursorKey, 0);
1421
+ break;
1422
+ }
1423
+ const units = [];
1424
+ for (const r of rows) {
1425
+ if (cfg.chunked) {
1426
+ const parts = chunkForEmbedding(r.text);
1427
+ if (parts.length === 0) continue;
1428
+ parts.forEach((text, chunkIx) => units.push({ id: r.id, chunkIx, text }));
1429
+ } else {
1430
+ units.push({ id: r.id, chunkIx: 0, text: r.text });
1431
+ }
1432
+ }
1433
+ const vecs = await embedBatch(units.map((u) => u.text));
1434
+ if (!active) active = getActiveEmbedModel();
1435
+ if (!active) {
1436
+ break;
1437
+ }
1438
+ const tx = db.transaction(() => {
1439
+ const del = cfg.chunked ? db.prepare(
1440
+ `DELETE FROM ${cfg.embeddingTable}
1441
+ WHERE ${cfg.idCol} = ? AND model_id = ? AND dim = ?`
1442
+ ) : null;
1443
+ const stmt = cfg.chunked ? db.prepare(
1444
+ `INSERT OR REPLACE INTO ${cfg.embeddingTable}
1445
+ (${cfg.idCol}, chunk_ix, model_id, dim, vec, created_at)
1446
+ VALUES (?, ?, ?, ?, ?, datetime('now'))`
1447
+ ) : db.prepare(
1448
+ `INSERT OR REPLACE INTO ${cfg.embeddingTable}
1449
+ (${cfg.idCol}, model_id, dim, vec, created_at)
1450
+ VALUES (?, ?, ?, ?, datetime('now'))`
1451
+ );
1452
+ const cleared = /* @__PURE__ */ new Set();
1453
+ const embeddedIds = /* @__PURE__ */ new Set();
1454
+ for (let i = 0; i < units.length; i++) {
1455
+ const v = vecs[i];
1456
+ if (!v) continue;
1457
+ const u = units[i];
1458
+ if (del && !cleared.has(u.id)) {
1459
+ del.run(u.id, active.modelId, active.dim);
1460
+ cleared.add(u.id);
1461
+ }
1462
+ if (cfg.chunked) {
1463
+ stmt.run(u.id, u.chunkIx, active.modelId, active.dim, float32ToBlob(v));
1464
+ } else {
1465
+ stmt.run(u.id, active.modelId, active.dim, float32ToBlob(v));
1466
+ }
1467
+ embeddedIds.add(u.id);
1468
+ }
1469
+ embedded += embeddedIds.size;
1470
+ });
1471
+ tx();
1472
+ scanned += rows.length;
1473
+ cursor = rows[rows.length - 1].id;
1474
+ writeCursor(db, cfg.metaTable, cursorKey, cursor);
1475
+ }
1476
+ } catch {
1477
+ }
1478
+ return { embedded, scanned };
1479
+ }
1480
+ var KNOWN_META_TABLES;
1481
+ var init_memory_embed_sweep = __esm({
1482
+ "src/memory-embed-sweep.ts"() {
1483
+ "use strict";
1484
+ init_memory_embedder();
1485
+ init_memory_vector();
1486
+ KNOWN_META_TABLES = /* @__PURE__ */ new Set(["memory_meta", "knowledge_meta"]);
1487
+ }
1488
+ });
636
1489
 
637
1490
  // src/memory-db.ts
1491
+ var memory_db_exports = {};
1492
+ __export(memory_db_exports, {
1493
+ CONSOLIDATION_LESSON_EVIDENCE: () => CONSOLIDATION_LESSON_EVIDENCE,
1494
+ MEMORY_FILE_TITLE_LIKE: () => MEMORY_FILE_TITLE_LIKE,
1495
+ MEMORY_FILE_TITLE_PREFIX: () => MEMORY_FILE_TITLE_PREFIX,
1496
+ MEMORY_SCHEMA_VERSION: () => MEMORY_SCHEMA_VERSION,
1497
+ TOOL_COST_EVENTS_RETENTION_DAYS: () => TOOL_COST_EVENTS_RETENTION_DAYS,
1498
+ USAGE_COUNTER_ARMED_KEY: () => USAGE_COUNTER_ARMED_KEY,
1499
+ addConversationTurn: () => addConversationTurn,
1500
+ addFailureClass: () => addFailureClass,
1501
+ addObservation: () => addObservation,
1502
+ addSummary: () => addSummary,
1503
+ addToolCallDetail: () => addToolCallDetail,
1504
+ addUserPrompt: () => addUserPrompt,
1505
+ appendIncidentToFailureClass: () => appendIncidentToFailureClass,
1506
+ armUsageCounter: () => armUsageCounter,
1507
+ assignImportance: () => assignImportance,
1508
+ autoDetectTaskId: () => autoDetectTaskId,
1509
+ createSession: () => createSession,
1510
+ deduplicateFailedAttempt: () => deduplicateFailedAttempt,
1511
+ dequeuePendingSync: () => dequeuePendingSync,
1512
+ drainRulePromotionEvents: () => drainRulePromotionEvents,
1513
+ drainTeamPromotions: () => drainTeamPromotions,
1514
+ drainTeamRevocations: () => drainTeamRevocations,
1515
+ embedMissingObservations: () => embedMissingObservations,
1516
+ endSession: () => endSession,
1517
+ enqueueRulePromotionEvent: () => enqueueRulePromotionEvent,
1518
+ enqueueSyncPayload: () => enqueueSyncPayload,
1519
+ enqueueTeamPromotion: () => enqueueTeamPromotion,
1520
+ enqueueTeamRevocation: () => enqueueTeamRevocation,
1521
+ expireOldLowValueObservations: () => expireOldLowValueObservations,
1522
+ getConversationTurns: () => getConversationTurns,
1523
+ getCrossTaskProgress: () => getCrossTaskProgress,
1524
+ getDecisionsAbout: () => getDecisionsAbout,
1525
+ getFailedAttempts: () => getFailedAttempts,
1526
+ getFailureClasses: () => getFailureClasses,
1527
+ getLastProcessedLine: () => getLastProcessedLine,
1528
+ getMemoryDb: () => getMemoryDb,
1529
+ getMemoryMeta: () => getMemoryMeta,
1530
+ getObservabilityDbSize: () => getObservabilityDbSize,
1531
+ getRecentObservations: () => getRecentObservations,
1532
+ getRecurrenceCountForPromptHash: () => getRecurrenceCountForPromptHash,
1533
+ getSessionStats: () => getSessionStats,
1534
+ getSessionSummaries: () => getSessionSummaries,
1535
+ getSessionTimeline: () => getSessionTimeline,
1536
+ getSessionsByTask: () => getSessionsByTask,
1537
+ getToolPatterns: () => getToolPatterns,
1538
+ incrementRetryCount: () => incrementRetryCount,
1539
+ initMemorySchema: () => initMemorySchema,
1540
+ linkSessionToTask: () => linkSessionToTask,
1541
+ markRecordSuperseded: () => markRecordSuperseded,
1542
+ memoryTableHasTemporal: () => memoryTableHasTemporal,
1543
+ migrateAuditLogCheckExtension: () => migrateAuditLogCheckExtension,
1544
+ migrateMemoryFilesFor4B: () => migrateMemoryFilesFor4B,
1545
+ migrateObservationEmbeddingChunks: () => migrateObservationEmbeddingChunks,
1546
+ pruneOldConversationTurns: () => pruneOldConversationTurns,
1547
+ pruneOldObservations: () => pruneOldObservations,
1548
+ pruneToolCostEvents: () => pruneToolCostEvents,
1549
+ recordRecallHits: () => recordRecallHits,
1550
+ recordTelemetry: () => recordTelemetry,
1551
+ removePendingSync: () => removePendingSync,
1552
+ sanitizeFts5Query: () => sanitizeFts5Query,
1553
+ sanitizeFts5QueryOr: () => sanitizeFts5QueryOr,
1554
+ scoreFailureClasses: () => scoreFailureClasses,
1555
+ searchConversationTurns: () => searchConversationTurns,
1556
+ searchObservations: () => searchObservations,
1557
+ setLastProcessedLine: () => setLastProcessedLine,
1558
+ setMemoryMeta: () => setMemoryMeta,
1559
+ upsertObservationEmbedding: () => upsertObservationEmbedding,
1560
+ usageWarmupElapsed: () => usageWarmupElapsed
1561
+ });
1562
+ import Database2 from "better-sqlite3";
1563
+ import { dirname as dirname5, basename as basename2 } from "path";
1564
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
1565
+ function sanitizeFts5Query(raw) {
1566
+ const trimmed = raw.trim();
1567
+ if (!trimmed) return '""';
1568
+ const tokens = trimmed.replace(/"/g, "").split(/\s+/).filter(Boolean);
1569
+ return tokens.map((t2) => `"${t2}"`).join(" ");
1570
+ }
1571
+ function sanitizeFts5QueryOr(raw) {
1572
+ const trimmed = raw.trim();
1573
+ if (!trimmed) return '""';
1574
+ const tokens = trimmed.replace(/"/g, "").split(/\s+/).filter((t2) => t2.replace(/[^a-zA-Z0-9]/g, "").length >= 3);
1575
+ if (tokens.length === 0) return '""';
1576
+ return tokens.map((t2) => `"${t2}"`).join(" OR ");
1577
+ }
638
1578
  function getMemoryDb() {
639
1579
  const dbPath = getResolvedPaths().memoryDbPath;
640
- const dir = dirname2(dbPath);
641
- if (!existsSync3(dir)) {
642
- mkdirSync2(dir, { recursive: true });
1580
+ const dir = dirname5(dbPath);
1581
+ if (!existsSync6(dir)) {
1582
+ mkdirSync4(dir, { recursive: true });
643
1583
  }
644
- const db = new Database(dbPath);
1584
+ const preExisting = existsSync6(dbPath);
1585
+ const db = new Database2(dbPath);
645
1586
  db.pragma("journal_mode = WAL");
646
1587
  db.pragma("foreign_keys = ON");
1588
+ const onDisk = db.pragma("user_version", { simple: true });
1589
+ if (preExisting && onDisk !== MEMORY_SCHEMA_VERSION) {
1590
+ backupBeforeSchemaChange(
1591
+ getProjectRoot(),
1592
+ dbPath,
1593
+ (err) => recordHookFailure("memory-db:pre-ddl-backup", err, { dbPath, onDisk })
1594
+ );
1595
+ }
647
1596
  initMemorySchema(db);
1597
+ if (onDisk !== MEMORY_SCHEMA_VERSION) {
1598
+ db.pragma(`user_version = ${MEMORY_SCHEMA_VERSION}`);
1599
+ }
648
1600
  return db;
649
1601
  }
1602
+ function pruneToolCostEvents(db) {
1603
+ const result = db.prepare(
1604
+ `DELETE FROM tool_cost_events WHERE created_at < datetime('now', '-' || ? || ' days')`
1605
+ ).run(TOOL_COST_EVENTS_RETENTION_DAYS);
1606
+ return result.changes;
1607
+ }
1608
+ function migrateObservationEmbeddingChunks(db) {
1609
+ const cols = db.pragma("table_info(observation_embeddings)");
1610
+ if (cols.length === 0) return;
1611
+ if (cols.some((c) => c.name === "chunk_ix")) return;
1612
+ db.pragma("foreign_keys = OFF");
1613
+ try {
1614
+ db.exec("BEGIN TRANSACTION");
1615
+ db.exec(`
1616
+ CREATE TABLE observation_embeddings_new (
1617
+ observation_id INTEGER NOT NULL REFERENCES observations(id) ON DELETE CASCADE,
1618
+ chunk_ix INTEGER NOT NULL DEFAULT 0,
1619
+ model_id TEXT NOT NULL,
1620
+ dim INTEGER NOT NULL,
1621
+ vec BLOB NOT NULL,
1622
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1623
+ PRIMARY KEY (observation_id, model_id, dim, chunk_ix)
1624
+ );
1625
+ INSERT INTO observation_embeddings_new
1626
+ (observation_id, chunk_ix, model_id, dim, vec, created_at)
1627
+ SELECT observation_id, 0, model_id, dim, vec, created_at FROM observation_embeddings;
1628
+ DROP TABLE observation_embeddings;
1629
+ ALTER TABLE observation_embeddings_new RENAME TO observation_embeddings;
1630
+ CREATE INDEX IF NOT EXISTS idx_obs_emb_model ON observation_embeddings(model_id);
1631
+ `);
1632
+ db.exec("COMMIT");
1633
+ } catch (err) {
1634
+ db.exec("ROLLBACK");
1635
+ throw err;
1636
+ } finally {
1637
+ db.pragma("foreign_keys = ON");
1638
+ }
1639
+ }
1640
+ function migrateMemoryFilesFor4B(db) {
1641
+ const ADDITIONS = [
1642
+ // The renderer's authorship credential (OD-1). NULL on every pre-existing row,
1643
+ // which is the safe direction: no MAC ⇒ unverifiable ⇒ the file is HUMAN.
1644
+ { table: "memory_files", name: "massu_render_mac", decl: "TEXT" },
1645
+ // F-15 stickiness: once a file is human, only `massu memory adopt` reverses it.
1646
+ { table: "memory_files", name: "adopted_human_at_epoch", decl: "INTEGER" },
1647
+ // OD-2: a CACHE of `.massu-tombstones.jsonl`. The ledger is the source of truth.
1648
+ { table: "memory_files", name: "tombstoned_at_epoch", decl: "INTEGER" },
1649
+ { table: "memory_files", name: "origin", decl: "TEXT NOT NULL DEFAULT 'local'" },
1650
+ { table: "memory_files", name: "render_suppressed", decl: "INTEGER NOT NULL DEFAULT 0" },
1651
+ // N-03 — the SOURCE row. This is the one F-08 actually requires.
1652
+ { table: "observations", name: "origin", decl: "TEXT NOT NULL DEFAULT 'local'" }
1653
+ ];
1654
+ for (const add of ADDITIONS) {
1655
+ const cols = db.prepare(`PRAGMA table_info(${add.table})`).all();
1656
+ if (cols.length === 0) continue;
1657
+ if (cols.some((c) => c.name === add.name)) continue;
1658
+ db.exec(`ALTER TABLE ${add.table} ADD COLUMN ${add.name} ${add.decl}`);
1659
+ }
1660
+ }
650
1661
  function migrateAuditLogCheckExtension(db) {
651
1662
  const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
652
1663
  if (!row) return;
@@ -659,7 +1670,17 @@ function migrateAuditLogCheckExtension(db) {
659
1670
  "compaction",
660
1671
  "rule_candidate_emitted",
661
1672
  "rule_promoted",
662
- "rule_dismissed"
1673
+ "rule_dismissed",
1674
+ // A-19/N-02 — without these the memory-file events throw a CHECK violation.
1675
+ // In the ingest path the throw is swallowed by a bare catch, so observability
1676
+ // silently produces NOTHING; in the renderer's transaction it ROLLS BACK a
1677
+ // legitimate render. This migration is what lets an EXISTING db accept them.
1678
+ "memory_file_ingested",
1679
+ "memory_file_expired",
1680
+ "memory_file_adopted_human",
1681
+ "memory_file_rendered",
1682
+ "memory_file_render_refused",
1683
+ "memory_file_tombstoned"
663
1684
  ];
664
1685
  const checkClauseMatch = row.sql.match(/event_type\s+TEXT\s+NOT\s+NULL\s+CHECK\s*\(\s*event_type\s+IN\s*\(([\s\S]*?)\)\s*\)/i);
665
1686
  if (checkClauseMatch) {
@@ -676,7 +1697,9 @@ function migrateAuditLogCheckExtension(db) {
676
1697
  timestamp TEXT DEFAULT (datetime('now')),
677
1698
  event_type TEXT NOT NULL CHECK(event_type IN (
678
1699
  'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
679
- 'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed'
1700
+ 'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
1701
+ 'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
1702
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
680
1703
  )),
681
1704
  actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
682
1705
  model_id TEXT,
@@ -748,6 +1771,11 @@ function initMemorySchema(db) {
748
1771
  importance INTEGER NOT NULL DEFAULT 3 CHECK(importance BETWEEN 1 AND 5),
749
1772
  recurrence_count INTEGER NOT NULL DEFAULT 1,
750
1773
  original_tokens INTEGER DEFAULT 0,
1774
+ -- B-10/F-08/N-03: the SOURCE-row provenance flag. The renderer refuses any
1775
+ -- row whose origin is not 'local' BEFORE it computes a path, mints a
1776
+ -- credential, or takes a snapshot. A Slice-5 synced memory arrives here with
1777
+ -- origin='team' and must never reach disk without CR-55's gates.
1778
+ origin TEXT NOT NULL DEFAULT 'local',
751
1779
  created_at TEXT NOT NULL,
752
1780
  created_at_epoch INTEGER NOT NULL,
753
1781
  FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
@@ -846,6 +1874,27 @@ function initMemorySchema(db) {
846
1874
  value TEXT NOT NULL
847
1875
  );
848
1876
  `);
1877
+ db.exec(`
1878
+ CREATE TABLE IF NOT EXISTS memory_usage (
1879
+ source TEXT NOT NULL,
1880
+ record_id INTEGER NOT NULL,
1881
+ hit_count INTEGER NOT NULL DEFAULT 0,
1882
+ hits_windowed REAL NOT NULL DEFAULT 0,
1883
+ last_hit_epoch INTEGER,
1884
+ last_reweight_epoch INTEGER,
1885
+ PRIMARY KEY (source, record_id)
1886
+ );
1887
+
1888
+ CREATE TABLE IF NOT EXISTS memory_usage_sessions (
1889
+ source TEXT NOT NULL,
1890
+ record_id INTEGER NOT NULL,
1891
+ session_id TEXT NOT NULL,
1892
+ PRIMARY KEY (source, record_id, session_id)
1893
+ );
1894
+
1895
+ CREATE INDEX IF NOT EXISTS idx_memory_usage_hits
1896
+ ON memory_usage(source, hits_windowed DESC);
1897
+ `);
849
1898
  db.exec(`
850
1899
  CREATE TABLE IF NOT EXISTS conversation_turns (
851
1900
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -998,7 +2047,9 @@ function initMemorySchema(db) {
998
2047
  timestamp TEXT DEFAULT (datetime('now')),
999
2048
  event_type TEXT NOT NULL CHECK(event_type IN (
1000
2049
  'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
1001
- 'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed'
2050
+ 'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
2051
+ 'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
2052
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
1002
2053
  )),
1003
2054
  actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
1004
2055
  model_id TEXT,
@@ -1018,6 +2069,17 @@ function initMemorySchema(db) {
1018
2069
  ON audit_log (event_type, json_extract(metadata, '$.prompt_hash'))
1019
2070
  WHERE event_type = 'rule_promoted';
1020
2071
  `);
2072
+ db.exec(`
2073
+ CREATE TABLE IF NOT EXISTS hook_health (
2074
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2075
+ hook TEXT NOT NULL,
2076
+ error TEXT NOT NULL,
2077
+ context_json TEXT,
2078
+ occurred_at TEXT NOT NULL DEFAULT (datetime('now'))
2079
+ );
2080
+ CREATE INDEX IF NOT EXISTS idx_hook_health_hook ON hook_health(hook);
2081
+ CREATE INDEX IF NOT EXISTS idx_hook_health_time ON hook_health(occurred_at DESC);
2082
+ `);
1021
2083
  db.exec(`
1022
2084
  CREATE TABLE IF NOT EXISTS prompt_outcomes_signal_blacklist (
1023
2085
  signal TEXT PRIMARY KEY,
@@ -1029,6 +2091,30 @@ function initMemorySchema(db) {
1029
2091
  ON prompt_outcomes_signal_blacklist(dismissal_count DESC);
1030
2092
  `);
1031
2093
  migrateAuditLogCheckExtension(db);
2094
+ db.exec(`
2095
+ CREATE TABLE IF NOT EXISTS memory_files (
2096
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2097
+ rel_path TEXT NOT NULL UNIQUE COLLATE NOCASE,
2098
+ name TEXT,
2099
+ raw TEXT NOT NULL,
2100
+ frontmatter_json TEXT,
2101
+ body TEXT NOT NULL,
2102
+ content_hash TEXT NOT NULL,
2103
+ ingest_schema_version INTEGER NOT NULL DEFAULT 1,
2104
+ massu_authored INTEGER NOT NULL DEFAULT 0,
2105
+ massu_render_mac TEXT,
2106
+ adopted_human_at_epoch INTEGER,
2107
+ tombstoned_at_epoch INTEGER,
2108
+ origin TEXT NOT NULL DEFAULT 'local',
2109
+ render_suppressed INTEGER NOT NULL DEFAULT 0,
2110
+ observation_id INTEGER,
2111
+ synced_at_epoch INTEGER,
2112
+ expired_at_epoch INTEGER
2113
+ );
2114
+ CREATE INDEX IF NOT EXISTS idx_mf_hash ON memory_files(content_hash);
2115
+ CREATE INDEX IF NOT EXISTS idx_mf_expired ON memory_files(expired_at_epoch);
2116
+ CREATE INDEX IF NOT EXISTS idx_mf_obs ON memory_files(observation_id);
2117
+ `);
1032
2118
  db.exec(`
1033
2119
  CREATE TABLE IF NOT EXISTS validation_results (
1034
2120
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1247,28 +2333,309 @@ function initMemorySchema(db) {
1247
2333
  if (!outboundCols.some((c) => c.name === "hardened")) {
1248
2334
  db.exec(`ALTER TABLE team_promotion_outbound ADD COLUMN hardened INTEGER NOT NULL DEFAULT 0`);
1249
2335
  }
1250
- if (!outboundCols.some((c) => c.name === "review_attestation_json")) {
1251
- db.exec(`ALTER TABLE team_promotion_outbound ADD COLUMN review_attestation_json TEXT`);
2336
+ if (!outboundCols.some((c) => c.name === "review_attestation_json")) {
2337
+ db.exec(`ALTER TABLE team_promotion_outbound ADD COLUMN review_attestation_json TEXT`);
2338
+ }
2339
+ const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
2340
+ if (!sessionCols.some((c) => c.name === "consolidated_at")) {
2341
+ db.exec(`ALTER TABLE sessions ADD COLUMN consolidated_at TEXT`);
2342
+ }
2343
+ if (!sessionCols.some((c) => c.name === "consolidated_at_epoch")) {
2344
+ db.exec(`ALTER TABLE sessions ADD COLUMN consolidated_at_epoch INTEGER`);
2345
+ }
2346
+ if (!sessionCols.some((c) => c.name === "consolidated_status")) {
2347
+ db.exec(`ALTER TABLE sessions ADD COLUMN consolidated_status TEXT`);
2348
+ }
2349
+ const BITEMPORAL_COLUMNS = [
2350
+ { name: "valid_from", type: "TEXT" },
2351
+ { name: "valid_to", type: "TEXT" },
2352
+ { name: "ingested_at", type: "TEXT" },
2353
+ { name: "expired_at", type: "TEXT" },
2354
+ { name: "valid_from_epoch", type: "INTEGER" },
2355
+ { name: "valid_to_epoch", type: "INTEGER" },
2356
+ { name: "ingested_at_epoch", type: "INTEGER" },
2357
+ { name: "expired_at_epoch", type: "INTEGER" },
2358
+ { name: "superseded_by", type: "INTEGER" }
2359
+ ];
2360
+ for (const table of ["observations", "architecture_decisions"]) {
2361
+ const cols = db.prepare(`PRAGMA table_info(${table})`).all();
2362
+ for (const col of BITEMPORAL_COLUMNS) {
2363
+ if (!cols.some((c) => c.name === col.name)) {
2364
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${col.name} ${col.type}`);
2365
+ }
2366
+ }
2367
+ }
2368
+ db.exec(`
2369
+ UPDATE observations
2370
+ SET valid_from = created_at,
2371
+ ingested_at = created_at,
2372
+ valid_from_epoch = created_at_epoch,
2373
+ ingested_at_epoch = created_at_epoch
2374
+ WHERE valid_from_epoch IS NULL;
2375
+ `);
2376
+ db.exec(`
2377
+ UPDATE architecture_decisions
2378
+ SET valid_from = created_at,
2379
+ ingested_at = created_at,
2380
+ valid_from_epoch = CAST(strftime('%s', created_at) AS INTEGER),
2381
+ ingested_at_epoch = CAST(strftime('%s', created_at) AS INTEGER)
2382
+ WHERE valid_from_epoch IS NULL AND created_at IS NOT NULL;
2383
+ `);
2384
+ db.exec(`
2385
+ CREATE INDEX IF NOT EXISTS idx_observations_expired ON observations(expired_at_epoch);
2386
+ CREATE INDEX IF NOT EXISTS idx_ad_expired ON architecture_decisions(expired_at_epoch);
2387
+ `);
2388
+ db.exec(`
2389
+ CREATE TABLE IF NOT EXISTS failure_classes (
2390
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2391
+ name TEXT NOT NULL UNIQUE,
2392
+ description TEXT NOT NULL,
2393
+ diff_patterns TEXT NOT NULL DEFAULT '[]',
2394
+ file_patterns TEXT NOT NULL DEFAULT '[]',
2395
+ prompt_keywords TEXT NOT NULL DEFAULT '[]',
2396
+ incidents TEXT NOT NULL DEFAULT '[]',
2397
+ rules TEXT NOT NULL DEFAULT '[]',
2398
+ scanner_checks TEXT NOT NULL DEFAULT '[]',
2399
+ known_message TEXT NOT NULL DEFAULT '',
2400
+ needs_review INTEGER NOT NULL DEFAULT 0,
2401
+ created_at TEXT DEFAULT (datetime('now')),
2402
+ updated_at TEXT DEFAULT (datetime('now'))
2403
+ );
2404
+ CREATE INDEX IF NOT EXISTS idx_fc_name ON failure_classes(name);
2405
+ CREATE INDEX IF NOT EXISTS idx_fc_needs_review ON failure_classes(needs_review);
2406
+ `);
2407
+ db.exec(`
2408
+ CREATE TABLE IF NOT EXISTS observation_embeddings (
2409
+ observation_id INTEGER NOT NULL REFERENCES observations(id) ON DELETE CASCADE,
2410
+ chunk_ix INTEGER NOT NULL DEFAULT 0,
2411
+ model_id TEXT NOT NULL,
2412
+ dim INTEGER NOT NULL,
2413
+ vec BLOB NOT NULL,
2414
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2415
+ PRIMARY KEY (observation_id, model_id, dim, chunk_ix)
2416
+ );
2417
+ CREATE INDEX IF NOT EXISTS idx_obs_emb_model ON observation_embeddings(model_id);
2418
+ `);
2419
+ migrateObservationEmbeddingChunks(db);
2420
+ migrateMemoryFilesFor4B(db);
2421
+ }
2422
+ function enqueueSyncPayload(db, payload) {
2423
+ db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
2424
+ }
2425
+ function getMemoryMeta(db, key) {
2426
+ const row = db.prepare("SELECT value FROM memory_meta WHERE key = ?").get(key);
2427
+ return row ? row.value : null;
2428
+ }
2429
+ function setMemoryMeta(db, key, value) {
2430
+ db.prepare("INSERT OR REPLACE INTO memory_meta (key, value) VALUES (?, ?)").run(key, value);
2431
+ }
2432
+ function upsertObservationEmbedding(db, observationId, vec, modelId, dim) {
2433
+ db.prepare(
2434
+ `INSERT OR REPLACE INTO observation_embeddings (observation_id, model_id, dim, vec, created_at)
2435
+ VALUES (?, ?, ?, ?, datetime('now'))`
2436
+ ).run(observationId, modelId, dim, float32ToBlob(vec));
2437
+ }
2438
+ function observationEmbedText(title, detail) {
2439
+ const t2 = (title ?? "").trim();
2440
+ const d = (detail ?? "").trim();
2441
+ return d ? `${t2}. ${d}` : t2;
2442
+ }
2443
+ async function embedMissingObservations(db, opts = {}) {
2444
+ return runEmbedSweep(
2445
+ db,
2446
+ {
2447
+ embeddingTable: "observation_embeddings",
2448
+ idCol: "observation_id",
2449
+ metaTable: "memory_meta",
2450
+ sourceLabel: "observation",
2451
+ // A-04: memory bodies are now stored WHOLE (up to ~14K chars). One vector per
2452
+ // memory would cover only its first ~1,000 chars.
2453
+ chunked: true,
2454
+ selectMissing: (d, cursor, model, batchSize) => {
2455
+ const rows = model ? d.prepare(
2456
+ `SELECT o.id AS id, o.title AS title, o.detail AS detail
2457
+ FROM observations o
2458
+ WHERE o.id > ?
2459
+ AND NOT EXISTS (
2460
+ SELECT 1 FROM observation_embeddings e
2461
+ WHERE e.observation_id = o.id AND e.model_id = ? AND e.dim = ?
2462
+ )
2463
+ ORDER BY o.id LIMIT ?`
2464
+ ).all(cursor, model.modelId, model.dim, batchSize) : d.prepare(
2465
+ `SELECT o.id AS id, o.title AS title, o.detail AS detail
2466
+ FROM observations o
2467
+ WHERE o.id > ?
2468
+ AND NOT EXISTS (
2469
+ SELECT 1 FROM observation_embeddings e WHERE e.observation_id = o.id
2470
+ )
2471
+ ORDER BY o.id LIMIT ?`
2472
+ ).all(cursor, batchSize);
2473
+ return rows.map((r) => ({ id: r.id, text: observationEmbedText(r.title, r.detail) }));
2474
+ }
2475
+ },
2476
+ opts
2477
+ );
2478
+ }
2479
+ function enqueueTeamPromotion(db, promo) {
2480
+ db.prepare(`
2481
+ INSERT OR REPLACE INTO team_promotion_outbound
2482
+ (prompt_hash, destination, draft_text, score, signals_json, content_hash, hardened, review_attestation_json, created_at)
2483
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
2484
+ `).run(
2485
+ promo.prompt_hash,
2486
+ promo.destination,
2487
+ promo.draft_text.slice(0, MAX_DRAFT_TEXT_LEN),
2488
+ promo.score ?? null,
2489
+ JSON.stringify(promo.signals ?? []),
2490
+ promo.content_hash,
2491
+ promo.hardened ? 1 : 0,
2492
+ promo.review_attestation !== void 0 ? JSON.stringify(promo.review_attestation) : null
2493
+ );
2494
+ }
2495
+ function drainTeamPromotions(db) {
2496
+ const rows = db.prepare(`
2497
+ SELECT prompt_hash, destination, draft_text, score, signals_json, content_hash, hardened, review_attestation_json
2498
+ FROM team_promotion_outbound ORDER BY created_at ASC LIMIT 1000
2499
+ `).all();
2500
+ if (rows.length === 0) return [];
2501
+ db.prepare("DELETE FROM team_promotion_outbound").run();
2502
+ return rows.map((r) => ({
2503
+ prompt_hash: r.prompt_hash,
2504
+ destination: r.destination,
2505
+ draft_text: r.draft_text,
2506
+ score: r.score ?? void 0,
2507
+ signals: safeJsonArray(r.signals_json),
2508
+ content_hash: r.content_hash,
2509
+ hardened: r.hardened === 1,
2510
+ review_attestation: r.review_attestation_json ? safeJsonParse(r.review_attestation_json) : void 0
2511
+ }));
2512
+ }
2513
+ function enqueueTeamRevocation(db, promptHash) {
2514
+ db.prepare(`
2515
+ INSERT OR REPLACE INTO team_revocation_outbound (prompt_hash, created_at)
2516
+ VALUES (?, datetime('now'))
2517
+ `).run(promptHash);
2518
+ }
2519
+ function drainTeamRevocations(db) {
2520
+ const rows = db.prepare(
2521
+ `SELECT prompt_hash FROM team_revocation_outbound ORDER BY created_at ASC LIMIT 1000`
2522
+ ).all();
2523
+ if (rows.length === 0) return [];
2524
+ db.prepare("DELETE FROM team_revocation_outbound").run();
2525
+ return rows.map((r) => r.prompt_hash);
2526
+ }
2527
+ function getRecurrenceCountForPromptHash(db, promptHash) {
2528
+ try {
2529
+ const row = db.prepare(`
2530
+ SELECT json_extract(metadata, '$.recurrence_count') AS rc
2531
+ FROM audit_log
2532
+ WHERE event_type = 'rule_promoted'
2533
+ AND json_extract(metadata, '$.prompt_hash') = ?
2534
+ LIMIT 1
2535
+ `).get(promptHash);
2536
+ if (!row || row.rc === null || row.rc === void 0) return null;
2537
+ return Number(row.rc);
2538
+ } catch {
2539
+ return null;
2540
+ }
2541
+ }
2542
+ function enqueueRulePromotionEvent(db, ev) {
2543
+ try {
2544
+ db.prepare(`
2545
+ INSERT INTO rule_promotion_events_outbound
2546
+ (prompt_hash, event_type, metadata_json, created_at)
2547
+ VALUES (?, ?, ?, ?)
2548
+ `).run(
2549
+ ev.prompt_hash,
2550
+ ev.event_type,
2551
+ JSON.stringify(ev.metadata ?? {}),
2552
+ ev.created_at
2553
+ );
2554
+ db.prepare(`
2555
+ DELETE FROM rule_promotion_events_outbound
2556
+ WHERE id NOT IN (
2557
+ SELECT id FROM rule_promotion_events_outbound ORDER BY id DESC LIMIT ?
2558
+ )
2559
+ `).run(FUNNEL_EVENT_OUTBOX_CAP);
2560
+ } catch {
2561
+ }
2562
+ }
2563
+ function drainRulePromotionEvents(db) {
2564
+ const rows = db.prepare(`
2565
+ SELECT id, prompt_hash, event_type, metadata_json, created_at
2566
+ FROM rule_promotion_events_outbound ORDER BY id ASC LIMIT ?
2567
+ `).all(FUNNEL_EVENT_DRAIN_LIMIT);
2568
+ if (rows.length === 0) return [];
2569
+ const maxId = rows[rows.length - 1].id;
2570
+ db.prepare("DELETE FROM rule_promotion_events_outbound WHERE id <= ?").run(maxId);
2571
+ return rows.map((r) => ({
2572
+ prompt_hash: r.prompt_hash,
2573
+ event_type: r.event_type,
2574
+ created_at: r.created_at,
2575
+ metadata: safeJsonParse(r.metadata_json) ?? {}
2576
+ }));
2577
+ }
2578
+ function recordTelemetry(db, eventType, data) {
2579
+ try {
2580
+ db.prepare(`
2581
+ INSERT INTO analytics_events (event_type, event_data, created_at)
2582
+ VALUES (?, ?, datetime('now'))
2583
+ `).run(eventType, JSON.stringify(data));
2584
+ } catch {
2585
+ }
2586
+ }
2587
+ function safeJsonArray(json) {
2588
+ try {
2589
+ const v = JSON.parse(json);
2590
+ return Array.isArray(v) ? v : [];
2591
+ } catch {
2592
+ return [];
2593
+ }
2594
+ }
2595
+ function safeJsonParse(json) {
2596
+ try {
2597
+ return JSON.parse(json);
2598
+ } catch {
2599
+ return void 0;
1252
2600
  }
1253
- db.exec(`
1254
- CREATE TABLE IF NOT EXISTS failure_classes (
1255
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1256
- name TEXT NOT NULL UNIQUE,
1257
- description TEXT NOT NULL,
1258
- diff_patterns TEXT NOT NULL DEFAULT '[]',
1259
- file_patterns TEXT NOT NULL DEFAULT '[]',
1260
- prompt_keywords TEXT NOT NULL DEFAULT '[]',
1261
- incidents TEXT NOT NULL DEFAULT '[]',
1262
- rules TEXT NOT NULL DEFAULT '[]',
1263
- scanner_checks TEXT NOT NULL DEFAULT '[]',
1264
- known_message TEXT NOT NULL DEFAULT '',
1265
- needs_review INTEGER NOT NULL DEFAULT 0,
1266
- created_at TEXT DEFAULT (datetime('now')),
1267
- updated_at TEXT DEFAULT (datetime('now'))
2601
+ }
2602
+ function dequeuePendingSync(db, limit = 10) {
2603
+ const stale = db.prepare(
2604
+ "SELECT id, retry_count, last_error FROM pending_sync WHERE retry_count >= 10 LIMIT 10000"
2605
+ ).all();
2606
+ if (stale.length > 0) {
2607
+ const ids = stale.map((s) => s.id);
2608
+ db.prepare(`DELETE FROM pending_sync WHERE id IN (${ids.map(() => "?").join(",")})`).run(...ids);
2609
+ const lastErrors = [...new Set(stale.map((s) => s.last_error).filter(Boolean))];
2610
+ process.stderr.write(
2611
+ `[massu] WARNING: ${stale.length} cloud-sync queue item(s) discarded after 10+ retries. Likely cause: invalid API key or unreachable endpoint. Recent errors: ${lastErrors.slice(0, 3).join("; ") || "(none recorded)"}
2612
+ `
1268
2613
  );
1269
- CREATE INDEX IF NOT EXISTS idx_fc_name ON failure_classes(name);
1270
- CREATE INDEX IF NOT EXISTS idx_fc_needs_review ON failure_classes(needs_review);
1271
- `);
2614
+ try {
2615
+ db.prepare(`
2616
+ INSERT INTO analytics_events (event_type, event_data, created_at)
2617
+ VALUES (?, ?, datetime('now'))
2618
+ `).run(
2619
+ "cloud_sync_giveup",
2620
+ JSON.stringify({
2621
+ discarded_count: stale.length,
2622
+ recent_errors: lastErrors.slice(0, 3)
2623
+ })
2624
+ );
2625
+ } catch {
2626
+ }
2627
+ }
2628
+ return db.prepare(
2629
+ "SELECT id, payload, retry_count FROM pending_sync ORDER BY created_at ASC LIMIT ?"
2630
+ ).all(limit);
2631
+ }
2632
+ function removePendingSync(db, id) {
2633
+ db.prepare("DELETE FROM pending_sync WHERE id = ?").run(id);
2634
+ }
2635
+ function incrementRetryCount(db, id, error) {
2636
+ db.prepare(
2637
+ "UPDATE pending_sync SET retry_count = retry_count + 1, last_error = ? WHERE id = ?"
2638
+ ).run(error, id);
1272
2639
  }
1273
2640
  function assignImportance(type, vrResult) {
1274
2641
  switch (type) {
@@ -1296,7 +2663,7 @@ function assignImportance(type, vrResult) {
1296
2663
  }
1297
2664
  function autoDetectTaskId(planFile) {
1298
2665
  if (!planFile) return null;
1299
- const base = basename(planFile);
2666
+ const base = basename2(planFile);
1300
2667
  return base.replace(/\.md$/, "");
1301
2668
  }
1302
2669
  function createSession(db, sessionId, opts) {
@@ -1307,29 +2674,91 @@ function createSession(db, sessionId, opts) {
1307
2674
  VALUES (?, ?, ?, ?, ?, ?)
1308
2675
  `).run(sessionId, opts?.branch ?? null, opts?.planFile ?? null, taskId, now.toISOString(), Math.floor(now.getTime() / 1e3));
1309
2676
  }
2677
+ function endSession(db, sessionId, status = "completed") {
2678
+ const now = /* @__PURE__ */ new Date();
2679
+ db.prepare(`
2680
+ UPDATE sessions SET status = ?, ended_at = ?, ended_at_epoch = ? WHERE session_id = ?
2681
+ `).run(status, now.toISOString(), Math.floor(now.getTime() / 1e3), sessionId);
2682
+ }
2683
+ function memoryTableHasTemporal(db, table) {
2684
+ let perDb = _temporalColCache.get(db);
2685
+ if (!perDb) {
2686
+ perDb = /* @__PURE__ */ new Map();
2687
+ _temporalColCache.set(db, perDb);
2688
+ }
2689
+ let has = perDb.get(table);
2690
+ if (has === void 0) {
2691
+ try {
2692
+ const cols = db.prepare(`PRAGMA table_info(${table})`).all();
2693
+ has = cols.some((c) => c.name === "valid_from_epoch");
2694
+ } catch {
2695
+ has = false;
2696
+ }
2697
+ perDb.set(table, has);
2698
+ }
2699
+ return has;
2700
+ }
1310
2701
  function addObservation(db, sessionId, type, title, detail, opts) {
1311
2702
  const now = /* @__PURE__ */ new Date();
1312
2703
  const importance = opts?.importance ?? assignImportance(type, opts?.evidence?.includes("PASS") ? "PASS" : void 0);
1313
- const result = db.prepare(`
1314
- INSERT INTO observations (session_id, type, title, detail, files_involved, plan_item, cr_rule, vr_type, evidence, importance, original_tokens, created_at, created_at_epoch)
1315
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1316
- `).run(
1317
- sessionId,
1318
- type,
1319
- title,
1320
- detail,
1321
- JSON.stringify(opts?.filesInvolved ?? []),
1322
- opts?.planItem ?? null,
1323
- opts?.crRule ?? null,
1324
- opts?.vrType ?? null,
1325
- opts?.evidence ?? null,
1326
- importance,
1327
- opts?.originalTokens ?? 0,
1328
- now.toISOString(),
1329
- Math.floor(now.getTime() / 1e3)
1330
- );
2704
+ const iso = now.toISOString();
2705
+ const epochSec = Math.floor(now.getTime() / 1e3);
2706
+ let result;
2707
+ if (memoryTableHasTemporal(db, "observations")) {
2708
+ result = db.prepare(`
2709
+ INSERT INTO observations (session_id, type, title, detail, files_involved, plan_item, cr_rule, vr_type, evidence, importance, original_tokens, created_at, created_at_epoch, valid_from, ingested_at, valid_from_epoch, ingested_at_epoch)
2710
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2711
+ `).run(
2712
+ sessionId,
2713
+ type,
2714
+ title,
2715
+ detail,
2716
+ JSON.stringify(opts?.filesInvolved ?? []),
2717
+ opts?.planItem ?? null,
2718
+ opts?.crRule ?? null,
2719
+ opts?.vrType ?? null,
2720
+ opts?.evidence ?? null,
2721
+ importance,
2722
+ opts?.originalTokens ?? 0,
2723
+ iso,
2724
+ epochSec,
2725
+ iso,
2726
+ iso,
2727
+ epochSec,
2728
+ epochSec
2729
+ );
2730
+ } else {
2731
+ result = db.prepare(`
2732
+ INSERT INTO observations (session_id, type, title, detail, files_involved, plan_item, cr_rule, vr_type, evidence, importance, original_tokens, created_at, created_at_epoch)
2733
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2734
+ `).run(
2735
+ sessionId,
2736
+ type,
2737
+ title,
2738
+ detail,
2739
+ JSON.stringify(opts?.filesInvolved ?? []),
2740
+ opts?.planItem ?? null,
2741
+ opts?.crRule ?? null,
2742
+ opts?.vrType ?? null,
2743
+ opts?.evidence ?? null,
2744
+ importance,
2745
+ opts?.originalTokens ?? 0,
2746
+ iso,
2747
+ epochSec
2748
+ );
2749
+ }
1331
2750
  return Number(result.lastInsertRowid);
1332
2751
  }
2752
+ function markRecordSuperseded(db, table, recordId, successorId, nowEpochSec = Math.floor(Date.now() / 1e3)) {
2753
+ const iso = new Date(nowEpochSec * 1e3).toISOString();
2754
+ const statusClause = table === "architecture_decisions" ? `, status = 'superseded'` : "";
2755
+ const res = db.prepare(
2756
+ `UPDATE ${table}
2757
+ SET valid_to = ?, expired_at = ?, valid_to_epoch = ?, expired_at_epoch = ?, superseded_by = ?${statusClause}
2758
+ WHERE id = ? AND expired_at IS NULL AND id != ?`
2759
+ ).run(iso, iso, nowEpochSec, nowEpochSec, successorId, recordId, successorId);
2760
+ return res.changes > 0;
2761
+ }
1333
2762
  function addSummary(db, sessionId, summary) {
1334
2763
  const now = /* @__PURE__ */ new Date();
1335
2764
  db.prepare(`
@@ -1351,6 +2780,194 @@ function addSummary(db, sessionId, summary) {
1351
2780
  Math.floor(now.getTime() / 1e3)
1352
2781
  );
1353
2782
  }
2783
+ function addUserPrompt(db, sessionId, text, promptNumber) {
2784
+ const now = /* @__PURE__ */ new Date();
2785
+ db.prepare(`
2786
+ INSERT INTO user_prompts (session_id, prompt_text, prompt_number, created_at, created_at_epoch)
2787
+ VALUES (?, ?, ?, ?, ?)
2788
+ `).run(sessionId, text, promptNumber, now.toISOString(), Math.floor(now.getTime() / 1e3));
2789
+ }
2790
+ function searchObservations(db, query, opts) {
2791
+ const limit = opts?.limit ?? 20;
2792
+ let sql = `
2793
+ SELECT o.id, o.type, o.title, o.created_at, o.session_id, o.importance,
2794
+ rank
2795
+ FROM observations_fts
2796
+ JOIN observations o ON observations_fts.rowid = o.id
2797
+ WHERE observations_fts MATCH ?
2798
+ `;
2799
+ const params = [sanitizeFts5Query(query)];
2800
+ if (opts?.type) {
2801
+ sql += " AND o.type = ?";
2802
+ params.push(opts.type);
2803
+ }
2804
+ if (opts?.crRule) {
2805
+ sql += " AND o.cr_rule = ?";
2806
+ params.push(opts.crRule);
2807
+ }
2808
+ if (opts?.dateFrom) {
2809
+ sql += " AND o.created_at >= ?";
2810
+ params.push(opts.dateFrom);
2811
+ }
2812
+ sql += " ORDER BY rank LIMIT ?";
2813
+ params.push(limit);
2814
+ return db.prepare(sql).all(...params);
2815
+ }
2816
+ function getRecentObservations(db, limit = 20, sessionId) {
2817
+ if (sessionId) {
2818
+ return db.prepare(`
2819
+ SELECT id, type, title, detail, importance, created_at, session_id
2820
+ FROM observations WHERE session_id = ?
2821
+ ORDER BY created_at_epoch DESC LIMIT ?
2822
+ `).all(sessionId, limit);
2823
+ }
2824
+ return db.prepare(`
2825
+ SELECT id, type, title, detail, importance, created_at, session_id
2826
+ FROM observations
2827
+ ORDER BY created_at_epoch DESC LIMIT ?
2828
+ `).all(limit);
2829
+ }
2830
+ function getSessionSummaries(db, limit = 10) {
2831
+ return db.prepare(`
2832
+ SELECT session_id, request, completed, failed_attempts, plan_progress, created_at
2833
+ FROM session_summaries
2834
+ ORDER BY created_at_epoch DESC LIMIT ?
2835
+ `).all(limit);
2836
+ }
2837
+ function getSessionTimeline(db, sessionId) {
2838
+ const session = db.prepare("SELECT * FROM sessions WHERE session_id = ?").get(sessionId);
2839
+ const observations = db.prepare("SELECT * FROM observations WHERE session_id = ? ORDER BY created_at_epoch ASC").all(sessionId);
2840
+ const summary = db.prepare("SELECT * FROM session_summaries WHERE session_id = ? ORDER BY created_at_epoch DESC LIMIT 1").get(sessionId);
2841
+ const prompts = db.prepare("SELECT * FROM user_prompts WHERE session_id = ? ORDER BY prompt_number ASC").all(sessionId);
2842
+ return {
2843
+ session: session ?? null,
2844
+ observations,
2845
+ summary: summary ?? null,
2846
+ prompts
2847
+ };
2848
+ }
2849
+ function getFailedAttempts(db, query, limit = 20) {
2850
+ if (query) {
2851
+ return db.prepare(`
2852
+ SELECT o.id, o.title, o.detail, o.session_id, o.recurrence_count, o.created_at
2853
+ FROM observations_fts
2854
+ JOIN observations o ON observations_fts.rowid = o.id
2855
+ WHERE observations_fts MATCH ? AND o.type = 'failed_attempt'
2856
+ ORDER BY o.recurrence_count DESC, rank LIMIT ?
2857
+ `).all(sanitizeFts5Query(query), limit);
2858
+ }
2859
+ return db.prepare(`
2860
+ SELECT id, title, detail, session_id, recurrence_count, created_at
2861
+ FROM observations WHERE type = 'failed_attempt'
2862
+ ORDER BY recurrence_count DESC, created_at_epoch DESC LIMIT ?
2863
+ `).all(limit);
2864
+ }
2865
+ function getDecisionsAbout(db, query, limit = 20) {
2866
+ return db.prepare(`
2867
+ SELECT o.id, o.title, o.detail, o.session_id, o.created_at
2868
+ FROM observations_fts
2869
+ JOIN observations o ON observations_fts.rowid = o.id
2870
+ WHERE observations_fts MATCH ? AND o.type = 'decision'
2871
+ ORDER BY rank LIMIT ?
2872
+ `).all(sanitizeFts5Query(query), limit);
2873
+ }
2874
+ function armUsageCounter(db, nowEpochSec) {
2875
+ const existing = getMemoryMeta(db, USAGE_COUNTER_ARMED_KEY);
2876
+ if (existing) {
2877
+ const n = Number(existing);
2878
+ if (Number.isFinite(n) && n > 0) return n;
2879
+ }
2880
+ const now = nowEpochSec ?? Math.floor(Date.now() / 1e3);
2881
+ setMemoryMeta(db, USAGE_COUNTER_ARMED_KEY, String(now));
2882
+ return now;
2883
+ }
2884
+ function usageWarmupElapsed(db, warmupDays, nowEpochSec) {
2885
+ const armed = getMemoryMeta(db, USAGE_COUNTER_ARMED_KEY);
2886
+ if (!armed) return false;
2887
+ const armedEpoch = Number(armed);
2888
+ if (!Number.isFinite(armedEpoch) || armedEpoch <= 0) return false;
2889
+ const now = nowEpochSec ?? Math.floor(Date.now() / 1e3);
2890
+ return now - armedEpoch >= warmupDays * 86400;
2891
+ }
2892
+ function expireOldLowValueObservations(db, opts) {
2893
+ const now = opts.nowEpochSec ?? Math.floor(Date.now() / 1e3);
2894
+ if (!usageWarmupElapsed(db, opts.usageWarmupDays, now)) return 0;
2895
+ const cutoffEpoch = now - opts.retentionDays * 86400;
2896
+ const typePlaceholders = opts.protectedTypes.length ? opts.protectedTypes.map(() => "?").join(",") : "''";
2897
+ const iso = new Date(now * 1e3).toISOString();
2898
+ const result = db.prepare(
2899
+ `UPDATE observations
2900
+ SET expired_at = ?,
2901
+ expired_at_epoch = ?,
2902
+ valid_to = ?,
2903
+ valid_to_epoch = ?
2904
+ WHERE expired_at IS NULL
2905
+ AND created_at_epoch < ?
2906
+ AND importance <= ?
2907
+ AND type NOT IN (${typePlaceholders})
2908
+ AND COALESCE(evidence, '') != ?
2909
+ -- A file-backed row mirrors a file the human still keeps on disk.
2910
+ -- Value-decay may never retire it; only the file's removal can.
2911
+ AND title NOT LIKE ?
2912
+ AND NOT EXISTS (
2913
+ SELECT 1 FROM memory_usage u
2914
+ WHERE u.source = 'observation'
2915
+ AND u.record_id = observations.id
2916
+ AND u.hit_count > 0
2917
+ )
2918
+ AND NOT EXISTS (
2919
+ -- The grace period: a row demoted within the last cadence
2920
+ -- window is NOT yet expirable, so being pushed to the floor can
2921
+ -- never be immediately fatal.
2922
+ SELECT 1 FROM memory_usage u
2923
+ WHERE u.source = 'observation'
2924
+ AND u.record_id = observations.id
2925
+ AND u.last_reweight_epoch IS NOT NULL
2926
+ AND u.last_reweight_epoch > ?
2927
+ )`
2928
+ ).run(
2929
+ iso,
2930
+ now,
2931
+ iso,
2932
+ now,
2933
+ cutoffEpoch,
2934
+ opts.importanceFloor,
2935
+ ...opts.protectedTypes,
2936
+ CONSOLIDATION_LESSON_EVIDENCE,
2937
+ MEMORY_FILE_TITLE_LIKE,
2938
+ now - (opts.reweightIntervalDays ?? 1) * 86400
2939
+ );
2940
+ return result.changes;
2941
+ }
2942
+ function pruneOldObservations(db, opts) {
2943
+ return expireOldLowValueObservations(db, opts);
2944
+ }
2945
+ function recordRecallHits(db, sessionId, hits, nowEpochSec) {
2946
+ if (!hits.length) return 0;
2947
+ const now = nowEpochSec ?? Math.floor(Date.now() / 1e3);
2948
+ let recorded = 0;
2949
+ const claim = db.prepare(
2950
+ `INSERT OR IGNORE INTO memory_usage_sessions (source, record_id, session_id)
2951
+ VALUES (?, ?, ?)`
2952
+ );
2953
+ const bump = db.prepare(
2954
+ `INSERT INTO memory_usage (source, record_id, hit_count, hits_windowed, last_hit_epoch)
2955
+ VALUES (?, ?, 1, 1, ?)
2956
+ ON CONFLICT(source, record_id) DO UPDATE SET
2957
+ hit_count = hit_count + 1,
2958
+ hits_windowed = hits_windowed + 1,
2959
+ last_hit_epoch = excluded.last_hit_epoch`
2960
+ );
2961
+ const tx = db.transaction(() => {
2962
+ for (const h of hits) {
2963
+ if (claim.run(h.source, h.id, sessionId).changes === 0) continue;
2964
+ bump.run(h.source, h.id, now);
2965
+ recorded++;
2966
+ }
2967
+ });
2968
+ tx();
2969
+ return recorded;
2970
+ }
1354
2971
  function deduplicateFailedAttempt(db, sessionId, title, detail, opts) {
1355
2972
  const existing = db.prepare(`
1356
2973
  SELECT id, recurrence_count FROM observations
@@ -1366,13 +2983,355 @@ function deduplicateFailedAttempt(db, sessionId, title, detail, opts) {
1366
2983
  importance: 5
1367
2984
  });
1368
2985
  }
2986
+ function getSessionsByTask(db, taskId) {
2987
+ return db.prepare(`
2988
+ SELECT session_id, status, started_at, ended_at, plan_phase
2989
+ FROM sessions WHERE task_id = ?
2990
+ ORDER BY started_at_epoch DESC
2991
+ LIMIT 10000
2992
+ `).all(taskId);
2993
+ }
2994
+ function getCrossTaskProgress(db, taskId) {
2995
+ const sessions = db.prepare(`
2996
+ SELECT session_id FROM sessions WHERE task_id = ? LIMIT 10000
2997
+ `).all(taskId);
2998
+ const merged = {};
2999
+ for (const session of sessions) {
3000
+ const summaries = db.prepare(`
3001
+ SELECT plan_progress FROM session_summaries WHERE session_id = ? LIMIT 10000
3002
+ `).all(session.session_id);
3003
+ for (const summary of summaries) {
3004
+ try {
3005
+ const progress = JSON.parse(summary.plan_progress);
3006
+ for (const [key, value] of Object.entries(progress)) {
3007
+ if (!merged[key] || value === "complete" || value === "in_progress" && merged[key] === "pending") {
3008
+ merged[key] = value;
3009
+ }
3010
+ }
3011
+ } catch (_e) {
3012
+ }
3013
+ }
3014
+ }
3015
+ return merged;
3016
+ }
3017
+ function linkSessionToTask(db, sessionId, taskId) {
3018
+ db.prepare("UPDATE sessions SET task_id = ? WHERE session_id = ?").run(taskId, sessionId);
3019
+ }
3020
+ function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantResponse, toolCallsJson, toolCallCount, promptTokens, responseTokens) {
3021
+ const result = db.prepare(`
3022
+ INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens)
3023
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3024
+ `).run(
3025
+ sessionId,
3026
+ turnNumber,
3027
+ userPrompt,
3028
+ assistantResponse ? assistantResponse.slice(0, 1e4) : null,
3029
+ toolCallsJson,
3030
+ toolCallCount,
3031
+ promptTokens,
3032
+ responseTokens
3033
+ );
3034
+ return Number(result.lastInsertRowid);
3035
+ }
3036
+ function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, inputSize, outputSize, success, filesInvolved) {
3037
+ db.prepare(`
3038
+ INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved)
3039
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3040
+ `).run(
3041
+ sessionId,
3042
+ turnNumber,
3043
+ toolName,
3044
+ inputSummary ? inputSummary.slice(0, 500) : null,
3045
+ inputSize,
3046
+ outputSize,
3047
+ success ? 1 : 0,
3048
+ filesInvolved ? JSON.stringify(filesInvolved) : null
3049
+ );
3050
+ }
3051
+ function getLastProcessedLine(db, sessionId) {
3052
+ const row = db.prepare("SELECT value FROM memory_meta WHERE key = ?").get(`last_processed_line:${sessionId}`);
3053
+ return row ? parseInt(row.value, 10) : 0;
3054
+ }
3055
+ function setLastProcessedLine(db, sessionId, lineNumber) {
3056
+ db.prepare("INSERT OR REPLACE INTO memory_meta (key, value) VALUES (?, ?)").run(`last_processed_line:${sessionId}`, String(lineNumber));
3057
+ }
3058
+ function pruneOldConversationTurns(db, retentionDays = 90) {
3059
+ const cutoffEpoch = Math.floor(Date.now() / 1e3) - retentionDays * 86400;
3060
+ const turnsResult = db.prepare("DELETE FROM conversation_turns WHERE created_at_epoch < ?").run(cutoffEpoch);
3061
+ const detailsResult = db.prepare("DELETE FROM tool_call_details WHERE created_at_epoch < ?").run(cutoffEpoch);
3062
+ return { turnsDeleted: turnsResult.changes, detailsDeleted: detailsResult.changes };
3063
+ }
3064
+ function getConversationTurns(db, sessionId, opts) {
3065
+ let sql = "SELECT id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens, created_at FROM conversation_turns WHERE session_id = ?";
3066
+ const params = [sessionId];
3067
+ if (opts?.turnFrom !== void 0) {
3068
+ sql += " AND turn_number >= ?";
3069
+ params.push(opts.turnFrom);
3070
+ }
3071
+ if (opts?.turnTo !== void 0) {
3072
+ sql += " AND turn_number <= ?";
3073
+ params.push(opts.turnTo);
3074
+ }
3075
+ sql += " ORDER BY turn_number ASC";
3076
+ return db.prepare(sql).all(...params);
3077
+ }
3078
+ function searchConversationTurns(db, query, opts) {
3079
+ const limit = opts?.limit ?? 20;
3080
+ let sql = `
3081
+ SELECT ct.id, ct.session_id, ct.turn_number, ct.user_prompt, ct.tool_call_count, ct.response_tokens, ct.created_at, rank
3082
+ FROM conversation_turns_fts
3083
+ JOIN conversation_turns ct ON conversation_turns_fts.rowid = ct.id
3084
+ WHERE conversation_turns_fts MATCH ?
3085
+ `;
3086
+ const params = [sanitizeFts5Query(query)];
3087
+ if (opts?.sessionId) {
3088
+ sql += " AND ct.session_id = ?";
3089
+ params.push(opts.sessionId);
3090
+ }
3091
+ if (opts?.dateFrom) {
3092
+ sql += " AND ct.created_at >= ?";
3093
+ params.push(opts.dateFrom);
3094
+ }
3095
+ if (opts?.dateTo) {
3096
+ sql += " AND ct.created_at <= ?";
3097
+ params.push(opts.dateTo);
3098
+ }
3099
+ if (opts?.minToolCalls !== void 0) {
3100
+ sql += " AND ct.tool_call_count >= ?";
3101
+ params.push(opts.minToolCalls);
3102
+ }
3103
+ sql += " ORDER BY rank LIMIT ?";
3104
+ params.push(limit);
3105
+ return db.prepare(sql).all(...params);
3106
+ }
3107
+ function getToolPatterns(db, opts) {
3108
+ const groupBy = opts?.groupBy ?? "tool";
3109
+ const params = [];
3110
+ let whereClause = "";
3111
+ const conditions = [];
3112
+ if (opts?.sessionId) {
3113
+ conditions.push("session_id = ?");
3114
+ params.push(opts.sessionId);
3115
+ }
3116
+ if (opts?.toolName) {
3117
+ conditions.push("tool_name = ?");
3118
+ params.push(opts.toolName);
3119
+ }
3120
+ if (opts?.dateFrom) {
3121
+ conditions.push("created_at >= ?");
3122
+ params.push(opts.dateFrom);
3123
+ }
3124
+ if (conditions.length > 0) {
3125
+ whereClause = "WHERE " + conditions.join(" AND ");
3126
+ }
3127
+ let sql;
3128
+ switch (groupBy) {
3129
+ case "session":
3130
+ sql = `SELECT session_id, COUNT(*) as call_count, COUNT(DISTINCT tool_name) as unique_tools,
3131
+ SUM(CASE WHEN tool_success = 1 THEN 1 ELSE 0 END) as successes,
3132
+ SUM(CASE WHEN tool_success = 0 THEN 1 ELSE 0 END) as failures,
3133
+ AVG(tool_output_size) as avg_output_size
3134
+ FROM tool_call_details ${whereClause}
3135
+ GROUP BY session_id ORDER BY call_count DESC`;
3136
+ break;
3137
+ case "day":
3138
+ sql = `SELECT date(created_at) as day, COUNT(*) as call_count, COUNT(DISTINCT tool_name) as unique_tools,
3139
+ SUM(CASE WHEN tool_success = 1 THEN 1 ELSE 0 END) as successes
3140
+ FROM tool_call_details ${whereClause}
3141
+ GROUP BY date(created_at) ORDER BY day DESC`;
3142
+ break;
3143
+ default:
3144
+ sql = `SELECT tool_name, COUNT(*) as call_count,
3145
+ SUM(CASE WHEN tool_success = 1 THEN 1 ELSE 0 END) as successes,
3146
+ SUM(CASE WHEN tool_success = 0 THEN 1 ELSE 0 END) as failures,
3147
+ AVG(tool_output_size) as avg_output_size,
3148
+ AVG(tool_input_size) as avg_input_size
3149
+ FROM tool_call_details ${whereClause}
3150
+ GROUP BY tool_name ORDER BY call_count DESC`;
3151
+ break;
3152
+ }
3153
+ return db.prepare(sql).all(...params);
3154
+ }
3155
+ function getSessionStats(db, opts) {
3156
+ if (opts?.sessionId) {
3157
+ const turns = db.prepare("SELECT COUNT(*) as turn_count, SUM(tool_call_count) as total_tool_calls, SUM(prompt_tokens) as total_prompt_tokens, SUM(response_tokens) as total_response_tokens FROM conversation_turns WHERE session_id = ?").get(opts.sessionId);
3158
+ const toolBreakdown = db.prepare("SELECT tool_name, COUNT(*) as count FROM tool_call_details WHERE session_id = ? GROUP BY tool_name ORDER BY count DESC").all(opts.sessionId);
3159
+ const session = db.prepare("SELECT * FROM sessions WHERE session_id = ?").get(opts.sessionId);
3160
+ return [{
3161
+ session_id: opts.sessionId,
3162
+ status: session?.status ?? "unknown",
3163
+ started_at: session?.started_at ?? null,
3164
+ ended_at: session?.ended_at ?? null,
3165
+ ...turns,
3166
+ tool_breakdown: toolBreakdown
3167
+ }];
3168
+ }
3169
+ const limit = opts?.limit ?? 10;
3170
+ return db.prepare(`
3171
+ SELECT s.session_id, s.status, s.started_at, s.ended_at,
3172
+ COUNT(ct.id) as turn_count,
3173
+ COALESCE(SUM(ct.tool_call_count), 0) as total_tool_calls,
3174
+ COALESCE(SUM(ct.prompt_tokens), 0) as total_prompt_tokens,
3175
+ COALESCE(SUM(ct.response_tokens), 0) as total_response_tokens
3176
+ FROM sessions s
3177
+ LEFT JOIN conversation_turns ct ON s.session_id = ct.session_id
3178
+ GROUP BY s.session_id
3179
+ ORDER BY s.started_at_epoch DESC
3180
+ LIMIT ?
3181
+ `).all(limit);
3182
+ }
3183
+ function getObservabilityDbSize(db) {
3184
+ const turnsCount = db.prepare("SELECT COUNT(*) as c FROM conversation_turns").get().c;
3185
+ const detailsCount = db.prepare("SELECT COUNT(*) as c FROM tool_call_details").get().c;
3186
+ const obsCount = db.prepare("SELECT COUNT(*) as c FROM observations").get().c;
3187
+ const pageCount = db.pragma("page_count")[0]?.page_count ?? 0;
3188
+ const pageSize = db.pragma("page_size")[0]?.page_size ?? 4096;
3189
+ return {
3190
+ conversation_turns_count: turnsCount,
3191
+ tool_call_details_count: detailsCount,
3192
+ observations_count: obsCount,
3193
+ db_page_count: pageCount,
3194
+ db_page_size: pageSize,
3195
+ estimated_size_mb: Math.round(pageCount * pageSize / (1024 * 1024) * 100) / 100
3196
+ };
3197
+ }
3198
+ function addFailureClass(db, opts) {
3199
+ const result = db.prepare(`
3200
+ INSERT OR IGNORE INTO failure_classes (name, description, diff_patterns, file_patterns, prompt_keywords, incidents, rules, scanner_checks, known_message, needs_review)
3201
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3202
+ `).run(
3203
+ opts.name,
3204
+ opts.description,
3205
+ JSON.stringify(opts.diffPatterns ?? []),
3206
+ JSON.stringify(opts.filePatterns ?? []),
3207
+ JSON.stringify(opts.promptKeywords ?? []),
3208
+ JSON.stringify(opts.incidents ?? []),
3209
+ JSON.stringify(opts.rules ?? []),
3210
+ JSON.stringify(opts.scannerChecks ?? []),
3211
+ opts.knownMessage ?? "",
3212
+ opts.needsReview ? 1 : 0
3213
+ );
3214
+ return Number(result.lastInsertRowid);
3215
+ }
3216
+ function getFailureClasses(db) {
3217
+ const rows = db.prepare("SELECT * FROM failure_classes ORDER BY name LIMIT 10000").all();
3218
+ return rows.map((row) => ({
3219
+ id: row.id,
3220
+ name: row.name,
3221
+ description: row.description,
3222
+ diff_patterns: JSON.parse(row.diff_patterns || "[]"),
3223
+ file_patterns: JSON.parse(row.file_patterns || "[]"),
3224
+ prompt_keywords: JSON.parse(row.prompt_keywords || "[]"),
3225
+ incidents: JSON.parse(row.incidents || "[]"),
3226
+ rules: JSON.parse(row.rules || "[]"),
3227
+ scanner_checks: JSON.parse(row.scanner_checks || "[]"),
3228
+ known_message: row.known_message,
3229
+ needs_review: !!row.needs_review
3230
+ }));
3231
+ }
3232
+ function appendIncidentToFailureClass(db, className, incidentId) {
3233
+ const row = db.prepare("SELECT incidents FROM failure_classes WHERE name = ?").get(className);
3234
+ if (!row) return;
3235
+ const incidents = JSON.parse(row.incidents || "[]");
3236
+ if (!incidents.includes(incidentId)) {
3237
+ incidents.push(incidentId);
3238
+ db.prepare("UPDATE failure_classes SET incidents = ?, updated_at = datetime('now') WHERE name = ?").run(JSON.stringify(incidents), className);
3239
+ }
3240
+ }
3241
+ function scoreFailureClasses(db, matchText, filePath, promptContext, weights) {
3242
+ const classes = getFailureClasses(db);
3243
+ if (classes.length === 0) return null;
3244
+ const diffWeight = weights?.diffPatternWeight ?? 3;
3245
+ const fileWeight = weights?.filePatternWeight ?? 2;
3246
+ const promptWeight = weights?.promptKeywordWeight ?? 2;
3247
+ let bestMatch = null;
3248
+ for (const fc of classes) {
3249
+ let score = 0;
3250
+ for (const pattern of fc.diff_patterns) {
3251
+ if (!pattern) continue;
3252
+ try {
3253
+ if (new RegExp(pattern, "i").test(matchText)) {
3254
+ score += diffWeight;
3255
+ }
3256
+ } catch {
3257
+ if (matchText.toLowerCase().includes(pattern.toLowerCase())) {
3258
+ score += diffWeight;
3259
+ }
3260
+ }
3261
+ }
3262
+ for (const pattern of fc.file_patterns) {
3263
+ if (!pattern) continue;
3264
+ try {
3265
+ if (new RegExp(pattern).test(filePath)) {
3266
+ score += fileWeight;
3267
+ }
3268
+ } catch {
3269
+ if (filePath.includes(pattern)) {
3270
+ score += fileWeight;
3271
+ }
3272
+ }
3273
+ }
3274
+ if (promptContext) {
3275
+ for (const keyword of fc.prompt_keywords) {
3276
+ if (!keyword) continue;
3277
+ try {
3278
+ if (new RegExp(keyword, "i").test(promptContext)) {
3279
+ score += promptWeight;
3280
+ }
3281
+ } catch {
3282
+ if (promptContext.toLowerCase().includes(keyword.toLowerCase())) {
3283
+ score += promptWeight;
3284
+ }
3285
+ }
3286
+ }
3287
+ }
3288
+ if (!bestMatch || score > bestMatch.score) {
3289
+ bestMatch = {
3290
+ name: fc.name,
3291
+ score,
3292
+ incidentCount: fc.incidents.length,
3293
+ rules: fc.rules,
3294
+ knownMessage: fc.known_message
3295
+ };
3296
+ }
3297
+ }
3298
+ return bestMatch;
3299
+ }
3300
+ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_DRAIN_LIMIT, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
3301
+ var init_memory_db = __esm({
3302
+ "src/memory-db.ts"() {
3303
+ "use strict";
3304
+ init_config();
3305
+ init_memory_vector();
3306
+ init_db_backup();
3307
+ init_hook_failure_signal();
3308
+ init_memory_embed_sweep();
3309
+ MEMORY_SCHEMA_VERSION = 1;
3310
+ TOOL_COST_EVENTS_RETENTION_DAYS = 90;
3311
+ MAX_DRAFT_TEXT_LEN = 16384;
3312
+ FUNNEL_EVENT_DRAIN_LIMIT = 5e3;
3313
+ FUNNEL_EVENT_OUTBOX_CAP = 2e4;
3314
+ _temporalColCache = /* @__PURE__ */ new WeakMap();
3315
+ CONSOLIDATION_LESSON_EVIDENCE = "consolidation:session-summary";
3316
+ MEMORY_FILE_TITLE_PREFIX = "[memory-file] ";
3317
+ MEMORY_FILE_TITLE_LIKE = `${MEMORY_FILE_TITLE_PREFIX}%`;
3318
+ USAGE_COUNTER_ARMED_KEY = "usage_counter_armed_epoch";
3319
+ }
3320
+ });
3321
+
3322
+ // src/hooks/post-tool-use.ts
3323
+ init_memory_db();
1369
3324
 
1370
3325
  // src/transcript-parser.ts
1371
3326
  function estimateTokens(text) {
1372
3327
  return Math.ceil(text.length / 4);
1373
3328
  }
1374
3329
 
3330
+ // src/observation-extractor.ts
3331
+ init_memory_db();
3332
+
1375
3333
  // src/adr-generator.ts
3334
+ init_config();
1376
3335
  var DEFAULT_DETECTION_PHRASES = ["chose", "decided", "switching to", "moving from", "going with"];
1377
3336
  function getDetectionPhrases() {
1378
3337
  return getConfig().governance?.adr?.detection_phrases ?? DEFAULT_DETECTION_PHRASES;
@@ -1382,9 +3341,152 @@ function detectDecisionPatterns(text) {
1382
3341
  const lower = text.toLowerCase();
1383
3342
  return phrases.some((phrase) => lower.includes(phrase));
1384
3343
  }
3344
+ function extractAlternatives(description) {
3345
+ const alternatives = [];
3346
+ const overMatch = description.match(/(\w[\w\s-]+)\s+over\s+(\w[\w\s-]+)/i);
3347
+ if (overMatch) {
3348
+ alternatives.push(overMatch[1].trim());
3349
+ alternatives.push(overMatch[2].trim());
3350
+ }
3351
+ const insteadMatch = description.match(/(\w[\w\s-]+)\s+instead\s+of\s+(\w[\w\s-]+)/i);
3352
+ if (insteadMatch) {
3353
+ alternatives.push(insteadMatch[1].trim());
3354
+ alternatives.push(insteadMatch[2].trim());
3355
+ }
3356
+ const switchMatch = description.match(/switching\s+from\s+(\w[\w\s-]+)\s+to\s+(\w[\w\s-]+)/i);
3357
+ if (switchMatch) {
3358
+ alternatives.push(switchMatch[2].trim());
3359
+ alternatives.push(switchMatch[1].trim());
3360
+ }
3361
+ return [...new Set(alternatives)];
3362
+ }
3363
+ function storeDecision(db, decision) {
3364
+ const existing = new Set(
3365
+ db.prepare(`PRAGMA table_info(architecture_decisions)`).all().map(
3366
+ (c) => c.name
3367
+ )
3368
+ );
3369
+ const cols = ["title", "context", "decision", "alternatives", "consequences", "session_id", "status"];
3370
+ const vals = [
3371
+ decision.title,
3372
+ decision.context,
3373
+ decision.decision,
3374
+ JSON.stringify(decision.alternatives),
3375
+ decision.consequences,
3376
+ decision.sessionId ?? null,
3377
+ decision.status ?? "accepted"
3378
+ ];
3379
+ if (existing.has("affected_files")) {
3380
+ cols.push("affected_files");
3381
+ vals.push(decision.affectedFiles ? JSON.stringify(decision.affectedFiles) : null);
3382
+ }
3383
+ const hasTemporal = existing.has("valid_from_epoch");
3384
+ const extraSql = hasTemporal ? `, valid_from, ingested_at, valid_from_epoch, ingested_at_epoch` : "";
3385
+ const extraVals = hasTemporal ? `, datetime('now'), datetime('now'), CAST(strftime('%s','now') AS INTEGER), CAST(strftime('%s','now') AS INTEGER)` : "";
3386
+ const result = db.prepare(
3387
+ `INSERT INTO architecture_decisions (${cols.join(", ")}${extraSql})
3388
+ VALUES (${cols.map(() => "?").join(", ")}${extraVals})`
3389
+ ).run(...vals);
3390
+ return Number(result.lastInsertRowid);
3391
+ }
3392
+
3393
+ // src/observation-extractor.ts
3394
+ init_config();
3395
+ import { homedir as homedir4 } from "os";
3396
+
3397
+ // src/hooks/lib/tool-response.ts
3398
+ function asString(v) {
3399
+ return typeof v === "string" ? v : "";
3400
+ }
3401
+ function fromContentBlocks(arr) {
3402
+ const parts = [];
3403
+ for (const block of arr) {
3404
+ if (typeof block === "string") {
3405
+ parts.push(block);
3406
+ } else if (block && typeof block === "object") {
3407
+ const t2 = asString(block.text);
3408
+ if (t2) parts.push(t2);
3409
+ }
3410
+ }
3411
+ return parts.length > 0 ? parts.join("\n") : null;
3412
+ }
3413
+ function textFromObject(o) {
3414
+ if ("stdout" in o || "stderr" in o) {
3415
+ const out = asString(o.stdout);
3416
+ const err = asString(o.stderr);
3417
+ return err ? out ? `${out}
3418
+ ${err}` : err : out;
3419
+ }
3420
+ const file = o.file;
3421
+ if (file && typeof file === "object") {
3422
+ const c = asString(file.content);
3423
+ if (c) return c;
3424
+ }
3425
+ const content = o.content;
3426
+ if (typeof content === "string" && content) return content;
3427
+ if (Array.isArray(content)) {
3428
+ const blocks = fromContentBlocks(content);
3429
+ if (blocks) return blocks;
3430
+ }
3431
+ if ("structuredPatch" in o || "oldString" in o || "newString" in o) {
3432
+ const path = asString(o.filePath);
3433
+ const newStr = asString(o.newString);
3434
+ return [path, newStr].filter(Boolean).join("\n");
3435
+ }
3436
+ for (const k of ["text", "message", "output", "result", "stdout"]) {
3437
+ const v = asString(o[k]);
3438
+ if (v) return v;
3439
+ }
3440
+ try {
3441
+ return JSON.stringify(o);
3442
+ } catch {
3443
+ return "";
3444
+ }
3445
+ }
3446
+ function detectError(raw, text) {
3447
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
3448
+ const o = raw;
3449
+ if (o.is_error === true || o.isError === true) return true;
3450
+ if (o.interrupted === true) return true;
3451
+ }
3452
+ return /^Error: (Exit code \d+|.*failed)/i.test(text.trimStart());
3453
+ }
3454
+ function normalizeToolResponse(raw) {
3455
+ if (raw === null || raw === void 0 || raw === "") {
3456
+ return { text: "", isError: false, shape: "empty" };
3457
+ }
3458
+ let text;
3459
+ let shape;
3460
+ if (typeof raw === "string") {
3461
+ text = raw;
3462
+ shape = "string";
3463
+ } else if (Array.isArray(raw)) {
3464
+ shape = "array";
3465
+ const blocks = fromContentBlocks(raw);
3466
+ if (blocks !== null) {
3467
+ text = blocks;
3468
+ } else {
3469
+ try {
3470
+ text = JSON.stringify(raw);
3471
+ } catch {
3472
+ text = "";
3473
+ }
3474
+ }
3475
+ } else if (typeof raw === "object") {
3476
+ shape = "object";
3477
+ try {
3478
+ text = textFromObject(raw);
3479
+ } catch {
3480
+ text = "";
3481
+ }
3482
+ } else {
3483
+ shape = "string";
3484
+ text = String(raw);
3485
+ }
3486
+ return { text, isError: detectError(raw, text), shape };
3487
+ }
1385
3488
 
1386
3489
  // src/observation-extractor.ts
1387
- import { homedir as homedir3 } from "os";
1388
3490
  var PRIVATE_PATTERNS = [
1389
3491
  /\/Users\/\w+/,
1390
3492
  // Absolute macOS paths
@@ -1423,7 +3525,7 @@ function isNoisyToolCall(tc, seenReads2) {
1423
3525
  const trivialPatterns = /^(ls|pwd|echo|cat\s|head\s|tail\s|wc\s)/;
1424
3526
  if (trivialPatterns.test(cmd)) return true;
1425
3527
  }
1426
- if (!tc.result || tc.result.trim() === "") return true;
3528
+ if (!tc.result || typeof tc.result !== "string" || tc.result.trim() === "") return true;
1427
3529
  return false;
1428
3530
  }
1429
3531
  function classifyToolCall(tc) {
@@ -1575,25 +3677,27 @@ function shortenPath(filePath) {
1575
3677
  if (filePath.startsWith(root + "/")) {
1576
3678
  return filePath.slice(root.length + 1);
1577
3679
  }
1578
- const home = homedir3();
3680
+ const home = homedir4();
1579
3681
  if (filePath.startsWith(home + "/")) {
1580
3682
  return "~/" + filePath.slice(home.length + 1);
1581
3683
  }
1582
3684
  return filePath;
1583
3685
  }
1584
- function classifyRealTimeToolCall(toolName, toolInput, toolResponse, seenReads2) {
3686
+ function classifyRealTimeToolCall(toolName, toolInput, toolResponse, seenReads2, isError) {
3687
+ const normalized = normalizeToolResponse(toolResponse);
1585
3688
  const tc = {
1586
3689
  toolName,
1587
3690
  toolUseId: "",
1588
3691
  input: toolInput,
1589
- result: toolResponse,
1590
- isError: false
3692
+ result: normalized.text,
3693
+ isError: isError ?? normalized.isError
1591
3694
  };
1592
3695
  if (isNoisyToolCall(tc, seenReads2)) return null;
1593
- if (toolResponse && detectDecisionPatterns(toolResponse)) {
1594
- const firstLine = toolResponse.split("\n")[0].slice(0, 200);
3696
+ const responseText = normalized.text;
3697
+ if (responseText && detectDecisionPatterns(responseText)) {
3698
+ const firstLine = responseText.split("\n")[0].slice(0, 200);
1595
3699
  const title = `Architecture decision: ${firstLine}`;
1596
- const detail = toolResponse.slice(0, 1e3);
3700
+ const detail = responseText.slice(0, 1e3);
1597
3701
  return {
1598
3702
  type: "decision",
1599
3703
  title,
@@ -1601,14 +3705,15 @@ function classifyRealTimeToolCall(toolName, toolInput, toolResponse, seenReads2)
1601
3705
  visibility: classifyVisibility(title, detail),
1602
3706
  opts: {
1603
3707
  importance: assignImportance("decision"),
1604
- originalTokens: estimateTokens(toolResponse),
1605
- ...extractLinkedReferences(toolResponse)
3708
+ originalTokens: estimateTokens(responseText),
3709
+ ...extractLinkedReferences(responseText)
1606
3710
  }
1607
3711
  };
1608
3712
  }
1609
3713
  return classifyToolCall(tc);
1610
3714
  }
1611
- function detectPlanProgress(toolResponse) {
3715
+ function detectPlanProgress(rawToolResponse) {
3716
+ const toolResponse = normalizeToolResponse(rawToolResponse).text;
1612
3717
  const results = [];
1613
3718
  const progressPattern = /(P\d+-\d+)\s*[:\-]?\s*(COMPLETE|PASS|DONE|complete|pass|done)/g;
1614
3719
  let match;
@@ -1619,6 +3724,7 @@ function detectPlanProgress(toolResponse) {
1619
3724
  }
1620
3725
 
1621
3726
  // src/audit-trail.ts
3727
+ init_config();
1622
3728
  function logAuditEntry(db, entry) {
1623
3729
  db.prepare(`
1624
3730
  INSERT INTO audit_log (session_id, event_type, actor, model_id, file_path, change_type, rules_in_effect, approval_status, evidence, metadata)
@@ -1638,6 +3744,7 @@ function logAuditEntry(db, entry) {
1638
3744
  }
1639
3745
 
1640
3746
  // src/regression-detector.ts
3747
+ init_config();
1641
3748
  function calculateHealthScore(testsPassing, testsFailing, modificationsSinceTest, lastTested, lastModified) {
1642
3749
  let score = 100;
1643
3750
  if (testsFailing > 0) {
@@ -1714,15 +3821,19 @@ function recordTestResult(db, featureKey, passing, failing) {
1714
3821
  );
1715
3822
  }
1716
3823
 
3824
+ // src/validation-engine.ts
3825
+ init_config();
3826
+
1717
3827
  // src/import-resolver.ts
1718
- import { readFileSync as readFileSync3, existsSync as existsSync4, statSync } from "fs";
1719
- import { resolve as resolve5, dirname as dirname3, join } from "path";
3828
+ init_config();
3829
+ import { readFileSync as readFileSync4, existsSync as existsSync7, statSync as statSync2 } from "fs";
3830
+ import { resolve as resolve6, dirname as dirname6, join as join4 } from "path";
1720
3831
 
1721
3832
  // src/security-utils.ts
1722
- import { resolve as resolve4, normalize } from "path";
3833
+ import { resolve as resolve5, normalize } from "path";
1723
3834
  function ensureWithinRoot(filePath, projectRoot) {
1724
- const resolvedRoot = resolve4(projectRoot);
1725
- const resolvedPath = resolve4(resolvedRoot, filePath);
3835
+ const resolvedRoot = resolve5(projectRoot);
3836
+ const resolvedPath = resolve5(resolvedRoot, filePath);
1726
3837
  const normalizedPath = normalize(resolvedPath);
1727
3838
  const normalizedRoot = normalize(resolvedRoot);
1728
3839
  if (!normalizedPath.startsWith(normalizedRoot + "/") && normalizedPath !== normalizedRoot) {
@@ -1764,6 +3875,9 @@ function enforceSeverityFloors(configWeights, defaults) {
1764
3875
  return result;
1765
3876
  }
1766
3877
 
3878
+ // src/lib/sql-table-names.ts
3879
+ init_config();
3880
+
1767
3881
  // src/import-resolver.ts
1768
3882
  function resolveImportPath(specifier, fromFile) {
1769
3883
  if (!specifier.startsWith(".") && !specifier.startsWith("@/")) {
@@ -1772,23 +3886,23 @@ function resolveImportPath(specifier, fromFile) {
1772
3886
  let basePath;
1773
3887
  if (specifier.startsWith("@/")) {
1774
3888
  const paths = getResolvedPaths();
1775
- basePath = resolve5(paths.pathAlias["@"] ?? paths.srcDir, specifier.slice(2));
3889
+ basePath = resolve6(paths.pathAlias["@"] ?? paths.srcDir, specifier.slice(2));
1776
3890
  } else {
1777
- basePath = resolve5(dirname3(fromFile), specifier);
3891
+ basePath = resolve6(dirname6(fromFile), specifier);
1778
3892
  }
1779
- if (existsSync4(basePath) && !isDirectory(basePath)) {
3893
+ if (existsSync7(basePath) && !isDirectory(basePath)) {
1780
3894
  return toRelative(basePath);
1781
3895
  }
1782
3896
  const resolvedPaths = getResolvedPaths();
1783
3897
  for (const ext of resolvedPaths.extensions) {
1784
3898
  const withExt = basePath + ext;
1785
- if (existsSync4(withExt)) {
3899
+ if (existsSync7(withExt)) {
1786
3900
  return toRelative(withExt);
1787
3901
  }
1788
3902
  }
1789
3903
  for (const indexFile of resolvedPaths.indexFiles) {
1790
- const indexPath = join(basePath, indexFile);
1791
- if (existsSync4(indexPath)) {
3904
+ const indexPath = join4(basePath, indexFile);
3905
+ if (existsSync7(indexPath)) {
1792
3906
  return toRelative(indexPath);
1793
3907
  }
1794
3908
  }
@@ -1796,7 +3910,7 @@ function resolveImportPath(specifier, fromFile) {
1796
3910
  }
1797
3911
  function isDirectory(path) {
1798
3912
  try {
1799
- return statSync(path).isDirectory();
3913
+ return statSync2(path).isDirectory();
1800
3914
  } catch {
1801
3915
  return false;
1802
3916
  }
@@ -1810,7 +3924,7 @@ function toRelative(absPath) {
1810
3924
  }
1811
3925
 
1812
3926
  // src/validation-engine.ts
1813
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
3927
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
1814
3928
  function getValidationChecks() {
1815
3929
  return getConfig().governance?.validation?.checks ?? {
1816
3930
  rule_compliance: true,
@@ -1838,7 +3952,7 @@ function validateFile(filePath, projectRoot) {
1838
3952
  });
1839
3953
  return checks;
1840
3954
  }
1841
- if (!existsSync5(absPath)) {
3955
+ if (!existsSync8(absPath)) {
1842
3956
  checks.push({
1843
3957
  name: "file_exists",
1844
3958
  severity: "error",
@@ -1847,7 +3961,7 @@ function validateFile(filePath, projectRoot) {
1847
3961
  });
1848
3962
  return checks;
1849
3963
  }
1850
- const source = readFileSync4(absPath, "utf-8");
3964
+ const source = readFileSync5(absPath, "utf-8");
1851
3965
  const lines = source.split("\n");
1852
3966
  if (activeChecks.rule_compliance !== false) {
1853
3967
  for (const ruleSet of config.rules) {
@@ -1945,7 +4059,8 @@ function storeValidationResult(db, filePath, checks, sessionId, validationType =
1945
4059
  }
1946
4060
 
1947
4061
  // src/security-scorer.ts
1948
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
4062
+ init_config();
4063
+ import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
1949
4064
  var DEFAULT_SECURITY_PATTERNS = [
1950
4065
  {
1951
4066
  regex: /\bexec\s*\(\s*[`"'].*\$\{/,
@@ -2036,12 +4151,12 @@ function scoreFileSecurity(filePath, projectRoot) {
2036
4151
  }]
2037
4152
  };
2038
4153
  }
2039
- if (!existsSync6(absPath)) {
4154
+ if (!existsSync9(absPath)) {
2040
4155
  return { riskScore: 0, findings: [] };
2041
4156
  }
2042
4157
  let source;
2043
4158
  try {
2044
- source = readFileSync5(absPath, "utf-8");
4159
+ source = readFileSync6(absPath, "utf-8");
2045
4160
  } catch {
2046
4161
  return { riskScore: 0, findings: [] };
2047
4162
  }
@@ -2080,8 +4195,9 @@ function storeSecurityScore(db, sessionId, filePath, riskScore, findings) {
2080
4195
  }
2081
4196
 
2082
4197
  // src/hooks/post-tool-use.ts
2083
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync3, statSync as statSync2 } from "fs";
2084
- import { join as join2 } from "path";
4198
+ init_config();
4199
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync2, existsSync as existsSync11, mkdirSync as mkdirSync5, statSync as statSync3 } from "fs";
4200
+ import { join as join5, basename as pathBasename2, dirname as pathDirname, resolve as pathResolve } from "path";
2085
4201
 
2086
4202
  // src/lib/recurrence-incrementer.ts
2087
4203
  var ANSI = /\x1b\[[0-9;]*m/g;
@@ -2141,44 +4257,185 @@ function incrementRecurrenceCountsForScannerFailures(db, sessionId, scannerStdou
2141
4257
  return updates;
2142
4258
  }
2143
4259
 
4260
+ // src/hooks/post-tool-use.ts
4261
+ init_hook_failure_signal();
4262
+
2144
4263
  // src/memory-file-ingest.ts
2145
- import { readFileSync as readFileSync6, existsSync as existsSync7, readdirSync } from "fs";
4264
+ import { readFileSync as readFileSync7, existsSync as existsSync10, readdirSync as readdirSync2 } from "fs";
4265
+ init_memory_db();
2146
4266
  import { parse as parseYaml2 } from "yaml";
4267
+ import { basename as pathBasename } from "path";
4268
+ import { createHash } from "crypto";
4269
+ function stripMdExtension(filePathOrName) {
4270
+ return pathBasename(filePathOrName).replace(/\.md$/, "");
4271
+ }
4272
+ function parseFrontmatterLoosely(raw) {
4273
+ const out = {};
4274
+ const metadata = {};
4275
+ const WANTED = /* @__PURE__ */ new Set(["name", "description", "type", "confidence"]);
4276
+ let inMetadata = false;
4277
+ for (const line of raw.split("\n")) {
4278
+ if (/^metadata:\s*$/.test(line)) {
4279
+ inMetadata = true;
4280
+ continue;
4281
+ }
4282
+ if (/^[A-Za-z_][\w-]*:/.test(line)) inMetadata = false;
4283
+ const m = line.match(/^(\s*)([A-Za-z_][\w-]*):\s*(.*)$/);
4284
+ if (!m) continue;
4285
+ const [, indent, key, rawValue] = m;
4286
+ if (!WANTED.has(key)) continue;
4287
+ let value = rawValue.trim();
4288
+ if (value.startsWith('"') && value.endsWith('"') && value.length > 1 || value.startsWith("'") && value.endsWith("'") && value.length > 1) {
4289
+ value = value.slice(1, -1);
4290
+ }
4291
+ if (!value) continue;
4292
+ if (indent.length > 0 && inMetadata) metadata[key] = value;
4293
+ else if (indent.length === 0) out[key] = value;
4294
+ }
4295
+ if (Object.keys(metadata).length > 0) out.metadata = metadata;
4296
+ return out;
4297
+ }
4298
+ function readMemoryFileFrontmatter(content) {
4299
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
4300
+ if (!match) return void 0;
4301
+ try {
4302
+ return parseYaml2(match[1]) ?? void 0;
4303
+ } catch {
4304
+ return parseFrontmatterLoosely(match[1]);
4305
+ }
4306
+ }
4307
+ function readMemoryKey(fm, key) {
4308
+ const metadata = fm.metadata;
4309
+ if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
4310
+ const nested = metadata[key];
4311
+ if (nested != null) return String(nested);
4312
+ }
4313
+ const top = fm[key];
4314
+ if (top != null) return String(top);
4315
+ return void 0;
4316
+ }
4317
+ var INGEST_SCHEMA_VERSION = 1;
4318
+ var MAX_MEMORY_FILE_BYTES = 1e6;
4319
+ function auditMemoryFileEvent(db, sessionId, eventType, detail) {
4320
+ if (!sessionId) return;
4321
+ try {
4322
+ db.prepare(
4323
+ `INSERT INTO audit_log (session_id, event_type, actor, evidence)
4324
+ VALUES (?, ?, 'hook', ?)`
4325
+ ).run(sessionId, eventType, detail);
4326
+ } catch {
4327
+ }
4328
+ }
4329
+ function upsertMemoryFileMirror(db, f) {
4330
+ const now = Math.floor(Date.now() / 1e3);
4331
+ const existing = db.prepare(
4332
+ `SELECT id, content_hash, ingest_schema_version FROM memory_files WHERE rel_path = ?`
4333
+ ).get(f.relPath);
4334
+ if (existing && existing.content_hash === f.contentHash && existing.ingest_schema_version === INGEST_SCHEMA_VERSION) {
4335
+ return "unchanged";
4336
+ }
4337
+ if (existing) {
4338
+ db.prepare(
4339
+ `UPDATE memory_files
4340
+ SET name = ?, raw = ?, frontmatter_json = ?, body = ?, content_hash = ?,
4341
+ ingest_schema_version = ?, synced_at_epoch = ?,
4342
+ expired_at_epoch = NULL
4343
+ WHERE id = ?`
4344
+ ).run(
4345
+ f.name,
4346
+ f.raw,
4347
+ f.frontmatterJson,
4348
+ f.body,
4349
+ f.contentHash,
4350
+ INGEST_SCHEMA_VERSION,
4351
+ now,
4352
+ existing.id
4353
+ );
4354
+ return "updated";
4355
+ }
4356
+ db.prepare(
4357
+ `INSERT INTO memory_files
4358
+ (rel_path, name, raw, frontmatter_json, body, content_hash, ingest_schema_version, synced_at_epoch)
4359
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
4360
+ ).run(
4361
+ f.relPath,
4362
+ f.name,
4363
+ f.raw,
4364
+ f.frontmatterJson,
4365
+ f.body,
4366
+ f.contentHash,
4367
+ INGEST_SCHEMA_VERSION,
4368
+ now
4369
+ );
4370
+ return "inserted";
4371
+ }
2147
4372
  function ingestMemoryFile(db, sessionId, filePath) {
2148
- if (!existsSync7(filePath)) return "skipped";
2149
- const content = readFileSync6(filePath, "utf-8");
2150
- const basename2 = (filePath.split("/").pop() ?? "").replace(".md", "");
2151
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
2152
- let name = basename2;
4373
+ if (!existsSync10(filePath)) return "skipped";
4374
+ const content = readFileSync7(filePath, "utf-8");
4375
+ if (Buffer.byteLength(content, "utf-8") > MAX_MEMORY_FILE_BYTES) {
4376
+ process.stderr.write(
4377
+ `[massu] memory file exceeds ${MAX_MEMORY_FILE_BYTES} bytes, not ingested: ${filePath}
4378
+ `
4379
+ );
4380
+ return "skipped";
4381
+ }
4382
+ const basename3 = stripMdExtension(filePath);
4383
+ const fm = readMemoryFileFrontmatter(content);
4384
+ let name = basename3;
2153
4385
  let description = "";
2154
4386
  let type = "discovery";
2155
4387
  let confidence;
2156
- if (frontmatterMatch) {
2157
- try {
2158
- const fm = parseYaml2(frontmatterMatch[1]);
2159
- name = fm.name ?? basename2;
2160
- description = fm.description ?? "";
2161
- type = fm.type ?? "discovery";
2162
- confidence = fm.confidence != null ? Number(fm.confidence) : void 0;
2163
- } catch {
2164
- }
4388
+ if (fm) {
4389
+ name = fm.name ?? basename3;
4390
+ description = fm.description ?? "";
4391
+ type = readMemoryKey(fm, "type") ?? "discovery";
4392
+ const rawConfidence = readMemoryKey(fm, "confidence");
4393
+ confidence = rawConfidence != null ? Number(rawConfidence) : void 0;
2165
4394
  }
2166
4395
  const obsType = mapMemoryTypeToObservationType(type);
2167
4396
  const importance = confidence != null ? Math.max(1, Math.min(5, Math.round(confidence * 4 + 1))) : 4;
2168
4397
  const bodyMatch = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)/);
2169
- const body = bodyMatch ? bodyMatch[1].trim().slice(0, 500) : "";
2170
- const title = `[memory-file] ${name}`;
4398
+ const body = bodyMatch ? bodyMatch[1] : "";
4399
+ const title = `${MEMORY_FILE_TITLE_PREFIX}${name}`;
2171
4400
  const detail = description ? `${description}
2172
4401
 
2173
4402
  ${body}` : body;
4403
+ const relPath = pathBasename(filePath);
4404
+ const contentHash = createHash("sha256").update(content, "utf-8").digest("hex");
4405
+ const mirror = upsertMemoryFileMirror(db, {
4406
+ relPath,
4407
+ name,
4408
+ raw: content,
4409
+ frontmatterJson: fm ? JSON.stringify(fm) : null,
4410
+ body,
4411
+ contentHash
4412
+ });
2174
4413
  const existing = db.prepare(
2175
- "SELECT id FROM observations WHERE title = ? LIMIT 1"
4414
+ "SELECT id, type FROM observations WHERE title = ? LIMIT 1"
2176
4415
  ).get(title);
4416
+ const projectionCurrent = existing !== void 0 && existing.type === obsType;
4417
+ if (mirror === "unchanged" && projectionCurrent) {
4418
+ db.prepare(
4419
+ `UPDATE observations
4420
+ SET expired_at = NULL, expired_at_epoch = NULL,
4421
+ valid_to = NULL, valid_to_epoch = NULL
4422
+ WHERE title = ? AND expired_at IS NOT NULL`
4423
+ ).run(title);
4424
+ return "skipped";
4425
+ }
2177
4426
  if (existing) {
2178
- db.prepare("UPDATE observations SET detail = ?, importance = ? WHERE id = ?").run(detail, importance, existing.id);
4427
+ db.prepare(
4428
+ `UPDATE observations
4429
+ SET type = ?, detail = ?, importance = ?,
4430
+ expired_at = NULL, expired_at_epoch = NULL,
4431
+ valid_to = NULL, valid_to_epoch = NULL
4432
+ WHERE id = ?`
4433
+ ).run(obsType, detail, importance, existing.id);
4434
+ auditMemoryFileEvent(db, sessionId, "memory_file_ingested", relPath);
2179
4435
  return "updated";
2180
4436
  } else {
2181
4437
  addObservation(db, sessionId, obsType, title, detail, { importance });
4438
+ auditMemoryFileEvent(db, sessionId, "memory_file_ingested", relPath);
2182
4439
  return "inserted";
2183
4440
  }
2184
4441
  }
@@ -2210,7 +4467,10 @@ async function main() {
2210
4467
  try {
2211
4468
  const input = await readStdin();
2212
4469
  const hookInput = JSON.parse(input);
2213
- const { session_id, tool_name, tool_input, tool_response } = hookInput;
4470
+ const { session_id, tool_name, tool_input } = hookInput;
4471
+ const { text: tool_response, isError: toolErrored } = normalizeToolResponse(
4472
+ hookInput.tool_response
4473
+ );
2214
4474
  if (currentSessionId !== session_id) {
2215
4475
  seenReads.clear();
2216
4476
  currentSessionId = session_id;
@@ -2228,6 +4488,27 @@ async function main() {
2228
4488
  } else {
2229
4489
  addObservation(db, session_id, observation.type, observation.title, observation.detail, observation.opts);
2230
4490
  }
4491
+ if (observation.type === "decision") {
4492
+ try {
4493
+ const decisionText = observation.detail ?? observation.title;
4494
+ const alternatives = extractAlternatives(decisionText);
4495
+ const existing = db.prepare(
4496
+ `SELECT id FROM architecture_decisions WHERE session_id = ? AND title = ? LIMIT 1`
4497
+ ).get(session_id, observation.title);
4498
+ if (!existing) {
4499
+ storeDecision(db, {
4500
+ title: observation.title,
4501
+ context: "",
4502
+ decision: decisionText,
4503
+ alternatives,
4504
+ consequences: "",
4505
+ sessionId: session_id,
4506
+ affectedFiles: observation.opts?.filesInvolved
4507
+ });
4508
+ }
4509
+ } catch {
4510
+ }
4511
+ }
2231
4512
  if (tool_response) {
2232
4513
  const progress = detectPlanProgress(tool_response);
2233
4514
  if (progress.length > 0) {
@@ -2323,9 +4604,14 @@ async function main() {
2323
4604
  try {
2324
4605
  if (tool_name === "Edit" || tool_name === "Write") {
2325
4606
  const filePath = tool_input.file_path ?? "";
2326
- if (filePath && filePath.includes("/memory/") && filePath.endsWith(".md")) {
2327
- const basename2 = filePath.split("/").pop() ?? "";
2328
- if (basename2 !== "MEMORY.md") {
4607
+ if (filePath && filePath.endsWith(".md") && pathBasename2(filePath) !== "MEMORY.md") {
4608
+ let memoryDir = "";
4609
+ try {
4610
+ memoryDir = getResolvedPaths().memoryDir;
4611
+ } catch {
4612
+ memoryDir = "";
4613
+ }
4614
+ if (memoryDir && pathResolve(pathDirname(filePath)) === pathResolve(memoryDir)) {
2329
4615
  ingestMemoryFile(db, session_id, filePath);
2330
4616
  }
2331
4617
  }
@@ -2359,10 +4645,10 @@ async function main() {
2359
4645
  } catch (err) {
2360
4646
  try {
2361
4647
  const projectRoot = process.cwd();
2362
- const dir = join2(projectRoot, ".massu", "rule-candidates");
2363
- if (!existsSync8(dir)) mkdirSync3(dir, { recursive: true });
2364
- const logPath = join2(dir, ".cr53-increment-failures.jsonl");
2365
- const pre = existsSync8(logPath) ? readFileSync7(logPath, "utf-8") : "";
4648
+ const dir = join5(projectRoot, ".massu", "rule-candidates");
4649
+ if (!existsSync11(dir)) mkdirSync5(dir, { recursive: true });
4650
+ const logPath = join5(dir, ".cr53-increment-failures.jsonl");
4651
+ const pre = existsSync11(logPath) ? readFileSync8(logPath, "utf-8") : "";
2366
4652
  const sep = pre && !pre.endsWith("\n") ? "\n" : "";
2367
4653
  writeFileSync2(logPath, pre + sep + JSON.stringify({
2368
4654
  session_id,
@@ -2376,7 +4662,8 @@ async function main() {
2376
4662
  } finally {
2377
4663
  db.close();
2378
4664
  }
2379
- } catch (_e) {
4665
+ } catch (err) {
4666
+ recordHookFailure("post-tool-use", err);
2380
4667
  }
2381
4668
  process.exit(0);
2382
4669
  }
@@ -2446,14 +4733,14 @@ function parseTestRunOutput(output) {
2446
4733
  return null;
2447
4734
  }
2448
4735
  function readStdin() {
2449
- return new Promise((resolve6) => {
4736
+ return new Promise((resolve7) => {
2450
4737
  let data = "";
2451
4738
  process.stdin.setEncoding("utf-8");
2452
4739
  process.stdin.on("data", (chunk) => {
2453
4740
  data += chunk;
2454
4741
  });
2455
- process.stdin.on("end", () => resolve6(data));
2456
- setTimeout(() => resolve6(data), 3e3);
4742
+ process.stdin.on("end", () => resolve7(data));
4743
+ setTimeout(() => resolve7(data), 3e3);
2457
4744
  });
2458
4745
  }
2459
4746
  var _cachedConventions = null;
@@ -2466,13 +4753,13 @@ var _conventionDefaults = {
2466
4753
  function readConventions(cwd) {
2467
4754
  try {
2468
4755
  const projectRoot = cwd ?? process.cwd();
2469
- const configPath = join2(projectRoot, "massu.config.yaml");
2470
- if (!existsSync8(configPath)) return _conventionDefaults;
2471
- const mtimeMs = statSync2(configPath).mtimeMs;
4756
+ const configPath = join5(projectRoot, "massu.config.yaml");
4757
+ if (!existsSync11(configPath)) return _conventionDefaults;
4758
+ const mtimeMs = statSync3(configPath).mtimeMs;
2472
4759
  if (_cachedConventions !== null && _cachedConventionsPath === configPath && _cachedConventionsMtimeMs === mtimeMs) {
2473
4760
  return _cachedConventions;
2474
4761
  }
2475
- const content = readFileSync7(configPath, "utf-8");
4762
+ const content = readFileSync8(configPath, "utf-8");
2476
4763
  const parsed = parseYaml3(content);
2477
4764
  if (!parsed || typeof parsed !== "object") return _conventionDefaults;
2478
4765
  const conventions = parsed.conventions;
@@ -2490,7 +4777,7 @@ function readConventions(cwd) {
2490
4777
  }
2491
4778
  }
2492
4779
  function isKnowledgeSourceFile(filePath) {
2493
- const basename2 = filePath.split("/").pop() ?? "";
4780
+ const basename3 = filePath.split("/").pop() ?? "";
2494
4781
  const conventions = readConventions();
2495
4782
  const knowledgeSourcePatterns = [
2496
4783
  ...conventions.knowledgeSourceFiles,
@@ -2499,16 +4786,16 @@ function isKnowledgeSourceFile(filePath) {
2499
4786
  "knowledge-indexer.ts",
2500
4787
  "knowledge-tools.ts"
2501
4788
  ];
2502
- return knowledgeSourcePatterns.some((p) => basename2 === p) || filePath.includes("/memory/") || filePath.includes(conventions.claudeDirName + "/");
4789
+ return knowledgeSourcePatterns.some((p) => basename3 === p) || filePath.includes("/memory/") || filePath.includes(conventions.claudeDirName + "/");
2503
4790
  }
2504
4791
  function checkMemoryFileIntegrity(filePath) {
2505
4792
  const issues = [];
2506
4793
  try {
2507
- if (!existsSync8(filePath)) {
4794
+ if (!existsSync11(filePath)) {
2508
4795
  issues.push("MEMORY.md file does not exist after write");
2509
4796
  return issues;
2510
4797
  }
2511
- const content = readFileSync7(filePath, "utf-8");
4798
+ const content = readFileSync8(filePath, "utf-8");
2512
4799
  const lines = content.split("\n");
2513
4800
  const MAX_LINES = 200;
2514
4801
  if (lines.length > MAX_LINES) {