codehere 1.0.1 → 1.0.3

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 (163) hide show
  1. package/README.md +35 -193
  2. package/dist/application/use-cases/check-claimed-tests.js +81 -0
  3. package/dist/application/use-cases/detect-test-weakening.js +237 -0
  4. package/dist/application/use-cases/rerun-tests.js +110 -7
  5. package/dist/index.js +38 -1212
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -323
  7. package/dist/presentation/cli/commands/check-command.js +178 -23
  8. package/dist/presentation/cli/commands/help-command.js +21 -285
  9. package/dist/ui.js +5 -236
  10. package/dist/utils/version.js +0 -6
  11. package/package.json +9 -38
  12. package/dist/application/services/verdict-service.js +0 -154
  13. package/dist/application/use-cases/assess-cross-vendor.js +0 -126
  14. package/dist/application/use-cases/build-cross-vendor-verdict.js +0 -118
  15. package/dist/application/use-cases/domain-trust.js +0 -110
  16. package/dist/application/use-cases/export-aat.js +0 -107
  17. package/dist/application/use-cases/generate-acceptance-criteria.js +0 -223
  18. package/dist/application/use-cases/generate-final-report.js +0 -259
  19. package/dist/application/use-cases/generate-narration.js +0 -130
  20. package/dist/application/use-cases/generate-thread-title.js +0 -181
  21. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  22. package/dist/application/use-cases/plan-auto-review.js +0 -57
  23. package/dist/application/use-cases/reconcile-audit.js +0 -100
  24. package/dist/application/use-cases/search-sessions.js +0 -70
  25. package/dist/application/use-cases/should-auto-review.js +0 -71
  26. package/dist/application/use-cases/thread-export.js +0 -245
  27. package/dist/application/use-cases/verify-claim.js +0 -456
  28. package/dist/application/use-cases/write-pass.js +0 -277
  29. package/dist/domain/audit.js +0 -11
  30. package/dist/domain/services/codebase-detector.js +0 -85
  31. package/dist/domain/task.js +0 -91
  32. package/dist/error-handler.js +0 -139
  33. package/dist/infrastructure/completion/bash-completion.js +0 -29
  34. package/dist/infrastructure/completion/fish-completion.js +0 -17
  35. package/dist/infrastructure/completion/zsh-completion.js +0 -25
  36. package/dist/infrastructure/config/config-manager.js +0 -174
  37. package/dist/infrastructure/config/product-context.js +0 -49
  38. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  39. package/dist/infrastructure/context/adapters/figma-source.js +0 -198
  40. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -160
  41. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  42. package/dist/infrastructure/context/context-resolver.js +0 -144
  43. package/dist/infrastructure/context/context-source.js +0 -114
  44. package/dist/infrastructure/context/github-api.js +0 -110
  45. package/dist/infrastructure/context/github-device-flow.js +0 -65
  46. package/dist/infrastructure/context/oauth-flow.js +0 -207
  47. package/dist/infrastructure/context/oauth-store.js +0 -122
  48. package/dist/infrastructure/context/registry-bootstrap.js +0 -95
  49. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  50. package/dist/infrastructure/cost/budget-config.js +0 -114
  51. package/dist/infrastructure/cost/cost-router.js +0 -97
  52. package/dist/infrastructure/cost/spend-aggregator.js +0 -222
  53. package/dist/infrastructure/cost/team-ledger.js +0 -355
  54. package/dist/infrastructure/discovery/discovery-log.js +0 -85
  55. package/dist/infrastructure/hooks/audit-tip-signature.js +0 -336
  56. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -1018
  57. package/dist/infrastructure/hooks/hook-mode.js +0 -87
  58. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -49
  59. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  60. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  61. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  62. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  63. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  64. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -68
  65. package/dist/infrastructure/orchestration/aider-adapter.js +0 -611
  66. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  67. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -592
  68. package/dist/infrastructure/orchestration/codex-adapter.js +0 -574
  69. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -416
  70. package/dist/infrastructure/orchestration/context-handoff.js +0 -162
  71. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  72. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  73. package/dist/infrastructure/orchestration/detect-ollama.js +0 -75
  74. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  75. package/dist/infrastructure/orchestration/failover-config.js +0 -67
  76. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  77. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -663
  78. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -689
  79. package/dist/infrastructure/orchestration/graphify-context.js +0 -209
  80. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  81. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -388
  82. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  83. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  84. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  85. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -580
  86. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -906
  87. package/dist/infrastructure/orchestration/readiness.js +0 -103
  88. package/dist/infrastructure/orchestration/session-manager.js +0 -1590
  89. package/dist/infrastructure/orchestration/session-persistence.js +0 -394
  90. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  91. package/dist/infrastructure/orchestration/write-pass-store.js +0 -38
  92. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  93. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  94. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -3935
  95. package/dist/infrastructure/protocols/mcp-tools.js +0 -401
  96. package/dist/infrastructure/protocols/probe-port.js +0 -36
  97. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  98. package/dist/infrastructure/protocols/ui/app-script-artifacts.js +0 -121
  99. package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +0 -44
  100. package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +0 -56
  101. package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +0 -588
  102. package/dist/infrastructure/protocols/ui/app-script-context-sources.js +0 -168
  103. package/dist/infrastructure/protocols/ui/app-script-conversation.js +0 -67
  104. package/dist/infrastructure/protocols/ui/app-script-drawer.js +0 -115
  105. package/dist/infrastructure/protocols/ui/app-script-failover.js +0 -133
  106. package/dist/infrastructure/protocols/ui/app-script-github.js +0 -113
  107. package/dist/infrastructure/protocols/ui/app-script-observer.js +0 -104
  108. package/dist/infrastructure/protocols/ui/app-script-onboarding.js +0 -80
  109. package/dist/infrastructure/protocols/ui/app-script-overlay.js +0 -203
  110. package/dist/infrastructure/protocols/ui/app-script-recovery.js +0 -136
  111. package/dist/infrastructure/protocols/ui/app-script-render.js +0 -166
  112. package/dist/infrastructure/protocols/ui/app-script-share.js +0 -78
  113. package/dist/infrastructure/protocols/ui/app-script-sidebar.js +0 -194
  114. package/dist/infrastructure/protocols/ui/app-script-tour.js +0 -78
  115. package/dist/infrastructure/protocols/ui/app-script-wpass.js +0 -312
  116. package/dist/infrastructure/protocols/ui/app-script.js +0 -2998
  117. package/dist/infrastructure/protocols/ui/font-faces.js +0 -7
  118. package/dist/infrastructure/protocols/ui/styles.js +0 -2147
  119. package/dist/infrastructure/protocols/ui/tokens.js +0 -96
  120. package/dist/infrastructure/protocols/ui-page-v2.js +0 -320
  121. package/dist/infrastructure/protocols/verify-page.js +0 -1677
  122. package/dist/infrastructure/security/file-collection.js +0 -118
  123. package/dist/infrastructure/security/fix-code.js +0 -144
  124. package/dist/infrastructure/security/fix-secret.js +0 -89
  125. package/dist/infrastructure/security/permissions-config.js +0 -235
  126. package/dist/infrastructure/security/scan-types.js +0 -10
  127. package/dist/infrastructure/security/ship-scan.js +0 -195
  128. package/dist/infrastructure/security/trust-scorer.js +0 -724
  129. package/dist/infrastructure/security/vibe-checks.js +0 -99
  130. package/dist/infrastructure/storage/atomic-write.js +0 -86
  131. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  132. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  133. package/dist/infrastructure/storage/project-thread-mirror.js +0 -671
  134. package/dist/infrastructure/storage/project-thread-reader.js +0 -409
  135. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  136. package/dist/monitoring.js +0 -212
  137. package/dist/presentation/cli/commands/audit-command.js +0 -201
  138. package/dist/presentation/cli/commands/budget-command.js +0 -79
  139. package/dist/presentation/cli/commands/config-command.js +0 -125
  140. package/dist/presentation/cli/commands/context-source-command.js +0 -217
  141. package/dist/presentation/cli/commands/hook-command.js +0 -265
  142. package/dist/presentation/cli/commands/init-command.js +0 -590
  143. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  144. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  145. package/dist/presentation/cli/commands/permission-command.js +0 -105
  146. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  147. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  148. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  149. package/dist/presentation/cli/commands/report-command.js +0 -403
  150. package/dist/presentation/cli/commands/scan-command.js +0 -52
  151. package/dist/presentation/cli/commands/scan-tool-command.js +0 -110
  152. package/dist/presentation/cli/commands/search-command.js +0 -51
  153. package/dist/presentation/cli/commands/setup-command.js +0 -330
  154. package/dist/presentation/cli/commands/status-command.js +0 -614
  155. package/dist/presentation/cli/commands/team-command.js +0 -256
  156. package/dist/presentation/cli/commands/thread-command.js +0 -370
  157. package/dist/presentation/cli/commands/trust-command.js +0 -328
  158. package/dist/presentation/cli/commands/verify-command.js +0 -298
  159. package/dist/presentation/cli/error-display.js +0 -328
  160. package/dist/presentation/cli/repl.js +0 -805
  161. package/dist/presentation/cli/task-deliverable.js +0 -153
  162. package/dist/testing/cassette.js +0 -125
  163. package/dist/testing/replay-adapter.js +0 -147
