@meetless/mla 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/cli.js +31 -5
  3. package/dist/commands/activate.js +39 -18
  4. package/dist/commands/agent-memory.js +333 -0
  5. package/dist/commands/enrich.js +211 -2
  6. package/dist/commands/internal-auto-index.js +64 -1
  7. package/dist/commands/internal-pretool-observe.js +86 -1
  8. package/dist/commands/internal-redact-capture.js +130 -0
  9. package/dist/commands/pilot.js +385 -0
  10. package/dist/lib/agent-memory-capture/binding.js +115 -0
  11. package/dist/lib/agent-memory-capture/classify.js +68 -0
  12. package/dist/lib/agent-memory-capture/collector.js +69 -0
  13. package/dist/lib/agent-memory-capture/containment.js +74 -0
  14. package/dist/lib/agent-memory-capture/ledger.js +43 -0
  15. package/dist/lib/agent-memory-capture/live-collector.js +148 -0
  16. package/dist/lib/agent-memory-capture/live-ledger.js +45 -0
  17. package/dist/lib/agent-memory-capture/live-pipeline.js +344 -0
  18. package/dist/lib/agent-memory-capture/lock.js +98 -0
  19. package/dist/lib/agent-memory-capture/paths.js +47 -0
  20. package/dist/lib/agent-memory-capture/pipeline.js +222 -0
  21. package/dist/lib/agent-memory-capture/report.js +131 -0
  22. package/dist/lib/agent-memory-capture/types.js +14 -0
  23. package/dist/lib/agent-memory-capture/upsert-client.js +104 -0
  24. package/dist/lib/analytics/enforcement-classify.js +65 -0
  25. package/dist/lib/analytics/enforcement-incident.js +83 -0
  26. package/dist/lib/analytics/envelope.js +55 -1
  27. package/dist/lib/analytics/pilot.js +313 -0
  28. package/dist/lib/enrichment/ingest.js +98 -13
  29. package/dist/lib/enrichment/materialize-rules.js +81 -0
  30. package/dist/lib/enrichment/plan.js +72 -15
  31. package/dist/lib/enrichment/protocol.js +85 -5
  32. package/dist/lib/enrichment/scout-brief.js +35 -6
  33. package/dist/lib/redactor.js +104 -1
  34. package/dist/lib/scanner/agent-memory.js +55 -4
  35. package/dist/lib/scanner/managed-rules.js +0 -0
  36. package/dist/lib/scanner/scan.js +52 -1
  37. package/dist/lib/scanner/score.js +41 -3
  38. package/dist/lib/scanner/scout-mission.js +9 -7
  39. package/dist/lib/upgrade-apply.js +30 -0
  40. package/dist/lib/wire.js +2 -0
  41. package/package.json +3 -3
