osborn 0.9.173 → 0.9.175

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/index.js CHANGED
@@ -6088,7 +6088,10 @@ async function main() {
6088
6088
  }
6089
6089
  // Feature B — per-flow stop (does NOT affect other running flows)
6090
6090
  else if (data.type === 'stop_dispatch' && currentLLM) {
6091
- currentLLM.stopAgent?.(String(data.agentId));
6091
+ const agentId = String(data.agentId);
6092
+ currentLLM.stopAgent?.(agentId);
6093
+ // Always confirm so frontend clears the card (covers ghost/stale flows too)
6094
+ await sendToFrontend({ type: 'agent_stopped', agent_id: agentId });
6092
6095
  }
6093
6096
  else if (data.type === 'join_meeting') {
6094
6097
  const meetingUrl = data.url;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.173",
3
+ "version": "0.9.175",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {