osborn 0.9.174 → 0.9.176

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.
@@ -259,6 +259,12 @@ export declare class ClaudeLLM extends llm.LLM {
259
259
  * Public so ClaudeLLMStream can call it via this.#llmRef.spawnReviewer().
260
260
  */
261
261
  spawnReviewer(agentId: string, writerOutput: string, emitter: EventEmitter): Promise<void>;
262
+ /**
263
+ * Dispatcher v1 — auto-spawn a tester after a writer sub-agent completes,
264
+ * in parallel with spawnReviewer. Runs tests against the diff and emits
265
+ * dispatch_rejected with verdict 'TEST-FAIL' if they fail.
266
+ */
267
+ spawnTester(agentId: string, _writerOutput: string, emitter: EventEmitter): Promise<void>;
262
268
  /**
263
269
  * Dispatcher v1 — research gate: vet a researcher sub-agent's output before
264
270
  * it reaches the main agent. Emits dispatch_rejected with verdict 'NEEDS-MORE'
@@ -1410,6 +1410,95 @@ export class ClaudeLLM extends llm.LLM {
1410
1410
  console.error('[DISPATCH] reviewer spawn failed (non-fatal):', err);
1411
1411
  }
1412
1412
  }
1413
+ /**
1414
+ * Dispatcher v1 — auto-spawn a tester after a writer sub-agent completes,
1415
+ * in parallel with spawnReviewer. Runs tests against the diff and emits
1416
+ * dispatch_rejected with verdict 'TEST-FAIL' if they fail.
1417
+ */
1418
+ async spawnTester(agentId, _writerOutput, emitter) {
1419
+ // Use a scoped key so reviewer's dedup guard (keyed on raw agentId) doesn't block us.
1420
+ const testerKey = `${agentId}:tester`;
1421
+ if (this.#dispatchedFor.has(testerKey))
1422
+ return;
1423
+ this.#dispatchedFor.add(testerKey);
1424
+ try {
1425
+ let gitDiff = '';
1426
+ try {
1427
+ const { execSync } = await import('child_process');
1428
+ const diffStat = execSync('git diff HEAD~1 HEAD --stat 2>/dev/null', { cwd: this.#opts.workingDirectory, timeout: 5000 }).toString().trim();
1429
+ const diff = execSync('git diff HEAD~1 HEAD 2>/dev/null', { cwd: this.#opts.workingDirectory, timeout: 5000 }).toString().trim();
1430
+ 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>` : '';
1431
+ }
1432
+ catch {
1433
+ // non-fatal
1434
+ }
1435
+ const prompt = [
1436
+ 'Run tests and verify that the change below did not introduce regressions.',
1437
+ 'Use the git diff as your scope — run the full existing test suite to establish a baseline,',
1438
+ 'then target tests at the specific changed files/functions.',
1439
+ 'If no test suite exists, run the build or execute the changed code directly to confirm it works.',
1440
+ 'End your reply with exactly `RESULT: PASS` or `RESULT: FAIL` followed by a brief summary.',
1441
+ gitDiff,
1442
+ ].join('\n');
1443
+ const testerOptions = {
1444
+ cwd: this.#opts.workingDirectory,
1445
+ permissionMode: 'default',
1446
+ systemPrompt: NAMED_AGENTS.tester.prompt,
1447
+ allowedTools: ['Read', 'Glob', 'Grep', 'Bash', 'Write', 'Edit'],
1448
+ hooks: {
1449
+ PreToolUse: [{
1450
+ matcher: '.*',
1451
+ hooks: [async (input) => {
1452
+ const toolName = input?.tool_name || 'unknown';
1453
+ const toolInput = input?.tool_input || {};
1454
+ emitter.emit('tool_use', { name: toolName, input: toolInput, agentRole: 'tester' });
1455
+ return {};
1456
+ }],
1457
+ }],
1458
+ PostToolUse: [{
1459
+ matcher: '.*',
1460
+ hooks: [async (input) => {
1461
+ const toolName = input?.tool_name || 'unknown';
1462
+ const toolInput = input?.tool_input || {};
1463
+ const toolResponse = input?.tool_response;
1464
+ emitter.emit('tool_result', { name: toolName, input: toolInput, response: toolResponse, agentRole: 'tester' });
1465
+ return {};
1466
+ }],
1467
+ }],
1468
+ },
1469
+ };
1470
+ console.log(`[DISPATCH] spawning tester for agentId=${agentId.slice(0, 8)}`);
1471
+ const testerQuery = query({ prompt, options: testerOptions });
1472
+ this.#activeQueries.add(testerQuery);
1473
+ this.#activeQueriesById.set(testerKey, testerQuery);
1474
+ let testerText = '';
1475
+ try {
1476
+ for await (const msg of testerQuery) {
1477
+ const m = msg;
1478
+ if (m.type === 'result' && m.result) {
1479
+ testerText = String(m.result);
1480
+ }
1481
+ }
1482
+ }
1483
+ finally {
1484
+ this.#activeQueries.delete(testerQuery);
1485
+ this.#activeQueriesById.delete(testerKey);
1486
+ }
1487
+ const resultMatch = testerText.match(/RESULT:\s*(PASS|FAIL)/i);
1488
+ const result = resultMatch ? resultMatch[1].toUpperCase() : null;
1489
+ if (result === 'FAIL') {
1490
+ console.log(`[DISPATCH] tester FAIL for agentId=${agentId.slice(0, 8)}`);
1491
+ statusManager.upsertDispatch(agentId, { dispatchState: 'rejected', artifact: testerText });
1492
+ emitter.emit('dispatch_rejected', { tuid: agentId, verdict: 'TEST-FAIL', review: testerText });
1493
+ }
1494
+ else {
1495
+ console.log(`[DISPATCH] tester PASS for agentId=${agentId.slice(0, 8)}`);
1496
+ }
1497
+ }
1498
+ catch (err) {
1499
+ console.error('[DISPATCH] tester spawn failed (non-fatal):', err);
1500
+ }
1501
+ }
1413
1502
  /**
1414
1503
  * Dispatcher v1 — research gate: vet a researcher sub-agent's output before
1415
1504
  * it reaches the main agent. Emits dispatch_rejected with verdict 'NEEDS-MORE'
@@ -1996,11 +2085,12 @@ class ClaudeLLMStream extends llm.LLMStream {
1996
2085
  const aid = input?.agent_id ?? ('sa-' + Date.now());
1997
2086
  statusManager.upsertDispatch(aid, { subagentType: at, dispatchState: 'completed', artifact: msg });
1998
2087
  this.#eventEmitter.emit('task_completed', { agent_type: at, agent_id: aid, last_assistant_message: String(msg).slice(0, 400) });
1999
- // Infinite-loop guard — never re-dispatch the reviewer or reasoner.
2000
- if (at === 'reviewer' || at === 'reasoner')
2088
+ // Infinite-loop guard — never re-dispatch the reviewer, tester, or reasoner.
2089
+ if (at === 'reviewer' || at === 'tester' || at === 'reasoner')
2001
2090
  return {};
2002
2091
  if (at === 'writer' && msg) {
2003
2092
  void this.#llmRef.spawnReviewer(aid, msg, this.#eventEmitter);
2093
+ void this.#llmRef.spawnTester(aid, msg, this.#eventEmitter);
2004
2094
  }
2005
2095
  else if (at === 'researcher' && msg) {
2006
2096
  void this.#llmRef.spawnResearchGate(aid, msg, this.#eventEmitter);
package/dist/voice-io.js CHANGED
@@ -131,8 +131,8 @@ export const DEFAULT_VOICE_IO_CONFIG = {
131
131
  export const DIRECT_MODE_STT = {
132
132
  // provider: 'groq-whisper', model: 'whisper-large-v3-turbo', // Batch — needs VAD
133
133
  // provider: 'openai-whisper', model: 'whisper-1', // Batch — needs VAD
134
- // provider: 'deepgram', model: 'nova-3', language: 'en', // Streaming, silence-based endpointing
135
- provider: 'deepgram-flux', model: 'flux-general-en', language: 'en', // Streaming, ML-based turn detection (requires Deepgram V2 access)
134
+ provider: 'deepgram', model: 'nova-3', language: 'en', // Streaming, silence-based endpointing
135
+ // provider: 'deepgram-flux', model: 'flux-general-en', language: 'en', // Streaming, ML-based turn detection — Flux V2 has silent keepalive timeout bug (~30s silence kills connection)
136
136
  };
137
137
  export const DIRECT_MODE_TTS = {
138
138
  // provider: 'deepgram', model: 'aura-2-asteria-en', // WebSocket-based: handles TTS abort cleanly (no unrecoverable crash on interruption)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.174",
3
+ "version": "0.9.176",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {