codehere 0.8.0 → 1.0.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.
Files changed (119) hide show
  1. package/README.md +37 -184
  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 +329 -0
  5. package/dist/index.js +74 -1004
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -265
  7. package/dist/presentation/cli/commands/check-command.js +251 -0
  8. package/dist/presentation/cli/commands/help-command.js +22 -286
  9. package/dist/ui.js +19 -438
  10. package/dist/utils/version.js +0 -12
  11. package/package.json +18 -27
  12. package/dist/application/use-cases/domain-trust.js +0 -110
  13. package/dist/application/use-cases/generate-final-report.js +0 -134
  14. package/dist/application/use-cases/generate-narration.js +0 -128
  15. package/dist/application/use-cases/generate-thread-title.js +0 -178
  16. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  17. package/dist/application/use-cases/thread-export.js +0 -155
  18. package/dist/application/use-cases/verify-claim.js +0 -274
  19. package/dist/application/use-cases/write-pass.js +0 -277
  20. package/dist/domain/entities/task.js +0 -61
  21. package/dist/domain/services/codebase-detector.js +0 -91
  22. package/dist/error-handler.js +0 -251
  23. package/dist/infrastructure/completion/bash-completion.js +0 -25
  24. package/dist/infrastructure/completion/fish-completion.js +0 -22
  25. package/dist/infrastructure/completion/zsh-completion.js +0 -30
  26. package/dist/infrastructure/config/config-manager.js +0 -173
  27. package/dist/infrastructure/config/product-context.js +0 -67
  28. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  29. package/dist/infrastructure/context/adapters/figma-source.js +0 -163
  30. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -159
  31. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  32. package/dist/infrastructure/context/context-resolver.js +0 -144
  33. package/dist/infrastructure/context/context-source.js +0 -114
  34. package/dist/infrastructure/context/oauth-flow.js +0 -207
  35. package/dist/infrastructure/context/oauth-store.js +0 -119
  36. package/dist/infrastructure/context/registry-bootstrap.js +0 -99
  37. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  38. package/dist/infrastructure/cost/budget-config.js +0 -113
  39. package/dist/infrastructure/cost/cost-router.js +0 -104
  40. package/dist/infrastructure/cost/spend-aggregator.js +0 -147
  41. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -888
  42. package/dist/infrastructure/hooks/hook-mode.js +0 -71
  43. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -43
  44. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  45. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  46. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  47. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  48. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  49. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -56
  50. package/dist/infrastructure/orchestration/aider-adapter.js +0 -447
  51. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  52. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -478
  53. package/dist/infrastructure/orchestration/codex-adapter.js +0 -538
  54. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -377
  55. package/dist/infrastructure/orchestration/context-handoff.js +0 -144
  56. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  57. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  58. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  59. package/dist/infrastructure/orchestration/failover-config.js +0 -77
  60. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  61. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -583
  62. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -558
  63. package/dist/infrastructure/orchestration/graphify-context.js +0 -254
  64. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  65. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -233
  66. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  67. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  68. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  69. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -510
  70. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -689
  71. package/dist/infrastructure/orchestration/readiness.js +0 -103
  72. package/dist/infrastructure/orchestration/session-manager.js +0 -1091
  73. package/dist/infrastructure/orchestration/session-persistence.js +0 -258
  74. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  75. package/dist/infrastructure/orchestration/write-pass-store.js +0 -46
  76. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  77. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  78. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -2853
  79. package/dist/infrastructure/protocols/probe-port.js +0 -36
  80. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  81. package/dist/infrastructure/protocols/ui-page-v2.js +0 -4418
  82. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  83. package/dist/infrastructure/protocols/verify-page.js +0 -983
  84. package/dist/infrastructure/security/file-collection.js +0 -118
  85. package/dist/infrastructure/security/input-validator.js +0 -152
  86. package/dist/infrastructure/security/permissions-config.js +0 -234
  87. package/dist/infrastructure/security/trust-scorer.js +0 -657
  88. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  89. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  90. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  91. package/dist/infrastructure/storage/project-thread-mirror.js +0 -524
  92. package/dist/infrastructure/storage/project-thread-reader.js +0 -172
  93. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  94. package/dist/infrastructure/storage/task-repository.js +0 -206
  95. package/dist/monitoring.js +0 -212
  96. package/dist/presentation/cli/commands/audit-command.js +0 -126
  97. package/dist/presentation/cli/commands/budget-command.js +0 -79
  98. package/dist/presentation/cli/commands/config-command.js +0 -125
  99. package/dist/presentation/cli/commands/context-source-command.js +0 -198
  100. package/dist/presentation/cli/commands/hook-command.js +0 -265
  101. package/dist/presentation/cli/commands/init-command.js +0 -578
  102. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  103. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  104. package/dist/presentation/cli/commands/permission-command.js +0 -116
  105. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  106. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  107. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  108. package/dist/presentation/cli/commands/report-command.js +0 -387
  109. package/dist/presentation/cli/commands/scan-tool-command.js +0 -94
  110. package/dist/presentation/cli/commands/setup-command.js +0 -307
  111. package/dist/presentation/cli/commands/status-command.js +0 -661
  112. package/dist/presentation/cli/commands/thread-command.js +0 -288
  113. package/dist/presentation/cli/commands/trust-command.js +0 -306
  114. package/dist/presentation/cli/commands/verify-command.js +0 -233
  115. package/dist/presentation/cli/error-display.js +0 -336
  116. package/dist/presentation/cli/repl.js +0 -797
  117. package/dist/presentation/cli/task-deliverable.js +0 -127
  118. package/dist/testing/cassette.js +0 -125
  119. package/dist/testing/replay-adapter.js +0 -166
@@ -1,689 +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
- function labelForClaudeToolUse(name, input) {
21
- const i = (input && typeof input === 'object' ? input : {});
22
- switch (name) {
23
- case 'Read':
24
- return 'Read ' + shortPath(String(i.file_path ?? 'file'));
25
- case 'Write':
26
- return 'Write ' + shortPath(String(i.file_path ?? 'file'));
27
- case 'Edit':
28
- return 'Edit ' + shortPath(String(i.file_path ?? 'file'));
29
- case 'MultiEdit': {
30
- const edits = Array.isArray(i.edits) ? i.edits : [];
31
- const first = edits[0];
32
- return `Edit ${shortPath(String(first?.file_path ?? 'file'))} (${edits.length})`;
33
- }
34
- case 'NotebookEdit':
35
- return 'Edit notebook ' + shortPath(String(i.notebook_path ?? 'notebook'));
36
- case 'Bash': {
37
- const cmd = String(i.command ?? '')
38
- .replace(/\n/g, ' ')
39
- .trim();
40
- return truncate('Run ' + cmd);
41
- }
42
- case 'Grep':
43
- // M2 plain-English pass: "Grep" is a unix command name, jargon
44
- // for non-technical users. "Search code for" tells the user what
45
- // it actually does.
46
- return 'Search code for ' + truncate(String(i.pattern ?? ''), 40);
47
- case 'Glob':
48
- return 'List files matching ' + truncate(String(i.pattern ?? ''), 40);
49
- case 'WebFetch':
50
- return 'Fetch ' + truncate(String(i.url ?? ''), 50);
51
- case 'WebSearch':
52
- return 'Search the web for ' + truncate(String(i.query ?? ''), 50);
53
- case 'TodoWrite':
54
- return 'Plan ' + (Array.isArray(i.todos) ? `${i.todos.length} items` : 'tasks');
55
- case 'Task':
56
- // M2 plain-English pass: "subagent" is internal vocab. "Specialist"
57
- // matches the team-roster framing the user already sees in the
58
- // dashboard ("codehere · Conductor", "Claude Code specialist", etc.)
59
- return 'Hand off to a specialist';
60
- default:
61
- return truncate(String(name || 'tool'));
62
- }
63
- }
64
- /**
65
- * Extract human-readable text from a Claude tool_result content block.
66
- * Handles the two shapes the SDK emits:
67
- * { content: 'plain text' } (most)
68
- * { content: [{type:'text', text:'...'}, {type:'image', ...}, ...] } (multi-block)
69
- */
70
- function extractClaudeToolResultText(b) {
71
- const c = b.content;
72
- if (typeof c === 'string')
73
- return c;
74
- if (Array.isArray(c)) {
75
- const parts = [];
76
- for (const part of c) {
77
- if (part && typeof part === 'object') {
78
- const p = part;
79
- if (p.type === 'text' && typeof p.text === 'string')
80
- parts.push(p.text);
81
- }
82
- else if (typeof part === 'string') {
83
- parts.push(part);
84
- }
85
- }
86
- return parts.join('\n');
87
- }
88
- return '';
89
- }
90
- /**
91
- * Truncate output for step preview. Keeps the LAST ~400 chars (tail is
92
- * usually more useful than head for command output — errors land
93
- * there). Adds a leading ellipsis when truncation occurs.
94
- */
95
- function truncateOutput(s) {
96
- const max = 400;
97
- const stripped = s.replace(/\[[0-9;]*m/g, ''); // strip ANSI
98
- if (stripped.length <= max)
99
- return stripped;
100
- return '…' + stripped.slice(-(max - 1));
101
- }
102
- function unwrapShellWrapper(command) {
103
- // Codex wraps its shell calls in `/bin/zsh -lc "…"` or `sh -c "…"`. Strip that
104
- // so the step label reads as the actual command instead of the wrapper.
105
- const m = command.match(/^(?:\/bin\/(?:zsh|bash|sh)|sh|bash|zsh)\s+-l?c\s+["'](.+)["']\s*$/);
106
- return m ? m[1].trim() : command.trim();
107
- }
108
- function labelForCodexCommand(command) {
109
- const raw = unwrapShellWrapper(String(command).replace(/\n/g, ' ').trim());
110
- // Heuristic friendly prefix for common operations
111
- if (/^cat\s+/.test(raw))
112
- return truncate('Read ' + raw.replace(/^cat\s+/, '').split(/\s+/)[0]);
113
- if (/^sed\s+-n\s+/.test(raw)) {
114
- // e.g. `sed -n '1,260p' README.md` → "Read README.md"
115
- const tail = raw.split(/\s+/).slice(-1)[0];
116
- return truncate('Read ' + tail);
117
- }
118
- if (/^(?:ls|find)\b/.test(raw))
119
- return truncate('List ' + raw.replace(/^(?:ls|find)\s*/, ''));
120
- // M2 plain-English pass: "File op:" is awkward; describe what's
121
- // happening per command kind.
122
- if (/^mkdir\b/.test(raw))
123
- return truncate('Create folder: ' + raw.replace(/^mkdir\s+(?:-\S+\s+)*/, ''));
124
- if (/^cp\b/.test(raw))
125
- return truncate('Copy: ' + raw.replace(/^cp\s+(?:-\S+\s+)*/, ''));
126
- if (/^mv\b/.test(raw))
127
- return truncate('Move/rename: ' + raw.replace(/^mv\s+(?:-\S+\s+)*/, ''));
128
- if (/^rm\b/.test(raw))
129
- return truncate('Remove: ' + raw.replace(/^rm\s+(?:-\S+\s+)*/, ''));
130
- if (/^touch\b/.test(raw))
131
- return truncate('Create file: ' + raw.replace(/^touch\s+/, ''));
132
- if (/\becho\s+.+>/.test(raw))
133
- return truncate('Write to file: ' + raw);
134
- if (/^git\s+/.test(raw))
135
- return truncate(raw);
136
- if (/^npm\s+test\b/.test(raw) || /^pytest\b/.test(raw) || /^vitest\b/.test(raw))
137
- return truncate('Run tests');
138
- return truncate('Run ' + raw);
139
- }
140
- export class ProgressAggregator {
141
- agentId;
142
- _steps = [];
143
- toolUseIndex = new Map(); // tool_use_id → step idx (Claude)
144
- constructor(agentId) {
145
- this.agentId = agentId;
146
- }
147
- get steps() {
148
- return this._steps;
149
- }
150
- /** @returns true if the step list changed (caller emits synthetic event). */
151
- ingest(event) {
152
- const type = String(event.type || '');
153
- const data = (event.data ?? {});
154
- if (this.agentId === 'claude-code')
155
- return this.ingestClaude(type, data);
156
- if (this.agentId === 'codex')
157
- return this.ingestCodex(type, data);
158
- if (this.agentId === 'gemini')
159
- return this.ingestGemini(type, data);
160
- if (this.agentId === 'aider')
161
- return this.ingestAider(type, data);
162
- if (this.agentId === 'opencode')
163
- return this.ingestOpencode(type, data);
164
- return false;
165
- }
166
- // OpenCode · `run --format json` emits tool_use with shape
167
- // { type:'tool_use', part:{ tool, state:{ input, output, status } } }. Was
168
- // never wired into the aggregator (opencode showed ZERO timeline steps — a
169
- // real gap surfaced by the mock harness). Build a step on the terminal
170
- // (status==='completed') event so opencode is a first-class agent.
171
- ingestOpencode(type, data) {
172
- // OpenCode's narrative text rides `text` events with part.text (the same
173
- // shape the adapter's summary extraction reads) — cross-vendor say capture
174
- // per the v1 spec, so opencode's Final Report stops degrading to the
175
- // deterministic fallback.
176
- if (type === 'text') {
177
- const part = (data.part ?? {});
178
- return this.pushSay(String(part.text ?? ''));
179
- }
180
- if (type !== 'tool_use')
181
- return false;
182
- const part = (data.part ?? {});
183
- if (part.state?.status && part.state.status !== 'completed')
184
- return false; // skip in-flight
185
- const map = {
186
- bash: 'Bash',
187
- read: 'Read',
188
- write: 'Write',
189
- edit: 'Edit',
190
- grep: 'Grep',
191
- glob: 'Glob',
192
- webfetch: 'WebFetch',
193
- websearch: 'WebSearch',
194
- };
195
- const tool = map[String(part.tool ?? '').toLowerCase()] || 'Bash';
196
- const label = labelForClaudeToolUse(tool, part.state?.input);
197
- const output = typeof part.state?.output === 'string' ? part.state.output : '';
198
- this._steps.push({
199
- label,
200
- state: 'done',
201
- startedAt: Date.now(),
202
- completedAt: Date.now(),
203
- ...(output ? { output: truncateOutput(output) } : {}),
204
- });
205
- return true;
206
- }
207
- /** Flush: mark every still-active step as failed (called on abnormal exit). */
208
- finalize(status) {
209
- // Surface any trailing aider reasoning that had no following action.
210
- let changed = this.flushAiderSay();
211
- for (const s of this._steps) {
212
- if (s.state === 'active' || s.state === 'pending') {
213
- s.state = status === 'completed' ? 'done' : 'failed';
214
- s.completedAt = Date.now();
215
- changed = true;
216
- }
217
- }
218
- return changed;
219
- }
220
- addStep(label, state = 'active') {
221
- const idx = this._steps.push({ label, state, startedAt: Date.now() }) - 1;
222
- return idx;
223
- }
224
- /** The agent's own narration as a kind:'say' step — the "watch it work"
225
- * signal AND what the Final Report engine reuses as the summary. Cross-
226
- * vendor by decision of the v1 spec (the room thread is "tool steps woven
227
- * with the agent's plain-English narration" — for every agent, not just
228
- * claude/aider). Same 500-char cap as the original claude capture. */
229
- pushSay(text) {
230
- const said = String(text ?? '').trim();
231
- if (!said)
232
- return false;
233
- this._steps.push({
234
- kind: 'say',
235
- label: said.length > 500 ? said.slice(0, 500) + '\\u2026' : said,
236
- state: 'done',
237
- startedAt: Date.now(),
238
- completedAt: Date.now(),
239
- });
240
- return true;
241
- }
242
- ingestClaude(type, data) {
243
- if (type === 'assistant') {
244
- const msg = (data.message ?? {});
245
- const content = Array.isArray(msg.content) ? msg.content : [];
246
- let changed = false;
247
- for (const blk of content) {
248
- const b = (blk && typeof blk === 'object' ? blk : {});
249
- if (b.type === 'text' && typeof b.text === 'string' && b.text.trim()) {
250
- // The agent's own words/reasoning. In claude's stream the text block
251
- // precedes the tool_use in the same message — capture it first so the
252
- // timeline reads "explains → acts". This is the "watch it work" signal
253
- // the agent already produces; we were dropping it.
254
- const said = b.text.trim();
255
- this._steps.push({
256
- kind: 'say',
257
- label: said.length > 500 ? said.slice(0, 500) + '\\u2026' : said,
258
- state: 'done',
259
- startedAt: Date.now(),
260
- completedAt: Date.now(),
261
- });
262
- changed = true;
263
- }
264
- else if (b.type === 'tool_use') {
265
- 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);
270
- changed = true;
271
- }
272
- }
273
- return changed;
274
- }
275
- if (type === 'user') {
276
- // Match tool_result blocks back to their tool_use step.
277
- const msg = (data.message ?? {});
278
- const content = Array.isArray(msg.content) ? msg.content : [];
279
- let changed = false;
280
- for (const blk of content) {
281
- const b = (blk && typeof blk === 'object' ? blk : {});
282
- if (b.type === 'tool_result') {
283
- const id = String(b.tool_use_id ?? '');
284
- const idx = id ? this.toolUseIndex.get(id) : undefined;
285
- if (idx !== undefined) {
286
- const step = this._steps[idx];
287
- step.state = b.is_error ? 'failed' : 'done';
288
- 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.
295
- const out = extractClaudeToolResultText(b);
296
- if (out)
297
- step.output = truncateOutput(out);
298
- changed = true;
299
- }
300
- }
301
- }
302
- return changed;
303
- }
304
- return false;
305
- }
306
- /**
307
- * Gemini's stream-json schema (verified against @google/gemini-cli source):
308
- * tool_use: { tool_name, tool_id, parameters } — adds an active step
309
- * tool_result: { tool_id, ...output } — closes by tool_id
310
- *
311
- * We reuse the Claude-shaped tool labelling as a best-effort default — Gemini's
312
- * tool surface (Read, Edit, Write, Shell, Grep, Glob, etc.) overlaps heavily
313
- * with the Claude SDK tool naming since both emerged from the same agent
314
- * lineage. If a specific Gemini tool name doesn't match, the default branch in
315
- * `labelForClaudeToolUse` falls back to the raw tool name.
316
- */
317
- ingestGemini(type, data) {
318
- if (type === 'tool_use') {
319
- const id = String(data.tool_id ?? '');
320
- const name = String(data.tool_name ?? 'tool');
321
- const label = labelForClaudeToolUse(name, data.parameters);
322
- const idx = this.addStep(label, 'active');
323
- if (id)
324
- this.toolUseIndex.set(id, idx);
325
- return true;
326
- }
327
- if (type === 'tool_result') {
328
- const id = String(data.tool_id ?? '');
329
- const idx = id ? this.toolUseIndex.get(id) : undefined;
330
- if (idx !== undefined) {
331
- const step = this._steps[idx];
332
- // Gemini's tool_result may carry an `error` field on failure; treat any
333
- // non-undefined `error` as failed. Otherwise mark done.
334
- const isError = data.error !== undefined && data.error !== null;
335
- step.state = isError ? 'failed' : 'done';
336
- step.completedAt = Date.now();
337
- // P22 slice 3 — Gemini tool_result output. Schema is sparse;
338
- // accept the common output/result/content fields, with error
339
- // text as fallback when isError is true.
340
- const out = (typeof data.output === 'string' && data.output) ||
341
- (typeof data.result === 'string' && data.result) ||
342
- (typeof data.content === 'string' && data.content) ||
343
- (isError && data.error && typeof data.error === 'object'
344
- ? `${data.error.type ?? ''} ${data.error.message ?? ''}`
345
- : '');
346
- if (out)
347
- step.output = truncateOutput(String(out));
348
- return true;
349
- }
350
- }
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).
354
- if (type === 'content' && typeof data.text === 'string') {
355
- return this.pushSay(data.text);
356
- }
357
- return false;
358
- }
359
- /**
360
- * Aider observability parity (v0.5 Path B per docs/design/f3_memory_spec.md
361
- * + ~/.claude/.../memory/learning_aider.md).
362
- *
363
- * Aider doesn't expose a structured event stream — we parse stdout lines
364
- * matching the EXACT patterns aider's `base_coder.py` writes when it
365
- * applies an edit, creates a new file, or runs a shell command (verified
366
- * against installed aider 0.86.2 source — line refs below).
367
- *
368
- * Patterns we match:
369
- * "Running <command>" — base_coder.py:2472 — fires AFTER
370
- * aider's confirm_ask returns yes (which `--yes-always` answers
371
- * automatically). Aider has already started executing by the time we see
372
- * this — OBSERVABILITY, not blocking.
373
- * "Applied edit to <path>" — base_coder.py:2334 — fires after
374
- * a SEARCH/REPLACE block lands. Used for both edits AND net-new files
375
- * created via the same flow (aider doesn't distinguish in this message).
376
- * "Creating empty file <path>" — base_coder.py:461 — fires when
377
- * aider creates a brand-new empty file as scaffolding before edits.
378
- *
379
- * Patterns we DELIBERATELY do NOT match:
380
- * "Did not apply edit to ..." — explicit non-event; we drop these
381
- * (would be a false-positive Edit step otherwise).
382
- * "Create new file?" — that's a CONFIRM_ASK prompt, not a
383
- * tool_output. With `--yes-always` it never fires interactively, and
384
- * it's not the action-taken signal anyway.
385
- *
386
- * Each matched pattern → one ProgressStep marked done immediately (aider
387
- * doesn't emit separate completion events; the line itself IS the completion).
388
- *
389
- * Label vocabulary is aligned with Claude / Gemini / Codex labels for
390
- * cross-agent UI consistency: `Run X` for shell, `Edit <path>` for file
391
- * edits, `Write <path>` for net-new file creation.
392
- *
393
- * ANSI handling: HEADLESS_FLAGS in aider-adapter.ts include `--no-pretty`
394
- * which strips colors. As defense-in-depth (in case extraArgs override that
395
- * or aider regresses), we strip ANSI escape sequences before matching.
396
- *
397
- * Limitation: codehere CANNOT block aider's actions via this path. The
398
- * upstream-PR Path A (adding `--pre-action-hook` to aider) is what enables
399
- * blocking parity. v0.5 ships observability; v0.5.x or v0.6 ships blocking
400
- * once Path A merges.
401
- */
402
- static ANSI_RE = /\x1b\[[0-9;]*m/g;
403
- /**
404
- * Per-instance set of file paths aider just created via "Creating empty file ...".
405
- * Aider emits BOTH `Creating empty file <p>` AND `Applied edit to <p>` for one
406
- * logical net-new-file creation (file gets touched, then initial content
407
- * gets applied). Without this set we'd record two steps (`Write <p>` then
408
- * `Edit <p>`) for one action — codex Round 2 caught this. We suppress the
409
- * subsequent `Applied edit to` for the same path so the user sees `Write`
410
- * once, not `Write` followed by `Edit`.
411
- */
412
- aiderJustCreated = new Set();
413
- /**
414
- * P22 slice 4 (Aider) — output buffering. Aider's command output
415
- * streams as raw stdout lines AFTER a "Running X" announcement and
416
- * BEFORE the next aider-pattern (or end of session). This index is
417
- * the most-recent "Run" step that's still collecting its tail. When
418
- * a new aider-pattern fires we flush the buffer onto step.output and
419
- * reset.
420
- *
421
- * Only "Running" steps collect output — file-edit/write actions
422
- * don't have a captured stdout; their target IS the visible state.
423
- */
424
- aiderRunningStepIdx = null;
425
- aiderOutputBuffer = [];
426
- /**
427
- * A1 (PRODUCT_STATE §0 NEXT #1) — Aider's natural-language reasoning.
428
- * Aider streams its plain-English explanation to stdout BEFORE the
429
- * SEARCH/REPLACE blocks and "Applied edit" lines. We were dropping every
430
- * such line. Mirror ingestClaude's text-block handling: collect consecutive
431
- * prose lines (skipping diff fences + SEARCH/REPLACE markers + aider's own
432
- * status noise) and flush them as ONE kind:'say' step before the next
433
- * action — the "watch it work" signal across aider, not just claude.
434
- */
435
- aiderSayBuffer = [];
436
- aiderInFence = false;
437
- aiderInDiff = false;
438
- /** Aider stdout lines that are status/noise, never reasoning prose. The
439
- * "must contain a space + length >= 4" guard in collectAiderSay already
440
- * drops bare filenames, fence langs and spinner rules; this catches the
441
- * 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 )/;
443
- aiderFlushOutput() {
444
- if (this.aiderRunningStepIdx === null)
445
- return;
446
- const step = this._steps[this.aiderRunningStepIdx];
447
- if (!step) {
448
- this.aiderRunningStepIdx = null;
449
- this.aiderOutputBuffer = [];
450
- return;
451
- }
452
- if (this.aiderOutputBuffer.length > 0) {
453
- step.output = truncateOutput(this.aiderOutputBuffer.join('\n'));
454
- }
455
- this.aiderRunningStepIdx = null;
456
- this.aiderOutputBuffer = [];
457
- }
458
- /** Flush buffered aider prose as one kind:'say' step. Returns true if a step
459
- * was pushed (so finalize can fold it into its changed flag). */
460
- flushAiderSay() {
461
- if (this.aiderSayBuffer.length === 0)
462
- return false;
463
- const said = this.aiderSayBuffer.join(' ').replace(/\s+/g, ' ').trim();
464
- this.aiderSayBuffer = [];
465
- if (!said)
466
- return false;
467
- this._steps.push({
468
- kind: 'say',
469
- label: said.length > 500 ? said.slice(0, 500) + '…' : said,
470
- state: 'done',
471
- startedAt: Date.now(),
472
- completedAt: Date.now(),
473
- });
474
- return true;
475
- }
476
- /** Collect one aider stdout line as reasoning prose unless it's a fence, a
477
- * SEARCH/REPLACE marker/body, status noise, or a bare token. */
478
- collectAiderSay(line) {
479
- if (line.startsWith('```')) {
480
- this.aiderInFence = !this.aiderInFence;
481
- return;
482
- }
483
- if (this.aiderInFence)
484
- return;
485
- if (/^<<<<<<< SEARCH/.test(line)) {
486
- this.aiderInDiff = true;
487
- return;
488
- }
489
- if (/^>>>>>>> REPLACE/.test(line)) {
490
- this.aiderInDiff = false;
491
- return;
492
- }
493
- if (this.aiderInDiff)
494
- return;
495
- if (ProgressAggregator.AIDER_NONPROSE_RE.test(line))
496
- return;
497
- // Require a space + minimum length so filenames/code tokens don't leak in.
498
- if (line.length < 4 || !line.includes(' '))
499
- return;
500
- this.aiderSayBuffer.push(line);
501
- }
502
- ingestAider(type, data) {
503
- if (type !== 'stdout')
504
- return false;
505
- const raw = typeof data.line === 'string' ? data.line : '';
506
- // Strip ANSI escape codes BEFORE matching so the parser survives a
507
- // `--no-pretty` regression or override.
508
- const line = raw.replace(ProgressAggregator.ANSI_RE, '').trim();
509
- if (!line)
510
- return false;
511
- // EXPLICIT NON-EVENT: defense-in-depth guard. The `^Applied edit to` regex
512
- // below wouldn't match "Did not apply edit to ..." because of the anchor,
513
- // but if either pattern ever loosens, this short-circuit prevents a false
514
- // Edit step from creeping in.
515
- if (/^Did not apply edit to\s+/.test(line))
516
- return false;
517
- // "Running <command>" — base_coder.py:2472
518
- let m = line.match(/^Running\s+(.+)$/);
519
- if (m) {
520
- // P22 slice 4 — flush the previous Running step's buffer before
521
- // starting the new one. New step is the active output target.
522
- this.aiderFlushOutput();
523
- this.flushAiderSay(); // emit any reasoning prose that preceded this run
524
- const idx = this._steps.length;
525
- this._steps.push({
526
- label: truncate('Run ' + m[1]),
527
- state: 'done',
528
- startedAt: Date.now(),
529
- completedAt: Date.now(),
530
- });
531
- this.aiderRunningStepIdx = idx;
532
- return true;
533
- }
534
- // "Applied edit to <path>" — base_coder.py:2334
535
- m = line.match(/^Applied edit to\s+(.+)$/);
536
- if (m) {
537
- const path = m[1];
538
- // P22 slice 4 — flush any in-flight running-step output before
539
- // recording the edit (which is a new aider-pattern signal).
540
- this.aiderFlushOutput();
541
- this.flushAiderSay(); // emit reasoning prose that preceded this edit
542
- // If aider JUST created this file, the Write step already represents the
543
- // action; skip the Edit step that aider emits for the initial content.
544
- // Clear from the set after consuming so subsequent edits to the same
545
- // path (in this same session) DO show as Edit.
546
- if (this.aiderJustCreated.has(path)) {
547
- this.aiderJustCreated.delete(path);
548
- return false;
549
- }
550
- this._steps.push({
551
- label: 'Edit ' + shortPath(path),
552
- state: 'done',
553
- startedAt: Date.now(),
554
- completedAt: Date.now(),
555
- });
556
- return true;
557
- }
558
- // "Creating empty file <path>" — base_coder.py:461 — net-new file scaffold
559
- m = line.match(/^Creating empty file\s+(.+)$/);
560
- if (m) {
561
- const path = m[1];
562
- this.aiderFlushOutput();
563
- this.flushAiderSay(); // emit reasoning prose that preceded this write
564
- this.aiderJustCreated.add(path);
565
- // Use "Write" to align with Claude/Gemini's net-new-file label vocabulary.
566
- this._steps.push({
567
- label: 'Write ' + shortPath(path),
568
- state: 'done',
569
- startedAt: Date.now(),
570
- completedAt: Date.now(),
571
- });
572
- return true;
573
- }
574
- // P22 slice 4 — non-pattern stdout line WITHIN a Running window
575
- // gets buffered as that step's output. Cap at 50 lines so a long-
576
- // running command (e.g., `npm test` printing thousands) doesn't
577
- // bloat memory; truncateOutput at flush time keeps the on-step
578
- // value bounded too.
579
- if (this.aiderRunningStepIdx !== null && this.aiderOutputBuffer.length < 50) {
580
- this.aiderOutputBuffer.push(line);
581
- // Update the active step's output incrementally so the dashboard
582
- // sees the tail before the next aider-pattern flushes.
583
- const step = this._steps[this.aiderRunningStepIdx];
584
- if (step)
585
- step.output = truncateOutput(this.aiderOutputBuffer.join('\n'));
586
- return true; // mark as "step changed" so the SSE re-emits codehere:steps
587
- }
588
- // Outside a Running window: aider's natural-language reasoning. Buffer it
589
- // (skipping diff fences + markers + status noise); flushed as one kind:'say'
590
- // step before the next action or at finalize. (PRODUCT_STATE §0 NEXT #1)
591
- if (this.aiderRunningStepIdx === null)
592
- this.collectAiderSay(line);
593
- return false;
594
- }
595
- ingestCodex(type, data) {
596
- if (type !== 'item.completed')
597
- return false;
598
- const item = (data.item ?? {});
599
- const itemType = String(item.type ?? '');
600
- if (itemType === 'command_execution') {
601
- const label = labelForCodexCommand(String(item.command ?? ''));
602
- const ok = item.exit_code === 0 || item.status === 'completed';
603
- // P22 slice 2 — capture aggregated_output for the bash preview.
604
- // Codex emits the full stdout+stderr in this field on completion.
605
- const aggregated = typeof item.aggregated_output === 'string' ? item.aggregated_output : '';
606
- this._steps.push({
607
- label,
608
- state: ok ? 'done' : 'failed',
609
- startedAt: Date.now(),
610
- completedAt: Date.now(),
611
- ...(aggregated ? { output: truncateOutput(aggregated) } : {}),
612
- });
613
- return true;
614
- }
615
- if (itemType === 'tool_call' || itemType === 'local_shell_call') {
616
- const name = String(item.tool_name ?? item.name ?? 'tool');
617
- // Tool-call items may carry a `result` or `output` field too.
618
- const result = (typeof item.result === 'string' ? item.result : '') || (typeof item.output === 'string' ? item.output : '');
619
- this._steps.push({
620
- label: truncate(name),
621
- state: 'done',
622
- startedAt: Date.now(),
623
- completedAt: Date.now(),
624
- ...(result ? { output: truncateOutput(result) } : {}),
625
- });
626
- return true;
627
- }
628
- // file_change — codex emits one item per Edit/Write/Delete batch. The audit
629
- // mirror (codex-adapter.ts) already handles these; the timeline-step path was
630
- // dropping them, so real codex file edits were invisible in "watch it work"
631
- // (mock-currency audit 2026-06-02 caught this — the mock emits the real shape).
632
- if (itemType === 'file_change') {
633
- const changes = Array.isArray(item.changes)
634
- ? item.changes
635
- : [];
636
- let changed = false;
637
- for (const ch of changes) {
638
- const path = String(ch.path ?? '');
639
- const kind = String(ch.kind ?? 'update');
640
- const toolName = kind === 'add' ? 'Write' : kind === 'delete' ? 'Delete' : 'Edit';
641
- // Label directly so the path always shows (labelForClaudeToolUse only
642
- // knows Edit/Write/Read — it drops the path for Delete).
643
- this._steps.push({
644
- label: path ? toolName + ' ' + path : toolName,
645
- state: 'done',
646
- startedAt: Date.now(),
647
- completedAt: Date.now(),
648
- });
649
- changed = true;
650
- }
651
- return changed;
652
- }
653
- // mcp_tool_call + web_search — real visible actions the codex-adapter mirrors
654
- // to audit; surface them as timeline steps too (2026-06-02 follow-up, Ega-approved
655
- // class: "watch it work"). Labels mirror codex-adapter conventions.
656
- if (itemType === 'mcp_tool_call') {
657
- const server = String(item.server ?? '');
658
- const tool = String(item.tool ?? 'mcp');
659
- this._steps.push({
660
- label: 'MCP · ' + (server ? server + '.' : '') + tool,
661
- state: 'done',
662
- startedAt: Date.now(),
663
- completedAt: Date.now(),
664
- });
665
- return true;
666
- }
667
- if (itemType === 'web_search') {
668
- const q = String(item.query ?? '').trim().slice(0, 80);
669
- this._steps.push({
670
- label: q ? 'Web search · ' + q : 'Web search',
671
- state: 'done',
672
- startedAt: Date.now(),
673
- completedAt: Date.now(),
674
- });
675
- return true;
676
- }
677
- // agent_message = codex's own narration. Captured as kind:'say' since the
678
- // v1 spec settled the taste call that used to be flagged here (pm-layers
679
- // "just chatter" decision): the room thread is "tool steps woven with the
680
- // agent's plain-English narration" for EVERY vendor, and the Final Report
681
- // engine reuses 'say' steps as the summary — without this, codex always
682
- // degraded to the deterministic fallback.
683
- if (itemType === 'agent_message') {
684
- return this.pushSay(String(item.text ?? ''));
685
- }
686
- return false;
687
- }
688
- }
689
- //# sourceMappingURL=progress-aggregator.js.map