@@ -1,906 +0,0 @@
1
- /**
2
- * ProgressAggregator — PM UX Layer 2 (principles #1 "web is not a terminal", #2 "info density").
3
- *
4
- * Ingests the raw TaskEvent stream and produces semantic ProgressStep[] that a
5
- * product manager would actually read ("Read src/session.ts", "Run npm test",
6
- * "Writing reply…") rather than the underlying token-level chatter.
7
- *
8
- * Agent-aware by design: Claude's stream-json tool_use blocks and Codex's
9
- * item.completed entries label differently, so we dispatch on the agent id.
10
- * Keeps the per-adapter files free of PM-layer concerns.
11
- */
12
- const MAX_LABEL = 60;
13
- function shortPath(p) {
14
- const parts = String(p).split('/');
15
- return parts.length <= 2 ? p : '…/' + parts.slice(-2).join('/');
16
- }
17
- function truncate(s, max = MAX_LABEL) {
18
- return s.length > max ? s.slice(0, max - 1) + '…' : s;
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
- }
50
- function labelForClaudeToolUse(name, input) {
51
- const i = (input && typeof input === 'object' ? input : {});
52
- switch (name) {
53
- case 'Read':
54
- return 'Read ' + shortPath(String(i.file_path ?? 'file'));
55
- case 'Write':
56
- return 'Write ' + shortPath(String(i.file_path ?? 'file'));
57
- case 'Edit':
58
- return 'Edit ' + shortPath(String(i.file_path ?? 'file'));
59
- case 'MultiEdit': {
60
- const edits = Array.isArray(i.edits) ? i.edits : [];
61
- const first = edits[0];
62
- return `Edit ${shortPath(String(first?.file_path ?? 'file'))} (${edits.length})`;
63
- }
64
- case 'NotebookEdit':
65
- return 'Edit notebook ' + shortPath(String(i.notebook_path ?? 'notebook'));
66
- case 'Bash': {
67
- const cmd = String(i.command ?? '')
68
- .replace(/\n/g, ' ')
69
- .trim();
70
- return truncate('Run ' + cmd);
71
- }
72
- case 'Grep':
73
- // M2 plain-English pass: "Grep" is a unix command name, jargon
74
- // for non-technical users. "Search code for" tells the user what
75
- // it actually does.
76
- return 'Search code for ' + truncate(String(i.pattern ?? ''), 40);
77
- case 'Glob':
78
- return 'List files matching ' + truncate(String(i.pattern ?? ''), 40);
79
- case 'WebFetch':
80
- return 'Fetch ' + truncate(String(i.url ?? ''), 50);
81
- case 'WebSearch':
82
- return 'Search the web for ' + truncate(String(i.query ?? ''), 50);
83
- case 'TodoWrite':
84
- return 'Plan ' + (Array.isArray(i.todos) ? `${i.todos.length} items` : 'tasks');
85
- case 'Task':
86
- // M2 plain-English pass: "subagent" is internal vocab. "Specialist"
87
- // matches the team-roster framing the user already sees in the
88
- // dashboard ("codehere · Conductor", "Claude Code specialist", etc.)
89
- return 'Hand off to a specialist';
90
- default:
91
- return truncate(String(name || 'tool'));
92
- }
93
- }
94
- /**
95
- * Extract human-readable text from a Claude tool_result content block.
96
- * Handles the two shapes the SDK emits:
97
- * { content: 'plain text' } (most)
98
- * { content: [{type:'text', text:'...'}, {type:'image', ...}, ...] } (multi-block)
99
- */
100
- function extractClaudeToolResultText(b) {
101
- const c = b.content;
102
- if (typeof c === 'string')
103
- return c;
104
- if (Array.isArray(c)) {
105
- const parts = [];
106
- for (const part of c) {
107
- if (part && typeof part === 'object') {
108
- const p = part;
109
- if (p.type === 'text' && typeof p.text === 'string')
110
- parts.push(p.text);
111
- }
112
- else if (typeof part === 'string') {
113
- parts.push(part);
114
- }
115
- }
116
- return parts.join('\n');
117
- }
118
- return '';
119
- }
120
- /**
121
- * Truncate output for step preview. Keeps the LAST ~400 chars (tail is
122
- * usually more useful than head for command output — errors land
123
- * there). Adds a leading ellipsis when truncation occurs.
124
- */
125
- function truncateOutput(s) {
126
- const max = 400;
127
- const stripped = s.replace(/\[[0-9;]*m/g, ''); // strip ANSI
128
- if (stripped.length <= max)
129
- return stripped;
130
- return '…' + stripped.slice(-(max - 1));
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
- }
152
- function unwrapShellWrapper(command) {
153
- // Codex wraps its shell calls in `/bin/zsh -lc "…"` or `sh -c "…"`. Strip that
154
- // so the step label reads as the actual command instead of the wrapper.
155
- const m = command.match(/^(?:\/bin\/(?:zsh|bash|sh)|sh|bash|zsh)\s+-l?c\s+["'](.+)["']\s*$/);
156
- return m ? m[1].trim() : command.trim();
157
- }
158
- function labelForCodexCommand(command) {
159
- const raw = unwrapShellWrapper(String(command).replace(/\n/g, ' ').trim());
160
- // Heuristic friendly prefix for common operations
161
- if (/^cat\s+/.test(raw))
162
- return truncate('Read ' + raw.replace(/^cat\s+/, '').split(/\s+/)[0]);
163
- if (/^sed\s+-n\s+/.test(raw)) {
164
- // e.g. `sed -n '1,260p' README.md` → "Read README.md"
165
- const tail = raw.split(/\s+/).slice(-1)[0];
166
- return truncate('Read ' + tail);
167
- }
168
- if (/^(?:ls|find)\b/.test(raw))
169
- return truncate('List ' + raw.replace(/^(?:ls|find)\s*/, ''));
170
- // M2 plain-English pass: "File op:" is awkward; describe what's
171
- // happening per command kind.
172
- if (/^mkdir\b/.test(raw))
173
- return truncate('Create folder: ' + raw.replace(/^mkdir\s+(?:-\S+\s+)*/, ''));
174
- if (/^cp\b/.test(raw))
175
- return truncate('Copy: ' + raw.replace(/^cp\s+(?:-\S+\s+)*/, ''));
176
- if (/^mv\b/.test(raw))
177
- return truncate('Move/rename: ' + raw.replace(/^mv\s+(?:-\S+\s+)*/, ''));
178
- if (/^rm\b/.test(raw))
179
- return truncate('Remove: ' + raw.replace(/^rm\s+(?:-\S+\s+)*/, ''));
180
- if (/^touch\b/.test(raw))
181
- return truncate('Create file: ' + raw.replace(/^touch\s+/, ''));
182
- if (/\becho\s+.+>/.test(raw))
183
- return truncate('Write to file: ' + raw);
184
- if (/^git\s+/.test(raw))
185
- return truncate(raw);
186
- if (/^npm\s+test\b/.test(raw) || /^pytest\b/.test(raw) || /^vitest\b/.test(raw))
187
- return truncate('Run tests');
188
- return truncate('Run ' + raw);
189
- }
190
- export class ProgressAggregator {
191
- agentId;
192
- _steps = [];
193
- toolUseIndex = new Map(); // tool_use_id → step idx (Claude)
194
- constructor(agentId) {
195
- this.agentId = agentId;
196
- }
197
- get steps() {
198
- return this._steps;
199
- }
200
- /** @returns true if the step list changed (caller emits synthetic event). */
201
- ingest(event) {
202
- const type = String(event.type || '');
203
- const data = (event.data ?? {});
204
- if (this.agentId === 'claude-code')
205
- return this.ingestClaude(type, data);
206
- if (this.agentId === 'codex')
207
- return this.ingestCodex(type, data);
208
- if (this.agentId === 'gemini')
209
- return this.ingestGemini(type, data);
210
- if (this.agentId === 'aider')
211
- return this.ingestAider(type, data);
212
- if (this.agentId === 'opencode')
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
- }
252
- return false;
253
- }
254
- // OpenCode · `run --format json` emits tool_use with shape
255
- // { type:'tool_use', part:{ tool, state:{ input, output, status } } }. Was
256
- // never wired into the aggregator (opencode showed ZERO timeline steps — a
257
- // real gap surfaced by the mock harness). Build a step on the terminal
258
- // (status==='completed') event so opencode is a first-class agent.
259
- ingestOpencode(type, data) {
260
- // OpenCode's narrative text rides `text` events with part.text (the same
261
- // shape the adapter's summary extraction reads) — cross-vendor say capture
262
- // per the v1 spec, so opencode's Final Report stops degrading to the
263
- // deterministic fallback.
264
- if (type === 'text') {
265
- const part = (data.part ?? {});
266
- return this.pushSay(String(part.text ?? ''));
267
- }
268
- if (type !== 'tool_use')
269
- return false;
270
- const part = (data.part ?? {});
271
- if (part.state?.status && part.state.status !== 'completed')
272
- return false; // skip in-flight
273
- const map = {
274
- bash: 'Bash',
275
- read: 'Read',
276
- write: 'Write',
277
- edit: 'Edit',
278
- grep: 'Grep',
279
- glob: 'Glob',
280
- webfetch: 'WebFetch',
281
- websearch: 'WebSearch',
282
- };
283
- const tool = map[String(part.tool ?? '').toLowerCase()] || 'Bash';
284
- const label = labelForClaudeToolUse(tool, part.state?.input);
285
- const output = typeof part.state?.output === 'string' ? part.state.output : '';
286
- this._steps.push({
287
- label,
288
- state: 'done',
289
- startedAt: Date.now(),
290
- completedAt: Date.now(),
291
- ...(output ? { output: truncateOutput(output) } : {}),
292
- });
293
- return true;
294
- }
295
- /** Flush: mark every still-active step as failed (called on abnormal exit). */
296
- finalize(status) {
297
- // Surface any trailing reasoning that had no following action.
298
- let changed = this.flushAiderSay();
299
- if (this.flushGeminiSay())
300
- changed = true;
301
- for (const s of this._steps) {
302
- if (s.state === 'active' || s.state === 'pending') {
303
- s.state = status === 'completed' ? 'done' : 'failed';
304
- s.completedAt = Date.now();
305
- changed = true;
306
- }
307
- }
308
- return changed;
309
- }
310
- addStep(label, state = 'active') {
311
- const idx = this._steps.push({ label, state, startedAt: Date.now() }) - 1;
312
- return idx;
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
- }
351
- /** The agent's own narration as a kind:'say' step — the "watch it work"
352
- * signal AND what the Final Report engine reuses as the summary. Cross-
353
- * vendor by decision of the v1 spec (the room thread is "tool steps woven
354
- * with the agent's plain-English narration" — for every agent, not just
355
- * claude/aider). Same 500-char cap as the original claude capture. */
356
- pushSay(text) {
357
- const said = String(text ?? '').trim();
358
- if (!said)
359
- return false;
360
- this._steps.push({
361
- kind: 'say',
362
- label: said.length > 20000 ? said.slice(0, 20000) + '…' : said,
363
- state: 'done',
364
- startedAt: Date.now(),
365
- completedAt: Date.now(),
366
- });
367
- return true;
368
- }
369
- ingestClaude(type, data) {
370
- if (type === 'assistant') {
371
- const msg = (data.message ?? {});
372
- const content = Array.isArray(msg.content) ? msg.content : [];
373
- let changed = false;
374
- for (const blk of content) {
375
- const b = (blk && typeof blk === 'object' ? blk : {});
376
- if (b.type === 'text' && typeof b.text === 'string' && b.text.trim()) {
377
- // The agent's own words/reasoning. In claude's stream the text block
378
- // precedes the tool_use in the same message — capture it first so the
379
- // timeline reads "explains → acts". This is the "watch it work" signal
380
- // the agent already produces; we were dropping it.
381
- const said = b.text.trim();
382
- this._steps.push({
383
- kind: 'say',
384
- label: said.length > 20000 ? said.slice(0, 20000) + '…' : said,
385
- state: 'done',
386
- startedAt: Date.now(),
387
- completedAt: Date.now(),
388
- });
389
- changed = true;
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
- }
406
- else if (b.type === 'tool_use') {
407
- const id = String(b.id ?? '');
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
- }
434
- changed = true;
435
- }
436
- }
437
- return changed;
438
- }
439
- if (type === 'user') {
440
- // Match tool_result blocks back to their tool_use step.
441
- const msg = (data.message ?? {});
442
- const content = Array.isArray(msg.content) ? msg.content : [];
443
- let changed = false;
444
- for (const blk of content) {
445
- const b = (blk && typeof blk === 'object' ? blk : {});
446
- if (b.type === 'tool_result') {
447
- const id = String(b.tool_use_id ?? '');
448
- const idx = id ? this.toolUseIndex.get(id) : undefined;
449
- if (idx !== undefined) {
450
- const step = this._steps[idx];
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';
455
- step.completedAt = Date.now();
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.
459
- const out = extractClaudeToolResultText(b);
460
- if (out) {
461
- step.fullOutput = out;
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
- }
474
- changed = true;
475
- }
476
- }
477
- }
478
- return changed;
479
- }
480
- return false;
481
- }
482
- /**
483
- * Gemini's stream-json schema (verified against @google/gemini-cli source):
484
- * tool_use: { tool_name, tool_id, parameters } — adds an active step
485
- * tool_result: { tool_id, ...output } — closes by tool_id
486
- *
487
- * We reuse the Claude-shaped tool labelling as a best-effort default — Gemini's
488
- * tool surface (Read, Edit, Write, Shell, Grep, Glob, etc.) overlaps heavily
489
- * with the Claude SDK tool naming since both emerged from the same agent
490
- * lineage. If a specific Gemini tool name doesn't match, the default branch in
491
- * `labelForClaudeToolUse` falls back to the raw tool name.
492
- */
493
- ingestGemini(type, data) {
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();
498
- const id = String(data.tool_id ?? '');
499
- const name = String(data.tool_name ?? 'tool');
500
- const label = labelForClaudeToolUse(name, data.parameters);
501
- const idx = this.addStep(label, 'active');
502
- if (id)
503
- this.toolUseIndex.set(id, idx);
504
- return true;
505
- }
506
- if (type === 'tool_result') {
507
- const id = String(data.tool_id ?? '');
508
- const idx = id ? this.toolUseIndex.get(id) : undefined;
509
- if (idx !== undefined) {
510
- const step = this._steps[idx];
511
- // Gemini's tool_result may carry an `error` field on failure; treat any
512
- // non-undefined `error` as failed. Otherwise mark done.
513
- const isError = data.error !== undefined && data.error !== null;
514
- step.state = isError ? 'failed' : 'done';
515
- step.completedAt = Date.now();
516
- // P22 slice 3 — Gemini tool_result output. Schema is sparse;
517
- // accept the common output/result/content fields, with error
518
- // text as fallback when isError is true.
519
- const out = (typeof data.output === 'string' && data.output) ||
520
- (typeof data.result === 'string' && data.result) ||
521
- (typeof data.content === 'string' && data.content) ||
522
- (isError && data.error && typeof data.error === 'object'
523
- ? `${data.error.type ?? ''} ${data.error.message ?? ''}`
524
- : '');
525
- if (out)
526
- step.output = truncateOutput(String(out));
527
- return true;
528
- }
529
- }
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.
552
- if (type === 'content' && typeof data.text === 'string') {
553
- return this.pushSay(data.text);
554
- }
555
- return false;
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
- }
567
- /**
568
- * Aider observability parity (v0.5 Path B per docs/design/f3_memory_spec.md
569
- * + ~/.claude/.../memory/learning_aider.md).
570
- *
571
- * Aider doesn't expose a structured event stream — we parse stdout lines
572
- * matching the EXACT patterns aider's `base_coder.py` writes when it
573
- * applies an edit, creates a new file, or runs a shell command (verified
574
- * against installed aider 0.86.2 source — line refs below).
575
- *
576
- * Patterns we match:
577
- * "Running <command>" — base_coder.py:2472 — fires AFTER
578
- * aider's confirm_ask returns yes (which `--yes-always` answers
579
- * automatically). Aider has already started executing by the time we see
580
- * this — OBSERVABILITY, not blocking.
581
- * "Applied edit to <path>" — base_coder.py:2334 — fires after
582
- * a SEARCH/REPLACE block lands. Used for both edits AND net-new files
583
- * created via the same flow (aider doesn't distinguish in this message).
584
- * "Creating empty file <path>" — base_coder.py:461 — fires when
585
- * aider creates a brand-new empty file as scaffolding before edits.
586
- *
587
- * Patterns we DELIBERATELY do NOT match:
588
- * "Did not apply edit to ..." — explicit non-event; we drop these
589
- * (would be a false-positive Edit step otherwise).
590
- * "Create new file?" — that's a CONFIRM_ASK prompt, not a
591
- * tool_output. With `--yes-always` it never fires interactively, and
592
- * it's not the action-taken signal anyway.
593
- *
594
- * Each matched pattern → one ProgressStep marked done immediately (aider
595
- * doesn't emit separate completion events; the line itself IS the completion).
596
- *
597
- * Label vocabulary is aligned with Claude / Gemini / Codex labels for
598
- * cross-agent UI consistency: `Run X` for shell, `Edit <path>` for file
599
- * edits, `Write <path>` for net-new file creation.
600
- *
601
- * ANSI handling: HEADLESS_FLAGS in aider-adapter.ts include `--no-pretty`
602
- * which strips colors. As defense-in-depth (in case extraArgs override that
603
- * or aider regresses), we strip ANSI escape sequences before matching.
604
- *
605
- * Limitation: codehere CANNOT block aider's actions via this path. The
606
- * upstream-PR Path A (adding `--pre-action-hook` to aider) is what enables
607
- * blocking parity. v0.5 ships observability; v0.5.x or v0.6 ships blocking
608
- * once Path A merges.
609
- */
610
- static ANSI_RE = /\x1b\[[0-9;]*m/g;
611
- /**
612
- * Per-instance set of file paths aider just created via "Creating empty file ...".
613
- * Aider emits BOTH `Creating empty file <p>` AND `Applied edit to <p>` for one
614
- * logical net-new-file creation (file gets touched, then initial content
615
- * gets applied). Without this set we'd record two steps (`Write <p>` then
616
- * `Edit <p>`) for one action — codex Round 2 caught this. We suppress the
617
- * subsequent `Applied edit to` for the same path so the user sees `Write`
618
- * once, not `Write` followed by `Edit`.
619
- */
620
- aiderJustCreated = new Set();
621
- /**
622
- * P22 slice 4 (Aider) — output buffering. Aider's command output
623
- * streams as raw stdout lines AFTER a "Running X" announcement and
624
- * BEFORE the next aider-pattern (or end of session). This index is
625
- * the most-recent "Run" step that's still collecting its tail. When
626
- * a new aider-pattern fires we flush the buffer onto step.output and
627
- * reset.
628
- *
629
- * Only "Running" steps collect output — file-edit/write actions
630
- * don't have a captured stdout; their target IS the visible state.
631
- */
632
- aiderRunningStepIdx = null;
633
- aiderOutputBuffer = [];
634
- /**
635
- * A1 (PRODUCT_STATE §0 NEXT #1) — Aider's natural-language reasoning.
636
- * Aider streams its plain-English explanation to stdout BEFORE the
637
- * SEARCH/REPLACE blocks and "Applied edit" lines. We were dropping every
638
- * such line. Mirror ingestClaude's text-block handling: collect consecutive
639
- * prose lines (skipping diff fences + SEARCH/REPLACE markers + aider's own
640
- * status noise) and flush them as ONE kind:'say' step before the next
641
- * action — the "watch it work" signal across aider, not just claude.
642
- */
643
- aiderSayBuffer = [];
644
- aiderInFence = false;
645
- aiderInDiff = false;
646
- /** Aider stdout lines that are status/noise, never reasoning prose. The
647
- * "must contain a space + length >= 4" guard in collectAiderSay already
648
- * drops bare filenames, fence langs and spinner rules; this catches the
649
- * noisy lines that DO contain spaces. */
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;
656
- aiderFlushOutput() {
657
- if (this.aiderRunningStepIdx === null)
658
- return;
659
- const step = this._steps[this.aiderRunningStepIdx];
660
- if (!step) {
661
- this.aiderRunningStepIdx = null;
662
- this.aiderOutputBuffer = [];
663
- return;
664
- }
665
- if (this.aiderOutputBuffer.length > 0) {
666
- step.output = truncateOutput(this.aiderOutputBuffer.join('\n'));
667
- }
668
- this.aiderRunningStepIdx = null;
669
- this.aiderOutputBuffer = [];
670
- }
671
- /** Flush buffered aider prose as one kind:'say' step. Returns true if a step
672
- * was pushed (so finalize can fold it into its changed flag). */
673
- flushAiderSay() {
674
- if (this.aiderSayBuffer.length === 0)
675
- return false;
676
- const said = this.aiderSayBuffer.join(' ').replace(/\s+/g, ' ').trim();
677
- this.aiderSayBuffer = [];
678
- if (!said)
679
- return false;
680
- this._steps.push({
681
- kind: 'say',
682
- label: said.length > 20000 ? said.slice(0, 20000) + '…' : said,
683
- state: 'done',
684
- startedAt: Date.now(),
685
- completedAt: Date.now(),
686
- });
687
- return true;
688
- }
689
- /** Collect one aider stdout line as reasoning prose unless it's a fence, a
690
- * SEARCH/REPLACE marker/body, status noise, or a bare token. */
691
- collectAiderSay(line) {
692
- if (line.startsWith('```')) {
693
- this.aiderInFence = !this.aiderInFence;
694
- return;
695
- }
696
- if (this.aiderInFence)
697
- return;
698
- if (/^<<<<<<< SEARCH/.test(line)) {
699
- this.aiderInDiff = true;
700
- return;
701
- }
702
- if (/^>>>>>>> REPLACE/.test(line)) {
703
- this.aiderInDiff = false;
704
- return;
705
- }
706
- if (this.aiderInDiff)
707
- return;
708
- if (ProgressAggregator.AIDER_NONPROSE_RE.test(line))
709
- return;
710
- if (ProgressAggregator.AIDER_BOILERPLATE_RE.test(line))
711
- return; // CLI startup noise, anywhere in the line
712
- // Require a space + minimum length so filenames/code tokens don't leak in.
713
- if (line.length < 4 || !line.includes(' '))
714
- return;
715
- this.aiderSayBuffer.push(line);
716
- }
717
- ingestAider(type, data) {
718
- if (type !== 'stdout')
719
- return false;
720
- const raw = typeof data.line === 'string' ? data.line : '';
721
- // Strip ANSI escape codes BEFORE matching so the parser survives a
722
- // `--no-pretty` regression or override.
723
- const line = raw.replace(ProgressAggregator.ANSI_RE, '').trim();
724
- if (!line)
725
- return false;
726
- // EXPLICIT NON-EVENT: defense-in-depth guard. The `^Applied edit to` regex
727
- // below wouldn't match "Did not apply edit to ..." because of the anchor,
728
- // but if either pattern ever loosens, this short-circuit prevents a false
729
- // Edit step from creeping in.
730
- if (/^Did not apply edit to\s+/.test(line))
731
- return false;
732
- // "Running <command>" — base_coder.py:2472
733
- let m = line.match(/^Running\s+(.+)$/);
734
- if (m) {
735
- // P22 slice 4 — flush the previous Running step's buffer before
736
- // starting the new one. New step is the active output target.
737
- this.aiderFlushOutput();
738
- this.flushAiderSay(); // emit any reasoning prose that preceded this run
739
- const idx = this._steps.length;
740
- this._steps.push({
741
- label: truncate('Run ' + m[1]),
742
- state: 'done',
743
- startedAt: Date.now(),
744
- completedAt: Date.now(),
745
- });
746
- this.aiderRunningStepIdx = idx;
747
- return true;
748
- }
749
- // "Applied edit to <path>" — base_coder.py:2334
750
- m = line.match(/^Applied edit to\s+(.+)$/);
751
- if (m) {
752
- const path = m[1];
753
- // P22 slice 4 — flush any in-flight running-step output before
754
- // recording the edit (which is a new aider-pattern signal).
755
- this.aiderFlushOutput();
756
- this.flushAiderSay(); // emit reasoning prose that preceded this edit
757
- // If aider JUST created this file, the Write step already represents the
758
- // action; skip the Edit step that aider emits for the initial content.
759
- // Clear from the set after consuming so subsequent edits to the same
760
- // path (in this same session) DO show as Edit.
761
- if (this.aiderJustCreated.has(path)) {
762
- this.aiderJustCreated.delete(path);
763
- return false;
764
- }
765
- this._steps.push({
766
- label: 'Edit ' + shortPath(path),
767
- state: 'done',
768
- startedAt: Date.now(),
769
- completedAt: Date.now(),
770
- });
771
- return true;
772
- }
773
- // "Creating empty file <path>" — base_coder.py:461 — net-new file scaffold
774
- m = line.match(/^Creating empty file\s+(.+)$/);
775
- if (m) {
776
- const path = m[1];
777
- this.aiderFlushOutput();
778
- this.flushAiderSay(); // emit reasoning prose that preceded this write
779
- this.aiderJustCreated.add(path);
780
- // Use "Write" to align with Claude/Gemini's net-new-file label vocabulary.
781
- this._steps.push({
782
- label: 'Write ' + shortPath(path),
783
- state: 'done',
784
- startedAt: Date.now(),
785
- completedAt: Date.now(),
786
- });
787
- return true;
788
- }
789
- // P22 slice 4 — non-pattern stdout line WITHIN a Running window
790
- // gets buffered as that step's output. Cap at 50 lines so a long-
791
- // running command (e.g., `npm test` printing thousands) doesn't
792
- // bloat memory; truncateOutput at flush time keeps the on-step
793
- // value bounded too.
794
- if (this.aiderRunningStepIdx !== null && this.aiderOutputBuffer.length < 50) {
795
- this.aiderOutputBuffer.push(line);
796
- // Update the active step's output incrementally so the dashboard
797
- // sees the tail before the next aider-pattern flushes.
798
- const step = this._steps[this.aiderRunningStepIdx];
799
- if (step)
800
- step.output = truncateOutput(this.aiderOutputBuffer.join('\n'));
801
- return true; // mark as "step changed" so the SSE re-emits codehere:steps
802
- }
803
- // Outside a Running window: aider's natural-language reasoning. Buffer it
804
- // (skipping diff fences + markers + status noise); flushed as one kind:'say'
805
- // step before the next action or at finalize. (PRODUCT_STATE §0 NEXT #1)
806
- if (this.aiderRunningStepIdx === null)
807
- this.collectAiderSay(line);
808
- return false;
809
- }
810
- ingestCodex(type, data) {
811
- if (type !== 'item.completed')
812
- return false;
813
- const item = (data.item ?? {});
814
- const itemType = String(item.type ?? '');
815
- if (itemType === 'command_execution') {
816
- const label = labelForCodexCommand(String(item.command ?? ''));
817
- const ok = item.exit_code === 0 || item.status === 'completed';
818
- // P22 slice 2 — capture aggregated_output for the bash preview.
819
- // Codex emits the full stdout+stderr in this field on completion.
820
- const aggregated = typeof item.aggregated_output === 'string' ? item.aggregated_output : '';
821
- this._steps.push({
822
- label,
823
- state: ok ? 'done' : 'failed',
824
- startedAt: Date.now(),
825
- completedAt: Date.now(),
826
- ...(aggregated ? { output: truncateOutput(aggregated) } : {}),
827
- });
828
- return true;
829
- }
830
- if (itemType === 'tool_call' || itemType === 'local_shell_call') {
831
- const name = String(item.tool_name ?? item.name ?? 'tool');
832
- // Tool-call items may carry a `result` or `output` field too.
833
- const result = (typeof item.result === 'string' ? item.result : '') || (typeof item.output === 'string' ? item.output : '');
834
- this._steps.push({
835
- label: truncate(name),
836
- state: 'done',
837
- startedAt: Date.now(),
838
- completedAt: Date.now(),
839
- ...(result ? { output: truncateOutput(result) } : {}),
840
- });
841
- return true;
842
- }
843
- // file_change — codex emits one item per Edit/Write/Delete batch. The audit
844
- // mirror (codex-adapter.ts) already handles these; the timeline-step path was
845
- // dropping them, so real codex file edits were invisible in "watch it work"
846
- // (mock-currency audit 2026-06-02 caught this — the mock emits the real shape).
847
- if (itemType === 'file_change') {
848
- const changes = Array.isArray(item.changes)
849
- ? item.changes
850
- : [];
851
- let changed = false;
852
- for (const ch of changes) {
853
- const path = String(ch.path ?? '');
854
- const kind = String(ch.kind ?? 'update');
855
- const toolName = kind === 'add' ? 'Write' : kind === 'delete' ? 'Delete' : 'Edit';
856
- // Label directly so the path always shows (labelForClaudeToolUse only
857
- // knows Edit/Write/Read — it drops the path for Delete).
858
- this._steps.push({
859
- label: path ? toolName + ' ' + path : toolName,
860
- state: 'done',
861
- startedAt: Date.now(),
862
- completedAt: Date.now(),
863
- });
864
- changed = true;
865
- }
866
- return changed;
867
- }
868
- // mcp_tool_call + web_search — real visible actions the codex-adapter mirrors
869
- // to audit; surface them as timeline steps too (2026-06-02 follow-up, Ega-approved
870
- // class: "watch it work"). Labels mirror codex-adapter conventions.
871
- if (itemType === 'mcp_tool_call') {
872
- const server = String(item.server ?? '');
873
- const tool = String(item.tool ?? 'mcp');
874
- this._steps.push({
875
- label: 'MCP · ' + (server ? server + '.' : '') + tool,
876
- state: 'done',
877
- startedAt: Date.now(),
878
- completedAt: Date.now(),
879
- });
880
- return true;
881
- }
882
- if (itemType === 'web_search') {
883
- const q = String(item.query ?? '')
884
- .trim()
885
- .slice(0, 80);
886
- this._steps.push({
887
- label: q ? 'Web search · ' + q : 'Web search',
888
- state: 'done',
889
- startedAt: Date.now(),
890
- completedAt: Date.now(),
891
- });
892
- return true;
893
- }
894
- // agent_message = codex's own narration. Captured as kind:'say' since the
895
- // v1 spec settled the taste call that used to be flagged here (pm-layers
896
- // "just chatter" decision): the room thread is "tool steps woven with the
897
- // agent's plain-English narration" for EVERY vendor, and the Final Report
898
- // engine reuses 'say' steps as the summary — without this, codex always
899
- // degraded to the deterministic fallback.
900
- if (itemType === 'agent_message') {
901
- return this.pushSay(String(item.text ?? ''));
902
- }
903
- return false;
904
- }
905
- }
906
- //# sourceMappingURL=progress-aggregator.js.map