@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.
- package/dist/build-info.json +3 -3
- package/dist/cli.js +31 -5
- package/dist/commands/activate.js +39 -18
- package/dist/commands/agent-memory.js +333 -0
- package/dist/commands/enrich.js +211 -2
- package/dist/commands/internal-auto-index.js +64 -1
- package/dist/commands/internal-pretool-observe.js +86 -1
- package/dist/commands/internal-redact-capture.js +130 -0
- package/dist/commands/pilot.js +385 -0
- package/dist/lib/agent-memory-capture/binding.js +115 -0
- package/dist/lib/agent-memory-capture/classify.js +68 -0
- package/dist/lib/agent-memory-capture/collector.js +69 -0
- package/dist/lib/agent-memory-capture/containment.js +74 -0
- package/dist/lib/agent-memory-capture/ledger.js +43 -0
- package/dist/lib/agent-memory-capture/live-collector.js +148 -0
- package/dist/lib/agent-memory-capture/live-ledger.js +45 -0
- package/dist/lib/agent-memory-capture/live-pipeline.js +344 -0
- package/dist/lib/agent-memory-capture/lock.js +98 -0
- package/dist/lib/agent-memory-capture/paths.js +47 -0
- package/dist/lib/agent-memory-capture/pipeline.js +222 -0
- package/dist/lib/agent-memory-capture/report.js +131 -0
- package/dist/lib/agent-memory-capture/types.js +14 -0
- package/dist/lib/agent-memory-capture/upsert-client.js +104 -0
- package/dist/lib/analytics/enforcement-classify.js +65 -0
- package/dist/lib/analytics/enforcement-incident.js +83 -0
- package/dist/lib/analytics/envelope.js +55 -1
- package/dist/lib/analytics/pilot.js +313 -0
- package/dist/lib/enrichment/ingest.js +98 -13
- package/dist/lib/enrichment/materialize-rules.js +81 -0
- package/dist/lib/enrichment/plan.js +72 -15
- package/dist/lib/enrichment/protocol.js +85 -5
- package/dist/lib/enrichment/scout-brief.js +35 -6
- package/dist/lib/redactor.js +104 -1
- package/dist/lib/scanner/agent-memory.js +55 -4
- package/dist/lib/scanner/managed-rules.js +0 -0
- package/dist/lib/scanner/scan.js +52 -1
- package/dist/lib/scanner/score.js +41 -3
- package/dist/lib/scanner/scout-mission.js +9 -7
- package/dist/lib/upgrade-apply.js +30 -0
- package/dist/lib/wire.js +2 -0
- 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
|
+
}
|