codehere 0.8.0 → 1.0.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.
Files changed (134) hide show
  1. package/README.md +22 -13
  2. package/dist/application/services/verdict-service.js +154 -0
  3. package/dist/application/use-cases/assess-cross-vendor.js +126 -0
  4. package/dist/application/use-cases/build-cross-vendor-verdict.js +118 -0
  5. package/dist/application/use-cases/export-aat.js +107 -0
  6. package/dist/application/use-cases/generate-acceptance-criteria.js +223 -0
  7. package/dist/application/use-cases/generate-final-report.js +140 -15
  8. package/dist/application/use-cases/generate-narration.js +5 -3
  9. package/dist/application/use-cases/generate-thread-title.js +5 -2
  10. package/dist/application/use-cases/plan-auto-review.js +57 -0
  11. package/dist/application/use-cases/reconcile-audit.js +100 -0
  12. package/dist/application/use-cases/rerun-tests.js +226 -0
  13. package/dist/application/use-cases/search-sessions.js +70 -0
  14. package/dist/application/use-cases/should-auto-review.js +71 -0
  15. package/dist/application/use-cases/thread-export.js +90 -0
  16. package/dist/application/use-cases/verify-claim.js +187 -5
  17. package/dist/domain/audit.js +11 -0
  18. package/dist/domain/services/codebase-detector.js +0 -6
  19. package/dist/domain/task.js +91 -0
  20. package/dist/error-handler.js +3 -115
  21. package/dist/index.js +269 -25
  22. package/dist/infrastructure/completion/bash-completion.js +7 -3
  23. package/dist/infrastructure/completion/fish-completion.js +7 -12
  24. package/dist/infrastructure/completion/zsh-completion.js +7 -12
  25. package/dist/infrastructure/config/config-manager.js +4 -3
  26. package/dist/infrastructure/config/product-context.js +1 -19
  27. package/dist/infrastructure/context/adapters/figma-source.js +37 -2
  28. package/dist/infrastructure/context/adapters/github-issue-source.js +3 -2
  29. package/dist/infrastructure/context/github-api.js +110 -0
  30. package/dist/infrastructure/context/github-device-flow.js +65 -0
  31. package/dist/infrastructure/context/oauth-store.js +5 -2
  32. package/dist/infrastructure/context/registry-bootstrap.js +0 -4
  33. package/dist/infrastructure/cost/budget-config.js +3 -2
  34. package/dist/infrastructure/cost/cost-router.js +16 -23
  35. package/dist/infrastructure/cost/spend-aggregator.js +90 -15
  36. package/dist/infrastructure/cost/team-ledger.js +355 -0
  37. package/dist/infrastructure/discovery/discovery-log.js +85 -0
  38. package/dist/infrastructure/hooks/audit-tip-signature.js +336 -0
  39. package/dist/infrastructure/hooks/claude-sdk-hook.js +133 -3
  40. package/dist/infrastructure/hooks/hook-mode.js +20 -4
  41. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +8 -2
  42. package/dist/infrastructure/hooks/pre-prompt-scan.js +3 -3
  43. package/dist/infrastructure/observability/execution-tracer.js +1 -1
  44. package/dist/infrastructure/orchestration/agents-config-seed.js +22 -10
  45. package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
  46. package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
  47. package/dist/infrastructure/orchestration/claude-code-adapter.js +117 -3
  48. package/dist/infrastructure/orchestration/codex-adapter.js +52 -16
  49. package/dist/infrastructure/orchestration/conductor-adapter.js +46 -7
  50. package/dist/infrastructure/orchestration/context-handoff.js +32 -14
  51. package/dist/infrastructure/orchestration/detect-ollama.js +75 -0
  52. package/dist/infrastructure/orchestration/failover-config.js +0 -10
  53. package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
  54. package/dist/infrastructure/orchestration/generic-cli-adapter.js +132 -1
  55. package/dist/infrastructure/orchestration/graphify-context.js +14 -59
  56. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +164 -9
  57. package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
  58. package/dist/infrastructure/orchestration/progress-aggregator.js +238 -21
  59. package/dist/infrastructure/orchestration/session-manager.js +593 -94
  60. package/dist/infrastructure/orchestration/session-persistence.js +177 -41
  61. package/dist/infrastructure/orchestration/thread-key-store.js +1 -1
  62. package/dist/infrastructure/orchestration/write-pass-store.js +0 -8
  63. package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
  64. package/dist/infrastructure/protocols/mcp-server-impl.js +1462 -380
  65. package/dist/infrastructure/protocols/mcp-tools.js +401 -0
  66. package/dist/infrastructure/protocols/ui/app-script-artifacts.js +121 -0
  67. package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +44 -0
  68. package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +56 -0
  69. package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +588 -0
  70. package/dist/infrastructure/protocols/ui/app-script-context-sources.js +168 -0
  71. package/dist/infrastructure/protocols/ui/app-script-conversation.js +67 -0
  72. package/dist/infrastructure/protocols/ui/app-script-drawer.js +115 -0
  73. package/dist/infrastructure/protocols/ui/app-script-failover.js +133 -0
  74. package/dist/infrastructure/protocols/ui/app-script-github.js +113 -0
  75. package/dist/infrastructure/protocols/ui/app-script-observer.js +104 -0
  76. package/dist/infrastructure/protocols/ui/app-script-onboarding.js +80 -0
  77. package/dist/infrastructure/protocols/ui/app-script-overlay.js +203 -0
  78. package/dist/infrastructure/protocols/ui/app-script-recovery.js +136 -0
  79. package/dist/infrastructure/protocols/ui/app-script-render.js +166 -0
  80. package/dist/infrastructure/protocols/ui/app-script-share.js +78 -0
  81. package/dist/infrastructure/protocols/ui/app-script-sidebar.js +194 -0
  82. package/dist/infrastructure/protocols/ui/app-script-tour.js +78 -0
  83. package/dist/infrastructure/protocols/ui/app-script-wpass.js +312 -0
  84. package/dist/infrastructure/protocols/ui/app-script.js +2998 -0
  85. package/dist/infrastructure/protocols/ui/font-faces.js +7 -0
  86. package/dist/infrastructure/protocols/ui/styles.js +2147 -0
  87. package/dist/infrastructure/protocols/ui/tokens.js +96 -0
  88. package/dist/infrastructure/protocols/ui-page-v2.js +116 -4214
  89. package/dist/infrastructure/protocols/verify-page.js +849 -155
  90. package/dist/infrastructure/security/fix-code.js +144 -0
  91. package/dist/infrastructure/security/fix-secret.js +89 -0
  92. package/dist/infrastructure/security/permissions-config.js +3 -2
  93. package/dist/infrastructure/security/scan-types.js +10 -0
  94. package/dist/infrastructure/security/ship-scan.js +195 -0
  95. package/dist/infrastructure/security/trust-scorer.js +70 -3
  96. package/dist/infrastructure/security/vibe-checks.js +99 -0
  97. package/dist/infrastructure/storage/atomic-write.js +86 -0
  98. package/dist/infrastructure/storage/project-thread-mirror.js +150 -3
  99. package/dist/infrastructure/storage/project-thread-reader.js +261 -24
  100. package/dist/monitoring.js +2 -2
  101. package/dist/presentation/cli/commands/audit-command.js +79 -4
  102. package/dist/presentation/cli/commands/budget-command.js +2 -2
  103. package/dist/presentation/cli/commands/check-command.js +96 -0
  104. package/dist/presentation/cli/commands/context-source-command.js +20 -1
  105. package/dist/presentation/cli/commands/help-command.js +5 -5
  106. package/dist/presentation/cli/commands/hook-command.js +5 -5
  107. package/dist/presentation/cli/commands/init-command.js +26 -14
  108. package/dist/presentation/cli/commands/memory-command.js +28 -28
  109. package/dist/presentation/cli/commands/permission-command.js +1 -12
  110. package/dist/presentation/cli/commands/protocols-builder.js +2 -2
  111. package/dist/presentation/cli/commands/protocols-command.js +4 -4
  112. package/dist/presentation/cli/commands/repo-map-builder.js +1 -1
  113. package/dist/presentation/cli/commands/report-command.js +48 -32
  114. package/dist/presentation/cli/commands/scan-command.js +52 -0
  115. package/dist/presentation/cli/commands/scan-tool-command.js +24 -8
  116. package/dist/presentation/cli/commands/search-command.js +51 -0
  117. package/dist/presentation/cli/commands/setup-command.js +31 -8
  118. package/dist/presentation/cli/commands/status-command.js +39 -86
  119. package/dist/presentation/cli/commands/team-command.js +256 -0
  120. package/dist/presentation/cli/commands/thread-command.js +87 -5
  121. package/dist/presentation/cli/commands/trust-command.js +53 -31
  122. package/dist/presentation/cli/commands/verify-command.js +74 -9
  123. package/dist/presentation/cli/error-display.js +0 -8
  124. package/dist/presentation/cli/repl.js +15 -7
  125. package/dist/presentation/cli/task-deliverable.js +36 -10
  126. package/dist/testing/replay-adapter.js +0 -19
  127. package/dist/ui.js +51 -239
  128. package/dist/utils/version.js +0 -6
  129. package/package.json +30 -10
  130. package/dist/domain/entities/task.js +0 -61
  131. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  132. package/dist/infrastructure/security/input-validator.js +0 -152
  133. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  134. package/dist/infrastructure/storage/task-repository.js +0 -206
@@ -17,6 +17,36 @@ function shortPath(p) {
17
17
  function truncate(s, max = MAX_LABEL) {
18
18
  return s.length > max ? s.slice(0, max - 1) + '…' : s;
19
19
  }
20
+ /** A successful tool_result sometimes extracts to no text (structured payloads),
21
+ * leaving the step row bare. For tools whose SUCCESS carries a computable claim
22
+ * (Edit succeeded = the replacement applied), derive the outcome from the input
23
+ * we captured at tool_use time. Only states what the success guarantees. */
24
+ function synthesizeOutcome(tool, input) {
25
+ if (!tool || !input)
26
+ return '';
27
+ if (tool === 'Edit' || tool === 'MultiEdit') {
28
+ // MultiEdit carries {edits:[{old_string,new_string,replace_all?}]}; Edit is flat.
29
+ const edits = Array.isArray(input.edits) ? input.edits : [input];
30
+ let d = 0;
31
+ for (const e of edits) {
32
+ // replace_all applies per-occurrence — a computed delta would understate
33
+ // the real change, so claim only what success guarantees.
34
+ if (e.replace_all)
35
+ return 'change applied';
36
+ const o = String(e.old_string ?? '');
37
+ const n = String(e.new_string ?? '');
38
+ if (!o && !n)
39
+ continue;
40
+ d += n.split('\n').length - o.split('\n').length;
41
+ }
42
+ return d === 0 ? 'change applied' : `${d > 0 ? '+' + d : d} lines · change applied`;
43
+ }
44
+ if (tool === 'Write') {
45
+ const c = String(input.content ?? '');
46
+ return c ? `${c.split('\n').length} lines written` : 'file written';
47
+ }
48
+ return '';
49
+ }
20
50
  function labelForClaudeToolUse(name, input) {
21
51
  const i = (input && typeof input === 'object' ? input : {});
22
52
  switch (name) {
@@ -99,6 +129,26 @@ function truncateOutput(s) {
99
129
  return stripped;
100
130
  return '…' + stripped.slice(-(max - 1));
101
131
  }
132
+ /** Parse a Claude AskUserQuestion tool_use input into the step.question shape.
133
+ * Defensive — SDK shape is { questions: [{ question, header?, multiSelect?,
134
+ * options: [{ label, description? }] }] }; missing pieces degrade gracefully. */
135
+ function parseAskUserQuestion(toolUseId, input) {
136
+ const raw = Array.isArray(input?.questions) ? input.questions : [];
137
+ const questions = raw.map((qq) => {
138
+ const q = (qq && typeof qq === 'object' ? qq : {});
139
+ const opts = Array.isArray(q.options) ? q.options : [];
140
+ return {
141
+ question: String(q.question ?? ''),
142
+ ...(q.header ? { header: String(q.header) } : {}),
143
+ ...(q.multiSelect ? { multiSelect: true } : {}),
144
+ options: opts.map((oo) => {
145
+ const o = (oo && typeof oo === 'object' ? oo : {});
146
+ return { label: String(o.label ?? ''), ...(o.description ? { description: String(o.description) } : {}) };
147
+ }),
148
+ };
149
+ });
150
+ return { toolUseId, questions };
151
+ }
102
152
  function unwrapShellWrapper(command) {
103
153
  // Codex wraps its shell calls in `/bin/zsh -lc "…"` or `sh -c "…"`. Strip that
104
154
  // so the step label reads as the actual command instead of the wrapper.
@@ -161,6 +211,44 @@ export class ProgressAggregator {
161
211
  return this.ingestAider(type, data);
162
212
  if (this.agentId === 'opencode')
163
213
  return this.ingestOpencode(type, data);
214
+ // Generic-config agents (cursor, etc.): the generic-cli-adapter normalizes
215
+ // their stream-json to flat tool_use + stdout events. Build steps from those
216
+ // so "watch it work" isn't empty (real dogfood 2026-06-23: cursor runs edited
217
+ // files + verified green but rendered 0 steps).
218
+ return this.ingestGeneric(type, data);
219
+ }
220
+ ingestGeneric(type, data) {
221
+ if (type === 'tool_use') {
222
+ const name = String(data.name ?? data.tool_name ?? 'tool');
223
+ const input = (data.input ?? data.parameters ?? {});
224
+ const arg = (typeof input.path === 'string' && input.path) ||
225
+ (typeof input.file_path === 'string' && input.file_path) ||
226
+ (typeof input.command === 'string' && input.command) ||
227
+ (typeof input.query === 'string' && input.query) ||
228
+ '';
229
+ const short = arg ? arg.split('/').pop() || arg : '';
230
+ const verb = name ? name.charAt(0).toUpperCase() + name.slice(1) : 'Tool';
231
+ this._steps.push({
232
+ label: truncate(short ? `${verb} ${short}` : verb),
233
+ state: 'done',
234
+ startedAt: Date.now(),
235
+ completedAt: Date.now(),
236
+ });
237
+ return true;
238
+ }
239
+ if (type === 'stdout') {
240
+ const line = String(data.line ?? '').trim();
241
+ if (!line)
242
+ return false;
243
+ this._steps.push({
244
+ kind: 'say',
245
+ label: truncate(line),
246
+ state: 'done',
247
+ startedAt: Date.now(),
248
+ completedAt: Date.now(),
249
+ });
250
+ return true;
251
+ }
164
252
  return false;
165
253
  }
166
254
  // OpenCode · `run --format json` emits tool_use with shape
@@ -206,8 +294,10 @@ export class ProgressAggregator {
206
294
  }
207
295
  /** Flush: mark every still-active step as failed (called on abnormal exit). */
208
296
  finalize(status) {
209
- // Surface any trailing aider reasoning that had no following action.
297
+ // Surface any trailing reasoning that had no following action.
210
298
  let changed = this.flushAiderSay();
299
+ if (this.flushGeminiSay())
300
+ changed = true;
211
301
  for (const s of this._steps) {
212
302
  if (s.state === 'active' || s.state === 'pending') {
213
303
  s.state = status === 'completed' ? 'done' : 'failed';
@@ -221,6 +311,43 @@ export class ProgressAggregator {
221
311
  const idx = this._steps.push({ label, state, startedAt: Date.now() }) - 1;
222
312
  return idx;
223
313
  }
314
+ /**
315
+ * Git-diff fallback (Phase 1 — aider parity). Aider has no structured event
316
+ * stream; its file-edit steps are parsed from stdout lines ("Applied edit to
317
+ * X" / "Creating empty file X") matched against aider's `base_coder.py`
318
+ * wording. A version bump that re-words those lines would silently yield ZERO
319
+ * file-action steps — a run that really changed files would then render an
320
+ * empty timeline and a "no files changed" report (the exact "did nothing"
321
+ * lie user-#1 hit on a real aider run).
322
+ *
323
+ * This is the format-independent safety net. Given the git-diff file list the
324
+ * artifacts-extractor already computed (which the adapter passes on exit), if
325
+ * NO file-action step exists, synthesize one per changed file from the diff —
326
+ * git is more authoritative than stdout parsing, not less, so these aren't
327
+ * guesses. No-op when the stdout parse already produced file-action steps (so
328
+ * we never double-count). Returns true iff a synthetic step was added.
329
+ */
330
+ applyFileFallback(files) {
331
+ if (!files || files.length === 0)
332
+ return false;
333
+ const hasFileAction = this._steps.some((s) => /^(Edit|Write|Delete) /.test(s.label));
334
+ if (hasFileAction)
335
+ return false;
336
+ let added = false;
337
+ for (const f of files) {
338
+ const path = String(f?.path ?? '').trim();
339
+ if (!path)
340
+ continue;
341
+ this._steps.push({
342
+ label: 'Edit ' + shortPath(path),
343
+ state: 'done',
344
+ startedAt: Date.now(),
345
+ completedAt: Date.now(),
346
+ });
347
+ added = true;
348
+ }
349
+ return added;
350
+ }
224
351
  /** The agent's own narration as a kind:'say' step — the "watch it work"
225
352
  * signal AND what the Final Report engine reuses as the summary. Cross-
226
353
  * vendor by decision of the v1 spec (the room thread is "tool steps woven
@@ -232,7 +359,7 @@ export class ProgressAggregator {
232
359
  return false;
233
360
  this._steps.push({
234
361
  kind: 'say',
235
- label: said.length > 500 ? said.slice(0, 500) + '\\u2026' : said,
362
+ label: said.length > 20000 ? said.slice(0, 20000) + '…' : said,
236
363
  state: 'done',
237
364
  startedAt: Date.now(),
238
365
  completedAt: Date.now(),
@@ -254,19 +381,56 @@ export class ProgressAggregator {
254
381
  const said = b.text.trim();
255
382
  this._steps.push({
256
383
  kind: 'say',
257
- label: said.length > 500 ? said.slice(0, 500) + '\\u2026' : said,
384
+ label: said.length > 20000 ? said.slice(0, 20000) + '…' : said,
258
385
  state: 'done',
259
386
  startedAt: Date.now(),
260
387
  completedAt: Date.now(),
261
388
  });
262
389
  changed = true;
263
390
  }
391
+ else if (b.type === 'thinking') {
392
+ // Claude's interleaved reasoning block — was 100% dropped. Surface it
393
+ // as a muted 'think' step so the user can watch the agent reason.
394
+ const think = typeof b.thinking === 'string' ? b.thinking.trim() : '';
395
+ if (think) {
396
+ this._steps.push({
397
+ kind: 'think',
398
+ label: think.length > 20000 ? think.slice(0, 20000) + '…' : think,
399
+ state: 'done',
400
+ startedAt: Date.now(),
401
+ completedAt: Date.now(),
402
+ });
403
+ changed = true;
404
+ }
405
+ }
264
406
  else if (b.type === 'tool_use') {
265
407
  const id = String(b.id ?? '');
266
- const label = labelForClaudeToolUse(String(b.name ?? ''), b.input);
267
- const idx = this.addStep(label, 'active');
268
- if (id)
269
- this.toolUseIndex.set(id, idx);
408
+ const name = String(b.name ?? '');
409
+ const input = b.input && typeof b.input === 'object' ? b.input : undefined;
410
+ if (name === 'AskUserQuestion') {
411
+ // The agent is asking the user. Capture the questions+options (was
412
+ // discarded) and mark it 'ask' so the UI renders a question card —
413
+ // not a generic failed tool step.
414
+ const q = parseAskUserQuestion(id, input);
415
+ const n = q.questions.length;
416
+ const idx = this.addStep(`Asking you ${n} question${n === 1 ? '' : 's'}`, 'active');
417
+ const step = this._steps[idx];
418
+ step.kind = 'ask';
419
+ step.toolName = name;
420
+ step.question = q;
421
+ if (id)
422
+ this.toolUseIndex.set(id, idx);
423
+ }
424
+ else {
425
+ const label = labelForClaudeToolUse(name, input);
426
+ const idx = this.addStep(label, 'active');
427
+ const step = this._steps[idx];
428
+ step.toolName = name;
429
+ if (input)
430
+ step.toolInput = input;
431
+ if (id)
432
+ this.toolUseIndex.set(id, idx);
433
+ }
270
434
  changed = true;
271
435
  }
272
436
  }
@@ -284,17 +448,29 @@ export class ProgressAggregator {
284
448
  const idx = id ? this.toolUseIndex.get(id) : undefined;
285
449
  if (idx !== undefined) {
286
450
  const step = this._steps[idx];
287
- step.state = b.is_error ? 'failed' : 'done';
451
+ // An un-answered AskUserQuestion returns is_error headless, but it's
452
+ // a PENDING question, not a failure — don't flip it to 'failed'.
453
+ if (step.kind !== 'ask')
454
+ step.state = b.is_error ? 'failed' : 'done';
288
455
  step.completedAt = Date.now();
289
- // P22 / Track B #8 — capture tool-result text for the
290
- // dashboard's bash-output preview. Claude SDK shape:
291
- // content: string (most cases)
292
- // content: Array<{type:'text', text:string}> (multi-block)
293
- // Truncate to last ~400 chars so the SSE payload + render
294
- // cost stay bounded.
456
+ // Capture BOTH: fullOutput (untruncated, ANSI preserved — for the
457
+ // expandable view) and output (bounded, ANSI-stripped preview so the
458
+ // per-step SSE snapshot stays small). On failure, keep the error body.
295
459
  const out = extractClaudeToolResultText(b);
296
- if (out)
460
+ if (out) {
461
+ step.fullOutput = out;
297
462
  step.output = truncateOutput(out);
463
+ if (b.is_error)
464
+ step.errorText = out;
465
+ }
466
+ else if (!b.is_error && step.kind !== 'ask') {
467
+ // Structured results extract to '' and the row would read as a bare
468
+ // "Edit x · 0.5s". For tools whose success has a computable meaning,
469
+ // synthesize a truthful outcome from the input we already hold.
470
+ const synth = synthesizeOutcome(step.toolName, step.toolInput);
471
+ if (synth)
472
+ step.output = synth;
473
+ }
298
474
  changed = true;
299
475
  }
300
476
  }
@@ -316,6 +492,9 @@ export class ProgressAggregator {
316
492
  */
317
493
  ingestGemini(type, data) {
318
494
  if (type === 'tool_use') {
495
+ // Narration precedes the action in gemini's stream — flush any buffered
496
+ // assistant text first so the timeline reads "explains → acts".
497
+ this.flushGeminiSay();
319
498
  const id = String(data.tool_id ?? '');
320
499
  const name = String(data.tool_name ?? 'tool');
321
500
  const label = labelForClaudeToolUse(name, data.parameters);
@@ -348,14 +527,43 @@ export class ProgressAggregator {
348
527
  return true;
349
528
  }
350
529
  }
351
- // Gemini's narrative text rides `content` events — same cross-vendor say
352
- // capture as claude/aider/codex (v1 spec: narration woven into the thread,
353
- // and the Final Report engine reuses it as the summary).
530
+ // Gemini's narrative text — THE REAL STREAM SHAPE (verified against
531
+ // @google/gemini-cli StreamJsonFormatter, mirrored in gemini-adapter.ts):
532
+ // `message` events `{ role:'assistant', content, delta?: bool }`, where
533
+ // content arrives as delta CHUNKS. The old `content`-type branch below
534
+ // never matched a real run — so real gemini sessions produced ZERO say
535
+ // steps and every Final Report degraded to the deterministic "didn't
536
+ // leave a written summary" fallback (R10 dogfood gap c). Buffer deltas;
537
+ // flush as ONE say step at a message boundary (non-delta), before the
538
+ // next action (tool_use above), or at stream end (result/error/finalize).
539
+ if (type === 'message') {
540
+ if (data.role === 'assistant' && typeof data.content === 'string' && data.content) {
541
+ this.geminiSayBuffer.push(data.content);
542
+ if (data.delta !== true)
543
+ return this.flushGeminiSay();
544
+ return false; // chunk buffered; the flush is the visible step
545
+ }
546
+ return false;
547
+ }
548
+ if (type === 'result' || type === 'error') {
549
+ return this.flushGeminiSay();
550
+ }
551
+ // Legacy shape kept for old cassettes: `content` events with a text field.
354
552
  if (type === 'content' && typeof data.text === 'string') {
355
553
  return this.pushSay(data.text);
356
554
  }
357
555
  return false;
358
556
  }
557
+ /** Buffered gemini assistant delta chunks awaiting a boundary flush. */
558
+ geminiSayBuffer = [];
559
+ /** Flush buffered gemini narration as one kind:'say' step. */
560
+ flushGeminiSay() {
561
+ if (this.geminiSayBuffer.length === 0)
562
+ return false;
563
+ const text = this.geminiSayBuffer.join('');
564
+ this.geminiSayBuffer = [];
565
+ return this.pushSay(text);
566
+ }
359
567
  /**
360
568
  * Aider observability parity (v0.5 Path B per docs/design/f3_memory_spec.md
361
569
  * + ~/.claude/.../memory/learning_aider.md).
@@ -439,7 +647,12 @@ export class ProgressAggregator {
439
647
  * "must contain a space + length >= 4" guard in collectAiderSay already
440
648
  * drops bare filenames, fence langs and spinner rules; this catches the
441
649
  * noisy lines that DO contain spaces. */
442
- static AIDER_NONPROSE_RE = /^(=======\s*$|Tokens:|Cost:|Commit [0-9a-f]|Applied edit to |Running |Creating empty file |Did not |Added \S+ to the chat|Removed \S+ from the chat|No changes made|Warning: |Scanning repo|Initial repo scan|Repo-map:|Main model:|Weak model:|Model:|Git repo:|Use \/|aider |Committing )/;
650
+ static AIDER_NONPROSE_RE = /^(=======\s*$|Tokens:|Cost:|Commit [0-9a-f]|Applied edit to |Running |Creating empty file |Did not |Added \S+ to the chat|Removed \S+ from the chat|No changes made|Warning: |Scanning repo|Initial repo scan|Repo-map:|Repo-map can|Main model:|Weak model:|Model:|Git repo:|Cur working dir|Git working dir|Use \/|aider |Aider v|Committing )/;
651
+ /** Aider CLI startup/warning boilerplate that arrives as prose-shaped lines
652
+ * (often prefixed "Note: ..."), so it slips past the line-start regex above.
653
+ * Matched ANYWHERE in the line by its distinctive phrases — this is the noise
654
+ * that leaked into "the agent's words" and made the room read like stderr. */
655
+ static AIDER_BOILERPLATE_RE = /in-chat filenames|working dir|repo-map can'?t|deleted from the file system|but not from git\?/i;
443
656
  aiderFlushOutput() {
444
657
  if (this.aiderRunningStepIdx === null)
445
658
  return;
@@ -466,7 +679,7 @@ export class ProgressAggregator {
466
679
  return false;
467
680
  this._steps.push({
468
681
  kind: 'say',
469
- label: said.length > 500 ? said.slice(0, 500) + '…' : said,
682
+ label: said.length > 20000 ? said.slice(0, 20000) + '…' : said,
470
683
  state: 'done',
471
684
  startedAt: Date.now(),
472
685
  completedAt: Date.now(),
@@ -494,6 +707,8 @@ export class ProgressAggregator {
494
707
  return;
495
708
  if (ProgressAggregator.AIDER_NONPROSE_RE.test(line))
496
709
  return;
710
+ if (ProgressAggregator.AIDER_BOILERPLATE_RE.test(line))
711
+ return; // CLI startup noise, anywhere in the line
497
712
  // Require a space + minimum length so filenames/code tokens don't leak in.
498
713
  if (line.length < 4 || !line.includes(' '))
499
714
  return;
@@ -665,7 +880,9 @@ export class ProgressAggregator {
665
880
  return true;
666
881
  }
667
882
  if (itemType === 'web_search') {
668
- const q = String(item.query ?? '').trim().slice(0, 80);
883
+ const q = String(item.query ?? '')
884
+ .trim()
885
+ .slice(0, 80);
669
886
  this._steps.push({
670
887
  label: q ? 'Web search · ' + q : 'Web search',
671
888
  state: 'done',