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.
- package/dist/claude-llm.d.ts +16 -26
- package/dist/claude-llm.js +106 -106
- package/package.json +1 -1
package/dist/claude-llm.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dist/claude-llm.js
CHANGED
|
@@ -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
|
-
// ≤
|
|
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 =
|
|
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
|
-
|
|
478
|
-
|
|
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
|
|
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
|
|
874
|
-
*
|
|
875
|
-
* '
|
|
876
|
-
* '
|
|
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(
|
|
879
|
-
|
|
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 (
|
|
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 =
|
|
889
|
-
if (!filePath || !
|
|
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 ${
|
|
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
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
-
//
|
|
2159
|
-
//
|
|
2160
|
-
//
|
|
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
|
|
2176
|
-
|
|
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 (
|
|
2184
|
-
//
|
|
2185
|
-
//
|
|
2186
|
-
if (
|
|
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
|
-
//
|
|
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
|
-
//
|
|
2563
|
-
//
|
|
2564
|
-
const
|
|
2565
|
-
if (
|
|
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
|
|
2573
|
+
console.warn(`[DISPATCH] unknown chain target '${role}' for ${at} — skipped`);
|
|
2574
2574
|
return Promise.resolve();
|
|
2575
2575
|
};
|
|
2576
|
-
if (
|
|
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
|
|
2578
|
+
void (async () => { for (const r of chain.then)
|
|
2579
2579
|
await spawn(r); })();
|
|
2580
2580
|
}
|
|
2581
2581
|
else {
|
|
2582
|
-
for (const r of
|
|
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:
|
|
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;
|