osborn 0.9.213 → 0.9.214
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.js +88 -10
- package/dist/index.js +12 -10
- package/package.json +1 -1
package/dist/claude-llm.js
CHANGED
|
@@ -237,13 +237,23 @@ function enumerateSkillsForCompaction() {
|
|
|
237
237
|
const first = body.split('\n').map(l => l.trim()).find(l => l && !l.startsWith('#'));
|
|
238
238
|
desc = first || '(no description)';
|
|
239
239
|
}
|
|
240
|
+
// Option A: expose the skill's SECTION HEADINGS so the compaction model can
|
|
241
|
+
// target a specific section for a surgical update (via UPDATE_SECTION) rather
|
|
242
|
+
// than re-dumping a whole new skill. The summarizer has no tool access — this
|
|
243
|
+
// is the only way it can "see inside" the file.
|
|
244
|
+
const body = raw.replace(/^---\n[\s\S]*?\n---\n?/, '');
|
|
245
|
+
const headings = (body.match(/^#{2,3}\s+.+$/gm) || [])
|
|
246
|
+
.map(h => h.replace(/^#{2,3}\s+/, '').trim())
|
|
247
|
+
.filter(Boolean);
|
|
248
|
+
if (desc.length > 180)
|
|
249
|
+
desc = desc.slice(0, 177) + '…';
|
|
250
|
+
lines.push(`- ${name}: ${desc}`);
|
|
251
|
+
if (headings.length)
|
|
252
|
+
lines.push(` sections: ${headings.join(' | ')}`);
|
|
240
253
|
}
|
|
241
254
|
catch {
|
|
242
|
-
|
|
255
|
+
lines.push(`- ${name}: (unreadable)`);
|
|
243
256
|
}
|
|
244
|
-
if (desc.length > 180)
|
|
245
|
-
desc = desc.slice(0, 177) + '…';
|
|
246
|
-
lines.push(`- ${name}: ${desc}`);
|
|
247
257
|
}
|
|
248
258
|
}
|
|
249
259
|
catch (err) {
|
|
@@ -2164,16 +2174,26 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
2164
2174
|
// proliferated duplicates. We hand it the current skill set + an adversarial,
|
|
2165
2175
|
// self-critical directive to merge/refine rather than create anew.
|
|
2166
2176
|
const existingSkills = enumerateSkillsForCompaction();
|
|
2167
|
-
|
|
2177
|
+
// Count only skill lines (`- name: …`), not the ` sections:` continuation lines.
|
|
2178
|
+
const skillCount = existingSkills
|
|
2179
|
+
? existingSkills.split('\n').filter(l => l.startsWith('- ')).length
|
|
2180
|
+
: 0;
|
|
2168
2181
|
const criticBlock = existingSkills
|
|
2169
2182
|
? `\n\n---\n\n=== EXISTING SKILLS (${skillCount}) ===\n`
|
|
2170
|
-
+ `These skills ALREADY EXIST for this user
|
|
2171
|
-
+ `
|
|
2172
|
-
+ `
|
|
2183
|
+
+ `These skills ALREADY EXIST for this user. Each is listed as \`- name: description\`, and where known, `
|
|
2184
|
+
+ `an indented \`sections:\` line lists that skill's section headings (\`## \`/\`### \`). `
|
|
2185
|
+
+ `Before you emit SKILL_CANDIDATES or BEHAVIORAL_LEARNINGS, act as an ADVERSARIAL reviewer of your own output:\n`
|
|
2186
|
+
+ `1. If what you learned refines ONE section of an existing skill, do NOT re-emit the whole skill. Instead emit a `
|
|
2187
|
+
+ `surgical section update inside SKILL_CANDIDATES using this exact block form (the heading must match a `
|
|
2188
|
+
+ `\`sections:\` entry below verbatim):\n`
|
|
2189
|
+
+ ` --- UPDATE_SECTION: <skill-name> > <exact section heading> ---\n`
|
|
2190
|
+
+ ` <the new markdown that should REPLACE that section's body>\n`
|
|
2191
|
+
+ ` --- END UPDATE_SECTION ---\n`
|
|
2192
|
+
+ `2. If a candidate duplicates or substantially overlaps one below but isn't a single-section tweak, `
|
|
2173
2193
|
+ `re-emit it under the EXACT SAME kebab-case name, and only if it needs a substantive update; otherwise omit it.\n`
|
|
2174
|
-
+ `
|
|
2194
|
+
+ `3. Propose a brand-new skill ONLY if nothing below covers it, it was CONFIRMED working this session, `
|
|
2175
2195
|
+ `and it generalizes to future sessions on different tasks.\n`
|
|
2176
|
-
+ `
|
|
2196
|
+
+ `4. Prefer refining/section-updating over proliferating. A small set of sharp, non-overlapping skills is the goal — `
|
|
2177
2197
|
+ `reject your own low-signal or one-off candidates.\n\n`
|
|
2178
2198
|
+ `${existingSkills}\n`
|
|
2179
2199
|
: '';
|
|
@@ -2286,6 +2306,64 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
2286
2306
|
catch (skillErr) {
|
|
2287
2307
|
console.error('⚠️ PostCompact: SKILL_CANDIDATES write failed:', skillErr instanceof Error ? skillErr.message : skillErr);
|
|
2288
2308
|
}
|
|
2309
|
+
// ── Section 3b: UPDATE_SECTION — surgical, section-level edits to EXISTING skills ──
|
|
2310
|
+
// The model (via the PreCompact preamble) emits blocks of the form:
|
|
2311
|
+
// --- UPDATE_SECTION: <skill-name> > <exact heading> ---
|
|
2312
|
+
// <replacement markdown for that section body>
|
|
2313
|
+
// --- END UPDATE_SECTION ---
|
|
2314
|
+
// We splice the new body under the matching `## `/`### ` heading, replacing
|
|
2315
|
+
// everything down to the next heading of the same-or-higher level. We NEVER
|
|
2316
|
+
// create a file here — an unknown skill/heading is logged and skipped, so the
|
|
2317
|
+
// model can't silently mint a new skill through this path.
|
|
2318
|
+
try {
|
|
2319
|
+
const updateRe = /---\s*UPDATE_SECTION:\s*([a-z][a-z0-9-]{1,39})\s*>\s*([^\n]+?)\s*---\n([\s\S]*?)---\s*END UPDATE_SECTION\s*---/g;
|
|
2320
|
+
let um;
|
|
2321
|
+
while ((um = updateRe.exec(summary)) !== null) {
|
|
2322
|
+
const targetSkill = um[1].trim();
|
|
2323
|
+
const targetHeading = um[2].trim();
|
|
2324
|
+
const newBody = um[3].trim();
|
|
2325
|
+
const skillPath = join(skillDir, '.claude', 'skills', targetSkill, 'SKILL.md');
|
|
2326
|
+
if (!existsSyncFs(skillPath)) {
|
|
2327
|
+
console.warn(`⚠️ PostCompact: UPDATE_SECTION skipped — no such skill '${targetSkill}'`);
|
|
2328
|
+
continue;
|
|
2329
|
+
}
|
|
2330
|
+
const content = readSyncFs(skillPath, 'utf-8');
|
|
2331
|
+
const lines2 = content.split('\n');
|
|
2332
|
+
// Find the heading line (## or ###) whose text matches targetHeading (case-insensitive).
|
|
2333
|
+
const norm = (s) => s.replace(/^#{2,3}\s+/, '').trim().toLowerCase();
|
|
2334
|
+
const hIdx = lines2.findIndex(l => /^#{2,3}\s+/.test(l) && norm(l) === targetHeading.toLowerCase());
|
|
2335
|
+
if (hIdx === -1) {
|
|
2336
|
+
console.warn(`⚠️ PostCompact: UPDATE_SECTION skipped — heading '${targetHeading}' not found in '${targetSkill}'`);
|
|
2337
|
+
continue;
|
|
2338
|
+
}
|
|
2339
|
+
const level = (lines2[hIdx].match(/^#+/) || ['##'])[0].length;
|
|
2340
|
+
// Section ends at the next heading of the same-or-higher level (fewer/equal #s).
|
|
2341
|
+
let end = lines2.length;
|
|
2342
|
+
for (let i = hIdx + 1; i < lines2.length; i++) {
|
|
2343
|
+
const m2 = lines2[i].match(/^(#{1,6})\s+/);
|
|
2344
|
+
if (m2 && m2[1].length <= level) {
|
|
2345
|
+
end = i;
|
|
2346
|
+
break;
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
const rebuilt = [
|
|
2350
|
+
...lines2.slice(0, hIdx + 1),
|
|
2351
|
+
'',
|
|
2352
|
+
newBody,
|
|
2353
|
+
'',
|
|
2354
|
+
...lines2.slice(end),
|
|
2355
|
+
].join('\n');
|
|
2356
|
+
writeSyncFs(skillPath, rebuilt, 'utf-8');
|
|
2357
|
+
console.log(`🧠 PostCompact: UPDATE_SECTION applied — '${targetSkill}' › '${targetHeading}' (${newBody.length} chars)`);
|
|
2358
|
+
skillsWritten++;
|
|
2359
|
+
if (!skillNames.includes(targetSkill))
|
|
2360
|
+
skillNames.push(targetSkill);
|
|
2361
|
+
progress('Updated section', `${targetSkill} › ${targetHeading}`);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
catch (updErr) {
|
|
2365
|
+
console.error('⚠️ PostCompact: UPDATE_SECTION apply failed:', updErr instanceof Error ? updErr.message : updErr);
|
|
2366
|
+
}
|
|
2289
2367
|
// ── Section 4: BEHAVIORAL_LEARNINGS — write to learned-behaviors/SKILL.md ──
|
|
2290
2368
|
try {
|
|
2291
2369
|
const learnings = extractSection('=== BEHAVIORAL_LEARNINGS ===');
|
package/dist/index.js
CHANGED
|
@@ -559,10 +559,12 @@ function startApiServer(workingDir, port) {
|
|
|
559
559
|
if (req.method === 'GET' && url.pathname === '/skills') {
|
|
560
560
|
// Installed skills — same list the chat's get_skills data-channel message
|
|
561
561
|
// returns, exposed over HTTP so the DASHBOARD (no LiveKit connection) can
|
|
562
|
-
// render the skills manager too.
|
|
563
|
-
//
|
|
562
|
+
// render the skills manager too. Reads ~/.claude/skills (homedir) — the SINGLE
|
|
563
|
+
// location the agent ingests from (loadAllSkills) and PostCompact writes to.
|
|
564
|
+
// Previously used process.cwd()/.claude/skills, which the agent never read —
|
|
565
|
+
// so UI-created skills were invisible to the agent (the "divergence").
|
|
564
566
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
565
|
-
res.end(JSON.stringify({ skills: loadSkillsList(
|
|
567
|
+
res.end(JSON.stringify({ skills: loadSkillsList(homedir()) }));
|
|
566
568
|
return;
|
|
567
569
|
}
|
|
568
570
|
if (req.method === 'GET' && url.pathname === '/agents') {
|
|
@@ -4630,7 +4632,7 @@ async function main() {
|
|
|
4630
4632
|
mcpServers: getMcpServerStatusList(config),
|
|
4631
4633
|
enabledMcpServers: enabledMcpNames,
|
|
4632
4634
|
workingDirectory: workingDir,
|
|
4633
|
-
skills: loadSkillsList(
|
|
4635
|
+
skills: loadSkillsList(homedir()),
|
|
4634
4636
|
namedAgents: Object.entries(NAMED_AGENTS).map(([name, a]) => ({
|
|
4635
4637
|
name, description: a.description, model: a.model, tools: a.tools,
|
|
4636
4638
|
})),
|
|
@@ -5330,7 +5332,7 @@ async function main() {
|
|
|
5330
5332
|
else if (data.type === 'get_skills') {
|
|
5331
5333
|
await sendToFrontend({
|
|
5332
5334
|
type: 'skills_status',
|
|
5333
|
-
skills: loadSkillsList(
|
|
5335
|
+
skills: loadSkillsList(homedir()),
|
|
5334
5336
|
});
|
|
5335
5337
|
}
|
|
5336
5338
|
else if (data.type === 'get_agents') {
|
|
@@ -5404,11 +5406,11 @@ async function main() {
|
|
|
5404
5406
|
}
|
|
5405
5407
|
else {
|
|
5406
5408
|
try {
|
|
5407
|
-
const skillDir = join(
|
|
5409
|
+
const skillDir = join(homedir(), '.claude', 'skills', skillName);
|
|
5408
5410
|
mkdirSync(skillDir, { recursive: true });
|
|
5409
5411
|
writeFileSync(join(skillDir, 'SKILL.md'), skillContent, 'utf-8');
|
|
5410
5412
|
console.log(`📚 Skill added: ${skillName}`);
|
|
5411
|
-
const skills = loadSkillsList(
|
|
5413
|
+
const skills = loadSkillsList(homedir());
|
|
5412
5414
|
await sendToFrontend({ type: 'skill_add_result', success: true, skills });
|
|
5413
5415
|
}
|
|
5414
5416
|
catch (err) {
|
|
@@ -5419,7 +5421,7 @@ async function main() {
|
|
|
5419
5421
|
}
|
|
5420
5422
|
else if (data.type === 'skill_get') {
|
|
5421
5423
|
const folder = (data.name || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
5422
|
-
const p = join(
|
|
5424
|
+
const p = join(homedir(), '.claude', 'skills', folder, 'SKILL.md');
|
|
5423
5425
|
if (folder && existsSync(p)) {
|
|
5424
5426
|
await sendToFrontend({ type: 'skill_content', name: folder, content: readFileSync(p, 'utf-8') });
|
|
5425
5427
|
}
|
|
@@ -5429,7 +5431,7 @@ async function main() {
|
|
|
5429
5431
|
}
|
|
5430
5432
|
else if (data.type === 'skill_remove') {
|
|
5431
5433
|
const folder = (data.name || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
5432
|
-
const dir = join(
|
|
5434
|
+
const dir = join(homedir(), '.claude', 'skills', folder);
|
|
5433
5435
|
if (!folder || !existsSync(dir)) {
|
|
5434
5436
|
await sendToFrontend({ type: 'skill_remove_result', success: false, error: 'skill not found' });
|
|
5435
5437
|
}
|
|
@@ -5437,7 +5439,7 @@ async function main() {
|
|
|
5437
5439
|
try {
|
|
5438
5440
|
rmSync(dir, { recursive: true, force: true });
|
|
5439
5441
|
console.log(`🗑️ Skill removed: ${folder}`);
|
|
5440
|
-
await sendToFrontend({ type: 'skill_remove_result', success: true, skills: loadSkillsList(
|
|
5442
|
+
await sendToFrontend({ type: 'skill_remove_result', success: true, skills: loadSkillsList(homedir()) });
|
|
5441
5443
|
}
|
|
5442
5444
|
catch (err) {
|
|
5443
5445
|
await sendToFrontend({ type: 'skill_remove_result', success: false, error: String(err) });
|