osborn 0.9.219 → 0.9.221

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.
@@ -46,10 +46,6 @@ export declare const NAMED_AGENTS: {
46
46
  description: string;
47
47
  tools: string[];
48
48
  grounded: boolean;
49
- coordination: {
50
- then: string[];
51
- startNote: string;
52
- };
53
49
  model: string;
54
50
  prompt: string;
55
51
  };
@@ -63,28 +59,13 @@ export declare const NAMED_AGENTS: {
63
59
  description: string;
64
60
  tools: string[];
65
61
  grounded: boolean;
66
- policy: {
67
- write: "anywhere";
68
- };
69
- reminder: string;
70
- coordination: {
71
- then: string[];
72
- mode: "parallel";
73
- startNote: string;
74
- };
62
+ criticalSystemReminder_EXPERIMENTAL: string;
75
63
  model: string;
76
64
  prompt: string;
77
65
  };
78
66
  tester: {
79
67
  description: string;
80
68
  tools: string[];
81
- policy: {
82
- write: {
83
- extensions: RegExp;
84
- label: string;
85
- matchBasename: boolean;
86
- };
87
- };
88
69
  model: string;
89
70
  prompt: string;
90
71
  };
@@ -98,12 +79,6 @@ export declare const NAMED_AGENTS: {
98
79
  reviewer: {
99
80
  description: string;
100
81
  tools: string[];
101
- policy: {
102
- write: {
103
- extensions: RegExp;
104
- label: string;
105
- };
106
- };
107
82
  model: string;
108
83
  prompt: string;
109
84
  };
@@ -134,6 +109,21 @@ export declare function applyTurbo(agents: Record<string, any>, turbo: boolean):
134
109
  * Adversarial agents (reviewer/tester) leave `grounded` unset → untouched, stay blind.
135
110
  */
136
111
  export declare function applyGrounding(agents: Record<string, any>, sessionId: string | null, workingDir: string | undefined): Record<string, any>;
112
+ /**
113
+ * Decide a Write/Edit/MultiEdit for the acting agent (null = main orchestrator).
114
+ * Pure lookup against WRITE_RULES. Returns:
115
+ * 'allow' → fall through (→ canUseTool workspace auto-approve, or {}).
116
+ * 'defer' → PreToolUse 'ask' (canUseTool decides).
117
+ * 'deny' → hard block with reason.
118
+ */
119
+ export declare function decideWrite(agentType: string | null, filePath: string): {
120
+ decision: 'allow' | 'defer' | 'deny';
121
+ reason?: string;
122
+ };
123
+ export declare const VERIFIER_CHAIN: Record<string, {
124
+ then: string[];
125
+ mode?: 'parallel' | 'sequential';
126
+ }>;
137
127
  /**
138
128
  * Claude LLM - Wraps Claude Agent SDK for LiveKit
139
129
  * Research mode: reads anything, writes only to session workspace
@@ -86,13 +86,13 @@ async function buildRecallInjection(sessionId, workingDir, prompt) {
86
86
  return ''; // recall is best-effort — never break the turn
87
87
  }
88
88
  }
89
- // ≤3 direct tool call budget per turn. Reset on every UserPromptSubmit (new user message).
89
+ // ≤5 direct tool call budget per turn. Reset on every UserPromptSubmit (new user message).
90
90
  // Enforced mechanically in PreToolUse — the model CANNOT exceed this regardless of JSONL history.
91
91
  // Task/Agent delegations are exempt (delegation is what we WANT). Sub-agent tool calls
92
92
  // (agent_type !== null) are exempt (they're inside a delegation). Only the main orchestrator
93
93
  // agent's direct tool calls count against the budget.
94
94
  let turnToolCallCount = 0;
95
- const TOOL_CALL_BUDGET = 3;
95
+ const TOOL_CALL_BUDGET = 5;
96
96
  /**
97
97
  * Strip markdown formatting for TTS (text-to-speech)
98
98
  * Removes **bold**, ##headers, ```code```, etc. so TTS doesn't read them literally
@@ -366,12 +366,6 @@ export const NAMED_AGENTS = {
366
366
  ].join(' '),
367
367
  tools: ['Read', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch', 'Task'],
368
368
  grounded: true, // applyGrounding() injects the osborn-recall command + ensures Bash
369
- // Declarative flow. == today: after the researcher finishes, a reasoner-based
370
- // research gate judges completeness and may send it back for more.
371
- coordination: {
372
- then: ['reasoner'],
373
- startNote: 'When you finish, a reasoner-based gate judges whether your findings are COMPLETE and well-sourced against the task; thin or unsourced findings get sent back to you. Cite file paths + line numbers and explicitly note what you looked for but did NOT find.',
374
- },
375
369
  model: 'sonnet',
376
370
  prompt: [
377
371
  'You are Osborn\'s research agent. Your job is information gathering — thorough, structured, factual.',
@@ -408,6 +402,7 @@ export const NAMED_AGENTS = {
408
402
  'Return findings to the orchestrator — never directly to the user.',
409
403
  'Run several researchers in parallel when there are independent threads to investigate.',
410
404
  'Hand off back to the orchestrator; it decides whether to invoke planner or writer next.',
405
+ 'After you return, a reasoner-based gate judges whether your findings are COMPLETE and well-sourced; thin or unsourced findings get sent back to you — so cite paths/line numbers and state what you looked for but did NOT find.',
411
406
  ].join('\n'),
412
407
  },
413
408
  reasoner: {
@@ -474,17 +469,8 @@ export const NAMED_AGENTS = {
474
469
  ].join(' '),
475
470
  tools: ['Read', 'Write', 'Edit', 'MultiEdit', 'Bash', 'Glob', 'Grep', 'NotebookRead', 'NotebookEdit'],
476
471
  grounded: true, // applyGrounding() injects the osborn-recall command
477
- policy: { write: 'anywhere' }, // sole writer — unrestricted; funnels through canUseTool
478
- // Soft behavior via the composable `reminder` seam → SDK criticalSystemReminder_EXPERIMENTAL.
479
- // Pinned into the writer's system prompt as a hard-to-ignore reminder.
480
- reminder: 'BEFORE you write implementation code: make sure a test exists for the behavior you are about to change. If none exists, say so explicitly in your report so the tester can cover it — the tester is the agent that writes tests. NEVER weaken, skip, or delete a test to make your change pass.',
481
- // Declarative flow (drives SubagentStop dispatch + SubagentStart injection).
482
- // == today: after the writer finishes, reviewer AND tester run in parallel.
483
- coordination: {
484
- then: ['reviewer', 'tester'],
485
- mode: 'parallel',
486
- startNote: 'When you finish, your change is automatically verified in parallel: a reviewer checks correctness against the git diff, and a tester runs the suite. Make the change review-ready and leave the tree in a runnable state — do not skip cleanup expecting a second pass.',
487
- },
472
+ // SDK-native soft-behavior field — pinned into the writer's system prompt.
473
+ criticalSystemReminder_EXPERIMENTAL: 'BEFORE you write implementation code: make sure a test exists for the behavior you are about to change. If none exists, say so explicitly in your report so the tester can cover it — the tester is the agent that writes tests. NEVER weaken, skip, or delete a test to make your change pass.',
488
474
  model: 'opus',
489
475
  prompt: [
490
476
  'You are Osborn\'s writer agent. You execute file changes with a verify-first approach.',
@@ -527,6 +513,7 @@ export const NAMED_AGENTS = {
527
513
  'Invoked AFTER the planner produces a written plan — the writer is the SOLE agent that edits files.',
528
514
  'Do not invoke writer until a plan exists for any multi-step change.',
529
515
  'When the writer returns, the orchestrator invokes tester AND reviewer in parallel before surfacing results.',
516
+ 'Because that verification runs automatically after you finish, make the change review-ready and leave the tree in a runnable state — do not skip cleanup expecting a second pass.',
530
517
  ].join('\n'),
531
518
  },
532
519
  tester: {
@@ -536,9 +523,7 @@ export const NAMED_AGENTS = {
536
523
  'Returns structured pass/fail results with exact output — does NOT edit files.',
537
524
  'NOT GROUNDED: no session index access — runs tests with fresh eyes, adversarial validation.',
538
525
  ].join(' '),
539
- tools: ['Bash', 'Read', 'Glob', 'Grep', 'Write', 'Edit'],
540
- // Fail-closed: may ONLY write test files (basename .test/.spec.[jt]sx?).
541
- policy: { write: { extensions: /\.(test|spec)\.[jt]sx?$/, label: 'test', matchBasename: true } },
526
+ tools: ['Bash', 'Read', 'Glob', 'Grep', 'Write', 'Edit'], // write-gate: test files only (WRITE_RULES.tester)
542
527
  model: 'sonnet',
543
528
  prompt: [
544
529
  'You are Osborn\'s tester agent. Your job is running tests and builds, then reporting results.',
@@ -687,9 +672,7 @@ export const NAMED_AGENTS = {
687
672
  'and returns an ACCEPT or REJECT verdict with specific, actionable feedback. May write documentation files (.md etc.) only.',
688
673
  'NOT GROUNDED: no session index access — reviews with fresh eyes for unbiased adversarial check.',
689
674
  ].join(' '),
690
- tools: ['Read', 'Glob', 'Grep', 'Bash', 'Write', 'Edit'],
691
- // Fail-closed: may ONLY write documentation files.
692
- policy: { write: { extensions: /\.(md|markdown|mdx|txt|rst|adoc)$/i, label: 'documentation' } },
675
+ tools: ['Read', 'Glob', 'Grep', 'Bash', 'Write', 'Edit'], // write-gate: docs only (WRITE_RULES.reviewer)
693
676
  model: 'sonnet',
694
677
  prompt: [
695
678
  'You are Osborn\'s reviewer agent. You are the VERIFY step in a generator-verifier loop.',
@@ -857,64 +840,53 @@ export function applyGrounding(agents, sessionId, workingDir) {
857
840
  }
858
841
  return out;
859
842
  }
860
- const DEFAULT_WRITE_POLICY = 'workspace';
843
+ const WRITE_RULES = {
844
+ writer: 'anywhere',
845
+ tester: { extensions: /\.(test|spec)\.[jt]sx?$/, label: 'test', matchBasename: true },
846
+ reviewer: { extensions: /\.(md|markdown|mdx|txt|rst|adoc)$/i, label: 'documentation' },
847
+ };
861
848
  /** Path is inside the per-session sandbox workspace. */
862
849
  function isWorkspacePath(filePath) {
863
850
  return !!filePath && (filePath.includes('/osb/') ||
864
851
  filePath.includes('.osborn/sessions/') ||
865
852
  filePath.includes('.osborn/research/'));
866
853
  }
867
- /** Effective write policy for the acting agent (null agentType = main orchestrator). */
868
- function resolveWritePolicy(agentType, roster) {
869
- const def = agentType ? roster?.[agentType] : null;
870
- return def?.policy?.write ?? DEFAULT_WRITE_POLICY;
871
- }
872
854
  /**
873
- * Decide a Write/Edit/MultiEdit against a policy. Pure — no side effects.
874
- * 'allow' → let it fall through (→ canUseTool workspace auto-approve, or {}).
875
- * 'defer' → PreToolUse returns permissionDecision:'ask' (canUseTool decides).
876
- * 'deny' → hard block with reason.
855
+ * Decide a Write/Edit/MultiEdit for the acting agent (null = main orchestrator).
856
+ * Pure lookup against WRITE_RULES. Returns:
857
+ * 'allow' → fall through (→ canUseTool workspace auto-approve, or {}).
858
+ * 'defer' → PreToolUse 'ask' (canUseTool decides).
859
+ * 'deny' → hard block with reason.
877
860
  */
878
- function decideWrite(policy, filePath) {
879
- if (policy === 'anywhere')
861
+ export function decideWrite(agentType, filePath) {
862
+ const rule = (agentType && WRITE_RULES[agentType]) || 'workspace';
863
+ if (rule === 'anywhere')
880
864
  return { decision: 'defer' };
881
- if (policy === 'workspace') {
865
+ if (rule === 'workspace') {
882
866
  if (filePath && !isWorkspacePath(filePath)) {
883
867
  return { decision: 'deny', reason: 'Research mode: writes restricted to session workspace.' };
884
868
  }
885
869
  return { decision: 'allow' };
886
870
  }
887
871
  // Extension whitelist — fail closed (empty/unknown path denied).
888
- const target = policy.matchBasename ? (filePath ? basename(resolve(filePath)) : '') : filePath;
889
- if (!filePath || !policy.extensions.test(target)) {
872
+ const target = rule.matchBasename ? (filePath ? basename(resolve(filePath)) : '') : filePath;
873
+ if (!filePath || !rule.extensions.test(target)) {
890
874
  const reason = filePath
891
- ? `Write denied: ${filePath} is not a ${policy.label} file. This agent may only write ${policy.label} files.`
875
+ ? `Write denied: ${filePath} is not a ${rule.label} file. This agent may only write ${rule.label} files.`
892
876
  : 'Write denied: could not determine target file path. Failing closed.';
893
877
  return { decision: 'deny', reason };
894
878
  }
895
879
  return { decision: 'defer' };
896
880
  }
897
- /** Coordination config for the acting/target agent (null agentType = main). */
898
- function coordinationFor(agentType, roster) {
899
- const def = agentType ? roster?.[agentType] : null;
900
- return def?.coordination ?? null;
901
- }
902
- /**
903
- * Strip/map behavior meta-fields so the roster is a clean AgentDefinition set
904
- * for the SDK: drop `policy` (write-gate) and `coordination` (hook-driven),
905
- * and map `reminder` → criticalSystemReminder_EXPERIMENTAL. NEVER mutates input.
906
- */
907
- function finalizeRoster(agents) {
908
- const out = {};
909
- for (const [name, agent] of Object.entries(agents)) {
910
- const { policy, reminder, coordination, ...rest } = agent;
911
- if (reminder && !rest.criticalSystemReminder_EXPERIMENTAL) {
912
- rest.criticalSystemReminder_EXPERIMENTAL = reminder;
913
- }
914
- out[name] = rest;
915
- }
916
- return out;
917
- }
881
+ // ── Verifier chaining ───────────────────────────────────────────────────────
882
+ // A plain name→verifiers lookup read by the SubagentStop hook. Same wiring the
883
+ // old hardcoded branches had (writer→reviewer+tester, researcher→reasoner gate),
884
+ // gathered in one visible place. Names resolve to spawnReviewer / spawnTester /
885
+ // spawnResearchGate. Any agent not listed chains to nothing.
886
+ export const VERIFIER_CHAIN = {
887
+ writer: { then: ['reviewer', 'tester'], mode: 'parallel' },
888
+ researcher: { then: ['reasoner'] },
889
+ };
918
890
  const RESEARCH_TOOLS = [
919
891
  'Read', 'Write', 'Edit', 'Glob', 'Grep',
920
892
  'Bash', 'WebSearch', 'WebFetch',
@@ -1732,6 +1704,17 @@ export class ClaudeLLM extends llm.LLM {
1732
1704
  return {};
1733
1705
  }],
1734
1706
  }],
1707
+ // Self-review gate — block ONCE (stop_hook_active-guarded) so the reviewer
1708
+ // re-checks its own verdict before returning, instead of stopping on a
1709
+ // first-pass judgment.
1710
+ Stop: [{
1711
+ matcher: '.*',
1712
+ hooks: [async (input) => {
1713
+ if (input?.stop_hook_active)
1714
+ return {};
1715
+ return { decision: 'block', reason: 'Before you finalize: re-read your findings against the diff once. Confirm each BLOCKER/MAJOR is real and reproducible (not a style nit or a false positive), and that your verdict matches the severity of what you actually found. Then end with exactly `VERDICT: ACCEPT` or `VERDICT: REJECT`.' };
1716
+ }],
1717
+ }],
1735
1718
  },
1736
1719
  };
