osborn 0.9.212 → 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/.claude/skills/browser-apply/SKILL.md +5 -0
- package/.claude/skills/browser-screen-recorder/SKILL.md +5 -0
- package/.claude/skills/deploy-workflow/SKILL.md +5 -0
- package/.claude/skills/ground-assumptions/SKILL.md +5 -0
- package/.claude/skills/markdown-to-pdf/SKILL.md +5 -0
- package/.claude/skills/meetings/SKILL.md +5 -0
- package/.claude/skills/pdf-to-markdown/SKILL.md +5 -0
- package/.claude/skills/playwright-browser/SKILL.md +5 -0
- package/.claude/skills/recall/SKILL.md +5 -0
- package/.claude/skills/send-media/SKILL.md +5 -0
- package/.claude/skills/shadcn/SKILL.md +5 -0
- package/.claude/skills/slack-readonly/SKILL.md +5 -0
- package/.claude/skills/voice-native-sync/SKILL.md +5 -0
- package/.claude/skills/youtube-transcript/SKILL.md +5 -0
- package/dist/claude-llm.js +162 -6
- package/dist/index.js +12 -10
- package/package.json +1 -1
- package/dist/prompts/skill-learner-prompt.md +0 -46
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: browser-apply
|
|
3
|
+
description: Interactively automate Workday and other multi-step JS-heavy job application forms using Playwright MCP browser tools, filling one page at a time with a screenshot and user confirmation before advancing. Use when applying to a Workday ATS or any staged online job application needing visible, confirmable progress.
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# Skill: Browser Apply — Step-by-Step Workday Application
|
|
2
7
|
|
|
3
8
|
Automate Workday job applications interactively, one step at a time. Each step takes a screenshot, confirms what's on screen, fills the current page, and waits before proceeding.
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: browser-screen-recorder
|
|
3
|
+
description: Drive any web app in a real browser and record proof — per-action screenshots, video clips, audio capture, DevTools diagnostics, and metrics, with mic synthesis and natural-language clicking. Use as the QA protocol after building or changing any frontend, for debugging web-reachable issues, verifying deployed changes, or reproducing user reports.
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# Browser Screen Recorder — drive any web app (voice, hands + a brain) and record proof
|
|
2
7
|
|
|
3
8
|
## SKILL IDENTITY
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deploy-workflow
|
|
3
|
+
description: The end-to-end release procedure for shipping osborn agent changes — build and verify locally, bump the package.json version, push to GitHub, publish to NPM from the Mac, and update the running Sprite from the registry. Use every time code or prompt changes are ready to ship.
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# Deploy Workflow
|
|
2
7
|
|
|
3
8
|
Use this every time changes are ready to ship.
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ground-assumptions
|
|
3
|
+
description: Treat every load-bearing assumption as a hypothesis to verify against real evidence before recommending an implementation strategy, rather than trusting training-data intuition or "it should work". Use whenever the conversation enters a planning, design, or architecture phase, or before making any performance, behavioral, or comparative claim.
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# Ground Assumptions
|
|
2
7
|
|
|
3
8
|
## SKILL IDENTITY
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: markdown-to-pdf
|
|
3
|
+
description: Export a Markdown document, spec, or research findings as a formatted PDF using md-to-pdf, pandoc, or markdown-pdf, saving the result to the session workspace. Use when the user wants to create or generate a PDF from a Markdown file.
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# Skill: Markdown to PDF
|
|
2
7
|
|
|
3
8
|
Export Markdown documents as formatted PDF files.
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: meetings
|
|
3
|
+
description: Silently take notes and track TODOs while osborn sits in a live meeting via a Recall.ai bot, and pull transcripts on demand, delegating file and transcript work to the writer sub-agent to avoid the tool-call budget. Use for auto-tagged [MEETING] transcript chunks or when the user asks to grab, summarize, or write notes/todos from a meeting.
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# Skill: Meetings
|
|
2
7
|
|
|
3
8
|
Silent note-taking and TODO-tracking when osborn is sitting in a live meeting,
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pdf-to-markdown
|
|
3
|
+
description: Convert a PDF document to readable Markdown text using the built-in Read tool, pdf-parse-cli, or pdftotext, preserving headings and tables and saving the result to the session workspace. Use when the user provides a PDF file path and wants to read, search, or work with its contents.
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# Skill: PDF to Markdown
|
|
2
7
|
|
|
3
8
|
Convert PDF documents to readable Markdown text.
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: playwright-browser
|
|
3
|
+
description: Automate web browser interactions via the @playwright/cli — navigate pages, click buttons and links, fill and submit forms, take screenshots, and extract text or structured data using token-efficient element references. Use to interact with a URL, automate a multi-step web workflow, or test a UI flow.
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# Skill: Playwright Browser Automation
|
|
2
7
|
|
|
3
8
|
Automate web browser interactions — navigate pages, click buttons, fill forms, take screenshots, and extract content.
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: recall
|
|
3
|
+
description: Query this session's full untruncated history — every message, thinking block, and tool call stored in a per-session SQLite database with keyword and semantic search via the osborn-recall command. Use when you need an earlier decision, file, error, number, or name no longer in context, or to check prior work before acting as a grounded agent.
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# Recall
|
|
2
7
|
|
|
3
8
|
## SKILL IDENTITY
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: send-media
|
|
3
|
+
description: Upload a local file to the osborn public upload endpoint (Supabase storage) to get a permanent public URL, then render it inline in your next reply as a markdown image or link. Use whenever you have a local screenshot, image, PDF, clip, or log the user should see rather than have described.
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# send-media — deliver screenshots, images, and files to the user INLINE in chat
|
|
2
7
|
|
|
3
8
|
> 📅 Last updated: 2026-08-09. Use whenever you have a local file the user should SEE
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shadcn
|
|
3
|
+
description: Add and configure shadcn/ui components — the copy-paste library built on Radix UI and Tailwind CSS — in a Next.js or React project via the shadcn CLI, including first-time init and adding single or multiple components. Use when the user wants to add UI components like buttons, dialogs, cards, forms, or tables.
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# Skill: shadcn/ui Components
|
|
2
7
|
|
|
3
8
|
Add and configure shadcn/ui components in a Next.js or React project.
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: slack-readonly
|
|
3
|
+
description: Search and read Slack messages, threads, and channels from the shell via read-only GET calls to the Slack Web API using a SLACK_TOKEN, with no posting, reactions, or edits by design. Use when asked to find, read, or summarize Slack content; refuse any request to send or write.
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# Slack (read-only) — search and read Slack from the shell
|
|
2
7
|
|
|
3
8
|
> 📅 **Last updated: 2026-08-09.** Recreated from scratch after the Mac data-loss events wiped the
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: voice-native-sync
|
|
3
|
+
description: Sync osborn sessions between the local Mac and cloud machines, starting with a mandatory served-version check that self-updates and aborts on a stale local copy to prevent unresumable-session corruption. Use when uploading or downloading sessions for the voice-native osborn setup.
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# Voice Native Sync Skill
|
|
2
7
|
|
|
3
8
|
## SKILL IDENTITY
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: youtube-transcript
|
|
3
|
+
description: Fetch a YouTube video's auto-generated English subtitles with yt-dlp, strip the SRT timing markers to clean transcript text, and save it to the session workspace. Use when the user asks to get a transcript, subtitles, captions, or summary from a YouTube video URL.
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# Skill: YouTube Transcript
|
|
2
7
|
|
|
3
8
|
Fetch and save transcripts from YouTube videos.
|
package/dist/claude-llm.js
CHANGED
|
@@ -199,6 +199,68 @@ function loadAllSkills(_workingDir) {
|
|
|
199
199
|
console.log(`📚 Loaded ${skillMap.size} skill(s) from ${homeSkillsDir}`);
|
|
200
200
|
return `<available-skills>\n${[...skillMap.values()].join('\n\n---\n\n')}\n</available-skills>`;
|
|
201
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Enumerate the agent's CURRENT skills (name + one-line description) from
|
|
204
|
+
* ~/.claude/skills, for injection into the PreCompact instruction. This is the
|
|
205
|
+
* list the compaction model dedupes/merges against so it stops re-emitting
|
|
206
|
+
* duplicate skills every session. Description is taken from YAML frontmatter
|
|
207
|
+
* (`description:`) when present, else the WHEN: line, else the first non-heading
|
|
208
|
+
* line. Returns one `- name: description` per line, or '' if none.
|
|
209
|
+
*/
|
|
210
|
+
function enumerateSkillsForCompaction() {
|
|
211
|
+
const dir = join(homedir(), '.claude', 'skills');
|
|
212
|
+
if (!existsSync(dir))
|
|
213
|
+
return '';
|
|
214
|
+
const lines = [];
|
|
215
|
+
try {
|
|
216
|
+
for (const name of readdirSync(dir).sort()) {
|
|
217
|
+
const file = join(dir, name, 'SKILL.md');
|
|
218
|
+
if (!existsSync(file))
|
|
219
|
+
continue;
|
|
220
|
+
let desc = '';
|
|
221
|
+
try {
|
|
222
|
+
const raw = readFileSync(file, 'utf-8');
|
|
223
|
+
const fm = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
224
|
+
if (fm) {
|
|
225
|
+
const m = fm[1].match(/^description:\s*(.+)$/m);
|
|
226
|
+
if (m)
|
|
227
|
+
desc = m[1].trim();
|
|
228
|
+
}
|
|
229
|
+
if (!desc) {
|
|
230
|
+
const body = raw.replace(/^---\n[\s\S]*?\n---\n?/, '');
|
|
231
|
+
const when = body.match(/^\s*WHEN:\s*(.+)$/mi);
|
|
232
|
+
if (when)
|
|
233
|
+
desc = when[1].trim();
|
|
234
|
+
}
|
|
235
|
+
if (!desc) {
|
|
236
|
+
const body = raw.replace(/^---\n[\s\S]*?\n---\n?/, '');
|
|
237
|
+
const first = body.split('\n').map(l => l.trim()).find(l => l && !l.startsWith('#'));
|
|
238
|
+
desc = first || '(no description)';
|
|
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(' | ')}`);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
lines.push(`- ${name}: (unreadable)`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
console.warn('⚠️ enumerateSkillsForCompaction failed:', err instanceof Error ? err.message : err);
|
|
261
|
+
}
|
|
262
|
+
return lines.join('\n');
|
|
263
|
+
}
|
|
202
264
|
// Compaction threshold: Fable 5 runs a 1M context window, so let sessions use
|
|
203
265
|
// all of it before auto-compacting. autoCompactWindow max is 1_000_000; the SDK
|
|
204
266
|
// reads it from settings.json (settingSources includes 'user'), so merge it into
|
|
@@ -2105,8 +2167,38 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
2105
2167
|
try {
|
|
2106
2168
|
this.#opts.onCompactionEvent?.({ type: 'compaction_started', trigger: input?.trigger });
|
|
2107
2169
|
const instructionPath = join(__claudeLlmDir, 'prompts', 'compact-learnings-instruction.md');
|
|
2108
|
-
const
|
|
2109
|
-
|
|
2170
|
+
const instructionRaw = existsSync(instructionPath) ? readFileSync(instructionPath, 'utf-8') : '';
|
|
2171
|
+
// Populate the EXISTING SKILLS section the instruction .md already refers to
|
|
2172
|
+
// (lines 42 & 55) but that nothing used to fill. Without this, the model is told
|
|
2173
|
+
// to "not re-emit skills already shown" against a list it never received — so it
|
|
2174
|
+
// proliferated duplicates. We hand it the current skill set + an adversarial,
|
|
2175
|
+
// self-critical directive to merge/refine rather than create anew.
|
|
2176
|
+
const existingSkills = enumerateSkillsForCompaction();
|
|
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;
|
|
2181
|
+
const criticBlock = existingSkills
|
|
2182
|
+
? `\n\n---\n\n=== EXISTING SKILLS (${skillCount}) ===\n`
|
|
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, `
|
|
2193
|
+
+ `re-emit it under the EXACT SAME kebab-case name, and only if it needs a substantive update; otherwise omit it.\n`
|
|
2194
|
+
+ `3. Propose a brand-new skill ONLY if nothing below covers it, it was CONFIRMED working this session, `
|
|
2195
|
+
+ `and it generalizes to future sessions on different tasks.\n`
|
|
2196
|
+
+ `4. Prefer refining/section-updating over proliferating. A small set of sharp, non-overlapping skills is the goal — `
|
|
2197
|
+
+ `reject your own low-signal or one-off candidates.\n\n`
|
|
2198
|
+
+ `${existingSkills}\n`
|
|
2199
|
+
: '';
|
|
2200
|
+
const instruction = instructionRaw + criticBlock;
|
|
2201
|
+
console.log(`🧠 PreCompact: injecting instruction (${instruction.length} chars, ${skillCount} existing skills, trigger=${input?.trigger || 'unknown'})`);
|
|
2110
2202
|
return { systemMessage: instruction };
|
|
2111
2203
|
}
|
|
2112
2204
|
catch (err) {
|
|
@@ -2166,7 +2258,7 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
2166
2258
|
const decPath = join(decFolder, 'SKILL.md');
|
|
2167
2259
|
mkdirSync(decFolder, { recursive: true });
|
|
2168
2260
|
const existing = existsSyncFs(decPath) ? readSyncFs(decPath, 'utf-8') : '';
|
|
2169
|
-
const header = existing ? '' :
|
|
2261
|
+
const header = existing ? '' : `---\nname: decisions\ndescription: "Project-scoped architectural and implementation decisions auto-extracted from compaction summaries; consult before revisiting settled choices."\nmetadata:\n type: learned\n source: postcompact\n---\n\n# Project Decisions\n\nAuto-extracted from compact summaries.\n\n`;
|
|
2170
2262
|
const entry = `\n## ${today} (session ${sessionId.substring(0, 8)})\n${projectLines.join('\n')}\n`;
|
|
2171
2263
|
writeSyncFs(decPath, header + existing + entry, 'utf-8');
|
|
2172
2264
|
console.log(`🧠 PostCompact: appended ${projectLines.length} decision(s) to ${decPath}`);
|
|
@@ -2196,8 +2288,14 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
2196
2288
|
const skillFolder = join(skillDir, '.claude', 'skills', name);
|
|
2197
2289
|
const skillPath = join(skillFolder, 'SKILL.md');
|
|
2198
2290
|
mkdirSync(skillFolder, { recursive: true });
|
|
2199
|
-
|
|
2200
|
-
|
|
2291
|
+
// Emit STANDARD skill format: YAML frontmatter (name + description) so
|
|
2292
|
+
// learned skills conform to the same shape the loader/other agents expect.
|
|
2293
|
+
// Description is derived from the candidate's WHEN: line; JSON.stringify
|
|
2294
|
+
// yields a safely-quoted YAML scalar even when it contains colons/quotes.
|
|
2295
|
+
const whenMatch = body.match(/^\s*WHEN:\s*(.+)$/mi);
|
|
2296
|
+
const desc = (whenMatch ? whenMatch[1].trim() : `Learned procedure: ${name}`).replace(/\s+/g, ' ').slice(0, 200);
|
|
2297
|
+
const frontmatter = `---\nname: ${name}\ndescription: ${JSON.stringify(desc)}\nmetadata:\n type: learned\n source: postcompact\n session: ${sessionId.substring(0, 8)}\n updated: ${today}\n---\n\n`;
|
|
2298
|
+
writeSyncFs(skillPath, frontmatter + `# ${name}\n\n` + body + '\n', 'utf-8');
|
|
2201
2299
|
console.log(`🧠 PostCompact: wrote skill '${name}' to ${skillPath}`);
|
|
2202
2300
|
skillsWritten++;
|
|
2203
2301
|
skillNames.push(name);
|
|
@@ -2208,6 +2306,64 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
2208
2306
|
catch (skillErr) {
|
|
2209
2307
|
console.error('⚠️ PostCompact: SKILL_CANDIDATES write failed:', skillErr instanceof Error ? skillErr.message : skillErr);
|
|
2210
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
|
+
}
|
|
2211
2367
|
// ── Section 4: BEHAVIORAL_LEARNINGS — write to learned-behaviors/SKILL.md ──
|
|
2212
2368
|
try {
|
|
2213
2369
|
const learnings = extractSection('=== BEHAVIORAL_LEARNINGS ===');
|
|
@@ -2216,7 +2372,7 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
2216
2372
|
const skillFolder = join(skillDir, '.claude', 'skills', 'learned-behaviors');
|
|
2217
2373
|
const skillPath = join(skillFolder, 'SKILL.md');
|
|
2218
2374
|
mkdirSync(skillFolder, { recursive: true });
|
|
2219
|
-
const header =
|
|
2375
|
+
const header = `---\nname: learned-behaviors\ndescription: "User corrections, preferences, domain knowledge, effective patterns, and anti-patterns learned across sessions; apply to align with how this user works."\nmetadata:\n type: learned\n source: postcompact\n updated: ${today}\n---\n\n# Learned Behaviors\n\nAuto-extracted from voice sessions via PostCompact.\nLast updated: ${today} | Session: ${sessionId.substring(0, 8)}...\n\n`;
|
|
2220
2376
|
writeSyncFs(skillPath, header + learnings + '\n', 'utf-8');
|
|
2221
2377
|
console.log(`🧠 PostCompact: wrote learned behaviors to ${skillPath} (${learnings.length} chars)`);
|
|
2222
2378
|
skillsWritten++;
|
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) });
|
package/package.json
CHANGED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
You are analyzing a voice AI assistant conversation to extract behavioral learnings that should persist across sessions.
|
|
2
|
-
|
|
3
|
-
The conversation is between a user and "Osborn" — a voice AI thinking partner. Your job is to identify:
|
|
4
|
-
|
|
5
|
-
1. USER CORRECTIONS — things the user explicitly told the agent to stop doing or start doing differently
|
|
6
|
-
2. USER PREFERENCES — recurring patterns in how the user wants to work (tools, approaches, communication style)
|
|
7
|
-
3. DOMAIN KNOWLEDGE — specific technical facts learned during the session (API behaviors, selectors, platform quirks, vendor-specific details)
|
|
8
|
-
4. EFFECTIVE PATTERNS — approaches that worked well and the user confirmed or accepted without pushback
|
|
9
|
-
5. ANTI-PATTERNS — approaches that failed, got the user frustrated, or had to be abandoned
|
|
10
|
-
|
|
11
|
-
For each item, include:
|
|
12
|
-
- The specific learning (concrete, actionable)
|
|
13
|
-
- Brief context for WHY (so future sessions can judge if it still applies)
|
|
14
|
-
- Confidence level: HIGH (user explicitly stated it), MEDIUM (inferred from user behavior), LOW (observed but not confirmed)
|
|
15
|
-
|
|
16
|
-
Output as markdown in this exact format:
|
|
17
|
-
|
|
18
|
-
```markdown
|
|
19
|
-
# Session Learnings — {date}
|
|
20
|
-
|
|
21
|
-
## User Corrections (HIGH confidence)
|
|
22
|
-
- {correction}: {context}
|
|
23
|
-
|
|
24
|
-
## User Preferences (MEDIUM-HIGH confidence)
|
|
25
|
-
- {preference}: {context}
|
|
26
|
-
|
|
27
|
-
## Domain Knowledge Learned (varies)
|
|
28
|
-
- [{confidence}] {fact}: {how it was verified}
|
|
29
|
-
|
|
30
|
-
## Effective Patterns (MEDIUM confidence)
|
|
31
|
-
- {pattern}: {when it worked}
|
|
32
|
-
|
|
33
|
-
## Anti-Patterns to Avoid (HIGH confidence)
|
|
34
|
-
- {anti-pattern}: {what went wrong}
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Be SELECTIVE. Only include items that are:
|
|
38
|
-
- Generalizable to future sessions (not one-off task details)
|
|
39
|
-
- Actionable (the agent can actually change behavior based on this)
|
|
40
|
-
- Non-obvious (things the agent wouldn't know from its system prompt alone)
|
|
41
|
-
|
|
42
|
-
Do NOT include:
|
|
43
|
-
- Task-specific details (file paths, variable names, specific code changes)
|
|
44
|
-
- Things already in the system prompt
|
|
45
|
-
- Trivial confirmations or greetings
|
|
46
|
-
- Speculative patterns not grounded in the conversation
|