@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.
- package/LICENSE +201 -0
- package/README.md +81 -0
- package/dist/build-info.json +9 -0
- package/dist/bundles/ask-core.js +396 -0
- package/dist/bundles/mcp.js +16592 -0
- package/dist/bundles/trace-core.js +263 -0
- package/dist/cli.js +828 -0
- package/dist/commands/activate.js +781 -0
- package/dist/commands/adoption.js +130 -0
- package/dist/commands/ask.js +290 -0
- package/dist/commands/context.js +114 -0
- package/dist/commands/debug.js +313 -0
- package/dist/commands/doctor.js +1021 -0
- package/dist/commands/enrich.js +427 -0
- package/dist/commands/evidence.js +229 -0
- package/dist/commands/flush.js +184 -0
- package/dist/commands/graph.js +104 -0
- package/dist/commands/init.js +272 -0
- package/dist/commands/internal-active-review.js +322 -0
- package/dist/commands/internal-auto-index.js +188 -0
- package/dist/commands/internal-capture-decisions.js +320 -0
- package/dist/commands/internal-evidence-correlate.js +239 -0
- package/dist/commands/internal-evidence-hooks.js +240 -0
- package/dist/commands/internal-evidence-inject.js +231 -0
- package/dist/commands/internal-finalize.js +221 -0
- package/dist/commands/internal-pretool-observe.js +225 -0
- package/dist/commands/internal-refresh.js +136 -0
- package/dist/commands/internal-session-nudge.js +120 -0
- package/dist/commands/internal-steer-sync.js +117 -0
- package/dist/commands/internal-turn-recap.js +140 -0
- package/dist/commands/kb.js +375 -0
- package/dist/commands/kb_add.js +681 -0
- package/dist/commands/kb_forget.js +283 -0
- package/dist/commands/kb_move.js +45 -0
- package/dist/commands/kb_pending.js +410 -0
- package/dist/commands/kb_personal.js +149 -0
- package/dist/commands/kb_promote.js +188 -0
- package/dist/commands/kb_purge.js +168 -0
- package/dist/commands/kb_reingest.js +335 -0
- package/dist/commands/kb_retime.js +170 -0
- package/dist/commands/kb_review.js +391 -0
- package/dist/commands/kb_revision.js +179 -0
- package/dist/commands/kb_show.js +385 -0
- package/dist/commands/label.js +226 -0
- package/dist/commands/login.js +295 -0
- package/dist/commands/logout.js +108 -0
- package/dist/commands/mcp-supervisor.js +93 -0
- package/dist/commands/mcp.js +227 -0
- package/dist/commands/queue-prune.js +98 -0
- package/dist/commands/review.js +358 -0
- package/dist/commands/rewire.js +124 -0
- package/dist/commands/rules.js +728 -0
- package/dist/commands/scan-context.js +67 -0
- package/dist/commands/session.js +347 -0
- package/dist/commands/stats.js +479 -0
- package/dist/commands/status.js +61 -0
- package/dist/commands/summary.js +250 -0
- package/dist/commands/turn.js +114 -0
- package/dist/commands/uninstall.js +222 -0
- package/dist/commands/whoami.js +102 -0
- package/dist/commands/workspace.js +130 -0
- package/dist/hooks-template/ce0-post-tool-use.sh +34 -0
- package/dist/hooks-template/ce0-session-start.sh +49 -0
- package/dist/hooks-template/ce0-stop.sh +29 -0
- package/dist/hooks-template/ce0-user-prompt-submit.sh +38 -0
- package/dist/hooks-template/common.sh +934 -0
- package/dist/hooks-template/event-batch-filter.jq +67 -0
- package/dist/hooks-template/flush.sh +503 -0
- package/dist/hooks-template/post-tool-use.sh +423 -0
- package/dist/hooks-template/pre-tool-use.sh +69 -0
- package/dist/hooks-template/session-start.sh +140 -0
- package/dist/hooks-template/stop.sh +308 -0
- package/dist/hooks-template/user-prompt-submit.sh +1162 -0
- package/dist/lib/activation.js +79 -0
- package/dist/lib/active-conflict-cache.js +141 -0
- package/dist/lib/active-memory.js +59 -0
- package/dist/lib/active-review-runner.js +26 -0
- package/dist/lib/agent-decision/index.js +25 -0
- package/dist/lib/agent-decision/keys.js +49 -0
- package/dist/lib/agent-decision/normalize-claude.js +183 -0
- package/dist/lib/agent-decision/types.js +21 -0
- package/dist/lib/agent-decision/validate.js +216 -0
- package/dist/lib/analytics/capture.js +96 -0
- package/dist/lib/analytics/command-event.js +267 -0
- package/dist/lib/analytics/consent.js +58 -0
- package/dist/lib/analytics/coverage-gap.js +96 -0
- package/dist/lib/analytics/envelope.js +236 -0
- package/dist/lib/analytics/event-id.js +86 -0
- package/dist/lib/analytics/evidence.js +150 -0
- package/dist/lib/analytics/followthrough.js +194 -0
- package/dist/lib/analytics/forwarder.js +109 -0
- package/dist/lib/analytics/logs.js +78 -0
- package/dist/lib/analytics/metrics.js +78 -0
- package/dist/lib/analytics/recorder.js +92 -0
- package/dist/lib/analytics/review-analytics.js +75 -0
- package/dist/lib/analytics/sequence.js +77 -0
- package/dist/lib/analytics/store.js +131 -0
- package/dist/lib/analytics/turn-recap.js +279 -0
- package/dist/lib/artifact_id.js +108 -0
- package/dist/lib/auth-breaker.js +161 -0
- package/dist/lib/auto-index.js +112 -0
- package/dist/lib/classifier.js +88 -0
- package/dist/lib/config.js +298 -0
- package/dist/lib/conflict-advisory.js +64 -0
- package/dist/lib/debug-bundle.js +520 -0
- package/dist/lib/enrichment/ingest.js +301 -0
- package/dist/lib/enrichment/plan.js +253 -0
- package/dist/lib/enrichment/protocol.js +359 -0
- package/dist/lib/enrichment/scout-brief.js +176 -0
- package/dist/lib/failure-telemetry.js +444 -0
- package/dist/lib/git.js +200 -0
- package/dist/lib/governance-cache.js +77 -0
- package/dist/lib/governed-path-cache.js +76 -0
- package/dist/lib/http.js +677 -0
- package/dist/lib/identity-envelope.js +23 -0
- package/dist/lib/kb-candidate.js +65 -0
- package/dist/lib/kb_acl.js +98 -0
- package/dist/lib/login.js +353 -0
- package/dist/lib/mcp-fetchers.js +130 -0
- package/dist/lib/mcp-restart.js +47 -0
- package/dist/lib/observability.js +805 -0
- package/dist/lib/open-url.js +33 -0
- package/dist/lib/orphan-guard.js +70 -0
- package/dist/lib/packaged.js +21 -0
- package/dist/lib/reconcile-sessions.js +171 -0
- package/dist/lib/redactor.js +89 -0
- package/dist/lib/relationship-candidate-query.js +27 -0
- package/dist/lib/render.js +611 -0
- package/dist/lib/rules/applicability.js +64 -0
- package/dist/lib/rules/attest-code-rule-version.js +47 -0
- package/dist/lib/rules/attest-notes-location.js +217 -0
- package/dist/lib/rules/attest-rule-version.js +69 -0
- package/dist/lib/rules/canonical-json.js +97 -0
- package/dist/lib/rules/ce0-emit.js +64 -0
- package/dist/lib/rules/ce0-evidence.js +281 -0
- package/dist/lib/rules/ce0-recall-sample.js +82 -0
- package/dist/lib/rules/ce0-rule.js +55 -0
- package/dist/lib/rules/ce0-sampling-bucket.js +15 -0
- package/dist/lib/rules/ce0-store.js +683 -0
- package/dist/lib/rules/ce0-telemetry-project.js +93 -0
- package/dist/lib/rules/ce0-telemetry.js +158 -0
- package/dist/lib/rules/code-rule-registry.js +17 -0
- package/dist/lib/rules/command-match.js +185 -0
- package/dist/lib/rules/consult-evidence-binding.js +27 -0
- package/dist/lib/rules/consultation-capture-adapter.js +193 -0
- package/dist/lib/rules/content-match.js +56 -0
- package/dist/lib/rules/deny-admission.js +99 -0
- package/dist/lib/rules/durable-observation.js +190 -0
- package/dist/lib/rules/enforce-notes-version.js +421 -0
- package/dist/lib/rules/evaluation-input-hash.js +126 -0
- package/dist/lib/rules/evaluator.js +108 -0
- package/dist/lib/rules/inert-rule-families.js +51 -0
- package/dist/lib/rules/input-authority-resolver.js +241 -0
- package/dist/lib/rules/interception-schema.js +170 -0
- package/dist/lib/rules/interception-store.js +267 -0
- package/dist/lib/rules/live-input-authority.js +66 -0
- package/dist/lib/rules/local-matcher.js +108 -0
- package/dist/lib/rules/local-observe.js +79 -0
- package/dist/lib/rules/local-rule-version-repo.js +214 -0
- package/dist/lib/rules/memory-requirement.js +109 -0
- package/dist/lib/rules/notes-observe.js +39 -0
- package/dist/lib/rules/notes-path.js +261 -0
- package/dist/lib/rules/notes-rule.js +75 -0
- package/dist/lib/rules/observe-adapter.js +114 -0
- package/dist/lib/rules/observed-rule-hash.js +119 -0
- package/dist/lib/rules/prompt-submit-adapter.js +132 -0
- package/dist/lib/rules/requirement-subject.js +240 -0
- package/dist/lib/rules/rule-activity.js +67 -0
- package/dist/lib/rules/rule-version-hash.js +151 -0
- package/dist/lib/rules/runtime-scope.js +55 -0
- package/dist/lib/rules/stop-adapter.js +116 -0
- package/dist/lib/rules/stop-response-snapshot.js +174 -0
- package/dist/lib/rules/types.js +10 -0
- package/dist/lib/rules/ulid.js +46 -0
- package/dist/lib/rules/version-evaluation.js +156 -0
- package/dist/lib/scanner/agent-memory.js +99 -0
- package/dist/lib/scanner/bootstrap-summary.js +87 -0
- package/dist/lib/scanner/cache.js +59 -0
- package/dist/lib/scanner/frontmatter.js +42 -0
- package/dist/lib/scanner/parse-directives.js +69 -0
- package/dist/lib/scanner/parse-structured.js +72 -0
- package/dist/lib/scanner/render.js +73 -0
- package/dist/lib/scanner/scan.js +132 -0
- package/dist/lib/scanner/score.js +38 -0
- package/dist/lib/scanner/scout-mission.js +126 -0
- package/dist/lib/scanner/types.js +7 -0
- package/dist/lib/session-scope.js +195 -0
- package/dist/lib/spool.js +355 -0
- package/dist/lib/staleness.js +100 -0
- package/dist/lib/steer-cache.js +87 -0
- package/dist/lib/tagged-reference.js +20 -0
- package/dist/lib/temporal.js +109 -0
- package/dist/lib/turn-recap-emit.js +67 -0
- package/dist/lib/unwire.js +253 -0
- package/dist/lib/update-check.js +469 -0
- package/dist/lib/update-notifier.js +217 -0
- package/dist/lib/upgrade-apply.js +643 -0
- package/dist/lib/wire.js +1087 -0
- package/dist/lib/workspace.js +96 -0
- package/dist/lib/zip.js +154 -0
- package/dist/pretool-entry.js +37 -0
- 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
|
+
}
|