1737
1720
  console.log(`[DISPATCH] spawning reviewer for agentId=${agentId.slice(0, 8)}`);
@@ -1821,6 +1804,16 @@ export class ClaudeLLM extends llm.LLM {
1821
1804
  return {};
1822
1805
  }],
1823
1806
  }],
1807
+ // Self-review gate — block ONCE (stop_hook_active-guarded) so the tester
1808
+ // re-checks its own conclusion before returning.
1809
+ Stop: [{
1810
+ matcher: '.*',
1811
+ hooks: [async (input) => {
1812
+ if (input?.stop_hook_active)
1813
+ return {};
1814
+ return { decision: 'block', reason: 'Before you finalize: re-check your conclusion once. Did you actually RUN the commands (not assume the outcome)? Does your result match the real output, and did you distinguish a genuine regression from a flaky/environment failure? Then end with exactly `RESULT: PASS` or `RESULT: FAIL` followed by a brief summary.' };
1815
+ }],
1816
+ }],
1824
1817
  },
1825
1818
  };
1826
1819
  console.log(`[DISPATCH] spawning tester for agentId=${agentId.slice(0, 8)}`);
@@ -1907,6 +1900,16 @@ export class ClaudeLLM extends llm.LLM {
1907
1900
  return {};
1908
1901
  }],
1909
1902
  }],
1903
+ // Self-review gate — block ONCE (stop_hook_active-guarded) so the gate
1904
+ // re-checks its own PASS/NEEDS-MORE call before returning.
1905
+ Stop: [{
1906
+ matcher: '.*',
1907
+ hooks: [async (input) => {
1908
+ if (input?.stop_hook_active)
1909
+ return {};
1910
+ return { decision: 'block', reason: 'Before you finalize: re-check your gate decision once. Is the research genuinely complete and well-sourced for the original question — not passing thin work, nor failing solid work? Then end with exactly `GATE: PASS` or `GATE: NEEDS-MORE`.' };
1911
+ }],
1912
+ }],
1910
1913
  },
1911
1914
  };
1912
1915
  console.log(`[DISPATCH] spawning research-gate for agentId=${agentId.slice(0, 8)}`);
@@ -2034,10 +2037,6 @@ class ClaudeLLMStream extends llm.LLMStream {
2034
2037
  ? getSessionWorkspace(this.#opts.workingDirectory, sessionId)
2035
2038
  : null;
2036
2039
  const allowedTools = this.#opts.allowedTools || [];
2037
- // Roster the PreToolUse write-gate reads policy from — the PRE-strip view
2038
- // (retains `policy`), so DB-backed custom agents (set_agents) get their
2039
- // write policy enforced too. The SDK receives the finalized copy (agents:).
2040
- const enforcementRoster = this.#opts.agents ?? NAMED_AGENTS;
2041
2040
  const sdkOptions = {
2042
2041
  cwd: this.#opts.workingDirectory,
2043
2042
  permissionMode: this.#opts.permissionMode,
@@ -2155,35 +2154,22 @@ class ClaudeLLMStream extends llm.LLMStream {
2155
2154
  }
2156
2155
  console.log(`🔧 Tool call ${turnToolCallCount}/${TOOL_CALL_BUDGET}: ${toolName}`);
2157
2156
  }
2158
- // Delegation-point injection — when the hub spawns a sub-agent, add
2159
- // that agent's discretionary delegationNote to the hub's context
2160
- // (e.g. "add a tester for high-risk edits"). Empty by default → inert.
2161
- if (toolName === 'Task') {
2162
- const targetType = String(toolInput?.subagent_type || '');
2163
- const note = coordinationFor(targetType || null, enforcementRoster)?.delegationNote;
2164
- if (note) {
2165
- this.#eventEmitter.emit('tool_use', { name: toolName, input: toolInput, agentRole: agentType || 'main' });
2166
- console.log(`🤝 Delegation note → subagent_type=${targetType}`);
2167
- return { hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: note } };
2168
- }
2169
- }
2170
- // Write/Edit/MultiEdit access control — DATA-DRIVEN by each agent's
2171
- // declarative policy.write (see AgentWritePolicy). Replaces the old
2172
- // hardcoded per-role branches; DB-backed custom agents get enforced too.
2157
+ // Write/Edit/MultiEdit access control — plain WRITE_RULES lookup
2158
+ // keyed by agent name (see decideWrite). Same behavior the old
2159
+ // hardcoded per-role branches had, now in one visible place.
2173
2160
  if (toolName === 'Write' || toolName === 'Edit' || toolName === 'MultiEdit') {
2174
2161
  const filePath = String(toolInput.file_path || '');
2175
- const policy = resolveWritePolicy(agentType, enforcementRoster);
2176
- const { decision, reason } = decideWrite(policy, filePath);
2177
- console.log(`🔎 Write gate: agent=${agentType ?? 'main'} policy=${JSON.stringify(policy)} path="${filePath || '(none)'}" → ${decision}`);
2162
+ const { decision, reason } = decideWrite(agentType, filePath);
2163
+ console.log(`🔎 Write gate: agent=${agentType ?? 'main'} path="${filePath || '(none)'}" → ${decision}`);
2178
2164
  if (decision === 'deny') {
2179
2165
  this.#eventEmitter.emit('tool_blocked', { name: toolName, reason });
2180
2166
  return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny' }, reason };
2181
2167
  }
2182
2168
  if (decision === 'defer') {
2183
- // Writer (write:'anywhere') surfaces its write in the live trace
2184
- // before the permission dialog; adversarial agents (reviewer/tester)
2185
- // do NOT emit here — parity with the prior hardcoded branches.
2186
- if (policy === 'anywhere') {
2169
+ // Writer ('anywhere') surfaces its write in the live trace before
2170
+ // the permission dialog; adversarial agents (reviewer/tester) do
2171
+ // NOT emit here — parity with the prior hardcoded branches.
2172
+ if (agentType === 'writer') {
2187
2173
  this.#eventEmitter.emit('tool_use', { name: toolName, input: toolInput, agentRole: agentType || 'main' });
2188
2174
  }
2189
2175
  return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'ask' } };
@@ -2531,17 +2517,32 @@ class ClaudeLLMStream extends llm.LLMStream {
2531
2517
  return {};
2532
2518
  }]
2533
2519
  }],
