@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,805 @@
1
+ "use strict";
2
+ // mla observability spine: trace_id + Sentry helpers + build-info loader.
3
+ //
4
+ // See notes/20260530-mla-observability-diagnostic-spine.md for the full design.
5
+ // One trace_id per mla run; same string used as Sentry tag, X-Trace-ID header,
6
+ // and Langfuse trace id once intel propagates it.
7
+ //
8
+ // Canonical format is 32 hex chars (16 random bytes hex-encoded). This is OTel-
9
+ // native and matches intel's existing RequestContext.langfuse_trace_id format.
10
+ // Spec (§4) names UUIDv4, but every Langfuse SDK and intel itself wants 32-hex
11
+ // without dashes; zero translation across planes is the prize.
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.TRACE_FLUSH_CEILING_MS = exports.HTTP_FLUSH_TIMEOUT_MS = exports.TraceRoundTripError = exports.telemetryDisabled = void 0;
47
+ exports.loadBuildInfo = loadBuildInfo;
48
+ exports.mintTraceId = mintTraceId;
49
+ exports.isCanonicalTraceId = isCanonicalTraceId;
50
+ exports.canonicalizeSessionId = canonicalizeSessionId;
51
+ exports.redactSentryEvent = redactSentryEvent;
52
+ exports.initSentry = initSentry;
53
+ exports.isSentryAvailable = isSentryAvailable;
54
+ exports.setWorkspaceConfig = setWorkspaceConfig;
55
+ exports.getWorkspaceConfig = getWorkspaceConfig;
56
+ exports.workspaceSentryAllowed = workspaceSentryAllowed;
57
+ exports.pickSafeObservabilityFields = pickSafeObservabilityFields;
58
+ exports.setSafeObservabilityContext = setSafeObservabilityContext;
59
+ exports.captureCliError = captureCliError;
60
+ exports.captureCliNonZeroExit = captureCliNonZeroExit;
61
+ exports.captureBootstrapError = captureBootstrapError;
62
+ exports.boundedSentryFlush = boundedSentryFlush;
63
+ exports.setRunTraceId = setRunTraceId;
64
+ exports.getRunTraceId = getRunTraceId;
65
+ exports.setRunSessionId = setRunSessionId;
66
+ exports.getRunSessionId = getRunSessionId;
67
+ exports.resetRunSessionIdForTesting = resetRunSessionIdForTesting;
68
+ exports.mintRunId = mintRunId;
69
+ exports.setRunId = setRunId;
70
+ exports.getRunId = getRunId;
71
+ exports.resetRunIdForTesting = resetRunIdForTesting;
72
+ exports.setRepoFingerprint = setRepoFingerprint;
73
+ exports.getRepoFingerprint = getRepoFingerprint;
74
+ exports.resetRepoFingerprintForTesting = resetRepoFingerprintForTesting;
75
+ exports.noteIntelEchoedTraceId = noteIntelEchoedTraceId;
76
+ exports.didIntelEchoTraceId = didIntelEchoTraceId;
77
+ exports.resetIntelEchoForTesting = resetIntelEchoForTesting;
78
+ exports.didTraceFlushSucceed = didTraceFlushSucceed;
79
+ exports.resetTraceFlushOutcomeForTesting = resetTraceFlushOutcomeForTesting;
80
+ exports.makeHttpFlush = makeHttpFlush;
81
+ exports.setRunTracer = setRunTracer;
82
+ exports.getRunTracer = getRunTracer;
83
+ exports.resetRunTracerForTesting = resetRunTracerForTesting;
84
+ exports.routeNameFromPath = routeNameFromPath;
85
+ exports.redactArgvForSpan = redactArgvForSpan;
86
+ exports.langfuseTraceUrl = langfuseTraceUrl;
87
+ exports.langfuseDeepLink = langfuseDeepLink;
88
+ exports.shouldPrintDeepLink = shouldPrintDeepLink;
89
+ exports.maybePrintDeepLink = maybePrintDeepLink;
90
+ exports.createRunTracer = createRunTracer;
91
+ exports.boundedTraceFlush = boundedTraceFlush;
92
+ const crypto = __importStar(require("crypto"));
93
+ const fs = __importStar(require("fs"));
94
+ const path = __importStar(require("path"));
95
+ const Sentry = __importStar(require("@sentry/node"));
96
+ const redactor_1 = require("./redactor");
97
+ const config_1 = require("./config");
98
+ Object.defineProperty(exports, "telemetryDisabled", { enumerable: true, get: function () { return config_1.telemetryDisabled; } });
99
+ const failure_telemetry_1 = require("./failure-telemetry");
100
+ function loadTraceCore() {
101
+ try {
102
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
103
+ return require(path.resolve(__dirname, "..", "bundles", "trace-core.js"));
104
+ }
105
+ catch (e) {
106
+ const code = e?.code;
107
+ if (code !== "MODULE_NOT_FOUND" && code !== "ERR_MODULE_NOT_FOUND")
108
+ throw e;
109
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
110
+ return require("@meetless/trace-core");
111
+ }
112
+ }
113
+ const { makeNoopTracer, makeTracer } = loadTraceCore();
114
+ let cachedBuildInfo = null;
115
+ function loadBuildInfo() {
116
+ if (cachedBuildInfo)
117
+ return cachedBuildInfo;
118
+ try {
119
+ const raw = fs.readFileSync(path.join(__dirname, "..", "build-info.json"), "utf8");
120
+ cachedBuildInfo = JSON.parse(raw);
121
+ }
122
+ catch {
123
+ cachedBuildInfo = {
124
+ version: "0.0.0",
125
+ sha: "dev",
126
+ branch: "dev",
127
+ dirty: true,
128
+ builtAt: new Date().toISOString(),
129
+ };
130
+ }
131
+ return cachedBuildInfo;
132
+ }
133
+ function mintTraceId() {
134
+ return crypto.randomBytes(16).toString("hex");
135
+ }
136
+ // OBS-1 shape: a canonical trace_id is exactly 32 lowercase hex chars (OTel
137
+ // native, the shape mintTraceId produces and the shape intel adopts verbatim as
138
+ // a Langfuse trace id). The single source of truth for the shape guard, reused
139
+ // by the debug-bundle command's `--trace-id` validation (Phase 5 / gap 6.7) so a
140
+ // malformed id is rejected up front rather than seeding a bad bundle path.
141
+ const CANONICAL_TRACE_ID_RE = /^[0-9a-f]{32}$/;
142
+ function isCanonicalTraceId(value) {
143
+ return typeof value === "string" && CANONICAL_TRACE_ID_RE.test(value);
144
+ }
145
+ // The single canonical Claude agent-session UUID grammar, byte-identical to the
146
+ // Python twin (intel app/observability/langfuse_session.py:_AGENT_SESSION_RE) and
147
+ // the Bash hook twin. NEVER a language UUID parser: Python's uuid.UUID accepts
148
+ // braces, urn:uuid:, and un-dashed 32-hex, and JS has no native UUID parser at
149
+ // all; agreeing on this one explicit regex is what stops the same Claude id
150
+ // canonicalizing to two different strings and splitting the Langfuse Session.
151
+ const AGENT_SESSION_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
152
+ // TS twin of canonicalize_agent_session_id. Pure: no metric, no logging. Trim,
153
+ // match the regex (case-insensitive), lowercase; no match -> null. The regex is
154
+ // anchored, so any leftover whitespace or header-injection byte after trim fails
155
+ // the match and yields null, which is what keeps the composed value safe to pass
156
+ // to a `-H` curl header downstream.
157
+ function canonicalizeSessionId(raw) {
158
+ if (raw === null || raw === undefined)
159
+ return null;
160
+ const s = raw.trim();
161
+ return AGENT_SESSION_RE.test(s) ? s.toLowerCase() : null;
162
+ }
163
+ let sentryAvailable = false;
164
+ // Keys whose VALUE is always a credential regardless of entropy, redacted by
165
+ // name in every Sentry event (breadcrumb data, contexts, extra, tags, request
166
+ // headers, exception/stack vars). This is the §9 Sentry-redaction invariant
167
+ // (Finding K / Patch P7): the observability layer must never ship an
168
+ // Authorization header, access/refresh token, PKCE codeVerifier, control token,
169
+ // or INTERNAL_API_KEY off the machine, even with telemetry enabled.
170
+ //
171
+ // Bare `code` is deliberately ABSENT: it collides with error/status/language
172
+ // codes. The one-time login-grant `code` is 64-hex high-entropy, so it is
173
+ // caught by the value-based redactor (entropy heuristic in redact()) instead.
174
+ const SENTRY_SENSITIVE_KEY = /(authorization|access[_-]?token|refresh[_-]?token|code[_-]?verifier|control[_-]?token|internal[_-]?api[_-]?key|\bapi[_-]?key\b|x-api-key|secret|passw(?:or)?d|\bbearer\b|cookie|\btoken\b)/i;
175
+ // Keys carrying NON-secret high-entropy identifiers (trace/span/event/run ids,
176
+ // git sha, build version, environment). These are exactly the strings the value
177
+ // redactor's entropy heuristic would otherwise nuke, and they are the whole
178
+ // point of the observability spine (the cross-plane trace-id join + release
179
+ // correlation). Exempt them from value redaction; everything else high-entropy
180
+ // stays conservatively redacted (over-redaction is the safe failure mode).
181
+ const SENTRY_SAFE_IDENTIFIER_KEY = /^(x-)?(trace|span|event|run)[_-]?id$|^trace_source$|^release$|^dist$|^sha$|^environment$|^mla_version$|^platform$/i;
182
+ // One recursive scrub of a Sentry event. Per field: a credential KEY collapses
183
+ // to [REDACTED]; a safe-identifier key passes its value verbatim; every other
184
+ // string runs through the shared value redactor (Bearer/provider tokens, PEM,
185
+ // cookies, and the high-entropy heuristic that catches the grant code +
186
+ // codeVerifier even when they hide in a free-text body).
187
+ function scrubEventNode(value, keyHint) {
188
+ if (keyHint !== undefined && SENTRY_SENSITIVE_KEY.test(keyHint))
189
+ return redactor_1.REDACTED;
190
+ if (typeof value === "string") {
191
+ if (keyHint !== undefined && SENTRY_SAFE_IDENTIFIER_KEY.test(keyHint))
192
+ return value;
193
+ return (0, redactor_1.redact)(value);
194
+ }
195
+ if (Array.isArray(value))
196
+ return value.map((v) => scrubEventNode(v, keyHint));
197
+ if (value !== null && typeof value === "object") {
198
+ const out = {};
199
+ for (const [k, v] of Object.entries(value)) {
200
+ out[k] = scrubEventNode(v, k);
201
+ }
202
+ return out;
203
+ }
204
+ return value;
205
+ }
206
+ // beforeSend hook: scrub every event before it leaves the process. On any
207
+ // redaction error we DROP the event (return null) rather than risk shipping an
208
+ // unscrubbed payload.
209
+ function redactSentryEvent(event) {
210
+ if (event === null || event === undefined)
211
+ return event;
212
+ try {
213
+ return scrubEventNode(event, undefined);
214
+ }
215
+ catch {
216
+ return null;
217
+ }
218
+ }
219
+ function initSentry(buildInfo) {
220
+ // 4.4 kill switch wins over everything, including a baked production DSN.
221
+ if ((0, config_1.telemetryDisabled)()) {
222
+ sentryAvailable = false;
223
+ return false;
224
+ }
225
+ // Dev-only override: only honored when build-info reports a dev build
226
+ // (no baked DSN). Production binaries ignore the env override entirely.
227
+ // OSS-facing MEETLESS_SENTRY_DSN wins; legacy MLA_SENTRY_DSN still accepted.
228
+ const bakedDsn = buildInfo.sentryDsn;
229
+ const isDev = !bakedDsn;
230
+ const dsn = isDev
231
+ ? process.env.MEETLESS_SENTRY_DSN || process.env.MLA_SENTRY_DSN
232
+ : bakedDsn;
233
+ if (!dsn) {
234
+ sentryAvailable = false;
235
+ return false;
236
+ }
237
+ Sentry.init({
238
+ dsn,
239
+ release: buildInfo.sha,
240
+ environment: buildInfo.dirty ? "dev" : "prod",
241
+ // Pure CLI: no transaction sampling, no tracing integrations.
242
+ // Errors and explicit captureMessage only.
243
+ tracesSampleRate: 0,
244
+ // §9 redaction invariant (Finding K / P7): scrub credentials out of every
245
+ // event before transport. Must exist before any token-capturing path (login
246
+ // / refresh) can land a token in a breadcrumb or stack var.
247
+ beforeSend: (event) => redactSentryEvent(event),
248
+ });
249
+ Sentry.setTags({
250
+ mla_version: `${buildInfo.version} (${buildInfo.sha}${buildInfo.dirty ? "-dirty" : ""})`,
251
+ platform: process.platform,
252
+ trace_source: "mla-cli",
253
+ });
254
+ sentryAvailable = true;
255
+ return true;
256
+ }
257
+ function isSentryAvailable() {
258
+ return sentryAvailable;
259
+ }
260
+ // Run-local workspace config snapshot. Read by capture helpers to decide
261
+ // whether non-bootstrap captures are allowed (cycle 4 fix 4). null until
262
+ // workspace-me has loaded; bootstrap captures fire under null without
263
+ // consulting this gate.
264
+ let currentWorkspaceConfig = null;
265
+ function setWorkspaceConfig(cfg) {
266
+ currentWorkspaceConfig = cfg;
267
+ }
268
+ function getWorkspaceConfig() {
269
+ return currentWorkspaceConfig;
270
+ }
271
+ // Tenant guardrail (§9): even if workspace.tracing.sentryEnabled were flipped
272
+ // true on a non-dogfood workspace by a config drift, the CLI still refuses to
273
+ // send full-context captures. Only ws_an_local or workspaces explicitly flagged
274
+ // tracing_dogfood: true clear the gate.
275
+ function workspaceSentryAllowed(cfg) {
276
+ if (!cfg)
277
+ return false;
278
+ if (cfg.tracing?.sentryEnabled !== true)
279
+ return false;
280
+ if (cfg.workspaceId === "ws_an_local")
281
+ return true;
282
+ return cfg.tracingDogfood === true;
283
+ }
284
+ const SAFE_OBSERVABILITY_KEYS = [
285
+ "traceId",
286
+ "langfuseUrl",
287
+ "release",
288
+ "command",
289
+ "exitCode",
290
+ "traceSource",
291
+ "workspaceIdOrHash",
292
+ ];
293
+ // Pick only the allowlisted keys from arbitrary input, dropping anything else
294
+ // plus null/undefined and non-scalar values (so an object smuggled into an
295
+ // allowlisted key still cannot leak). Exported so the OBS-9 guarantee is
296
+ // unit-testable without a live Sentry scope.
297
+ function pickSafeObservabilityFields(input) {
298
+ const out = {};
299
+ for (const key of SAFE_OBSERVABILITY_KEYS) {
300
+ const value = input[key];
301
+ if (value === undefined || value === null)
302
+ continue;
303
+ if (typeof value === "string" || typeof value === "number") {
304
+ out[key] = value;
305
+ }
306
+ }
307
+ return out;
308
+ }
309
+ // Attach the allowlisted observability fields as a single Sentry "observability"
310
+ // context block; returns the safe object actually attached (for tests). Pass the
311
+ // full candidate object: non-allowlisted keys never reach Sentry. When nothing
312
+ // safe survives, the context is set to null so no empty frame is advertised.
313
+ function setSafeObservabilityContext(scope, fields) {
314
+ const safe = pickSafeObservabilityFields(fields);
315
+ scope.setContext("observability", Object.keys(safe).length > 0 ? safe : null);
316
+ return safe;
317
+ }
318
+ // Build the OBS-9-safe observability fields for a Sentry capture from run-local
319
+ // state. langfuseUrl is included only when the workspace config carries a project
320
+ // id; otherwise it is omitted (the allowlist drops the undefined). release is the
321
+ // build sha so a Sentry event pins the exact binary.
322
+ function captureObservabilityFields(ctx) {
323
+ const cfg = currentWorkspaceConfig;
324
+ const projectId = cfg?.tracing?.langfuseProjectId ?? null;
325
+ return {
326
+ traceId: ctx.traceId,
327
+ langfuseUrl: projectId ? langfuseTraceUrl(projectId, ctx.traceId) : undefined,
328
+ release: cachedBuildInfo?.sha,
329
+ command: ctx.sub ? `${ctx.command} ${ctx.sub}` : ctx.command,
330
+ exitCode: ctx.exitCode,
331
+ traceSource: "mla-cli",
332
+ workspaceIdOrHash: cfg?.workspaceId,
333
+ };
334
+ }
335
+ function captureCliError(err, ctx) {
336
+ if (!sentryAvailable)
337
+ return;
338
+ if (!workspaceSentryAllowed(currentWorkspaceConfig))
339
+ return;
340
+ Sentry.withScope((scope) => {
341
+ scope.setTag("trace_id", ctx.traceId);
342
+ scope.setTag("command", ctx.command);
343
+ scope.setTag("sub", ctx.sub ?? "none");
344
+ setSafeObservabilityContext(scope, captureObservabilityFields(ctx));
345
+ scope.setLevel("error");
346
+ Sentry.captureException(err);
347
+ });
348
+ }
349
+ function captureCliNonZeroExit(ctx) {
350
+ if (!sentryAvailable)
351
+ return;
352
+ if (!workspaceSentryAllowed(currentWorkspaceConfig))
353
+ return;
354
+ Sentry.withScope((scope) => {
355
+ scope.setTag("trace_id", ctx.traceId);
356
+ scope.setTag("command", ctx.command);
357
+ scope.setTag("sub", ctx.sub ?? "none");
358
+ scope.setTag("exit_code", String(ctx.exitCode));
359
+ setSafeObservabilityContext(scope, captureObservabilityFields(ctx));
360
+ scope.setLevel("warning");
361
+ Sentry.captureMessage(`mla ${ctx.command} exited ${ctx.exitCode}`);
362
+ });
363
+ }
364
+ // Bootstrap captures bypass the workspace gate by design: workspace config has
365
+ // not loaded yet, but we still need visibility on bad token / control
366
+ // unreachable. Tags are minimal so PII risk is zero.
367
+ function captureBootstrapError(err, ctx) {
368
+ if (!sentryAvailable)
369
+ return;
370
+ Sentry.withScope((scope) => {
371
+ scope.setTag("trace_id", ctx.traceId);
372
+ scope.setTag("phase", "bootstrap");
373
+ scope.setLevel("error");
374
+ Sentry.captureException(err);
375
+ });
376
+ }
377
+ const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
378
+ async function boundedSentryFlush() {
379
+ if (!sentryAvailable)
380
+ return;
381
+ try {
382
+ await Promise.race([Sentry.flush(500), sleep(500)]);
383
+ }
384
+ catch {
385
+ // Never block exit on Sentry flush failure.
386
+ }
387
+ }
388
+ // Per-run trace_id storage. Read by mlaFetch to stamp X-Trace-ID on every
389
+ // outbound HTTP request. Set exactly once at the top of the run.
390
+ let currentTraceId = null;
391
+ function setRunTraceId(traceId) {
392
+ currentTraceId = traceId;
393
+ }
394
+ function getRunTraceId() {
395
+ return currentTraceId;
396
+ }
397
+ // Per-run agent session id storage (the raw canonical Claude UUID, NOT the
398
+ // composed namespaced value). Read by buildIntelHeaders to stamp
399
+ // X-Agent-Session-ID on intel calls so the workspace-authoritative sink composes
400
+ // the session exactly once (INV-COMPOSE-ONCE: the raw UUID rides the wire; intel
401
+ // composes). Set exactly once at the top of the run from CLAUDE_CODE_SESSION_ID,
402
+ // already canonicalized; null when the CLI is not running inside a Claude session
403
+ // or the env value is malformed (the header is then simply not stamped).
404
+ let currentSessionId = null;
405
+ function setRunSessionId(sessionId) {
406
+ currentSessionId = sessionId;
407
+ }
408
+ function getRunSessionId() {
409
+ return currentSessionId;
410
+ }
411
+ // Test-only: clear the run-local session id so suites don't bleed state.
412
+ function resetRunSessionIdForTesting() {
413
+ currentSessionId = null;
414
+ }
415
+ // Per-run analytics run_id (INV-RUN-1). Distinct identity from trace_id: minted
416
+ // independently, never derived from it. trace_id is the cross-system
417
+ // observability key; run_id is the analytics invocation key. They are 1:1 at the
418
+ // CLI in v1, but kept separate so hooks/MCP/child-traces can mint their own
419
+ // run_id under a shared trace later. uuid (not the 32-hex trace shape) so the two
420
+ // can never be confused.
421
+ let currentRunId = null;
422
+ function mintRunId() {
423
+ return crypto.randomUUID();
424
+ }
425
+ function setRunId(runId) {
426
+ currentRunId = runId;
427
+ }
428
+ function getRunId() {
429
+ return currentRunId;
430
+ }
431
+ // Test-only: clear the run-local id so suites don't bleed state across cases.
432
+ function resetRunIdForTesting() {
433
+ currentRunId = null;
434
+ }
435
+ // Per-run repo fingerprint (analytics attribution, spec section 3.7 / T1.10). A
436
+ // NON-identifying one-way hash of the git remote/repo the run executed in,
437
+ // computed ONCE at bootstrap (the git I/O lives in computeRepoFingerprint) and
438
+ // read back by the analytics recorder. Stored as a run-local singleton, exactly
439
+ // like run_id/trace_id, so buildEvent stays a pure read and never shells out to
440
+ // git per event. null when the run is not in a git repo (or git is unavailable);
441
+ // attribution then carries a null repoFingerprint rather than a fabricated one.
442
+ let currentRepoFingerprint = null;
443
+ function setRepoFingerprint(fingerprint) {
444
+ currentRepoFingerprint = fingerprint;
445
+ }
446
+ function getRepoFingerprint() {
447
+ return currentRepoFingerprint;
448
+ }
449
+ // Test-only: clear the run-local fingerprint so suites don't bleed state.
450
+ function resetRepoFingerprintForTesting() {
451
+ currentRepoFingerprint = null;
452
+ }
453
+ // Intel-echo observation (P2.4 deep-link gate). Set true when ANY intel
454
+ // response in this run carried our X-Trace-ID back in its response headers.
455
+ // Observation only: the CLI never adopts the response id (immutability is
456
+ // preserved by mlaFetch / intelGet / intelPost; they never read it back into
457
+ // currentTraceId). The deep link is printed only when tracer.flush() succeeded
458
+ // OR this flag is true (server-side intel route already produced a Langfuse
459
+ // trace under the inbound id).
460
+ let intelEchoedRunTraceId = false;
461
+ // Thrown only in strict/debug mode (see traceStrictMode) when intel positively
462
+ // echoes a trace id that is NOT ours. Typed so a caller can distinguish a
463
+ // propagation-integrity failure from an ordinary HTTP error.
464
+ class TraceRoundTripError extends Error {
465
+ sent;
466
+ echoed;
467
+ constructor(sent, echoed) {
468
+ super(`trace-id round-trip mismatch: sent ${sent}, intel echoed ${echoed}`);
469
+ this.sent = sent;
470
+ this.echoed = echoed;
471
+ this.name = "TraceRoundTripError";
472
+ }
473
+ }
474
+ exports.TraceRoundTripError = TraceRoundTripError;
475
+ // Strict trace mode (the "strict/debug flag" of spec gap 6.5 / Phase 4). When
476
+ // MEETLESS_TRACE_STRICT or MLA_TRACE_STRICT is truthy, a round-trip MISMATCH is
477
+ // fatal (throws) instead of a one-line warning. Off by default: a mismatch is a
478
+ // confidence-check failure, not a reason to fail a user's command. CI and local
479
+ // debugging flip it on to make a silent propagation break loud.
480
+ function traceStrictMode() {
481
+ const v = process.env.MEETLESS_TRACE_STRICT || process.env.MLA_TRACE_STRICT;
482
+ if (!v)
483
+ return false;
484
+ const norm = v.trim().toLowerCase();
485
+ return norm === "1" || norm === "true" || norm === "yes" || norm === "on";
486
+ }
487
+ // P4-T2: assert the trace_id round-trip against intel's echoed X-Trace-ID.
488
+ // - absent header -> graceful no-op (older intel, or a proxy stripped it): the
489
+ // deep-link gate already falls back to didTraceFlushSucceed(), so a missing
490
+ // echo only weakens a confidence check; it never breaks a join.
491
+ // - match -> record the positive confirmation.
492
+ // - mismatch -> a real propagation break (a proxy rewrote the header, or
493
+ // intel minted a fresh id because ours never arrived). Warn by default; throw
494
+ // a TraceRoundTripError under strict/debug. Both ids are observability join
495
+ // keys, not payloads, so naming them is OBS-9-safe and is exactly what you
496
+ // need to debug the split.
497
+ function noteIntelEchoedTraceId(echoedId) {
498
+ if (!echoedId)
499
+ return;
500
+ if (!currentTraceId)
501
+ return;
502
+ const normalized = echoedId.toLowerCase();
503
+ if (normalized === currentTraceId) {
504
+ intelEchoedRunTraceId = true;
505
+ return;
506
+ }
507
+ if (traceStrictMode()) {
508
+ throw new TraceRoundTripError(currentTraceId, normalized);
509
+ }
510
+ process.stderr.write(`warn: trace-id round-trip mismatch (sent ${currentTraceId}, ` +
511
+ `intel echoed ${normalized}); traces for this run may be split\n`);
512
+ }
513
+ function didIntelEchoTraceId() {
514
+ return intelEchoedRunTraceId;
515
+ }
516
+ function resetIntelEchoForTesting() {
517
+ intelEchoedRunTraceId = false;
518
+ }
519
+ // Trace-flush outcome (P2.4 deep-link gate). True when boundedTraceFlush
520
+ // observed tracer.flush() resolve without throwing. The deep link is printed
521
+ // only when this is true OR intel echoed the run trace id; otherwise we'd be
522
+ // advertising URLs that resolve to nothing.
523
+ let traceFlushSucceeded = false;
524
+ function didTraceFlushSucceed() {
525
+ return traceFlushSucceeded;
526
+ }
527
+ function resetTraceFlushOutcomeForTesting() {
528
+ traceFlushSucceeded = false;
529
+ }
530
+ // HTTP-backed flush function the tracer calls inside flush(). POSTs the span
531
+ // batch to control's /internal/v1/agent-traces/ingest. Auth uses the same
532
+ // bearer the CLI uses for every other control hop (controlToken). workspaceId
533
+ // is injected from the CLI's run-local workspace config so control can apply
534
+ // the §9 tenant guardrail (only ws_an_local or tracing_dogfood workspaces are
535
+ // allowed to relay). On non-2xx, throw so boundedTraceFlush prints its single
536
+ // stderr line.
537
+ // The HTTP flush's per-request deadline: the AbortController aborts the fetch at
538
+ // this point. This is the AUTHORITATIVE trace-upload timeout.
539
+ exports.HTTP_FLUSH_TIMEOUT_MS = 1500;
540
+ // The outer ceiling boundedTraceFlush races tracer.flush() against. It MUST stay
541
+ // >= HTTP_FLUSH_TIMEOUT_MS so the HTTP deadline is the real timeout and this is
542
+ // only a backstop for a flush that hangs WITHOUT honoring its own deadline (e.g.
543
+ // a non-HTTP flushFn). When this was 500ms and the HTTP timeout was 1500ms, a
544
+ // slow-but-successful upload (500-1500ms) was killed by this race, dropped, and
545
+ // reported as a false "timeout" (Finding #2). The +500ms headroom lets the inner
546
+ // AbortController fire first on a genuinely slow network so the user sees the
547
+ // precise HTTP error, not this generic ceiling.
548
+ exports.TRACE_FLUSH_CEILING_MS = exports.HTTP_FLUSH_TIMEOUT_MS + 500;
549
+ function makeHttpFlush(opts) {
550
+ const timeout = opts.timeoutMs ?? exports.HTTP_FLUSH_TIMEOUT_MS;
551
+ return async (payload) => {
552
+ const url = `${opts.controlUrl}/internal/v1/agent-traces/ingest`;
553
+ const controller = new AbortController();
554
+ const timer = setTimeout(() => controller.abort(), timeout);
555
+ try {
556
+ // agent-traces ingest is a workspace-bound write behind control's
557
+ // AgentReviewWorkspaceGuard (INV-AUTH-1): it 403s "Actor identity
558
+ // required for this write" unless the caller presents an actor. lib/http.ts
559
+ // stamps X-Meetless-Actor on every other control hop; this hand-rolled
560
+ // flush must mirror that, or every self-trace upload 403s. A blank/
561
+ // whitespace-only actor is treated as absent (same as buildRequestHeaders).
562
+ const headers = {
563
+ Authorization: `Bearer ${opts.controlToken}`,
564
+ "Content-Type": "application/json",
565
+ "X-Trace-ID": payload.traceId,
566
+ };
567
+ if (opts.actorUserId && opts.actorUserId.trim().length > 0) {
568
+ headers["X-Meetless-Actor"] = opts.actorUserId;
569
+ }
570
+ const res = await fetch(url, {
571
+ method: "POST",
572
+ headers,
573
+ body: JSON.stringify({ ...payload, workspaceId: opts.workspaceId }),
574
+ signal: controller.signal,
575
+ });
576
+ if (!res.ok) {
577
+ const body = await res.text().catch(() => "");
578
+ const e = new Error(`POST ${url} -> HTTP ${res.status}: ${body.slice(0, 200)}`);
579
+ e.status = res.status;
580
+ // §9 tenant guardrail: a 403 TRACING_NOT_ENABLED_FOR_WORKSPACE is a
581
+ // deliberate policy refusal (this workspace simply does not relay CLI
582
+ // self-traces), not a failure. Tag it so boundedTraceFlush skips the
583
+ // user-facing warning. Any other non-2xx (auth 403, 5xx, etc.) stays
584
+ // a real, warnable failure.
585
+ if (res.status === 403 && body.includes(TRACING_DISABLED_CODE)) {
586
+ e.tracingDisabledByPolicy = true;
587
+ }
588
+ throw e;
589
+ }
590
+ }
591
+ finally {
592
+ clearTimeout(timer);
593
+ }
594
+ };
595
+ }
596
+ // Control's §9 error code for "this workspace is not authorized to relay
597
+ // traces" (apps/control api-exception.ts TRACING_NOT_ENABLED_FOR_WORKSPACE).
598
+ // Shared by the tag site (makeHttpFlush) and the classifier so the two cannot
599
+ // drift on the literal.
600
+ const TRACING_DISABLED_CODE = "TRACING_NOT_ENABLED_FOR_WORKSPACE";
601
+ // True when a flush error is the §9 tracing-policy refusal: either tagged by
602
+ // makeHttpFlush, or (defense in depth, if the tag is ever lost in transit) a
603
+ // 403 whose message carries the policy code. Such a refusal is expected and
604
+ // must NOT surface a "trace upload failed" warning.
605
+ function isTracingPolicyDisabled(err) {
606
+ if (!err || typeof err !== "object")
607
+ return false;
608
+ const e = err;
609
+ if (e.tracingDisabledByPolicy === true)
610
+ return true;
611
+ return (e.status === 403 &&
612
+ typeof e.message === "string" &&
613
+ e.message.includes(TRACING_DISABLED_CODE));
614
+ }
615
+ function describeFlushErr(err) {
616
+ if (err && typeof err === "object") {
617
+ const e = err;
618
+ if (e.status)
619
+ return `HTTP ${e.status}`;
620
+ if (e.name === "AbortError")
621
+ return "timeout";
622
+ if (e.message)
623
+ return e.message.slice(0, 120);
624
+ }
625
+ return String(err).slice(0, 120);
626
+ }
627
+ // Per-run tracer storage. Read by mlaFetch / intelGet / intelPost to start
628
+ // child spans around each outbound HTTP call. Set exactly once at the top of
629
+ // the run (cli.ts runCliBootstrap). Null when the CLI is invoked without a
630
+ // reachable config (e.g. mla init) so HTTP layers can no-op cheaply.
631
+ let currentTracer = null;
632
+ function setRunTracer(tracer) {
633
+ currentTracer = tracer;
634
+ }
635
+ function getRunTracer() {
636
+ return currentTracer;
637
+ }
638
+ function resetRunTracerForTesting() {
639
+ currentTracer = null;
640
+ }
641
+ // Convert an outbound URL path into the right side of an `intel.<route>` or
642
+ // `control.<route>` span name. Drops `/internal/v1/` / `/v1/` prefixes, strips
643
+ // query strings, and collapses obvious id-shaped path segments to `:id` so a
644
+ // fleet of `coordination-cases.cse_xxxx` spans roll up cleanly in Langfuse.
645
+ const ID_LIKE = /^([a-z]+_[A-Za-z0-9]{4,}|[0-9a-f]{20,}|[0-9a-fA-F-]{36})$/;
646
+ function routeNameFromPath(rawPath) {
647
+ const qIdx = rawPath.indexOf("?");
648
+ const noQuery = qIdx >= 0 ? rawPath.slice(0, qIdx) : rawPath;
649
+ let p = noQuery;
650
+ if (p.startsWith("/internal/v1/"))
651
+ p = p.slice("/internal/v1/".length);
652
+ else if (p.startsWith("/v1/"))
653
+ p = p.slice("/v1/".length);
654
+ else if (p.startsWith("/"))
655
+ p = p.slice(1);
656
+ const parts = p.split("/").filter((s) => s.length > 0);
657
+ const sanitized = parts.map((s) => {
658
+ let token;
659
+ try {
660
+ token = decodeURIComponent(s);
661
+ }
662
+ catch {
663
+ token = s;
664
+ }
665
+ return ID_LIKE.test(token) ? ":id" : token;
666
+ });
667
+ return sanitized.join(".") || "root";
668
+ }
669
+ // Argv redaction for the root span attribute (P2.2 + spec §10.P2 must-test 6).
670
+ // Reuses the shared secret redactor so a token leaked on the command line is
671
+ // stripped before the trace reaches Langfuse. The redactor returns the input
672
+ // unchanged for empty strings, otherwise a transformed string; the cast is
673
+ // safe because every input here is already a string.
674
+ function redactArgvForSpan(argv) {
675
+ return argv.map((arg) => (0, redactor_1.redact)(arg));
676
+ }
677
+ // Canonical Langfuse trace URL. ONE algorithm, mirrored byte-for-byte by the
678
+ // Python twin (intel app/core/observability_context.py: langfuse_trace_url). A
679
+ // Python-built URL that drifts from this shape produces a Sentry deep-link that
680
+ // 404s, so the two are locked together by a cross-language fixture test
681
+ // (test/fixtures/langfuse-url-fixtures.json; spec gap 6.3). The CLI has no
682
+ // self-host config, so the host is always the Langfuse Cloud default.
683
+ function langfuseTraceUrl(projectId, traceId) {
684
+ return `https://cloud.langfuse.com/project/${projectId}/traces/${traceId}`;
685
+ }
686
+ // P2.3 / spec §8 deep-link printer. Gate is the explicit boolean conjunction:
687
+ // workspace config loaded, tracing.enabled true, langfuseProjectId set, AND
688
+ // (flush succeeded OR intel echoed the inbound X-Trace-ID). Any single
689
+ // condition failing returns false and prints nothing; printing when no trace
690
+ // landed advertises dead URLs.
691
+ function langfuseDeepLink(projectId, traceId) {
692
+ return `trace: ${langfuseTraceUrl(projectId, traceId)}`;
693
+ }
694
+ function shouldPrintDeepLink(opts) {
695
+ if (!opts.config)
696
+ return false;
697
+ if (opts.config.tracing?.enabled !== true)
698
+ return false;
699
+ if (!opts.config.tracing.langfuseProjectId)
700
+ return false;
701
+ return opts.flushSucceeded || opts.intelEchoed;
702
+ }
703
+ function maybePrintDeepLink(opts) {
704
+ if (!shouldPrintDeepLink(opts))
705
+ return false;
706
+ const projectId = opts.config.tracing.langfuseProjectId;
707
+ process.stdout.write(`${langfuseDeepLink(projectId, opts.traceId)}\n`);
708
+ return true;
709
+ }
710
+ // Build the run's tracer. Returns a no-op tracer when no HTTP flush function
711
+ // is available (mla init / no config / fully-offline command). Otherwise
712
+ // returns a real tracer whose flush POSTs to control's relay. flush() is
713
+ // always called once (via boundedTraceFlush in cli.ts); the no-op tracer
714
+ // resolves immediately so the lifecycle is unconditional.
715
+ function createRunTracer(opts) {
716
+ if (!opts.flushFn) {
717
+ return makeNoopTracer({ traceId: opts.traceId });
718
+ }
719
+ return makeTracer({
720
+ traceId: opts.traceId,
721
+ rootName: opts.rootName,
722
+ client: {
723
+ mlaVersion: opts.buildInfo.version,
724
+ platform: process.platform,
725
+ },
726
+ flushFn: opts.flushFn,
727
+ });
728
+ }
729
+ // Visible-failure stderr line on flush error (spec §6.2). Exactly one line, no
730
+ // retry, no persistence. Caller's command exit code is preserved; this never
731
+ // throws so it cannot promote a successful command into a failed one.
732
+ //
733
+ // The ceiling uses a cleared setTimeout (not sleep().then(throw)) so the timer
734
+ // is released when flush wins; otherwise it would hold the event loop open until
735
+ // the full ceiling elapses after success (functionally masked by CLI's
736
+ // process.exit, but leaks open handles in jest and would block any programmatic
737
+ // caller). The ceiling defaults to TRACE_FLUSH_CEILING_MS, kept wider than the
738
+ // HTTP flush's own deadline so a slow-but-successful upload is not killed here
739
+ // (Finding #2); callers/tests may pass a tighter ceiling.
740
+ async function boundedTraceFlush(tracer, ceilingMs = exports.TRACE_FLUSH_CEILING_MS) {
741
+ let timer = null;
742
+ let flushErr = null;
743
+ const timeoutPromise = new Promise((_, reject) => {
744
+ timer = setTimeout(() => {
745
+ const err = new Error(`timeout: trace flush exceeded ${ceilingMs}ms`);
746
+ flushErr = err;
747
+ reject(err);
748
+ }, ceilingMs);
749
+ });
750
+ try {
751
+ let flushPromise;
752
+ try {
753
+ flushPromise = Promise.resolve(tracer.flush());
754
+ }
755
+ catch (err) {
756
+ // A synchronous throw inside tracer.flush() before it returns a promise.
757
+ flushPromise = Promise.reject(err);
758
+ }
759
+ await Promise.race([
760
+ flushPromise.catch((e) => {
761
+ flushErr = e;
762
+ throw e;
763
+ }),
764
+ timeoutPromise,
765
+ ]);
766
+ traceFlushSucceeded = true;
767
+ }
768
+ catch {
769
+ // Not relayed either way, so no trace URL to advertise (cli.ts gates on
770
+ // didTraceFlushSucceed()).
771
+ traceFlushSucceeded = false;
772
+ // A §9 tracing-policy refusal is expected, not a failure: stay silent.
773
+ // Every real failure (auth, 5xx, connection refused, timeout) still warns.
774
+ if (!isTracingPolicyDisabled(flushErr)) {
775
+ process.stderr.write(`warn: trace upload failed (${describeFlushErr(flushErr)}); ` +
776
+ `Sentry event still carries trace_id\n`);
777
+ // F8 (telemetry-upload-failed): the trace upload itself failed on a real,
778
+ // non-policy error. Record it to the local deadletter so the failure is
779
+ // never silently lost (a telemetry system that fails silently gives false
780
+ // "no alerts = no problems"). Per INV-SENTRY-NOISE-BUDGET this is
781
+ // local-deadletter + the local warning above, NOT a Sentry event from the
782
+ // CLI. recordTelemetryUploadFailure respects the kill switch and never
783
+ // throws; the extra guard keeps a future change from breaking the flush
784
+ // path (this must never promote a successful command into a failed one).
785
+ try {
786
+ const wsCfg = getWorkspaceConfig();
787
+ const status = flushErr?.status;
788
+ (0, failure_telemetry_1.recordTelemetryUploadFailure)({
789
+ traceId: tracer.traceId,
790
+ workspaceId: wsCfg?.workspaceId ?? null,
791
+ surface: "mla-cli",
792
+ reasonCode: "trace_upload_failed",
793
+ status: typeof status === "number" ? status : undefined,
794
+ });
795
+ }
796
+ catch {
797
+ // F8 recording is best-effort; swallow so flush stays non-throwing.
798
+ }
799
+ }
800
+ }
801
+ finally {
802
+ if (timer)
803
+ clearTimeout(timer);
804
+ }
805
+ }