@meetless/mla 0.1.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.
Files changed (202) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +81 -0
  3. package/dist/build-info.json +9 -0
  4. package/dist/bundles/ask-core.js +396 -0
  5. package/dist/bundles/mcp.js +16592 -0
  6. package/dist/bundles/trace-core.js +263 -0
  7. package/dist/cli.js +828 -0
  8. package/dist/commands/activate.js +781 -0
  9. package/dist/commands/adoption.js +130 -0
  10. package/dist/commands/ask.js +290 -0
  11. package/dist/commands/context.js +114 -0
  12. package/dist/commands/debug.js +313 -0
  13. package/dist/commands/doctor.js +1021 -0
  14. package/dist/commands/enrich.js +427 -0
  15. package/dist/commands/evidence.js +229 -0
  16. package/dist/commands/flush.js +184 -0
  17. package/dist/commands/graph.js +104 -0
  18. package/dist/commands/init.js +272 -0
  19. package/dist/commands/internal-active-review.js +322 -0
  20. package/dist/commands/internal-auto-index.js +188 -0
  21. package/dist/commands/internal-capture-decisions.js +320 -0
  22. package/dist/commands/internal-evidence-correlate.js +239 -0
  23. package/dist/commands/internal-evidence-hooks.js +240 -0
  24. package/dist/commands/internal-evidence-inject.js +231 -0
  25. package/dist/commands/internal-finalize.js +221 -0
  26. package/dist/commands/internal-pretool-observe.js +225 -0
  27. package/dist/commands/internal-refresh.js +136 -0
  28. package/dist/commands/internal-session-nudge.js +120 -0
  29. package/dist/commands/internal-steer-sync.js +117 -0
  30. package/dist/commands/internal-turn-recap.js +140 -0
  31. package/dist/commands/kb.js +375 -0
  32. package/dist/commands/kb_add.js +681 -0
  33. package/dist/commands/kb_forget.js +283 -0
  34. package/dist/commands/kb_move.js +45 -0
  35. package/dist/commands/kb_pending.js +410 -0
  36. package/dist/commands/kb_personal.js +149 -0
  37. package/dist/commands/kb_promote.js +188 -0
  38. package/dist/commands/kb_purge.js +168 -0
  39. package/dist/commands/kb_reingest.js +335 -0
  40. package/dist/commands/kb_retime.js +170 -0
  41. package/dist/commands/kb_review.js +391 -0
  42. package/dist/commands/kb_revision.js +179 -0
  43. package/dist/commands/kb_show.js +385 -0
  44. package/dist/commands/label.js +226 -0
  45. package/dist/commands/login.js +295 -0
  46. package/dist/commands/logout.js +108 -0
  47. package/dist/commands/mcp-supervisor.js +93 -0
  48. package/dist/commands/mcp.js +227 -0
  49. package/dist/commands/queue-prune.js +98 -0
  50. package/dist/commands/review.js +358 -0
  51. package/dist/commands/rewire.js +124 -0
  52. package/dist/commands/rules.js +728 -0
  53. package/dist/commands/scan-context.js +67 -0
  54. package/dist/commands/session.js +347 -0
  55. package/dist/commands/stats.js +479 -0
  56. package/dist/commands/status.js +61 -0
  57. package/dist/commands/summary.js +250 -0
  58. package/dist/commands/turn.js +114 -0
  59. package/dist/commands/uninstall.js +222 -0
  60. package/dist/commands/whoami.js +102 -0
  61. package/dist/commands/workspace.js +130 -0
  62. package/dist/hooks-template/ce0-post-tool-use.sh +34 -0
  63. package/dist/hooks-template/ce0-session-start.sh +49 -0
  64. package/dist/hooks-template/ce0-stop.sh +29 -0
  65. package/dist/hooks-template/ce0-user-prompt-submit.sh +38 -0
  66. package/dist/hooks-template/common.sh +934 -0
  67. package/dist/hooks-template/event-batch-filter.jq +67 -0
  68. package/dist/hooks-template/flush.sh +503 -0
  69. package/dist/hooks-template/post-tool-use.sh +423 -0
  70. package/dist/hooks-template/pre-tool-use.sh +69 -0
  71. package/dist/hooks-template/session-start.sh +140 -0
  72. package/dist/hooks-template/stop.sh +308 -0
  73. package/dist/hooks-template/user-prompt-submit.sh +1162 -0
  74. package/dist/lib/activation.js +79 -0
  75. package/dist/lib/active-conflict-cache.js +141 -0
  76. package/dist/lib/active-memory.js +59 -0
  77. package/dist/lib/active-review-runner.js +26 -0
  78. package/dist/lib/agent-decision/index.js +25 -0
  79. package/dist/lib/agent-decision/keys.js +49 -0
  80. package/dist/lib/agent-decision/normalize-claude.js +183 -0
  81. package/dist/lib/agent-decision/types.js +21 -0
  82. package/dist/lib/agent-decision/validate.js +216 -0
  83. package/dist/lib/analytics/capture.js +96 -0
  84. package/dist/lib/analytics/command-event.js +267 -0
  85. package/dist/lib/analytics/consent.js +58 -0
  86. package/dist/lib/analytics/coverage-gap.js +96 -0
  87. package/dist/lib/analytics/envelope.js +236 -0
  88. package/dist/lib/analytics/event-id.js +86 -0
  89. package/dist/lib/analytics/evidence.js +150 -0
  90. package/dist/lib/analytics/followthrough.js +194 -0
  91. package/dist/lib/analytics/forwarder.js +109 -0
  92. package/dist/lib/analytics/logs.js +78 -0
  93. package/dist/lib/analytics/metrics.js +78 -0
  94. package/dist/lib/analytics/recorder.js +92 -0
  95. package/dist/lib/analytics/review-analytics.js +75 -0
  96. package/dist/lib/analytics/sequence.js +77 -0
  97. package/dist/lib/analytics/store.js +131 -0
  98. package/dist/lib/analytics/turn-recap.js +279 -0
  99. package/dist/lib/artifact_id.js +108 -0
  100. package/dist/lib/auth-breaker.js +161 -0
  101. package/dist/lib/auto-index.js +112 -0
  102. package/dist/lib/classifier.js +88 -0
  103. package/dist/lib/config.js +298 -0
  104. package/dist/lib/conflict-advisory.js +64 -0
  105. package/dist/lib/debug-bundle.js +520 -0
  106. package/dist/lib/enrichment/ingest.js +301 -0
  107. package/dist/lib/enrichment/plan.js +253 -0
  108. package/dist/lib/enrichment/protocol.js +359 -0
  109. package/dist/lib/enrichment/scout-brief.js +176 -0
  110. package/dist/lib/failure-telemetry.js +444 -0
  111. package/dist/lib/git.js +200 -0
  112. package/dist/lib/governance-cache.js +77 -0
  113. package/dist/lib/governed-path-cache.js +76 -0
  114. package/dist/lib/http.js +677 -0
  115. package/dist/lib/identity-envelope.js +23 -0
  116. package/dist/lib/kb-candidate.js +65 -0
  117. package/dist/lib/kb_acl.js +98 -0
  118. package/dist/lib/login.js +353 -0
  119. package/dist/lib/mcp-fetchers.js +130 -0
  120. package/dist/lib/mcp-restart.js +47 -0
  121. package/dist/lib/observability.js +805 -0
  122. package/dist/lib/open-url.js +33 -0
  123. package/dist/lib/orphan-guard.js +70 -0
  124. package/dist/lib/packaged.js +21 -0
  125. package/dist/lib/reconcile-sessions.js +171 -0
  126. package/dist/lib/redactor.js +89 -0
  127. package/dist/lib/relationship-candidate-query.js +27 -0
  128. package/dist/lib/render.js +611 -0
  129. package/dist/lib/rules/applicability.js +64 -0
  130. package/dist/lib/rules/attest-code-rule-version.js +47 -0
  131. package/dist/lib/rules/attest-notes-location.js +217 -0
  132. package/dist/lib/rules/attest-rule-version.js +69 -0
  133. package/dist/lib/rules/canonical-json.js +97 -0
  134. package/dist/lib/rules/ce0-emit.js +64 -0
  135. package/dist/lib/rules/ce0-evidence.js +281 -0
  136. package/dist/lib/rules/ce0-recall-sample.js +82 -0
  137. package/dist/lib/rules/ce0-rule.js +55 -0
  138. package/dist/lib/rules/ce0-sampling-bucket.js +15 -0
  139. package/dist/lib/rules/ce0-store.js +683 -0
  140. package/dist/lib/rules/ce0-telemetry-project.js +93 -0
  141. package/dist/lib/rules/ce0-telemetry.js +158 -0
  142. package/dist/lib/rules/code-rule-registry.js +17 -0
  143. package/dist/lib/rules/command-match.js +185 -0
  144. package/dist/lib/rules/consult-evidence-binding.js +27 -0
  145. package/dist/lib/rules/consultation-capture-adapter.js +193 -0
  146. package/dist/lib/rules/content-match.js +56 -0
  147. package/dist/lib/rules/deny-admission.js +99 -0
  148. package/dist/lib/rules/durable-observation.js +190 -0
  149. package/dist/lib/rules/enforce-notes-version.js +421 -0
  150. package/dist/lib/rules/evaluation-input-hash.js +126 -0
  151. package/dist/lib/rules/evaluator.js +108 -0
  152. package/dist/lib/rules/inert-rule-families.js +51 -0
  153. package/dist/lib/rules/input-authority-resolver.js +241 -0
  154. package/dist/lib/rules/interception-schema.js +170 -0
  155. package/dist/lib/rules/interception-store.js +267 -0
  156. package/dist/lib/rules/live-input-authority.js +66 -0
  157. package/dist/lib/rules/local-matcher.js +108 -0
  158. package/dist/lib/rules/local-observe.js +79 -0
  159. package/dist/lib/rules/local-rule-version-repo.js +214 -0
  160. package/dist/lib/rules/memory-requirement.js +109 -0
  161. package/dist/lib/rules/notes-observe.js +39 -0
  162. package/dist/lib/rules/notes-path.js +261 -0
  163. package/dist/lib/rules/notes-rule.js +75 -0
  164. package/dist/lib/rules/observe-adapter.js +114 -0
  165. package/dist/lib/rules/observed-rule-hash.js +119 -0
  166. package/dist/lib/rules/prompt-submit-adapter.js +132 -0
  167. package/dist/lib/rules/requirement-subject.js +240 -0
  168. package/dist/lib/rules/rule-activity.js +67 -0
  169. package/dist/lib/rules/rule-version-hash.js +151 -0
  170. package/dist/lib/rules/runtime-scope.js +55 -0
  171. package/dist/lib/rules/stop-adapter.js +116 -0
  172. package/dist/lib/rules/stop-response-snapshot.js +174 -0
  173. package/dist/lib/rules/types.js +10 -0
  174. package/dist/lib/rules/ulid.js +46 -0
  175. package/dist/lib/rules/version-evaluation.js +156 -0
  176. package/dist/lib/scanner/agent-memory.js +99 -0
  177. package/dist/lib/scanner/bootstrap-summary.js +87 -0
  178. package/dist/lib/scanner/cache.js +59 -0
  179. package/dist/lib/scanner/frontmatter.js +42 -0
  180. package/dist/lib/scanner/parse-directives.js +69 -0
  181. package/dist/lib/scanner/parse-structured.js +72 -0
  182. package/dist/lib/scanner/render.js +73 -0
  183. package/dist/lib/scanner/scan.js +132 -0
  184. package/dist/lib/scanner/score.js +38 -0
  185. package/dist/lib/scanner/scout-mission.js +126 -0
  186. package/dist/lib/scanner/types.js +7 -0
  187. package/dist/lib/session-scope.js +195 -0
  188. package/dist/lib/spool.js +355 -0
  189. package/dist/lib/staleness.js +100 -0
  190. package/dist/lib/steer-cache.js +87 -0
  191. package/dist/lib/tagged-reference.js +20 -0
  192. package/dist/lib/temporal.js +109 -0
  193. package/dist/lib/turn-recap-emit.js +67 -0
  194. package/dist/lib/unwire.js +253 -0
  195. package/dist/lib/update-check.js +469 -0
  196. package/dist/lib/update-notifier.js +217 -0
  197. package/dist/lib/upgrade-apply.js +643 -0
  198. package/dist/lib/wire.js +1087 -0
  199. package/dist/lib/workspace.js +96 -0
  200. package/dist/lib/zip.js +154 -0
  201. package/dist/pretool-entry.js +37 -0
  202. package/package.json +75 -0
@@ -0,0 +1,320 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.parseArgs = parseArgs;
37
+ exports.normalizePostToolUseInput = normalizePostToolUseInput;
38
+ exports.scanTranscriptForDecisions = scanTranscriptForDecisions;
39
+ exports.toSpoolEvents = toSpoolEvents;
40
+ exports.runCaptureDecisions = runCaptureDecisions;
41
+ const fs = __importStar(require("fs"));
42
+ const agent_decision_1 = require("../lib/agent-decision");
43
+ const normalize_claude_1 = require("../lib/agent-decision/normalize-claude");
44
+ const SOURCES = ["post_tool_use", "stop_transcript_scan"];
45
+ // Strict argv parsing, mirroring the convention in internal-finalize.ts: any
46
+ // unknown flag, a missing required flag, or a bare positional throws (exit 2 in
47
+ // the wrapper). A silent default here would let a flush.sh wiring bug capture
48
+ // decisions under the wrong source or session and never surface.
49
+ function parseArgs(argv) {
50
+ let source;
51
+ let session;
52
+ let transcript;
53
+ let spool;
54
+ for (let i = 0; i < argv.length; i++) {
55
+ const a = argv[i];
56
+ switch (a) {
57
+ case "--source":
58
+ source = argv[++i];
59
+ break;
60
+ case "--session":
61
+ session = argv[++i];
62
+ break;
63
+ case "--transcript":
64
+ transcript = argv[++i];
65
+ break;
66
+ case "--spool":
67
+ spool = argv[++i];
68
+ break;
69
+ default:
70
+ throw new Error(`Unknown argument: ${a}. usage: mla _internal capture-decisions --source <post_tool_use|stop_transcript_scan> --session <id> [--transcript <path>] [--spool <path>]`);
71
+ }
72
+ }
73
+ if (source === undefined || !SOURCES.includes(source)) {
74
+ throw new Error(`--source must be one of ${SOURCES.join(", ")} (got ${JSON.stringify(source)})`);
75
+ }
76
+ if (session === undefined || session.length === 0) {
77
+ throw new Error("--session <providerSessionId> is required");
78
+ }
79
+ if (source === "stop_transcript_scan" && (transcript === undefined || transcript.length === 0)) {
80
+ throw new Error("--transcript <path> is required when --source stop_transcript_scan");
81
+ }
82
+ const out = { source: source, session };
83
+ if (transcript !== undefined)
84
+ out.transcript = transcript;
85
+ if (spool !== undefined)
86
+ out.spool = spool;
87
+ return out;
88
+ }
89
+ function isPlainObject(v) {
90
+ return typeof v === "object" && v !== null && !Array.isArray(v);
91
+ }
92
+ // PostToolUse path: one hook payload -> canonical payloads. Returns [] (never
93
+ // throws) when the tool is not AskUserQuestion or the payload is structurally
94
+ // unusable, so the hook it rides on is never crashed by a surprise shape.
95
+ function normalizePostToolUseInput(hookPayload, opts) {
96
+ if (!isPlainObject(hookPayload))
97
+ return [];
98
+ if (hookPayload.tool_name !== normalize_claude_1.CLAUDE_TOOL_NAME)
99
+ return [];
100
+ const toolUseId = hookPayload.tool_use_id;
101
+ const toolInput = hookPayload.tool_input;
102
+ const toolResponse = hookPayload.tool_response;
103
+ const questions = isPlainObject(toolInput) ? toolInput.questions : undefined;
104
+ // tool_response.answers is an object keyed on the EXACT question text (verified
105
+ // against a real PostToolUse payload and a real transcript sidecar). A missing
106
+ // or malformed answers map yields no decisions: an unanswered question is not a
107
+ // captured human decision, which the normalizer already enforces per-question.
108
+ const answers = isPlainObject(toolResponse) ? toolResponse.answers : undefined;
109
+ if (typeof toolUseId !== "string" || !Array.isArray(questions) || !isPlainObject(answers)) {
110
+ return [];
111
+ }
112
+ return (0, normalize_claude_1.normalizeClaudeAskUserQuestion)({
113
+ toolUseId,
114
+ questions: questions,
115
+ answers,
116
+ }, {
117
+ providerSessionId: opts.providerSessionId,
118
+ capturedBy: "post_tool_use",
119
+ occurredAt: opts.occurredAt,
120
+ });
121
+ }
122
+ // Stop backstop path: scan transcript JSONL for AskUserQuestion tool_use /
123
+ // tool_result pairs. A single forward pass works because an assistant tool_use
124
+ // line always precedes its user tool_result line in chronological JSONL.
125
+ //
126
+ // The tool_use line (assistant) is authoritative for the offered questions +
127
+ // options; the matching user line carries the toolUseResult sidecar with the
128
+ // answers keyed on question text, plus a tool_result block echoing the
129
+ // tool_use_id used to pair them. occurredAt is the user line's recorded
130
+ // timestamp when present (the moment the answer landed); it is stored but is
131
+ // NEVER an identity input, so it may differ from the primary path safely.
132
+ function scanTranscriptForDecisions(lines, opts) {
133
+ const askQuestions = new Map();
134
+ const out = [];
135
+ for (const line of lines) {
136
+ const trimmed = line.trim();
137
+ if (trimmed.length === 0)
138
+ continue;
139
+ let obj;
140
+ try {
141
+ obj = JSON.parse(trimmed);
142
+ }
143
+ catch {
144
+ continue; // a truncated / non-JSON line is skipped, not fatal
145
+ }
146
+ if (!isPlainObject(obj))
147
+ continue;
148
+ const message = obj.message;
149
+ const content = isPlainObject(message) ? message.content : undefined;
150
+ if (obj.type === "assistant") {
151
+ if (!Array.isArray(content))
152
+ continue;
153
+ for (const block of content) {
154
+ if (isPlainObject(block) &&
155
+ block.type === "tool_use" &&
156
+ block.name === normalize_claude_1.CLAUDE_TOOL_NAME &&
157
+ typeof block.id === "string") {
158
+ const input = block.input;
159
+ const questions = isPlainObject(input) ? input.questions : undefined;
160
+ if (Array.isArray(questions)) {
161
+ askQuestions.set(block.id, questions);
162
+ }
163
+ }
164
+ }
165
+ continue;
166
+ }
167
+ if (obj.type === "user") {
168
+ if (!Array.isArray(content))
169
+ continue;
170
+ let toolUseId;
171
+ for (const block of content) {
172
+ if (isPlainObject(block) && block.type === "tool_result" && typeof block.tool_use_id === "string") {
173
+ toolUseId = block.tool_use_id;
174
+ break;
175
+ }
176
+ }
177
+ if (toolUseId === undefined)
178
+ continue;
179
+ const questions = askQuestions.get(toolUseId);
180
+ if (questions === undefined)
181
+ continue; // not an AskUserQuestion result
182
+ const sidecar = obj.toolUseResult;
183
+ const answers = isPlainObject(sidecar) ? sidecar.answers : undefined;
184
+ if (!isPlainObject(answers))
185
+ continue; // unanswered / unexpected shape
186
+ const occurredAt = typeof obj.timestamp === "string" ? obj.timestamp : undefined;
187
+ const ctx = {
188
+ providerSessionId: opts.providerSessionId,
189
+ capturedBy: "stop_transcript_scan",
190
+ };
191
+ if (occurredAt !== undefined)
192
+ ctx.occurredAt = occurredAt;
193
+ out.push(...(0, normalize_claude_1.normalizeClaudeAskUserQuestion)({ toolUseId, questions, answers }, ctx));
194
+ }
195
+ }
196
+ return out;
197
+ }
198
+ // Wrap validated canonical payloads into spool-event envelopes, deduping against
199
+ // eventKeys already present (the backstop skips what the primary spooled) and
200
+ // within the batch. Validation is fail-soft: a malformed decision is logged and
201
+ // skipped, never crashing the hook (spec: validate-and-skip at capture time).
202
+ function toSpoolEvents(payloads, opts) {
203
+ const seen = new Set(opts.existingEventKeys ?? []);
204
+ const out = [];
205
+ for (const payload of payloads) {
206
+ const errs = (0, agent_decision_1.validateCanonicalDecisionPayload)(payload);
207
+ if (errs.length > 0) {
208
+ opts.logError?.(`[capture-decisions] skipping invalid decision ${String(payload.providerEventId)}: ${errs.join("; ")}`);
209
+ continue;
210
+ }
211
+ const eventKey = (0, agent_decision_1.buildEventKey)(payload.provider, payload.providerEventId);
212
+ if (seen.has(eventKey))
213
+ continue;
214
+ seen.add(eventKey);
215
+ out.push({
216
+ ts: opts.ts,
217
+ event: agent_decision_1.AGENT_DECISION_EVENT,
218
+ eventKey,
219
+ sessionId: opts.sessionId,
220
+ payload,
221
+ });
222
+ }
223
+ return out;
224
+ }
225
+ function readStdinReal() {
226
+ return new Promise((resolve, reject) => {
227
+ const chunks = [];
228
+ process.stdin.on("data", (c) => chunks.push(c));
229
+ process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
230
+ process.stdin.on("error", reject);
231
+ });
232
+ }
233
+ const defaultDeps = {
234
+ readStdin: readStdinReal,
235
+ readFile: (p) => fs.readFileSync(p, "utf8"),
236
+ now: () => new Date().toISOString(),
237
+ writeLine: (line) => process.stdout.write(line + "\n"),
238
+ logError: (msg) => console.error(msg),
239
+ };
240
+ // Read the existing spool (if any) and collect its eventKeys so the backstop
241
+ // does not re-emit a decision the primary path already spooled. A missing file
242
+ // (no spool yet) is an empty set, not an error.
243
+ function readSpoolEventKeys(path, deps) {
244
+ const keys = new Set();
245
+ let raw;
246
+ try {
247
+ raw = deps.readFile(path);
248
+ }
249
+ catch {
250
+ return keys;
251
+ }
252
+ for (const line of raw.split("\n")) {
253
+ const trimmed = line.trim();
254
+ if (trimmed.length === 0)
255
+ continue;
256
+ try {
257
+ const obj = JSON.parse(trimmed);
258
+ if (typeof obj.eventKey === "string")
259
+ keys.add(obj.eventKey);
260
+ }
261
+ catch {
262
+ // ignore unparseable spool lines
263
+ }
264
+ }
265
+ return keys;
266
+ }
267
+ async function runCaptureDecisions(argv, deps = defaultDeps) {
268
+ let parsed;
269
+ try {
270
+ parsed = parseArgs(argv);
271
+ }
272
+ catch (e) {
273
+ deps.logError(e.message);
274
+ return 2;
275
+ }
276
+ const ts = deps.now();
277
+ let payloads;
278
+ if (parsed.source === "post_tool_use") {
279
+ const stdin = await deps.readStdin();
280
+ const trimmed = stdin.trim();
281
+ if (trimmed.length === 0)
282
+ return 0; // nothing piped in is a clean no-op
283
+ let hookPayload;
284
+ try {
285
+ hookPayload = JSON.parse(trimmed);
286
+ }
287
+ catch (e) {
288
+ deps.logError(`[capture-decisions] stdin is not valid JSON: ${e.message}`);
289
+ return 0; // never crash the hook on a malformed payload
290
+ }
291
+ payloads = normalizePostToolUseInput(hookPayload, {
292
+ providerSessionId: parsed.session,
293
+ occurredAt: ts,
294
+ });
295
+ }
296
+ else {
297
+ let raw;
298
+ try {
299
+ raw = deps.readFile(parsed.transcript);
300
+ }
301
+ catch (e) {
302
+ deps.logError(`[capture-decisions] cannot read transcript ${parsed.transcript}: ${e.message}`);
303
+ return 0; // a missing transcript is not a hook-crashing error
304
+ }
305
+ payloads = scanTranscriptForDecisions(raw.split("\n"), {
306
+ providerSessionId: parsed.session,
307
+ });
308
+ }
309
+ const existingEventKeys = parsed.spool ? readSpoolEventKeys(parsed.spool, deps) : undefined;
310
+ const events = toSpoolEvents(payloads, {
311
+ sessionId: parsed.session,
312
+ ts,
313
+ existingEventKeys,
314
+ logError: deps.logError,
315
+ });
316
+ for (const event of events) {
317
+ deps.writeLine(JSON.stringify(event));
318
+ }
319
+ return 0;
320
+ }
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ // `mla _internal evidence-correlate` -- the v1 local correlator (INV-CORRELATOR-1,
3
+ // spec sections 7.4, 10.5). Fired DETACHED from the Stop hook (spawn_evidence_correlate)
4
+ // at the end of every session. It closes every eligible PENDING inject window and
5
+ // appends one mla_evidence_outcome per closed inject to the local events.jsonl, then
6
+ // best-effort forwards when telemetry is on.
7
+ //
8
+ // It processes ALL pending injects across ALL sessions, not just the stopping one:
9
+ // a cross-session inject only closes by time_limit minutes later, and a Stop is the
10
+ // natural recompute tick. Idempotency comes from two guards: a skip-set of inject_ids
11
+ // that already have an outcome line, and the deterministic outcome event_id
12
+ // (sha256(inject_id:outcome_version), event-id.ts) so a re-run cannot inflate counts
13
+ // even across a race. An inject whose window is still open derives no outcome and
14
+ // stays pending (the ABSENCE of an outcome line, never dropped, never counted ignored).
15
+ //
16
+ // The outcome carries the INJECT's trace_id + run_id + workspace_id + session_id
17
+ // (section 11.3: the enrichment-outcome record is keyed by inject_id and stamped with
18
+ // the inject's trace/run), so the outcome joins back to the enrichment that produced
19
+ // it. The correlator is the recompute engine, not a new logical owner (INV-RUN-1: the
20
+ // outcome belongs to the inject's run).
21
+ //
22
+ // Fail-soft: every error is swallowed and the command exits 0 (a strict argv parse
23
+ // error -> 2), so closing windows can never disturb the session it spawned from.
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.parseArgs = parseArgs;
26
+ exports.runInternalEvidenceCorrelate = runInternalEvidenceCorrelate;
27
+ exports.splitEvidenceEvents = splitEvidenceEvents;
28
+ const config_1 = require("../lib/config");
29
+ const coverage_gap_1 = require("../lib/analytics/coverage-gap");
30
+ const evidence_1 = require("../lib/analytics/evidence");
31
+ const followthrough_1 = require("../lib/analytics/followthrough");
32
+ const logs_1 = require("../lib/analytics/logs");
33
+ const recorder_1 = require("../lib/analytics/recorder");
34
+ const store_1 = require("../lib/analytics/store");
35
+ // v1 takes no flags: the correlator always sweeps every pending inject across every
36
+ // session. Any argument is a strict error (exit 2), matching the other _internal
37
+ // commands. The hook calls it with no args.
38
+ function parseArgs(argv) {
39
+ if (argv.length > 0) {
40
+ throw new Error(`Unknown flag for \`mla _internal evidence-correlate\`: ${argv[0]}`);
41
+ }
42
+ }
43
+ // Reconstruct the minimal inject view deriveOutcome needs from a stored
44
+ // mla_evidence_inject jsonl line (the event is flat: payload + envelope at one level).
45
+ function toInjectRecord(ev) {
46
+ return {
47
+ inject_id: typeof ev.inject_id === "string" ? ev.inject_id : "",
48
+ session_id: typeof ev.session_id === "string" ? ev.session_id : "",
49
+ turn_index: typeof ev.turn_index === "number" && Number.isFinite(ev.turn_index)
50
+ ? ev.turn_index
51
+ : null,
52
+ offered_source_ids: Array.isArray(ev.offered_source_ids)
53
+ ? ev.offered_source_ids.filter((x) => typeof x === "string")
54
+ : [],
55
+ window_deadline: typeof ev.window_deadline === "string" ? ev.window_deadline : "",
56
+ };
57
+ }
58
+ // Highest turn observed per session, so deriveOutcome can tell whether the full
59
+ // 3-turn window has elapsed (turn_limit). Built from every signal that advances the
60
+ // per-session turn counter: ask-traces (the true counter, every prompt), pulls,
61
+ // citations, and the inject events themselves.
62
+ function buildMaxTurnBySession(asks, calls, citations, injects) {
63
+ const m = new Map();
64
+ const bump = (sid, turn) => {
65
+ if (typeof sid !== "string" || !sid)
66
+ return;
67
+ if (typeof turn !== "number" || !Number.isFinite(turn))
68
+ return;
69
+ const cur = m.get(sid);
70
+ if (cur === undefined || turn > cur)
71
+ m.set(sid, turn);
72
+ };
73
+ for (const a of asks)
74
+ bump(a.session_id, a.turn_index);
75
+ for (const c of calls)
76
+ bump(c.session_id, c.turn_index);
77
+ for (const r of citations)
78
+ bump(r.session_id, r.turn_index);
79
+ for (const ev of injects)
80
+ bump(ev.session_id, ev.turn_index);
81
+ return m;
82
+ }
83
+ async function runInternalEvidenceCorrelate(argv, deps = {}) {
84
+ try {
85
+ parseArgs(argv);
86
+ }
87
+ catch (e) {
88
+ console.error(e.message);
89
+ return 2;
90
+ }
91
+ const env = deps.env ?? process.env;
92
+ try {
93
+ const read = deps.read ?? store_1.readEvents;
94
+ const events = read(env);
95
+ // Split the local event log into pending injects and the set of inject_ids that
96
+ // already have an outcome (the idempotency skip-set). `gapInjectIds` is the set
97
+ // that already carry an inject-time mla_coverage_gap, so the outcome-time
98
+ // `candidates_found_not_used` gap fires only when the inject surfaced no gap
99
+ // at inject time (spec §7.5: "emitted only when inject-time emitted nothing").
100
+ const injectEvents = [];
101
+ const closed = new Set();
102
+ const gapInjectIds = new Set();
103
+ for (const ev of events) {
104
+ if (ev.event_type === "mla_evidence_inject")
105
+ injectEvents.push(ev);
106
+ else if (ev.event_type === "mla_evidence_outcome") {
107
+ if (typeof ev.inject_id === "string" && ev.inject_id)
108
+ closed.add(ev.inject_id);
109
+ }
110
+ else if (ev.event_type === "mla_coverage_gap") {
111
+ if (typeof ev.inject_id === "string" && ev.inject_id)
112
+ gapInjectIds.add(ev.inject_id);
113
+ }
114
+ }
115
+ const readLog = deps.readLog ?? logs_1.readLogJsonl;
116
+ const asks = readLog("ask-traces.jsonl");
117
+ const calls = (0, followthrough_1.parseMcpCalls)(readLog("mcp-calls.jsonl"));
118
+ const citations = (0, followthrough_1.parseReportCitations)(readLog("report-citations.jsonl"));
119
+ const maxTurnBySession = buildMaxTurnBySession(asks, calls, citations, injectEvents);
120
+ const nowMs = deps.nowMs ?? Date.now();
121
+ const nowIso = new Date(nowMs).toISOString();
122
+ const record = deps.record ?? recorder_1.recordAnalyticsEvent;
123
+ const ctxBase = {
124
+ nowMs,
125
+ maxTurnBySession,
126
+ ...(deps.window !== undefined ? { window: deps.window } : {}),
127
+ };
128
+ let closedCount = 0;
129
+ let pendingCount = 0;
130
+ for (const ev of injectEvents) {
131
+ const injectId = typeof ev.inject_id === "string" ? ev.inject_id : "";
132
+ if (!injectId || closed.has(injectId))
133
+ continue; // already closed -> skip
134
+ // A stored inject must carry the run/trace it belongs to; without them the
135
+ // outcome cannot join back to the enrichment, so leave it pending rather than
136
+ // fabricate a join from the correlator's own run context.
137
+ const runId = typeof ev.run_id === "string" && ev.run_id ? ev.run_id : null;
138
+ const traceId = typeof ev.trace_id === "string" && ev.trace_id ? ev.trace_id : null;
139
+ if (!runId || !traceId) {
140
+ pendingCount++;
141
+ continue;
142
+ }
143
+ const inject = toInjectRecord(ev);
144
+ const derived = (0, evidence_1.deriveOutcome)(inject, calls, citations, ctxBase);
145
+ if (!derived) {
146
+ pendingCount++;
147
+ continue;
148
+ }
149
+ const ctx = {
150
+ workspaceId: typeof ev.workspace_id === "string" ? ev.workspace_id : null,
151
+ sessionId: inject.session_id || null,
152
+ distinctId: typeof ev.distinct_id === "string" ? ev.distinct_id : null,
153
+ runId,
154
+ traceId,
155
+ source: "hook",
156
+ now: nowIso,
157
+ };
158
+ record(ctx, {
159
+ eventType: "mla_evidence_outcome",
160
+ eventId: derived.event_id,
161
+ payload: derived.payload,
162
+ }, env);
163
+ // Outcome-time coverage gap (spec §7.5, INV-COVERAGE-GAP-1): a confident,
164
+ // non-empty inject that the agent had its full turn window to use yet
165
+ // referenced none of is `candidates_found_not_used` (fix retrieval/ranking,
166
+ // not capture). Only on an `ignored` close (turn_limit, the full opportunity
167
+ // was observed), only when candidates actually came back (not zero_results),
168
+ // and only when no inject-time gap already classified this inject. The
169
+ // deterministic event_id + the once-per-inject close make it idempotent.
170
+ const hadCandidates = ev.zero_results !== true;
171
+ if (derived.payload.outcome === "ignored" &&
172
+ hadCandidates &&
173
+ !gapInjectIds.has(injectId)) {
174
+ record(ctx, {
175
+ eventType: "mla_coverage_gap",
176
+ eventId: (0, coverage_gap_1.coverageGapNotUsedEventId)(injectId),
177
+ payload: (0, coverage_gap_1.buildCoverageGapPayload)({
178
+ injectId,
179
+ coverageGapType: "candidates_found_not_used",
180
+ // The correlator cannot recover the original query topic; the
181
+ // inject-time gap carries it when known. Default to unknown here.
182
+ queryTopicCategory: "unknown",
183
+ retrievalConfidence: (0, coverage_gap_1.coerceRetrievalConfidence)(typeof ev.retrieval_confidence === "string"
184
+ ? ev.retrieval_confidence
185
+ : null),
186
+ zeroResults: false,
187
+ }),
188
+ }, env);
189
+ gapInjectIds.add(injectId);
190
+ }
191
+ // Guard against a duplicate (S, inject_id) line in the same sweep; the
192
+ // deterministic event_id already makes a cross-process race idempotent.
193
+ closed.add(injectId);
194
+ closedCount++;
195
+ }
196
+ // Best-effort, telemetry-gated forward (the consent gate is inside the
197
+ // forwarder). Skipped when the run has no control config.
198
+ const readCfg = deps.readCfg ??
199
+ (() => {
200
+ try {
201
+ return (0, config_1.readConfig)();
202
+ }
203
+ catch {
204
+ return null;
205
+ }
206
+ });
207
+ const cfg = readCfg();
208
+ if (cfg) {
209
+ const flush = deps.flush ?? recorder_1.flushAnalyticsEvents;
210
+ await flush(cfg, env);
211
+ }
212
+ console.log(JSON.stringify({
213
+ correlated: true,
214
+ closed: closedCount,
215
+ pending: pendingCount,
216
+ total: injectEvents.length,
217
+ }));
218
+ return 0;
219
+ }
220
+ catch {
221
+ // Fail-soft: a correlation failure never disturbs the session that spawned it.
222
+ console.log(JSON.stringify({ correlated: false, reason: "error" }));
223
+ return 0;
224
+ }
225
+ }
226
+ // Exported only so a future reader (mla stats) can reuse the same event-log split.
227
+ function splitEvidenceEvents(events) {
228
+ const injects = [];
229
+ const closedInjectIds = new Set();
230
+ for (const ev of events) {
231
+ if (ev.event_type === "mla_evidence_inject")
232
+ injects.push(ev);
233
+ else if (ev.event_type === "mla_evidence_outcome") {
234
+ if (typeof ev.inject_id === "string" && ev.inject_id)
235
+ closedInjectIds.add(ev.inject_id);
236
+ }
237
+ }
238
+ return { injects, closedInjectIds };
239
+ }