@moda-ai/cli 1.22.1 → 1.22.2

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 (115) hide show
  1. package/dist/cli-g8qs2tj9.js +9777 -0
  2. package/dist/cli-pwbvtes8.js +2312 -0
  3. package/dist/cli-qszcpda3.js +284 -0
  4. package/dist/cli.js +4775 -1159
  5. package/dist/index-6h0nc1r5.js +2765 -0
  6. package/dist/provision-j5gq8cvx.js +72 -0
  7. package/package.json +3 -2
  8. package/skills/integration/index.json +2 -2
  9. package/dist/agent-schemas.d.ts +0 -3
  10. package/dist/agent-schemas.js +0 -358
  11. package/dist/agent.d.ts +0 -5
  12. package/dist/agent.js +0 -477
  13. package/dist/analyst-tui.d.ts +0 -81
  14. package/dist/analyst-tui.js +0 -314
  15. package/dist/api-client.d.ts +0 -46
  16. package/dist/api-client.js +0 -120
  17. package/dist/ask-stream.d.ts +0 -146
  18. package/dist/ask-stream.js +0 -473
  19. package/dist/auth-storage.d.ts +0 -35
  20. package/dist/auth-storage.js +0 -159
  21. package/dist/auth.d.ts +0 -50
  22. package/dist/auth.js +0 -694
  23. package/dist/cli-session.d.ts +0 -26
  24. package/dist/cli-session.js +0 -169
  25. package/dist/cli.d.ts +0 -44
  26. package/dist/command-context.d.ts +0 -62
  27. package/dist/command-context.js +0 -57
  28. package/dist/command-registry.d.ts +0 -22
  29. package/dist/command-registry.js +0 -15
  30. package/dist/config.d.ts +0 -60
  31. package/dist/config.js +0 -110
  32. package/dist/doctor.d.ts +0 -79
  33. package/dist/doctor.js +0 -1182
  34. package/dist/errors.d.ts +0 -50
  35. package/dist/errors.js +0 -64
  36. package/dist/exit-funnel.d.ts +0 -14
  37. package/dist/exit-funnel.js +0 -53
  38. package/dist/harness-artifact-bodies.d.ts +0 -28
  39. package/dist/harness-artifact-bodies.js +0 -276
  40. package/dist/harness-candidates.d.ts +0 -77
  41. package/dist/harness-candidates.js +0 -482
  42. package/dist/harness-remote.d.ts +0 -135
  43. package/dist/harness-remote.js +0 -540
  44. package/dist/harness-report.d.ts +0 -145
  45. package/dist/harness-report.js +0 -1579
  46. package/dist/harness-sync-limits.d.ts +0 -8
  47. package/dist/harness-sync-limits.js +0 -21
  48. package/dist/harness.d.ts +0 -463
  49. package/dist/harness.js +0 -4009
  50. package/dist/init/index.d.ts +0 -78
  51. package/dist/init/index.js +0 -1442
  52. package/dist/init/mcp.d.ts +0 -2
  53. package/dist/init/mcp.js +0 -27
  54. package/dist/init/progress-board.d.ts +0 -80
  55. package/dist/init/progress-board.js +0 -225
  56. package/dist/init/rules.d.ts +0 -6
  57. package/dist/init/rules.js +0 -171
  58. package/dist/init/sdk-catalog.d.ts +0 -10
  59. package/dist/init/sdk-catalog.js +0 -80
  60. package/dist/init/sdk-guide.d.ts +0 -19
  61. package/dist/init/sdk-guide.js +0 -185
  62. package/dist/init/sdk-integration.d.ts +0 -53
  63. package/dist/init/sdk-integration.js +0 -835
  64. package/dist/init/sdk-protocol.d.ts +0 -135
  65. package/dist/init/sdk-protocol.js +0 -152
  66. package/dist/init/sdk-verification.d.ts +0 -27
  67. package/dist/init/sdk-verification.js +0 -123
  68. package/dist/init/tenant.d.ts +0 -67
  69. package/dist/init/tenant.js +0 -421
  70. package/dist/intelligence-schema.d.ts +0 -349
  71. package/dist/intelligence-schema.js +0 -174
  72. package/dist/keys.d.ts +0 -47
  73. package/dist/keys.js +0 -136
  74. package/dist/manifest.d.ts +0 -5
  75. package/dist/manifest.js +0 -114
  76. package/dist/markdown-terminal.d.ts +0 -49
  77. package/dist/markdown-terminal.js +0 -212
  78. package/dist/output.d.ts +0 -191
  79. package/dist/output.js +0 -624
  80. package/dist/production-intelligence.d.ts +0 -175
  81. package/dist/production-intelligence.js +0 -1430
  82. package/dist/profile-commands.d.ts +0 -10
  83. package/dist/profile-commands.js +0 -342
  84. package/dist/profiles.d.ts +0 -112
  85. package/dist/profiles.js +0 -573
  86. package/dist/prompts-ab.d.ts +0 -71
  87. package/dist/prompts-ab.js +0 -313
  88. package/dist/prompts-propose.d.ts +0 -15
  89. package/dist/prompts-propose.js +0 -212
  90. package/dist/prompts.d.ts +0 -58
  91. package/dist/prompts.js +0 -418
  92. package/dist/provision.d.ts +0 -28
  93. package/dist/provision.js +0 -104
  94. package/dist/redact.d.ts +0 -2
  95. package/dist/redact.js +0 -90
  96. package/dist/schemas.d.ts +0 -225
  97. package/dist/schemas.js +0 -126
  98. package/dist/skills-bundled.d.ts +0 -35
  99. package/dist/skills-bundled.js +0 -157
  100. package/dist/skills-manifest.d.ts +0 -33
  101. package/dist/skills-manifest.js +0 -244
  102. package/dist/skills.d.ts +0 -57
  103. package/dist/skills.js +0 -783
  104. package/dist/sync.d.ts +0 -5
  105. package/dist/sync.js +0 -36
  106. package/dist/telemetry.d.ts +0 -56
  107. package/dist/telemetry.js +0 -167
  108. package/dist/transforms.d.ts +0 -25
  109. package/dist/transforms.js +0 -102
  110. package/dist/tui.d.ts +0 -48
  111. package/dist/tui.js +0 -393
  112. package/dist/update-check.d.ts +0 -31
  113. package/dist/update-check.js +0 -141
  114. package/dist/workspace.d.ts +0 -12
  115. package/dist/workspace.js +0 -1294
@@ -1,1430 +0,0 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { callControlAPI, callDataAPI } from './api-client.js';
4
- import { streamAsk, streamFallbackNotice } from './ask-stream.js';
5
- import { resolveModaBaseUrl } from './config.js';
6
- import { CliAuthError, CliInputError } from './errors.js';
7
- import { EXIT_DEGRADED, EXIT_OK, safeWriteStderr } from './output.js';
8
- import { renderMarkdownForTerminal, shouldColorTerminalOutput, stripAnsi, terminalStyles, } from './markdown-terminal.js';
9
- import { asNumber, asRecord, asString } from './transforms.js';
10
- import { INTELLIGENCE_SCHEMA_VERSION, } from './intelligence-schema.js';
11
- const INVESTIGATION_SCHEMA_VERSION = 'production_investigation.v0.1';
12
- const OVERVIEW_SCHEMA_VERSION = 'production_overview.v0.1';
13
- const ASK_SCHEMA_VERSION = 'production_ask.v0.1';
14
- export async function runProductionCommand(context) {
15
- const daysBack = numberFlag(context.flags, 'days-back', 7);
16
- if (context.command === 'overview') {
17
- const briefing = await loadOverviewBriefing({ daysBack, cwd: context.cwd });
18
- if (context.flags.raw === 'true') {
19
- context.output.writeData(briefing.rawOverview, {
20
- summary: 'Loaded raw Moda overview.',
21
- nextCommands: ['moda overview', `moda investigate --days-back=${daysBack}`],
22
- });
23
- }
24
- else if (context.outputMode === 'human') {
25
- if (!context.quiet)
26
- console.log(renderOverviewBriefingHuman(briefing));
27
- }
28
- else {
29
- context.output.writeData(briefing, overviewAgentOptions(briefing));
30
- }
31
- return { exitCode: 0 };
32
- }
33
- if (context.command === 'ask') {
34
- const question = context.positionals.join(' ').trim();
35
- if (!question) {
36
- throw new CliInputError('<question> is required', 'Usage: moda ask "what should I fix first?" [--days-back=N]');
37
- }
38
- const result = await askModaForContext(context, question, daysBack);
39
- if (context.outputMode === 'human') {
40
- if (!context.quiet)
41
- console.log(renderAskHuman(result));
42
- }
43
- else {
44
- // The agent/JSON `data` payload is the canonical moda.intelligence.v1
45
- // value (carrying evidence grounding + per-claim verdicts). The
46
- // moda.agent.v1 envelope still wraps it; only the inner `data` is v1.
47
- context.output.writeData(result.intelligence, {
48
- ...askAgentOptions(result),
49
- status: result.degraded ? 'degraded' : 'success',
50
- });
51
- }
52
- // Exit 3 on a degraded/local-fallback answer so a scripted agent can detect
53
- // it from the exit code alone (in addition to `data.degraded` / status).
54
- return { exitCode: result.degraded ? EXIT_DEGRADED : EXIT_OK };
55
- }
56
- if (context.command === 'failures') {
57
- const investigation = await investigateProduction({
58
- daysBack,
59
- cwd: context.cwd,
60
- failureOnly: true,
61
- toolName: context.flags.tool,
62
- });
63
- writeInvestigation(context, investigation);
64
- return { exitCode: 0 };
65
- }
66
- if (context.command === 'investigate') {
67
- const investigation = await investigateProduction({
68
- daysBack,
69
- cwd: context.cwd,
70
- conversationId: context.flags.conversation,
71
- toolName: context.flags.tool,
72
- });
73
- writeInvestigation(context, investigation);
74
- return { exitCode: 0 };
75
- }
76
- throw new CliInputError(`Unknown production command '${context.command}'`);
77
- }
78
- export async function loadOverviewBriefing(options) {
79
- const endpoint = `/overview?days_back=${options.daysBack}`;
80
- const overview = await callDataAPI(endpoint);
81
- const hints = loadHarnessHints(options.cwd);
82
- const evidenceRefs = [
83
- {
84
- id: 'overview:data',
85
- kind: 'data_api',
86
- label: `Overview for last ${options.daysBack} day(s)`,
87
- endpoint,
88
- value: overviewMetrics(overview),
89
- },
90
- ];
91
- const findings = findingsFromOverview(overview, options.daysBack, hints, evidenceRefs);
92
- const topFinding = findings[0] ?? null;
93
- const conversations = numberAt(overview, ['conversations', 'total']);
94
- const recent = recentActivity(overview)[0];
95
- const dataFlowStatus = conversations > 0 || recent ? 'flowing' : 'quiet';
96
- const nextCommand = topFinding
97
- ? `moda investigate --days-back=${options.daysBack}`
98
- : 'moda doctor --online';
99
- return {
100
- schemaVersion: OVERVIEW_SCHEMA_VERSION,
101
- generatedAt: new Date().toISOString(),
102
- daysBack: options.daysBack,
103
- status: topFinding
104
- ? 'attention'
105
- : dataFlowStatus === 'flowing'
106
- ? 'healthy'
107
- : 'quiet',
108
- dataFlow: {
109
- status: dataFlowStatus,
110
- conversations,
111
- ...(recent ? { lastEvent: recent } : {}),
112
- },
113
- harnessSync: localSyncBrief(options.cwd, '.moda/sync-state.json', 'Harness graph'),
114
- promptSync: localSyncBrief(options.cwd, '.moda/prompts.lock.json', 'Prompt versions'),
115
- skillSync: localSkillBrief(options.cwd),
116
- metrics: overviewMetrics(overview),
117
- topFinding,
118
- evidenceRefs,
119
- nextCommand,
120
- rawOverview: overview,
121
- };
122
- }
123
- export async function investigateProduction(options) {
124
- const warnings = [];
125
- const sourceStatus = {};
126
- const hints = loadHarnessHints(options.cwd);
127
- const overviewEndpoint = `/overview?days_back=${options.daysBack}`;
128
- const toolEndpoint = options.toolName
129
- ? `/tool-failures/${encodeURIComponent(options.toolName)}?days_back=${options.daysBack}&limit=5`
130
- : `/tool-failures?days_back=${options.daysBack}`;
131
- const frustrationsEndpoint = `/frustrations?days_back=${options.daysBack}&limit=5`;
132
- const [overview, tools, frustrations] = await Promise.all([
133
- safeData(overviewEndpoint),
134
- safeData(toolEndpoint),
135
- safeData(frustrationsEndpoint),
136
- ]);
137
- for (const [key, result] of Object.entries({ overview, tools, frustrations })) {
138
- sourceStatus[key] = result.status;
139
- if (result.warning)
140
- warnings.push(result.warning);
141
- }
142
- const evidenceRefs = [];
143
- const findings = [];
144
- addOverviewEvidence(overview.data, overviewEndpoint, evidenceRefs);
145
- findings.push(...toolFailureFindings(tools.data, options.daysBack, hints, evidenceRefs, options.toolName));
146
- if (!options.failureOnly) {
147
- findings.push(...frustrationFindings(frustrations.data, options.daysBack, evidenceRefs));
148
- }
149
- if (findings.length === 0) {
150
- findings.push(insufficientDataFinding(overview.data, evidenceRefs));
151
- }
152
- findings.sort(compareFindings);
153
- const boundedFindings = findings.slice(0, 8);
154
- const nextCommands = nextCommandsForFindings(boundedFindings, options.daysBack);
155
- return {
156
- schemaVersion: INVESTIGATION_SCHEMA_VERSION,
157
- generatedAt: new Date().toISOString(),
158
- daysBack: options.daysBack,
159
- scope: {
160
- ...(options.conversationId ? { conversationId: options.conversationId } : {}),
161
- ...(options.toolName ? { toolName: options.toolName } : {}),
162
- ...(options.question ? { question: options.question } : {}),
163
- },
164
- summary: summarizeInvestigation(boundedFindings),
165
- findings: boundedFindings,
166
- evidenceRefs: uniqueEvidenceRefs(evidenceRefs),
167
- nextCommands,
168
- warnings,
169
- sourceStatus,
170
- };
171
- }
172
- export async function askModa(options) {
173
- // Capture *why* the cloud path was not used so the caller (human or agent)
174
- // can see it, instead of silently swallowing the error and masquerading the
175
- // local fallback as a normal answer.
176
- let fallbackReason = 'Moda Cloud ask endpoint returned no answer.';
177
- try {
178
- const response = await callControlAPI('/cli/ask', {
179
- method: 'POST',
180
- body: JSON.stringify({
181
- question: options.question,
182
- daysBack: options.daysBack,
183
- profile: options.profile,
184
- harness: localHarnessSummary(options.cwd),
185
- }),
186
- }, options.profile ? { profile: options.profile } : {}, {
187
- timeoutMs: askTimeoutMs(),
188
- retries: 0,
189
- // A stream that just dropped can poison undici's pooled connection to
190
- // this origin; without this the buffered fallback fails instantly at
191
- // the transport level against a healthy backend. One retry, network
192
- // faults only — HTTP errors and the run timeout still never retry.
193
- retryOnceOnNetworkError: true,
194
- onNetworkRetry: options.onNetworkRetry,
195
- });
196
- const cloud = normalizeCloudAsk(response, options.question);
197
- if (cloud)
198
- return cloud;
199
- fallbackReason = 'Moda Cloud ask endpoint returned an empty answer.';
200
- }
201
- catch (error) {
202
- fallbackReason = `Moda Cloud ask endpoint unavailable: ${error instanceof Error ? error.message : String(error)}`;
203
- }
204
- const investigation = await investigateProduction({
205
- daysBack: options.daysBack,
206
- cwd: options.cwd,
207
- question: options.question,
208
- });
209
- const top = investigation.findings[0];
210
- const answer = top
211
- ? `Moda Cloud intelligence is not available, so I synthesized this from production evidence. The strongest signal is: ${top.title}. ${top.summary}`
212
- : 'Moda Cloud intelligence is not available, and the local Data API evidence did not contain a strong production signal.';
213
- const confidence = capFallbackConfidence(investigation.summary.confidence);
214
- const evidenceRefs = investigation.evidenceRefs.slice(0, 12);
215
- const findings = investigation.findings.slice(0, 5);
216
- const nextCommands = investigation.nextCommands;
217
- const warnings = [
218
- 'Cloud ask endpoint unavailable; synthesized answer from local Data API evidence.',
219
- fallbackReason,
220
- ...investigation.warnings,
221
- ];
222
- return {
223
- schemaVersion: ASK_SCHEMA_VERSION,
224
- generatedAt: new Date().toISOString(),
225
- source: 'local_fallback',
226
- degraded: true,
227
- fallbackReason,
228
- question: options.question,
229
- answer,
230
- confidence,
231
- evidenceRefs,
232
- findings,
233
- nextCommands,
234
- warnings,
235
- // Degraded answers still carry a v1-shaped payload so every surface reads
236
- // one contract; `source`/`degraded` flag the lower trust (preserving exit
237
- // code 3 + status 'degraded').
238
- intelligence: synthesizeIntelligenceV1({
239
- question: options.question,
240
- answer,
241
- confidence,
242
- evidenceRefs,
243
- nextCommands,
244
- warnings,
245
- source: 'local_fallback',
246
- degraded: true,
247
- findings,
248
- }),
249
- };
250
- }
251
- /**
252
- * Whether `moda ask` should use the live SSE endpoint: only for a human
253
- * watching a TTY, and only when not explicitly opted out via `--no-stream`
254
- * (or `--stream=false`). `--json`/`--agent`/piped/quiet callers always take
255
- * the buffered path so machine-readable contracts are untouched.
256
- */
257
- export function shouldStreamAsk(context, isTTY = process.stdout.isTTY === true) {
258
- if (context.outputMode !== 'human' || context.quiet)
259
- return false;
260
- if ('no-stream' in context.flags)
261
- return false;
262
- if (context.flags.stream === 'false')
263
- return false;
264
- return isTTY;
265
- }
266
- /**
267
- * Human-TTY ask entrypoint: try the streaming endpoint for live progress and
268
- * fall back to the buffered `askModa` on ANY stream problem (endpoint missing,
269
- * connection dropped mid-run, framing error). The fallback is transparent —
270
- * a note is printed only in verbose mode — so the outcome is never worse than
271
- * the buffered path.
272
- */
273
- async function askModaForContext(context, question, daysBack) {
274
- if (shouldStreamAsk(context)) {
275
- // Progress lines go to stderr, so key color off the stderr TTY. Tool cards
276
- // (`• …`), deliberation lines (`◦ …`), and liveness ticks (`… …`) are
277
- // dimmed subtly; phase lines (`▸ …`) stay at full strength.
278
- const progressStyle = terminalStyles(shouldColorTerminalOutput({ isTTY: process.stderr.isTTY === true }));
279
- const dimmed = (line) => line.startsWith('•') || line.startsWith('◦') || line.startsWith('…');
280
- try {
281
- const outcome = await streamAsk({
282
- question,
283
- daysBack,
284
- profile: context.profile,
285
- harness: localHarnessSummary(context.cwd),
286
- timeoutMs: askTimeoutMs(),
287
- onProgress: (line) => safeWriteStderr(`${dimmed(line) ? progressStyle.dim(line) : line}\n`),
288
- });
289
- return askResultFromStreamOutcome(outcome, question);
290
- }
291
- catch (error) {
292
- // Auth problems must keep their structured exit-4 handling.
293
- if (error instanceof CliAuthError)
294
- throw error;
295
- const notice = streamFallbackNotice(error);
296
- if (notice) {
297
- // The stream died AFTER substantial visible progress: the buffered
298
- // re-run roughly doubles the wait, so say so instead of going silent.
299
- safeWriteStderr(`[moda] ${notice}\n`);
300
- }
301
- else if (context.verbose) {
302
- safeWriteStderr(`[moda] live progress unavailable (${error instanceof Error ? error.message : String(error)}); using buffered ask.\n`);
303
- }
304
- }
305
- }
306
- return askModa({
307
- question,
308
- daysBack,
309
- cwd: context.cwd,
310
- profile: context.profile,
311
- onNetworkRetry: context.verbose
312
- ? () => safeWriteStderr('[moda] network hiccup reaching Moda Cloud; retrying the buffered ask once…\n')
313
- : undefined,
314
- });
315
- }
316
- /**
317
- * Project a completed stream into the same `ProductionAskResult` the buffered
318
- * path produces, so the final human render is identical. A terminal `error`
319
- * event mirrors the buffered error contract (a generic answer with `unknown`
320
- * confidence — the buffered endpoint also returns HTTP 200 in that case).
321
- */
322
- export function askResultFromStreamOutcome(outcome, question) {
323
- const answer = outcome.errorMessage ?? outcome.answer;
324
- const confidence = outcome.errorMessage
325
- ? 'unknown'
326
- : normalizeConfidence(outcome.confidence);
327
- const evidenceRefs = outcome.errorMessage
328
- ? []
329
- : outcome.citations.map((citation) => ({
330
- id: citation.id,
331
- kind: evidenceKind(citation.source),
332
- label: citation.label,
333
- href: citation.href,
334
- }));
335
- const nextCommands = outcome.errorMessage ? [] : outcome.nextCommands;
336
- const warnings = outcome.errorMessage
337
- ? ['The analysis agent failed; returning a generic message.']
338
- : [];
339
- return {
340
- schemaVersion: ASK_SCHEMA_VERSION,
341
- generatedAt: new Date().toISOString(),
342
- source: 'cloud',
343
- degraded: false,
344
- question,
345
- answer,
346
- confidence,
347
- evidenceRefs,
348
- findings: [],
349
- nextCommands,
350
- warnings,
351
- intelligence: synthesizeIntelligenceV1({
352
- question,
353
- answer,
354
- confidence,
355
- evidenceRefs,
356
- nextCommands,
357
- warnings,
358
- source: 'cloud',
359
- degraded: false,
360
- }),
361
- };
362
- }
363
- /**
364
- * Timeout (ms) for the cloud `ask` POST. The backend enforces its own ~240s run
365
- * deadline under a 300s function limit, so the client waits long enough to
366
- * receive a real answer rather than aborting a healthy run at 30s and silently
367
- * falling back. Override with `MODA_ASK_TIMEOUT_MS`.
368
- */
369
- function askTimeoutMs() {
370
- const raw = Number(process.env.MODA_ASK_TIMEOUT_MS);
371
- return Number.isFinite(raw) && raw > 0 ? raw : 250_000;
372
- }
373
- function writeInvestigation(context, investigation) {
374
- if (context.outputMode === 'human') {
375
- if (!context.quiet)
376
- console.log(renderInvestigationHuman(investigation));
377
- return;
378
- }
379
- context.output.writeData(investigation, investigationAgentOptions(investigation));
380
- }
381
- async function safeData(endpoint) {
382
- try {
383
- return { data: await callDataAPI(endpoint), status: 'ok' };
384
- }
385
- catch (error) {
386
- return {
387
- data: null,
388
- status: 'unavailable',
389
- warning: `Could not load ${endpoint}: ${error instanceof Error ? error.message : String(error)}`,
390
- };
391
- }
392
- }
393
- function findingsFromOverview(overview, daysBack, hints, evidenceRefs) {
394
- const findings = [];
395
- const toolFailureTotal = numberAt(overview, ['tool_failures', 'total']);
396
- if (toolFailureTotal > 0) {
397
- const conversations = numberAt(overview, ['tool_failures', 'conversations']);
398
- const evidenceId = 'overview:tool_failures';
399
- evidenceRefs.push({
400
- id: evidenceId,
401
- kind: 'data_api',
402
- label: `${toolFailureTotal} tool failure(s) across ${conversations} conversation(s)`,
403
- endpoint: `/overview?days_back=${daysBack}`,
404
- value: valueAt(overview, ['tool_failures']),
405
- });
406
- findings.push({
407
- id: 'finding_overview_tool_failures',
408
- kind: 'tool_failure',
409
- title: `${toolFailureTotal} tool failure(s) need investigation`,
410
- severity: severityForCount(toolFailureTotal, 10, 3),
411
- confidence: 'medium',
412
- impactScore: 80 + Math.min(toolFailureTotal, 20),
413
- rankReason: 'Tool failures directly block agent tasks and have explicit production evidence.',
414
- summary: `Moda saw ${toolFailureTotal} failed tool call(s) in the last ${daysBack} day(s).`,
415
- evidenceRefIds: [evidenceId],
416
- likelyLocations: defaultLikelyLocations(hints, 'tool'),
417
- recommendedActions: [{
418
- id: 'action_investigate_tools',
419
- title: 'Open the ranked production investigation.',
420
- rationale: 'The overview only has aggregate counts; investigation pulls examples and likely file paths.',
421
- command: `moda investigate --days-back=${daysBack}`,
422
- mutability: 'read',
423
- requiresApproval: false,
424
- }],
425
- });
426
- }
427
- const frustrationRate = numberAt(overview, ['frustrations', 'rate_pct']);
428
- const frustrated = numberAt(overview, ['frustrations', 'frustrated']);
429
- if (frustrated > 0 || frustrationRate >= 10) {
430
- const evidenceId = 'overview:frustrations';
431
- evidenceRefs.push({
432
- id: evidenceId,
433
- kind: 'data_api',
434
- label: `${frustrationRate}% frustration rate`,
435
- endpoint: `/overview?days_back=${daysBack}`,
436
- value: valueAt(overview, ['frustrations']),
437
- });
438
- findings.push({
439
- id: 'finding_overview_frustration',
440
- kind: 'frustration',
441
- title: `${frustrationRate}% user frustration rate`,
442
- severity: frustrationRate >= 25 ? 'high' : 'medium',
443
- confidence: 'medium',
444
- impactScore: 65 + Math.min(frustrationRate, 30),
445
- rankReason: 'Frustration indicates users are getting stuck even when runs may not hard-fail.',
446
- summary: `Moda classified ${frustrated} frustrated conversation(s) in the last ${daysBack} day(s).`,
447
- evidenceRefIds: [evidenceId],
448
- likelyLocations: defaultLikelyLocations(hints, 'prompt'),
449
- recommendedActions: [{
450
- id: 'action_investigate_frustration',
451
- title: 'Inspect frustration anchors.',
452
- rationale: 'Frustration anchors show the user turn where the experience degraded.',
453
- command: `moda frustrations --days-back=${daysBack} --include-window`,
454
- mutability: 'read',
455
- requiresApproval: false,
456
- }],
457
- });
458
- }
459
- return findings.sort(compareFindings);
460
- }
461
- function toolFailureFindings(data, daysBack, hints, evidenceRefs, scopedTool) {
462
- const tools = toolRows(data, scopedTool);
463
- return tools.flatMap((tool) => {
464
- const name = tool.name;
465
- if (!name)
466
- return [];
467
- const count = tool.count;
468
- if (count <= 0)
469
- return [];
470
- const evidenceId = `tool:${name}`;
471
- const subtype = tool.subtype ? ` (${tool.subtype})` : '';
472
- evidenceRefs.push({
473
- id: evidenceId,
474
- kind: 'tool_failure',
475
- label: `${name}: ${count} failure(s)${subtype}`,
476
- endpoint: scopedTool
477
- ? `/tool-failures/${encodeURIComponent(name)}?days_back=${daysBack}`
478
- : `/tool-failures?days_back=${daysBack}`,
479
- toolName: name,
480
- value: tool.raw,
481
- });
482
- const location = likelyToolLocation(name, hints);
483
- if (location?.path) {
484
- evidenceRefs.push({
485
- id: `harness:tool:${name}`,
486
- kind: 'harness',
487
- label: `Likely tool definition for ${name}`,
488
- path: location.path,
489
- toolName: name,
490
- });
491
- }
492
- const conversations = tool.conversations > 0 ? tool.conversations : count;
493
- return [{
494
- id: `finding_tool_failure_${slug(name)}`,
495
- kind: 'tool_failure',
496
- title: `${name} is failing in production`,
497
- severity: severityForCount(count, 10, 3),
498
- confidence: 'high',
499
- impactScore: 90 + Math.min(count, 25),
500
- rankReason: `${count} failed call(s) across ${conversations} conversation(s), with direct tool failure evidence.`,
501
- summary: `${name} produced ${count} failed call(s) in the last ${daysBack} day(s).${tool.subtype ? ` The dominant subtype is ${tool.subtype}.` : ''}`,
502
- evidenceRefIds: location?.path ? [evidenceId, `harness:tool:${name}`] : [evidenceId],
503
- likelyLocations: [location ?? unknownLocation('tool', `No local definition matched ${name}.`)],
504
- recommendedActions: [{
505
- id: `action_tool_detail_${slug(name)}`,
506
- title: `Inspect ${name} failure examples.`,
507
- rationale: 'Examples include conversation anchors and error subtypes.',
508
- command: `moda tool-failure-detail ${name} --include-window`,
509
- mutability: 'read',
510
- requiresApproval: false,
511
- }],
512
- }];
513
- });
514
- }
515
- function frustrationFindings(data, daysBack, evidenceRefs) {
516
- const summary = asRecord(valueAt(data, ['summary']));
517
- const frustrations = Array.isArray(valueAt(data, ['frustrations']))
518
- ? valueAt(data, ['frustrations'])
519
- : [];
520
- const count = numberValue(summary?.frustrated_count) || frustrations.length;
521
- const atRisk = numberValue(summary?.at_risk_count);
522
- if (count <= 0 && atRisk <= 0 && frustrations.length === 0)
523
- return [];
524
- const first = asRecord(frustrations[0]);
525
- const conversationId = asString(first?.conversation_id);
526
- const quote = firstQuote(first);
527
- const evidenceId = conversationId ? `frustration:${conversationId}` : 'frustration:summary';
528
- evidenceRefs.push({
529
- id: evidenceId,
530
- kind: 'frustration',
531
- label: quote ? `Frustration anchor: "${quote}"` : `${count} frustrated conversation(s)`,
532
- endpoint: `/frustrations?days_back=${daysBack}&limit=5`,
533
- ...(conversationId ? { conversationId } : {}),
534
- value: first ?? summary ?? data,
535
- });
536
- return [{
537
- id: 'finding_frustration_rate',
538
- kind: 'frustration',
539
- title: count > 0 ? `${count} frustrated conversation(s)` : `${atRisk} at-risk conversation(s)`,
540
- severity: count >= 10 ? 'high' : 'medium',
541
- confidence: first ? 'high' : 'medium',
542
- impactScore: 70 + Math.min(count * 3 + atRisk, 25),
543
- rankReason: 'User frustration is a product-quality signal even when the agent technically completes.',
544
- summary: first?.primary_cause
545
- ? `Primary cause: ${String(first.primary_cause)}.`
546
- : `Moda found frustration or risk in the last ${daysBack} day(s).`,
547
- evidenceRefIds: [evidenceId],
548
- likelyLocations: [unknownLocation('prompt', 'Prompt or policy issue likely; inspect the conversation window.')],
549
- recommendedActions: [{
550
- id: 'action_frustration_window',
551
- title: 'Read the frustration window.',
552
- rationale: 'The user quote and nearby turns reveal what confused or blocked the user.',
553
- command: conversationId
554
- ? `moda context ${conversationId} --window=3`
555
- : `moda frustrations --days-back=${daysBack} --include-window`,
556
- mutability: 'read',
557
- requiresApproval: false,
558
- }],
559
- }];
560
- }
561
- function insufficientDataFinding(overview, evidenceRefs) {
562
- const evidenceId = 'overview:data';
563
- if (!evidenceRefs.some((ref) => ref.id === evidenceId)) {
564
- evidenceRefs.push({
565
- id: evidenceId,
566
- kind: 'data_api',
567
- label: 'Overview did not contain actionable production signals',
568
- endpoint: '/overview',
569
- value: overview,
570
- });
571
- }
572
- return {
573
- id: 'finding_insufficient_data',
574
- kind: 'insufficient_data',
575
- title: 'No strong production issue found yet',
576
- severity: 'info',
577
- confidence: 'medium',
578
- impactScore: 10,
579
- rankReason: 'Moda needs more recent production traces before ranking failures.',
580
- summary: 'Data flow may be quiet or current signals are below the investigation threshold.',
581
- evidenceRefIds: [evidenceId],
582
- likelyLocations: [],
583
- recommendedActions: [{
584
- id: 'action_doctor_online',
585
- title: 'Validate data flow.',
586
- rationale: 'A quiet overview can mean no traffic or broken ingestion.',
587
- command: 'moda doctor --online',
588
- mutability: 'read',
589
- requiresApproval: false,
590
- }],
591
- };
592
- }
593
- function normalizeCloudAsk(response, question) {
594
- const record = asRecord(response);
595
- const answer = asString(record?.answer);
596
- if (!answer)
597
- return null;
598
- const evidenceRefs = Array.isArray(record?.evidence_refs)
599
- ? record.evidence_refs.flatMap((item) => {
600
- const ref = asRecord(item);
601
- if (!ref)
602
- return [];
603
- return [{
604
- id: asString(ref.id) ?? `ask:evidence:${slug(asString(ref.label) ?? 'ref')}`,
605
- kind: evidenceKind(asString(ref.kind)),
606
- label: asString(ref.label) ?? asString(ref.id) ?? 'Cloud evidence',
607
- path: asString(ref.path) ?? undefined,
608
- value: ref.value,
609
- href: asString(ref.href) ?? undefined,
610
- }];
611
- })
612
- : [];
613
- const nextCommands = Array.isArray(record?.next_commands)
614
- ? record.next_commands.filter((item) => typeof item === 'string')
615
- : ['moda investigate'];
616
- const confidence = normalizeConfidence(record?.confidence);
617
- // Prefer the backend's canonical v1 payload when it ships one; otherwise
618
- // synthesize a v1 projection from the legacy snake_case fields so older
619
- // backends still produce a valid moda.intelligence.v1 `data`.
620
- const intelligenceRecord = asRecord(record?.intelligence);
621
- const intelligence = looksLikeIntelligenceV1(intelligenceRecord)
622
- ? coerceIntelligenceV1(intelligenceRecord, { question, answer, confidence })
623
- : synthesizeIntelligenceV1({
624
- question,
625
- answer,
626
- confidence,
627
- evidenceRefs,
628
- nextCommands,
629
- warnings: [],
630
- source: 'cloud',
631
- degraded: false,
632
- });
633
- return {
634
- schemaVersion: ASK_SCHEMA_VERSION,
635
- generatedAt: new Date().toISOString(),
636
- source: 'cloud',
637
- degraded: false,
638
- question,
639
- answer,
640
- confidence,
641
- evidenceRefs,
642
- findings: [],
643
- nextCommands,
644
- warnings: [],
645
- intelligence,
646
- };
647
- }
648
- /**
649
- * Heuristic "is this the canonical v1 payload?" guard. We accept an object that
650
- * either declares the matching `schema_version` or carries the core v1 fields
651
- * (`answer_markdown` / `summary`). A present-but-wrong `schema_version` is
652
- * rejected so a future breaking version falls back to synthesis rather than
653
- * being mis-projected.
654
- */
655
- function looksLikeIntelligenceV1(record) {
656
- if (!record)
657
- return false;
658
- const version = asString(record.schema_version);
659
- if (version && version !== INTELLIGENCE_SCHEMA_VERSION)
660
- return false;
661
- return (version === INTELLIGENCE_SCHEMA_VERSION ||
662
- typeof record.answer_markdown === 'string' ||
663
- asRecord(record.summary) !== null);
664
- }
665
- /**
666
- * Defensively project a backend `intelligence` object into a schema-valid
667
- * {@link ModaIntelligenceV1}. Every field is coerced and defaulted so the CLI
668
- * never emits a malformed payload even if the backend is partially shaped.
669
- */
670
- function coerceIntelligenceV1(record, context) {
671
- const summaryRecord = asRecord(record.summary);
672
- const summary = {
673
- text: asString(summaryRecord?.text) ?? context.answer,
674
- confidence: confidenceValue(summaryRecord?.confidence) ?? context.confidence,
675
- };
676
- const evidenceRefs = Array.isArray(record.evidence_refs)
677
- ? record.evidence_refs.flatMap(coerceV1EvidenceRef)
678
- : [];
679
- const answerClaims = Array.isArray(record.answer_claims)
680
- ? record.answer_claims.flatMap(coerceV1Claim)
681
- : [];
682
- const findings = Array.isArray(record.findings)
683
- ? record.findings.flatMap(coerceV1Finding)
684
- : [];
685
- const nextCommands = Array.isArray(record.next_commands)
686
- ? record.next_commands.flatMap(coerceV1NextCommand)
687
- : [];
688
- const warnings = stringArray(record.warnings);
689
- const meta = asRecord(record.meta);
690
- return {
691
- schema_version: INTELLIGENCE_SCHEMA_VERSION,
692
- generated_at: asString(record.generated_at) ?? new Date().toISOString(),
693
- command: asString(record.command) ?? 'ask',
694
- source: record.source === 'local_fallback' ? 'local_fallback' : 'cloud',
695
- degraded: record.degraded === true,
696
- question: asString(record.question) ?? context.question,
697
- summary,
698
- answer_markdown: asString(record.answer_markdown) ?? context.answer,
699
- answer_claims: answerClaims,
700
- findings,
701
- evidence_refs: evidenceRefs,
702
- next_commands: nextCommands,
703
- warnings,
704
- ...(meta ? { meta } : {}),
705
- };
706
- }
707
- /**
708
- * Build a v1 payload from legacy snake_case fields (or local-fallback evidence).
709
- * `evidence_refs` default to `grounding: 'measured'` and `answer_claims` is
710
- * empty because the legacy contract has no per-claim grounding verdicts.
711
- */
712
- function synthesizeIntelligenceV1(options) {
713
- return {
714
- schema_version: INTELLIGENCE_SCHEMA_VERSION,
715
- generated_at: new Date().toISOString(),
716
- command: 'ask',
717
- source: options.source,
718
- degraded: options.degraded,
719
- question: options.question,
720
- summary: { text: options.answer, confidence: options.confidence },
721
- answer_markdown: options.answer,
722
- answer_claims: [],
723
- findings: (options.findings ?? []).map(productionFindingToV1),
724
- evidence_refs: options.evidenceRefs.map(productionEvidenceToV1),
725
- next_commands: options.nextCommands.map(commandStringToV1),
726
- warnings: options.warnings,
727
- };
728
- }
729
- function coerceV1EvidenceRef(item) {
730
- const ref = asRecord(item);
731
- if (!ref)
732
- return [];
733
- const label = asString(ref.label);
734
- const anchor = coerceV1Anchor(asRecord(ref.anchor));
735
- const snippet = asString(ref.snippet);
736
- return [{
737
- id: asString(ref.id) ?? `ask:evidence:${slug(label ?? 'ref')}`,
738
- kind: asString(ref.kind) ?? 'ask',
739
- label: label ?? asString(ref.id) ?? 'Cloud evidence',
740
- ...(anchor ? { anchor } : {}),
741
- ...(snippet ? { snippet } : {}),
742
- grounding: provenanceValue(ref.grounding) ?? 'measured',
743
- }];
744
- }
745
- function coerceV1Anchor(record) {
746
- if (!record)
747
- return undefined;
748
- const anchor = {
749
- ...(asString(record.conversation_id) ? { conversation_id: asString(record.conversation_id) } : {}),
750
- ...(asNumber(record.message_index) !== null ? { message_index: asNumber(record.message_index) } : {}),
751
- ...(asString(record.run_id) ? { run_id: asString(record.run_id) } : {}),
752
- ...(asString(record.tool_name) ? { tool_name: asString(record.tool_name) } : {}),
753
- };
754
- return Object.keys(anchor).length > 0 ? anchor : undefined;
755
- }
756
- function coerceV1Claim(item) {
757
- const claim = asRecord(item);
758
- if (!claim)
759
- return [];
760
- const text = asString(claim.text);
761
- if (!text)
762
- return [];
763
- const value = asNumber(claim.value);
764
- const matchedPath = asString(claim.matched_path);
765
- return [{
766
- text,
767
- ...(value !== null ? { value } : {}),
768
- kind: claim.kind === 'percentage' || claim.kind === 'count' ? claim.kind : 'qualitative',
769
- status: claim.status === 'grounded' || claim.status === 'ungrounded'
770
- ? claim.status
771
- : 'unverifiable',
772
- evidence_ref_ids: stringArray(claim.evidence_ref_ids),
773
- ...(matchedPath ? { matched_path: matchedPath } : {}),
774
- }];
775
- }
776
- function coerceV1Finding(item) {
777
- const finding = asRecord(item);
778
- if (!finding)
779
- return [];
780
- const id = asString(finding.id);
781
- const title = asString(finding.title);
782
- if (!id || !title)
783
- return [];
784
- const severity = finding.severity;
785
- const description = asString(finding.description);
786
- return [{
787
- id,
788
- title,
789
- ...(severity === 'critical' || severity === 'high' || severity === 'medium' ||
790
- severity === 'low' || severity === 'info'
791
- ? { severity }
792
- : {}),
793
- ...(description ? { description } : {}),
794
- evidence_ref_ids: stringArray(finding.evidence_ref_ids),
795
- recommended_actions: Array.isArray(finding.recommended_actions)
796
- ? finding.recommended_actions.flatMap(coerceV1RecommendedAction)
797
- : [],
798
- }];
799
- }
800
- function coerceV1RecommendedAction(item) {
801
- const action = asRecord(item);
802
- if (!action)
803
- return [];
804
- const id = asString(action.id);
805
- const title = asString(action.title);
806
- if (!id || !title)
807
- return [];
808
- const rationale = asString(action.rationale);
809
- const command = asString(action.command);
810
- return [{
811
- id,
812
- title,
813
- ...(rationale ? { rationale } : {}),
814
- ...(command ? { command } : {}),
815
- mutability: mutabilityValue(action.mutability) ?? 'unknown',
816
- requires_approval: action.requires_approval === true,
817
- }];
818
- }
819
- function coerceV1NextCommand(item) {
820
- if (typeof item === 'string')
821
- return [commandStringToV1(item)];
822
- const record = asRecord(item);
823
- if (!record)
824
- return [];
825
- const command = asString(record.command);
826
- if (!command)
827
- return [];
828
- const purpose = asString(record.purpose);
829
- return [{
830
- command,
831
- ...(purpose ? { purpose } : {}),
832
- mutability: mutabilityValue(record.mutability) ?? mutabilityForCommand(command),
833
- requires_approval: typeof record.requires_approval === 'boolean'
834
- ? record.requires_approval
835
- : requiresApprovalForCommand(command),
836
- }];
837
- }
838
- function productionEvidenceToV1(ref) {
839
- const anchor = {
840
- ...(ref.conversationId ? { conversation_id: ref.conversationId } : {}),
841
- ...(ref.runId ? { run_id: ref.runId } : {}),
842
- ...(ref.toolName ? { tool_name: ref.toolName } : {}),
843
- };
844
- return {
845
- id: ref.id,
846
- kind: ref.kind,
847
- label: ref.label,
848
- ...(Object.keys(anchor).length > 0 ? { anchor } : {}),
849
- grounding: 'measured',
850
- };
851
- }
852
- function productionFindingToV1(finding) {
853
- return {
854
- id: finding.id,
855
- title: finding.title,
856
- severity: finding.severity,
857
- description: `${finding.summary} ${finding.rankReason}`.trim(),
858
- evidence_ref_ids: finding.evidenceRefIds,
859
- recommended_actions: finding.recommendedActions.map((action) => ({
860
- id: action.id,
861
- title: action.title,
862
- rationale: action.rationale,
863
- command: action.command,
864
- mutability: action.mutability,
865
- requires_approval: action.requiresApproval,
866
- })),
867
- };
868
- }
869
- function commandStringToV1(command) {
870
- return {
871
- command,
872
- purpose: purposeForCommand(command),
873
- mutability: mutabilityForCommand(command),
874
- requires_approval: requiresApprovalForCommand(command),
875
- };
876
- }
877
- function mutabilityForCommand(command) {
878
- return command.includes(' sync') ? 'sync' : 'read';
879
- }
880
- function requiresApprovalForCommand(command) {
881
- return command.includes(' sync') || command.includes(' --yes');
882
- }
883
- function stringArray(value) {
884
- return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
885
- }
886
- function confidenceValue(value) {
887
- return value === 'high' || value === 'medium' || value === 'low' || value === 'unknown'
888
- ? value
889
- : null;
890
- }
891
- function provenanceValue(value) {
892
- return value === 'measured' || value === 'inferred' ? value : null;
893
- }
894
- function mutabilityValue(value) {
895
- return value === 'read' || value === 'write' || value === 'sync' || value === 'unknown'
896
- ? value
897
- : null;
898
- }
899
- function investigationAgentOptions(investigation) {
900
- return {
901
- summary: investigation.summary,
902
- findings: investigation.findings.map(agentFinding),
903
- evidenceRefs: investigation.evidenceRefs.map(agentEvidence),
904
- recommendedActions: investigation.findings.flatMap((finding) => finding.recommendedActions.map(agentAction)),
905
- nextCommands: investigation.nextCommands.map(nextCommand),
906
- warnings: investigation.warnings,
907
- };
908
- }
909
- function overviewAgentOptions(briefing) {
910
- return {
911
- summary: {
912
- text: `Moda health briefing: ${briefing.status}. Next: ${briefing.nextCommand}`,
913
- confidence: briefing.topFinding ? briefing.topFinding.confidence : 'medium',
914
- },
915
- findings: briefing.topFinding ? [agentFinding(briefing.topFinding)] : [],
916
- evidenceRefs: briefing.evidenceRefs.map(agentEvidence),
917
- recommendedActions: briefing.topFinding
918
- ? briefing.topFinding.recommendedActions.map(agentAction)
919
- : [],
920
- nextCommands: [nextCommand(briefing.nextCommand)],
921
- };
922
- }
923
- function askAgentOptions(result) {
924
- const errors = result.degraded
925
- ? [{
926
- code: 'cloud_unavailable',
927
- message: result.fallbackReason ??
928
- 'Moda Cloud intelligence was unavailable; answer synthesized from local Data API evidence.',
929
- // Non-blocking: an answer WAS produced, just from a degraded source. The
930
- // agent should still treat it as lower-trust (see exit code 3 / status).
931
- blocking: false,
932
- suggested_commands: ['moda doctor --online'],
933
- }]
934
- : [];
935
- return {
936
- summary: { text: result.answer, confidence: result.confidence },
937
- findings: result.findings.map(agentFinding),
938
- evidenceRefs: result.evidenceRefs.map(agentEvidence),
939
- recommendedActions: result.findings.flatMap((finding) => finding.recommendedActions.map(agentAction)),
940
- nextCommands: result.nextCommands.map(nextCommand),
941
- warnings: result.warnings,
942
- errors,
943
- };
944
- }
945
- function agentFinding(finding) {
946
- return {
947
- id: finding.id,
948
- title: finding.title,
949
- severity: finding.severity,
950
- description: `${finding.summary} ${finding.rankReason}`,
951
- evidence_ref_ids: finding.evidenceRefIds,
952
- };
953
- }
954
- function agentEvidence(ref) {
955
- return {
956
- id: ref.id,
957
- kind: ref.kind,
958
- label: ref.label,
959
- path: ref.path,
960
- line: ref.line,
961
- href: ref.href,
962
- };
963
- }
964
- function agentAction(action) {
965
- return {
966
- id: action.id,
967
- title: action.title,
968
- rationale: action.rationale,
969
- command: action.command,
970
- mutability: action.mutability,
971
- requires_approval: action.requiresApproval,
972
- };
973
- }
974
- function nextCommand(command) {
975
- return {
976
- command,
977
- purpose: purposeForCommand(command),
978
- mutability: command.includes(' sync') ? 'sync' : 'read',
979
- requires_approval: command.includes(' sync') || command.includes(' --yes'),
980
- };
981
- }
982
- function renderOverviewBriefingHuman(briefing) {
983
- const lines = [];
984
- lines.push('Moda overview');
985
- lines.push('Production health briefing');
986
- lines.push('');
987
- lines.push(`Status ${briefing.status}`);
988
- lines.push(`Data flow ${briefing.dataFlow.status} (${briefing.dataFlow.conversations} conversation(s))`);
989
- if (briefing.dataFlow.lastEvent) {
990
- lines.push(`Last event ${briefing.dataFlow.lastEvent.summary}${briefing.dataFlow.lastEvent.timestamp ? ` - ${briefing.dataFlow.lastEvent.timestamp}` : ''}`);
991
- }
992
- lines.push('');
993
- lines.push('Sync');
994
- lines.push(` Harness ${briefing.harnessSync.status} - ${briefing.harnessSync.summary}`);
995
- lines.push(` Prompts ${briefing.promptSync.status} - ${briefing.promptSync.summary}`);
996
- lines.push(` Skills ${briefing.skillSync.status} - ${briefing.skillSync.summary}`);
997
- lines.push('');
998
- lines.push('Signals');
999
- for (const [label, value] of overviewSignalPairs(briefing.metrics)) {
1000
- lines.push(` ${label.padEnd(18)} ${String(value)}`);
1001
- }
1002
- lines.push('');
1003
- lines.push('Top signal');
1004
- if (briefing.topFinding) {
1005
- lines.push(` ${briefing.topFinding.severity.toUpperCase()} ${briefing.topFinding.title}`);
1006
- lines.push(` ${briefing.topFinding.summary}`);
1007
- lines.push(` Why ranked: ${briefing.topFinding.rankReason}`);
1008
- }
1009
- else {
1010
- lines.push(' No urgent production issue found in the selected window.');
1011
- }
1012
- lines.push('');
1013
- lines.push('Next best action');
1014
- lines.push(` ${briefing.nextCommand}`);
1015
- return lines.join('\n');
1016
- }
1017
- function renderInvestigationHuman(investigation) {
1018
- const lines = [];
1019
- lines.push('Moda investigation');
1020
- lines.push(`Window: last ${investigation.daysBack} day(s)`);
1021
- lines.push('');
1022
- lines.push(investigation.summary.text);
1023
- for (const finding of investigation.findings) {
1024
- lines.push('');
1025
- lines.push(`${finding.severity.toUpperCase()} ${finding.title}`);
1026
- lines.push(` Confidence: ${finding.confidence}`);
1027
- lines.push(` Impact: ${finding.impactScore}`);
1028
- lines.push(` ${finding.summary}`);
1029
- lines.push(` Why ranked: ${finding.rankReason}`);
1030
- if (finding.likelyLocations.length > 0) {
1031
- lines.push(' Likely location:');
1032
- for (const location of finding.likelyLocations.slice(0, 3)) {
1033
- lines.push(` - ${location.path ?? location.label} (${location.confidence})`);
1034
- }
1035
- }
1036
- if (finding.recommendedActions.length > 0) {
1037
- lines.push(' Next:');
1038
- for (const action of finding.recommendedActions.slice(0, 2)) {
1039
- lines.push(` ${action.command}`);
1040
- }
1041
- }
1042
- }
1043
- lines.push('');
1044
- lines.push('Next commands');
1045
- for (const command of investigation.nextCommands.slice(0, 6))
1046
- lines.push(` ${command}`);
1047
- if (investigation.warnings.length > 0) {
1048
- lines.push('');
1049
- lines.push('Warnings');
1050
- for (const warning of investigation.warnings.slice(0, 5))
1051
- lines.push(` - ${warning}`);
1052
- }
1053
- return lines.join('\n');
1054
- }
1055
- export function renderAskHuman(result, options = {}) {
1056
- // Same convention as the rest of the CLI: color only for a human at a TTY,
1057
- // honoring NO_COLOR and TERM=dumb. All information is identical either way.
1058
- const color = options.color ?? shouldColorTerminalOutput();
1059
- const style = terminalStyles(color);
1060
- const lines = [];
1061
- lines.push(style.heading('Moda ask'));
1062
- lines.push(`${style.dim('Question:')} ${result.question}`);
1063
- if (result.degraded) {
1064
- lines.push('');
1065
- lines.push('[degraded] Moda Cloud was unavailable — this answer is synthesized from local Data API evidence (lower trust).');
1066
- }
1067
- lines.push('');
1068
- lines.push(renderMarkdownForTerminal(result.answer, { color }));
1069
- lines.push('');
1070
- lines.push(`Confidence: ${style.bold(result.confidence)}`);
1071
- lines.push(`Source: ${result.source}${result.degraded ? ' (degraded)' : ''}`);
1072
- if (result.evidenceRefs.length > 0) {
1073
- lines.push('');
1074
- lines.push(style.bold('Evidence'));
1075
- // Subtle grounding marker: flag model-inferred evidence so a human reader
1076
- // can weight it. Ids/anchors stay hidden in the human projection.
1077
- const grounding = new Map(result.intelligence.evidence_refs.map((ref) => [ref.id, ref.grounding]));
1078
- const dashboardBase = resolveModaBaseUrl();
1079
- for (const ref of result.evidenceRefs.slice(0, 8)) {
1080
- const marker = grounding.get(ref.id) === 'inferred' ? ' (inferred)' : '';
1081
- // Backend hrefs are root-relative dashboard paths; print the absolute
1082
- // URL so terminals auto-linkify it and the reader can click through.
1083
- // Strip ANSI from all backend-controlled fields before printing.
1084
- const label = stripAnsi(ref.label);
1085
- const path = ref.path ? ` (${stripAnsi(ref.path)})` : '';
1086
- const link = ref.href ? `\n ${style.dim(`${dashboardBase}${stripAnsi(ref.href)}`)}` : '';
1087
- lines.push(` - ${label}${path}${marker}${link}`);
1088
- }
1089
- }
1090
- const unverifiedClaims = result.intelligence.answer_claims.filter((claim) => claim.status !== 'grounded').length;
1091
- if (unverifiedClaims > 0) {
1092
- lines.push('');
1093
- lines.push(`Note: ${unverifiedClaims} claim(s) could not be grounded against tool data.`);
1094
- }
1095
- if (result.nextCommands.length > 0) {
1096
- lines.push('');
1097
- lines.push(style.bold('Next commands'));
1098
- for (const command of result.nextCommands.slice(0, 5))
1099
- lines.push(` ${stripAnsi(command)}`);
1100
- }
1101
- if (result.warnings.length > 0) {
1102
- lines.push('');
1103
- lines.push(style.bold('Warnings'));
1104
- for (const warning of result.warnings.slice(0, 3))
1105
- lines.push(` - ${stripAnsi(warning)}`);
1106
- }
1107
- return lines.join('\n');
1108
- }
1109
- function addOverviewEvidence(data, endpoint, refs) {
1110
- refs.push({
1111
- id: 'overview:data',
1112
- kind: 'data_api',
1113
- label: 'Production overview aggregate',
1114
- endpoint,
1115
- value: overviewMetrics(data),
1116
- });
1117
- }
1118
- function toolRows(data, scopedTool) {
1119
- const record = asRecord(data);
1120
- const directToolName = asString(record?.tool_name) ?? scopedTool;
1121
- const examples = Array.isArray(record?.examples) ? record.examples : [];
1122
- if (directToolName) {
1123
- return [{
1124
- name: directToolName,
1125
- count: numberValue(record?.failure_count) || numberValue(record?.total_failures) || examples.length,
1126
- conversations: numberValue(record?.failed_conversations) || numberValue(record?.conversation_count) || examples.length,
1127
- subtype: asString(record?.top_error_subtype) ?? asString(record?.error_subtype) ?? undefined,
1128
- raw: data,
1129
- }];
1130
- }
1131
- const tools = Array.isArray(record?.tools)
1132
- ? record.tools
1133
- : Array.isArray(record?.tool_failures)
1134
- ? record.tool_failures
1135
- : Array.isArray(record?.failures)
1136
- ? record.failures
1137
- : [];
1138
- return tools.flatMap((item) => {
1139
- const row = asRecord(item);
1140
- const name = asString(row?.tool_name) ?? asString(row?.name);
1141
- if (!name)
1142
- return [];
1143
- return [{
1144
- name,
1145
- count: numberValue(row?.failure_count) || numberValue(row?.total_failures) || numberValue(row?.count),
1146
- conversations: numberValue(row?.failed_conversations) || numberValue(row?.conversation_count),
1147
- subtype: asString(row?.top_error_subtype) ?? asString(row?.error_subtype) ?? undefined,
1148
- raw: row,
1149
- }];
1150
- });
1151
- }
1152
- function firstQuote(row) {
1153
- const quotes = Array.isArray(row?.user_quotes) ? row.user_quotes : [];
1154
- const first = asRecord(quotes[0]);
1155
- return asString(first?.quote)?.slice(0, 160) ?? undefined;
1156
- }
1157
- function likelyToolLocation(toolName, hints) {
1158
- const normalized = normalizeKey(toolName);
1159
- for (const [key, path] of hints.toolPaths.entries()) {
1160
- if (key.includes(normalized) || normalized.includes(key)) {
1161
- return {
1162
- kind: 'tool',
1163
- path,
1164
- label: `Tool definition for ${toolName}`,
1165
- confidence: 'medium',
1166
- reason: 'Matched tool name against the local harness graph.',
1167
- };
1168
- }
1169
- }
1170
- return null;
1171
- }
1172
- function defaultLikelyLocations(hints, kind) {
1173
- const source = kind === 'tool' ? hints.toolPaths : kind === 'prompt' ? hints.promptPaths : hints.skillPaths;
1174
- const first = [...source.values()][0];
1175
- if (first) {
1176
- return [{
1177
- kind,
1178
- path: first,
1179
- label: `${kind} artifact`,
1180
- confidence: 'low',
1181
- reason: 'The overview is aggregate-only; investigation will map specific production evidence.',
1182
- }];
1183
- }
1184
- if (hints.reportPath) {
1185
- return [{
1186
- kind: 'harness',
1187
- path: hints.reportPath,
1188
- label: 'Local harness manifest',
1189
- confidence: 'low',
1190
- reason: 'Use the local harness graph to map production signals to code.',
1191
- }];
1192
- }
1193
- return [];
1194
- }
1195
- function unknownLocation(kind, reason) {
1196
- return {
1197
- kind,
1198
- label: 'Unknown local path',
1199
- confidence: 'unknown',
1200
- reason,
1201
- };
1202
- }
1203
- function loadHarnessHints(cwd) {
1204
- const hints = {
1205
- toolPaths: new Map(),
1206
- promptPaths: new Map(),
1207
- skillPaths: new Map(),
1208
- entrypointPaths: [],
1209
- };
1210
- const harnessPath = join(cwd, '.moda', 'harness.json');
1211
- if (!existsSync(harnessPath))
1212
- return hints;
1213
- hints.reportPath = '.moda/harness.json';
1214
- try {
1215
- const parsed = JSON.parse(readFileSync(harnessPath, 'utf8'));
1216
- const graph = asRecord(parsed);
1217
- const artifacts = Array.isArray(graph?.artifacts) ? graph.artifacts : [];
1218
- for (const item of artifacts) {
1219
- const artifact = asRecord(item);
1220
- if (!artifact)
1221
- continue;
1222
- const name = asString(artifact.name) ?? asString(artifact.id) ?? '';
1223
- const path = asString(artifact.sourcePath) ?? asString(artifact.path);
1224
- const type = asString(artifact.type);
1225
- if (!path || !name)
1226
- continue;
1227
- const key = normalizeKey(name);
1228
- if (type === 'tool')
1229
- hints.toolPaths.set(key, path);
1230
- if (type === 'prompt')
1231
- hints.promptPaths.set(key, path);
1232
- if (type === 'skill')
1233
- hints.skillPaths.set(key, path);
1234
- if (type === 'entrypoint')
1235
- hints.entrypointPaths.push(path);
1236
- }
1237
- }
1238
- catch {
1239
- return hints;
1240
- }
1241
- return hints;
1242
- }
1243
- function localHarnessSummary(cwd) {
1244
- const path = join(cwd, '.moda', 'harness.json');
1245
- if (!existsSync(path))
1246
- return null;
1247
- try {
1248
- const parsed = JSON.parse(readFileSync(path, 'utf8'));
1249
- return {
1250
- harnessId: parsed.harnessId,
1251
- graphHash: parsed.graphHash,
1252
- runtimeAgentCount: Array.isArray(parsed.runtimeAgents) ? parsed.runtimeAgents.length : undefined,
1253
- };
1254
- }
1255
- catch {
1256
- return null;
1257
- }
1258
- }
1259
- function localSyncBrief(cwd, relativePath, label) {
1260
- const path = join(cwd, relativePath);
1261
- if (!existsSync(path)) {
1262
- return { status: 'missing', summary: `${label} state not found`, path: relativePath };
1263
- }
1264
- return { status: 'present', summary: `${label} state exists locally`, path: relativePath };
1265
- }
1266
- function localSkillBrief(cwd) {
1267
- const claudeSkill = join(cwd, '.claude', 'skills');
1268
- const cursorRules = join(cwd, '.cursor', 'rules');
1269
- if (existsSync(claudeSkill) || existsSync(cursorRules)) {
1270
- return { status: 'present', summary: 'Local agent skill/rule directory exists' };
1271
- }
1272
- return { status: 'missing', summary: 'No local agent skill/rule directory found' };
1273
- }
1274
- function summarizeInvestigation(findings) {
1275
- const top = findings[0];
1276
- if (!top) {
1277
- return { text: 'No production findings were generated.', confidence: 'unknown' };
1278
- }
1279
- if (top.kind === 'insufficient_data') {
1280
- return { text: top.title, confidence: top.confidence };
1281
- }
1282
- return {
1283
- text: `Top production finding: ${top.title}. ${top.summary}`,
1284
- confidence: top.confidence,
1285
- };
1286
- }
1287
- function nextCommandsForFindings(findings, daysBack) {
1288
- const commands = findings
1289
- .flatMap((finding) => finding.recommendedActions.map((action) => action.command))
1290
- .filter(Boolean);
1291
- commands.push(`moda overview --days-back=${daysBack}`);
1292
- commands.push('moda doctor --online');
1293
- return [...new Set(commands)].slice(0, 8);
1294
- }
1295
- function compareFindings(a, b) {
1296
- const severityDelta = severityWeight(b.severity) - severityWeight(a.severity);
1297
- if (severityDelta !== 0)
1298
- return severityDelta;
1299
- return b.impactScore - a.impactScore;
1300
- }
1301
- function severityWeight(severity) {
1302
- return { critical: 5, high: 4, medium: 3, low: 2, info: 1 }[severity];
1303
- }
1304
- function severityForCount(count, high, medium) {
1305
- if (count >= high)
1306
- return 'critical';
1307
- if (count >= medium)
1308
- return 'high';
1309
- return 'medium';
1310
- }
1311
- function uniqueEvidenceRefs(refs) {
1312
- const seen = new Set();
1313
- const out = [];
1314
- for (const ref of refs) {
1315
- if (seen.has(ref.id))
1316
- continue;
1317
- seen.add(ref.id);
1318
- out.push(ref);
1319
- }
1320
- return out;
1321
- }
1322
- function overviewMetrics(data) {
1323
- return {
1324
- conversations: valueAt(data, ['conversations', 'total']) ?? 0,
1325
- conversationTrendPct: valueAt(data, ['conversations', 'trend_pct']) ?? 0,
1326
- toolFailures: valueAt(data, ['tool_failures', 'total']) ?? 0,
1327
- failedToolConversations: valueAt(data, ['tool_failures', 'conversations']) ?? 0,
1328
- impactedTools: valueAt(data, ['tool_failures', 'tools']) ?? 0,
1329
- frustrated: valueAt(data, ['frustrations', 'frustrated']) ?? 0,
1330
- atRisk: valueAt(data, ['frustrations', 'at_risk']) ?? 0,
1331
- frustrationRatePct: valueAt(data, ['frustrations', 'rate_pct']) ?? 0,
1332
- recentActivity: recentActivity(data).length,
1333
- };
1334
- }
1335
- function overviewSignalPairs(metrics) {
1336
- return [
1337
- ['Conversations', metrics.conversations],
1338
- ['Tool failures', metrics.toolFailures],
1339
- ['Failed convos', metrics.failedToolConversations],
1340
- ['Impacted tools', metrics.impactedTools],
1341
- ['Frustrated', metrics.frustrated],
1342
- ['At risk', metrics.atRisk],
1343
- ['Frustration %', metrics.frustrationRatePct],
1344
- ['Recent events', metrics.recentActivity],
1345
- ];
1346
- }
1347
- function recentActivity(data) {
1348
- const activity = valueAt(data, ['recent_activity']);
1349
- if (!Array.isArray(activity))
1350
- return [];
1351
- return activity.flatMap((item) => {
1352
- const row = asRecord(item);
1353
- const summary = asString(row?.summary) ?? asString(row?.title) ?? asString(row?.event);
1354
- if (!summary)
1355
- return [];
1356
- return [{
1357
- summary,
1358
- timestamp: asString(row?.timestamp) ?? asString(row?.created_at) ?? undefined,
1359
- conversationId: asString(row?.conversation_id) ?? undefined,
1360
- }];
1361
- });
1362
- }
1363
- function valueAt(value, path) {
1364
- let cursor = value;
1365
- for (const key of path) {
1366
- const record = asRecord(cursor);
1367
- if (!record)
1368
- return undefined;
1369
- cursor = record[key];
1370
- }
1371
- return cursor;
1372
- }
1373
- function numberAt(value, path) {
1374
- return numberValue(valueAt(value, path));
1375
- }
1376
- function numberValue(value) {
1377
- const direct = asNumber(value);
1378
- if (direct !== null)
1379
- return direct;
1380
- if (typeof value === 'string') {
1381
- const parsed = Number(value);
1382
- return Number.isFinite(parsed) ? parsed : 0;
1383
- }
1384
- return 0;
1385
- }
1386
- function numberFlag(flags, key, fallback) {
1387
- const raw = flags[key];
1388
- const parsed = Number(raw);
1389
- if (!Number.isFinite(parsed) || parsed < 1)
1390
- return fallback;
1391
- return Math.min(Math.floor(parsed), 90);
1392
- }
1393
- function normalizeConfidence(value) {
1394
- if (value === 'high' || value === 'medium' || value === 'low' || value === 'unknown')
1395
- return value;
1396
- return 'medium';
1397
- }
1398
- function capFallbackConfidence(confidence) {
1399
- return confidence === 'high' ? 'medium' : confidence;
1400
- }
1401
- function evidenceKind(value) {
1402
- if (value === 'data_api' ||
1403
- value === 'tool_failure' ||
1404
- value === 'frustration' ||
1405
- value === 'harness' ||
1406
- value === 'ask')
1407
- return value;
1408
- return 'ask';
1409
- }
1410
- function normalizeKey(value) {
1411
- return value.toLowerCase().replace(/[^a-z0-9]+/g, '');
1412
- }
1413
- function slug(value) {
1414
- const clean = value.replace(/[^a-zA-Z0-9_-]+/g, '_').replace(/^_+|_+$/g, '');
1415
- return clean || 'unknown';
1416
- }
1417
- function purposeForCommand(command) {
1418
- if (command.startsWith('moda investigate'))
1419
- return 'Open the ranked production investigation.';
1420
- if (command.startsWith('moda tool-failure-detail'))
1421
- return 'Inspect tool failure examples and conversation anchors.';
1422
- if (command.startsWith('moda context'))
1423
- return 'Read the relevant conversation window.';
1424
- if (command.startsWith('moda doctor'))
1425
- return 'Validate Moda setup and data flow.';
1426
- if (command.startsWith('moda overview'))
1427
- return 'Return to the harness health briefing.';
1428
- return 'Suggested next Moda command.';
1429
- }
1430
- //# sourceMappingURL=production-intelligence.js.map