osborn 0.9.216 → 0.9.218
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 +17 -0
- package/dist/claude-llm.js +108 -45
- package/package.json +1 -1
package/dist/claude-llm.d.ts
CHANGED
|
@@ -59,12 +59,23 @@ export declare const NAMED_AGENTS: {
|
|
|
59
59
|
description: string;
|
|
60
60
|
tools: string[];
|
|
61
61
|
grounded: boolean;
|
|
62
|
+
policy: {
|
|
63
|
+
write: "anywhere";
|
|
64
|
+
};
|
|
65
|
+
reminder: string;
|
|
62
66
|
model: string;
|
|
63
67
|
prompt: string;
|
|
64
68
|
};
|
|
65
69
|
tester: {
|
|
66
70
|
description: string;
|
|
67
71
|
tools: string[];
|
|
72
|
+
policy: {
|
|
73
|
+
write: {
|
|
74
|
+
extensions: RegExp;
|
|
75
|
+
label: string;
|
|
76
|
+
matchBasename: boolean;
|
|
77
|
+
};
|
|
78
|
+
};
|
|
68
79
|
model: string;
|
|
69
80
|
prompt: string;
|
|
70
81
|
};
|
|
@@ -78,6 +89,12 @@ export declare const NAMED_AGENTS: {
|
|
|
78
89
|
reviewer: {
|
|
79
90
|
description: string;
|
|
80
91
|
tools: string[];
|
|
92
|
+
policy: {
|
|
93
|
+
write: {
|
|
94
|
+
extensions: RegExp;
|
|
95
|
+
label: string;
|
|
96
|
+
};
|
|
97
|
+
};
|
|
81
98
|
model: string;
|
|
82
99
|
prompt: string;
|
|
83
100
|
};
|
package/dist/claude-llm.js
CHANGED
|
@@ -139,6 +139,29 @@ function getSubagentsDir(workingDir) {
|
|
|
139
139
|
mkdirSync(dir, { recursive: true });
|
|
140
140
|
return dir;
|
|
141
141
|
}
|
|
142
|
+
// DEPRECATED / DEAD (kept for reference, do NOT re-wire): read skills from an
|
|
143
|
+
// arbitrary agentDir/.claude/skills. This was the cwd-based reader that caused the
|
|
144
|
+
// home-vs-cwd divergence. All skill reads now go through the single source of truth
|
|
145
|
+
// (~/.claude/skills) via loadAllSkills / enumerateSkillsForCompaction / loadSkillsList.
|
|
146
|
+
// Commented out rather than removed so the old shape stays visible.
|
|
147
|
+
// function loadSkillsFromDir(agentDir: string): string {
|
|
148
|
+
// const skillsDir = join(agentDir, '.claude', 'skills')
|
|
149
|
+
// if (!existsSync(skillsDir)) return ''
|
|
150
|
+
// const skills: string[] = []
|
|
151
|
+
// try {
|
|
152
|
+
// for (const skillName of readdirSync(skillsDir)) {
|
|
153
|
+
// const skillFile = join(skillsDir, skillName, 'SKILL.md')
|
|
154
|
+
// if (existsSync(skillFile)) {
|
|
155
|
+
// skills.push(readFileSync(skillFile, 'utf-8').trim())
|
|
156
|
+
// }
|
|
157
|
+
// }
|
|
158
|
+
// } catch (err) {
|
|
159
|
+
// console.warn('⚠️ Failed to load skills:', err)
|
|
160
|
+
// }
|
|
161
|
+
// if (skills.length === 0) return ''
|
|
162
|
+
// console.log(`📚 Loaded ${skills.length} skill(s) from ${skillsDir}`)
|
|
163
|
+
// return `<available-skills>\n${skills.join('\n\n---\n\n')}\n</available-skills>`
|
|
164
|
+
// }
|
|
142
165
|
/**
|
|
143
166
|
* Loads skills from both ~/.claude/skills/ (home dir) and {workingDir}/.claude/skills/ (project dir).
|
|
144
167
|
* Merges results, deduplicating by skill directory name — home dir wins on conflicts.
|
|
@@ -445,6 +468,10 @@ export const NAMED_AGENTS = {
|
|
|
445
468
|
].join(' '),
|
|
446
469
|
tools: ['Read', 'Write', 'Edit', 'MultiEdit', 'Bash', 'Glob', 'Grep', 'NotebookRead', 'NotebookEdit'],
|
|
447
470
|
grounded: true, // applyGrounding() injects the osborn-recall command
|
|
471
|
+
policy: { write: 'anywhere' }, // sole writer — unrestricted; funnels through canUseTool
|
|
472
|
+
// Soft behavior via the composable `reminder` seam → SDK criticalSystemReminder_EXPERIMENTAL.
|
|
473
|
+
// Pinned into the writer's system prompt as a hard-to-ignore reminder.
|
|
474
|
+
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.',
|
|
448
475
|
model: 'opus',
|
|
449
476
|
prompt: [
|
|
450
477
|
'You are Osborn\'s writer agent. You execute file changes with a verify-first approach.',
|
|
@@ -497,6 +524,8 @@ export const NAMED_AGENTS = {
|
|
|
497
524
|
'NOT GROUNDED: no session index access — runs tests with fresh eyes, adversarial validation.',
|
|
498
525
|
].join(' '),
|
|
499
526
|
tools: ['Bash', 'Read', 'Glob', 'Grep', 'Write', 'Edit'],
|
|
527
|
+
// Fail-closed: may ONLY write test files (basename .test/.spec.[jt]sx?).
|
|
528
|
+
policy: { write: { extensions: /\.(test|spec)\.[jt]sx?$/, label: 'test', matchBasename: true } },
|
|
500
529
|
model: 'sonnet',
|
|
501
530
|
prompt: [
|
|
502
531
|
'You are Osborn\'s tester agent. Your job is running tests and builds, then reporting results.',
|
|
@@ -646,6 +675,8 @@ export const NAMED_AGENTS = {
|
|
|
646
675
|
'NOT GROUNDED: no session index access — reviews with fresh eyes for unbiased adversarial check.',
|
|
647
676
|
].join(' '),
|
|
648
677
|
tools: ['Read', 'Glob', 'Grep', 'Bash', 'Write', 'Edit'],
|
|
678
|
+
// Fail-closed: may ONLY write documentation files.
|
|
679
|
+
policy: { write: { extensions: /\.(md|markdown|mdx|txt|rst|adoc)$/i, label: 'documentation' } },
|
|
649
680
|
model: 'sonnet',
|
|
650
681
|
prompt: [
|
|
651
682
|
'You are Osborn\'s reviewer agent. You are the VERIFY step in a generator-verifier loop.',
|
|
@@ -813,6 +844,59 @@ export function applyGrounding(agents, sessionId, workingDir) {
|
|
|
813
844
|
}
|
|
814
845
|
return out;
|
|
815
846
|
}
|
|
847
|
+
const DEFAULT_WRITE_POLICY = 'workspace';
|
|
848
|
+
/** Path is inside the per-session sandbox workspace. */
|
|
849
|
+
function isWorkspacePath(filePath) {
|
|
850
|
+
return !!filePath && (filePath.includes('/osb/') ||
|
|
851
|
+
filePath.includes('.osborn/sessions/') ||
|
|
852
|
+
filePath.includes('.osborn/research/'));
|
|
853
|
+
}
|
|
854
|
+
/** Effective write policy for the acting agent (null agentType = main orchestrator). */
|
|
855
|
+
function resolveWritePolicy(agentType, roster) {
|
|
856
|
+
const def = agentType ? roster?.[agentType] : null;
|
|
857
|
+
return def?.policy?.write ?? DEFAULT_WRITE_POLICY;
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Decide a Write/Edit/MultiEdit against a policy. Pure — no side effects.
|
|
861
|
+
* 'allow' → let it fall through (→ canUseTool workspace auto-approve, or {}).
|
|
862
|
+
* 'defer' → PreToolUse returns permissionDecision:'ask' (canUseTool decides).
|
|
863
|
+
* 'deny' → hard block with reason.
|
|
864
|
+
*/
|
|
865
|
+
function decideWrite(policy, filePath) {
|
|
866
|
+
if (policy === 'anywhere')
|
|
867
|
+
return { decision: 'defer' };
|
|
868
|
+
if (policy === 'workspace') {
|
|
869
|
+
if (filePath && !isWorkspacePath(filePath)) {
|
|
870
|
+
return { decision: 'deny', reason: 'Research mode: writes restricted to session workspace.' };
|
|
871
|
+
}
|
|
872
|
+
return { decision: 'allow' };
|
|
873
|
+
}
|
|
874
|
+
// Extension whitelist — fail closed (empty/unknown path denied).
|
|
875
|
+
const target = policy.matchBasename ? (filePath ? basename(resolve(filePath)) : '') : filePath;
|
|
876
|
+
if (!filePath || !policy.extensions.test(target)) {
|
|
877
|
+
const reason = filePath
|
|
878
|
+
? `Write denied: ${filePath} is not a ${policy.label} file. This agent may only write ${policy.label} files.`
|
|
879
|
+
: 'Write denied: could not determine target file path. Failing closed.';
|
|
880
|
+
return { decision: 'deny', reason };
|
|
881
|
+
}
|
|
882
|
+
return { decision: 'defer' };
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Strip/map behavior meta-fields so the roster is a clean AgentDefinition set
|
|
886
|
+
* for the SDK: drop `policy` (enforced in-process by the write-gate) and map
|
|
887
|
+
* `reminder` → criticalSystemReminder_EXPERIMENTAL. NEVER mutates the input.
|
|
888
|
+
*/
|
|
889
|
+
function finalizeRoster(agents) {
|
|
890
|
+
const out = {};
|
|
891
|
+
for (const [name, agent] of Object.entries(agents)) {
|
|
892
|
+
const { policy, reminder, ...rest } = agent;
|
|
893
|
+
if (reminder && !rest.criticalSystemReminder_EXPERIMENTAL) {
|
|
894
|
+
rest.criticalSystemReminder_EXPERIMENTAL = reminder;
|
|
895
|
+
}
|
|
896
|
+
out[name] = rest;
|
|
897
|
+
}
|
|
898
|
+
return out;
|
|
899
|
+
}
|
|
816
900
|
const RESEARCH_TOOLS = [
|
|
817
901
|
'Read', 'Write', 'Edit', 'Glob', 'Grep',
|
|
818
902
|
'Bash', 'WebSearch', 'WebFetch',
|
|
@@ -1932,6 +2016,10 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
1932
2016
|
? getSessionWorkspace(this.#opts.workingDirectory, sessionId)
|
|
1933
2017
|
: null;
|
|
1934
2018
|
const allowedTools = this.#opts.allowedTools || [];
|
|
2019
|
+
// Roster the PreToolUse write-gate reads policy from — the PRE-strip view
|
|
2020
|
+
// (retains `policy`), so DB-backed custom agents (set_agents) get their
|
|
2021
|
+
// write policy enforced too. The SDK receives the finalized copy (agents:).
|
|
2022
|
+
const enforcementRoster = this.#opts.agents ?? NAMED_AGENTS;
|
|
1935
2023
|
const sdkOptions = {
|
|
1936
2024
|
cwd: this.#opts.workingDirectory,
|
|
1937
2025
|
permissionMode: this.#opts.permissionMode,
|
|
@@ -2049,54 +2137,29 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
2049
2137
|
}
|
|
2050
2138
|
console.log(`🔧 Tool call ${turnToolCallCount}/${TOOL_CALL_BUDGET}: ${toolName}`);
|
|
2051
2139
|
}
|
|
2052
|
-
// Write/Edit/MultiEdit access control
|
|
2140
|
+
// Write/Edit/MultiEdit access control — DATA-DRIVEN by each agent's
|
|
2141
|
+
// declarative policy.write (see AgentWritePolicy). Replaces the old
|
|
2142
|
+
// hardcoded per-role branches; DB-backed custom agents get enforced too.
|
|
2053
2143
|
if (toolName === 'Write' || toolName === 'Edit' || toolName === 'MultiEdit') {
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
this.#eventEmitter.emit('
|
|
2060
|
-
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: '
|
|
2061
|
-
}
|
|
2062
|
-
// Reviewer agent: ONLY documentation-extension files allowed — fail closed
|
|
2063
|
-
if (agentType === 'reviewer') {
|
|
2064
|
-
const reviewerPath = String(toolInput.file_path || '');
|
|
2065
|
-
const DOC_EXTENSIONS = /\.(md|markdown|mdx|txt|rst|adoc)$/i;
|
|
2066
|
-
if (!reviewerPath || !DOC_EXTENSIONS.test(reviewerPath)) {
|
|
2067
|
-
const reason = reviewerPath
|
|
2068
|
-
? `Reviewer write denied: ${reviewerPath} is not a documentation file (.md/.markdown/.mdx/.txt/.rst/.adoc). Reviewer may only write documentation.`
|
|
2069
|
-
: 'Reviewer write denied: could not determine target file path. Failing closed.';
|
|
2070
|
-
console.log(`🚫 Reviewer write blocked: ${reviewerPath || '(no path)'} — not a doc extension`);
|
|
2071
|
-
this.#eventEmitter.emit('tool_blocked', { name: toolName, reason });
|
|
2072
|
-
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny' }, reason };
|
|
2073
|
-
}
|
|
2074
|
-
console.log(`📝 Reviewer doc write allowed: ${reviewerPath}`);
|
|
2075
|
-
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'ask' } };
|
|
2144
|
+
const filePath = String(toolInput.file_path || '');
|
|
2145
|
+
const policy = resolveWritePolicy(agentType, enforcementRoster);
|
|
2146
|
+
const { decision, reason } = decideWrite(policy, filePath);
|
|
2147
|
+
console.log(`🔎 Write gate: agent=${agentType ?? 'main'} policy=${JSON.stringify(policy)} path="${filePath || '(none)'}" → ${decision}`);
|
|
2148
|
+
if (decision === 'deny') {
|
|
2149
|
+
this.#eventEmitter.emit('tool_blocked', { name: toolName, reason });
|
|
2150
|
+
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny' }, reason };
|
|
2076
2151
|
}
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
const reason = testerPath
|
|
2084
|
-
? `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.`
|
|
2085
|
-
: 'Tester write denied: could not determine target file path. Failing closed.';
|
|
2086
|
-
console.log(`🚫 Tester write blocked: ${testerPath || '(no path)'} — not a test file`);
|
|
2087
|
-
this.#eventEmitter.emit('tool_blocked', { name: toolName, reason });
|
|
2088
|
-
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny' }, reason };
|
|
2152
|
+
if (decision === 'defer') {
|
|
2153
|
+
// Writer (write:'anywhere') surfaces its write in the live trace
|
|
2154
|
+
// before the permission dialog; adversarial agents (reviewer/tester)
|
|
2155
|
+
// do NOT emit here — parity with the prior hardcoded branches.
|
|
2156
|
+
if (policy === 'anywhere') {
|
|
2157
|
+
this.#eventEmitter.emit('tool_use', { name: toolName, input: toolInput, agentRole: agentType || 'main' });
|
|
2089
2158
|
}
|
|
2090
|
-
console.log(`🧪 Tester test-file write allowed: ${testerPath}`);
|
|
2091
2159
|
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'ask' } };
|
|
2092
2160
|
}
|
|
2093
|
-
//
|
|
2094
|
-
|
|
2095
|
-
if (filePath && !filePath.includes('/osb/') && !filePath.includes('.osborn/sessions/') && !filePath.includes('.osborn/research/')) {
|
|
2096
|
-
console.log(`🚫 Research mode: blocked write to ${filePath} (agent_type: ${agentType ?? 'main'})`);
|
|
2097
|
-
this.#eventEmitter.emit('tool_blocked', { name: toolName, reason: 'Research mode: writes restricted to session workspace' });
|
|
2098
|
-
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny' }, reason: 'Research mode: writes restricted to session workspace.' };
|
|
2099
|
-
}
|
|
2161
|
+
// decision === 'allow' → fall through to the shared tail (emits
|
|
2162
|
+
// tool_use, returns {} → canUseTool workspace auto-approve).
|
|
2100
2163
|
}
|
|
2101
2164
|
console.log(`🔧 Claude: ${toolName}`);
|
|
2102
2165
|
this.#eventEmitter.emit('tool_use', { name: toolName, input: toolInput, agentRole: agentType || 'main' });
|
|
@@ -2490,9 +2553,9 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
2490
2553
|
// opts.agents is undefined — the ?? NAMED_AGENTS fallback would skip
|
|
2491
2554
|
// the override entirely. Explicitly apply applyTurbo(NAMED_AGENTS)
|
|
2492
2555
|
// so built-in agents always get FAST_MODEL when turbo is on.
|
|
2493
|
-
agents: applyGrounding(this.#llmRef.turbo
|
|
2556
|
+
agents: finalizeRoster(applyGrounding(this.#llmRef.turbo
|
|
2494
2557
|
? applyTurbo(this.#opts.agents ?? NAMED_AGENTS, true)
|
|
2495
|
-
: (this.#opts.agents ?? NAMED_AGENTS), this.#sessionId, this.#opts.workingDirectory),
|
|
2558
|
+
: (this.#opts.agents ?? NAMED_AGENTS), this.#sessionId, this.#opts.workingDirectory)),
|
|
2496
2559
|
};
|
|
2497
2560
|
// Run Claude Agent SDK query() and stream results
|
|
2498
2561
|
let hasOutput = false;
|