2520
+ // Idle-agent guard / end-of-run self-review. When the main agent tries
2521
+ // to end its turn while background work is still in flight, block ONCE
2522
+ // (stop_hook_active guards against a loop) and force one more turn so it
2523
+ // either waits + synthesizes, or explicitly tells the user what's still
2524
+ // running — never goes silent leaving dispatched work dangling.
2525
+ Stop: [{
2526
+ matcher: '.*',
2527
+ hooks: [async (input) => {
2528
+ const inFlight = Array.isArray(input?.background_tasks) ? input.background_tasks : [];
2529
+ if (input?.stop_hook_active || inFlight.length === 0)
2530
+ return {};
2531
+ const labels = inFlight
2532
+ .map((t) => t?.agent_type || t?.name || t?.command || t?.type || 'task')
2533
+ .slice(0, 6);
2534
+ console.log(`🛑 Stop gate: ${inFlight.length} background task(s) in flight → blocking once for self-review`);
2535
+ return {
2536
+ decision: 'block',
2537
+ reason: `Before you end your turn: ${inFlight.length} background task(s) are still running (${labels.join(', ')}). Do NOT go idle. Either wait for them and synthesize their results into your answer, or explicitly tell the user what is still running and what you will do when it finishes.`,
2538
+ };
2539
+ }]
2540
+ }],
2534
2541
  SubagentStart: [{
2535
2542
  matcher: '.*',
2536
2543
  hooks: [async (input) => {
2537
2544
  console.log('[LIFECYCLE-PROBE] SubagentStart', JSON.stringify(input));
2538
2545
  this.#eventEmitter.emit('agent_started', { agent_type: input?.agent_type, agent_id: input?.agent_id });
2539
- // Inject the agent's declarative startNote (who it is paired with) —
2540
- // reliable, always-seen at boot regardless of what the hub relayed.
2541
- const startNote = coordinationFor(input?.agent_type ?? null, enforcementRoster)?.startNote;
2542
- if (startNote) {
2543
- return { hookSpecificOutput: { hookEventName: 'SubagentStart', additionalContext: startNote } };
2544
- }
2545
2546
  return {};
2546
2547
  }]
2547
2548
  }],
@@ -2555,14 +2556,13 @@ class ClaudeLLMStream extends llm.LLMStream {
2555
2556
  statusManager.upsertDispatch(aid, { subagentType: at, dispatchState: 'completed', artifact: msg });
2556
2557
  this.#eventEmitter.emit('task_completed', { agent_type: at, agent_id: aid, last_assistant_message: String(msg).slice(0, 400) });
2557
2558
  // Infinite-loop guard — verifiers never re-dispatch (they carry no
2558
- // coordination.then anyway; this is defense-in-depth against a
2559
- // DB-backed agent accidentally arming a loop).
2559
+ // VERIFIER_CHAIN entry anyway; defense-in-depth).
2560
2560
  if (at === 'reviewer' || at === 'tester' || at === 'reasoner')
2561
2561
  return {};
2562
- // Declarative verifier chaining — driven by the finishing agent's
2563
- // coordination.then (replaces the hardcoded writer/researcher branches).
2564
- const coord = coordinationFor(at ?? null, enforcementRoster);
2565
- if (coord?.then?.length && msg) {
2562
+ // Verifier chaining — driven by the plain VERIFIER_CHAIN lookup
2563
+ // (replaces the hardcoded writer/researcher branches).
2564
+ const chain = at ? VERIFIER_CHAIN[at] : undefined;
2565
+ if (chain?.then?.length && msg) {
2566
2566
  const spawn = (role) => {
2567
2567
  if (role === 'reviewer')
2568
2568
  return this.#llmRef.spawnReviewer(aid, msg, this.#eventEmitter);
@@ -2570,16 +2570,16 @@ class ClaudeLLMStream extends llm.LLMStream {
2570
2570
  return this.#llmRef.spawnTester(aid, msg, this.#eventEmitter);
2571
2571
  if (role === 'reasoner' || role === 'gate')
2572
2572
  return this.#llmRef.spawnResearchGate(aid, msg, this.#eventEmitter);
2573
- console.warn(`[DISPATCH] unknown coordination target '${role}' for ${at} — skipped`);
2573
+ console.warn(`[DISPATCH] unknown chain target '${role}' for ${at} — skipped`);
2574
2574
  return Promise.resolve();
2575
2575
  };
2576
- if (coord.mode === 'sequential') {
2576
+ if (chain.mode === 'sequential') {
2577
2577
  // Await in order WITHOUT blocking the hook return (fire the chain async).
2578
- void (async () => { for (const r of coord.then)
2578
+ void (async () => { for (const r of chain.then)
2579
2579
  await spawn(r); })();
2580
2580
  }
2581
2581
  else {
2582
- for (const r of coord.then)
2582
+ for (const r of chain.then)
2583
2583
  void spawn(r);
2584
2584
  }
2585
2585
  }
@@ -2608,9 +2608,9 @@ class ClaudeLLMStream extends llm.LLMStream {
2608
2608
  // opts.agents is undefined — the ?? NAMED_AGENTS fallback would skip
2609
2609
  // the override entirely. Explicitly apply applyTurbo(NAMED_AGENTS)
2610
2610
  // so built-in agents always get FAST_MODEL when turbo is on.
2611
- agents: finalizeRoster(applyGrounding(this.#llmRef.turbo
2611
+ agents: applyGrounding(this.#llmRef.turbo
2612
2612
  ? applyTurbo(this.#opts.agents ?? NAMED_AGENTS, true)
2613
- : (this.#opts.agents ?? NAMED_AGENTS), this.#sessionId, this.#opts.workingDirectory)),
2613
+ : (this.#opts.agents ?? NAMED_AGENTS), this.#sessionId, this.#opts.workingDirectory),
2614
2614
  };
2615
2615
  // Run Claude Agent SDK query() and stream results
2616
2616
  let hasOutput = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.219",
3
+ "version": "0.9.221",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {