@moda-ai/cli 1.22.0 → 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.
- package/dist/cli-g8qs2tj9.js +9777 -0
- package/dist/cli-pwbvtes8.js +2312 -0
- package/dist/cli-qszcpda3.js +284 -0
- package/dist/cli.js +4775 -1159
- package/dist/index-6h0nc1r5.js +2765 -0
- package/dist/provision-j5gq8cvx.js +72 -0
- package/package.json +3 -2
- package/skills/integration/index.json +2 -2
- package/dist/agent-schemas.d.ts +0 -3
- package/dist/agent-schemas.js +0 -358
- package/dist/agent.d.ts +0 -5
- package/dist/agent.js +0 -477
- package/dist/analyst-tui.d.ts +0 -81
- package/dist/analyst-tui.js +0 -314
- package/dist/api-client.d.ts +0 -46
- package/dist/api-client.js +0 -120
- package/dist/ask-stream.d.ts +0 -146
- package/dist/ask-stream.js +0 -473
- package/dist/auth-storage.d.ts +0 -35
- package/dist/auth-storage.js +0 -159
- package/dist/auth.d.ts +0 -50
- package/dist/auth.js +0 -694
- package/dist/cli-session.d.ts +0 -26
- package/dist/cli-session.js +0 -169
- package/dist/cli.d.ts +0 -44
- package/dist/command-context.d.ts +0 -62
- package/dist/command-context.js +0 -57
- package/dist/command-registry.d.ts +0 -22
- package/dist/command-registry.js +0 -15
- package/dist/config.d.ts +0 -60
- package/dist/config.js +0 -110
- package/dist/doctor.d.ts +0 -79
- package/dist/doctor.js +0 -1182
- package/dist/errors.d.ts +0 -50
- package/dist/errors.js +0 -64
- package/dist/exit-funnel.d.ts +0 -14
- package/dist/exit-funnel.js +0 -53
- package/dist/harness-artifact-bodies.d.ts +0 -28
- package/dist/harness-artifact-bodies.js +0 -276
- package/dist/harness-candidates.d.ts +0 -77
- package/dist/harness-candidates.js +0 -482
- package/dist/harness-remote.d.ts +0 -135
- package/dist/harness-remote.js +0 -540
- package/dist/harness-report.d.ts +0 -145
- package/dist/harness-report.js +0 -1579
- package/dist/harness-sync-limits.d.ts +0 -8
- package/dist/harness-sync-limits.js +0 -21
- package/dist/harness.d.ts +0 -463
- package/dist/harness.js +0 -4009
- package/dist/init/index.d.ts +0 -78
- package/dist/init/index.js +0 -1442
- package/dist/init/mcp.d.ts +0 -2
- package/dist/init/mcp.js +0 -27
- package/dist/init/progress-board.d.ts +0 -80
- package/dist/init/progress-board.js +0 -225
- package/dist/init/rules.d.ts +0 -6
- package/dist/init/rules.js +0 -171
- package/dist/init/sdk-catalog.d.ts +0 -10
- package/dist/init/sdk-catalog.js +0 -80
- package/dist/init/sdk-guide.d.ts +0 -19
- package/dist/init/sdk-guide.js +0 -185
- package/dist/init/sdk-integration.d.ts +0 -53
- package/dist/init/sdk-integration.js +0 -835
- package/dist/init/sdk-protocol.d.ts +0 -135
- package/dist/init/sdk-protocol.js +0 -152
- package/dist/init/sdk-verification.d.ts +0 -27
- package/dist/init/sdk-verification.js +0 -123
- package/dist/init/tenant.d.ts +0 -67
- package/dist/init/tenant.js +0 -421
- package/dist/intelligence-schema.d.ts +0 -349
- package/dist/intelligence-schema.js +0 -174
- package/dist/keys.d.ts +0 -47
- package/dist/keys.js +0 -136
- package/dist/manifest.d.ts +0 -5
- package/dist/manifest.js +0 -114
- package/dist/markdown-terminal.d.ts +0 -49
- package/dist/markdown-terminal.js +0 -212
- package/dist/output.d.ts +0 -191
- package/dist/output.js +0 -624
- package/dist/production-intelligence.d.ts +0 -175
- package/dist/production-intelligence.js +0 -1430
- package/dist/profile-commands.d.ts +0 -10
- package/dist/profile-commands.js +0 -342
- package/dist/profiles.d.ts +0 -112
- package/dist/profiles.js +0 -573
- package/dist/prompts-ab.d.ts +0 -71
- package/dist/prompts-ab.js +0 -313
- package/dist/prompts-propose.d.ts +0 -15
- package/dist/prompts-propose.js +0 -212
- package/dist/prompts.d.ts +0 -58
- package/dist/prompts.js +0 -418
- package/dist/provision.d.ts +0 -28
- package/dist/provision.js +0 -104
- package/dist/redact.d.ts +0 -2
- package/dist/redact.js +0 -90
- package/dist/schemas.d.ts +0 -225
- package/dist/schemas.js +0 -126
- package/dist/skills-bundled.d.ts +0 -35
- package/dist/skills-bundled.js +0 -157
- package/dist/skills-manifest.d.ts +0 -33
- package/dist/skills-manifest.js +0 -244
- package/dist/skills.d.ts +0 -57
- package/dist/skills.js +0 -777
- package/dist/sync.d.ts +0 -5
- package/dist/sync.js +0 -36
- package/dist/telemetry.d.ts +0 -56
- package/dist/telemetry.js +0 -167
- package/dist/transforms.d.ts +0 -25
- package/dist/transforms.js +0 -102
- package/dist/tui.d.ts +0 -48
- package/dist/tui.js +0 -393
- package/dist/update-check.d.ts +0 -31
- package/dist/update-check.js +0 -141
- package/dist/workspace.d.ts +0 -12
- 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
|