osborn 0.8.15 → 0.8.17

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.
@@ -0,0 +1,114 @@
1
+ # Skill: Browser Apply — Step-by-Step Workday Application
2
+
3
+ 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.
4
+
5
+ **This skill uses the Playwright MCP tools** (`mcp__playwright__browser_*`) for direct browser control — no scripts needed.
6
+
7
+ ## When to Use
8
+ - Any Workday ATS application (`*.wd1.myworkdayjobs.com`)
9
+ - Any multi-step JS-heavy job application form
10
+ - When you want visible, confirmable progress at each step
11
+
12
+ ## Key Principle: Step-by-Step, Not One Big Script
13
+
14
+ Do NOT write a monolithic automation script. Instead:
15
+ 1. Navigate to the URL
16
+ 2. Take a screenshot — confirm what's on screen
17
+ 3. Fill only the current step's fields
18
+ 4. Take another screenshot — confirm fields filled correctly
19
+ 5. Ask the user "Ready for next step?" before clicking Next
20
+ 6. Click Next, wait for page load, screenshot again
21
+ 7. Repeat for each step
22
+
23
+ This approach catches rendering issues, unexpected fields, and errors before they cascade.
24
+
25
+ ## Step-by-Step Execution Pattern
26
+
27
+ ### Step 0 — Open the browser
28
+ Use: `mcp__playwright__browser_navigate` with the applyManually URL
29
+
30
+ Then: `mcp__playwright__browser_take_screenshot` — show the user what loaded
31
+
32
+ ### Step 1 — Create Account / Sign In
33
+ Take a snapshot with `mcp__playwright__browser_snapshot` to see element refs.
34
+ Fill fields using `mcp__playwright__browser_fill_form` or individual `mcp__playwright__browser_type` calls.
35
+ Screenshot to confirm. Then ask user before clicking Create Account / Sign In.
36
+
37
+ ### Step 2 — Start Application
38
+ If "Start Your Application" screen appears with Apply Manually button:
39
+ Screenshot it. Click "Apply Manually" using `mcp__playwright__browser_click`.
40
+ Screenshot after.
41
+
42
+ ### Step 3 — My Information
43
+ Snapshot → fill each field → screenshot → ask user before clicking Next.
44
+
45
+ Fields to fill:
46
+ - First Name, Last Name, Phone
47
+ - Address, City, State (dropdown), Zip
48
+ - Work authorization: Yes
49
+ - Sponsorship: No
50
+
51
+ ### Step 4 — My Experience
52
+ Snapshot → click Add for each job entry → fill title/company/dates/description → save each → screenshot.
53
+ Then add education entries.
54
+ Ask user before clicking Next.
55
+
56
+ ### Step 5 — Application Questions
57
+ Snapshot to see all questions. Fill each one. **Always confirm salary expectation with user before filling** — never guess. Screenshot. Ask before Next.
58
+
59
+ ### Step 6 — Voluntary Disclosures
60
+ Select "I do not wish to answer" / "Prefer not to disclose" for all. Screenshot. Ask before Next.
61
+
62
+ ### Step 7 — Self Identify
63
+ Fill name and date. Select disability option. Screenshot. Ask before Next.
64
+
65
+ ### Step 8 — Review
66
+ Screenshot the full review page. Confirm with user before clicking Submit.
67
+
68
+ ### Step 9 — Confirm submission
69
+ Screenshot the confirmation dialog. Save it.
70
+
71
+ ## Candidate Data (Osborn Ojure)
72
+
73
+ - Email: osbornojure@gmail.com
74
+ - Password: Workday2026!
75
+ - First: Osborn, Last: Ojure
76
+ - Phone: 3127185561
77
+ - Address: 1234 N Michigan Ave, Chicago, IL 60601
78
+
79
+ Jobs:
80
+ 1. Meta API Consultant at Prehype / Audos — April 2024 to Present
81
+ 2. Full Stack Developer, Freelance — January 2016 to Present
82
+
83
+ Education:
84
+ 1. A.S. Information Systems
85
+ 2. B.S. Psychology
86
+
87
+ ## Workday data-automation-id Selector Reference
88
+
89
+ | Field | Selector |
90
+ |---|---|
91
+ | Email | `input[type="email"]` |
92
+ | Password | `input[type="password"]` |
93
+ | First name | `[data-automation-id="legalNameSection_firstName"]` |
94
+ | Last name | `[data-automation-id="legalNameSection_lastName"]` |
95
+ | Phone | `[data-automation-id="phone-number"]` |
96
+ | Address | `[data-automation-id="addressSection_addressLine1"]` |
97
+ | City | `[data-automation-id="addressSection_city"]` |
98
+ | Zip | `[data-automation-id="addressSection_postalCode"]` |
99
+ | Job title | `[data-automation-id="jobTitle"]` |
100
+ | Company | `[data-automation-id="company"]` |
101
+ | Description | `[data-automation-id="description"]` |
102
+ | Next button | `[data-automation-id="bottom-navigation-next-btn"]` |
103
+ | Create Account | `[data-automation-id="click_filter"][aria-label="Create Account"]` |
104
+
105
+ ## Critical Rules
106
+ - headless: false always (Workday renders blank in headless)
107
+ - Confirm salary with user before every submission — never auto-fill
108
+ - After "You already applied to this job" error — that confirms a previous submission worked
109
+ - Use `{ force: true }` on Workday buttons — overlay click filters block normal clicks
110
+ - Always wait for networkidle or waitForSelector after navigation before interacting
111
+
112
+ ## Playwright Install Location
113
+ Run scripts from: `/Users/newupgrade/Desktop/Developer/osborn/frontend`
114
+ (playwright is in `node_modules` there)
package/.env.example CHANGED
@@ -19,3 +19,10 @@ ANTHROPIC_API_KEY=sk-ant-...
19
19
  # Smithery (cloud-hosted MCP servers - YouTube, GitHub, etc.)
