osborn 0.9.147 → 0.9.149

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.
@@ -332,9 +332,13 @@ export const NAMED_AGENTS = {
332
332
  'Flag any change that could break existing callers, even if no test currently covers it.',
333
333
  '',
334
334
  '## How to work',
335
+ '0. **Get the diff first (MANDATORY):** Run `git diff HEAD~1 HEAD --name-only` to get the list of',
336
+ ' changed files, then `git diff HEAD~1 HEAD` for the full diff. Build your entire test plan around',
337
+ ' the SPECIFIC files and functions that changed — not a generic sweep.',
335
338
  '1. Identify the correct test / build command from package.json, Makefile, or the task brief.',
336
339
  '2. Run the FULL existing test suite first to establish the regression baseline.',
337
- '3. Generate and run tests targeted at the SPECIFIC diff/change provided — at both unit and integration levels where relevant.',
340
+ '3. Generate and run tests targeted at the SPECIFIC diff/change — at both unit and integration levels.',
341
+ ' Focus on: the changed functions/components, their callers, and any behavior the diff modifies.',
338
342
  '4. Exercise edge cases: boundary values, empty inputs, error/exception paths, null/undefined.',
339
343
  '5. Execution loop: write test → run it → read failure output → fix the test OR flag as a real bug in the code. Do NOT silently paper over a real defect.',
340
344
  '6. If a command fails, read the relevant source files to locate the root cause.',
@@ -440,7 +444,19 @@ export const NAMED_AGENTS = {
440
444
  'You must NEVER write code, config, or source files (.ts, .js, .json, .env, etc.) — the write gate',
441
445
  'enforces this at the system level and will deny any such attempt.',
442
446
  '',
447
+ '## Backward-compatibility mandate',
448
+ 'For every change, explicitly verify:',
449
+ '- No existing exported function signature changed (parameter count/order/type, return type)',
450
+ '- No existing data-channel message schema changed (field names, types, required fields)',
451
+ '- No existing API endpoint behavior changed silently',
452
+ '- No existing UI prop interface changed in a breaking way',
453
+ '- Existing callers and consumers of the changed code still work',
454
+ 'Tag any violation BLOCKER — silent compat breaks are the hardest bugs to catch after the fact.',
455
+ '',
443
456
  '## How to work',
457
+ '0. **Get the diff first (MANDATORY):** Run `git diff HEAD~1 HEAD --stat` then `git diff HEAD~1 HEAD`.',
458
+ ' Build your entire review around what ACTUALLY changed — not the writer\'s narrative alone.',
459
+ ' If the task provides a diff, still verify it matches git history.',
444
460
  '1. Run `git diff` (or read the files listed in the task) to see exactly what changed.',
445
461
  '2. Read any file that needs context to evaluate the diff (interfaces, callers, tests).',
446
462
  '3. Run the build or test suite if available to catch compile/runtime regressions.',
@@ -552,6 +568,10 @@ export class ClaudeLLM extends llm.LLM {
552
568
  // No JSONL replay after the first cold start.
553
569
  #persistentQuery = null;
554
570
  #messageChannel = null;
571
+ // Read-along tracking — per-turn message ID and chunk counter for TTS highlighting
572
+ #currentTurnMessageId = null;
573
+ #currentTurnChunkIndex = 0;
574
+ #currentTurnChunks = [];
555
575
  #backgroundConsumerRunning = false;
556
576
  // Active queries — multiple can be running (SDK queues them internally).
557
577
  // We keep ALL references so interrupt() can stop whatever is currently executing.
@@ -1106,22 +1126,41 @@ export class ClaudeLLM extends llm.LLM {
1106
1126
  }
1107
1127
  // Stream assistant text → tts_say events
1108
1128
  if (msg.type === 'assistant' && msg.message?.content) {
1129
+ // Assign a stable messageId for this turn (first block sets it, rest reuse)
1130
+ if (!this.#currentTurnMessageId) {
1131
+ this.#currentTurnMessageId = crypto.randomUUID();
1132
+ this.#currentTurnChunkIndex = 0;
1133
+ this.#currentTurnChunks = [];
1134
+ }
1135
+ const turnMessageId = this.#currentTurnMessageId;
1109
1136
  for (const block of msg.message.content) {
1110
1137
  if (block.type === 'text' && block.text) {
1111
- callbacks.eventEmitter.emit('assistant_text', { text: block.text });
1138
+ const chunkIndex = this.#currentTurnChunkIndex;
1139
+ callbacks.eventEmitter.emit('assistant_text', { text: block.text, messageId: turnMessageId, chunkIndex });
1112
1140
  const ttsChunk = stripMarkdownForTTS(block.text);
1113
1141
  if (ttsChunk.trim()) {
1142
+ this.#currentTurnChunks.push(ttsChunk);
1143
+ this.#currentTurnChunkIndex++;
1114
1144
  console.log(`🔊 TTS say (${ttsChunk.length} chars): "${ttsChunk}"`);
1115
- callbacks.eventEmitter.emit('tts_say', { text: ttsChunk });
1145
+ callbacks.eventEmitter.emit('tts_say', { text: ttsChunk, messageId: turnMessageId, chunkIndex });
1116
1146
  }
1117
1147
  }
1118
1148
  }
1119
1149
  }
1120
1150
  // Result — marks end of a turn (but we keep consuming for next turn)
1121
1151
  if (msg.type === 'result') {
1152
+ const turnMessageId = this.#currentTurnMessageId;
1153
+ const turnChunks = [...this.#currentTurnChunks];
1122
1154
  if (msg.result) {
1123
- callbacks.eventEmitter.emit('assistant_result', { text: msg.result });
1155
+ callbacks.eventEmitter.emit('assistant_result', { text: msg.result, messageId: turnMessageId });
1124
1156
  }
1157
+ if (turnMessageId && turnChunks.length > 0) {
1158
+ callbacks.eventEmitter.emit('tts_chunks', { messageId: turnMessageId, chunks: turnChunks });
1159
+ }
1160
+ // Reset per-turn state for next turn
1161
+ this.#currentTurnMessageId = null;
1162
+ this.#currentTurnChunkIndex = 0;
1163
+ this.#currentTurnChunks = [];
1125
1164
  console.log('✅ Claude turn complete (persistent session stays alive)');
1126
1165
  }
1127
1166
  }
@@ -1158,13 +1197,26 @@ export class ClaudeLLM extends llm.LLM {
1158
1197
  const idxPathReviewer = (this.#sessionId && this.#opts.workingDirectory)
1159
1198
  ? getIndexPath(this.#sessionId, this.#opts.workingDirectory)
1160
1199
  : null;
1200
+ // Get the actual diff to give reviewer concrete evidence instead of just the narrative
1201
+ let gitDiff = '';
1202
+ try {
1203
+ const { execSync } = await import('child_process');
1204
+ const diffStat = execSync('git diff HEAD~1 HEAD --stat 2>/dev/null', { cwd: this.#opts.workingDirectory, timeout: 5000 }).toString().trim();
1205
+ const diff = execSync('git diff HEAD~1 HEAD 2>/dev/null', { cwd: this.#opts.workingDirectory, timeout: 5000 }).toString().trim();
1206
+ gitDiff = diffStat ? `\n\n<git_diff_stat>\n${diffStat}\n</git_diff_stat>\n\n<git_diff>\n${diff.slice(0, 6000)}\n</git_diff>` : '';
1207
+ }
1208
+ catch {
1209
+ // non-fatal: if git fails, proceed without diff
1210
+ }
1161
1211
  const prompt = [
1162
1212
  'Use the reviewer sub-agent to review this writer output for correctness/spec-adherence/obvious bugs.',
1213
+ 'The git diff is provided below — use it as the authoritative source of what changed.',
1163
1214
  'End your reply with exactly `VERDICT: ACCEPT` or `VERDICT: REJECT`.',
1164
1215
  ...(idxPathReviewer ? [`The session index is at ${idxPathReviewer} — you MUST read it before reviewing.`] : []),
1216
+ gitDiff,
1165
1217
  '',
1166
1218
  '<writer_output>',
1167
- writerOutput.slice(0, 8000),
1219
+ writerOutput.slice(0, 6000),
1168
1220
  '</writer_output>',
1169
1221
  ].join('\n');
1170
1222
  // Do NOT pass agents here — the reviewer must be review-only and must not
@@ -1854,6 +1906,10 @@ class ClaudeLLMStream extends llm.LLMStream {
1854
1906
  // Run Claude Agent SDK query() and stream results
1855
1907
  let hasOutput = false;
1856
1908
  let fullResponse = ''; // Collect full response for frontend
1909
+ // Per-turn read-along tracking (non-skipTTSQueue path)
1910
+ let streamTurnMessageId = null;
1911
+ let streamTurnChunkIndex = 0;
1912
+ let streamTurnChunks = [];
1857
1913
  // DIRECT MODE OPTIMIZATION: When skipTTSQueue is true, we run the Claude query
1858
1914
  // in the background and return from run() immediately. This is critical because:
1859
1915
  //
@@ -1940,20 +1996,29 @@ class ClaudeLLMStream extends llm.LLMStream {
1940
1996
  if (sdkRequestId) {
1941
1997
  this.#eventEmitter.emit('query_request_id', { requestId: sdkRequestId });
1942
1998
  }
1999
+ // Assign a stable messageId for this turn (first block sets it, rest reuse)
2000
+ if (!streamTurnMessageId) {
2001
+ streamTurnMessageId = crypto.randomUUID();
2002
+ streamTurnChunkIndex = 0;
2003
+ streamTurnChunks = [];
2004
+ }
1943
2005
  for (const block of message.message.content) {
1944
2006
  if (block.type === 'text' && block.text) {
1945
2007
  hasOutput = true;
1946
2008
  const rawText = block.text;
2009
+ const chunkIndex = streamTurnChunkIndex;
1947
2010
  // Emit RAW text to frontend (for chat bubbles with full formatting)
1948
- this.#eventEmitter.emit('assistant_text', { text: rawText });
2011
+ this.#eventEmitter.emit('assistant_text', { text: rawText, messageId: streamTurnMessageId, chunkIndex });
1949
2012
  // Strip markdown for clean speech
1950
2013
  const ttsChunk = stripMarkdownForTTS(rawText);
1951
2014
  if (ttsChunk.trim()) {
2015
+ streamTurnChunks.push(ttsChunk);
2016
+ streamTurnChunkIndex++;
1952
2017
  if (this.#opts.skipTTSQueue) {
1953
2018
  // Direct mode: emit event for session.say() — bypasses LiveKit's
1954
2019
  // BufferedTokenStream which causes stuck/delayed/out-of-order audio
1955
2020
  console.log(`🔊 TTS say (${ttsChunk.length} chars): "${ttsChunk}"`);
1956
- this.#eventEmitter.emit('tts_say', { text: ttsChunk });
2021
+ this.#eventEmitter.emit('tts_say', { text: ttsChunk, messageId: streamTurnMessageId, chunkIndex });
1957
2022
  }
1958
2023
  else {
1959
2024
  // Realtime mode: use LLM stream queue (framework handles TTS)
@@ -1971,7 +2036,15 @@ class ClaudeLLMStream extends llm.LLMStream {
1971
2036
  if (message.type === 'result' && message.result) {
1972
2037
  const rawResult = message.result;
1973
2038
  // Emit RAW result to frontend
1974
- this.#eventEmitter.emit('assistant_result', { text: rawResult });
2039
+ this.#eventEmitter.emit('assistant_result', { text: rawResult, messageId: streamTurnMessageId });
2040
+ // Emit ordered chunk list for frontend read-along
2041
+ if (streamTurnMessageId && streamTurnChunks.length > 0) {
2042
+ this.#eventEmitter.emit('tts_chunks', { messageId: streamTurnMessageId, chunks: streamTurnChunks });
2043
+ }
2044
+ // Reset per-turn state
2045
+ streamTurnMessageId = null;
2046
+ streamTurnChunkIndex = 0;
2047
+ streamTurnChunks = [];
1975
2048
  if (!hasOutput) {
1976
2049
  hasOutput = true;
1977
2050
  const ttsText = stripMarkdownForTTS(rawResult);
package/dist/index.js CHANGED
@@ -2814,6 +2814,8 @@ async function main() {
2814
2814
  text: data.text,
2815
2815
  isStreaming: true,
2816
2816
  agentRole: 'direct',
2817
+ messageId: data.messageId,
2818
+ chunkIndex: data.chunkIndex,
2817
2819
  });
2818
2820
  });
2819
2821
  // Wire up Claude final result - RAW result goes to frontend
@@ -2825,6 +2827,15 @@ async function main() {
2825
2827
  isStreaming: false,
2826
2828
  isFinal: true,
2827
2829
  agentRole: 'direct',
2830
+ messageId: data.messageId,
2831
+ });
2832
+ });
2833
+ // Wire up ordered TTS chunk list — emitted once per turn for read-along
2834
+ directLLM.events.on('tts_chunks', (data) => {
2835
+ sendToFrontend({
2836
+ type: 'tts_chunks',
2837
+ messageId: data.messageId,
2838
+ chunks: data.chunks,
2828
2839
  });
2829
2840
  });
2830
2841
  // Wire up permission requests - sends to frontend for user approval
@@ -3001,6 +3012,15 @@ async function main() {
3001
3012
  playbackStartedAt = Date.now();
3002
3013
  console.log(`🔊 [${sayId}] audio first frame out (playbackStarted)`);
3003
3014
  audioOutputRef.off('playbackStarted', onPlaybackStarted);
3015
+ // Notify frontend that this TTS chunk is now audibly playing
3016
+ if (data.messageId != null && data.chunkIndex != null) {
3017
+ sendToFrontend({
3018
+ type: 'tts_chunk_playing',
3019
+ messageId: data.messageId,
3020
+ chunkIndex: data.chunkIndex,
3021
+ text: data.text,
3022
+ });
3023
+ }
3004
3024
  };
3005
3025
  audioOutputRef.on('playbackStarted', onPlaybackStarted);
3006
3026
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.147",
3
+ "version": "0.9.149",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {