@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,681 @@
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.parseKbAddArgs = parseKbAddArgs;
37
+ exports.gitRootForVault = gitRootForVault;
38
+ exports.resolveVaultRoot = resolveVaultRoot;
39
+ exports.vaultRelPath = vaultRelPath;
40
+ exports.readCorpusMarker = readCorpusMarker;
41
+ exports.globFiles = globFiles;
42
+ exports.enumerateDocuments = enumerateDocuments;
43
+ exports.pollReceiptsToTerminal = pollReceiptsToTerminal;
44
+ exports.runKbAdd = runKbAdd;
45
+ const fs = __importStar(require("fs"));
46
+ const os = __importStar(require("os"));
47
+ const path = __importStar(require("path"));
48
+ const crypto_1 = require("crypto");
49
+ const config_1 = require("../lib/config");
50
+ const http_1 = require("../lib/http");
51
+ const kb_acl_1 = require("../lib/kb_acl");
52
+ const render_1 = require("../lib/render");
53
+ const open_url_1 = require("../lib/open-url");
54
+ const workspace_1 = require("../lib/workspace");
55
+ const governed_path_cache_1 = require("../lib/governed-path-cache");
56
+ const failure_telemetry_1 = require("../lib/failure-telemetry");
57
+ const observability_1 = require("../lib/observability");
58
+ // `mla kb add <path> --mode file|corpus --provenance <kind> [flags]`
59
+ // (proposal §4.1).
60
+ //
61
+ // Remote-capable: this command POSTs the note bodies to the intel route
62
+ // `POST /internal/v1/kb/add`, which owns the governed ingestion front door
63
+ // (intake_delivery -> execute_run_set -> activation CAS head swap) and the
64
+ // server-authoritative canonical identity. The CLI no longer spawns a local
65
+ // python subprocess or needs an intel checkout on the operator's machine, so
66
+ // seeding works from any laptop against any backend (local dogfood or staging)
67
+ // the same way every other `mla` command does (INV-OSS-1).
68
+ //
69
+ // Split of responsibilities:
70
+ // - CLIENT (here, holds the filesystem): strict argv parsing, owner-only ACL
71
+ // pre-flight, path-existence + mode/dir guards, vault-root resolution
72
+ // (MEETLESS_NOTES_ROOT -> git-repo walk-up; corpus mode = the corpus
73
+ // folder), corpus-marker (`.meetless-kb-corpus.json`) read + glob
74
+ // enumeration, content reads, and the vault-relative POSIX path per doc.
75
+ // - SERVER (intel route): prefixes the single `notes/` root, runs the PURE
76
+ // canonicalizer (NFC + casefold) to reproduce exactly what the local
77
+ // `notes_external_object_id` computes (so HTTP-seeded and locally-seeded
78
+ // docs dedup against each other), mints/dedups the revision, runs the heavy
79
+ // LDM body inline, and returns the KbAddReceipt array `render.ts` consumes.
80
+ //
81
+ // The receipt tail (sync-extract poll, governed-path cache, Console URL stamp,
82
+ // `--open`) is unchanged: it operates on the receipts the route returns exactly
83
+ // as it did on the receipts the worker used to print.
84
+ const CORPUS_MARKER = ".meetless-kb-corpus.json";
85
+ const DEFAULT_GLOB = "*.md";
86
+ const DEFAULT_PROFILE = "markdown_atomic_v1";
87
+ const VALUE_FLAGS = new Set([
88
+ "--mode",
89
+ "--provenance",
90
+ "--workspace",
91
+ "--profile",
92
+ "--glob",
93
+ "--ingest-run-id",
94
+ "--agent-session",
95
+ ]);
96
+ const BOOLEAN_FLAGS = new Set(["--allow-provenance-change", "--queue", "--open", "--reingest-if-active"]);
97
+ function parseKbAddArgs(argv) {
98
+ const out = {
99
+ allowProvenanceChange: false,
100
+ queue: false,
101
+ open: false,
102
+ reingestIfActive: false,
103
+ };
104
+ let positional = null;
105
+ for (let i = 0; i < argv.length; i++) {
106
+ const a = argv[i];
107
+ if (VALUE_FLAGS.has(a)) {
108
+ const v = argv[i + 1];
109
+ if (v === undefined) {
110
+ throw new Error(`Missing value for ${a}`);
111
+ }
112
+ if (v.startsWith("--") || v.startsWith("-")) {
113
+ throw new Error(`Missing value for ${a} (got the next flag ${v} instead)`);
114
+ }
115
+ switch (a) {
116
+ case "--mode":
117
+ if (v !== "file" && v !== "corpus") {
118
+ throw new Error(`--mode must be 'file' or 'corpus' (got '${v}')`);
119
+ }
120
+ out.mode = v;
121
+ break;
122
+ case "--provenance":
123
+ out.provenance = v;
124
+ break;
125
+ case "--workspace":
126
+ out.workspace = v;
127
+ break;
128
+ case "--profile":
129
+ out.profile = v;
130
+ break;
131
+ case "--glob":
132
+ out.glob = v;
133
+ break;
134
+ case "--ingest-run-id":
135
+ out.ingestRunId = v;
136
+ break;
137
+ case "--agent-session":
138
+ out.agentSession = v;
139
+ break;
140
+ }
141
+ i += 1;
142
+ continue;
143
+ }
144
+ if (BOOLEAN_FLAGS.has(a)) {
145
+ if (a === "--allow-provenance-change")
146
+ out.allowProvenanceChange = true;
147
+ else if (a === "--queue")
148
+ out.queue = true;
149
+ else if (a === "--open")
150
+ out.open = true;
151
+ else if (a === "--reingest-if-active")
152
+ out.reingestIfActive = true;
153
+ continue;
154
+ }
155
+ if (a.startsWith("--") || a.startsWith("-")) {
156
+ throw new Error(`Unknown flag: ${a}. Supported flags: ${[...VALUE_FLAGS, ...BOOLEAN_FLAGS].sort().join(", ")}`);
157
+ }
158
+ if (positional !== null) {
159
+ throw new Error(`\`mla kb add\` takes exactly one positional path (got '${positional}' and '${a}')`);
160
+ }
161
+ positional = a;
162
+ }
163
+ if (positional === null) {
164
+ throw new Error("`mla kb add` requires a positional <path>");
165
+ }
166
+ if (!out.mode) {
167
+ throw new Error("--mode file|corpus is required");
168
+ }
169
+ if (!out.provenance) {
170
+ throw new Error("--provenance <kind> is required");
171
+ }
172
+ return {
173
+ path: positional,
174
+ mode: out.mode,
175
+ provenance: out.provenance,
176
+ workspace: out.workspace,
177
+ profile: out.profile,
178
+ glob: out.glob,
179
+ ingestRunId: out.ingestRunId,
180
+ agentSession: out.agentSession,
181
+ allowProvenanceChange: !!out.allowProvenanceChange,
182
+ queue: !!out.queue,
183
+ open: !!out.open,
184
+ reingestIfActive: !!out.reingestIfActive,
185
+ };
186
+ }
187
+ // ---------------------------------------------------------------------------
188
+ // Vault-root resolution + enumeration (client-side; was tools/mla_kb_add.py)
189
+ //
190
+ // The governed identity is the vault-relative POSIX path under a single
191
+ // `notes/` root. The CLIENT alone holds the filesystem, so it resolves the
192
+ // vault root and computes each file's relative path here, exactly mirroring the
193
+ // python worker's `_resolve_vault_root` / `_enumerate_files` /
194
+ // `notes_external_object_id`, then ships `{relPath, content}` to the route. The
195
+ // SERVER prefixes `notes/` and canonicalizes, so the externalObjectId matches
196
+ // the locally-seeded one byte-for-byte (dedup parity).
197
+ // ---------------------------------------------------------------------------
198
+ function expandHome(p) {
199
+ if (p === "~")
200
+ return os.homedir();
201
+ if (p.startsWith("~/"))
202
+ return path.join(os.homedir(), p.slice(2));
203
+ return p;
204
+ }
205
+ // Walk up from `start` looking for a `.git` entry; return the containing dir.
206
+ // Mirrors the worker's `_git_root_for` (first/closest match wins).
207
+ function gitRootForVault(start) {
208
+ let cur;
209
+ try {
210
+ cur = fs.realpathSync(start);
211
+ }
212
+ catch {
213
+ cur = path.resolve(start);
214
+ }
215
+ for (;;) {
216
+ if (fs.existsSync(path.join(cur, ".git")))
217
+ return cur;
218
+ const parent = path.dirname(cur);
219
+ if (parent === cur)
220
+ return null;
221
+ cur = parent;
222
+ }
223
+ }
224
+ // Resolve the notes vault root the governed identity is relative to.
225
+ // Order (mirrors the worker, minus the removed `--vault-root` flag): corpus
226
+ // folder (corpus mode), else MEETLESS_NOTES_ROOT, else a git-repo-root walk-up
227
+ // from the file's directory. `resolvedPath` is the absolute target.
228
+ function resolveVaultRoot(flags, resolvedPath) {
229
+ if (flags.mode === "corpus") {
230
+ return fs.realpathSync(resolvedPath);
231
+ }
232
+ const envRoot = process.env.MEETLESS_NOTES_ROOT;
233
+ if (envRoot) {
234
+ const expanded = path.resolve(expandHome(envRoot));
235
+ if (!fs.existsSync(expanded) || !fs.statSync(expanded).isDirectory()) {
236
+ throw new Error(`MEETLESS_NOTES_ROOT=${envRoot} is not a directory`);
237
+ }
238
+ return fs.realpathSync(expanded);
239
+ }
240
+ const anchor = path.dirname(resolvedPath);
241
+ const gitRoot = gitRootForVault(anchor);
242
+ if (gitRoot)
243
+ return gitRoot;
244
+ throw new Error("could not resolve a notes vault root for the governed identity; set MEETLESS_NOTES_ROOT or run inside a git repo");
245
+ }
246
+ // The vault-relative POSIX path for `file`, validated to live INSIDE the vault.
247
+ // The server prefixes `notes/` + canonicalizes this. Mirrors the worker's
248
+ // `notes_external_object_id`, which raises when the file escapes the vault.
249
+ function vaultRelPath(vaultRoot, file) {
250
+ const root = fs.realpathSync(vaultRoot);
251
+ const f = fs.realpathSync(file);
252
+ const rel = path.relative(root, f);
253
+ if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) {
254
+ throw new Error(`file ${f} is not inside the notes vault root ${root}`);
255
+ }
256
+ return rel.split(path.sep).join("/");
257
+ }
258
+ // Read + validate `.meetless-kb-corpus.json`. Mirrors the worker's
259
+ // `_read_corpus_marker`: the marker pins the corpus to one workspace and may
260
+ // carry an allowedGlob / allowedProvenance guardrail.
261
+ function readCorpusMarker(folder, workspaceId) {
262
+ const markerPath = path.join(folder, CORPUS_MARKER);
263
+ if (!fs.existsSync(markerPath) || !fs.statSync(markerPath).isFile()) {
264
+ throw new Error(`corpus mode requires ${markerPath}; create one with the workspaceId and an optional allowedGlob / allowedProvenance guardrail`);
265
+ }
266
+ let raw;
267
+ try {
268
+ raw = JSON.parse(fs.readFileSync(markerPath, "utf8"));
269
+ }
270
+ catch (e) {
271
+ throw new Error(`${markerPath}: invalid JSON (${e.message})`);
272
+ }
273
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
274
+ throw new Error(`${markerPath}: marker must be a JSON object`);
275
+ }
276
+ const obj = raw;
277
+ if (obj.workspaceId !== workspaceId) {
278
+ throw new Error(`${markerPath}: workspaceId=${JSON.stringify(obj.workspaceId)} does NOT match --workspace (${JSON.stringify(workspaceId)}); the marker pins the corpus to one workspace`);
279
+ }
280
+ const allowedGlob = obj.allowedGlob ?? null;
281
+ if (allowedGlob !== null && typeof allowedGlob !== "string") {
282
+ throw new Error(`${markerPath}: allowedGlob must be a string`);
283
+ }
284
+ const allowedProvenance = obj.allowedProvenance ?? null;
285
+ if (allowedProvenance !== null && !Array.isArray(allowedProvenance)) {
286
+ throw new Error(`${markerPath}: allowedProvenance must be a list of strings`);
287
+ }
288
+ return {
289
+ workspaceId,
290
+ corpusName: (typeof obj.corpusName === "string" && obj.corpusName) || path.basename(folder),
291
+ allowedGlob: allowedGlob,
292
+ allowedProvenance: allowedProvenance,
293
+ };
294
+ }
295
+ function segmentToRegex(seg) {
296
+ let re = "";
297
+ for (const ch of seg) {
298
+ if (ch === "*")
299
+ re += "[^/]*";
300
+ else if (ch === "?")
301
+ re += "[^/]";
302
+ else
303
+ re += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
304
+ }
305
+ return new RegExp(`^${re}$`);
306
+ }
307
+ // Enumerate files under `root` matching a glob, mirroring python `Path.glob`:
308
+ // `**` matches zero-or-more directory segments, `*`/`?` match within a single
309
+ // segment, and (Unix-glob convention) a `*` segment skips dotfiles. Returns
310
+ // absolute file paths, deduped + sorted (matching the worker's `sorted(...)`).
311
+ function globFiles(root, pattern) {
312
+ const parts = pattern.split("/").filter((p) => p.length > 0);
313
+ const results = [];
314
+ const walk = (dir, idx) => {
315
+ if (idx >= parts.length)
316
+ return;
317
+ const part = parts[idx];
318
+ const isLast = idx === parts.length - 1;
319
+ let entries;
320
+ try {
321
+ entries = fs.readdirSync(dir, { withFileTypes: true });
322
+ }
323
+ catch {
324
+ return;
325
+ }
326
+ if (part === "**") {
327
+ // zero-directory case: try the rest of the pattern at this level...
328
+ walk(dir, idx + 1);
329
+ // ...then descend, keeping `**` active.
330
+ for (const e of entries) {
331
+ if (e.isDirectory())
332
+ walk(path.join(dir, e.name), idx);
333
+ }
334
+ return;
335
+ }
336
+ const rx = segmentToRegex(part);
337
+ const skipDot = !part.startsWith(".");
338
+ for (const e of entries) {
339
+ if (skipDot && e.name.startsWith("."))
340
+ continue;
341
+ if (!rx.test(e.name))
342
+ continue;
343
+ const full = path.join(dir, e.name);
344
+ if (isLast) {
345
+ if (e.isFile())
346
+ results.push(full);
347
+ }
348
+ else if (e.isDirectory()) {
349
+ walk(full, idx + 1);
350
+ }
351
+ }
352
+ };
353
+ walk(root, 0);
354
+ return Array.from(new Set(results)).sort();
355
+ }
356
+ // Build the per-document upload list (relative path + body). File mode is the
357
+ // single target; corpus mode globs the marker-pinned set under the folder.
358
+ function enumerateDocuments(flags, resolvedPath, vaultRoot, marker) {
359
+ if (flags.mode === "file") {
360
+ return [
361
+ {
362
+ relPath: vaultRelPath(vaultRoot, resolvedPath),
363
+ content: fs.readFileSync(resolvedPath, "utf8"),
364
+ },
365
+ ];
366
+ }
367
+ // corpus
368
+ let effectiveGlob = flags.glob ?? DEFAULT_GLOB;
369
+ if (marker?.allowedGlob) {
370
+ if (flags.glob && flags.glob !== DEFAULT_GLOB && flags.glob !== marker.allowedGlob) {
371
+ throw new Error(`corpus marker pins allowedGlob=${JSON.stringify(marker.allowedGlob)} but --glob=${JSON.stringify(flags.glob)} was passed; the marker wins. Drop --glob or align it with the marker.`);
372
+ }
373
+ effectiveGlob = marker.allowedGlob;
374
+ }
375
+ const files = globFiles(vaultRoot, effectiveGlob);
376
+ if (files.length === 0) {
377
+ throw new Error(`--mode corpus: no files matched ${effectiveGlob} under ${resolvedPath}`);
378
+ }
379
+ return files.map((f) => ({
380
+ relPath: vaultRelPath(vaultRoot, f),
381
+ content: fs.readFileSync(f, "utf8"),
382
+ }));
383
+ }
384
+ // Mirrors render.ts: only a body-changing FILE ingest (a minted, activated
385
+ // revision) enqueues a GRAPH_EXTRACT job worth polling. Corpus rollups are
386
+ // async-default; noop_unchanged and failed ingests enqueue nothing.
387
+ function receiptEnqueuesExtraction(r) {
388
+ return r.mode === "file" && r.outcome === "ingested";
389
+ }
390
+ async function pollReceiptsToTerminal(receipts, opts, deps) {
391
+ if (opts.queue)
392
+ return; // opt-out: leave the inferred async-queued state.
393
+ const deadline = deps.now() + opts.budgetMs;
394
+ for (const r of receipts) {
395
+ if (!receiptEnqueuesExtraction(r))
396
+ continue;
397
+ for (;;) {
398
+ let polled;
399
+ try {
400
+ polled = await deps.fetchExtraction(r.documentId);
401
+ }
402
+ catch {
403
+ // A transient read failure must NOT fail the ingest: the revision is
404
+ // already committed. Stop polling this receipt and let whatever state
405
+ // we last observed (or the inferred queued state) render.
406
+ break;
407
+ }
408
+ if (!polled)
409
+ break; // pre-B2 intel: no job state to read.
410
+ r.extraction = {
411
+ state: polled.state,
412
+ candidateCount: polled.candidateCount ?? null,
413
+ conflictCount: polled.conflictCount ?? null,
414
+ jobId: polled.jobId ?? null,
415
+ };
416
+ if (polled.state === "completed" || polled.state === "failed")
417
+ break;
418
+ if (deps.now() >= deadline)
419
+ break; // timeout: render queued/running honestly.
420
+ await deps.sleep(opts.intervalMs);
421
+ }
422
+ }
423
+ }
424
+ // Default budget mirrors the enrich-hook contract (deadline ~30s, budget ~25s;
425
+ // NT:20260528 §3.6). Interval keeps the poll count modest (~16 max) while still
426
+ // catching a fast worker within a couple seconds.
427
+ const EXTRACTION_POLL_BUDGET_MS = 25_000;
428
+ const EXTRACTION_POLL_INTERVAL_MS = 1_500;
429
+ const EXTRACTION_DETAIL_TIMEOUT_MS = 8_000;
430
+ // Build the real fetcher: GET the B2 detail route and project its `extraction`
431
+ // field into a PolledExtraction. When the job has COMPLETED we count the
432
+ // PENDING_REVIEW candidates on the doc (the ones `mla kb pending` will list) so
433
+ // the receipt summary lines up with the review command it points at; conflicts
434
+ // are the CONTRADICTS / SUPERSEDES subset.
435
+ function buildExtractionFetcher(cfg, workspaceId) {
436
+ return async (documentId) => {
437
+ const qs = new URLSearchParams({
438
+ workspaceId,
439
+ revisionLimit: "1",
440
+ auditLimit: "1",
441
+ }).toString();
442
+ const detail = await (0, http_1.intelGet)(cfg, `/internal/v1/kb/documents/${encodeURIComponent(documentId)}/detail?${qs}`, EXTRACTION_DETAIL_TIMEOUT_MS);
443
+ const ex = detail.extraction;
444
+ if (!ex)
445
+ return null;
446
+ let candidateCount = null;
447
+ let conflictCount = null;
448
+ if (ex.state === "completed") {
449
+ const pending = (detail.candidates ?? []).filter((c) => c.status === "PENDING_REVIEW");
450
+ candidateCount = pending.length;
451
+ conflictCount = pending.filter((c) => c.relationType === "CONTRADICTS" || c.relationType === "SUPERSEDES").length;
452
+ }
453
+ return {
454
+ state: ex.state,
455
+ jobId: ex.jobId ?? null,
456
+ candidateCount,
457
+ conflictCount,
458
+ };
459
+ };
460
+ }
461
+ // Local pre-flight echo so the operator sees what is about to happen before the
462
+ // (possibly slow) server-side per-file pipeline runs. Keeps the silent-shell-out
463
+ // feel of `mla session remember` but lets corpus mode announce its target.
464
+ function printPreflight(flags, cfg) {
465
+ const ws = flags.workspace || cfg.workspaceId;
466
+ const target = path.resolve(flags.path);
467
+ console.log(`mla kb add (${flags.mode}) workspace=${ws} provenance=${flags.provenance} path=${target}`);
468
+ }
469
+ // Scale the request timeout by document count: a single file is fast (one
470
+ // inline LDM body + embeds), but a corpus holds the connection while the server
471
+ // ingests every doc sequentially. 120s floor for the common single-file/seed
472
+ // case, ~20s/doc above that.
473
+ function ingestTimeoutMs(docCount) {
474
+ return Math.max(120_000, docCount * 20_000);
475
+ }
476
+ async function runKbAdd(argv) {
477
+ // Parse flags BEFORE loading config so `--workspace <id>` can override the
478
+ // marker-resolved workspace (T1.1 folder = workspace). Passing the override
479
+ // into readKbConfig short-circuits marker resolution, so an admin can curate
480
+ // another workspace without activating the current directory.
481
+ let flags;
482
+ try {
483
+ flags = parseKbAddArgs(argv);
484
+ }
485
+ catch (e) {
486
+ console.error(e.message);
487
+ return 2;
488
+ }
489
+ let cfg;
490
+ try {
491
+ cfg = (0, config_1.readKbConfig)(flags.workspace);
492
+ }
493
+ catch (e) {
494
+ console.error(e.message);
495
+ return 2;
496
+ }
497
+ // §13.14 owner-only ACL: verify the configured actor is a workspace OWNER
498
+ // before any side effect (ingest POST, outbox emit). v1 has no KB_CURATE
499
+ // scope per §11 Q8.
500
+ try {
501
+ await (0, kb_acl_1.verifyKbActorIsOwner)(cfg);
502
+ }
503
+ catch (e) {
504
+ if (e instanceof kb_acl_1.KbOwnerCheckError) {
505
+ console.error(e.message);
506
+ // F5 (kb-write-blocked): the agent tried to write a lesson down and the
507
+ // owner-only ACL refused it. This is the canonical F5 signal. Records to
508
+ // the local deadletter only (never throws, respects the kill switch).
509
+ (0, failure_telemetry_1.recordKbWriteBlocked)({
510
+ traceId: (0, observability_1.getRunTraceId)(),
511
+ workspaceId: cfg.workspaceId,
512
+ reasonCode: "owner_gate",
513
+ status: 2,
514
+ });
515
+ return 2;
516
+ }
517
+ throw e;
518
+ }
519
+ // §4.1 explicit-path guards. The server also enforces these implicitly but
520
+ // surfacing them at the CLI boundary gives operators a faster, clearer error
521
+ // than waiting on a round trip.
522
+ const resolved = path.resolve(flags.path);
523
+ if (!fs.existsSync(resolved)) {
524
+ console.error(`path does not exist: ${resolved}`);
525
+ return 2;
526
+ }
527
+ const stat = fs.statSync(resolved);
528
+ if (flags.mode === "file" && stat.isDirectory()) {
529
+ console.error(`--mode file requires a file path, got directory: ${resolved}`);
530
+ return 2;
531
+ }
532
+ if (flags.mode === "corpus" && !stat.isDirectory()) {
533
+ console.error(`--mode corpus requires a directory path, got file: ${resolved}`);
534
+ return 2;
535
+ }
536
+ const workspaceId = flags.workspace || cfg.workspaceId;
537
+ // Resolve the vault root + assemble the upload list client-side (the CLI is
538
+ // the only side that holds the filesystem). Marker read (corpus) + the
539
+ // allowedProvenance guardrail run here, before the body bytes are read.
540
+ let documents;
541
+ let marker = null;
542
+ let corpusRootDisplay = null;
543
+ try {
544
+ if (flags.mode === "corpus") {
545
+ marker = readCorpusMarker(resolved, workspaceId);
546
+ // The corpus-marker provenance guardrail still applies to the operator's
547
+ // stated intent even though provenance is advisory at the governed layer.
548
+ if (marker.allowedProvenance && !marker.allowedProvenance.includes(flags.provenance)) {
549
+ throw new Error(`corpus marker allowedProvenance=${JSON.stringify(marker.allowedProvenance)} does NOT include --provenance=${flags.provenance}`);
550
+ }
551
+ }
552
+ const vaultRoot = resolveVaultRoot(flags, resolved);
553
+ if (flags.mode === "corpus")
554
+ corpusRootDisplay = vaultRoot;
555
+ documents = enumerateDocuments(flags, resolved, vaultRoot, marker);
556
+ }
557
+ catch (e) {
558
+ console.error(e.message);
559
+ return 2;
560
+ }
561
+ printPreflight(flags, cfg);
562
+ // Relay the session UUID, canonicalized (defense in depth: a direct
563
+ // `mla kb add --agent-session X` may carry a non-canonical value). The server
564
+ // canonicalizes again and is the authoritative fail-closed gate; an invalid
565
+ // value yields no session here, never a composed or console value.
566
+ const agentSession = (0, observability_1.canonicalizeSessionId)(flags.agentSession ?? null);
567
+ const body = {
568
+ workspaceId,
569
+ actor: cfg.actorUserId,
570
+ documents,
571
+ provenance: flags.provenance, // advisory; the server derives the recorded value
572
+ profile: flags.profile || DEFAULT_PROFILE,
573
+ agentSession: agentSession ?? undefined,
574
+ mode: flags.mode,
575
+ corpusName: marker?.corpusName,
576
+ };
577
+ let receipts;
578
+ try {
579
+ const res = await (0, http_1.intelPost)(cfg, "/internal/v1/kb/add", body, ingestTimeoutMs(documents.length));
580
+ receipts = res.receipts ?? [];
581
+ }
582
+ catch (e) {
583
+ console.error(`kb add failed: ${e.message}`);
584
+ // F5 (kb-write-blocked): the ingest POST did not land, so the lesson did not
585
+ // land. Record locally only; never throws, kill-switch aware.
586
+ (0, failure_telemetry_1.recordKbWriteBlocked)({
587
+ traceId: (0, observability_1.getRunTraceId)(),
588
+ workspaceId,
589
+ reasonCode: "ingest_post_failed",
590
+ status: 1,
591
+ });
592
+ return 1;
593
+ }
594
+ if (receipts.length === 0) {
595
+ console.error("kb add: the ingest route returned no receipts.");
596
+ return 1;
597
+ }
598
+ // The server has no filesystem, so it cannot fill the corpus display root.
599
+ // Stamp the resolved corpus folder back onto the rollup the operator sees.
600
+ if (flags.mode === "corpus" && corpusRootDisplay && receipts[0].corpus) {
601
+ receipts[0].corpus.rootPath = corpusRootDisplay;
602
+ }
603
+ // A per-doc intake failure is reported in the receipt, not the HTTP status.
604
+ // Mirror the worker's exit semantics: any failed doc -> non-zero exit.
605
+ const anyFailed = receipts.some((r) => r.outcome === "failed");
606
+ if (anyFailed) {
607
+ (0, failure_telemetry_1.recordKbWriteBlocked)({
608
+ traceId: (0, observability_1.getRunTraceId)(),
609
+ workspaceId,
610
+ reasonCode: "ingest_doc_failed",
611
+ status: 1,
612
+ });
613
+ }
614
+ const exit = anyFailed ? 1 : 0;
615
+ // B3: sync-extract by default. Block on the worker-owned GRAPH_EXTRACT job by
616
+ // polling the B2 detail route to a terminal state or the latency budget.
617
+ // --queue opts out; corpus / failed / no-op-restore receipts are skipped by
618
+ // receiptEnqueuesExtraction so we never serialize on a bulk ingest.
619
+ const willPoll = !flags.queue && receipts.some(receiptEnqueuesExtraction);
620
+ if (willPoll) {
621
+ console.error("waiting for relationship extraction (up to 25s; pass --queue to skip and check `mla kb show` later)...");
622
+ }
623
+ await pollReceiptsToTerminal(receipts, {
624
+ queue: flags.queue,
625
+ budgetMs: EXTRACTION_POLL_BUDGET_MS,
626
+ intervalMs: EXTRACTION_POLL_INTERVAL_MS,
627
+ }, {
628
+ fetchExtraction: buildExtractionFetcher(cfg, workspaceId),
629
+ sleep: (ms) => new Promise((res) => setTimeout(res, ms)),
630
+ now: () => Date.now(),
631
+ });
632
+ // Task 3.4: owner-namespaced governed-path cache write-after-ingest. Record
633
+ // every produced doc as "this owner governed this exact (repo, path) as KB doc
634
+ // <id>" so a later turn can recognize a governed surface without a server round
635
+ // trip. This is the SAFE half only: we NEVER gate the POST on a cache hit (the
636
+ // server's resolve_by_canonical_path is the authoritative resolver, and a stale
637
+ // 3-day entry could point at a doc tombstoned server-side). The whole pass is
638
+ // best-effort: a cache write must never fail or interrupt the add.
639
+ try {
640
+ // repoRootHash: prefer the .meetless.json marker DIRECTORY (the governed repo
641
+ // root per the folder=workspace T1.1 binding), resolved from where the target
642
+ // file actually lives so it matches that file's repo. Fall back to process.cwd()
643
+ // only when the file's tree carries no usable marker.
644
+ const markerCtx = (0, workspace_1.findWorkspaceContext)(path.dirname(resolved));
645
+ const repoRootDir = markerCtx ? markerCtx.markerDir : process.cwd();
646
+ const repoRootHash = (0, crypto_1.createHash)("sha256").update(repoRootDir).digest("hex").slice(0, 24);
647
+ for (const receipt of receipts) {
648
+ const entry = (0, governed_path_cache_1.governedPathEntryForReceipt)(receipt, {
649
+ workspaceId,
650
+ ownerUserId: cfg.actorUserId,
651
+ repoRootHash,
652
+ });
653
+ if (entry)
654
+ (0, governed_path_cache_1.writeGovernedPath)(entry.key, entry.docId, config_1.HOME);
655
+ }
656
+ }
657
+ catch (e) {
658
+ // Advise, never block: the revision already committed. Swallow and continue.
659
+ console.error(`governed-path cache write skipped: ${e.message}`);
660
+ }
661
+ // B4a: stamp the Console review URL onto every receipt so `kb add` always
662
+ // surfaces the clickable human review surface. The server does not know the
663
+ // console base; the CLI owns it via getConsoleUrl(cfg).
664
+ const consoleUrl = `${(0, config_1.getConsoleUrl)(cfg)}/relationships`;
665
+ for (const receipt of receipts) {
666
+ receipt.consoleUrl = consoleUrl;
667
+ console.log((0, render_1.renderKbAddReceipt)(receipt));
668
+ console.log("");
669
+ }
670
+ // B4b: `--open` is opt-in (the URL is always printed in the receipt above; we
671
+ // NEVER auto-open, since the agent-proxy loop drives `kb add` headless). Launch
672
+ // once for the whole add, not per-receipt. Status note -> stderr.
673
+ if (flags.open) {
674
+ const res = (0, open_url_1.openUrl)(consoleUrl);
675
+ if (res.ok)
676
+ console.error(`opened ${consoleUrl} in your browser`);
677
+ else
678
+ console.error(`could not open a browser (${res.error}); the URL is in the receipt above`);
679
+ }
680
+ return exit;
681
+ }