@@ -0,0 +1,385 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderReport = renderReport;
4
+ exports.runPilot = runPilot;
5
+ // `mla pilot` -- the pilot value-signal surface (memo
6
+ // notes/20260624-mla-new-user-value-and-brownfield-proof.md §6 Phase 3).
7
+ //
8
+ // mla pilot feedback --point <p> --verdict <v> [--prevented-mistake] [--sampled] [--json]
9
+ // Record ONE explicit human verdict at a natural point. This is the pilot's
10
+ // primary value signal (the four hypotheses are scored from these), captured
11
+ // only at decision moments -- a reviewed candidate, a SAMPLED injection, a
12
+ // fired alert, a blocked tool call -- never on every injection (memo lines
13
+ // 551-554). Appended to the same local events.jsonl as every other signal.
14
+ //
15
+ // mla pilot report [--window <Nd>] [--json]
16
+ // The "simple report" half of the memo's "store raw events and generate a
17
+ // simple report/export; do NOT build a dashboard first" (line 553). Folds the
18
+ // local events into the four hypotheses. No server, no dashboard.
19
+ //
20
+ // mla pilot export [--window <Nd>] [--out <path>]
21
+ // The "export" half: dump the raw pilot-feedback events (the genuinely-new
22
+ // signal) as JSONL for offline analysis or to hand to An. The full raw stream
23
+ // is always ~/.meetless/events.jsonl; this is the focused pilot subset.
24
+ //
25
+ // mla pilot sample-check --inject-id <id> [--n <N>]
26
+ // Consult the deterministic 1-in-N sampling gate (the honest mechanism behind
27
+ // "sampled occasionally"). A predicate command: exit 0 = sample this injection,
28
+ // exit 1 = skip it, exit 2 = usage error. A hook gates a feedback prompt on it.
29
+ //
30
+ // Every payload is enums/ids/booleans only (INV-POSTHOG-PII-1); no statement text,
31
+ // path, or query is ever recorded. The command is fail-soft on the record path: a
32
+ // pilot verdict must never break because analytics had a disk hiccup.
33
+ const node_fs_1 = require("node:fs");
34
+ const pilot_1 = require("../lib/analytics/pilot");
35
+ const envelope_1 = require("../lib/analytics/envelope");
36
+ const recorder_1 = require("../lib/analytics/recorder");
37
+ const store_1 = require("../lib/analytics/store");
38
+ const workspace_1 = require("../lib/workspace");
39
+ const DEFAULT_WINDOW_DAYS = 30;
40
+ // --- shared parsing ---------------------------------------------------------
41
+ // Parse `--window 7d` / `--window 30` (bare integer = days). Same grammar as
42
+ // `mla stats` so the two value surfaces read identically.
43
+ function parseWindowDays(raw) {
44
+ if (raw === undefined)
45
+ throw new Error("--window requires a value (e.g. 7d, 30d, 30)");
46
+ const m = /^(\d+)(d)?$/.exec(raw.trim());
47
+ if (!m)
48
+ throw new Error(`--window must be a positive number of days (e.g. 7d, 30d). Got: ${raw}`);
49
+ const days = Number(m[1]);
50
+ if (!Number.isInteger(days) || days <= 0) {
51
+ throw new Error(`--window must be a positive number of days. Got: ${raw}`);
52
+ }
53
+ return days;
54
+ }
55
+ function parseFeedbackArgs(argv) {
56
+ let point;
57
+ let verdict;
58
+ let preventedMistake = false;
59
+ let sampled = false;
60
+ let json = false;
61
+ for (let i = 0; i < argv.length; i++) {
62
+ const a = argv[i];
63
+ if (a === "--point")
64
+ point = argv[++i];
65
+ else if (a === "--verdict")
66
+ verdict = argv[++i];
67
+ else if (a === "--prevented-mistake")
68
+ preventedMistake = true;
69
+ else if (a === "--sampled")
70
+ sampled = true;
71
+ else if (a === "--json")
72
+ json = true;
73
+ else
74
+ throw new Error(`Unknown flag for \`mla pilot feedback\`: ${a}`);
75
+ }
76
+ if (!point)
77
+ throw new Error("--point is required (one of: " + envelope_1.PILOT_FEEDBACK_POINTS.join(", ") + ")");
78
+ if (!verdict)
79
+ throw new Error("--verdict is required (one of: " + envelope_1.PILOT_FEEDBACK_VERDICTS.join(", ") + ")");
80
+ // The point/verdict pairing + the prevented_mistake constraint are enforced by
81
+ // validatePilotFeedback (called inside buildPilotFeedbackEvent); here we only
82
+ // confirm the values are in the closed enums so the cast is sound.
83
+ if (!envelope_1.PILOT_FEEDBACK_POINTS.includes(point)) {
84
+ throw new Error(`unknown --point "${point}" (expected one of: ${envelope_1.PILOT_FEEDBACK_POINTS.join(", ")})`);
85
+ }
86
+ if (!envelope_1.PILOT_FEEDBACK_VERDICTS.includes(verdict)) {
87
+ throw new Error(`unknown --verdict "${verdict}" (expected one of: ${envelope_1.PILOT_FEEDBACK_VERDICTS.join(", ")})`);
88
+ }
89
+ return {
90
+ point: point,
91
+ verdict: verdict,
92
+ preventedMistake,
93
+ sampled,
94
+ json,
95
+ };
96
+ }
97
+ function runFeedback(argv, deps) {
98
+ let args;
99
+ try {
100
+ args = parseFeedbackArgs(argv);
101
+ }
102
+ catch (e) {
103
+ console.error(e.message);
104
+ return 2;
105
+ }
106
+ const input = {
107
+ point: args.point,
108
+ verdict: args.verdict,
109
+ preventedMistake: args.preventedMistake,
110
+ sampled: args.sampled,
111
+ };
112
+ // Build first: this validates the point/verdict pairing and the prevented_mistake
113
+ // tag. A bad combination is a usage error (exit 2), refused BEFORE anything is
114
+ // written -- not a recorded malformed event.
115
+ let recordInput;
116
+ try {
117
+ recordInput = (0, pilot_1.buildPilotFeedbackEvent)(input);
118
+ }
119
+ catch (e) {
120
+ console.error(e.message);
121
+ return 2;
122
+ }
123
+ const env = deps.env ?? process.env;
124
+ const record = deps.record ?? recorder_1.recordAnalyticsEvent;
125
+ const resolveWs = deps.resolveWorkspaceId ?? workspace_1.tryResolveWorkspaceId;
126
+ const nowMs = deps.nowMs ?? Date.now();
127
+ const ctx = {
128
+ workspaceId: resolveWs(),
129
+ sessionId: (env.CLAUDE_CODE_SESSION_ID || "").trim() || null,
130
+ now: new Date(nowMs).toISOString(),
131
+ };
132
+ let ev;
133
+ try {
134
+ ev = record(ctx, recordInput, env);
135
+ }
136
+ catch {
137
+ // Fail-soft: a verdict must never be blocked by an analytics I/O fault. We
138
+ // still report it as recorded-intent so the operator is not left guessing.
139
+ if (args.json) {
140
+ console.log(JSON.stringify({ recorded: false, point: args.point, verdict: args.verdict }, null, 2));
141
+ }
142
+ else {
143
+ console.error("warning: pilot feedback could not be persisted (analytics I/O error); continuing.");
144
+ }
145
+ return 0;
146
+ }
147
+ const feedbackId = ev.feedback_id ?? null;
148
+ if (args.json) {
149
+ console.log(JSON.stringify({
150
+ recorded: true,
151
+ feedback_id: feedbackId,
152
+ point: args.point,
153
+ verdict: args.verdict,
154
+ prevented_mistake: args.preventedMistake,
155
+ sampled: args.sampled,
156
+ }, null, 2));
157
+ }
158
+ else {
159
+ const tag = args.preventedMistake ? " (prevented a mistake)" : "";
160
+ console.log(`Recorded pilot feedback: ${args.point} -> ${args.verdict}${tag}.`);
161
+ }
162
+ return 0;
163
+ }
164
+ function parseReportArgs(argv) {
165
+ const out = { windowDays: DEFAULT_WINDOW_DAYS, json: false };
166
+ for (let i = 0; i < argv.length; i++) {
167
+ const a = argv[i];
168
+ if (a === "--json")
169
+ out.json = true;
170
+ else if (a === "--window")
171
+ out.windowDays = parseWindowDays(argv[++i]);
172
+ else
173
+ throw new Error(`Unknown flag for \`mla pilot report\`: ${a}`);
174
+ }
175
+ return out;
176
+ }
177
+ function pct(r) {
178
+ return r === null ? "n/a" : (r * 100).toFixed(0) + "%";
179
+ }
180
+ function dur(ms) {
181
+ if (ms === null)
182
+ return "n/a";
183
+ if (ms < 1000)
184
+ return `${ms}ms`;
185
+ const s = Math.round(ms / 1000);
186
+ if (s < 90)
187
+ return `${s}s`;
188
+ const m = Math.round(s / 60);
189
+ if (m < 90)
190
+ return `${m}m`;
191
+ const h = Math.round(m / 60);
192
+ if (h < 48)
193
+ return `${h}h`;
194
+ return `${Math.round(h / 24)}d`;
195
+ }
196
+ function renderReport(r) {
197
+ const L = [];
198
+ L.push(`mla pilot report, last ${r.window_days}d (workspace-local):`);
199
+ L.push("");
200
+ // H1.
201
+ const o = r.onboarding;
202
+ L.push("1. Onboarding discovers useful knowledge");
203
+ L.push(` Candidates: ${o.candidate_accept} accepted, ${o.candidate_reject} rejected (acceptance ${pct(o.acceptance_rate)}).`);
204
+ L.push(` Review-flow corroboration: ${o.review_accept} accepted, ${o.review_reject} rejected.`);
205
+ L.push(` Time to first useful item: ${dur(o.time_to_first_useful_ms)}.`);
206
+ L.push("");
207
+ // H2.
208
+ const inj = r.injection;
209
+ L.push("2. Injected context is useful, not noisy");
210
+ L.push(` Verdicts: ${inj.useful} useful, ${inj.noise} noise, ${inj.uncertain} uncertain (usefulness ${pct(inj.usefulness_rate)}).`);
211
+ L.push(` Sampled ${inj.sampled_feedback_count} of ${inj.total_injects} injection(s) (${pct(inj.feedback_coverage_rate)} coverage -- we did NOT prompt on every injection).`);
212
+ L.push("");
213
+ // H3.
214
+ const co = r.coordination;
215
+ L.push("3. mla catches genuine coordination errors");
216
+ L.push(` Alerts: ${co.alert_confirmed} confirmed, ${co.alert_disposed} disposed, ${co.alert_uncertain} uncertain (confirmation ${pct(co.confirmation_rate)}).`);
217
+ L.push(` Prevented mistakes (tagged): ${co.prevented_mistakes}.`);
218
+ L.push(` Denies: ${co.deny_correct} correct, ${co.deny_incorrect} incorrect (annoyance ${pct(co.deny_annoyance_rate)}).`);
219
+ L.push(` Contradiction corroboration: ${co.contradictions_surfaced} surfaced, ${co.contradictions_acted_on} acted on.`);
220
+ L.push("");
221
+ // H4.
222
+ const ret = r.retention;
223
+ L.push("4. Value persists beyond novelty");
224
+ L.push(` First seen: ${ret.first_seen ?? "n/a"}. Last seen: ${ret.last_seen ?? "n/a"}.`);
225
+ L.push(` Active days: ${ret.active_days}. Used after 7d: ${ret.used_after_7d ? "yes" : "no"}. Used after 14d: ${ret.used_after_14d ? "yes" : "no"}.`);
226
+ return L.join("\n");
227
+ }
228
+ function runReport(argv, deps) {
229
+ let args;
230
+ try {
231
+ args = parseReportArgs(argv);
232
+ }
233
+ catch (e) {
234
+ console.error(e.message);
235
+ return 2;
236
+ }
237
+ const read = deps.read ?? store_1.readEvents;
238
+ const nowMs = deps.nowMs ?? Date.now();
239
+ const opts = { windowDays: args.windowDays, nowMs };
240
+ const report = (0, pilot_1.aggregatePilot)(read(deps.env), opts);
241
+ if (args.json) {
242
+ console.log(JSON.stringify(report, null, 2));
243
+ }
244
+ else {
245
+ console.log(renderReport(report));
246
+ }
247
+ return 0;
248
+ }
249
+ function parseExportArgs(argv) {
250
+ const out = { windowDays: DEFAULT_WINDOW_DAYS, out: null };
251
+ for (let i = 0; i < argv.length; i++) {
252
+ const a = argv[i];
253
+ if (a === "--out")
254
+ out.out = argv[++i] ?? null;
255
+ else if (a === "--window")
256
+ out.windowDays = parseWindowDays(argv[++i]);
257
+ else
258
+ throw new Error(`Unknown flag for \`mla pilot export\`: ${a}`);
259
+ }
260
+ if (out.out !== null && out.out.trim().length === 0) {
261
+ throw new Error("--out requires a file path");
262
+ }
263
+ return out;
264
+ }
265
+ function runExport(argv, deps) {
266
+ let args;
267
+ try {
268
+ args = parseExportArgs(argv);
269
+ }
270
+ catch (e) {
271
+ console.error(e.message);
272
+ return 2;
273
+ }
274
+ const read = deps.read ?? store_1.readEvents;
275
+ const nowMs = deps.nowMs ?? Date.now();
276
+ const startMs = nowMs - args.windowDays * 24 * 60 * 60 * 1000;
277
+ // Export the genuinely-new pilot-feedback signal, windowed. The full raw stream
278
+ // is always events.jsonl; this is the focused subset a recipient can recompute
279
+ // the report from without seeing every command/inject event.
280
+ const lines = [];
281
+ for (const ev of read(deps.env)) {
282
+ if (ev.event_type !== "mla_pilot_feedback")
283
+ continue;
284
+ const t = Date.parse(ev.created_at);
285
+ if (!Number.isFinite(t) || t < startMs)
286
+ continue;
287
+ lines.push(JSON.stringify(ev));
288
+ }
289
+ const payload = lines.length > 0 ? lines.join("\n") + "\n" : "";
290
+ if (args.out) {
291
+ const writeFile = deps.writeFile ?? ((p, d) => (0, node_fs_1.writeFileSync)(p, d, "utf8"));
292
+ try {
293
+ writeFile(args.out, payload);
294
+ }
295
+ catch (e) {
296
+ console.error(`mla pilot export could not write ${args.out}: ${e.message}`);
297
+ return 1;
298
+ }
299
+ console.log(`Exported ${lines.length} pilot feedback event(s) to ${args.out}.`);
300
+ }
301
+ else {
302
+ // Raw JSONL to stdout: no trailing message so it pipes cleanly.
303
+ if (payload)
304
+ process.stdout.write(payload);
305
+ }
306
+ return 0;
307
+ }
308
+ function parseSampleCheckArgs(argv, env) {
309
+ let injectId;
310
+ let n;
311
+ let json = false;
312
+ for (let i = 0; i < argv.length; i++) {
313
+ const a = argv[i];
314
+ if (a === "--inject-id")
315
+ injectId = argv[++i];
316
+ else if (a === "--n") {
317
+ const raw = argv[++i];
318
+ const parsed = Number(raw);
319
+ if (!Number.isFinite(parsed) || parsed < 0 || !Number.isInteger(parsed)) {
320
+ throw new Error(`--n must be a non-negative integer. Got: ${raw}`);
321
+ }
322
+ n = parsed;
323
+ }
324
+ else if (a === "--json")
325
+ json = true;
326
+ else
327
+ throw new Error(`Unknown flag for \`mla pilot sample-check\`: ${a}`);
328
+ }
329
+ if (!injectId)
330
+ throw new Error("--inject-id is required");
331
+ return { injectId, n: n ?? (0, pilot_1.injectionSampleN)(env), json };
332
+ }
333
+ function runSampleCheck(argv, deps) {
334
+ const env = deps.env ?? process.env;
335
+ let args;
336
+ try {
337
+ args = parseSampleCheckArgs(argv, env);
338
+ }
339
+ catch (e) {
340
+ console.error(e.message);
341
+ return 2;
342
+ }
343
+ const sample = (0, pilot_1.shouldSampleInjection)(args.injectId, args.n);
344
+ if (args.json) {
345
+ console.log(JSON.stringify({ inject_id: args.injectId, n: args.n, sample }, null, 2));
346
+ }
347
+ else {
348
+ console.log(sample ? "sample" : "skip");
349
+ }
350
+ // Predicate exit contract (documented in help): 0 = sample, 1 = skip. A hook
351
+ // branches on this to decide whether to surface a feedback prompt for the inject.
352
+ return sample ? 0 : 1;
353
+ }
354
+ // --- entry point ------------------------------------------------------------
355
+ const PILOT_USAGE = "Usage:\n" +
356
+ " mla pilot feedback --point <candidate|injection|alert|deny> --verdict <v> [--prevented-mistake] [--sampled] [--json]\n" +
357
+ " mla pilot report [--window <Nd>] [--json]\n" +
358
+ " mla pilot export [--window <Nd>] [--out <path>]\n" +
359
+ " mla pilot sample-check --inject-id <id> [--n <N>] [--json] (exit 0 = sample, 1 = skip)\n" +
360
+ "\n" +
361
+ "Verdicts by point: candidate -> accept|reject; injection -> useful|noise|uncertain;\n" +
362
+ "alert -> confirmed|disposed|uncertain; deny -> correct|incorrect.\n" +
363
+ `Injection sampling default: 1 in ${pilot_1.DEFAULT_INJECTION_SAMPLE_N} (override with MEETLESS_PILOT_INJECTION_SAMPLE_N or --n).`;
364
+ async function runPilot(argv, deps = {}) {
365
+ const [sub, ...rest] = argv;
366
+ switch (sub) {
367
+ case "feedback":
368
+ return runFeedback(rest, deps);
369
+ case "report":
370
+ return runReport(rest, deps);
371
+ case "export":
372
+ return runExport(rest, deps);
373
+ case "sample-check":
374
+ return runSampleCheck(rest, deps);
375
+ case undefined:
376
+ case "help":
377
+ case "--help":
378
+ case "-h":
379
+ console.log(PILOT_USAGE);
380
+ return sub === undefined ? 2 : 0;
381
+ default:
382
+ console.error(`Unknown \`mla pilot\` subcommand: ${sub}\n\n${PILOT_USAGE}`);
383
+ return 2;
384
+ }
385
+ }
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readBindingStore = readBindingStore;
4
+ exports.writeBindingStore = writeBindingStore;
5
+ exports.canonicalizeDir = canonicalizeDir;
6
+ exports.enableBinding = enableBinding;
7
+ exports.disableBinding = disableBinding;
8
+ exports.listBindings = listBindings;
9
+ exports.listEnabledBindings = listEnabledBindings;
10
+ // src/lib/agent-memory-capture/binding.ts
11
+ //
12
+ // The local capture-binding registry (§3). One binding per canonical memory
13
+ // directory (its realpath is the identity key). Reactivation of the same
14
+ // directory + workspace REUSES the bindingId; disable preserves it; never
15
+ // deletes it. The synthetic source path embeds the bindingId, so a regenerated
16
+ // id would fork one physical file into duplicate server sources.
17
+ const node_crypto_1 = require("node:crypto");
18
+ const node_fs_1 = require("node:fs");
19
+ const node_path_1 = require("node:path");
20
+ const config_1 = require("../config");
21
+ const paths_1 = require("./paths");
22
+ function emptyStore() {
23
+ return { version: 1, bindings: [] };
24
+ }
25
+ function readBindingStore(home = config_1.HOME) {
26
+ let raw;
27
+ try {
28
+ raw = (0, node_fs_1.readFileSync)((0, paths_1.bindingsPath)(home), "utf8");
29
+ }
30
+ catch {
31
+ return emptyStore();
32
+ }
33
+ try {
34
+ const parsed = JSON.parse(raw);
35
+ if (!parsed || !Array.isArray(parsed.bindings))
36
+ return emptyStore();
37
+ return { version: 1, bindings: parsed.bindings };
38
+ }
39
+ catch {
40
+ return emptyStore();
41
+ }
42
+ }
43
+ function writeBindingStore(store, home = config_1.HOME) {
44
+ const dest = (0, paths_1.bindingsPath)(home);
45
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(dest), { recursive: true });
46
+ const tmp = `${dest}.${process.pid}.tmp`;
47
+ (0, node_fs_1.writeFileSync)(tmp, JSON.stringify(store, null, 2) + "\n", { mode: 0o600 });
48
+ (0, node_fs_1.renameSync)(tmp, dest);
49
+ }
50
+ // Resolve a directory to its canonical realpath. Returns null when the path does
51
+ // not resolve (missing dir): the caller surfaces an actionable diagnostic
52
+ // rather than persisting an unverifiable binding.
53
+ function canonicalizeDir(dir) {
54
+ try {
55
+ return (0, node_fs_1.realpathSync)(dir);
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ // Enable (or reactivate) capture for a directory + workspace. Reuses the
62
+ // bindingId when one already exists for the same canonical directory. A request
63
+ // to bind a directory already bound to a DIFFERENT workspace is a
64
+ // MEMORY-WORKSPACE-1 conflict: refuse and report (the caller disables nothing
65
+ // silently; one directory binds exactly one workspace).
66
+ function enableBinding(rawDir, workspaceId, nowIso, home = config_1.HOME) {
67
+ const memoryDir = canonicalizeDir(rawDir);
68
+ if (!memoryDir)
69
+ return { ok: false, reason: "unresolved-dir" };
70
+ const store = readBindingStore(home);
71
+ const existing = store.bindings.find((b) => b.memoryDir === memoryDir);
72
+ if (existing && existing.workspaceId !== workspaceId) {
73
+ return {
74
+ ok: false,
75
+ reason: "workspace-conflict",
76
+ conflictWorkspaceId: existing.workspaceId,
77
+ };
78
+ }
79
+ if (existing) {
80
+ const reactivated = !existing.enabled;
81
+ existing.enabled = true;
82
+ // Preserve bindingId and consentedAt; the binding is the same one.
83
+ writeBindingStore(store, home);
84
+ return { ok: true, binding: existing, reactivated };
85
+ }
86
+ const binding = {
87
+ bindingId: (0, node_crypto_1.randomUUID)(),
88
+ memoryDir,
89
+ workspaceId,
90
+ enabled: true,
91
+ consentedAt: nowIso,
92
+ };
93
+ store.bindings.push(binding);
94
+ writeBindingStore(store, home);
95
+ return { ok: true, binding, reactivated: false };
96
+ }
97
+ // Disable a binding for a directory. Preserves the bindingId so a later
98
+ // reactivation keeps the same synthetic source identity. Returns the binding if
99
+ // one was found.
100
+ function disableBinding(rawDir, home = config_1.HOME) {
101
+ const memoryDir = canonicalizeDir(rawDir) ?? rawDir;
102
+ const store = readBindingStore(home);
103
+ const binding = store.bindings.find((b) => b.memoryDir === memoryDir);
104
+ if (!binding)
105
+ return null;
106
+ binding.enabled = false;
107
+ writeBindingStore(store, home);
108
+ return binding;
109
+ }
110
+ function listBindings(home = config_1.HOME) {
111
+ return readBindingStore(home).bindings;
112
+ }
113
+ function listEnabledBindings(home = config_1.HOME) {
114
+ return readBindingStore(home).bindings.filter((b) => b.enabled);
115
+ }
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ // src/lib/agent-memory-capture/classify.ts
3
+ //
4
+ // Eligibility is decided by frontmatter `metadata.type`, NEVER by filename
5
+ // (notes/20260626-agent-memory-auto-capture-proposal.md §1, §4). MVP captures
6
+ // `project` only; `user`/`feedback`/`reference` are skipped. The existing
7
+ // scanner/frontmatter.ts parser is intentionally flat (scalar `key: value`
8
+ // only) and does not descend into the nested `metadata:` block, so this module
9
+ // extracts `metadata.type` itself.
10
+ //
11
+ // "Malformed frontmatter" (an opened `---` fence that never closes) is a
12
+ // distinct outcome from "no project type": the former routes to a processing
13
+ // failure (do not upload, do not retire, retry when corrected), the latter to
14
+ // ineligible/reclassified. Keep them separate.
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.classifyMemory = classifyMemory;
17
+ exports.isCapturable = isCapturable;
18
+ function stripScalar(v) {
19
+ let s = v.trim();
20
+ if (s.length >= 2 && ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'")))) {
21
+ s = s.slice(1, -1);
22
+ }
23
+ return s.trim().toLowerCase();
24
+ }
25
+ function classifyMemory(text) {
26
+ if (!text.startsWith("---\n")) {
27
+ return { type: null, hasFrontmatter: false, malformed: false };
28
+ }
29
+ const end = text.indexOf("\n---", 4);
30
+ if (end === -1) {
31
+ // Opened but never closed: a real structural defect, not just "no type".
32
+ return { type: null, hasFrontmatter: true, malformed: true };
33
+ }
34
+ const block = text.slice(4, end);
35
+ const lines = block.split("\n");
36
+ let type = null;
37
+ let inMetadata = false;
38
+ for (const line of lines) {
39
+ // Enter the nested metadata: block.
40
+ if (/^metadata:\s*$/.test(line)) {
41
+ inMetadata = true;
42
+ continue;
43
+ }
44
+ // A new top-level key (no leading whitespace, ends the metadata block).
45
+ if (inMetadata && /^\S/.test(line)) {
46
+ inMetadata = false;
47
+ }
48
+ // metadata.type (indented under metadata:).
49
+ if (inMetadata) {
50
+ const m = /^\s+type:\s*(\S.*?)\s*$/.exec(line);
51
+ if (m) {
52
+ type = stripScalar(m[1]);
53
+ break;
54
+ }
55
+ continue;
56
+ }
57
+ // Fallback: a top-level `type:` (defensive; the corpus nests it).
58
+ const top = /^type:\s*(\S.*?)\s*$/.exec(line);
59
+ if (top && type === null) {
60
+ type = stripScalar(top[1]);
61
+ }
62
+ }
63
+ return { type, hasFrontmatter: true, malformed: false };
64
+ }
65
+ // MVP captures `project` only.
66
+ function isCapturable(c) {
67
+ return c.type === "project";
68
+ }
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.appendDecisions = appendDecisions;
4
+ exports.runDryRunCollector = runDryRunCollector;
5
+ // src/lib/agent-memory-capture/collector.ts
6
+ //
7
+ // Orchestrates one dry-run collection pass across all enabled bindings: take the
8
+ // per-binding lock (skip if a live collector holds it), run the exact-byte
9
+ // pipeline, append only the ACTIONABLE decisions to a metadata-only JSONL, and
10
+ // release the lock. Uploads nothing (Phase 1).
11
+ const node_fs_1 = require("node:fs");
12
+ const node_path_1 = require("node:path");
13
+ const config_1 = require("../config");
14
+ const lock_1 = require("./lock");
15
+ const binding_1 = require("./binding");
16
+ const pipeline_1 = require("./pipeline");
17
+ const paths_1 = require("./paths");
18
+ // Append the actionable decisions for one binding to its JSONL. Metadata only:
19
+ // each line is {sourceId, relativePath, hash, bytes, decision, reason,
20
+ // secretRuleIds, observedAt}. NEVER raw content. Unchanged/skipped no-ops are
21
+ // dropped so the log does not grow without bound.
22
+ function appendDecisions(bindingId, records, home = config_1.HOME) {
23
+ const actionable = records.filter((r) => (0, pipeline_1.isActionable)(r.decision));
24
+ if (actionable.length === 0)
25
+ return 0;
26
+ const dest = (0, paths_1.decisionLogPath)(bindingId, home);
27
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(dest), { recursive: true });
28
+ const lines = actionable.map((r) => JSON.stringify(r)).join("\n") + "\n";
29
+ (0, node_fs_1.appendFileSync)(dest, lines, { mode: 0o600 });
30
+ return actionable.length;
31
+ }
32
+ function runForBinding(binding, deps) {
33
+ const home = deps.home ?? config_1.HOME;
34
+ const lock = (0, lock_1.acquireBindingLock)(binding.bindingId, deps.nowIso, home);
35
+ if (!lock) {
36
+ return { bindingId: binding.bindingId, summary: null, locked: false, appended: 0 };
37
+ }
38
+ try {
39
+ const summary = (0, pipeline_1.collectOnce)(binding, deps);
40
+ const appended = appendDecisions(binding.bindingId, summary.records, home);
41
+ return { bindingId: binding.bindingId, summary, locked: true, appended };
42
+ }
43
+ finally {
44
+ lock.release();
45
+ }
46
+ }
47
+ // Run a dry-run pass over every enabled binding. Fail-soft per binding: one
48
+ // binding's error never aborts the others.
49
+ function runDryRunCollector(opts) {
50
+ const home = opts.home ?? config_1.HOME;
51
+ const bindings = (0, binding_1.listEnabledBindings)(home);
52
+ const deps = {
53
+ nowIso: opts.nowIso,
54
+ home,
55
+ ...(opts.scan ? { scan: opts.scan } : {}),
56
+ ...(opts.scannerVersion ? { scannerVersion: opts.scannerVersion } : {}),
57
+ ...(opts.scannerMode ? { scannerMode: opts.scannerMode } : {}),
58
+ };
59
+ const out = [];
60
+ for (const b of bindings) {
61
+ try {
62
+ out.push(runForBinding(b, deps));
63
+ }
64
+ catch {
65
+ out.push({ bindingId: b.bindingId, summary: null, locked: false, appended: 0 });
66
+ }
67
+ }
68
+ return out;
69
+ }