osborn 0.9.217 → 0.9.219
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 +26 -0
- package/dist/claude-llm.js +146 -51
- package/package.json +1 -1
package/dist/claude-llm.d.ts
CHANGED
|
@@ -46,6 +46,10 @@ 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
|
+
};
|
|
49
53
|
model: string;
|
|
50
54
|
prompt: string;
|
|
51
55
|
};
|
|
@@ -59,12 +63,28 @@ export declare const NAMED_AGENTS: {
|
|
|
59
63
|
description: string;
|
|
60
64
|
tools: string[];
|
|
61
65
|
grounded: boolean;
|
|
66
|
+
policy: {
|
|
67
|
+
write: "anywhere";
|
|
68
|
+
};
|
|
69
|
+
reminder: string;
|
|
70
|
+
coordination: {
|
|
71
|
+
then: string[];
|
|
72
|
+
mode: "parallel";
|
|
73
|
+
startNote: string;
|
|
74
|
+
};
|
|
62
75
|
model: string;
|
|
63
76
|
prompt: string;
|
|
64
77
|
};
|
|
65
78
|
tester: {
|
|
66
79
|
description: string;
|
|
67
80
|
tools: string[];
|
|
81
|
+
policy: {
|
|
82
|
+
write: {
|
|
83
|
+
extensions: RegExp;
|
|
84
|
+
label: string;
|
|
85
|
+
matchBasename: boolean;
|
|
86
|
+
};
|
|
87
|
+
};
|
|
68
88
|
model: string;
|
|
69
89
|
prompt: string;
|
|
70
90
|
};
|
|
@@ -78,6 +98,12 @@ export declare const NAMED_AGENTS: {
|
|
|
78
98
|
reviewer: {
|
|
79
99
|
description: string;
|
|
80
100
|
tools: string[];
|
|
101
|
+
policy: {
|
|
102
|
+
write: {
|
|
103
|
+
extensions: RegExp;
|
|
104
|
+
label: string;
|
|
105
|
+
};
|
|
106
|
+
};
|
|
81
107
|
model: string;
|
|
82
108
|
prompt: string;
|
|
83
109
|
};
|
package/dist/claude-llm.js
CHANGED
|
@@ -366,6 +366,12 @@ 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
|
+
},
|
|
369
375
|
model: 'sonnet',
|
|
370
376
|
prompt: [
|
|
371
377
|
'You are Osborn\'s research agent. Your job is information gathering — thorough, structured, factual.',
|
|
@@ -468,6 +474,17 @@ export const NAMED_AGENTS = {
|
|
|
468
474
|
].join(' '),
|
|
469
475
|
tools: ['Read', 'Write', 'Edit', 'MultiEdit', 'Bash', 'Glob', 'Grep', 'NotebookRead', 'NotebookEdit'],
|
|
470
476
|
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
|
+
},
|
|
471
488
|
model: 'opus',
|
|
472
489
|
prompt: [
|
|
473
490
|
'You are Osborn\'s writer agent. You execute file changes with a verify-first approach.',
|
|
@@ -520,6 +537,8 @@ export const NAMED_AGENTS = {
|
|
|
520
537
|
'NOT GROUNDED: no session index access — runs tests with fresh eyes, adversarial validation.',
|
|
521
538
|
].join(' '),
|
|
522
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 } },
|
|
523
542
|
model: 'sonnet',
|
|
524
543
|
prompt: [
|
|
525
544
|
'You are Osborn\'s tester agent. Your job is running tests and builds, then reporting results.',
|
|
@@ -669,6 +688,8 @@ export const NAMED_AGENTS = {
|
|
|
669
688
|
'NOT GROUNDED: no session index access — reviews with fresh eyes for unbiased adversarial check.',
|
|
670
689
|
].join(' '),
|
|
671
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' } },
|
|
672
693
|
model: 'sonnet',
|
|
673
694
|
prompt: [
|
|
674
695
|
'You are Osborn\'s reviewer agent. You are the VERIFY step in a generator-verifier loop.',
|
|
@@ -836,6 +857,64 @@ export function applyGrounding(agents, sessionId, workingDir) {
|
|
|
836
857
|
}
|
|
837
858
|
return out;
|
|
838
859
|
}
|
|
860
|
+
const DEFAULT_WRITE_POLICY = 'workspace';
|
|
861
|
+
/** Path is inside the per-session sandbox workspace. */
|
|
862
|
+
function isWorkspacePath(filePath) {
|
|
863
|
+
return !!filePath && (filePath.includes('/osb/') ||
|
|
864
|
+
filePath.includes('.osborn/sessions/') ||
|
|
865
|
+
filePath.includes('.osborn/research/'));
|
|
866
|
+
}
|
|
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
|
+
/**
|
|
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.
|
|
877
|
+
*/
|
|
878
|
+
function decideWrite(policy, filePath) {
|
|
879
|
+
if (policy === 'anywhere')
|
|
880
|
+
return { decision: 'defer' };
|
|
881
|
+
if (policy === 'workspace') {
|
|
882
|
+
if (filePath && !isWorkspacePath(filePath)) {
|
|
883
|
+
return { decision: 'deny', reason: 'Research mode: writes restricted to session workspace.' };
|
|
884
|
+
}
|
|
885
|
+
return { decision: 'allow' };
|
|
886
|
+
}
|
|
887
|
+
// 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)) {
|
|
890
|
+
const reason = filePath
|
|
891
|
+
? `Write denied: ${filePath} is not a ${policy.label} file. This agent may only write ${policy.label} files.`
|
|
892
|
+
: 'Write denied: could not determine target file path. Failing closed.';
|
|
893
|
+
return { decision: 'deny', reason };
|
|
894
|
+
}
|
|
895
|
+
return { decision: 'defer' };
|
|
896
|
+
}
|
|
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
|
+
}
|
|
839
918
|
const RESEARCH_TOOLS = [
|
|
840
919
|
'Read', 'Write', 'Edit', 'Glob', 'Grep',
|
|
841
920
|
'Bash', 'WebSearch', 'WebFetch',
|
|
@@ -1955,6 +2034,10 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
1955
2034
|
? getSessionWorkspace(this.#opts.workingDirectory, sessionId)
|
|
1956
2035
|
: null;
|
|
1957
2036
|
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;
|
|
1958
2041
|
const sdkOptions = {
|
|
1959
2042
|
cwd: this.#opts.workingDirectory,
|
|
1960
2043
|
permissionMode: this.#opts.permissionMode,
|
|
@@ -2072,54 +2155,41 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
2072
2155
|
}
|
|
2073
2156
|
console.log(`🔧 Tool call ${turnToolCallCount}/${TOOL_CALL_BUDGET}: ${toolName}`);
|
|
2074
2157
|
}
|
|
2075
|
-
//
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
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) {
|
|
2082
2165
|
this.#eventEmitter.emit('tool_use', { name: toolName, input: toolInput, agentRole: agentType || 'main' });
|
|
2083
|
-
|
|
2166
|
+
console.log(`🤝 Delegation note → subagent_type=${targetType}`);
|
|
2167
|
+
return { hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: note } };
|
|
2084
2168
|
}
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
}
|
|
2097
|
-
console.log(`📝 Reviewer doc write allowed: ${reviewerPath}`);
|
|
2098
|
-
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'ask' } };
|
|
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.
|
|
2173
|
+
if (toolName === 'Write' || toolName === 'Edit' || toolName === 'MultiEdit') {
|
|
2174
|
+
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}`);
|
|
2178
|
+
if (decision === 'deny') {
|
|
2179
|
+
this.#eventEmitter.emit('tool_blocked', { name: toolName, reason });
|
|
2180
|
+
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny' }, reason };
|
|
2099
2181
|
}
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
const reason = testerPath
|
|
2107
|
-
? `Tester write denied: ${testerPath} is not a test file (basename must match .test.ts/tsx/js/jsx or .spec.ts/tsx/js/jsx). Tester may only write test files.`
|
|
2108
|
-
: 'Tester write denied: could not determine target file path. Failing closed.';
|
|
2109
|
-
console.log(`🚫 Tester write blocked: ${testerPath || '(no path)'} — not a test file`);
|
|
2110
|
-
this.#eventEmitter.emit('tool_blocked', { name: toolName, reason });
|
|
2111
|
-
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny' }, reason };
|
|
2182
|
+
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') {
|
|
2187
|
+
this.#eventEmitter.emit('tool_use', { name: toolName, input: toolInput, agentRole: agentType || 'main' });
|
|
2112
2188
|
}
|
|
2113
|
-
console.log(`🧪 Tester test-file write allowed: ${testerPath}`);
|
|
2114
2189
|
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'ask' } };
|
|
2115
2190
|
}
|
|
2116
|
-
//
|
|
2117
|
-
|
|
2118
|
-
if (filePath && !filePath.includes('/osb/') && !filePath.includes('.osborn/sessions/') && !filePath.includes('.osborn/research/')) {
|
|
2119
|
-
console.log(`🚫 Research mode: blocked write to ${filePath} (agent_type: ${agentType ?? 'main'})`);
|
|
2120
|
-
this.#eventEmitter.emit('tool_blocked', { name: toolName, reason: 'Research mode: writes restricted to session workspace' });
|
|
2121
|
-
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny' }, reason: 'Research mode: writes restricted to session workspace.' };
|
|
2122
|
-
}
|
|
2191
|
+
// decision === 'allow' → fall through to the shared tail (emits
|
|
2192
|
+
// tool_use, returns {} → canUseTool workspace auto-approve).
|
|
2123
2193
|
}
|
|
2124
2194
|
console.log(`🔧 Claude: ${toolName}`);
|
|
2125
2195
|
this.#eventEmitter.emit('tool_use', { name: toolName, input: toolInput, agentRole: agentType || 'main' });
|
|
@@ -2466,6 +2536,12 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
2466
2536
|
hooks: [async (input) => {
|
|
2467
2537
|
console.log('[LIFECYCLE-PROBE] SubagentStart', JSON.stringify(input));
|
|
2468
2538
|
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
|
+
}
|
|
2469
2545
|
return {};
|
|
2470
2546
|
}]
|
|
2471
2547
|
}],
|
|
@@ -2478,15 +2554,34 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
2478
2554
|
const aid = input?.agent_id ?? ('sa-' + Date.now());
|
|
2479
2555
|
statusManager.upsertDispatch(aid, { subagentType: at, dispatchState: 'completed', artifact: msg });
|
|
2480
2556
|
this.#eventEmitter.emit('task_completed', { agent_type: at, agent_id: aid, last_assistant_message: String(msg).slice(0, 400) });
|
|
2481
|
-
// Infinite-loop guard — never re-dispatch
|
|
2557
|
+
// 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).
|
|
2482
2560
|
if (at === 'reviewer' || at === 'tester' || at === 'reasoner')
|
|
2483
2561
|
return {};
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
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) {
|
|
2566
|
+
const spawn = (role) => {
|
|
2567
|
+
if (role === 'reviewer')
|
|
2568
|
+
return this.#llmRef.spawnReviewer(aid, msg, this.#eventEmitter);
|
|
2569
|
+
if (role === 'tester')
|
|
2570
|
+
return this.#llmRef.spawnTester(aid, msg, this.#eventEmitter);
|
|
2571
|
+
if (role === 'reasoner' || role === 'gate')
|
|
2572
|
+
return this.#llmRef.spawnResearchGate(aid, msg, this.#eventEmitter);
|
|
2573
|
+
console.warn(`[DISPATCH] unknown coordination target '${role}' for ${at} — skipped`);
|
|
2574
|
+
return Promise.resolve();
|
|
2575
|
+
};
|
|
2576
|
+
if (coord.mode === 'sequential') {
|
|
2577
|
+
// Await in order WITHOUT blocking the hook return (fire the chain async).
|
|
2578
|
+
void (async () => { for (const r of coord.then)
|
|
2579
|
+
await spawn(r); })();
|
|
2580
|
+
}
|
|
2581
|
+
else {
|
|
2582
|
+
for (const r of coord.then)
|
|
2583
|
+
void spawn(r);
|
|
2584
|
+
}
|
|
2490
2585
|
}
|
|
2491
2586
|
return {};
|
|
2492
2587
|
}]
|
|
@@ -2513,9 +2608,9 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
2513
2608
|
// opts.agents is undefined — the ?? NAMED_AGENTS fallback would skip
|
|
2514
2609
|
// the override entirely. Explicitly apply applyTurbo(NAMED_AGENTS)
|
|
2515
2610
|
// so built-in agents always get FAST_MODEL when turbo is on.
|
|
2516
|
-
agents: applyGrounding(this.#llmRef.turbo
|
|
2611
|
+
agents: finalizeRoster(applyGrounding(this.#llmRef.turbo
|
|
2517
2612
|
? applyTurbo(this.#opts.agents ?? NAMED_AGENTS, true)
|
|
2518
|
-
: (this.#opts.agents ?? NAMED_AGENTS), this.#sessionId, this.#opts.workingDirectory),
|
|
2613
|
+
: (this.#opts.agents ?? NAMED_AGENTS), this.#sessionId, this.#opts.workingDirectory)),
|
|
2519
2614
|
};
|
|
2520
2615
|
// Run Claude Agent SDK query() and stream results
|
|
2521
2616
|
let hasOutput = false;
|