karajan-code 1.21.0 → 1.21.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.21.0",
3
+ "version": "1.21.2",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -3,56 +3,53 @@ import { assertAgentsAvailable } from "../agents/availability.js";
3
3
  import { resolveRole } from "../config.js";
4
4
  import { buildArchitectPrompt, parseArchitectOutput } from "../prompts/architect.js";
5
5
 
6
- function formatArchitect(result) {
7
- const lines = [];
8
- lines.push(`## Architecture Design`);
9
- lines.push(`**Verdict:** ${result.verdict || "unknown"}`, "");
6
+ function formatLayers(layers, lines) {
7
+ lines.push("### Layers");
8
+ for (const l of layers) {
9
+ lines.push(typeof l === "string" ? `- ${l}` : `- **${l.name}**: ${l.responsibility || ""}`);
10
+ }
11
+ lines.push("");
12
+ }
10
13
 
11
- const arch = result.architecture;
12
- if (arch) {
13
- if (arch.type) lines.push(`**Type:** ${arch.type}`, "");
14
+ function formatTradeoffs(tradeoffs, lines) {
15
+ lines.push("### Tradeoffs");
16
+ for (const t of tradeoffs) {
17
+ lines.push(`- **${t.decision}**: ${t.rationale || ""}`);
18
+ if (t.alternatives?.length) lines.push(` Alternatives: ${t.alternatives.join(", ")}`);
19
+ }
20
+ lines.push("");
21
+ }
14
22
 
15
- if (arch.layers?.length) {
16
- lines.push("### Layers");
17
- for (const l of arch.layers) {
18
- if (typeof l === "string") {
19
- lines.push(`- ${l}`);
20
- } else {
21
- lines.push(`- **${l.name}**: ${l.responsibility || ""}`);
22
- }
23
- }
24
- lines.push("");
25
- }
23
+ function formatApiContracts(contracts, lines) {
24
+ lines.push("### API Contracts");
25
+ for (const c of contracts) {
26
+ lines.push(`- \`${c.method || "GET"} ${c.endpoint}\``);
27
+ }
28
+ lines.push("");
29
+ }
26
30
 
27
- if (arch.patterns?.length) {
28
- lines.push("### Patterns");
29
- for (const p of arch.patterns) lines.push(`- ${p}`);
30
- lines.push("");
31
- }
31
+ function formatArchitecture(arch, lines) {
32
+ if (arch.type) lines.push(`**Type:** ${arch.type}`, "");
33
+ if (arch.layers?.length) formatLayers(arch.layers, lines);
34
+ if (arch.patterns?.length) {
35
+ lines.push("### Patterns");
36
+ for (const p of arch.patterns) lines.push(`- ${p}`);
37
+ lines.push("");
38
+ }
39
+ if (arch.tradeoffs?.length) formatTradeoffs(arch.tradeoffs, lines);
40
+ if (arch.apiContracts?.length) formatApiContracts(arch.apiContracts, lines);
41
+ }
32
42
 
33
- if (arch.tradeoffs?.length) {
34
- lines.push("### Tradeoffs");
35
- for (const t of arch.tradeoffs) {
36
- lines.push(`- **${t.decision}**: ${t.rationale || ""}`);
37
- if (t.alternatives?.length) lines.push(` Alternatives: ${t.alternatives.join(", ")}`);
38
- }
39
- lines.push("");
40
- }
43
+ function formatArchitect(result) {
44
+ const lines = [];
45
+ lines.push(`## Architecture Design`);
46
+ lines.push(`**Verdict:** ${result.verdict || "unknown"}`, "");
41
47
 
42
- if (arch.apiContracts?.length) {
43
- lines.push("### API Contracts");
44
- for (const c of arch.apiContracts) {
45
- lines.push(`- \`${c.method || "GET"} ${c.endpoint}\``);
46
- }
47
- lines.push("");
48
- }
49
- }
48
+ if (result.architecture) formatArchitecture(result.architecture, lines);
50
49
 
51
50
  if (result.questions?.length) {
52
51
  lines.push("### Clarification Questions");
53
- for (const q of result.questions) {
54
- lines.push(`- ${q.question || q}`);
55
- }
52
+ for (const q of result.questions) lines.push(`- ${q.question || q}`);
56
53
  lines.push("");
57
54
  }
58
55
 
@@ -2,57 +2,61 @@ import { createAgent } from "../agents/index.js";
2
2
  import { assertAgentsAvailable } from "../agents/availability.js";
3
3
  import { resolveRole } from "../config.js";
4
4
  import { buildDiscoverPrompt, parseDiscoverOutput } from "../prompts/discover.js";
5
- import { parseMaybeJsonString } from "../review/parser.js";
6
5
 
7
- function formatDiscover(result, mode) {
8
- const lines = [];
9
- lines.push(`## Discovery (${mode})`);
10
- lines.push(`**Verdict:** ${result.verdict || "unknown"}`, "");
11
-
12
- if (result.gaps?.length) {
13
- lines.push("## Gaps");
14
- for (const g of result.gaps) {
15
- const sev = g.severity ? ` [${g.severity}]` : "";
16
- lines.push(`- ${g.description || g}${sev}`);
17
- if (g.suggestedQuestion) lines.push(` → ${g.suggestedQuestion}`);
18
- }
19
- lines.push("");
6
+ function formatGaps(gaps, lines) {
7
+ lines.push("## Gaps");
8
+ for (const g of gaps) {
9
+ const sev = g.severity ? ` [${g.severity}]` : "";
10
+ lines.push(`- ${g.description || g}${sev}`);
11
+ if (g.suggestedQuestion) lines.push(` → ${g.suggestedQuestion}`);
20
12
  }
13
+ lines.push("");
14
+ }
21
15
 
22
- if (result.momTestQuestions?.length) {
23
- lines.push("## Mom Test Questions");
24
- for (const q of result.momTestQuestions) {
25
- lines.push(`- ${q.question || q}`);
26
- if (q.rationale) lines.push(` _${q.rationale}_`);
27
- }
28
- lines.push("");
16
+ function formatMomTest(questions, lines) {
17
+ lines.push("## Mom Test Questions");
18
+ for (const q of questions) {
19
+ lines.push(`- ${q.question || q}`);
20
+ if (q.rationale) lines.push(` _${q.rationale}_`);
29
21
  }
22
+ lines.push("");
23
+ }
30
24
 
31
- if (result.wendelChecklist?.length) {
32
- lines.push("## Wendel Checklist");
33
- for (const w of result.wendelChecklist) {
34
- const icon = w.status === "pass" ? "✓" : w.status === "fail" ? "✗" : "?";
35
- lines.push(`- [${icon}] ${w.condition}: ${w.justification || ""}`);
36
- }
37
- lines.push("");
25
+ function formatWendel(checklist, lines) {
26
+ lines.push("## Wendel Checklist");
27
+ for (const w of checklist) {
28
+ const icon = w.status === "pass" ? "✓" : w.status === "fail" ? "✗" : "?";
29
+ lines.push(`- [${icon}] ${w.condition}: ${w.justification || ""}`);
38
30
  }
31
+ lines.push("");
32
+ }
39
33
 
40
- if (result.classification) {
41
- lines.push("## Classification");
42
- lines.push(`- Type: ${result.classification.type}`);
43
- if (result.classification.adoptionRisk) lines.push(`- Adoption risk: ${result.classification.adoptionRisk}`);
44
- if (result.classification.frictionEstimate) lines.push(`- Friction: ${result.classification.frictionEstimate}`);
45
- lines.push("");
46
- }
34
+ function formatClassification(classification, lines) {
35
+ lines.push("## Classification");
36
+ lines.push(`- Type: ${classification.type}`);
37
+ if (classification.adoptionRisk) lines.push(`- Adoption risk: ${classification.adoptionRisk}`);
38
+ if (classification.frictionEstimate) lines.push(`- Friction: ${classification.frictionEstimate}`);
39
+ lines.push("");
40
+ }
47
41
 
48
- if (result.jtbds?.length) {
49
- lines.push("## Jobs-to-be-Done");
50
- for (const j of result.jtbds) {
51
- lines.push(`- **${j.id || ""}**: ${j.functional || j}`);
52
- }
53
- lines.push("");
42
+ function formatJtbds(jtbds, lines) {
43
+ lines.push("## Jobs-to-be-Done");
44
+ for (const j of jtbds) {
45
+ lines.push(`- **${j.id || ""}**: ${j.functional || j}`);
54
46
  }
47
+ lines.push("");
48
+ }
49
+
50
+ function formatDiscover(result, mode) {
51
+ const lines = [];
52
+ lines.push(`## Discovery (${mode})`);
53
+ lines.push(`**Verdict:** ${result.verdict || "unknown"}`, "");
55
54
 
55
+ if (result.gaps?.length) formatGaps(result.gaps, lines);
56
+ if (result.momTestQuestions?.length) formatMomTest(result.momTestQuestions, lines);
57
+ if (result.wendelChecklist?.length) formatWendel(result.wendelChecklist, lines);
58
+ if (result.classification) formatClassification(result.classification, lines);
59
+ if (result.jtbds?.length) formatJtbds(result.jtbds, lines);
56
60
  if (result.summary) lines.push(`---\n${result.summary}`);
57
61
  return lines.join("\n");
58
62
  }
package/src/config.js CHANGED
@@ -141,6 +141,7 @@ const DEFAULTS = {
141
141
  max_reviewer_retries: 3,
142
142
  max_tester_retries: 1,
143
143
  max_security_retries: 1,
144
+ max_auto_resumes: 2,
144
145
  expiry_days: 30
145
146
  },
146
147
  failFast: {
@@ -171,6 +171,69 @@ export function buildAskQuestion(server) {
171
171
  };
172
172
  }
173
173
 
174
+ const MAX_AUTO_RESUMES = 2;
175
+ const NON_RECOVERABLE_CATEGORIES = new Set([
176
+ "config_error", "auth_error", "agent_missing", "branch_error", "git_error"
177
+ ]);
178
+
179
+ async function attemptAutoResume({ err, config, logger, emitter, askQuestion, runLog }) {
180
+ const { category } = classifyError(err);
181
+ if (NON_RECOVERABLE_CATEGORIES.has(category)) return null;
182
+
183
+ // Find session ID from most recent session file
184
+ const { loadMostRecentSession } = await import("../session-store.js");
185
+ let session;
186
+ try {
187
+ session = await loadMostRecentSession();
188
+ } catch {
189
+ return null;
190
+ }
191
+ if (!session || !["failed", "stopped"].includes(session.status)) return null;
192
+
193
+ const maxRetries = config.session?.max_auto_resumes ?? MAX_AUTO_RESUMES;
194
+ const autoResumeCount = session.auto_resume_count || 0;
195
+ if (autoResumeCount >= maxRetries) {
196
+ runLog.logText(`[resilient] auto-resume limit reached (${maxRetries}), giving up`);
197
+ return null;
198
+ }
199
+
200
+ runLog.logText(`[resilient] run failed (${category}), auto-resuming (${autoResumeCount + 1}/${maxRetries})...`);
201
+ emitter.emit("progress", {
202
+ type: "resilient:auto_resume",
203
+ attempt: autoResumeCount + 1,
204
+ maxRetries,
205
+ errorCategory: category,
206
+ sessionId: session.id
207
+ });
208
+
209
+ // Increment counter and save before resuming
210
+ const { saveSession } = await import("../session-store.js");
211
+ session.auto_resume_count = autoResumeCount + 1;
212
+ await saveSession(session);
213
+
214
+ try {
215
+ const result = await resumeFlow({
216
+ sessionId: session.id,
217
+ config,
218
+ logger,
219
+ flags: {},
220
+ emitter,
221
+ askQuestion
222
+ });
223
+ const ok = !result.paused && (result.approved !== false);
224
+ runLog.logText(`[resilient] auto-resume ${ok ? "succeeded" : "finished"} — ok=${ok}`);
225
+ return { ok, ...result, autoResumed: true, autoResumeAttempt: autoResumeCount + 1 };
226
+ } catch (error) {
227
+ // Recursive: try again if still within limits
228
+ const nestedResult = await attemptAutoResume({
229
+ err: error, config, logger, emitter, askQuestion, runLog
230
+ });
231
+ if (nestedResult) return nestedResult;
232
+ runLog.logText(`[resilient] auto-resume failed: ${error.message}`);
233
+ return null;
234
+ }
235
+ }
236
+
174
237
  export async function handleRunDirect(a, server, extra) {
175
238
  const config = await buildConfig(a);
176
239
  await assertNotOnBaseBranch(config);
@@ -209,6 +272,12 @@ export async function handleRunDirect(a, server, extra) {
209
272
  const result = await runFlow({ task: a.task, config, logger, flags: a, emitter, askQuestion, pgTaskId, pgProject });
210
273
  runLog.logText(`[kj_run] finished — ok=${!result.paused && (result.approved !== false)}`);
211
274
  return { ok: !result.paused && (result.approved !== false), ...result };
275
+ } catch (err) {
276
+ const autoResumeResult = await attemptAutoResume({
277
+ err, config, logger, emitter, askQuestion, runLog, progressNotifier, extra
278
+ });
279
+ if (autoResumeResult) return autoResumeResult;
280
+ throw err;
212
281
  } finally {
213
282
  runLog.close();
214
283
  }
@@ -256,8 +256,14 @@ async function handleTddFailure({ tddEval, config, logger, emitter, eventBase, s
256
256
 
257
257
  export async function runTddCheckStage({ config, logger, emitter, eventBase, session, trackBudget, iteration, askQuestion }) {
258
258
  logger.setContext({ iteration, stage: "tdd" });
259
- const tddDiff = await generateDiff({ baseRef: session.session_start_sha });
260
- const untrackedFiles = await getUntrackedFiles();
259
+ let tddDiff, untrackedFiles;
260
+ try {
261
+ tddDiff = await generateDiff({ baseRef: session.session_start_sha });
262
+ untrackedFiles = await getUntrackedFiles();
263
+ } catch (err) {
264
+ logger.warn(`TDD diff generation failed: ${err.message}`);
265
+ return { action: "continue", stageResult: { ok: false, summary: `TDD check failed: ${err.message}` } };
266
+ }
261
267
  const tddEval = evaluateTddPolicy(tddDiff, config.development, untrackedFiles);
262
268
  await addCheckpoint(session, {
263
269
  stage: "tdd-policy",
@@ -366,7 +372,13 @@ export async function runSonarStage({ config, logger, emitter, eventBase, sessio
366
372
  const sonarRole = new SonarRole({ config, logger, emitter });
367
373
  await sonarRole.init({ iteration });
368
374
  const sonarStart = Date.now();
369
- const sonarOutput = await sonarRole.run();
375
+ let sonarOutput;
376
+ try {
377
+ sonarOutput = await sonarRole.run();
378
+ } catch (err) {
379
+ logger.warn(`Sonar threw: ${err.message}`);
380
+ sonarOutput = { ok: false, result: { error: err.message }, summary: `Sonar error: ${err.message}` };
381
+ }
370
382
  trackBudget({ role: "sonar", provider: "sonar", result: sonarOutput, duration_ms: Date.now() - sonarStart });
371
383
  const sonarResult = sonarOutput.result;
372
384
 
@@ -438,7 +450,13 @@ export async function runSonarCloudStage({ config, logger, emitter, eventBase, s
438
450
 
439
451
  const { runSonarCloudScan } = await import("../sonar/cloud-scanner.js");
440
452
  const scanStart = Date.now();
441
- const result = await runSonarCloudScan(config);
453
+ let result;
454
+ try {
455
+ result = await runSonarCloudScan(config);
456
+ } catch (err) {
457
+ logger.warn(`SonarCloud threw: ${err.message}`);
458
+ result = { ok: false, error: err.message };
459
+ }
442
460
  trackBudget({ role: "sonarcloud", provider: "sonarcloud", result: { ok: result.ok }, duration_ms: Date.now() - scanStart });
443
461
 
444
462
  await addCheckpoint(session, {
@@ -550,7 +568,13 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
550
568
  })
551
569
  );
552
570
 
553
- const diff = await fetchReviewDiff(session, logger);
571
+ let diff;
572
+ try {
573
+ diff = await fetchReviewDiff(session, logger);
574
+ } catch (err) {
575
+ logger.warn(`Review diff generation failed: ${err.message}`);
576
+ return { approved: false, blocking_issues: [{ description: `Diff generation failed: ${err.message}` }], non_blocking_suggestions: [], summary: `Reviewer failed: cannot generate diff — ${err.message}`, confidence: 0 };
577
+ }
554
578
  const reviewerOnOutput = ({ stream, line }) => {
555
579
  emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "reviewer" }, {
556
580
  message: line,
@@ -575,6 +599,9 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
575
599
  trackBudget({ role: "reviewer", provider: reviewer, model: reviewerRole.model, result, duration_ms: Date.now() - reviewerStart });
576
600
  }
577
601
  });
602
+ } catch (err) {
603
+ logger.warn(`Reviewer threw: ${err.message}`);
604
+ reviewerExec = { execResult: { ok: false, error: err.message }, attempts: [{ reviewer: reviewerRole.provider, result: { ok: false, error: err.message } }] };
578
605
  } finally {
579
606
  reviewerStall.stop();
580
607
  }
@@ -16,7 +16,13 @@ export async function runTesterStage({ config, logger, emitter, eventBase, sessi
16
16
  const tester = new TesterRole({ config, logger, emitter });
17
17
  await tester.init({ task, iteration });
18
18
  const testerStart = Date.now();
19
- const testerOutput = await tester.run({ task, diff });
19
+ let testerOutput;
20
+ try {
21
+ testerOutput = await tester.run({ task, diff });
22
+ } catch (err) {
23
+ logger.warn(`Tester threw: ${err.message}`);
24
+ testerOutput = { ok: false, summary: `Tester error: ${err.message}`, result: { error: err.message } };
25
+ }
20
26
  trackBudget({
21
27
  role: "tester",
22
28
  provider: config?.roles?.tester?.provider || coderRole.provider,
@@ -90,7 +96,13 @@ export async function runSecurityStage({ config, logger, emitter, eventBase, ses
90
96
  const security = new SecurityRole({ config, logger, emitter });
91
97
  await security.init({ task, iteration });
92
98
  const securityStart = Date.now();
93
- const securityOutput = await security.run({ task, diff });
99
+ let securityOutput;
100
+ try {
101
+ securityOutput = await security.run({ task, diff });
102
+ } catch (err) {
103
+ logger.warn(`Security threw: ${err.message}`);
104
+ securityOutput = { ok: false, summary: `Security error: ${err.message}`, result: { error: err.message } };
105
+ }
94
106
  trackBudget({
95
107
  role: "security",
96
108
  provider: config?.roles?.security?.provider || coderRole.provider,
@@ -69,6 +69,9 @@ export async function runTriageStage({ config, logger, emitter, eventBase, sessi
69
69
  let triageOutput;
70
70
  try {
71
71
  triageOutput = await triage.run({ task: session.task, onOutput: triageStall.onOutput });
72
+ } catch (err) {
73
+ logger.warn(`Triage threw: ${err.message}`);
74
+ triageOutput = { ok: false, summary: `Triage error: ${err.message}`, result: { error: err.message } };
72
75
  } finally {
73
76
  triageStall.stop();
74
77
  }
@@ -159,6 +162,9 @@ export async function runResearcherStage({ config, logger, emitter, eventBase, s
159
162
  let researchOutput;
160
163
  try {
161
164
  researchOutput = await researcher.run({ task: session.task, onOutput: researcherStall.onOutput });
165
+ } catch (err) {
166
+ logger.warn(`Researcher threw: ${err.message}`);
167
+ researchOutput = { ok: false, summary: `Researcher error: ${err.message}`, result: { error: err.message } };
162
168
  } finally {
163
169
  researcherStall.stop();
164
170
  }
@@ -281,6 +287,9 @@ export async function runArchitectStage({ config, logger, emitter, eventBase, se
281
287
  discoverResult,
282
288
  triageLevel
283
289
  });
290
+ } catch (err) {
291
+ logger.warn(`Architect threw: ${err.message}`);
292
+ architectOutput = { ok: false, summary: `Architect error: ${err.message}`, result: { error: err.message } };
284
293
  } finally {
285
294
  architectStall.stop();
286
295
  }
@@ -380,6 +389,9 @@ export async function runPlannerStage({ config, logger, emitter, eventBase, sess
380
389
  let planResult;
381
390
  try {
382
391
  planResult = await planRole.execute({ task, onOutput: plannerStall.onOutput });
392
+ } catch (err) {
393
+ logger.warn(`Planner threw: ${err.message}`);
394
+ planResult = { ok: false, result: { error: err.message }, summary: `Planner error: ${err.message}` };
383
395
  } finally {
384
396
  plannerStall.stop();
385
397
  }
@@ -453,6 +465,9 @@ export async function runDiscoverStage({ config, logger, emitter, eventBase, ses
453
465
  let discoverOutput;
454
466
  try {
455
467
  discoverOutput = await discover.run({ task: session.task, mode, onOutput: discoverStall.onOutput });
468
+ } catch (err) {
469
+ logger.warn(`Discover threw: ${err.message}`);
470
+ discoverOutput = { ok: false, summary: `Discover error: ${err.message}`, result: { error: err.message } };
456
471
  } finally {
457
472
  discoverStall.stop();
458
473
  }
@@ -16,7 +16,13 @@ export async function runReviewerWithFallback({ reviewerName, config, logger, em
16
16
  const role = new ReviewerRole({ config: reviewerConfig, logger, emitter, createAgentFn: createAgent });
17
17
  await role.init();
18
18
  for (let attempt = 1; attempt <= retries + 1; attempt += 1) {
19
- const execResult = await role.execute(reviewInput);
19
+ let execResult;
20
+ try {
21
+ execResult = await role.execute(reviewInput);
22
+ } catch (err) {
23
+ logger.warn(`Reviewer ${name} attempt ${attempt} threw: ${err.message}`);
24
+ execResult = { ok: false, result: { error: err.message }, summary: `Reviewer error: ${err.message}` };
25
+ }
20
26
  if (onAttemptResult) {
21
27
  await onAttemptResult({ reviewer: name, result: execResult.result });
22
28
  }
@@ -19,7 +19,16 @@ export async function invokeSolomon({ config, logger, emitter, eventBase, stage,
19
19
 
20
20
  const solomon = new SolomonRole({ config, logger, emitter });
21
21
  await solomon.init({ task: conflict.task || session.task, iteration });
22
- const ruling = await solomon.run({ conflict });
22
+ let ruling;
23
+ try {
24
+ ruling = await solomon.run({ conflict });
25
+ } catch (err) {
26
+ logger.warn(`Solomon threw: ${err.message}`);
27
+ return escalateToHuman({
28
+ askQuestion, session, emitter, eventBase, stage, iteration,
29
+ conflict: { ...conflict, solomonReason: `Solomon error: ${err.message}` }
30
+ });
31
+ }
23
32
 
24
33
  emitProgress(
25
34
  emitter,
@@ -63,6 +63,24 @@ export async function pauseSession(session, { question, context: pauseContext })
63
63
  await saveSession(session);
64
64
  }
65
65
 
66
+ export async function loadMostRecentSession() {
67
+ let entries;
68
+ try {
69
+ entries = await fs.readdir(SESSION_ROOT, { withFileTypes: true });
70
+ } catch {
71
+ return null;
72
+ }
73
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
74
+ for (let i = dirs.length - 1; i >= 0; i--) {
75
+ try {
76
+ return await loadSession(dirs[i]);
77
+ } catch {
78
+ continue;
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+
66
84
  export async function resumeSessionWithAnswer(sessionId, answer) {
67
85
  const session = await loadSession(sessionId);
68
86
  if (session.status !== "paused") {
@@ -183,13 +183,15 @@ export function readRunLog(projectDir, maxLines = 50) {
183
183
  const total = lines.length;
184
184
  const shown = lines.slice(-maxLines);
185
185
  const status = parseRunStatus(lines);
186
+ const MAX_LINE_CHARS = 2000;
187
+ const truncated = shown.map(l => l.length > MAX_LINE_CHARS ? l.slice(0, MAX_LINE_CHARS) + "… [truncated]" : l);
186
188
  return {
187
189
  ok: true,
188
190
  path: logPath,
189
191
  totalLines: total,
190
192
  status,
191
- lines: shown,
192
- summary: shown.join("\n")
193
+ lines: truncated,
194
+ summary: truncated.join("\n")
193
195
  };
194
196
  } catch (err) {
195
197
  return {