@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,216 @@
1
+ "use strict";
2
+ // src/lib/agent-decision/validate.ts
3
+ //
4
+ // Hand-rolled structural validator for the canonical agent-decision contract.
5
+ //
6
+ // Deviation noted (spec build-order item 1 suggested "TypeScript / Zod"): the mla
7
+ // CLI deliberately keeps a minimal dependency surface and ships NO Zod. This
8
+ // validator is the contract's teeth instead: it enforces every field rule and the
9
+ // cross-field invariants (INV-CHOICE-ID, decisionKind/multiSelect coherence,
10
+ // free_text/no_match coupling) that the spec spells out in sections 6 and 7. It
11
+ // returns a flat list of human-readable error strings; empty means valid.
12
+ //
13
+ // It is used by the contract tests and, fail-soft, at capture time so a malformed
14
+ // decision is logged and skipped rather than crashing the hook it rides on.
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.validateCanonicalDecisionPayload = validateCanonicalDecisionPayload;
17
+ exports.isValidCanonicalDecisionPayload = isValidCanonicalDecisionPayload;
18
+ const DECISION_KINDS = ["choice", "multi_choice", "free_text"];
19
+ const ANSWER_TYPES = ["choice_label", "multi_choice_labels", "free_text"];
20
+ const MATCH_STATUSES = ["exact_unique", "exact_ambiguous", "no_match"];
21
+ const CAPTURED_BY = ["post_tool_use", "stop_transcript_scan"];
22
+ const CHOICE_ID_RE = /^choice_\d+$/;
23
+ function isPlainObject(v) {
24
+ return typeof v === "object" && v !== null && !Array.isArray(v);
25
+ }
26
+ function isNonEmptyString(v) {
27
+ return typeof v === "string" && v.length > 0;
28
+ }
29
+ function validatePrompt(prompt, errs) {
30
+ if (!isPlainObject(prompt)) {
31
+ errs.push("prompt: must be an object {title, body}");
32
+ return;
33
+ }
34
+ if (typeof prompt.title !== "string")
35
+ errs.push("prompt.title: must be a string");
36
+ if (typeof prompt.body !== "string")
37
+ errs.push("prompt.body: must be a string");
38
+ }
39
+ function validateChoices(choices, errs) {
40
+ const ids = new Set();
41
+ if (!Array.isArray(choices)) {
42
+ errs.push("choices: must be an array");
43
+ return ids;
44
+ }
45
+ choices.forEach((c, i) => {
46
+ if (!isPlainObject(c)) {
47
+ errs.push(`choices[${i}]: must be an object {id,label,description?}`);
48
+ return;
49
+ }
50
+ // INV-CHOICE-ID: positional, collision-safe, never a slug.
51
+ if (typeof c.id !== "string" || !CHOICE_ID_RE.test(c.id)) {
52
+ errs.push(`choices[${i}].id: must match choice_<index>, got ${JSON.stringify(c.id)}`);
53
+ }
54
+ else {
55
+ if (ids.has(c.id))
56
+ errs.push(`choices[${i}].id: duplicate id ${c.id}`);
57
+ ids.add(c.id);
58
+ }
59
+ if (typeof c.label !== "string")
60
+ errs.push(`choices[${i}].label: must be a string`);
61
+ if (c.description !== undefined && typeof c.description !== "string") {
62
+ errs.push(`choices[${i}].description: must be a string when present`);
63
+ }
64
+ });
65
+ return ids;
66
+ }
67
+ function validateAnswer(answer, choiceIds, errs) {
68
+ if (!isPlainObject(answer)) {
69
+ errs.push("answer: must be an object");
70
+ return;
71
+ }
72
+ const a = answer;
73
+ if (typeof a.type !== "string" || !ANSWER_TYPES.includes(a.type)) {
74
+ errs.push(`answer.type: must be one of ${ANSWER_TYPES.join(", ")}`);
75
+ }
76
+ if (typeof a.choiceMatchStatus !== "string" || !MATCH_STATUSES.includes(a.choiceMatchStatus)) {
77
+ errs.push(`answer.choiceMatchStatus: must be one of ${MATCH_STATUSES.join(", ")}`);
78
+ }
79
+ if (!("raw" in a)) {
80
+ errs.push("answer.raw: must be present (INV-RAW-PRESERVATION applies to the raw answer too)");
81
+ }
82
+ // value shape depends on type.
83
+ if (a.type === "multi_choice_labels") {
84
+ if (!Array.isArray(a.value) || !a.value.every((x) => typeof x === "string")) {
85
+ errs.push("answer.value: must be a string[] when type is multi_choice_labels");
86
+ }
87
+ }
88
+ else if (a.type === "choice_label" || a.type === "free_text") {
89
+ if (typeof a.value !== "string") {
90
+ errs.push(`answer.value: must be a string when type is ${a.type}`);
91
+ }
92
+ }
93
+ // choiceId presence/format rules (single-select).
94
+ if (a.choiceId !== undefined) {
95
+ if (typeof a.choiceId !== "string" || !CHOICE_ID_RE.test(a.choiceId)) {
96
+ errs.push(`answer.choiceId: must match choice_<index>, got ${JSON.stringify(a.choiceId)}`);
97
+ }
98
+ else if (!choiceIds.has(a.choiceId)) {
99
+ errs.push(`answer.choiceId: ${a.choiceId} is not one of the offered choices`);
100
+ }
101
+ }
102
+ if (a.choiceIds !== undefined) {
103
+ if (!Array.isArray(a.choiceIds) || !a.choiceIds.every((x) => typeof x === "string" && CHOICE_ID_RE.test(x))) {
104
+ errs.push("answer.choiceIds: must be an array of choice_<index> ids when present");
105
+ }
106
+ else {
107
+ for (const cid of a.choiceIds) {
108
+ if (!choiceIds.has(cid))
109
+ errs.push(`answer.choiceIds: ${cid} is not one of the offered choices`);
110
+ }
111
+ }
112
+ }
113
+ // Cross-field coupling (spec section 6, derivation refined for multi-select).
114
+ // no_match is incompatible only with a singular choice_label claim. A single
115
+ // free_text answer carries it, and a multi_choice answer where no provided
116
+ // value matched an offered label may carry it too.
117
+ if (a.choiceMatchStatus === "no_match" && a.choiceId !== undefined) {
118
+ errs.push("answer: no_match must not carry a choiceId");
119
+ }
120
+ if (a.type === "choice_label") {
121
+ if (a.choiceMatchStatus === "no_match") {
122
+ errs.push("answer: choice_label cannot have choiceMatchStatus no_match");
123
+ }
124
+ if (a.choiceId === undefined) {
125
+ errs.push("answer: choice_label requires a choiceId");
126
+ }
127
+ if (a.choiceIds !== undefined) {
128
+ errs.push("answer: choice_label is single-select; use choiceId, not choiceIds");
129
+ }
130
+ }
131
+ if (a.type === "free_text") {
132
+ if (a.choiceMatchStatus !== "no_match") {
133
+ errs.push("answer: free_text requires choiceMatchStatus no_match");
134
+ }
135
+ if (a.choiceId !== undefined)
136
+ errs.push("answer: free_text must not carry a choiceId");
137
+ if (a.choiceIds !== undefined)
138
+ errs.push("answer: free_text must not carry choiceIds");
139
+ }
140
+ if (a.type === "multi_choice_labels" && a.choiceId !== undefined) {
141
+ errs.push("answer: multi_choice_labels is multi-select; use choiceIds, not a singular choiceId");
142
+ }
143
+ }
144
+ // Returns a flat list of validation errors; empty array means the payload
145
+ // satisfies the canonical contract.
146
+ function validateCanonicalDecisionPayload(payload) {
147
+ const errs = [];
148
+ if (!isPlainObject(payload)) {
149
+ return ["payload: must be an object"];
150
+ }
151
+ const p = payload;
152
+ if (!isNonEmptyString(p.provider))
153
+ errs.push("provider: must be a non-empty string");
154
+ if (!isNonEmptyString(p.providerSource))
155
+ errs.push("providerSource: must be a non-empty string");
156
+ if (!isNonEmptyString(p.providerEventId))
157
+ errs.push("providerEventId: must be a non-empty string");
158
+ if (p.providerToolName !== undefined && p.providerToolName !== null && typeof p.providerToolName !== "string") {
159
+ errs.push("providerToolName: must be a string, null, or omitted");
160
+ }
161
+ if (p.providerSessionId !== undefined && p.providerSessionId !== null && typeof p.providerSessionId !== "string") {
162
+ errs.push("providerSessionId: must be a string, null, or omitted");
163
+ }
164
+ if (typeof p.decisionKind !== "string" || !DECISION_KINDS.includes(p.decisionKind)) {
165
+ errs.push(`decisionKind: must be one of ${DECISION_KINDS.join(", ")}`);
166
+ }
167
+ validatePrompt(p.prompt, errs);
168
+ const choiceIds = validateChoices(p.choices, errs);
169
+ validateAnswer(p.answer, choiceIds, errs);
170
+ // decisionKind is derived from multiSelect + match outcome (spec section 6);
171
+ // enforce the coupling so the three fields can never drift.
172
+ const ansType = isPlainObject(p.answer) ? p.answer.type : undefined;
173
+ if (p.decisionKind === "multi_choice" && ansType !== "multi_choice_labels") {
174
+ errs.push("answer.type: must be multi_choice_labels when decisionKind is multi_choice");
175
+ }
176
+ if (p.decisionKind === "choice" && ansType !== "choice_label") {
177
+ errs.push("answer.type: must be choice_label when decisionKind is choice");
178
+ }
179
+ if (p.decisionKind === "free_text" && ansType !== "free_text") {
180
+ errs.push("answer.type: must be free_text when decisionKind is free_text");
181
+ }
182
+ if (typeof p.multiSelect !== "boolean") {
183
+ errs.push("multiSelect: must be a boolean");
184
+ }
185
+ else {
186
+ // Biconditional per spec section 6: multiSelect true iff decisionKind multi_choice.
187
+ if (p.multiSelect && p.decisionKind !== "multi_choice") {
188
+ errs.push("decisionKind: must be multi_choice when multiSelect is true");
189
+ }
190
+ if (!p.multiSelect && p.decisionKind === "multi_choice") {
191
+ errs.push("multiSelect: must be true when decisionKind is multi_choice");
192
+ }
193
+ }
194
+ if (typeof p.capturedBy !== "string" || !CAPTURED_BY.includes(p.capturedBy)) {
195
+ errs.push(`capturedBy: must be one of ${CAPTURED_BY.join(", ")}`);
196
+ }
197
+ if (p.turnIndex !== undefined && p.turnIndex !== null && typeof p.turnIndex !== "number") {
198
+ errs.push("turnIndex: must be a number, null, or omitted");
199
+ }
200
+ if (p.traceId !== undefined && p.traceId !== null && typeof p.traceId !== "string") {
201
+ errs.push("traceId: must be a string, null, or omitted");
202
+ }
203
+ if (p.actorDisplayName !== undefined && p.actorDisplayName !== null && typeof p.actorDisplayName !== "string") {
204
+ errs.push("actorDisplayName: must be a string, null, or omitted");
205
+ }
206
+ if (!("rawProviderPayload" in p)) {
207
+ errs.push("rawProviderPayload: must be present (INV-RAW-PRESERVATION)");
208
+ }
209
+ if (p.occurredAt !== undefined && typeof p.occurredAt !== "string") {
210
+ errs.push("occurredAt: must be an ISO string when present");
211
+ }
212
+ return errs;
213
+ }
214
+ function isValidCanonicalDecisionPayload(payload) {
215
+ return validateCanonicalDecisionPayload(payload).length === 0;
216
+ }
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ // captureCommandEvent: the run-finalize entry point that records one normalized
3
+ // `mla_command` journey event (spec section 6.2, section 11.4). It is the only
4
+ // thing cli.ts has to call. It runs after the command result is known and is
5
+ // fully defensive: any failure here is swallowed so analytics can never change a
6
+ // command's exit code or break its output.
7
+ //
8
+ // Order (matters): derive the sequence fields BEFORE recording, since they are
9
+ // read from the strictly-prior `mla_command` rows in the local jsonl; then record
10
+ // locally (durable, consent-gated); then best-effort forward to control (bounded,
11
+ // telemetry-gated). The local append happens even with remote telemetry off
12
+ // (local-first, INV-LOCAL-STATS-1); the forward is a no-op unless opted in.
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.buildCommandPayload = buildCommandPayload;
15
+ exports.captureCommandEvent = captureCommandEvent;
16
+ const command_event_1 = require("./command-event");
17
+ const sequence_1 = require("./sequence");
18
+ const store_1 = require("./store");
19
+ const recorder_1 = require("./recorder");
20
+ // Build the normalized command payload (pure). Exported so the privacy test can
21
+ // assert directly on what would be emitted, no I/O.
22
+ function buildCommandPayload(params) {
23
+ const { command, subcommand, flags_shape } = (0, command_event_1.normalizeCommand)(params.argv);
24
+ const { outcome, error_class, retryable } = (0, command_event_1.classifyOutcome)(params.exitCode, params.threw, params.thrown);
25
+ const seq = (0, sequence_1.computeSequence)(params.sessionId, params.startedAtMs, params.env);
26
+ const duration_ms = Math.max(0, params.nowMs - params.startedAtMs);
27
+ return {
28
+ command,
29
+ subcommand,
30
+ flags_shape,
31
+ scope: (0, command_event_1.classifyScope)(command, flags_shape),
32
+ duration_ms,
33
+ exit_code: params.exitCode,
34
+ outcome,
35
+ error_class,
36
+ retryable,
37
+ // The CLI command itself does not edit a code surface (that signal is for
38
+ // hook-origin events that wrap an edit). Honest default for a command event.
39
+ touched_surface: "unknown",
40
+ mla_version: params.mlaVersion,
41
+ git_sha: params.gitSha,
42
+ command_index_in_session: seq.command_index_in_session,
43
+ preceded_by: seq.preceded_by,
44
+ session_idle_gap_ms: seq.session_idle_gap_ms,
45
+ };
46
+ }
47
+ async function captureCommandEvent(params) {
48
+ const env = params.env ?? process.env;
49
+ try {
50
+ // `_internal` subcommands (evidence-inject, evidence-correlate, auto-index,
51
+ // finalize-session, active-review) are machine-internal plumbing spawned by
52
+ // hooks, not user journey steps. Emitting an mla_command for them pollutes the
53
+ // command-journey funnel with `command:"_internal", subcommand:null` noise, so
54
+ // skip the journey event entirely. The remote flush is kept: an internal
55
+ // command flushes its own buffer before this point, but keeping the flush
56
+ // preserves forwarding for any value events still buffered (no-op when empty).
57
+ const isInternal = (0, command_event_1.normalizeCommand)(params.argv).command === "_internal";
58
+ if (!isInternal) {
59
+ const payload = buildCommandPayload({
60
+ argv: params.argv,
61
+ exitCode: params.exitCode,
62
+ threw: params.threw,
63
+ thrown: params.thrown,
64
+ mlaVersion: params.mlaVersion,
65
+ gitSha: params.gitSha,
66
+ startedAtMs: params.startedAtMs,
67
+ nowMs: params.nowMs,
68
+ sessionId: params.sessionId,
69
+ env,
70
+ });
71
+ const nowIso = new Date(params.nowMs).toISOString();
72
+ const ctx = {
73
+ workspaceId: params.workspaceId,
74
+ sessionId: params.sessionId,
75
+ distinctId: params.actorUserId ?? (0, store_1.machineId)(),
76
+ // The un-collapsed actor cuid for attribution (T1.10): honest null on an
77
+ // actorless run, unlike distinctId which falls back to a hashed machine id.
78
+ actorWorkspaceUserId: params.actorUserId,
79
+ source: "cli",
80
+ now: nowIso,
81
+ };
82
+ // Local append (durable) + buffer. Mints the CLI-origin event_id once.
83
+ (0, recorder_1.recordAnalyticsEvent)(ctx, { eventType: "mla_command", payload: payload }, env, params.onError);
84
+ }
85
+ // Best-effort, bounded, telemetry-gated remote forward. Skipped entirely when
86
+ // the run has no control config.
87
+ if (params.cfg) {
88
+ await (0, recorder_1.flushAnalyticsEvents)(params.cfg, env, params.onError);
89
+ }
90
+ }
91
+ catch (err) {
92
+ // Analytics must never break a command. Surface on the debug hook only.
93
+ if (params.onError)
94
+ params.onError(err);
95
+ }
96
+ }
@@ -0,0 +1,267 @@
1
+ "use strict";
2
+ // mla_command normalization (spec section 6.2, INV-ARGV-1, INV-POSTHOG-PII-1).
3
+ //
4
+ // Turns a raw argv into the three privacy-safe shape fields the journey event
5
+ // carries: a known `command`, a known `subcommand` (or null), and a `flags_shape`
6
+ // built from approved flag NAMES only. Raw argv is never emitted: positional
7
+ // arguments (queries, paths, ids) do not start with a dash and are dropped; flag
8
+ // VALUES are split off at `=` or live in a separate token and are likewise
9
+ // dropped; an unrecognized command or flag is normalized away rather than passed
10
+ // through. This is the single chokepoint that keeps INV-ARGV-1 true.
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.APPROVED_FLAGS = exports.KNOWN_SUBCOMMANDS = exports.KNOWN_COMMANDS = void 0;
13
+ exports.normalizeCommand = normalizeCommand;
14
+ exports.classifyScope = classifyScope;
15
+ exports.classifyOutcome = classifyOutcome;
16
+ // The closed set of top-level commands `mla` dispatches (cli.ts:215). A first
17
+ // token outside this set is normalized to "unknown" so a typo'd path or secret
18
+ // pasted as argv[0] never reaches the wire.
19
+ exports.KNOWN_COMMANDS = new Set([
20
+ "init",
21
+ "rewire",
22
+ "activate",
23
+ "deactivate",
24
+ "mute",
25
+ "unmute",
26
+ "workspace",
27
+ "doctor",
28
+ "flush",
29
+ "review",
30
+ "cases",
31
+ "session",
32
+ "ask",
33
+ "kb",
34
+ "summary",
35
+ "label",
36
+ "adoption",
37
+ "stats",
38
+ "whoami",
39
+ "login",
40
+ "logout",
41
+ "debug",
42
+ "_internal",
43
+ ]);
44
+ // Known subcommand keywords per command. A second token is emitted as
45
+ // `subcommand` ONLY when it is in this set; otherwise it is a positional (an id,
46
+ // a query, a doc path) and `subcommand` is null. This is what stops
47
+ // `mla review <case-id>` or `mla ask "<query>"` from leaking the positional as a
48
+ // subcommand.
49
+ exports.KNOWN_SUBCOMMANDS = {
50
+ kb: new Set([
51
+ "add",
52
+ "show",
53
+ "reingest",
54
+ "forget",
55
+ "purge",
56
+ "move",
57
+ "review",
58
+ "pending",
59
+ "personal",
60
+ "promote",
61
+ "share",
62
+ "retime",
63
+ "summary",
64
+ ]),
65
+ workspace: new Set(["show", "use"]),
66
+ session: new Set(["show"]),
67
+ stats: new Set(["evidence"]),
68
+ _internal: new Set(["finalize-session", "active-review", "auto-index"]),
69
+ };
70
+ // Approved flag NAMES (INV-ARGV-1). A token starting with a dash is reduced to
71
+ // its name (leading dashes stripped, anything after `=` discarded) and kept only
72
+ // if it is in this set. The set is flag NAMES the CLI actually parses; values are
73
+ // never emitted regardless of allowlisting. Unknown flag names are dropped, not
74
+ // surfaced, so a future flag is invisible to analytics until it is added here
75
+ // (privacy-conservative by construction).
76
+ exports.APPROVED_FLAGS = new Set([
77
+ "accept",
78
+ "actor",
79
+ "agent",
80
+ "all",
81
+ "allow-file-missing",
82
+ "allow-provenance-change",
83
+ "anchor-type",
84
+ "apply",
85
+ "as-of",
86
+ "audit-all",
87
+ "cached",
88
+ "control-token",
89
+ "control-url",
90
+ "create",
91
+ "doc",
92
+ "dry-run",
93
+ "effective-date",
94
+ "evidence",
95
+ "force",
96
+ "from-root",
97
+ "gc",
98
+ "glob",
99
+ "global",
100
+ "harmful",
101
+ "help",
102
+ "here",
103
+ "include-tombstoned",
104
+ "ingest-run-id",
105
+ "intel-url",
106
+ "is-inside-work-tree",
107
+ "json",
108
+ "last",
109
+ "markdown",
110
+ "marker",
111
+ "max",
112
+ "min",
113
+ "mode",
114
+ "name",
115
+ "no-flush",
116
+ "no-install-flock",
117
+ "no-post-tool-use",
118
+ "no-project-rules",
119
+ "no-relation",
120
+ "noisy",
121
+ "note",
122
+ "oneline",
123
+ "open",
124
+ "path",
125
+ "plain",
126
+ "posture",
127
+ "prevented-mistake",
128
+ "profile",
129
+ "provenance",
130
+ "purge-expired",
131
+ "queue",
132
+ "quiet",
133
+ "reap-only",
134
+ "reason",
135
+ "reclassify",
136
+ "reject",
137
+ "repair",
138
+ "scope-section",
139
+ "session",
140
+ "show-current",
141
+ "show-toplevel",
142
+ "skill-only",
143
+ "stat",
144
+ "unsafe-capture-non-bash",
145
+ "useful",
146
+ "verbose",
147
+ "window",
148
+ "workspace",
149
+ "workspace-id",
150
+ "yes",
151
+ ]);
152
+ // Coarse command-to-scope map (where the command's effect primarily lands).
153
+ // Command-granularity on purpose: a per-subcommand effect tracker is not worth
154
+ // the maintenance for a breakdown dimension. Anchored to the spec's own example
155
+ // (section 11: `kb review` -> scope "local"), so `kb` is local even though some
156
+ // subcommands sync. `ask` / `adoption` / `summary` fundamentally read or compute
157
+ // over workspace data, so they are "workspace". A `--global` flag overrides to
158
+ // "global". Anything unrecognized is "unknown" (never guessed).
159
+ const WORKSPACE_SCOPE_COMMANDS = new Set(["ask", "adoption", "summary"]);
160
+ // Normalize the first token to a known command, "help"/"version" for the usage
161
+ // and version shortcuts, or "unknown".
162
+ function normalizeCommandToken(first) {
163
+ if (first === undefined || first === "help" || first === "--help" || first === "-h") {
164
+ return "help";
165
+ }
166
+ if (first === "--version" || first === "-v")
167
+ return "version";
168
+ if (exports.KNOWN_COMMANDS.has(first))
169
+ return first;
170
+ return "unknown";
171
+ }
172
+ // Reduce a raw token to its flag name, or null if it is not a flag (positional)
173
+ // or not approved. `--window=7d` -> "window"; `--window 7d` -> "window" (the
174
+ // "7d" is a separate token that returns null here); `query text` -> null.
175
+ function flagName(token) {
176
+ if (!token.startsWith("-"))
177
+ return null;
178
+ const stripped = token.replace(/^-+/, "");
179
+ if (!stripped)
180
+ return null;
181
+ const name = stripped.split("=", 1)[0].toLowerCase();
182
+ return exports.APPROVED_FLAGS.has(name) ? name : null;
183
+ }
184
+ // Build the privacy-safe command shape from argv. Pure, no I/O.
185
+ function normalizeCommand(argv) {
186
+ const command = normalizeCommandToken(argv[0]);
187
+ let subcommand = null;
188
+ const knownSubs = exports.KNOWN_SUBCOMMANDS[command];
189
+ if (knownSubs && typeof argv[1] === "string" && knownSubs.has(argv[1])) {
190
+ subcommand = argv[1];
191
+ }
192
+ // Scan ALL tokens for approved flags (flags can follow positionals). Dedupe and
193
+ // sort for a stable shape so PostHog funnels group identical invocations.
194
+ const flags = new Set();
195
+ for (const token of argv) {
196
+ const name = flagName(token);
197
+ if (name)
198
+ flags.add(name);
199
+ }
200
+ const flags_shape = [...flags].sort();
201
+ return { command, subcommand, flags_shape };
202
+ }
203
+ // Scope of the command's effect (best-effort, command-granularity). `--global`
204
+ // in flags_shape wins.
205
+ function classifyScope(command, flags_shape) {
206
+ if (flags_shape.includes("global"))
207
+ return "global";
208
+ if (command === "unknown")
209
+ return "unknown";
210
+ if (WORKSPACE_SCOPE_COMMANDS.has(command))
211
+ return "workspace";
212
+ return "local";
213
+ }
214
+ // Map a finished run (exit code + optional thrown error) to a closed-enum
215
+ // outcome, a PII-safe error_class (a class/category token, NEVER a message), and
216
+ // a retryable hint. The error's `status` (set by lib/http.buildError on HTTP
217
+ // failures) drives the HTTP mapping; otherwise the error code/name is inspected
218
+ // for the common network failure modes.
219
+ function classifyOutcome(exitCode, threw, thrown) {
220
+ if (exitCode === 0) {
221
+ return { outcome: "success", error_class: null, retryable: false };
222
+ }
223
+ if (!threw) {
224
+ // A command returned non-zero without throwing: a handled, user-facing
225
+ // failure (bad input, not found, usage error -> exit 2). No exception object
226
+ // to classify, so no error_class.
227
+ return { outcome: "user_error", error_class: null, retryable: false };
228
+ }
229
+ const err = thrown;
230
+ const status = err?.status;
231
+ if (typeof status === "number") {
232
+ if (status === 401)
233
+ return { outcome: "auth_error", error_class: "http_401", retryable: false };
234
+ if (status === 403)
235
+ return { outcome: "permission_denied", error_class: "http_403", retryable: false };
236
+ if (status === 408)
237
+ return { outcome: "timeout", error_class: "http_408", retryable: true };
238
+ if (status === 429)
239
+ return { outcome: "system_error", error_class: "http_429", retryable: true };
240
+ if (status === 400 || status === 422) {
241
+ return { outcome: "validation_error", error_class: `http_${status}`, retryable: false };
242
+ }
243
+ if (status >= 500)
244
+ return { outcome: "system_error", error_class: `http_${status}`, retryable: true };
245
+ // Other 4xx: a client-side problem the user must fix.
246
+ return { outcome: "user_error", error_class: `http_${status}`, retryable: false };
247
+ }
248
+ // No HTTP status: a network-layer or programmatic error. fetch() rejects with a
249
+ // TypeError ("fetch failed") wrapping a cause; AbortController aborts surface as
250
+ // an AbortError; node socket errors carry an errno code.
251
+ const code = (err?.code || "").toUpperCase();
252
+ const name = err?.name || "";
253
+ if (code === "ETIMEDOUT" || name === "AbortError" || name === "TimeoutError") {
254
+ return { outcome: "timeout", error_class: "timeout", retryable: true };
255
+ }
256
+ if (code === "ECONNREFUSED" ||
257
+ code === "ENOTFOUND" ||
258
+ code === "ECONNRESET" ||
259
+ code === "EAI_AGAIN" ||
260
+ name === "FetchError" ||
261
+ name === "TypeError" // node's fetch wraps network failures as TypeError
262
+ ) {
263
+ return { outcome: "network_error", error_class: "network_error", retryable: true };
264
+ }
265
+ // Unknown thrown error: a class name is PII-safe (it is a type, not a message).
266
+ return { outcome: "system_error", error_class: name || "Error", retryable: false };
267
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ // Analytics consent: the three privacy postures (spec section 9, INV-CONSENT-1).
3
+ //
4
+ // Local recording, remote ids-only analytics, and content-bearing trace upload
5
+ // are genuinely different privacy decisions; a user may want ids-only analytics
6
+ // on but content traces off. We reconcile with the EXISTING CLI kill switch
7
+ // (TELEMETRY.md) rather than inventing a parallel flag set:
8
+ //
9
+ // MEETLESS_LOCAL_STATS default ON -> write ~/.meetless/events.jsonl
10
+ // MEETLESS_TELEMETRY default OFF -> ship ids-only events to control->PostHog
11
+ // AND keep its existing kill-switch role
12
+ // MEETLESS_TRACE_UPLOAD default ON* -> content-bearing traces (Langfuse) + Sentry
13
+ //
14
+ // (*) MEETLESS_TRACE_UPLOAD's ABSENCE preserves today's trace-plane behavior so
15
+ // dogfood keeps working; the effective posture is still "off unless your server
16
+ // opts in" because control refuses with TRACING_NOT_ENABLED_FOR_WORKSPACE. The
17
+ // flag is an explicit content-trace sub-kill, independent of the analytics opt-in.
18
+ //
19
+ // The master kill switch (telemetryDisabled: MEETLESS_TELEMETRY in {off,0,false,no}
20
+ // OR truthy MEETLESS_NO_TELEMETRY) wins over BOTH remote planes.
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.localStatsEnabled = localStatsEnabled;
23
+ exports.remoteAnalyticsEnabled = remoteAnalyticsEnabled;
24
+ exports.traceUploadEnabled = traceUploadEnabled;
25
+ const observability_1 = require("../observability");
26
+ function isOff(v) {
27
+ const t = (v || "").trim().toLowerCase();
28
+ return t === "off" || t === "0" || t === "false" || t === "no";
29
+ }
30
+ function isOn(v) {
31
+ const t = (v || "").trim().toLowerCase();
32
+ return t === "on" || t === "1" || t === "true" || t === "yes";
33
+ }
34
+ // Local jsonl recording for `mla stats`. Default ON. Only an explicit off-value
35
+ // disables it. Independent of the remote planes: `mla stats` (local) works even
36
+ // with all remote telemetry off (INV-LOCAL-STATS-1, INV-CONSENT-1).
37
+ function localStatsEnabled(env = process.env) {
38
+ return !isOff(env.MEETLESS_LOCAL_STATS);
39
+ }
40
+ // Remote ids-only analytics upload (CLI -> control -> PostHog/rollups). Default
41
+ // OFF: it is an explicit opt-in via a truthy MEETLESS_TELEMETRY. The master kill
42
+ // (telemetryDisabled) always wins. Note the asymmetry with the trace plane:
43
+ // analytics is opt-IN (silent unless turned on), trace upload is opt-OUT.
44
+ function remoteAnalyticsEnabled(env = process.env) {
45
+ if ((0, observability_1.telemetryDisabled)(env))
46
+ return false;
47
+ return isOn(env.MEETLESS_TELEMETRY);
48
+ }
49
+ // Content-bearing trace upload (Langfuse spans) + Sentry error reporting. The
50
+ // master kill wins; an explicit MEETLESS_TRACE_UPLOAD off-value is the content
51
+ // sub-kill; absence preserves the pre-existing trace-plane behavior (still
52
+ // server-gated downstream). This is what gates initSentry() and the trace
53
+ // flushFn at their cli.ts call sites.
54
+ function traceUploadEnabled(env = process.env) {
55
+ if ((0, observability_1.telemetryDisabled)(env))
56
+ return false;
57
+ return !isOff(env.MEETLESS_TRACE_UPLOAD);
58
+ }