@moda-ai/cli 1.49.0 → 1.50.0

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.
@@ -7,7 +7,7 @@ import {
7
7
  saveProfileConfig,
8
8
  secretFilePathFromRef,
9
9
  writeSecretJsonFile
10
- } from "./cli-bqhs6sk7.js";
10
+ } from "./cli-xrs02c0t.js";
11
11
  import {
12
12
  __require
13
13
  } from "./cli-0v6na3yp.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  authFetch,
3
3
  getCachedLoginContext
4
- } from "./cli-c64d2jrw.js";
4
+ } from "./cli-7kbzqact.js";
5
5
  import {
6
6
  CliAccessError,
7
7
  CliInputError,
@@ -15,7 +15,7 @@ import {
15
15
  resolveTenantIdFromActiveProfile,
16
16
  saveCliConfig,
17
17
  writeSecretJsonFile
18
- } from "./cli-bqhs6sk7.js";
18
+ } from "./cli-xrs02c0t.js";
19
19
 
20
20
  // src/init/tenant.ts
21
21
  import { randomUUID } from "node:crypto";
@@ -13,7 +13,7 @@ import {
13
13
  resolvePromptSource,
14
14
  safePromptEvidencePath,
15
15
  scanHarness
16
- } from "./cli-m3bg9yyh.js";
16
+ } from "./cli-x0x0n35w.js";
17
17
  import {
18
18
  SKILL_SOURCE_HARNESS,
19
19
  SKILL_SOURCE_INTEGRATION_BUNDLED,
@@ -26,7 +26,7 @@ import {
26
26
  import {
27
27
  isAuthSessionValid,
28
28
  loadAuthSession
29
- } from "./cli-c64d2jrw.js";
29
+ } from "./cli-7kbzqact.js";
30
30
  import {
31
31
  CliInputError,
32
32
  EXIT_ERROR,
@@ -45,7 +45,7 @@ import {
45
45
  resolveTenantId,
46
46
  secretFilePathFromRef,
47
47
  validateConfig
48
- } from "./cli-bqhs6sk7.js";
48
+ } from "./cli-xrs02c0t.js";
49
49
  import {
50
50
  __commonJS,
51
51
  __require,
@@ -6711,7 +6711,7 @@ async function runPromptSyncExclusive(flags, profileOptions, options) {
6711
6711
  const analyzedDefinitions = new Map;
6712
6712
  if (flags["no-analyze"] !== "true") {
6713
6713
  options.onProgress?.("Analyzing prompt sources before sync.");
6714
- const { analyzeHarnessPrompts } = await import("./harness-4ypeg9dh.js");
6714
+ const { analyzeHarnessPrompts } = await import("./harness-7gr0xbtc.js");
6715
6715
  const result = await analyzeHarnessPrompts({
6716
6716
  rootDir: process.cwd(),
6717
6717
  existing: established.flatMap((prompt) => prompt.sourceDefinition ? [prompt.sourceDefinition.source] : []),
@@ -25,7 +25,7 @@ import {
25
25
  resolveTenantId,
26
26
  shouldInventoryFile,
27
27
  stringOption
28
- } from "./cli-bqhs6sk7.js";
28
+ } from "./cli-xrs02c0t.js";
29
29
  import {
30
30
  __commonJS,
31
31
  __require,
@@ -180210,7 +180210,7 @@ function citationExcerpt(rootDir, path, line) {
180210
180210
  const selected = line ? lines[line - 1] : lines.find((candidate) => candidate.trim().length > 0);
180211
180211
  if (!selected)
180212
180212
  return {};
180213
- return { excerpt: selected.trim().slice(0, 240) };
180213
+ return { excerpt: selected.trim() };
180214
180214
  } catch {
180215
180215
  return {};
180216
180216
  }
@@ -181065,7 +181065,6 @@ function enrichScanCaptureMetadata(graph) {
181065
181065
  // src/harness-candidates.ts
181066
181066
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
181067
181067
  import { basename as basename3, dirname as dirname3, extname, resolve as resolve4 } from "node:path";
181068
- var EXCERPT_MAX_CHARS = 240;
181069
181068
  var MAX_CANDIDATES_PER_FILE_PER_SIGNAL = 20;
181070
181069
  var MIN_PROMPT_LITERAL_CHARS = 160;
181071
181070
  var CONFIDENCE_WEIGHT = {
@@ -181311,8 +181310,7 @@ function instructionScore(text) {
181311
181310
  return score;
181312
181311
  }
181313
181312
  function normalizeExcerpt(text) {
181314
- const collapsed = text.replace(/\s+/g, " ").trim();
181315
- return collapsed.length > EXCERPT_MAX_CHARS ? `${collapsed.slice(0, EXCERPT_MAX_CHARS - 1)}…` : collapsed;
181313
+ return text.replace(/\s+/g, " ").trim();
181316
181314
  }
181317
181315
  function buildLineIndex(text) {
181318
181316
  const offsets = [0];
@@ -181423,7 +181421,7 @@ function detectPromptModule(file, out) {
181423
181421
  `).length),
181424
181422
  confidence: "medium",
181425
181423
  signal: "prompt_module.name_hint",
181426
- excerpt: normalizeExcerpt(file.text.slice(0, EXCERPT_MAX_CHARS * 2))
181424
+ excerpt: normalizeExcerpt(file.text)
181427
181425
  });
181428
181426
  }
181429
181427
  function detectPromptLiterals(file, out) {
@@ -181506,7 +181504,7 @@ function detectPromptFile(file, out) {
181506
181504
  `).length),
181507
181505
  confidence,
181508
181506
  signal,
181509
- excerpt: normalizeExcerpt(file.text.slice(0, EXCERPT_MAX_CHARS * 2))
181507
+ excerpt: normalizeExcerpt(file.text)
181510
181508
  });
181511
181509
  }
181512
181510
  function detectPromptLoaderReferences(file, filesByPath, out) {
@@ -184859,7 +184857,7 @@ async function runHarnessCommand(context) {
184859
184857
  throw new CliInputError("Focused remote prompt analysis is not supported yet.", "Use local --analyst=claude|codex|cursor|local-scan, or drop --concern for a full remote report.");
184860
184858
  }
184861
184859
  if (context.flags["github-actions"] === "true") {
184862
- const { runGithubActionsAnalyze } = await import("./harness-github-actions-dvq7spqh.js");
184860
+ const { runGithubActionsAnalyze } = await import("./harness-github-actions-q9kewzb6.js");
184863
184861
  const result = await runGithubActionsAnalyze(rootDir, {
184864
184862
  writeReport: (report2) => {
184865
184863
  const normalized = normalizeHarnessReport(report2);
@@ -186605,6 +186603,7 @@ var CONCERN_CANDIDATE_KINDS = {
186605
186603
  evals: ["model_config"]
186606
186604
  };
186607
186605
  var MAX_SEED_CANDIDATES_PER_KIND = 40;
186606
+ var MAX_INLINE_CANDIDATE_EXCERPT_CHARS = 500;
186608
186607
  var MAX_SEED_READ_PLAN_FILES = 25;
186609
186608
  var SEED_CANDIDATE_KIND_LABELS = {
186610
186609
  llm_call_site: "LLM call sites",
@@ -186644,7 +186643,7 @@ function renderAnalystCandidateLines(report, concern) {
186644
186643
  const shown = bucket.slice(0, MAX_SEED_CANDIDATES_PER_KIND);
186645
186644
  lines.push(`${label} (${bucket.length}${bucket.length > shown.length ? `, showing ${shown.length}` : ""}):`);
186646
186645
  for (const candidate of shown) {
186647
- lines.push(`- ${candidate.path}:${candidate.startLine} [${candidate.signal}] ${candidate.excerpt}`);
186646
+ lines.push(candidate.endLine > candidate.startLine ? `- ${candidate.path}:${candidate.startLine}-${candidate.endLine} [${candidate.signal}] (multi-line span; read lines ${candidate.startLine}-${candidate.endLine} of the file)` : candidate.excerpt.length > MAX_INLINE_CANDIDATE_EXCERPT_CHARS ? `- ${candidate.path}:${candidate.startLine} [${candidate.signal}] (${candidate.excerpt.length}-character match; read line ${candidate.startLine} of the file)` : `- ${candidate.path}:${candidate.startLine} [${candidate.signal}] ${candidate.excerpt}`);
186648
186647
  }
186649
186648
  }
186650
186649
  const readPlan = report.files.slice(0, MAX_SEED_READ_PLAN_FILES);
@@ -986,6 +986,9 @@ function packSnapshot(inputs, opts) {
986
986
  }
987
987
  // src/output.ts
988
988
  import { randomUUID } from "node:crypto";
989
+ import { lstatSync, mkdirSync as mkdirSync2, readdirSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
990
+ import { tmpdir } from "node:os";
991
+ import { join as join2 } from "node:path";
989
992
  var AGENT_SCHEMA_VERSION = "moda.agent.v1";
990
993
  var AGENT_EVENT_SCHEMA_VERSION = "moda.agent_event.v1";
991
994
  var FEEDBACK_TIP = 'Help us improve! If a result in this response looks wrong or is missing data, flag it: moda feedback "<describe what looked wrong>" [--category=...] [--conversation-id=... | --cluster-id=... | --tool-name=...]. Only submit genuine observations.';
@@ -1005,7 +1008,8 @@ var AGENT_ERROR_CODES = [
1005
1008
  "command_error",
1006
1009
  "access_denied",
1007
1010
  "unknown_error",
1008
- "cloud_unavailable"
1011
+ "cloud_unavailable",
1012
+ "output_error"
1009
1013
  ];
1010
1014
  var FALLBACK_RUN_ID = randomUUID();
1011
1015
  var fallbackSeq = 0;
@@ -1116,6 +1120,7 @@ function createCliOutput(options) {
1116
1120
  seq: 0,
1117
1121
  started: false,
1118
1122
  terminal: false,
1123
+ outputError: false,
1119
1124
  startedAt: Date.now(),
1120
1125
  lastEventAt: Date.now()
1121
1126
  };
@@ -1177,6 +1182,8 @@ function createCliOutput(options) {
1177
1182
  if (state.terminal)
1178
1183
  return;
1179
1184
  const envelope = writeEnvelope(data, writeOptions);
1185
+ if (envelope.errors.some((error) => error.code === "output_error"))
1186
+ state.outputError = true;
1180
1187
  if (options.mode === "agent-stream") {
1181
1188
  ensureStarted();
1182
1189
  emitEvent({
@@ -1272,6 +1279,9 @@ function createCliOutput(options) {
1272
1279
  hasTerminalOutput() {
1273
1280
  return state.terminal;
1274
1281
  },
1282
+ hasOutputError() {
1283
+ return state.outputError;
1284
+ },
1275
1285
  close() {
1276
1286
  clearHeartbeat(state);
1277
1287
  }
@@ -1281,41 +1291,198 @@ function createCliOutput(options) {
1281
1291
  function createAgentEnvelope(options) {
1282
1292
  const safeData = redactDeep(options.data);
1283
1293
  const maxBytes = agentMaxOutputBytes();
1294
+ let fullOutput;
1295
+ let saveFailed = false;
1296
+ let envelopeTextSaveFailed = false;
1297
+ const writeFullOutput = options.persistFullOutput === false ? undefined : () => {
1298
+ fullOutput ??= { path: writeAgentFullOutput(options.command, safeData) };
1299
+ if (!fullOutput.path)
1300
+ saveFailed = true;
1301
+ return fullOutput.path;
1302
+ };
1303
+ let text = redactDeep({
1304
+ summary: normalizeSummary(options.summary, options.command, options.status, safeData),
1305
+ findings: options.findings ?? [],
1306
+ evidence_refs: options.evidenceRefs ?? [],
1307
+ recommended_actions: options.recommendedActions ?? [],
1308
+ next_commands: normalizeNextCommands(options.nextCommands ?? []),
1309
+ warnings: options.warnings ?? inferWarnings(safeData)
1310
+ });
1311
+ let moved;
1284
1312
  const build = (bounded) => ({
1285
1313
  schema_version: AGENT_SCHEMA_VERSION,
1286
1314
  command: options.command,
1287
1315
  status: options.status,
1288
1316
  generated_at: new Date().toISOString(),
1289
1317
  ...options.profile ? { profile: options.profile } : {},
1290
- summary: normalizeSummary(options.summary, options.command, options.status, safeData),
1291
- findings: options.findings ?? [],
1292
- evidence_refs: options.evidenceRefs ?? [],
1293
- recommended_actions: options.recommendedActions ?? [],
1294
- next_commands: normalizeNextCommands(options.nextCommands ?? []),
1295
- artifacts: options.artifacts ?? [],
1318
+ summary: text.summary,
1319
+ findings: text.findings,
1320
+ evidence_refs: text.evidence_refs,
1321
+ recommended_actions: text.recommended_actions,
1322
+ next_commands: text.next_commands,
1323
+ artifacts: [
1324
+ ...options.artifacts ?? [],
1325
+ ...bounded.fullOutputPath ? [{ kind: "full_output", path: bounded.fullOutputPath, description: "Complete command output (the same JSON --json prints); data in this envelope was bounded." }] : [],
1326
+ ...moved?.path ? [{ kind: "full_envelope_text", path: moved.path, description: "Whole envelope text (summary, findings, evidence_refs, actions, next_commands, warnings) before oversized fields were moved out of this envelope." }] : []
1327
+ ],
1296
1328
  warnings: [
1297
- ...options.warnings ?? inferWarnings(safeData),
1298
- ...bounded.warning ? [bounded.warning] : []
1329
+ ...text.warnings,
1330
+ ...bounded.warning ? [bounded.warning] : [],
1331
+ ...moved ? [movedTextWarning(moved)] : []
1299
1332
  ],
1300
1333
  errors: options.errors ?? [],
1301
1334
  data: bounded.data,
1302
1335
  ...options.meta ? { meta: options.meta } : {},
1303
- ...bounded.truncation ? { truncation: bounded.truncation } : {}
1336
+ ...bounded.truncation || moved ? { truncation: {
1337
+ ...bounded.truncation ?? {},
1338
+ ...moved ? { envelope_text: {
1339
+ reason: "agent_output_byte_limit",
1340
+ moved_fields: moved.fields,
1341
+ complete: false,
1342
+ ...moved.whole ? { whole_text_moved: true } : {},
1343
+ ...moved.path ? { full_envelope_path: moved.path } : {}
1344
+ } } : {}
1345
+ } } : {}
1304
1346
  });
1305
- let envelope = build(boundForAgent(safeData, maxBytes));
1306
- const envelopeBytes = Buffer.byteLength(JSON.stringify(envelope), "utf8");
1307
- if (envelopeBytes > maxBytes) {
1308
- const overhead = envelopeBytes - Buffer.byteLength(JSON.stringify(envelope.data) ?? "", "utf8");
1309
- envelope = build(boundForAgent(safeData, Math.max(1024, maxBytes - overhead - 1024)));
1347
+ const sizeOf = (value) => Buffer.byteLength(JSON.stringify(value) ?? "", "utf8");
1348
+ const rebound = (current) => {
1349
+ const overhead = sizeOf(current) - sizeOf(current.data);
1350
+ return build(boundForAgent(safeData, Math.max(1024, maxBytes - overhead - 1024), { writeFullOutput }));
1351
+ };
1352
+ let envelope = build(boundForAgent(safeData, maxBytes, { writeFullOutput }));
1353
+ if (sizeOf(envelope) > maxBytes)
1354
+ envelope = rebound(envelope);
1355
+ if (sizeOf(envelope) > maxBytes && !saveFailed) {
1356
+ const original = text;
1357
+ const savedPath = options.persistFullOutput === false ? undefined : writeAgentFullOutput(`${options.command}-envelope`, original);
1358
+ if (options.persistFullOutput !== false && !savedPath) {
1359
+ saveFailed = true;
1360
+ envelopeTextSaveFailed = true;
1361
+ } else {
1362
+ const slots = envelopeTextSlots(original).sort((left, right) => right.bytes - left.bytes);
1363
+ text = structuredClone(original);
1364
+ moved = { path: savedPath, fields: [] };
1365
+ for (const slot of slots) {
1366
+ if (sizeOf(envelope) <= maxBytes)
1367
+ break;
1368
+ const pointer = movedTextPointer(slot.field, slot.text, savedPath);
1369
+ const added = Buffer.byteLength(JSON.stringify(pointer), "utf8") + 2 * movedFieldNameBytes(slot.field);
1370
+ if (added >= slot.bytes)
1371
+ continue;
1372
+ slot.set(text, pointer);
1373
+ moved.fields.push(slot.field);
1374
+ envelope = rebound(envelope);
1375
+ }
1376
+ if (moved.fields.length === 0)
1377
+ moved = undefined;
1378
+ envelope = rebound(envelope);
1379
+ if (sizeOf(envelope) > maxBytes) {
1380
+ text = {
1381
+ summary: { text: movedWholeTextPointer(savedPath), confidence: original.summary.confidence },
1382
+ findings: [],
1383
+ evidence_refs: [],
1384
+ recommended_actions: [],
1385
+ next_commands: [],
1386
+ warnings: []
1387
+ };
1388
+ moved = { path: savedPath, fields: ["*"], whole: true };
1389
+ envelope = rebound(envelope);
1390
+ }
1391
+ }
1392
+ }
1393
+ if (saveFailed) {
1394
+ envelope = {
1395
+ ...envelope,
1396
+ status: "error",
1397
+ errors: [...envelope.errors, saveFailedError(maxBytes, envelopeTextSaveFailed, fullOutput?.path)]
1398
+ };
1310
1399
  }
1311
1400
  return redactDeep(envelope);
1312
1401
  }
1402
+ function saveFailedError(maxBytes, envelopeTextSaveFailed, dataPath) {
1403
+ const where = "(MODA_AGENT_OUTPUT_DIR is unwritable or the disk is full)";
1404
+ const remedy = "Point MODA_AGENT_OUTPUT_DIR at a writable directory, rerun with --json (no byte limit), or narrow the query.";
1405
+ if (!envelopeTextSaveFailed) {
1406
+ return {
1407
+ code: "output_error",
1408
+ cause: "full_output_save_failed",
1409
+ message: `Output is over the ${maxBytes}-byte agent limit and the complete output could not be saved to a file ${where}, ` + `so nothing was moved out: this envelope holds the whole result, over the limit. ${remedy}`,
1410
+ blocking: false
1411
+ };
1412
+ }
1413
+ const dataState = dataPath ? `data is NOT the whole result: it was bounded to fit, and the complete data was saved to ${dataPath} ` + "(truncation.data.full_output_path); read it in line ranges or with jq. " : "data is the whole result. ";
1414
+ return {
1415
+ code: "output_error",
1416
+ cause: "envelope_text_save_failed",
1417
+ message: `Output is over the ${maxBytes}-byte agent limit and the oversized envelope text (summary, findings, evidence_refs, ` + `recommended_actions, next_commands, warnings) could not be saved to a file ${where}, so that text was kept whole ` + `in this envelope, over the limit. ${dataState}${remedy}`,
1418
+ blocking: false
1419
+ };
1420
+ }
1421
+ function envelopeTextSlots(text) {
1422
+ const slots = [
1423
+ { field: "summary.text", text: text.summary.text, set: (target, value) => {
1424
+ target.summary.text = value;
1425
+ } }
1426
+ ];
1427
+ text.findings.forEach((finding, index) => {
1428
+ slots.push({ field: `findings[${index}].title`, text: finding.title, set: (target, value) => {
1429
+ target.findings[index].title = value;
1430
+ } });
1431
+ if (finding.description) {
1432
+ slots.push({ field: `findings[${index}].description`, text: finding.description, set: (target, value) => {
1433
+ target.findings[index].description = value;
1434
+ } });
1435
+ }
1436
+ });
1437
+ text.evidence_refs.forEach((ref, index) => {
1438
+ slots.push({ field: `evidence_refs[${index}].label`, text: ref.label, set: (target, value) => {
1439
+ target.evidence_refs[index].label = value;
1440
+ } });
1441
+ });
1442
+ text.recommended_actions.forEach((action, index) => {
1443
+ slots.push({ field: `recommended_actions[${index}].title`, text: action.title, set: (target, value) => {
1444
+ target.recommended_actions[index].title = value;
1445
+ } });
1446
+ if (action.rationale) {
1447
+ slots.push({ field: `recommended_actions[${index}].rationale`, text: action.rationale, set: (target, value) => {
1448
+ target.recommended_actions[index].rationale = value;
1449
+ } });
1450
+ }
1451
+ });
1452
+ text.next_commands.forEach((command, index) => {
1453
+ slots.push({ field: `next_commands[${index}].purpose`, text: command.purpose, set: (target, value) => {
1454
+ target.next_commands[index].purpose = value;
1455
+ } });
1456
+ });
1457
+ text.warnings.forEach((warning, index) => {
1458
+ slots.push({ field: `warnings[${index}]`, text: warning, set: (target, value) => {
1459
+ target.warnings[index] = value;
1460
+ } });
1461
+ });
1462
+ return slots.map((slot) => ({ ...slot, bytes: Buffer.byteLength(JSON.stringify(slot.text), "utf8") }));
1463
+ }
1464
+ function movedTextPointer(field, value, path) {
1465
+ const chars = Array.from(value).length;
1466
+ return path ? `[${chars} characters moved whole to ${path} at ${field}]` : `[${chars} characters over the agent byte limit; not saved. Rerun with --json or raise MODA_AGENT_MAX_OUTPUT_BYTES]`;
1467
+ }
1468
+ function movedFieldNameBytes(field) {
1469
+ return Buffer.byteLength(JSON.stringify(field), "utf8") + 2;
1470
+ }
1471
+ function movedWholeTextPointer(path) {
1472
+ return path ? `[Envelope text (summary, findings, evidence_refs, recommended_actions, next_commands, warnings) was over the agent byte limit and moved whole to ${path}]` : "[Envelope text was over the agent byte limit; not saved. Rerun with --json or raise MODA_AGENT_MAX_OUTPUT_BYTES]";
1473
+ }
1474
+ function movedTextWarning(moved) {
1475
+ if (moved.whole) {
1476
+ return "Envelope text outside data was over the agent byte limit even after moving its largest fields: all of it was moved whole " + (moved.path ? `to ${moved.path} (read it with jq)` : "(the file could not be saved; rerun with --json or raise MODA_AGENT_MAX_OUTPUT_BYTES)") + ".";
1477
+ }
1478
+ return `Envelope text outside data was over the agent byte limit: ${moved.fields.length} field(s) were moved whole ` + (moved.path ? `to ${moved.path} (same field paths; read it with jq)` : "(the file could not be saved; rerun with --json or raise MODA_AGENT_MAX_OUTPUT_BYTES)") + ` and replaced by pointers: ${moved.fields.join(", ")}.`;
1479
+ }
1313
1480
  var AGENT_ENVELOPE_RESERVE_BYTES = 2048;
1314
1481
  function agentEnvelopeFits(command, data, options = {}) {
1315
1482
  const maxBytes = agentMaxOutputBytes() - AGENT_ENVELOPE_RESERVE_BYTES;
1316
1483
  if (Buffer.byteLength(JSON.stringify(data) ?? "", "utf8") > maxBytes)
1317
1484
  return false;
1318
- const envelope = createAgentEnvelope({ command, status: "ok", data, ...options });
1485
+ const envelope = createAgentEnvelope({ command, status: "ok", data, ...options, persistFullOutput: false });
1319
1486
  return !envelope.truncation && Buffer.byteLength(JSON.stringify(envelope), "utf8") <= maxBytes;
1320
1487
  }
1321
1488
  function errorWarnings(error) {
@@ -1559,41 +1726,61 @@ function inferWarnings(data) {
1559
1726
  }
1560
1727
  return [];
1561
1728
  }
1562
- var AGENT_MAX_STRING_CHARS = 2000;
1563
- var PROTECTED_STRING_KEY = /(cursor|token|_id$|^id$|url$|href$|^key$|hash$)/i;
1564
1729
  function agentMaxOutputBytes() {
1565
1730
  return Number(process.env.MODA_AGENT_MAX_OUTPUT_BYTES ?? 200000);
1566
1731
  }
1567
- function boundForAgent(data, maxBytes = agentMaxOutputBytes()) {
1732
+ var AGENT_FULL_OUTPUT_FILE = /-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z-[0-9a-f]{8}\.json$/;
1733
+ function agentOutputRetentionMs() {
1734
+ const hours = Number(process.env.MODA_AGENT_OUTPUT_RETENTION_HOURS ?? 24);
1735
+ return (Number.isFinite(hours) && hours > 0 ? hours : 24) * 3600000;
1736
+ }
1737
+ function pruneAgentFullOutputs(dir, keep, now = Date.now()) {
1738
+ const cutoff = now - agentOutputRetentionMs();
1739
+ let names;
1740
+ try {
1741
+ names = readdirSync(dir);
1742
+ } catch {
1743
+ return;
1744
+ }
1745
+ for (const name of names) {
1746
+ if (!AGENT_FULL_OUTPUT_FILE.test(name))
1747
+ continue;
1748
+ const path = join2(dir, name);
1749
+ if (path === keep)
1750
+ continue;
1751
+ try {
1752
+ const stat = lstatSync(path);
1753
+ if (stat.isFile() && stat.mtimeMs < cutoff)
1754
+ unlinkSync(path);
1755
+ } catch {}
1756
+ }
1757
+ }
1758
+ function writeAgentFullOutput(command, data) {
1759
+ try {
1760
+ const dir = process.env.MODA_AGENT_OUTPUT_DIR || join2(tmpdir(), "moda-agent-output");
1761
+ mkdirSync2(dir, { recursive: true, mode: 448 });
1762
+ const slug = command.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "output";
1763
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
1764
+ const path = join2(dir, `${slug}-${stamp}-${randomUUID().slice(0, 8)}.json`);
1765
+ writeFileSync2(path, `${JSON.stringify(data, null, 2)}
1766
+ `, { encoding: "utf8", mode: 384 });
1767
+ pruneAgentFullOutputs(dir, path);
1768
+ return path;
1769
+ } catch {
1770
+ return;
1771
+ }
1772
+ }
1773
+ function boundForAgent(data, maxBytes = agentMaxOutputBytes(), options = {}) {
1568
1774
  const sizeOf = (value) => Buffer.byteLength(JSON.stringify(value) ?? "", "utf8");
1569
1775
  const serialized = JSON.stringify(data) ?? "";
1570
1776
  const originalBytes = Buffer.byteLength(serialized, "utf8");
1571
1777
  if (originalBytes <= maxBytes)
1572
1778
  return { data };
1573
- let shortenedStrings = 0;
1574
- const shorten = (value, limit, key) => {
1575
- if (typeof value === "string") {
1576
- if (value.length <= limit || key !== undefined && PROTECTED_STRING_KEY.test(key))
1577
- return value;
1578
- shortenedStrings += 1;
1579
- return `${value.slice(0, limit)}… [truncated ${value.length - limit} chars]`;
1580
- }
1581
- if (Array.isArray(value))
1582
- return value.map((item) => shorten(item, limit));
1583
- if (value && typeof value === "object") {
1584
- return Object.fromEntries(Object.entries(value).map(([entryKey, item]) => [entryKey, shorten(item, limit, entryKey)]));
1585
- }
1586
- return value;
1587
- };
1588
- let bounded = data;
1779
+ const fullOutputPath = options.writeFullOutput?.();
1780
+ if (options.writeFullOutput && !fullOutputPath)
1781
+ return { data, saveFailed: true };
1782
+ const bounded = JSON.parse(serialized);
1589
1783
  let boundedBytes = originalBytes;
1590
- for (const limit of [AGENT_MAX_STRING_CHARS, 500, 200]) {
1591
- shortenedStrings = 0;
1592
- bounded = shorten(data, limit);
1593
- boundedBytes = sizeOf(bounded);
1594
- if (boundedBytes <= maxBytes)
1595
- break;
1596
- }
1597
1784
  const droppedItems = {};
1598
1785
  const collectArrays = (value, path, out) => {
1599
1786
  if (Array.isArray(value)) {
@@ -1630,39 +1817,35 @@ function boundForAgent(data, maxBytes = agentMaxOutputBytes()) {
1630
1817
  boundedBytes = sizeOf(bounded);
1631
1818
  }
1632
1819
  const dropped = Object.entries(droppedItems);
1633
- const parts = [
1634
- shortenedStrings > 0 ? `shortened ${shortenedStrings} long text field(s)` : "",
1635
- ...dropped.map(([path, count]) => `dropped the last ${count} item(s) of data.${path}`)
1636
- ].filter(Boolean);
1637
- const howToGetMore = "For complete results narrow the query (a smaller --limit, with --offset or --cursor paging where the command supports it), " + "rerun with --json (no byte limit), or raise MODA_AGENT_MAX_OUTPUT_BYTES.";
1820
+ const whereIsTheRest = fullOutputPath ? `The complete output (every item, no text shortened) is in ${fullOutputPath}; read it in line ranges or query it with jq. ` : "The complete output could not be saved to a file. ";
1821
+ const howToGetMore = whereIsTheRest + "Alternatively narrow the query (a smaller --limit, with --offset or --cursor paging where the command supports it), " + "rerun with --json (no byte limit), or raise MODA_AGENT_MAX_OUTPUT_BYTES.";
1822
+ const pathField = fullOutputPath ? { full_output_path: fullOutputPath } : {};
1638
1823
  if (boundedBytes > maxBytes) {
1639
- const stub = { truncated: true, original_bytes: originalBytes, max_bytes: maxBytes, preview: serialized.slice(0, Math.max(0, maxBytes - 256)) };
1640
- for (let stubBytes = sizeOf(stub);stubBytes > maxBytes && stub.preview.length > 0; stubBytes = sizeOf(stub)) {
1641
- stub.preview = stub.preview.slice(0, Math.max(0, stub.preview.length - (stubBytes - maxBytes) - 16));
1642
- }
1643
1824
  return {
1644
- data: stub,
1645
- warning: `Output was ${originalBytes} bytes, over the ${maxBytes}-byte agent limit, and could not be reduced while keeping its structure; data is a string preview. ${howToGetMore}`,
1825
+ data: { truncated: true, original_bytes: originalBytes, max_bytes: maxBytes, ...pathField },
1826
+ warning: `Output was ${originalBytes} bytes, over the ${maxBytes}-byte agent limit, and could not be reduced while keeping its structure; data is a pointer stub. ${howToGetMore}`,
1646
1827
  truncation: {
1647
- data: { original_bytes: originalBytes, max_bytes: maxBytes, reason: "agent_output_byte_limit", structure_preserved: false, complete: false }
1648
- }
1828
+ data: { original_bytes: originalBytes, max_bytes: maxBytes, reason: "agent_output_byte_limit", structure_preserved: false, complete: false, ...pathField }
1829
+ },
1830
+ fullOutputPath
1649
1831
  };
1650
1832
  }
1651
1833
  return {
1652
1834
  data: bounded,
1653
- warning: `Output was ${originalBytes} bytes, over the ${maxBytes}-byte agent limit: ${parts.join("; ")}. ${howToGetMore}`,
1835
+ warning: `Output was ${originalBytes} bytes, over the ${maxBytes}-byte agent limit: ${dropped.map(([path, count]) => `dropped the last ${count} item(s) of data.${path}`).join("; ")}. ${howToGetMore}`,
1654
1836
  truncation: {
1655
1837
  data: {
1656
1838
  original_bytes: originalBytes,
1657
1839
  bounded_bytes: boundedBytes,
1658
1840
  max_bytes: maxBytes,
1659
1841
  reason: "agent_output_byte_limit",
1660
- shortened_strings: shortenedStrings,
1661
1842
  dropped_items: droppedItems,
1662
1843
  structure_preserved: true,
1663
- complete: false
1844
+ complete: false,
1845
+ ...pathField
1664
1846
  }
1665
- }
1847
+ },
1848
+ fullOutputPath
1666
1849
  };
1667
1850
  }
1668
1851
  function asRecord(value) {
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  runPromptsCommand,
9
9
  runSkillsCommand,
10
10
  runStatusCommand
11
- } from "./cli-6xndg2h4.js";
11
+ } from "./cli-jqfm8dp4.js";
12
12
  import {
13
13
  ApiError,
14
14
  HARNESS_REPORT_APPROVAL_PATH,
@@ -37,7 +37,7 @@ import {
37
37
  terminalStyles,
38
38
  unknownSignalsSubcommandError,
39
39
  validateHarnessReport
40
- } from "./cli-m3bg9yyh.js";
40
+ } from "./cli-x0x0n35w.js";
41
41
  import"./cli-ssfq84k5.js";
42
42
  import {
43
43
  authFetch,
@@ -46,7 +46,7 @@ import {
46
46
  loadAuthSession,
47
47
  login,
48
48
  revokeCliSessionToken
49
- } from "./cli-c64d2jrw.js";
49
+ } from "./cli-7kbzqact.js";
50
50
  import {
51
51
  AGENT_ERROR_CODES,
52
52
  CliAccessError,
@@ -97,7 +97,7 @@ import {
97
97
  validateConfig,
98
98
  wasOutputCancelled,
99
99
  writeSecretJsonFile
100
- } from "./cli-bqhs6sk7.js";
100
+ } from "./cli-xrs02c0t.js";
101
101
  import {
102
102
  __require
103
103
  } from "./cli-0v6na3yp.js";
@@ -547,8 +547,16 @@ function buildFrustrationAnchor(row) {
547
547
  if (firstQuote) {
548
548
  signal = asString(firstQuote.signal);
549
549
  const quote = asString(firstQuote.quote);
550
- if (quote !== null)
551
- quote_preview = quote.slice(0, 200);
550
+ if (quote !== null) {
551
+ let end = 0;
552
+ let points = 0;
553
+ for (const ch of quote) {
554
+ if (points++ === 200)
555
+ break;
556
+ end += ch.length;
557
+ }
558
+ quote_preview = quote.slice(0, end);
559
+ }
552
560
  }
553
561
  return {
554
562
  kind: "frustration",
@@ -624,7 +632,6 @@ function withFrustrationAnchors(response) {
624
632
  });
625
633
  return { ...response, frustrations };
626
634
  }
627
- var TOOL_FAILURE_TEXT_LIMIT = 300;
628
635
  var TOOL_FAILURE_CONTEXT_RADIUS = 2;
629
636
  function shellWord(value) {
630
637
  return /^[\w@%+=:,./-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
@@ -674,26 +681,6 @@ function trimToolFailureConversations(response, opts) {
674
681
  });
675
682
  return { ...response, examples };
676
683
  }
677
- function compactToolFailureExamples(response, maxChars = TOOL_FAILURE_TEXT_LIMIT) {
678
- if (!Array.isArray(response.examples))
679
- return { data: response, truncated: 0 };
680
- let truncated = 0;
681
- const cap = (value) => {
682
- if (typeof value === "string") {
683
- if (value.length <= maxChars)
684
- return value;
685
- truncated += 1;
686
- return `${value.slice(0, maxChars)}… [truncated ${value.length - maxChars} chars]`;
687
- }
688
- if (Array.isArray(value))
689
- return value.map(cap);
690
- if (value && typeof value === "object") {
691
- return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, cap(child)]));
692
- }
693
- return value;
694
- };
695
- return { data: { ...response, examples: response.examples.map(cap) }, truncated };
696
- }
697
684
  function withToolFailureAnchors(response) {
698
685
  if (!Array.isArray(response.examples))
699
686
  return response;
@@ -5385,7 +5372,6 @@ var MODA_DIR = ".moda";
5385
5372
  var HARNESS_PATH = join4(MODA_DIR, "harness.json");
5386
5373
  var HARNESS_REPORT_MARKDOWN_PATH = join4(MODA_DIR, "HARNESS_REPORT.md");
5387
5374
  var HARNESS_SUMMARY_PATH = join4(MODA_DIR, "HARNESS.md");
5388
- var MAX_AGENT_EVIDENCE_BYTES = 24000;
5389
5375
  async function runAgentCommand(context) {
5390
5376
  const subcommand2 = context.positionals[0] ?? "context";
5391
5377
  switch (subcommand2) {
@@ -5738,7 +5724,7 @@ function expandEvidenceRef(ref, loaded, cwd) {
5738
5724
  label: artifact.description ?? artifact.path,
5739
5725
  path: artifact.path
5740
5726
  },
5741
- value: readBoundedModaArtifact(cwd, artifact.path),
5727
+ value: readModaArtifact(cwd, artifact.path),
5742
5728
  artifact
5743
5729
  };
5744
5730
  }
@@ -5769,11 +5755,11 @@ function readCitationExcerpt(cwd, citation) {
5769
5755
  return citation.excerpt;
5770
5756
  const text = readFileSync5(absolute, "utf8");
5771
5757
  if (citation.line && citation.line > 0) {
5772
- return text.split(/\r?\n/)[citation.line - 1]?.slice(0, 1000) ?? citation.excerpt;
5758
+ return text.split(/\r?\n/)[citation.line - 1] ?? citation.excerpt;
5773
5759
  }
5774
- return text.slice(0, 1000);
5760
+ return text;
5775
5761
  }
5776
- function readBoundedModaArtifact(cwd, artifactPath) {
5762
+ function readModaArtifact(cwd, artifactPath) {
5777
5763
  if (!artifactPath.startsWith(".moda/")) {
5778
5764
  throw new CliInputError(`Artifact '${artifactPath}' is outside .moda and cannot be expanded directly.`, "Use a citation evidence ref for source-file evidence.");
5779
5765
  }
@@ -5781,12 +5767,10 @@ function readBoundedModaArtifact(cwd, artifactPath) {
5781
5767
  if (!absolute || !existsSync4(absolute))
5782
5768
  throw missingEvidence(`artifact:${artifactPath}`);
5783
5769
  const size = statSync2(absolute).size;
5784
- const text = readFileSync5(absolute, "utf8");
5785
5770
  return {
5786
5771
  path: artifactPath,
5787
5772
  bytes: size,
5788
- truncated: size > MAX_AGENT_EVIDENCE_BYTES,
5789
- text: text.slice(0, MAX_AGENT_EVIDENCE_BYTES)
5773
+ text: readFileSync5(absolute, "utf8")
5790
5774
  };
5791
5775
  }
5792
5776
  function safeResolve(cwd, path) {
@@ -7282,7 +7266,7 @@ function toolRows(data, scopedTool) {
7282
7266
  function firstQuote(row) {
7283
7267
  const quotes = Array.isArray(row?.user_quotes) ? row.user_quotes : [];
7284
7268
  const first = asRecord(quotes[0]);
7285
- return asString(first?.quote)?.slice(0, 160) ?? undefined;
7269
+ return asString(first?.quote) ?? undefined;
7286
7270
  }
7287
7271
  function likelyToolLocation(toolName, hints) {
7288
7272
  const normalized = normalizeKey(toolName);
@@ -7595,7 +7579,10 @@ async function finishRun(context, outcome) {
7595
7579
  context.output.close();
7596
7580
  if (wasOutputCancelled())
7597
7581
  return EXIT_CANCELLED;
7598
- return outcome.exitCode ?? EXIT_OK;
7582
+ const exitCode = outcome.exitCode ?? EXIT_OK;
7583
+ if (context.output.hasOutputError() && (exitCode === EXIT_OK || exitCode === EXIT_DEGRADED))
7584
+ return EXIT_ERROR;
7585
+ return exitCode;
7599
7586
  }
7600
7587
  if (outcome.kind === "cancelled") {
7601
7588
  context.output.writeCancelled(outcome.reason ?? "Command cancelled.");
@@ -7659,6 +7646,8 @@ function buildManifest(commands) {
7659
7646
  "MODA_AUTH_TIMEOUT_MS",
7660
7647
  "MODA_HEARTBEAT_MS",
7661
7648
  "MODA_AGENT_MAX_OUTPUT_BYTES",
7649
+ "MODA_AGENT_OUTPUT_DIR",
7650
+ "MODA_AGENT_OUTPUT_RETENTION_HOURS",
7662
7651
  "MODA_CLI_TELEMETRY",
7663
7652
  "MODA_CLI_UPDATE_CHECK",
7664
7653
  "MODA_CLI_TIPS"
@@ -7922,8 +7911,7 @@ Commands:
7922
7911
  hallucinations Grounding detections: contradicted/verified outputs with rule breakdown
7923
7912
  laziness Laziness detections per trace (repeated calls, premature handoffs, ...)
7924
7913
  tool-failures Get tool failure overview
7925
- tool-failure-detail <tool_name> Get per-tool failure detail (--limit=N examples, 1-20, default 5;
7926
- long example text is cut to 300 chars)
7914
+ tool-failure-detail <tool_name> Get per-tool failure detail (--limit=N examples, 1-20, default 5)
7927
7915
  problems Rank cross-signal Problems by root cause (what to fix first)
7928
7916
  problems remainder Signals no Problem explains yet (--family, --cursor)
7929
7917
  problem-impact Live problem/use-case impact (--node-id, --problem-id, --mode, --days-back)
@@ -9531,11 +9519,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
9531
9519
  trimmed = { ...trimmed, subtypes: ranked.slice(0, TOOL_FAILURE_AGENT_SUBTYPES), subtypes_total: subtypes.length };
9532
9520
  out.push(`Showing the ${TOOL_FAILURE_AGENT_SUBTYPES} most frequent of ${subtypes.length} failure subtypes; --json lists all of them.`);
9533
9521
  }
9534
- const { data: compacted, truncated } = compactToolFailureExamples(trimmed);
9535
- if (truncated > 0) {
9536
- out.push(`${truncated} long text field(s) in examples were cut to ${TOOL_FAILURE_TEXT_LIMIT} characters to keep this output small. For the full text, run each example's \`context_command\` (\`moda context <conversation_id> --msg-index=<msg_index>\`).`);
9537
- }
9538
- return compacted;
9522
+ return trimmed;
9539
9523
  });
9540
9524
  break;
9541
9525
  }
@@ -10163,7 +10147,7 @@ var commandRegistry = createCommandRegistry([
10163
10147
  resetApiRequestCountBeforeRun: true,
10164
10148
  telemetry: "result",
10165
10149
  handler: async (context) => {
10166
- const { runInit } = await import("./index-pdg9t77n.js");
10150
+ const { runInit } = await import("./index-z0jpjcsm.js");
10167
10151
  if (context.outputMode === "agent-stream") {
10168
10152
  context.output.writeEvent({
10169
10153
  event: "started",
@@ -10204,7 +10188,7 @@ var commandRegistry = createCommandRegistry([
10204
10188
  resetApiRequestCountBeforeRun: true,
10205
10189
  telemetry: "result-and-error",
10206
10190
  handler: async (context) => {
10207
- const { runProvision } = await import("./provision-7fhbgk2d.js");
10191
+ const { runProvision } = await import("./provision-qkazpt0c.js");
10208
10192
  return runProvision(context, {
10209
10193
  resumeCommand: buildResumeCommand("provision", context.rawFlags)
10210
10194
  });
@@ -10701,7 +10685,7 @@ var commandRegistry = createCommandRegistry([
10701
10685
  }),
10702
10686
  legacyCommand({
10703
10687
  name: "tool-failure-detail",
10704
- description: "Get per-tool failure detail (each example embeds its anchor ±2 messages, long text cut to 300 chars)",
10688
+ description: "Get per-tool failure detail (each example embeds its anchor ±2 messages)",
10705
10689
  examples: [
10706
10690
  "moda tool-failure-detail search",
10707
10691
  "moda tool-failure-detail search --compact # no embedded conversation (same as --include-window=false)",
@@ -42,8 +42,8 @@ import {
42
42
  writeHarnessAnalyzeResult,
43
43
  writeHarnessArtifacts,
44
44
  writeHumanProgress
45
- } from "./cli-m3bg9yyh.js";
46
- import"./cli-bqhs6sk7.js";
45
+ } from "./cli-x0x0n35w.js";
46
+ import"./cli-xrs02c0t.js";
47
47
  import"./cli-0v6na3yp.js";
48
48
  export {
49
49
  writeHumanProgress,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  buildSourceSnapshot
3
- } from "./cli-m3bg9yyh.js";
4
- import"./cli-bqhs6sk7.js";
3
+ } from "./cli-x0x0n35w.js";
4
+ import"./cli-xrs02c0t.js";
5
5
  import"./cli-0v6na3yp.js";
6
6
 
7
7
  // src/harness-github-actions.ts
@@ -3,7 +3,7 @@ import {
3
3
  initPrompts,
4
4
  runPromptSync,
5
5
  runSkillSync
6
- } from "./cli-6xndg2h4.js";
6
+ } from "./cli-jqfm8dp4.js";
7
7
  import {
8
8
  codingAgentDisplayName,
9
9
  describeCodingAgentEvent,
@@ -22,7 +22,7 @@ import {
22
22
  runHarnessCommand,
23
23
  startRemoteAnalyze,
24
24
  stripAnsi
25
- } from "./cli-m3bg9yyh.js";
25
+ } from "./cli-x0x0n35w.js";
26
26
  import {
27
27
  bundledSkillsDir,
28
28
  installIntegrationSkill,
@@ -34,14 +34,14 @@ import {
34
34
  } from "./cli-ssfq84k5.js";
35
35
  import {
36
36
  selectTenantAndCreateKey
37
- } from "./cli-taf854xx.js";
37
+ } from "./cli-h4k23fb0.js";
38
38
  import {
39
39
  ConnectCodeError,
40
40
  connectWithCode,
41
41
  loadAuthSession,
42
42
  loadToken,
43
43
  login
44
- } from "./cli-c64d2jrw.js";
44
+ } from "./cli-7kbzqact.js";
45
45
  import {
46
46
  CliAuthError,
47
47
  CliInputError,
@@ -49,7 +49,7 @@ import {
49
49
  createCommandContext,
50
50
  resolveIngestUrl,
51
51
  resolveModaBaseUrl
52
- } from "./cli-bqhs6sk7.js";
52
+ } from "./cli-xrs02c0t.js";
53
53
  import"./cli-0v6na3yp.js";
54
54
 
55
55
  // src/init/index.ts
@@ -5,8 +5,8 @@ import {
5
5
  inspectCodeSelectors,
6
6
  inspectPythonSelectors,
7
7
  safePromptEvidencePath
8
- } from "./cli-m3bg9yyh.js";
9
- import"./cli-bqhs6sk7.js";
8
+ } from "./cli-x0x0n35w.js";
9
+ import"./cli-xrs02c0t.js";
10
10
  import {
11
11
  __commonJS,
12
12
  __toESM
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  selectTenantAndCreateKey
3
- } from "./cli-taf854xx.js";
3
+ } from "./cli-h4k23fb0.js";
4
4
  import {
5
5
  loadAuthSession
6
- } from "./cli-c64d2jrw.js";
6
+ } from "./cli-7kbzqact.js";
7
7
  import {
8
8
  CliAuthError,
9
9
  resolveIngestUrl,
10
10
  resolveModaBaseUrl,
11
11
  stringOption
12
- } from "./cli-bqhs6sk7.js";
12
+ } from "./cli-xrs02c0t.js";
13
13
  import"./cli-0v6na3yp.js";
14
14
 
15
15
  // src/provision.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.49.0",
3
+ "version": "1.50.0",
4
4
  "description": "CLI for Moda - AI agent analytics and observability",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "moda.skill_index.v1",
3
- "bundled_at": "2026-10-10T08:40:09.772Z",
4
- "cli_version": "1.49.0",
3
+ "bundled_at": "2026-10-10T14:07:17.742Z",
4
+ "cli_version": "1.50.0",
5
5
  "skills": [
6
6
  {
7
7
  "id": "integration-cloudflare-think",
@@ -364,8 +364,8 @@ Fields an agent should read:
364
364
 
365
365
  `errors[].code` is one of `input_error`, `api_error`, `network_error`,
366
366
  `auth_required`, `input_required`, `cancelled`, `command_error`,
367
- `access_denied`, `unknown_error`, or a documented domain code such as
368
- `cloud_unavailable`. `access_denied` (exit 1) is an access problem — no
367
+ `access_denied`, `output_error`, `unknown_error`, or a documented domain
368
+ code such as `cloud_unavailable`. `access_denied` (exit 1) is an access problem — no
369
369
  organizations or an insufficient (viewer) role — distinct from `auth_required`
370
370
  (exit 4, missing/expired credentials).
371
371
 
@@ -408,14 +408,26 @@ Treat an answer as degraded if **any** of these hold:
408
408
 
409
409
  ### Large outputs and unknown flags
410
410
 
411
- - **Byte limit.** In `--agent` mode, envelopes over 200 KB are bounded
412
- *without* breaking their shape: long text fields (usually embedded
413
- transcripts) are shortened with `… [truncated N chars]`, then trailing list
414
- items are dropped. `warnings[]` and `truncation.data` say exactly what was
415
- cut (`dropped_items`, `complete: false`). `data` is still normal JSON, so
416
- `data.frustrations[0].conversation_id` keeps working. For complete results,
417
- page with a smaller `--limit` and `--offset`, or rerun with `--json` (no byte
418
- limit).
411
+ - **Byte limit.** In `--agent` mode, envelopes over 200 KB
412
+ (`MODA_AGENT_MAX_OUTPUT_BYTES`) are bounded *without* cutting any text or
413
+ breaking their shape: the complete output (the same JSON `--json` prints) is
414
+ written to a local file, and whole trailing list items are dropped from
415
+ `data`. `truncation.data.full_output_path` (also an `artifacts[]` entry of
416
+ kind `full_output`) is that file; read it in line ranges or with `jq`.
417
+ These files are pruned after 24 hours (`MODA_AGENT_OUTPUT_RETENTION_HOURS`).
418
+ `truncation.data` also says what was dropped (`dropped_items`,
419
+ `complete: false`). `data` is still normal JSON, so
420
+ `data.frustrations[0].conversation_id` keeps working. If even one item is
421
+ over budget, `data` is a stub `{ truncated, original_bytes, max_bytes,
422
+ full_output_path }`. If that file cannot be written (unwritable
423
+ `MODA_AGENT_OUTPUT_DIR`, full disk), nothing is dropped: the envelope keeps
424
+ the whole result over the limit with `status: "error"`, an `output_error`
425
+ (`cause: "full_output_save_failed"`), and exit `1`. If only the oversized
426
+ envelope text could not be saved (`cause: "envelope_text_save_failed"`),
427
+ that text stays whole, and the error message says whether `data` is whole
428
+ or bounded with its complete copy at `truncation.data.full_output_path`.
429
+ You can also page with a smaller `--limit` and
430
+ `--offset`, or rerun with `--json` (no byte limit).
419
431
  - **Unknown flags fail.** A flag the command doesn't take (a typo, or a
420
432
  filter it lacks) exits `1` with the valid flag list and a "did you mean",
421
433
  instead of being silently ignored. Stray arguments on commands that take
@@ -746,9 +758,9 @@ Pass `--include-window` to attach a `window` field per row (instead of
746
758
  (1–5, default `1`) is the half-width; `--window=1` ⇒ 3 messages total
747
759
  (anchor ± 1).
748
760
 
749
- Examples default to `--limit=5` (max 20). Strings inside `examples[]` longer
750
- than 300 characters are cut, and a warning says so; read the full text with
751
- the row's `context_command`. Agent output lists the 20 most frequent
761
+ Examples default to `--limit=5` (max 20). Example text is returned whole; if
762
+ the agent envelope goes over its byte limit, whole trailing examples are
763
+ dropped and `truncation.data.full_output_path` holds every example. Agent output lists the 20 most frequent
752
764
  `subtypes` (`subtypes_total` has the full count); `--json` lists all.
753
765
 
754
766
  ### 7. Prompt management