@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,301 @@
1
+ "use strict";
2
+ // `enrich ingest`: load the authoritative run record (the agent supplies only
3
+ // {runId, results}, never trusted plan data), re-verify it, then validate + persist each
4
+ // scout's candidates. Security-critical: realpath containment, exist-at-HEAD via the
5
+ // tracked set, and commit-allowlist membership all live here (plan §5, §5b, §6, §6b, §9).
6
+ //
7
+ // HTTP is injected as a Persister so this module is fully unit-testable without a live
8
+ // intel server; the command wires the real kb-add POST. The filesystem/git probe is
9
+ // likewise injectable, default-built from the repo root.
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.defaultProbe = defaultProbe;
12
+ exports.verifyCandidate = verifyCandidate;
13
+ exports.renderCandidateDocument = renderCandidateDocument;
14
+ exports.statePath = statePath;
15
+ exports.loadState = loadState;
16
+ exports.writeState = writeState;
17
+ exports.ingestRun = ingestRun;
18
+ const node_fs_1 = require("node:fs");
19
+ const node_path_1 = require("node:path");
20
+ const protocol_1 = require("./protocol");
21
+ const plan_1 = require("./plan");
22
+ const SCOUT_SLOTS = ["documentation", "history"];
23
+ function safeRealpath(p) {
24
+ try {
25
+ return (0, node_fs_1.realpathSync)(p);
26
+ }
27
+ catch {
28
+ return p;
29
+ }
30
+ }
31
+ function defaultProbe(repoRoot, gitRunner = (0, plan_1.defaultGitRunner)(repoRoot)) {
32
+ const repoRealpath = safeRealpath(repoRoot);
33
+ let tracked = null;
34
+ return {
35
+ repoRealpath,
36
+ realpath: (absPath) => (0, node_fs_1.realpathSync)(absPath),
37
+ lineCount: (absPath) => (0, node_fs_1.readFileSync)(absPath, "utf8").split("\n").length,
38
+ isTracked: (relPath) => {
39
+ if (!tracked) {
40
+ try {
41
+ tracked = new Set(gitRunner(["ls-files"])
42
+ .split("\n")
43
+ .map((l) => l.trim())
44
+ .filter(Boolean));
45
+ }
46
+ catch {
47
+ tracked = new Set();
48
+ }
49
+ }
50
+ return tracked.has(relPath);
51
+ },
52
+ };
53
+ }
54
+ // --- impure candidate verification (shape already validated upstream) -------------
55
+ function verifyFileEvidence(ev, probe, push) {
56
+ const raw = ev.path.trim();
57
+ if ((0, node_path_1.isAbsolute)(raw)) {
58
+ push("path_traversal", `file path must be repo-relative: ${raw}`);
59
+ return;
60
+ }
61
+ const norm = raw.replace(/\\/g, "/").replace(/^\.\//, "");
62
+ if (norm.split("/").includes("..")) {
63
+ push("path_traversal", `file path may not contain "..": ${raw}`);
64
+ return;
65
+ }
66
+ if (!probe.isTracked(norm)) {
67
+ push("untracked_path", `file is not tracked at HEAD: ${norm}`);
68
+ return;
69
+ }
70
+ const abs = (0, node_path_1.join)(probe.repoRealpath, norm);
71
+ let real;
72
+ try {
73
+ real = probe.realpath(abs);
74
+ }
75
+ catch {
76
+ push("missing_file", `file does not exist: ${norm}`);
77
+ return;
78
+ }
79
+ if (real !== probe.repoRealpath && !real.startsWith(probe.repoRealpath + node_path_1.sep)) {
80
+ push("escapes_repo", `file resolves outside the repository: ${norm}`);
81
+ return;
82
+ }
83
+ let lines;
84
+ try {
85
+ lines = probe.lineCount(real);
86
+ }
87
+ catch {
88
+ push("missing_file", `file is unreadable: ${norm}`);
89
+ return;
90
+ }
91
+ if (ev.endLine > lines) {
92
+ push("line_out_of_range", `endLine ${ev.endLine} exceeds file length ${lines}: ${norm}`);
93
+ }
94
+ }
95
+ // Verifies a single shape-valid candidate against the filesystem + commit allowlist.
96
+ // Rejects the whole candidate if ANY anchor fails (a citation is only as trustworthy as
97
+ // its weakest anchor). Returns all errors for reporting.
98
+ function verifyCandidate(candidate, run, probe, index) {
99
+ const errors = [];
100
+ const push = (code, message) => {
101
+ errors.push({ index, code, message });
102
+ };
103
+ const allowlist = (0, protocol_1.commitAllowlist)(run);
104
+ for (const ev of candidate.evidence) {
105
+ if (ev.type === "file") {
106
+ verifyFileEvidence(ev, probe, push);
107
+ }
108
+ else if ((0, protocol_1.resolveAllowedCommit)(allowlist, ev.commit) === null) {
109
+ push("commit_not_in_allowlist", `commit is not in the plan's allowlist: ${ev.commit}`);
110
+ }
111
+ }
112
+ return errors;
113
+ }
114
+ // --- candidate -> governed document ----------------------------------------------
115
+ function renderCandidateDocument(candidate) {
116
+ const lines = [];
117
+ lines.push(candidate.statement.trim());
118
+ lines.push("");
119
+ lines.push(`Kind: ${candidate.kind}. Source: ${candidate.sourceScout} scout (onboarding enrichment, advisory; pending human review).`);
120
+ lines.push("");
121
+ lines.push("## Evidence");
122
+ for (const ev of candidate.evidence) {
123
+ if (ev.type === "file") {
124
+ lines.push(`- \`${ev.path}\` lines ${ev.startLine}-${ev.endLine}`);
125
+ }
126
+ else {
127
+ lines.push(`- commit \`${ev.commit}\`${ev.path ? ` (\`${ev.path}\`)` : ""}`);
128
+ }
129
+ }
130
+ return lines.join("\n") + "\n";
131
+ }
132
+ // --- per-scout state persistence (§6) --------------------------------------------
133
+ function statePath(home, workspaceId) {
134
+ return (0, node_path_1.join)(home, "workspaces", workspaceId, "onboarding-state.json");
135
+ }
136
+ function loadState(home, workspaceId) {
137
+ const path = statePath(home, workspaceId);
138
+ if (!(0, node_fs_1.existsSync)(path))
139
+ return null;
140
+ try {
141
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
142
+ if (parsed?.schemaVersion !== 1)
143
+ return null;
144
+ return parsed;
145
+ }
146
+ catch {
147
+ return null;
148
+ }
149
+ }
150
+ function writeState(home, state) {
151
+ const dir = (0, node_path_1.join)(home, "workspaces", state.workspaceId);
152
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
153
+ (0, node_fs_1.writeFileSync)(statePath(home, state.workspaceId), JSON.stringify(state, null, 2), "utf8");
154
+ }
155
+ function emptyScoutState() {
156
+ return { status: "not_started" };
157
+ }
158
+ // --- orchestration ---------------------------------------------------------------
159
+ async function ingestRun(input) {
160
+ const { env, request, persist, now } = input;
161
+ const envelope = (0, protocol_1.validateIngestRequestShape)(request);
162
+ if (!envelope.ok)
163
+ return { ok: false, rejectionReason: envelope.error, outcomes: [] };
164
+ const { runId, results } = envelope.request;
165
+ const run = (0, plan_1.loadRunRecord)(env.home, env.workspaceId, runId);
166
+ if (!run)
167
+ return { ok: false, rejectionReason: `unknown run: ${runId}`, outcomes: [], runId };
168
+ if (run.workspaceId !== env.workspaceId) {
169
+ return { ok: false, rejectionReason: "run record workspace mismatch", outcomes: [], runId };
170
+ }
171
+ if (safeRealpath(run.repositoryRoot) !== safeRealpath(env.repositoryRoot)) {
172
+ return { ok: false, rejectionReason: "run record repository mismatch", outcomes: [], runId };
173
+ }
174
+ if ((0, protocol_1.computePlanDigest)(run) !== run.planDigest) {
175
+ return { ok: false, rejectionReason: "plan digest mismatch (run record corrupt)", outcomes: [], runId };
176
+ }
177
+ const probe = input.probe ?? defaultProbe(env.repositoryRoot, input.gitRunner);
178
+ // Resume: a scout already "complete" is never re-processed (its candidates are
179
+ // immutable; §6). Carry prior state forward.
180
+ const prior = loadState(env.home, env.workspaceId);
181
+ const scoutState = {
182
+ documentation: prior?.scouts.documentation ?? emptyScoutState(),
183
+ history: prior?.scouts.history ?? emptyScoutState(),
184
+ };
185
+ const outcomes = [];
186
+ let totalAccepted = 0;
187
+ const cap = run.limits.maxCandidatesTotal;
188
+ for (const rawResult of results) {
189
+ const shape = (0, protocol_1.validateScoutResultShape)(rawResult);
190
+ if (!shape.ok) {
191
+ // Try to attribute a malformed envelope to a slot for retry; else surface loose.
192
+ const guessed = guessScoutName(rawResult);
193
+ if (guessed)
194
+ scoutState[guessed] = { status: "malformed", error: shape.error };
195
+ outcomes.push({
196
+ scout: guessed ?? "documentation",
197
+ received: 0,
198
+ accepted: 0,
199
+ rejected: 0,
200
+ persisted: 0,
201
+ errors: [{ index: -1, code: "malformed_envelope", message: shape.error }],
202
+ });
203
+ continue;
204
+ }
205
+ const result = shape.result;
206
+ const scout = result.scout;
207
+ if (scoutState[scout].status === "complete") {
208
+ outcomes.push({
209
+ scout,
210
+ received: 0,
211
+ accepted: 0,
212
+ rejected: 0,
213
+ persisted: 0,
214
+ errors: [{ index: -1, code: "already_complete", message: "scout already complete; skipped" }],
215
+ });
216
+ continue;
217
+ }
218
+ // The agent reports the scout did not finish: record it, persist nothing (rerun
219
+ // re-runs it). Avoids partial-persist duplication for unfinished scouts.
220
+ if (result.status !== "complete") {
221
+ scoutState[scout] = { status: result.status, error: result.error };
222
+ outcomes.push({
223
+ scout,
224
+ received: result.candidates.length,
225
+ accepted: 0,
226
+ rejected: 0,
227
+ persisted: 0,
228
+ errors: [{ index: -1, code: result.status, message: result.error ?? `scout ${result.status}` }],
229
+ });
230
+ continue;
231
+ }
232
+ // Complete + valid envelope: validate each candidate independently.
233
+ const accepted = [];
234
+ const errors = [];
235
+ result.candidates.forEach((raw, i) => {
236
+ if (totalAccepted + accepted.length >= cap) {
237
+ errors.push({ index: i, code: "candidate_cap_exceeded", message: `run candidate cap (${cap}) reached` });
238
+ return;
239
+ }
240
+ const shapeRes = (0, protocol_1.validateCandidateShape)(raw, i);
241
+ if (!shapeRes.ok) {
242
+ errors.push(...shapeRes.errors);
243
+ return;
244
+ }
245
+ const verifyErrors = verifyCandidate(shapeRes.candidate, run, probe, i);
246
+ if (verifyErrors.length > 0) {
247
+ errors.push(...verifyErrors);
248
+ return;
249
+ }
250
+ accepted.push(shapeRes.candidate);
251
+ });
252
+ // Dedup identical candidates (same id -> same path) before POST.
253
+ const docsByPath = new Map();
254
+ for (const c of accepted) {
255
+ const relPath = (0, protocol_1.candidateRelPath)(c);
256
+ if (!docsByPath.has(relPath))
257
+ docsByPath.set(relPath, { relPath, content: renderCandidateDocument(c) });
258
+ }
259
+ const docs = [...docsByPath.values()];
260
+ let persisted = 0;
261
+ let status = "complete";
262
+ if (docs.length > 0) {
263
+ try {
264
+ persisted = (await persist(docs)).persisted;
265
+ }
266
+ catch (e) {
267
+ status = "persistence_failed";
268
+ errors.push({ index: -1, code: "persistence_failed", message: e instanceof Error ? e.message : String(e) });
269
+ }
270
+ }
271
+ totalAccepted += accepted.length;
272
+ scoutState[scout] =
273
+ status === "complete" ? { status: "complete", candidateCount: accepted.length } : { status, error: "kb-add persistence failed" };
274
+ outcomes.push({
275
+ scout,
276
+ received: result.candidates.length,
277
+ accepted: accepted.length,
278
+ rejected: result.candidates.length - accepted.length,
279
+ persisted,
280
+ errors,
281
+ });
282
+ }
283
+ const allComplete = SCOUT_SLOTS.every((s) => scoutState[s].status === "complete");
284
+ const state = {
285
+ workspaceId: env.workspaceId,
286
+ schemaVersion: 1,
287
+ status: allComplete ? "complete" : "partial",
288
+ updatedAt: now,
289
+ scouts: { documentation: scoutState.documentation, history: scoutState.history },
290
+ };
291
+ writeState(env.home, state);
292
+ return { ok: true, runId, outcomes, state };
293
+ }
294
+ function guessScoutName(raw) {
295
+ if (raw && typeof raw === "object" && "scout" in raw) {
296
+ const s = raw.scout;
297
+ if (s === "documentation" || s === "history")
298
+ return s;
299
+ }
300
+ return null;
301
+ }
@@ -0,0 +1,253 @@
1
+ "use strict";
2
+ // `enrich plan` builder: turns the local repo into an immutable OnboardingRun record.
3
+ // Three jobs (plan §5b, §8, §14): (1) rank documentation targets the doc scout should
4
+ // read; (2) prepare a bounded git-history allowlist + context for the history scout;
5
+ // (3) assemble + digest + persist the run record, pruning stale ones.
6
+ //
7
+ // The nondeterministic dependency (git) is injected as a GitRunner so the parsing and
8
+ // byte-bounding logic is unit-testable without a real repo, mirroring the scanner's
9
+ // injected-clock idiom. Identity/digest math lives in protocol.ts.
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.defaultGitRunner = defaultGitRunner;
12
+ exports.buildDocumentationTargets = buildDocumentationTargets;
13
+ exports.prepareGitEvidence = prepareGitEvidence;
14
+ exports.buildOnboardingRun = buildOnboardingRun;
15
+ exports.runsDir = runsDir;
16
+ exports.runRecordPath = runRecordPath;
17
+ exports.writeRunRecord = writeRunRecord;
18
+ exports.loadRunRecord = loadRunRecord;
19
+ exports.pruneOldRuns = pruneOldRuns;
20
+ exports.createPlan = createPlan;
21
+ const node_child_process_1 = require("node:child_process");
22
+ const node_fs_1 = require("node:fs");
23
+ const node_path_1 = require("node:path");
24
+ const score_1 = require("../scanner/score");
25
+ const protocol_1 = require("./protocol");
26
+ // Defensive per-commit caps (not pinned by §8, which bounds the total). Conservative.
27
+ const MAX_BODY_CHARS = 1000;
28
+ const MAX_CHANGED_FILES_PER_COMMIT = 100;
29
+ // Sentinels for single-pass `git log` parsing. Chosen to be vanishingly unlikely to
30
+ // appear at the start of a commit-message line; a collision would garble at most one
31
+ // commit (bounded, all downstream candidates are PENDING + human-rejectable anyway).
32
+ const COMMIT_MARK = "@@MLA-ENRICH-COMMIT@@";
33
+ const META_END_MARK = "@@MLA-ENRICH-ENDMETA@@";
34
+ function defaultGitRunner(repoRoot) {
35
+ return (args) => (0, node_child_process_1.execFileSync)("git", args, { cwd: repoRoot, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
36
+ }
37
+ const TIER_RANK = { T1: 0, T2: 1, T3: 99, T4: 2 };
38
+ // Rank the doc targets the documentation scout should read: T1 instruction files first,
39
+ // then T2 decision docs, then T4 legacy notes; within a tier, deterministic by path.
40
+ // T3 (grounding-only) and unclassified files are excluded. Capped to the limit.
41
+ function buildDocumentationTargets(repoRoot, limit, gitRunner = defaultGitRunner(repoRoot)) {
42
+ let tracked;
43
+ try {
44
+ tracked = gitRunner(["ls-files"])
45
+ .split("\n")
46
+ .map((l) => l.trim())
47
+ .filter(Boolean);
48
+ }
49
+ catch {
50
+ return []; // not a git repo / git unavailable: no targets, never throw
51
+ }
52
+ const scored = [];
53
+ for (const path of tracked) {
54
+ const tier = (0, score_1.classifyTier)(path);
55
+ if (!tier || tier === "T3")
56
+ continue;
57
+ scored.push({ path, tier });
58
+ }
59
+ scored.sort((a, b) => TIER_RANK[a.tier] - TIER_RANK[b.tier] || a.path.localeCompare(b.path));
60
+ return scored.slice(0, Math.max(0, limit)).map((s, i) => ({ path: s.path, tier: s.tier, rank: i + 1 }));
61
+ }
62
+ // Prepare a bounded slice of recent git history: the commit allowlist (full SHAs) plus
63
+ // enough context (subject, bounded body, changed paths + statuses, rename info) for the
64
+ // history scout to distil decisions. No raw unbounded logs (§8). Merge commits are
65
+ // skipped: their diffs are mechanical noise; authored commits carry the decisions.
66
+ // A bounded diff excerpt is intentionally omitted in the MVP (the scout reads files at
67
+ // HEAD); it is a future toggle.
68
+ function prepareGitEvidence(repoRoot, opts) {
69
+ const gitRunner = opts.gitRunner ?? defaultGitRunner(repoRoot);
70
+ let raw;
71
+ try {
72
+ raw = gitRunner([
73
+ "log",
74
+ `-n`,
75
+ String(Math.max(0, opts.maxCommits)),
76
+ "--no-merges",
77
+ "--date=iso-strict",
78
+ "--name-status",
79
+ `--pretty=format:${COMMIT_MARK}%n%H%n%cI%n%s%n%b%n${META_END_MARK}`,
80
+ ]);
81
+ }
82
+ catch {
83
+ return { evidence: [], truncated: false }; // empty history / not a repo: no evidence
84
+ }
85
+ const parsed = parseGitLog(raw);
86
+ const evidence = [];
87
+ let bytes = 0;
88
+ let truncated = false;
89
+ for (const commit of parsed) {
90
+ if (evidence.length >= opts.maxCommits) {
91
+ truncated = true;
92
+ break;
93
+ }
94
+ const size = Buffer.byteLength(JSON.stringify(commit), "utf8");
95
+ if (bytes + size > opts.maxBytes && evidence.length > 0) {
96
+ truncated = true;
97
+ break; // keep at least one commit even if it alone exceeds the byte budget
98
+ }
99
+ bytes += size;
100
+ evidence.push(commit);
101
+ }
102
+ if (parsed.length > evidence.length)
103
+ truncated = true;
104
+ return { evidence, truncated };
105
+ }
106
+ // Single-pass parser for the sentinel-framed `git log --name-status` output above.
107
+ function parseGitLog(raw) {
108
+ const lines = raw.split("\n");
109
+ const out = [];
110
+ let i = 0;
111
+ while (i < lines.length) {
112
+ if (lines[i] !== COMMIT_MARK) {
113
+ i++;
114
+ continue;
115
+ }
116
+ // header block: hash, committer ISO, subject, then body until META_END_MARK
117
+ const commit = (lines[i + 1] ?? "").trim();
118
+ const timestamp = (lines[i + 2] ?? "").trim();
119
+ const subject = lines[i + 3] ?? "";
120
+ i += 4;
121
+ const bodyLines = [];
122
+ while (i < lines.length && lines[i] !== META_END_MARK && lines[i] !== COMMIT_MARK) {
123
+ bodyLines.push(lines[i]);
124
+ i++;
125
+ }
126
+ if (lines[i] === META_END_MARK)
127
+ i++; // consume the end marker
128
+ // name-status lines until the next commit marker (or EOF); skip blanks
129
+ const changedFiles = [];
130
+ while (i < lines.length && lines[i] !== COMMIT_MARK) {
131
+ const line = lines[i];
132
+ i++;
133
+ if (!line.trim())
134
+ continue;
135
+ if (changedFiles.length >= MAX_CHANGED_FILES_PER_COMMIT)
136
+ continue;
137
+ const parts = line.split("\t");
138
+ const status = parts[0]?.trim();
139
+ if (!status)
140
+ continue;
141
+ if (/^[RC]/.test(status) && parts.length >= 3) {
142
+ changedFiles.push({ path: parts[2], status, renamedFrom: parts[1] });
143
+ }
144
+ else if (parts.length >= 2) {
145
+ changedFiles.push({ path: parts[1], status });
146
+ }
147
+ }
148
+ if (!commit)
149
+ continue; // guard against a garbled record
150
+ out.push({
151
+ commit: commit.toLowerCase(),
152
+ timestamp,
153
+ subject,
154
+ body: bodyLines.join("\n").trim().slice(0, MAX_BODY_CHARS),
155
+ changedFiles,
156
+ });
157
+ }
158
+ return out;
159
+ }
160
+ // Assemble the run record: compute the deadline from the injected clock + budget and the
161
+ // plan digest over the integrity-bearing content. Pure (clock + runId injected).
162
+ function buildOnboardingRun(input) {
163
+ const limits = input.limits ?? (0, protocol_1.defaultLimits)();
164
+ const deadlineAt = new Date(Date.parse(input.now) + limits.budgetMs).toISOString();
165
+ const partial = {
166
+ protocolVersion: protocol_1.PROTOCOL_VERSION,
167
+ workspaceId: input.workspaceId,
168
+ repositoryRoot: input.repositoryRoot,
169
+ limits,
170
+ documentationTargets: input.documentationTargets,
171
+ historyEvidence: input.historyEvidence,
172
+ };
173
+ return {
174
+ ...partial,
175
+ runId: input.runId,
176
+ createdAt: input.now,
177
+ deadlineAt,
178
+ planDigest: (0, protocol_1.computePlanDigest)(partial),
179
+ };
180
+ }
181
+ // --- Persistence: ~/.meetless/workspaces/<ws>/onboarding-runs/<runId>.json ---------
182
+ function runsDir(home, workspaceId) {
183
+ return (0, node_path_1.join)(home, "workspaces", workspaceId, "onboarding-runs");
184
+ }
185
+ function runRecordPath(home, workspaceId, runId) {
186
+ return (0, node_path_1.join)(runsDir(home, workspaceId), `${runId}.json`);
187
+ }
188
+ function writeRunRecord(home, run) {
189
+ const dir = runsDir(home, run.workspaceId);
190
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
191
+ const path = runRecordPath(home, run.workspaceId, run.runId);
192
+ (0, node_fs_1.writeFileSync)(path, JSON.stringify(run, null, 2), "utf8");
193
+ return path;
194
+ }
195
+ function loadRunRecord(home, workspaceId, runId) {
196
+ const path = runRecordPath(home, workspaceId, runId);
197
+ if (!(0, node_fs_1.existsSync)(path))
198
+ return null;
199
+ try {
200
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
201
+ if (parsed?.protocolVersion !== protocol_1.PROTOCOL_VERSION || parsed.runId !== runId)
202
+ return null;
203
+ return parsed;
204
+ }
205
+ catch {
206
+ return null;
207
+ }
208
+ }
209
+ // Keep only the current active run record; drop older ones (§5b: no run-history
210
+ // retention). runId collisions are impossible (random), so "older" == "any other".
211
+ function pruneOldRuns(home, workspaceId, currentRunId) {
212
+ const dir = runsDir(home, workspaceId);
213
+ if (!(0, node_fs_1.existsSync)(dir))
214
+ return 0;
215
+ let removed = 0;
216
+ for (const name of (0, node_fs_1.readdirSync)(dir)) {
217
+ if (!name.endsWith(".json") || name === `${currentRunId}.json`)
218
+ continue;
219
+ try {
220
+ (0, node_fs_1.unlinkSync)((0, node_path_1.join)(dir, name));
221
+ removed++;
222
+ }
223
+ catch {
224
+ // best-effort cleanup; a leftover record is harmless (ingest loads by runId)
225
+ }
226
+ }
227
+ return removed;
228
+ }
229
+ // Orchestration helper used by the `enrich plan` command: scan the repo into targets +
230
+ // git evidence, assemble + persist the record, prune stale ones. Returns the run record
231
+ // (the command prints it as the plan envelope).
232
+ function createPlan(input) {
233
+ const limits = (0, protocol_1.defaultLimits)(input.budgetMs);
234
+ const gitRunner = input.gitRunner ?? defaultGitRunner(input.repositoryRoot);
235
+ const documentationTargets = buildDocumentationTargets(input.repositoryRoot, limits.maxDocumentTargets, gitRunner);
236
+ const { evidence: historyEvidence, truncated: historyTruncated } = prepareGitEvidence(input.repositoryRoot, {
237
+ maxCommits: limits.maxHistoryCommits,
238
+ maxBytes: limits.maxPreparedInputBytes,
239
+ gitRunner,
240
+ });
241
+ const run = buildOnboardingRun({
242
+ runId: input.runId,
243
+ workspaceId: input.workspaceId,
244
+ repositoryRoot: input.repositoryRoot,
245
+ now: input.now,
246
+ limits,
247
+ documentationTargets,
248
+ historyEvidence,
249
+ });
250
+ const recordPath = writeRunRecord(input.home, run);
251
+ const pruned = pruneOldRuns(input.home, input.workspaceId, input.runId);
252
+ return { run, recordPath, pruned, historyTruncated };
253
+ }