@moda-ai/cli 1.9.6 → 1.10.1
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/README.md +0 -8
- package/dist/agent-schemas.d.ts +3 -0
- package/dist/agent-schemas.js +298 -0
- package/dist/api-client.d.ts +12 -1
- package/dist/api-client.js +10 -8
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +68 -94
- package/dist/command-context.d.ts +6 -0
- package/dist/command-context.js +1 -1
- package/dist/intelligence-schema.d.ts +349 -0
- package/dist/intelligence-schema.js +174 -0
- package/dist/output.d.ts +25 -2
- package/dist/output.js +71 -2
- package/dist/production-intelligence.d.ts +21 -4
- package/dist/production-intelligence.js +360 -107
- package/dist/schemas.d.ts +20 -43
- package/dist/schemas.js +10 -26
- package/package.json +1 -1
- package/skills/moda-cli/SKILL.md +152 -56
|
@@ -2,7 +2,9 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { callControlAPI, callDataAPI } from './api-client.js';
|
|
4
4
|
import { CliInputError } from './errors.js';
|
|
5
|
+
import { EXIT_DEGRADED, EXIT_OK } from './output.js';
|
|
5
6
|
import { asNumber, asRecord, asString } from './transforms.js';
|
|
7
|
+
import { INTELLIGENCE_SCHEMA_VERSION, } from './intelligence-schema.js';
|
|
6
8
|
const INVESTIGATION_SCHEMA_VERSION = 'production_investigation.v0.1';
|
|
7
9
|
const OVERVIEW_SCHEMA_VERSION = 'production_overview.v0.1';
|
|
8
10
|
const ASK_SCHEMA_VERSION = 'production_ask.v0.1';
|
|
@@ -36,18 +38,17 @@ export async function runProductionCommand(context) {
|
|
|
36
38
|
console.log(renderAskHuman(result));
|
|
37
39
|
}
|
|
38
40
|
else {
|
|
39
|
-
|
|
41
|
+
// The agent/JSON `data` payload is the canonical moda.intelligence.v1
|
|
42
|
+
// value (carrying evidence grounding + per-claim verdicts). The
|
|
43
|
+
// moda.agent.v1 envelope still wraps it; only the inner `data` is v1.
|
|
44
|
+
context.output.writeData(result.intelligence, {
|
|
45
|
+
...askAgentOptions(result),
|
|
46
|
+
status: result.degraded ? 'degraded' : 'success',
|
|
47
|
+
});
|
|
40
48
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const runId = context.positional;
|
|
45
|
-
if (!runId) {
|
|
46
|
-
throw new CliInputError('<run_id> is required', 'Usage: moda why-failed <run_id>');
|
|
47
|
-
}
|
|
48
|
-
const investigation = await investigateProduction({ daysBack, cwd: context.cwd, runId });
|
|
49
|
-
writeInvestigation(context, investigation);
|
|
50
|
-
return { exitCode: 0 };
|
|
49
|
+
// Exit 3 on a degraded/local-fallback answer so a scripted agent can detect
|
|
50
|
+
// it from the exit code alone (in addition to `data.degraded` / status).
|
|
51
|
+
return { exitCode: result.degraded ? EXIT_DEGRADED : EXIT_OK };
|
|
51
52
|
}
|
|
52
53
|
if (context.command === 'failures') {
|
|
53
54
|
const investigation = await investigateProduction({
|
|
@@ -63,7 +64,6 @@ export async function runProductionCommand(context) {
|
|
|
63
64
|
const investigation = await investigateProduction({
|
|
64
65
|
daysBack,
|
|
65
66
|
cwd: context.cwd,
|
|
66
|
-
runId: context.flags.run,
|
|
67
67
|
conversationId: context.flags.conversation,
|
|
68
68
|
toolName: context.flags.tool,
|
|
69
69
|
loopId: context.flags.loop,
|
|
@@ -127,20 +127,16 @@ export async function investigateProduction(options) {
|
|
|
127
127
|
? `/tool-failures/${encodeURIComponent(options.toolName)}?days_back=${options.daysBack}&limit=5`
|
|
128
128
|
: `/tool-failures?days_back=${options.daysBack}`;
|
|
129
129
|
const frustrationsEndpoint = `/frustrations?days_back=${options.daysBack}&limit=5`;
|
|
130
|
-
const runsEndpoint = options.runId
|
|
131
|
-
? `/runs/${encodeURIComponent(options.runId)}`
|
|
132
|
-
: `/runs?time_range=${timeRangeForDays(options.daysBack)}&limit=20`;
|
|
133
130
|
const loopsEndpoint = options.loopId
|
|
134
131
|
? `/loops/${encodeURIComponent(options.loopId)}`
|
|
135
132
|
: '/loops/active?limit=20';
|
|
136
|
-
const [overview, tools, frustrations,
|
|
133
|
+
const [overview, tools, frustrations, loops] = await Promise.all([
|
|
137
134
|
safeData(overviewEndpoint),
|
|
138
135
|
safeData(toolEndpoint),
|
|
139
136
|
safeData(frustrationsEndpoint),
|
|
140
|
-
safeData(runsEndpoint),
|
|
141
137
|
safeData(loopsEndpoint),
|
|
142
138
|
]);
|
|
143
|
-
for (const [key, result] of Object.entries({ overview, tools, frustrations,
|
|
139
|
+
for (const [key, result] of Object.entries({ overview, tools, frustrations, loops })) {
|
|
144
140
|
sourceStatus[key] = result.status;
|
|
145
141
|
if (result.warning)
|
|
146
142
|
warnings.push(result.warning);
|
|
@@ -151,11 +147,9 @@ export async function investigateProduction(options) {
|
|
|
151
147
|
findings.push(...toolFailureFindings(tools.data, options.daysBack, hints, evidenceRefs, options.toolName));
|
|
152
148
|
if (!options.failureOnly) {
|
|
153
149
|
findings.push(...frustrationFindings(frustrations.data, options.daysBack, evidenceRefs));
|
|
154
|
-
findings.push(...runFindings(runs.data, evidenceRefs, options.runId));
|
|
155
150
|
findings.push(...loopFindings(loops.data, evidenceRefs, options.loopId));
|
|
156
151
|
}
|
|
157
152
|
else {
|
|
158
|
-
findings.push(...runFindings(runs.data, evidenceRefs, options.runId).filter((finding) => finding.kind === 'partial_run'));
|
|
159
153
|
findings.push(...loopFindings(loops.data, evidenceRefs, options.loopId));
|
|
160
154
|
}
|
|
161
155
|
if (findings.length === 0) {
|
|
@@ -169,7 +163,6 @@ export async function investigateProduction(options) {
|
|
|
169
163
|
generatedAt: new Date().toISOString(),
|
|
170
164
|
daysBack: options.daysBack,
|
|
171
165
|
scope: {
|
|
172
|
-
...(options.runId ? { runId: options.runId } : {}),
|
|
173
166
|
...(options.conversationId ? { conversationId: options.conversationId } : {}),
|
|
174
167
|
...(options.toolName ? { toolName: options.toolName } : {}),
|
|
175
168
|
...(options.loopId ? { loopId: options.loopId } : {}),
|
|
@@ -184,6 +177,10 @@ export async function investigateProduction(options) {
|
|
|
184
177
|
};
|
|
185
178
|
}
|
|
186
179
|
export async function askModa(options) {
|
|
180
|
+
// Capture *why* the cloud path was not used so the caller (human or agent)
|
|
181
|
+
// can see it, instead of silently swallowing the error and masquerading the
|
|
182
|
+
// local fallback as a normal answer.
|
|
183
|
+
let fallbackReason = 'Moda Cloud ask endpoint returned no answer.';
|
|
187
184
|
try {
|
|
188
185
|
const response = await callControlAPI('/cli/ask', {
|
|
189
186
|
method: 'POST',
|
|
@@ -193,14 +190,14 @@ export async function askModa(options) {
|
|
|
193
190
|
profile: options.profile,
|
|
194
191
|
harness: localHarnessSummary(options.cwd),
|
|
195
192
|
}),
|
|
196
|
-
});
|
|
193
|
+
}, options.profile ? { profile: options.profile } : {}, { timeoutMs: askTimeoutMs(), retries: 0 });
|
|
197
194
|
const cloud = normalizeCloudAsk(response, options.question);
|
|
198
195
|
if (cloud)
|
|
199
196
|
return cloud;
|
|
197
|
+
fallbackReason = 'Moda Cloud ask endpoint returned an empty answer.';
|
|
200
198
|
}
|
|
201
|
-
catch {
|
|
202
|
-
|
|
203
|
-
// deployed everywhere, the CLI keeps answering from bounded Data API evidence.
|
|
199
|
+
catch (error) {
|
|
200
|
+
fallbackReason = `Moda Cloud ask endpoint unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
204
201
|
}
|
|
205
202
|
const investigation = await investigateProduction({
|
|
206
203
|
daysBack: options.daysBack,
|
|
@@ -209,24 +206,56 @@ export async function askModa(options) {
|
|
|
209
206
|
});
|
|
210
207
|
const top = investigation.findings[0];
|
|
211
208
|
const answer = top
|
|
212
|
-
? `Moda Cloud intelligence is not available
|
|
213
|
-
: 'Moda Cloud intelligence is not available
|
|
209
|
+
? `Moda Cloud intelligence is not available, so I synthesized this from production evidence. The strongest signal is: ${top.title}. ${top.summary}`
|
|
210
|
+
: 'Moda Cloud intelligence is not available, and the local Data API evidence did not contain a strong production signal.';
|
|
211
|
+
const confidence = capFallbackConfidence(investigation.summary.confidence);
|
|
212
|
+
const evidenceRefs = investigation.evidenceRefs.slice(0, 12);
|
|
213
|
+
const findings = investigation.findings.slice(0, 5);
|
|
214
|
+
const nextCommands = investigation.nextCommands;
|
|
215
|
+
const warnings = [
|
|
216
|
+
'Cloud ask endpoint unavailable; synthesized answer from local Data API evidence.',
|
|
217
|
+
fallbackReason,
|
|
218
|
+
...investigation.warnings,
|
|
219
|
+
];
|
|
214
220
|
return {
|
|
215
221
|
schemaVersion: ASK_SCHEMA_VERSION,
|
|
216
222
|
generatedAt: new Date().toISOString(),
|
|
217
223
|
source: 'local_fallback',
|
|
224
|
+
degraded: true,
|
|
225
|
+
fallbackReason,
|
|
218
226
|
question: options.question,
|
|
219
227
|
answer,
|
|
220
|
-
confidence
|
|
221
|
-
evidenceRefs
|
|
222
|
-
findings
|
|
223
|
-
nextCommands
|
|
224
|
-
warnings
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
+
confidence,
|
|
229
|
+
evidenceRefs,
|
|
230
|
+
findings,
|
|
231
|
+
nextCommands,
|
|
232
|
+
warnings,
|
|
233
|
+
// Degraded answers still carry a v1-shaped payload so every surface reads
|
|
234
|
+
// one contract; `source`/`degraded` flag the lower trust (preserving exit
|
|
235
|
+
// code 3 + status 'degraded').
|
|
236
|
+
intelligence: synthesizeIntelligenceV1({
|
|
237
|
+
question: options.question,
|
|
238
|
+
answer,
|
|
239
|
+
confidence,
|
|
240
|
+
evidenceRefs,
|
|
241
|
+
nextCommands,
|
|
242
|
+
warnings,
|
|
243
|
+
source: 'local_fallback',
|
|
244
|
+
degraded: true,
|
|
245
|
+
findings,
|
|
246
|
+
}),
|
|
228
247
|
};
|
|
229
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Timeout (ms) for the cloud `ask` POST. The backend enforces its own ~240s run
|
|
251
|
+
* deadline under a 300s function limit, so the client waits long enough to
|
|
252
|
+
* receive a real answer rather than aborting a healthy run at 30s and silently
|
|
253
|
+
* falling back. Override with `MODA_ASK_TIMEOUT_MS`.
|
|
254
|
+
*/
|
|
255
|
+
function askTimeoutMs() {
|
|
256
|
+
const raw = Number(process.env.MODA_ASK_TIMEOUT_MS);
|
|
257
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 250_000;
|
|
258
|
+
}
|
|
230
259
|
function writeInvestigation(context, investigation) {
|
|
231
260
|
if (context.outputMode === 'human') {
|
|
232
261
|
if (!context.quiet)
|
|
@@ -415,45 +444,6 @@ function frustrationFindings(data, daysBack, evidenceRefs) {
|
|
|
415
444
|
}],
|
|
416
445
|
}];
|
|
417
446
|
}
|
|
418
|
-
function runFindings(data, evidenceRefs, scopedRunId) {
|
|
419
|
-
const rows = runRows(data);
|
|
420
|
-
return rows.flatMap((run) => {
|
|
421
|
-
const runId = run.id || scopedRunId;
|
|
422
|
-
const outcome = run.outcome;
|
|
423
|
-
if (!runId || !['failed', 'partial', 'abandoned', 'loop'].includes(outcome))
|
|
424
|
-
return [];
|
|
425
|
-
const evidenceId = `run:${runId}`;
|
|
426
|
-
evidenceRefs.push({
|
|
427
|
-
id: evidenceId,
|
|
428
|
-
kind: 'run',
|
|
429
|
-
label: `${run.agentName || 'Agent run'} ${outcome}`,
|
|
430
|
-
endpoint: `/runs/${runId}`,
|
|
431
|
-
runId,
|
|
432
|
-
conversationId: run.conversationId,
|
|
433
|
-
value: run.raw,
|
|
434
|
-
});
|
|
435
|
-
return [{
|
|
436
|
-
id: `finding_run_${slug(runId)}`,
|
|
437
|
-
kind: outcome === 'partial' ? 'partial_run' : 'tool_failure',
|
|
438
|
-
title: `${run.agentName || 'Agent run'} ended as ${outcome}`,
|
|
439
|
-
severity: outcome === 'failed' ? 'high' : 'medium',
|
|
440
|
-
confidence: 'high',
|
|
441
|
-
impactScore: outcome === 'failed' ? 88 : 76,
|
|
442
|
-
rankReason: `Run outcome is ${outcome}${run.reason ? `: ${run.reason}` : ''}.`,
|
|
443
|
-
summary: run.reason || 'Moda classified this run as incomplete or unhealthy.',
|
|
444
|
-
evidenceRefIds: [evidenceId],
|
|
445
|
-
likelyLocations: [unknownLocation('entrypoint', 'Map this run to a harness entrypoint after harness sync.')],
|
|
446
|
-
recommendedActions: [{
|
|
447
|
-
id: `action_run_${slug(runId)}`,
|
|
448
|
-
title: 'Open the run detail.',
|
|
449
|
-
rationale: 'Run detail includes receipts and missing effects.',
|
|
450
|
-
command: `moda run ${runId}`,
|
|
451
|
-
mutability: 'read',
|
|
452
|
-
requiresApproval: false,
|
|
453
|
-
}],
|
|
454
|
-
}];
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
447
|
function loopFindings(data, evidenceRefs, scopedLoopId) {
|
|
458
448
|
const rows = loopRows(data);
|
|
459
449
|
return rows.flatMap((loop) => {
|
|
@@ -547,10 +537,27 @@ function normalizeCloudAsk(response, question) {
|
|
|
547
537
|
? record.next_commands.filter((item) => typeof item === 'string')
|
|
548
538
|
: ['moda investigate'];
|
|
549
539
|
const confidence = normalizeConfidence(record?.confidence);
|
|
540
|
+
// Prefer the backend's canonical v1 payload when it ships one; otherwise
|
|
541
|
+
// synthesize a v1 projection from the legacy snake_case fields so older
|
|
542
|
+
// backends still produce a valid moda.intelligence.v1 `data`.
|
|
543
|
+
const intelligenceRecord = asRecord(record?.intelligence);
|
|
544
|
+
const intelligence = looksLikeIntelligenceV1(intelligenceRecord)
|
|
545
|
+
? coerceIntelligenceV1(intelligenceRecord, { question, answer, confidence })
|
|
546
|
+
: synthesizeIntelligenceV1({
|
|
547
|
+
question,
|
|
548
|
+
answer,
|
|
549
|
+
confidence,
|
|
550
|
+
evidenceRefs,
|
|
551
|
+
nextCommands,
|
|
552
|
+
warnings: [],
|
|
553
|
+
source: 'cloud',
|
|
554
|
+
degraded: false,
|
|
555
|
+
});
|
|
550
556
|
return {
|
|
551
557
|
schemaVersion: ASK_SCHEMA_VERSION,
|
|
552
558
|
generatedAt: new Date().toISOString(),
|
|
553
559
|
source: 'cloud',
|
|
560
|
+
degraded: false,
|
|
554
561
|
question,
|
|
555
562
|
answer,
|
|
556
563
|
confidence,
|
|
@@ -558,8 +565,260 @@ function normalizeCloudAsk(response, question) {
|
|
|
558
565
|
findings: [],
|
|
559
566
|
nextCommands,
|
|
560
567
|
warnings: [],
|
|
568
|
+
intelligence,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Heuristic "is this the canonical v1 payload?" guard. We accept an object that
|
|
573
|
+
* either declares the matching `schema_version` or carries the core v1 fields
|
|
574
|
+
* (`answer_markdown` / `summary`). A present-but-wrong `schema_version` is
|
|
575
|
+
* rejected so a future breaking version falls back to synthesis rather than
|
|
576
|
+
* being mis-projected.
|
|
577
|
+
*/
|
|
578
|
+
function looksLikeIntelligenceV1(record) {
|
|
579
|
+
if (!record)
|
|
580
|
+
return false;
|
|
581
|
+
const version = asString(record.schema_version);
|
|
582
|
+
if (version && version !== INTELLIGENCE_SCHEMA_VERSION)
|
|
583
|
+
return false;
|
|
584
|
+
return (version === INTELLIGENCE_SCHEMA_VERSION ||
|
|
585
|
+
typeof record.answer_markdown === 'string' ||
|
|
586
|
+
asRecord(record.summary) !== null);
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Defensively project a backend `intelligence` object into a schema-valid
|
|
590
|
+
* {@link ModaIntelligenceV1}. Every field is coerced and defaulted so the CLI
|
|
591
|
+
* never emits a malformed payload even if the backend is partially shaped.
|
|
592
|
+
*/
|
|
593
|
+
function coerceIntelligenceV1(record, context) {
|
|
594
|
+
const summaryRecord = asRecord(record.summary);
|
|
595
|
+
const summary = {
|
|
596
|
+
text: asString(summaryRecord?.text) ?? context.answer,
|
|
597
|
+
confidence: confidenceValue(summaryRecord?.confidence) ?? context.confidence,
|
|
598
|
+
};
|
|
599
|
+
const evidenceRefs = Array.isArray(record.evidence_refs)
|
|
600
|
+
? record.evidence_refs.flatMap(coerceV1EvidenceRef)
|
|
601
|
+
: [];
|
|
602
|
+
const answerClaims = Array.isArray(record.answer_claims)
|
|
603
|
+
? record.answer_claims.flatMap(coerceV1Claim)
|
|
604
|
+
: [];
|
|
605
|
+
const findings = Array.isArray(record.findings)
|
|
606
|
+
? record.findings.flatMap(coerceV1Finding)
|
|
607
|
+
: [];
|
|
608
|
+
const nextCommands = Array.isArray(record.next_commands)
|
|
609
|
+
? record.next_commands.flatMap(coerceV1NextCommand)
|
|
610
|
+
: [];
|
|
611
|
+
const warnings = stringArray(record.warnings);
|
|
612
|
+
const meta = asRecord(record.meta);
|
|
613
|
+
return {
|
|
614
|
+
schema_version: INTELLIGENCE_SCHEMA_VERSION,
|
|
615
|
+
generated_at: asString(record.generated_at) ?? new Date().toISOString(),
|
|
616
|
+
command: asString(record.command) ?? 'ask',
|
|
617
|
+
source: record.source === 'local_fallback' ? 'local_fallback' : 'cloud',
|
|
618
|
+
degraded: record.degraded === true,
|
|
619
|
+
question: asString(record.question) ?? context.question,
|
|
620
|
+
summary,
|
|
621
|
+
answer_markdown: asString(record.answer_markdown) ?? context.answer,
|
|
622
|
+
answer_claims: answerClaims,
|
|
623
|
+
findings,
|
|
624
|
+
evidence_refs: evidenceRefs,
|
|
625
|
+
next_commands: nextCommands,
|
|
626
|
+
warnings,
|
|
627
|
+
...(meta ? { meta } : {}),
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Build a v1 payload from legacy snake_case fields (or local-fallback evidence).
|
|
632
|
+
* `evidence_refs` default to `grounding: 'measured'` and `answer_claims` is
|
|
633
|
+
* empty because the legacy contract has no per-claim grounding verdicts.
|
|
634
|
+
*/
|
|
635
|
+
function synthesizeIntelligenceV1(options) {
|
|
636
|
+
return {
|
|
637
|
+
schema_version: INTELLIGENCE_SCHEMA_VERSION,
|
|
638
|
+
generated_at: new Date().toISOString(),
|
|
639
|
+
command: 'ask',
|
|
640
|
+
source: options.source,
|
|
641
|
+
degraded: options.degraded,
|
|
642
|
+
question: options.question,
|
|
643
|
+
summary: { text: options.answer, confidence: options.confidence },
|
|
644
|
+
answer_markdown: options.answer,
|
|
645
|
+
answer_claims: [],
|
|
646
|
+
findings: (options.findings ?? []).map(productionFindingToV1),
|
|
647
|
+
evidence_refs: options.evidenceRefs.map(productionEvidenceToV1),
|
|
648
|
+
next_commands: options.nextCommands.map(commandStringToV1),
|
|
649
|
+
warnings: options.warnings,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function coerceV1EvidenceRef(item) {
|
|
653
|
+
const ref = asRecord(item);
|
|
654
|
+
if (!ref)
|
|
655
|
+
return [];
|
|
656
|
+
const label = asString(ref.label);
|
|
657
|
+
const anchor = coerceV1Anchor(asRecord(ref.anchor));
|
|
658
|
+
const snippet = asString(ref.snippet);
|
|
659
|
+
return [{
|
|
660
|
+
id: asString(ref.id) ?? `ask:evidence:${slug(label ?? 'ref')}`,
|
|
661
|
+
kind: asString(ref.kind) ?? 'ask',
|
|
662
|
+
label: label ?? asString(ref.id) ?? 'Cloud evidence',
|
|
663
|
+
...(anchor ? { anchor } : {}),
|
|
664
|
+
...(snippet ? { snippet } : {}),
|
|
665
|
+
grounding: provenanceValue(ref.grounding) ?? 'measured',
|
|
666
|
+
}];
|
|
667
|
+
}
|
|
668
|
+
function coerceV1Anchor(record) {
|
|
669
|
+
if (!record)
|
|
670
|
+
return undefined;
|
|
671
|
+
const anchor = {
|
|
672
|
+
...(asString(record.conversation_id) ? { conversation_id: asString(record.conversation_id) } : {}),
|
|
673
|
+
...(asNumber(record.message_index) !== null ? { message_index: asNumber(record.message_index) } : {}),
|
|
674
|
+
...(asString(record.run_id) ? { run_id: asString(record.run_id) } : {}),
|
|
675
|
+
...(asString(record.tool_name) ? { tool_name: asString(record.tool_name) } : {}),
|
|
676
|
+
};
|
|
677
|
+
return Object.keys(anchor).length > 0 ? anchor : undefined;
|
|
678
|
+
}
|
|
679
|
+
function coerceV1Claim(item) {
|
|
680
|
+
const claim = asRecord(item);
|
|
681
|
+
if (!claim)
|
|
682
|
+
return [];
|
|
683
|
+
const text = asString(claim.text);
|
|
684
|
+
if (!text)
|
|
685
|
+
return [];
|
|
686
|
+
const value = asNumber(claim.value);
|
|
687
|
+
const matchedPath = asString(claim.matched_path);
|
|
688
|
+
return [{
|
|
689
|
+
text,
|
|
690
|
+
...(value !== null ? { value } : {}),
|
|
691
|
+
kind: claim.kind === 'percentage' || claim.kind === 'count' ? claim.kind : 'qualitative',
|
|
692
|
+
status: claim.status === 'grounded' || claim.status === 'ungrounded'
|
|
693
|
+
? claim.status
|
|
694
|
+
: 'unverifiable',
|
|
695
|
+
evidence_ref_ids: stringArray(claim.evidence_ref_ids),
|
|
696
|
+
...(matchedPath ? { matched_path: matchedPath } : {}),
|
|
697
|
+
}];
|
|
698
|
+
}
|
|
699
|
+
function coerceV1Finding(item) {
|
|
700
|
+
const finding = asRecord(item);
|
|
701
|
+
if (!finding)
|
|
702
|
+
return [];
|
|
703
|
+
const id = asString(finding.id);
|
|
704
|
+
const title = asString(finding.title);
|
|
705
|
+
if (!id || !title)
|
|
706
|
+
return [];
|
|
707
|
+
const severity = finding.severity;
|
|
708
|
+
const description = asString(finding.description);
|
|
709
|
+
return [{
|
|
710
|
+
id,
|
|
711
|
+
title,
|
|
712
|
+
...(severity === 'critical' || severity === 'high' || severity === 'medium' ||
|
|
713
|
+
severity === 'low' || severity === 'info'
|
|
714
|
+
? { severity }
|
|
715
|
+
: {}),
|
|
716
|
+
...(description ? { description } : {}),
|
|
717
|
+
evidence_ref_ids: stringArray(finding.evidence_ref_ids),
|
|
718
|
+
recommended_actions: Array.isArray(finding.recommended_actions)
|
|
719
|
+
? finding.recommended_actions.flatMap(coerceV1RecommendedAction)
|
|
720
|
+
: [],
|
|
721
|
+
}];
|
|
722
|
+
}
|
|
723
|
+
function coerceV1RecommendedAction(item) {
|
|
724
|
+
const action = asRecord(item);
|
|
725
|
+
if (!action)
|
|
726
|
+
return [];
|
|
727
|
+
const id = asString(action.id);
|
|
728
|
+
const title = asString(action.title);
|
|
729
|
+
if (!id || !title)
|
|
730
|
+
return [];
|
|
731
|
+
const rationale = asString(action.rationale);
|
|
732
|
+
const command = asString(action.command);
|
|
733
|
+
return [{
|
|
734
|
+
id,
|
|
735
|
+
title,
|
|
736
|
+
...(rationale ? { rationale } : {}),
|
|
737
|
+
...(command ? { command } : {}),
|
|
738
|
+
mutability: mutabilityValue(action.mutability) ?? 'unknown',
|
|
739
|
+
requires_approval: action.requires_approval === true,
|
|
740
|
+
}];
|
|
741
|
+
}
|
|
742
|
+
function coerceV1NextCommand(item) {
|
|
743
|
+
if (typeof item === 'string')
|
|
744
|
+
return [commandStringToV1(item)];
|
|
745
|
+
const record = asRecord(item);
|
|
746
|
+
if (!record)
|
|
747
|
+
return [];
|
|
748
|
+
const command = asString(record.command);
|
|
749
|
+
if (!command)
|
|
750
|
+
return [];
|
|
751
|
+
const purpose = asString(record.purpose);
|
|
752
|
+
return [{
|
|
753
|
+
command,
|
|
754
|
+
...(purpose ? { purpose } : {}),
|
|
755
|
+
mutability: mutabilityValue(record.mutability) ?? mutabilityForCommand(command),
|
|
756
|
+
requires_approval: typeof record.requires_approval === 'boolean'
|
|
757
|
+
? record.requires_approval
|
|
758
|
+
: requiresApprovalForCommand(command),
|
|
759
|
+
}];
|
|
760
|
+
}
|
|
761
|
+
function productionEvidenceToV1(ref) {
|
|
762
|
+
const anchor = {
|
|
763
|
+
...(ref.conversationId ? { conversation_id: ref.conversationId } : {}),
|
|
764
|
+
...(ref.runId ? { run_id: ref.runId } : {}),
|
|
765
|
+
...(ref.toolName ? { tool_name: ref.toolName } : {}),
|
|
766
|
+
};
|
|
767
|
+
return {
|
|
768
|
+
id: ref.id,
|
|
769
|
+
kind: ref.kind,
|
|
770
|
+
label: ref.label,
|
|
771
|
+
...(Object.keys(anchor).length > 0 ? { anchor } : {}),
|
|
772
|
+
grounding: 'measured',
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
function productionFindingToV1(finding) {
|
|
776
|
+
return {
|
|
777
|
+
id: finding.id,
|
|
778
|
+
title: finding.title,
|
|
779
|
+
severity: finding.severity,
|
|
780
|
+
description: `${finding.summary} ${finding.rankReason}`.trim(),
|
|
781
|
+
evidence_ref_ids: finding.evidenceRefIds,
|
|
782
|
+
recommended_actions: finding.recommendedActions.map((action) => ({
|
|
783
|
+
id: action.id,
|
|
784
|
+
title: action.title,
|
|
785
|
+
rationale: action.rationale,
|
|
786
|
+
command: action.command,
|
|
787
|
+
mutability: action.mutability,
|
|
788
|
+
requires_approval: action.requiresApproval,
|
|
789
|
+
})),
|
|
561
790
|
};
|
|
562
791
|
}
|
|
792
|
+
function commandStringToV1(command) {
|
|
793
|
+
return {
|
|
794
|
+
command,
|
|
795
|
+
purpose: purposeForCommand(command),
|
|
796
|
+
mutability: mutabilityForCommand(command),
|
|
797
|
+
requires_approval: requiresApprovalForCommand(command),
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
function mutabilityForCommand(command) {
|
|
801
|
+
return command.includes(' sync') ? 'sync' : 'read';
|
|
802
|
+
}
|
|
803
|
+
function requiresApprovalForCommand(command) {
|
|
804
|
+
return command.includes(' sync') || command.includes(' --yes');
|
|
805
|
+
}
|
|
806
|
+
function stringArray(value) {
|
|
807
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
|
|
808
|
+
}
|
|
809
|
+
function confidenceValue(value) {
|
|
810
|
+
return value === 'high' || value === 'medium' || value === 'low' || value === 'unknown'
|
|
811
|
+
? value
|
|
812
|
+
: null;
|
|
813
|
+
}
|
|
814
|
+
function provenanceValue(value) {
|
|
815
|
+
return value === 'measured' || value === 'inferred' ? value : null;
|
|
816
|
+
}
|
|
817
|
+
function mutabilityValue(value) {
|
|
818
|
+
return value === 'read' || value === 'write' || value === 'sync' || value === 'unknown'
|
|
819
|
+
? value
|
|
820
|
+
: null;
|
|
821
|
+
}
|
|
563
822
|
function investigationAgentOptions(investigation) {
|
|
564
823
|
return {
|
|
565
824
|
summary: investigation.summary,
|
|
@@ -585,6 +844,17 @@ function overviewAgentOptions(briefing) {
|
|
|
585
844
|
};
|
|
586
845
|
}
|
|
587
846
|
function askAgentOptions(result) {
|
|
847
|
+
const errors = result.degraded
|
|
848
|
+
? [{
|
|
849
|
+
code: 'cloud_unavailable',
|
|
850
|
+
message: result.fallbackReason ??
|
|
851
|
+
'Moda Cloud intelligence was unavailable; answer synthesized from local Data API evidence.',
|
|
852
|
+
// Non-blocking: an answer WAS produced, just from a degraded source. The
|
|
853
|
+
// agent should still treat it as lower-trust (see exit code 3 / status).
|
|
854
|
+
blocking: false,
|
|
855
|
+
suggested_commands: ['moda doctor --online'],
|
|
856
|
+
}]
|
|
857
|
+
: [];
|
|
588
858
|
return {
|
|
589
859
|
summary: { text: result.answer, confidence: result.confidence },
|
|
590
860
|
findings: result.findings.map(agentFinding),
|
|
@@ -592,6 +862,7 @@ function askAgentOptions(result) {
|
|
|
592
862
|
recommendedActions: result.findings.flatMap((finding) => finding.recommendedActions.map(agentAction)),
|
|
593
863
|
nextCommands: result.nextCommands.map(nextCommand),
|
|
594
864
|
warnings: result.warnings,
|
|
865
|
+
errors,
|
|
595
866
|
};
|
|
596
867
|
}
|
|
597
868
|
function agentFinding(finding) {
|
|
@@ -707,17 +978,31 @@ function renderAskHuman(result) {
|
|
|
707
978
|
const lines = [];
|
|
708
979
|
lines.push('Moda ask');
|
|
709
980
|
lines.push(`Question: ${result.question}`);
|
|
981
|
+
if (result.degraded) {
|
|
982
|
+
lines.push('');
|
|
983
|
+
lines.push('[degraded] Moda Cloud was unavailable — this answer is synthesized from local Data API evidence (lower trust).');
|
|
984
|
+
}
|
|
710
985
|
lines.push('');
|
|
711
986
|
lines.push(result.answer);
|
|
712
987
|
lines.push('');
|
|
713
988
|
lines.push(`Confidence: ${result.confidence}`);
|
|
989
|
+
lines.push(`Source: ${result.source}${result.degraded ? ' (degraded)' : ''}`);
|
|
714
990
|
if (result.evidenceRefs.length > 0) {
|
|
715
991
|
lines.push('');
|
|
716
992
|
lines.push('Evidence');
|
|
993
|
+
// Subtle grounding marker: flag model-inferred evidence so a human reader
|
|
994
|
+
// can weight it. Ids/anchors stay hidden in the human projection.
|
|
995
|
+
const grounding = new Map(result.intelligence.evidence_refs.map((ref) => [ref.id, ref.grounding]));
|
|
717
996
|
for (const ref of result.evidenceRefs.slice(0, 8)) {
|
|
718
|
-
|
|
997
|
+
const marker = grounding.get(ref.id) === 'inferred' ? ' (inferred)' : '';
|
|
998
|
+
lines.push(` - ${ref.label}${ref.path ? ` (${ref.path})` : ''}${marker}`);
|
|
719
999
|
}
|
|
720
1000
|
}
|
|
1001
|
+
const unverifiedClaims = result.intelligence.answer_claims.filter((claim) => claim.status !== 'grounded').length;
|
|
1002
|
+
if (unverifiedClaims > 0) {
|
|
1003
|
+
lines.push('');
|
|
1004
|
+
lines.push(`Note: ${unverifiedClaims} claim(s) could not be grounded against tool data.`);
|
|
1005
|
+
}
|
|
721
1006
|
if (result.nextCommands.length > 0) {
|
|
722
1007
|
lines.push('');
|
|
723
1008
|
lines.push('Next commands');
|
|
@@ -775,24 +1060,6 @@ function toolRows(data, scopedTool) {
|
|
|
775
1060
|
}];
|
|
776
1061
|
});
|
|
777
1062
|
}
|
|
778
|
-
function runRows(data) {
|
|
779
|
-
const record = asRecord(data);
|
|
780
|
-
const run = asRecord(record?.run);
|
|
781
|
-
const rows = Array.isArray(record?.runs) ? record.runs : run ? [run] : [];
|
|
782
|
-
return rows.flatMap((item) => {
|
|
783
|
-
const row = asRecord(item);
|
|
784
|
-
if (!row)
|
|
785
|
-
return [];
|
|
786
|
-
return [{
|
|
787
|
-
id: asString(row.job_id) ?? asString(row.run_id) ?? '',
|
|
788
|
-
outcome: asString(row.outcome) ?? 'unknown',
|
|
789
|
-
agentName: asString(row.agent_name) ?? undefined,
|
|
790
|
-
reason: asString(row.outcome_reason) ?? undefined,
|
|
791
|
-
conversationId: asString(row.conversation_id) ?? undefined,
|
|
792
|
-
raw: row,
|
|
793
|
-
}];
|
|
794
|
-
});
|
|
795
|
-
}
|
|
796
1063
|
function loopRows(data) {
|
|
797
1064
|
const record = asRecord(data);
|
|
798
1065
|
const rows = Array.isArray(record?.loops)
|
|
@@ -1056,17 +1323,6 @@ function numberFlag(flags, key, fallback) {
|
|
|
1056
1323
|
return fallback;
|
|
1057
1324
|
return Math.min(Math.floor(parsed), 90);
|
|
1058
1325
|
}
|
|
1059
|
-
function timeRangeForDays(days) {
|
|
1060
|
-
if (days <= 1)
|
|
1061
|
-
return '24h';
|
|
1062
|
-
if (days <= 3)
|
|
1063
|
-
return '3d';
|
|
1064
|
-
if (days <= 7)
|
|
1065
|
-
return '7d';
|
|
1066
|
-
if (days <= 30)
|
|
1067
|
-
return '30d';
|
|
1068
|
-
return '90d';
|
|
1069
|
-
}
|
|
1070
1326
|
function normalizeConfidence(value) {
|
|
1071
1327
|
if (value === 'high' || value === 'medium' || value === 'low' || value === 'unknown')
|
|
1072
1328
|
return value;
|
|
@@ -1080,7 +1336,6 @@ function evidenceKind(value) {
|
|
|
1080
1336
|
value === 'tool_failure' ||
|
|
1081
1337
|
value === 'frustration' ||
|
|
1082
1338
|
value === 'loop' ||
|
|
1083
|
-
value === 'run' ||
|
|
1084
1339
|
value === 'harness' ||
|
|
1085
1340
|
value === 'ask')
|
|
1086
1341
|
return value;
|
|
@@ -1102,8 +1357,6 @@ function purposeForCommand(command) {
|
|
|
1102
1357
|
return 'Read the relevant conversation window.';
|
|
1103
1358
|
if (command.startsWith('moda loop'))
|
|
1104
1359
|
return 'Inspect loop detection details.';
|
|
1105
|
-
if (command.startsWith('moda run'))
|
|
1106
|
-
return 'Inspect one agent run and side-effect receipts.';
|
|
1107
1360
|
if (command.startsWith('moda doctor'))
|
|
1108
1361
|
return 'Validate Moda setup and data flow.';
|
|
1109
1362
|
if (command.startsWith('moda overview'))
|