20
20
  # Get your key at: https://smithery.ai
21
21
  # SMITHERY_API_KEY=your-smithery-api-key
22
+
23
+ # Frontend URL — used by the agent to upload workspace artifacts (resumes,
24
+ # reports, PDFs, search indexes) to Supabase Storage via the frontend's
25
+ # /api/upload route. Without this, the agent falls back to inlining file
26
+ # content through the LiveKit data channel (capped at 30KB to avoid
27
+ # corrupting the publisher connection). Local dev: http://localhost:3000
28
+ # OSBORN_FRONTEND_URL=http://localhost:3000
@@ -0,0 +1,50 @@
1
+ const { chromium } = require('playwright');
2
+
3
+ (async () => {
4
+ const browser = await chromium.launch({ headless: false });
5
+ const page = await browser.newPage();
6
+
7
+ await page.goto('https://caresource.wd1.myworkdayjobs.com/CareSource/job/Remote/AI-Developer_R10487/apply', { waitUntil: 'networkidle' });
8
+ await page.waitForTimeout(2000);
9
+
10
+ // Click Apply Manually
11
+ console.log('Clicking Apply Manually...');
12
+ await page.click('text=Apply Manually');
13
+ await page.waitForTimeout(3000);
14
+
15
+ // Fill in email
16
+ console.log('Filling email field...');
17
+ const emailInput = await page.$('input[type="email"]');
18
+ if (emailInput) {
19
+ await emailInput.fill('osbornojure@gmail.com');
20
+ console.log('Email filled: osbornojure@gmail.com');
21
+ }
22
+
23
+ // Fill in password
24
+ console.log('Filling password fields...');
25
+ const passwordInputs = await page.$$('input[type="password"]');
26
+ if (passwordInputs.length >= 2) {
27
+ await passwordInputs[0].fill('workday2026!');
28
+ await passwordInputs[1].fill('workday2026!');
29
+ console.log('Passwords filled');
30
+ }
31
+
32
+ await page.waitForTimeout(1000);
33
+
34
+ // Click Create Account
35
+ const createBtn = await page.$('text=Create Account');
36
+ if (createBtn) {
37
+ console.log('Clicking Create Account...');
38
+ await createBtn.click();
39
+ await page.waitForTimeout(3000);
40
+ }
41
+
42
+ // Take screenshot
43
+ await page.screenshot({ path: '/tmp/caresource-step1.png' });
44
+ console.log('Screenshot saved to /tmp/caresource-step1.png');
45
+
46
+ // Get current page text
47
+ const text = await page.evaluate(() => document.body.innerText);
48
+ console.log('\n--- PAGE CONTENT ---');
49
+ console.log(text);
50
+ })();
@@ -0,0 +1,34 @@
1
+ import { chromium } from 'playwright';
2
+
3
+ (async () => {
4
+ const browser = await chromium.launch({ headless: false });
5
+ const page = await browser.newPage();
6
+
7
+ console.log('🌐 Navigating to CareSource...');
8
+ await page.goto('https://caresource.wd1.myworkdayjobs.com/CareSource/job/Remote/AI-Developer_R10487/apply', { waitUntil: 'networkidle' });
9
+ await page.waitForTimeout(2000);
10
+
11
+ console.log('📋 Clicking Apply Manually...');
12
+ await page.click('text=Apply Manually');
13
+ await page.waitForTimeout(3000);
14
+
15
+ console.log('📧 Filling email...');
16
+ const emailInput = await page.$('input[type="email"]');
17
+ if (emailInput) {
18
+ await emailInput.fill('osbornojure@gmail.com');
19
+ }
20
+
21
+ console.log('🔐 Filling passwords...');
22
+ const passwordInputs = await page.$$('input[type="password"]');
23
+ if (passwordInputs.length >= 2) {
24
+ await passwordInputs[0].fill('workday2026!');
25
+ await passwordInputs[1].fill('workday2026!');
26
+ }
27
+
28
+ await page.waitForTimeout(1000);
29
+ await page.screenshot({ path: '/tmp/caresource-step1.png' });
30
+
31
+ console.log('✅ Account form filled. Screenshot saved.');
32
+ console.log('\n📝 Browser is now open. You can review before submitting.');
33
+ console.log('Keep this window open and ready to proceed.\n');
34
+ })();
@@ -17,6 +17,13 @@ import { fileURLToPath } from 'node:url';
17
17
  // Directory of this module — used to locate co-located prompt files (e.g., turn-shape reminder).
18
18
  const __claudeLlmDir = dirname(fileURLToPath(import.meta.url));
19
19
  const TURN_SHAPE_REMINDER_PATH = join(__claudeLlmDir, 'prompts', 'turn-shape-reminder.md');
20
+ // ≤3 direct tool call budget per turn. Reset on every UserPromptSubmit (new user message).
21
+ // Enforced mechanically in PreToolUse — the model CANNOT exceed this regardless of JSONL history.
22
+ // Task/Agent delegations are exempt (delegation is what we WANT). Sub-agent tool calls
23
+ // (agent_type !== null) are exempt (they're inside a delegation). Only the main orchestrator
24
+ // agent's direct tool calls count against the budget.
25
+ let turnToolCallCount = 0;
26
+ const TOOL_CALL_BUDGET = 3;
20
27
  /**
21
28
  * Strip markdown formatting for TTS (text-to-speech)
22
29
  * Removes **bold**, ##headers, ```code```, etc. so TTS doesn't read them literally
@@ -750,8 +757,8 @@ class ClaudeLLMStream extends llm.LLMStream {
750
757
  cwd: this.#opts.workingDirectory,
751
758
  permissionMode: this.#opts.permissionMode,
752
759
  allowedTools,
753
- model: this.#opts.model || 'haiku', // haiku for speed with limited tools, sonnet for full research capabilities (including tool use trace in response)
754
- // model: this.#opts.model || 'claude-sonnet-4-6', // Sonnet orchestrator with named sub-agents (Haiku tested but ignored delegation rules)
760
+ // model: this.#opts.model || 'haiku', // haiku for speed with limited tools, sonnet for full research capabilities (including tool use trace in response)
761
+ model: this.#opts.model || 'claude-sonnet-4-6', // Sonnet orchestrator with named sub-agents (Haiku tested but ignored delegation rules)
755
762
  enableFileCheckpointing: true,
756
763
  extraArgs: { 'replay-user-messages': null },
757
764
  ...(this.#abortController && { abortController: this.#abortController }),
@@ -824,6 +831,22 @@ class ClaudeLLMStream extends llm.LLMStream {
824
831
  const toolInput = input?.tool_input || {};
825
832
  const agentType = input?.agent_type || null;
826
833
  console.log(`🔍 PreToolUse: toolName=${toolName} agent_type=${agentType} agent_id=${input?.agent_id || 'none'} all_keys=[${Object.keys(input || {}).join(', ')}]`);
834
+ // ≤3 direct tool call budget enforcement.
835
+ // Only counts calls from the MAIN orchestrator agent (agent_type === null).
836
+ // Task/Agent delegations are exempt — delegation is the desired behavior.
837
+ // Sub-agent tool calls are exempt — they're inside a delegation.
838
+ if (!agentType && toolName !== 'Task' && toolName !== 'Agent') {
839
+ turnToolCallCount++;
840
+ if (turnToolCallCount > TOOL_CALL_BUDGET) {
841
+ console.log(`🛑 Tool budget exceeded (${turnToolCallCount}/${TOOL_CALL_BUDGET}) — DENYING ${toolName}. Must delegate via Task.`);
842
+ this.#eventEmitter.emit('tool_blocked', { name: toolName, reason: `Tool call budget exceeded (${turnToolCallCount}/${TOOL_CALL_BUDGET}). Delegate via Task.` });
843
+ return {
844
+ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny' },
845
+ reason: `Hard limit: maximum ${TOOL_CALL_BUDGET} direct tool calls per turn (you are at ${turnToolCallCount}). Delegate the remaining work to a sub-agent via Task(subagent_type=\'researcher\'|\'writer\'|\'reasoner\', run_in_background: true). This is a system-enforced limit.`,
846
+ };
847
+ }
848
+ console.log(`🔧 Tool call ${turnToolCallCount}/${TOOL_CALL_BUDGET}: ${toolName}`);
849
+ }
827
850
  // Write/Edit/MultiEdit access control
828
851
  if (toolName === 'Write' || toolName === 'Edit' || toolName === 'MultiEdit') {
829
852
  // Writer sub-agent gets full write access everywhere
@@ -871,9 +894,11 @@ class ClaudeLLMStream extends llm.LLMStream {
871
894
  matcher: '.*',
872
895
  hooks: [async (input) => {
873
896
  try {
897
+ // Reset the per-turn tool call counter so the new turn starts fresh.
898
+ turnToolCallCount = 0;
874
899
  const reminder = readFileSync(TURN_SHAPE_REMINDER_PATH, 'utf-8');
875
900
  const promptPreview = String(input?.prompt || '').substring(0, 60).replace(/\n/g, ' ');
876
- console.log(`📌 UserPromptSubmit: injected turn-shape reminder (${reminder.length} chars) for prompt="${promptPreview}..."`);
901
+ console.log(`📌 UserPromptSubmit: injected turn-shape reminder (${reminder.length} chars) for prompt="${promptPreview}..." [tool budget reset to 0/${TOOL_CALL_BUDGET}]`);
877
902
  return {
878
903
  hookSpecificOutput: {
879
904
  hookEventName: 'UserPromptSubmit',
package/dist/index.js CHANGED
@@ -823,8 +823,13 @@ async function main() {
823
823
  // payloads corrupts the publisher transport, killing ALL subsequent sends.
824
824
  // We enforce a soft limit (truncate text/content fields) and a hard limit
825
825
  // (drop the message entirely with a warning) to prevent this.
826
- const MAX_MESSAGE_SIZE = 60000; // soft limit truncate text/content fields
827
- const HARD_MAX_MESSAGE_SIZE = 200000; // hard limit drop if still too large after truncation
826
+ // ⚠️ These limits protect the LiveKit SCTP publisher peer connection.
827
+ // During session resume, 12 artifact requests arrive simultaneously and the agent
828
+ // sends responses back-to-back. If the cumulative payload exceeds the SCTP buffer
829
+ // (~50-100 KB in rapid fire), the publisher PC enters a zombie state and NEVER
830
+ // recovers — the user hears nothing for the rest of the connection. Keep these low.
831
+ const MAX_MESSAGE_SIZE = 30000; // 30KB soft limit — truncate text/content fields
832
+ const HARD_MAX_MESSAGE_SIZE = 50000; // 50KB hard limit — drop if still too large after truncation
828
833
  async function sendToFrontend(data) {
829
834
  if (!localParticipant) {
830
835
  console.log('⚠️ sendToFrontend: no localParticipant!');
@@ -2698,34 +2703,85 @@ async function main() {
2698
2703
  if (filePath && (filePath.includes('/osb/') || filePath.includes('.osborn/sessions/') || filePath.includes('.osborn/research/'))) {
2699
2704
  try {
2700
2705
  const fs = await import('fs');
2706
+ const path = await import('path');
2701
2707
  const fileName = filePath.split('/').pop() || '';
2702
2708
  const ext = fileName.split('.').pop()?.toLowerCase() || '';
2703
2709
  const isImage = ['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(ext);
2704
- // WebRTC SCTP data channel max message size is ~256KB. Sending
2705
- // larger payloads corrupts the publisher transport, killing ALL
2706
- // subsequent sends (publishData, streamBytes, publishTranscription)
2707
- // with "could not establish publisher connection: timeout". This
2708
- // was the root cause of the career-ops session bug: a 480KB
2709
- // evaluation report blew through the limit on resume.
2710
- const MAX_DATA_CHANNEL_BYTES = 200_000; // 200KB — safe margin under 256KB SCTP limit
2711
- if (isImage) {
2710
+ const mimeByExt = {
2711
+ png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
2712
+ gif: 'image/gif', webp: 'image/webp', pdf: 'application/pdf',
2713
+ html: 'text/html', md: 'text/markdown', txt: 'text/plain',
2714
+ json: 'application/json',
2715
+ };
2716
+ const mimeType = mimeByExt[ext] || 'application/octet-stream';
2717
+ // Strategy: upload the file to Supabase Storage via the frontend's
2718
+ // /api/upload route and send back just the URL. This mirrors the
2719
+ // existing frontend→agent attachment flow (where the browser uploads
2720
+ // user attachments to Supabase and passes URLs to the agent). For
2721
+ // the reverse direction we do the same: URLs are ~100 bytes, so
2722
+ // the LiveKit data channel stays healthy regardless of file size.
2723
+ //
2724
+ // Fallback to inline send if OSBORN_FRONTEND_URL isn't configured
2725
+ // OR the upload fails — with a small size cap so we don't kill the
2726
+ // publisher PC with a 480KB payload (see earlier career-ops bug).
2727
+ const FRONTEND_URL = process.env.OSBORN_FRONTEND_URL || process.env.NEXT_PUBLIC_FRONTEND_URL || '';
2728
+ const MAX_INLINE_BYTES = 30_000; // fallback-only cap
2729
+ let uploadedUrl = null;
2730
+ if (FRONTEND_URL) {
2731
+ try {
2732
+ const buf = fs.readFileSync(filePath);
2733
+ const form = new FormData();
2734
+ form.append('file', new Blob([buf], { type: mimeType }), fileName);
2735
+ form.append('folder', 'artifacts');
2736
+ const r = await fetch(`${FRONTEND_URL.replace(/\/$/, '')}/api/upload`, {
2737
+ method: 'POST', body: form,
2738
+ signal: AbortSignal.timeout(15_000),
2739
+ });
2740
+ if (r.ok) {
2741
+ const j = await r.json();
2742
+ if (j.success && j.url) {
2743
+ uploadedUrl = j.url;
2744
+ console.log(`☁️ Uploaded artifact to Supabase: ${fileName} (${(buf.length / 1024).toFixed(0)}KB) → ${j.url.substring(0, 80)}...`);
2745
+ }
2746
+ else {
2747
+ console.warn(`⚠️ Upload failed for ${fileName}: ${j.error || 'unknown'}`);
2748
+ }
2749
+ }
2750
+ else {
2751
+ console.warn(`⚠️ Upload HTTP ${r.status} for ${fileName}`);
2752
+ }
2753
+ }
2754
+ catch (err) {
2755
+ console.warn(`⚠️ Upload threw for ${fileName}:`, err.message);
2756
+ }
2757
+ }
2758
+ if (uploadedUrl) {
2759
+ // Success path — send URL, no inline content.
2760
+ await sendToFrontend({
2761
+ type: 'research_artifact_content',
2762
+ filePath, fileName, url: uploadedUrl,
2763
+ isImage, mimeType,
2764
+ });
2765
+ }
2766
+ else if (isImage) {
2767
+ // Fallback: inline image (with size cap)
2712
2768
  const stats = fs.statSync(filePath);
2713
- const base64Size = Math.ceil(stats.size * 4 / 3); // base64 inflates ~33%
2714
- if (base64Size > MAX_DATA_CHANNEL_BYTES) {
2715
- console.log(`⚠️ Artifact too large for data channel: ${fileName} (${(base64Size / 1024).toFixed(0)}KB base64) — sending truncation notice`);
2769
+ const base64Size = Math.ceil(stats.size * 4 / 3);
2770
+ if (base64Size > MAX_INLINE_BYTES) {
2771
+ console.log(`⚠️ Artifact too large for inline fallback: ${fileName} (${(base64Size / 1024).toFixed(0)}KB base64) — sending truncation notice`);
2716
2772
  await sendToFrontend({ type: 'research_artifact_content', filePath, content: '', fileName, isImage: false, truncated: true, originalSize: stats.size });
2717
2773
  }
2718
2774
  else {
2719
2775
  const base64 = fs.readFileSync(filePath, 'base64');
2720
- await sendToFrontend({ type: 'research_artifact_content', filePath, content: base64, fileName, isImage: true, mimeType: `image/${ext}` });
2776
+ await sendToFrontend({ type: 'research_artifact_content', filePath, content: base64, fileName, isImage: true, mimeType });
2721
2777
  }
2722
2778
  }
2723
2779
  else {
2780
+ // Fallback: inline text (with size cap)
2724
2781
  const content = fs.readFileSync(filePath, 'utf-8');
2725
- if (Buffer.byteLength(content, 'utf-8') > MAX_DATA_CHANNEL_BYTES) {
2726
- // Send a truncated preview + metadata so the frontend knows the file exists
2727
- const truncated = content.substring(0, 50_000); // ~50KB text preview
2728
- console.log(`⚠️ Artifact too large for data channel: ${fileName} (${(Buffer.byteLength(content, 'utf-8') / 1024).toFixed(0)}KB) — sending truncated preview`);
2782
+ if (Buffer.byteLength(content, 'utf-8') > MAX_INLINE_BYTES) {
2783
+ const truncated = content.substring(0, 5_000);
2784
+ console.log(`⚠️ Artifact too large for inline fallback: ${fileName} (${(Buffer.byteLength(content, 'utf-8') / 1024).toFixed(0)}KB) sending truncated preview`);
2729
2785
  await sendToFrontend({ type: 'research_artifact_content', filePath, content: truncated, fileName, isImage: false, truncated: true, originalSize: Buffer.byteLength(content, 'utf-8') });
2730
2786
  }
2731
2787
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.8.15",
3
+ "version": "0.8.17",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {