feat(skills): single-source skill system with markdown SKILL.md + include directive

Skills move out of packages/core/src/application/assistant/skills/*/skill.ts
(TS string constants) into apps/skills/<id>/SKILL.md (Agent Skills spec format
— YAML frontmatter + markdown body). One directory, one loader, one place to
look at every skill the agent can load.

Key change vs the old dev system: a `{{include:<skill-id>}}` directive lets one
skill transclude another. This removes the parallel TS constant for the
knowledge-note style guide — it now lives at apps/skills/knowledge-note-style/
(hidden from catalog) and is pulled into doc-collab + the live-note and
background-task agents via the resolver instead of via a TS import.

Infrastructure:
- packages/core/src/skills/ — types, skill-md-parser, FS-backed official repo,
  SkillResolver with recursive {{include:<id>}} expansion + cycle detection
- packages/shared/src/skill.ts — SkillFrontmatter, SkillCatalogEntry,
  ResolvedSkill schemas
- DI: officialSkillsRepo + skillResolver registered; registerSkillsDir helper
  wires the path before any consumer resolves
- IPC: skills:list / skills:get (read-only) for the Settings UI
- Main: resolveSkillsDir picks Resources/skills (packaged) or repo apps/skills
  (dev). forge.config.cjs ships apps/skills/ as extraResource.

Consumer refactor:
- buildCopilotInstructions: catalog markdown built from resolver.getCatalog()
- builtin-tools: loadSkill uses resolver, new listSkills tool
- background-tasks/agent + live-note/agent: now async builders that load
  the knowledge-note-style skill content via resolver
- runtime.loadAgent: awaits the now-async builders
- Deleted: assistant/skills/ directory, knowledge-note-style.ts

UI:
- New SkillsSettings component (read-only list + detail view) wired into
  Settings dialog as the "Skills" tab.
This commit is contained in:
tusharmagar 2026-05-13 12:31:06 +05:30
parent b01af12148
commit 9a308cb7a9
38 changed files with 1217 additions and 1204 deletions

View file

@ -14,6 +14,9 @@ module.exports = {
protocols: [
{ name: 'Rowboat', schemes: ['rowboat'] },
],
// Bundles <repo>/apps/skills/ into Resources/skills/ in the packaged app.
// Read at runtime via process.resourcesPath in main.ts (resolveSkillsDir).
extraResource: [path.join(__dirname, '../../../skills')],
extendInfo: {
NSAudioCaptureUsageDescription: 'Rowboat needs access to system audio to transcribe meetings from other apps (Zoom, Meet, etc.)',
},

View file

@ -67,6 +67,7 @@ import {
listTasks,
readRunIds as readTaskRunIds,
} from '@x/core/dist/background-tasks/fileops.js';
import type { ISkillResolver } from '@x/core/dist/skills/resolver.js';
import { browserIpcHandlers } from './browser/ipc.js';
/**
@ -956,6 +957,16 @@ export function setupIpcHandlers() {
const runIds = await readTaskRunIds(args.slug, args.limit);
return { runIds };
},
// Skills handlers (read-only)
'skills:list': async () => {
const resolver = container.resolve<ISkillResolver>('skillResolver');
const skills = await resolver.getCatalog();
return { skills };
},
'skills:get': async (_event, args) => {
const resolver = container.resolve<ISkillResolver>('skillResolver');
return await resolver.resolve(args.id);
},
// Billing handler
'billing:getInfo': async () => {
return await getBillingInfo();

View file

@ -40,7 +40,7 @@ import started from "electron-squirrel-startup";
import { execSync, exec, execFileSync } from "node:child_process";
import { promisify } from "node:util";
import { init as initChromeSync } from "@x/core/dist/knowledge/chrome-extension/server/server.js";
import { registerBrowserControlService, registerNotificationService } from "@x/core/dist/di/container.js";
import { registerBrowserControlService, registerNotificationService, registerSkillsDir } from "@x/core/dist/di/container.js";
import { browserViewManager, BROWSER_PARTITION } from "./browser/view.js";
import { setupBrowserEventForwarding } from "./browser/ipc.js";
import { ElectronBrowserControlService } from "./browser/control-service.js";
@ -57,6 +57,16 @@ const execAsync = promisify(exec);
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
function resolveSkillsDir(): string {
if (app.isPackaged) {
// forge.config.cjs ships apps/skills/ as extraResource → Resources/skills/
return path.join(process.resourcesPath, "skills");
}
// Dev: main runs from apps/x/apps/main/.package/dist/main.cjs,
// so the repo's apps/skills lives 5 levels up + over one.
return path.resolve(__dirname, "..", "..", "..", "..", "..", "skills");
}
// run this as early in the main process as possible
if (started) app.quit();
@ -313,6 +323,9 @@ app.whenReady().then(async () => {
registerBrowserControlService(new ElectronBrowserControlService());
registerNotificationService(new ElectronNotificationService());
// Skills ship bundled with the app. Register the source directory before any
// consumer (resolver, IPC handlers, copilot instructions) resolves.
registerSkillsDir(resolveSkillsDir());
setupIpcHandlers();
setupBrowserEventForwarding();

View file

@ -2,7 +2,7 @@
import * as React from "react"
import { useState, useEffect, useCallback, useMemo } from "react"
import { Server, Key, Shield, Palette, Monitor, Sun, Moon, Loader2, CheckCircle2, Plus, X, Wrench, Search, ChevronRight, Link2, Tags, Mail, BookOpen, User, Plug } from "lucide-react"
import { Server, Key, Shield, Palette, Monitor, Sun, Moon, Loader2, CheckCircle2, Plus, X, Wrench, Search, ChevronRight, Link2, Tags, Mail, BookOpen, User, Plug, Sparkles } from "lucide-react"
import {
Dialog,
@ -24,8 +24,9 @@ import { useTheme } from "@/contexts/theme-context"
import { toast } from "sonner"
import { AccountSettings } from "@/components/settings/account-settings"
import { ConnectedAccountsSettings } from "@/components/settings/connected-accounts-settings"
import { SkillsSettings } from "@/components/settings/skills-settings"
type ConfigTab = "account" | "connected-accounts" | "models" | "mcp" | "security" | "appearance" | "tools" | "note-tagging"
type ConfigTab = "account" | "connected-accounts" | "models" | "mcp" | "security" | "appearance" | "tools" | "note-tagging" | "skills"
interface TabConfig {
id: ConfigTab
@ -88,6 +89,12 @@ const tabs: TabConfig[] = [
path: "config/tags.json",
description: "Configure tags for notes and emails",
},
{
id: "skills",
label: "Skills",
icon: Sparkles,
description: "View bundled copilot skills",
},
]
interface SettingsDialogProps {
@ -1715,7 +1722,7 @@ export function SettingsDialog({ children }: SettingsDialogProps) {
</div>
{/* Content */}
<div className={cn("flex-1 p-4 min-h-0", (activeTab === "models" || activeTab === "tools" || activeTab === "account" || activeTab === "connected-accounts") ? "overflow-y-auto" : activeTab === "note-tagging" ? "overflow-hidden flex flex-col" : "overflow-hidden")}>
<div className={cn("flex-1 p-4 min-h-0", (activeTab === "models" || activeTab === "tools" || activeTab === "account" || activeTab === "connected-accounts") ? "overflow-y-auto" : (activeTab === "note-tagging" || activeTab === "skills") ? "overflow-hidden flex flex-col" : "overflow-hidden")}>
{activeTab === "account" ? (
<AccountSettings dialogOpen={open} />
) : activeTab === "connected-accounts" ? (
@ -1728,6 +1735,8 @@ export function SettingsDialog({ children }: SettingsDialogProps) {
<NoteTaggingSettings dialogOpen={open} />
) : activeTab === "appearance" ? (
<AppearanceSettings />
) : activeTab === "skills" ? (
<SkillsSettings dialogOpen={open} />
) : activeTab === "tools" ? (
<ToolsLibrarySettings dialogOpen={open} rowboatConnected={rowboatConnected} />
) : loading ? (

View file

@ -0,0 +1,118 @@
"use client"
import { useState, useEffect, useCallback } from "react"
import { ArrowLeft } from "lucide-react"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import { toast } from "sonner"
import type { skill as skillShared } from "@x/shared"
type SkillCatalogEntry = skillShared.SkillCatalogEntry
type ResolvedSkill = skillShared.ResolvedSkill
interface SkillsSettingsProps {
dialogOpen: boolean
}
export function SkillsSettings({ dialogOpen }: SkillsSettingsProps) {
const [skills, setSkills] = useState<SkillCatalogEntry[]>([])
const [loading, setLoading] = useState(true)
const [selectedSkillId, setSelectedSkillId] = useState<string | null>(null)
const [selectedSkill, setSelectedSkill] = useState<ResolvedSkill | null>(null)
const loadSkills = useCallback(async () => {
try {
setLoading(true)
const result = await window.ipc.invoke("skills:list", null)
setSkills(result.skills)
} catch (err) {
console.error("Failed to load skills:", err)
toast.error("Failed to load skills")
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
if (dialogOpen) loadSkills()
}, [dialogOpen, loadSkills])
const handleSelectSkill = useCallback(async (skillId: string) => {
try {
const skill = await window.ipc.invoke("skills:get", { id: skillId })
if (skill) {
setSelectedSkillId(skillId)
setSelectedSkill(skill)
}
} catch (err) {
console.error("Failed to load skill:", err)
toast.error("Failed to load skill")
}
}, [])
if (loading) {
return (
<div className="h-full flex items-center justify-center text-sm text-muted-foreground">
Loading skills...
</div>
)
}
if (selectedSkillId && selectedSkill) {
return (
<div className="flex flex-col h-full overflow-hidden gap-3">
<div className="flex items-center gap-2 shrink-0">
<Button
variant="ghost"
size="sm"
onClick={() => {
setSelectedSkillId(null)
setSelectedSkill(null)
}}
className="h-8 px-2"
>
<ArrowLeft className="h-4 w-4 mr-1" />
Back
</Button>
<div className="flex-1 min-w-0">
<div className="font-medium text-sm truncate">{selectedSkill.title}</div>
<div className="text-xs text-muted-foreground truncate">{selectedSkill.summary}</div>
</div>
</div>
<div className="flex-1 overflow-y-auto rounded-md border bg-muted/20 p-3">
<pre className="text-xs font-mono whitespace-pre-wrap">{selectedSkill.content}</pre>
</div>
</div>
)
}
return (
<div className="flex flex-col h-full overflow-hidden">
<p className="text-xs text-muted-foreground shrink-0 mb-3">
Skills are read-only guidance bundled with the app. Updates ship with new app releases.
</p>
<div className="flex-1 overflow-y-auto -mx-1 px-1 space-y-1">
{skills.length === 0 ? (
<div className="text-sm text-muted-foreground text-center py-8">
No skills available.
</div>
) : (
skills.map((skill) => (
<button
key={skill.id}
onClick={() => handleSelectSkill(skill.id)}
className={cn(
"w-full text-left p-3 rounded-md border bg-card hover:bg-accent transition-colors",
)}
>
<div className="font-medium text-sm">{skill.title}</div>
<div className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
{skill.summary}
</div>
</button>
))
)}
</div>
</div>
)
}

View file

@ -403,11 +403,11 @@ export async function loadAgent(id: string): Promise<z.infer<typeof Agent>> {
}
if (id === "live-note-agent") {
return buildLiveNoteAgent();
return await buildLiveNoteAgent();
}
if (id === "background-task-agent") {
return buildBackgroundTaskAgent();
return await buildBackgroundTaskAgent();
}
if (id === 'note_creation') {

View file

@ -1,8 +1,26 @@
import { skillCatalog, buildSkillCatalog } from "./skills/index.js";
import { getRuntimeContext, getRuntimeContextPrompt } from "./runtime-context.js";
import { composioAccountsRepo } from "../../composio/repo.js";
import { isConfigured as isComposioConfigured } from "../../composio/client.js";
import { CURATED_TOOLKITS } from "@x/shared/dist/composio.js";
import container from "../../di/container.js";
import type { ISkillResolver } from "../../skills/resolver.js";
import type { SkillCatalogEntry } from "@x/shared/dist/skill.js";
function buildSkillCatalogMarkdown(skills: SkillCatalogEntry[]): string {
const sections = skills.map((skill) => [
`## ${skill.title}`,
`- **Skill id:** \`${skill.id}\``,
`- **Use it for:** ${skill.summary}`,
].join("\n"));
return [
"# Rowboat Skill Catalog",
"",
"Use this catalog to see which specialized skills you can load. Each entry lists the skill id plus a short description of when it helps.",
"",
sections.join("\n\n"),
].join("\n");
}
const runtimeContextPrompt = getRuntimeContextPrompt(getRuntimeContext());
@ -317,33 +335,22 @@ For browser pages, mention the URL in plain text or use the browser-control tool
Never output raw file paths in plain text when they could be wrapped in a filepath block unless the file does not exist yet.`;
}
/** Keep backward-compatible export for any external consumers */
export const CopilotInstructions = buildStaticInstructions(true, skillCatalog);
/**
* Cached Composio instructions. Invalidated by calling invalidateCopilotInstructionsCache().
*/
let cachedInstructions: string | null = null;
/**
* Invalidate the cached instructions so the next buildCopilotInstructions() call
* regenerates the Composio section. Call this after connecting/disconnecting a toolkit.
*/
export function invalidateCopilotInstructionsCache(): void {
cachedInstructions = null;
}
/**
* Build full copilot instructions with dynamic Composio tools section.
* Results are cached and reused until invalidated via invalidateCopilotInstructionsCache().
*/
export async function buildCopilotInstructions(): Promise<string> {
if (cachedInstructions !== null) return cachedInstructions;
const composioEnabled = await isComposioConfigured();
const catalog = composioEnabled
? skillCatalog
: buildSkillCatalog({ excludeIds: ['composio-integration'] });
const baseInstructions = buildStaticInstructions(composioEnabled, catalog);
const resolver = container.resolve<ISkillResolver>("skillResolver");
const allSkills = await resolver.getCatalog();
const filtered = composioEnabled
? allSkills
: allSkills.filter((s) => s.id !== 'composio-integration');
const catalogMarkdown = buildSkillCatalogMarkdown(filtered);
const baseInstructions = buildStaticInstructions(composioEnabled, catalogMarkdown);
const composioPrompt = await getComposioToolsPrompt();
cachedInstructions = composioPrompt
? baseInstructions + '\n' + composioPrompt

View file

@ -1,82 +0,0 @@
export const skill = String.raw`
# App Navigation Skill
You have access to the **app-navigation** tool which lets you control the Rowboat UI directly opening notes, switching views, filtering the knowledge base, and creating saved views.
## Actions
### open-note
Open a specific knowledge file in the editor pane.
**When to use:** When the user asks to see, open, or view a specific note (e.g., "open John's note", "show me the Acme project page").
**Parameters:**
- ` + "`path`" + `: Full workspace-relative path (e.g., ` + "`knowledge/People/John Smith.md`" + `)
**Tips:**
- Use ` + "`workspace-grep`" + ` first to find the exact path if you're unsure of the filename.
- Always pass the full ` + "`knowledge/...`" + ` path, not just the filename.
### open-view
Switch the UI to the graph or bases view.
**When to use:** When the user asks to see the knowledge graph, view all notes, or open the bases/table view.
**Parameters:**
- ` + "`view`" + `: ` + "`\"graph\"`" + ` or ` + "`\"bases\"`" + `
### update-base-view
Change filters, columns, sort order, or search in the bases (table) view.
**When to use:** When the user asks to find, filter, sort, or search notes. Examples: "show me all active customers", "filter by topic=hiring", "sort by name", "search for pricing".
**Parameters:**
- ` + "`filters`" + `: Object with ` + "`set`" + `, ` + "`add`" + `, ` + "`remove`" + `, or ` + "`clear`" + ` each takes an array of ` + "`{ category, value }`" + ` pairs.
- ` + "`set`" + `: Replace ALL current filters with these.
- ` + "`add`" + `: Append filters without removing existing ones.
- ` + "`remove`" + `: Remove specific filters.
- ` + "`clear: true`" + `: Remove all filters.
- ` + "`columns`" + `: Object with ` + "`set`" + `, ` + "`add`" + `, or ` + "`remove`" + ` each takes an array of column names (frontmatter keys).
- ` + "`sort`" + `: ` + "`{ field, dir }`" + ` where dir is ` + "`\"asc\"`" + ` or ` + "`\"desc\"`" + `.
- ` + "`search`" + `: Free-text search string.
**Tips:**
- If unsure what categories/values are available, call ` + "`get-base-state`" + ` first.
- For "show me X", prefer ` + "`filters.set`" + ` to start fresh rather than ` + "`filters.add`" + `.
- Categories come from frontmatter keys (e.g., relationship, status, topic, type).
- **CRITICAL: Do NOT pass ` + "`columns`" + ` unless the user explicitly asks to show/hide specific columns.** Omit the ` + "`columns`" + ` parameter entirely when only filtering, sorting, or searching. Passing ` + "`columns`" + ` will override the user's current column layout and can make the view appear empty.
### get-base-state
Retrieve information about what's in the knowledge base available filter categories, values, and note count.
**When to use:** When you need to know what properties exist before filtering, or when the user asks "what can I filter by?", "how many notes are there?", etc.
**Parameters:**
- ` + "`base_name`" + ` (optional): Name of a saved base to inspect.
### create-base
Save the current view configuration as a named base.
**When to use:** When the user asks to save a filtered view, create a saved search, or says "save this as [name]".
**Parameters:**
- ` + "`name`" + `: Human-readable name for the base.
## Workflow Example
1. User: "Show me all people who are customers"
2. First, check what properties are available:
` + "`app-navigation({ action: \"get-base-state\" })`" + `
3. Apply filters based on the available properties:
` + "`app-navigation({ action: \"update-base-view\", filters: { set: [{ category: \"relationship\", value: \"customer\" }] } })`" + `
4. If the user wants to save it:
` + "`app-navigation({ action: \"create-base\", name: \"Customers\" })`" + `
## Important Notes
- The ` + "`update-base-view`" + ` action will automatically navigate to the bases view if the user isn't already there.
- ` + "`open-note`" + ` validates that the file exists before navigating.
- Filter categories and values come from frontmatter in knowledge files.
- **Never send ` + "`columns`" + ` or ` + "`sort`" + ` with ` + "`update-base-view`" + ` unless the user specifically asks to change them.** Only pass the parameters you intend to change omitted parameters are left untouched.
`;
export default skill;

View file

@ -1,138 +0,0 @@
import { z } from 'zod';
import { stringify as stringifyYaml } from 'yaml';
import { BackgroundTaskSchema } from '@x/shared/dist/background-task.js';
const schemaYaml = stringifyYaml(z.toJSONSchema(BackgroundTaskSchema)).trimEnd();
export const skill = String.raw`
# Background Tasks Skill
A *background task* is a persistent agent the user configures once and the framework keeps firing on a schedule, inside time-of-day windows, and/or in response to matching incoming events (Gmail threads, calendar changes). Each task lives at \`bg-tasks/<slug>/\` and owns two artifacts:
- \`task.yaml\` — the spec (the user's **instructions**, triggers, runtime state). You and the user both treat this as the source of truth.
- \`index.md\` — the agent-owned body. The runtime never writes here; the bg-task agent does, each run.
A task is one of two shapes the agent decides per run from the verbs in \`instructions\`:
| Mode | Trigger verbs | Behavior |
|---|---|---|
| **OUTPUT** | "maintain / show / summarize / track / digest" | Rewrite \`index.md\` to reflect the current state. |
| **ACTION** | "send / draft / post / notify / file / reply / call" | Perform the action, then append a one-line journal entry under \`## Journal\` in \`index.md\`. |
Mixed instructions ("summarize and email it") trigger both.
## Tools you'll use (and ones you WON'T)
You have three dedicated builtin tools for this skill:
- \`create-background-task\` — materializes a new task on disk. **Use this. Do not write \`task.yaml\` yourself with \`workspace-edit\`, and do not search the codebase for IPC channels like \`bg-task:create\`** — they're renderer-side and not callable from here.
- \`patch-background-task\` — updates an existing task (instructions / triggers / active / model). Use this for the extend-don't-fork case.
- \`run-background-task-agent\` — manually fires a task to run now. Always call this immediately after \`create-background-task\` so the user sees content.
To inspect what tasks already exist, use \`workspace-glob\` on \`bg-tasks/*/task.yaml\` and \`workspace-readFile\` on candidates. The user's bg-tasks folder is workspace-relative.
## Mode: act-first
Bg-task creation is **action-first**. Don't ask "should I?" read the request, pick a name, call \`create-background-task\`, then call \`run-background-task-agent\` with the returned slug. Confirm in one line past-tense at the end. Tell the user the surface name: "Manage it from Background tasks in the sidebar."
The only exception: if a related bg-task already exists, **extend its instructions** via \`patch-background-task\` rather than creating a duplicate (see "Extend, don't fork").
## When you're loaded
The host's trigger paragraph loads this skill on:
- **Cadence**: "every morning", "daily", "hourly", "each Monday"
- **Watch/monitor**: "watch / monitor / keep an eye on / track / follow X"
- **Recurring artifact**: "morning briefing", "weekly review", "Acme deal dashboard"
- **Event-conditional**: "whenever a relevant email comes in, …"
- **Action verbs**: "draft / reply / call / post / notify / file / brief me on"
- **Decay questions**: "what's the weather", "top HN stories", "latest on X" answer the one-off, then offer
If the user explicitly says "live note" / "live-note", the host loads the \`live-note\` skill instead — don't try to handle that case here.
## Workflow
1. **Check for existing tasks.** Before creating, glob \`bg-tasks/*/task.yaml\` and read any candidates whose intent might overlap with the user's ask. If a related task exists, jump to "Extend, don't fork" below.
2. **Pick a name.** Use a short, friendly title in title-case: "Morning weather", "Q3 deal digest", "HN top stories". The framework slugifies it (lowercase, dashes) for the folder you don't manage the slug.
3. **Write the instructions.** Capture the user's intent in their own words, with concrete verbs. Bake any specifics (which source, which audience, output shape) into the instructions the agent re-reads them on every run.
- Good: *"Summarize my unread emails since yesterday 6pm into a one-paragraph digest plus a bulleted list of action items. Skip newsletters and automated notifications."*
- Bad: *"Daily email summary."* (vague agent will improvise unhelpfully)
4. **Pick triggers.** All three are independently optional; mix freely.
- \`cronExpr\` — exact times. \`"0 7 * * *"\` = 7am daily.
- \`windows\` — time-of-day bands. Each fires once per day inside the band, anywhere — forgiving when the app was offline.
- \`eventMatchCriteria\` — a natural-language description of which incoming events should wake the task (e.g. "Emails about Q3 OKRs from the leadership team"). Pass-1 routing matches; the agent does Pass-2 before acting.
No triggers at all = manual-only. The user clicks Run.
5. **Call \`create-background-task\`.** Required: \`name\`, \`instructions\`. Optional: \`triggers\`, \`model\`, \`provider\` (leave model/provider unset unless the user explicitly asked). The tool returns a slug.
6. **Call \`run-background-task-agent\`** with the slug. The agent runs once and populates \`index.md\`.
7. **Confirm.** One line. Name the task. Point at the sidebar. Done.
## Extend, don't fork
When the user's new ask overlaps with an existing task — e.g. they say "also include X" or the ask is a refinement of an existing task's intent call \`patch-background-task\` instead of creating a duplicate.
Signals that you should extend:
- The user says "also …" / "and on top of that …" / "while you're at it …"
- The new ask is a refinement of an existing task's intent (different threshold, additional source, slightly different output)
When extending, pass the full rewritten \`instructions\` — don't try to surgical-edit a single sentence. The agent rereads instructions every run, so a clean rewrite is fine. After \`patch-background-task\` returns, call \`run-background-task-agent\` on the same slug so the user sees the updated output.
## Worked examples
### OUTPUT morning briefing
User: *"Every morning at 7, give me a one-paragraph summary of overnight news in AI agents."*
1. \`create-background-task\` with:
- \`name\`: "AI agent overnight news"
- \`instructions\`: "Search the web and Hacker News for news about AI agents (autonomous LLM agents, agentic frameworks, agent benchmarks) published in the last 24 hours. Summarize the top developments in one paragraph (3-5 sentences) followed by a 3-5 item bulleted list of the most significant items with a single-sentence note each. Replace the body of index.md."
- \`triggers\`: { \`cronExpr\`: "0 7 * * *" }
2. \`run-background-task-agent\` slug=ai-agent-overnight-news.
3. "Done — created the **AI agent overnight news** task. It'll run every morning at 7 and you can find it in Background tasks in the sidebar."
### ACTION email auto-reply
User: *"Whenever I get an email about Q3 planning, draft a reply asking when they're free this week."*
1. \`create-background-task\` with:
- \`name\`: "Q3 email auto-reply drafts"
- \`instructions\`: "When an event arrives describing an email thread about Q3 planning, use the Gmail draft-create tool to draft a reply to the latest message asking the sender when they're free for a 30-minute call this week. Do not send the draft — leave it in Drafts for me to review. After drafting, append a journal entry to index.md noting the thread subject and the draft id."
- \`triggers\`: { \`eventMatchCriteria\`: "Emails about Q3 planning (roadmap, OKRs, headcount, exec priorities)" }
2. \`run-background-task-agent\` slug=q3-email-auto-reply-drafts.
3. "Done — created the **Q3 email auto-reply drafts** task. It'll fire on relevant Gmail threads. Manage it from Background tasks in the sidebar."
### ACTION + journal Slack watcher
User: *"Every weekday morning at 9, post a summary of unresolved high-priority issues to #engineering on Slack."*
1. \`create-background-task\` with:
- \`name\`: "Daily eng triage"
- \`instructions\`: "Each run, query <issue tracker> for unresolved issues labeled priority:high or above. Summarize counts by owner and the three oldest items. Send the summary to #engineering via the Slack tool. After sending, append a journal entry to index.md with the timestamp and the message id."
- \`triggers\`: { \`cronExpr\`: "0 9 * * 1-5" }
2. \`run-background-task-agent\` slug=daily-eng-triage.
## Canonical Schema
\`\`\`yaml
${schemaYaml}
\`\`\`
Notes:
- \`active\` defaults to true. Patch \`{ active: false }\` to pause without deleting.
- \`createdAt\` and \`lastRun\` are runtime-managed — never write them yourself.
- The \`triggers\` block reuses Live Notes' \`Triggers\` schema verbatim. Cron grace and 5-minute backoff semantics are identical.
## Exceptions
The \`Background tasks\` sidebar view has a "New task" button that opens a form-driven flow. If the user is editing fields there or asking about a specific task from that view, *you* are not the right surface — the form is. Point at it ("You can also do this from the New task button in the Background tasks view") and step aside.
`;
export default skill;

View file

@ -1,119 +0,0 @@
export const skill = String.raw`
# Browser Control Skill
You have access to the **browser-control** tool, which controls Rowboat's embedded browser pane directly.
Use this skill when the user asks you to open a website, browse in-app, search the web in the browser pane, click something on a page, fill a form, or otherwise interact with a live webpage inside Rowboat.
## Core Workflow
1. Start with ` + "`browser-control({ action: \"open\" })`" + ` if the browser pane may not already be open.
2. Use ` + "`browser-control({ action: \"read-page\" })`" + ` to inspect the current page.
3. The tool returns:
- ` + "`snapshotId`" + `
- page ` + "`url`" + ` and ` + "`title`" + `
- visible page text
- interactable elements with numbered ` + "`index`" + ` values
- ` + "`suggestedSkills`" + ` site-specific and interaction-specific skill hints for the current page
4. **Always inspect ` + "`suggestedSkills`" + ` before acting.** If any skill in the list matches what the user asked for (site or task), call ` + "`load-browser-skill({ id: \"<id>\" })`" + ` *first*, read it in full, then plan your actions. These skills encode selectors, timing, and gotchas that would otherwise cost you several failed attempts to rediscover. If no skill matches, proceed — but do not skip this check.
5. Prefer acting on those numbered indices with ` + "`click`" + ` / ` + "`type`" + ` / ` + "`press`" + `.
6. After each action, read the returned page snapshot before deciding the next step including re-checking ` + "`suggestedSkills`" + ` if the navigation landed you on a new domain.
## Actions
### open
Open the browser pane and ensure an active tab exists.
### get-state
Return the current browser tabs and active tab id.
### new-tab
Open a new browser tab.
Parameters:
- ` + "`target`" + ` (optional): URL or plain-language search query
### switch-tab
Switch to a tab by ` + "`tabId`" + `.
### close-tab
Close a tab by ` + "`tabId`" + `.
### navigate
Navigate the active tab.
Parameters:
- ` + "`target`" + `: URL or plain-language search query
Plain-language targets are converted into a search automatically.
### back / forward / reload
Standard browser navigation controls.
### read-page
Read the current page and return a compact snapshot.
Parameters:
- ` + "`maxElements`" + ` (optional)
- ` + "`maxTextLength`" + ` (optional)
### click
Click an element.
Prefer:
- ` + "`index`" + `: element index from ` + "`read-page`" + `
Optional:
- ` + "`snapshotId`" + `: include it when acting on a recent snapshot
- ` + "`selector`" + `: fallback only when no usable index exists
### type
Type into an input, textarea, or contenteditable element.
Parameters:
- ` + "`text`" + `: text to enter
- plus the same target fields as ` + "`click`" + `
### press
Send a key press such as ` + "`Enter`" + `, ` + "`Tab`" + `, ` + "`Escape`" + `, or arrow keys.
Parameters:
- ` + "`key`" + `
- optional target fields if you need to focus a specific element first
### scroll
Scroll the current page.
Parameters:
- ` + "`direction`" + `: ` + "`\"up\"`" + ` or ` + "`\"down\"`" + ` (optional; defaults down)
- ` + "`amount`" + `: pixel distance (optional)
### wait
Wait for the page to settle, useful after async UI changes.
Parameters:
- ` + "`ms`" + `: milliseconds to wait (optional)
## Companion Tools
### load-browser-skill
Rowboat caches a library of browser skills (from ` + "`browser-use/browser-harness`" + `) indexed by both **domain** (github, linkedin, amazon, booking, ) and **interaction type** within a domain (e.g. ` + "`github/repo-actions`" + `, ` + "`github/scraping`" + `, ` + "`arxiv-bulk/*`" + `). Whenever ` + "`browser-control`" + ` returns a ` + "`suggestedSkills`" + ` array which it does on ` + "`navigate`" + `, ` + "`new-tab`" + `, and ` + "`read-page`" + ` treat it as a required reading step, not optional. Pick the entry that matches the current task (domain match first, then the interaction-specific variant if one exists) and call ` + "`load-browser-skill({ id: \"<id>\" })`" + ` before attempting the action.
You can also proactively call ` + "`load-browser-skill({ action: \"list\", site: \"<site>\" })`" + ` when you know you're about to work on a site, to see what skills exist even if ` + "`suggestedSkills`" + ` is empty (e.g. before navigating).
These skills are written against a Python harness, so treat them as **reference knowledge**. Reuse the selectors, timing, and sequencing, but adapt them to Rowboat's structured browser actions. **Do not look for or call ` + "`http-fetch`" + `.** If a browser-harness recipe suggests ` + "`js(...)`" + ` or ` + "`http_get(...)`" + ` style shortcuts, treat those as non-portable and fall back to reading and interacting with the page itself.
## Important Rules
- Prefer ` + "`read-page`" + ` before interacting.
- Prefer element ` + "`index`" + ` over CSS selectors.
- If the tool says the snapshot is stale, call ` + "`read-page`" + ` again.
- After navigation, clicking, typing, pressing, or scrolling, use the returned page snapshot instead of assuming the page state.
- **Always check ` + "`suggestedSkills`" + ` after ` + "`navigate`" + `, ` + "`new-tab`" + `, or ` + "`read-page`" + `, and load the matching domain or interaction skill before acting.** Skipping this step is the single most common way to waste a dozen failed clicks on a site whose quirks are already documented. If the array is empty, proceed normally but don't skip the check.
- Do not try to use ` + "`http-fetch`" + `. If a browser-harness recipe mentions ` + "`http_get(...)`" + ` or a public API shortcut, adapt it to DOM-based browsing instead.
- Use Rowboat's browser for live interaction. Use web search tools for research where a live session is unnecessary.
- Do not wrap browser URLs or browser pages in ` + "```filepath" + ` blocks. Filepath cards are only for real files on disk, not web pages or browser tabs.
- If you mention a page the browser opened, use plain text for the URL/title instead of trying to create a clickable file card.
`;
export default skill;

View file

@ -1,228 +0,0 @@
export const skill = String.raw`
# Builtin Tools Reference
Load this skill when creating or modifying agents that need access to Rowboat's builtin tools (shell execution, file operations, etc.).
## Available Builtin Tools
Agents can use builtin tools by declaring them in the YAML frontmatter \`tools\` section with \`type: builtin\` and the appropriate \`name\`.
### executeCommand
**The most powerful and versatile builtin tool** - Execute any bash/shell command and get the output.
**Security note:** Commands are filtered through \`config/security.json\` in the workspace root. Populate this file with allowed command names (array or dictionary entries). Any command not present is blocked and returns exit code 126 so the agent knows it violated the policy.
**Agent tool declaration (YAML frontmatter):**
\`\`\`yaml
tools:
bash:
type: builtin
name: executeCommand
\`\`\`
**What it can do:**
- Run package managers (npm, pip, apt, brew, cargo, go get, etc.)
- Git operations (clone, commit, push, pull, status, diff, log, etc.)
- System operations (ps, top, df, du, find, grep, kill, etc.)
- Build and compilation (make, cargo build, go build, npm run build, etc.)
- Network operations (curl, wget, ping, ssh, netstat, etc.)
- Text processing (awk, sed, grep, jq, yq, cut, sort, uniq, etc.)
- Database operations (psql, mysql, mongo, redis-cli, etc.)
- Container operations (docker, kubectl, podman, etc.)
- Testing and debugging (pytest, jest, cargo test, etc.)
- File operations (cat, head, tail, wc, diff, patch, etc.)
- Any CLI tool or script execution
**Agent instruction examples:**
- "Use the bash tool to run git commands for version control operations"
- "Execute curl commands using the bash tool to fetch data from APIs"
- "Use bash to run 'npm install' and 'npm test' commands"
- "Run Python scripts using the bash tool with 'python script.py'"
- "Use bash to execute 'docker ps' and inspect container status"
- "Run database queries using 'psql' or 'mysql' commands via bash"
- "Use bash to execute system monitoring commands like 'top' or 'ps aux'"
**Pro tips for agent instructions:**
- Commands can be chained with && for sequential execution
- Use pipes (|) to combine Unix tools (e.g., "cat file.txt | grep pattern | wc -l")
- Redirect output with > or >> when needed
- Full bash shell features are available (variables, loops, conditionals, etc.)
- Tools like jq, yq, awk, sed can parse and transform data
**Example agent with executeCommand** (\`agents/arxiv-feed-reader.md\`):
\`\`\`markdown
---
model: gpt-5.1
tools:
bash:
type: builtin
name: executeCommand
---
# arXiv Feed Reader
Extract latest papers from the arXiv feed and summarize them.
Use curl to fetch the RSS feed, then parse it with yq and jq:
\\\`\\\`\\\`bash
curl -s https://rss.arxiv.org/rss/cs.AI | yq -p=xml -o=json | jq -r '.rss.channel.item[] | select(.title | test("agent"; "i")) | "\\(.title)\\n\\(.link)\\n\\(.description)\\n"'
\\\`\\\`\\\`
This will give you papers containing 'agent' in the title.
\`\`\`
**Another example - System monitoring agent** (\`agents/system-monitor.md\`):
\`\`\`markdown
---
model: gpt-5.1
tools:
bash:
type: builtin
name: executeCommand
---
# System Monitor
Monitor system resources using bash commands:
- Use 'df -h' for disk usage
- Use 'free -h' for memory
- Use 'top -bn1' for processes
- Use 'ps aux' for process list
Parse the output and report any issues.
\`\`\`
**Another example - Git automation agent** (\`agents/git-helper.md\`):
\`\`\`markdown
---
model: gpt-5.1
tools:
bash:
type: builtin
name: executeCommand
---
# Git Helper
Help with git operations. Use commands like:
- 'git status' - Check working tree status
- 'git log --oneline -10' - View recent commits
- 'git diff' - See changes
- 'git branch -a' - List branches
Can also run 'git add', 'git commit', 'git push' when instructed.
\`\`\`
## Agent-to-Agent Calling
Agents can call other agents as tools to create complex multi-step workflows. This is the core mechanism for building multi-agent systems in the CLI.
**Tool declaration (YAML frontmatter):**
\`\`\`yaml
tools:
summariser:
type: agent
name: summariser_agent
\`\`\`
**When to use:**
- Breaking complex tasks into specialized sub-agents
- Creating reusable agent components
- Orchestrating multi-step workflows
- Delegating specialized tasks (e.g., summarization, data processing, audio generation)
**How it works:**
- The agent calls the tool like any other tool
- The target agent receives the input and processes it
- Results are returned as tool output
- The calling agent can then continue processing or delegate further
**Example - Agent that delegates to a summarizer** (\`agents/paper_analyzer.md\`):
\`\`\`markdown
---
model: gpt-5.1
tools:
summariser:
type: agent
name: summariser_agent
---
# Paper Analyzer
Pick 2 interesting papers and summarise each using the summariser tool.
Pass the paper URL to the summariser. Don't ask for human input.
\`\`\`
**Tips for agent chaining:**
- Make instructions explicit about when to call other agents
- Pass clear, structured data between agents
- Add "Don't ask for human input" for autonomous workflows
- Keep each agent focused on a single responsibility
## Additional Builtin Tools
While \`executeCommand\` is the most versatile, other builtin tools exist for specific Rowboat operations (file management, agent inspection, etc.). These are primarily used by the Rowboat copilot itself and are not typically needed in user agents. If you need file operations, consider using bash commands like \`cat\`, \`echo\`, \`tee\`, etc. through \`executeCommand\`.
### Copilot-Specific Builtin Tools
The Rowboat copilot has access to special builtin tools that regular agents don't typically use. These tools help the copilot assist users with workspace management and MCP integration:
#### File & Directory Operations
- \`workspace-readdir\` - List directory contents (supports recursive exploration)
- \`workspace-readFile\` - Read file contents
- \`workspace-writeFile\` - Create or update file contents
- \`workspace-edit\` - Make precise edits by replacing specific text (safer than full rewrites)
- \`workspace-remove\` - Remove files or directories
- \`workspace-exists\` - Check if a file or directory exists
- \`workspace-stat\` - Get file/directory statistics
- \`workspace-mkdir\` - Create directories
- \`workspace-rename\` - Rename or move files/directories
- \`workspace-copy\` - Copy files
- \`workspace-getRoot\` - Get workspace root directory path
- \`workspace-glob\` - Find files matching a glob pattern (e.g., "**/*.ts", "agents/*.md")
- \`workspace-grep\` - Search file contents using regex, returns matching files and lines
#### Agent Operations
- \`analyzeAgent\` - Read and analyze an agent file structure
- \`loadSkill\` - Load a Rowboat skill definition into context
#### MCP Operations
- \`addMcpServer\` - Add or update an MCP server configuration (with validation)
- \`listMcpServers\` - List all available MCP servers
- \`listMcpTools\` - List all available tools from a specific MCP server
- \`executeMcpTool\` - **Execute a specific MCP tool on behalf of the user**
#### Using executeMcpTool as Copilot
The \`executeMcpTool\` builtin allows the copilot to directly execute MCP tools without creating an agent. Load the "mcp-integration" skill for complete guidance on discovering and executing MCP tools, including workflows, schema matching, and examples.
**When to use executeMcpTool vs creating an agent:**
- Use \`executeMcpTool\` for immediate, one-time tasks
- Create an agent when the user needs repeated use or autonomous operation
- Create an agent for complex multi-step workflows involving multiple tools
## Best Practices
1. **Give agents clear examples** in their instructions showing exact bash commands to run
2. **Explain output parsing** - show how to use jq, yq, grep, awk to extract data
3. **Chain commands efficiently** - use && for sequences, | for pipes
4. **Handle errors** - remind agents to check exit codes and stderr
5. **Be specific** - provide example commands rather than generic descriptions
6. **Security** - remind agents to validate inputs and avoid dangerous operations
## When to Use Builtin Tools vs MCP Tools vs Agent Tools
- **Use builtin executeCommand** when you need: CLI tools, system operations, data processing, git operations, any shell command
- **Use MCP tools** when you need: Web scraping (firecrawl), text-to-speech (elevenlabs), specialized APIs, external service integrations
- **Use agent tools (\`type: agent\`)** when you need: Complex multi-step logic, task delegation, specialized processing that benefits from LLM reasoning
Many tasks can be accomplished with just \`executeCommand\` and common Unix tools - it's incredibly powerful!
## Key Insight: Multi-Agent Workflows
In the CLI, multi-agent workflows are built by:
1. Creating specialized agents as Markdown files in the \`agents/\` directory
2. Creating an orchestrator agent that has other agents in its \`tools\` (YAML frontmatter)
3. Running the orchestrator with \`rowboatx --agent orchestrator_name\`
There are no separate "workflow" files - everything is an agent defined in Markdown!
`;
export default skill;

View file

@ -1,90 +0,0 @@
export const skill = String.raw`
# Code with Agents Skill
Use this skill when the user asks you to write code, build a project, create scripts, fix bugs, or do any software development task that should be delegated to a coding agent (Claude Code or Codex).
## Important: delegate ALL coding work
Once the user has chosen to use Claude Code or Codex, you MUST delegate ALL code-related tasks to the coding agent. This includes:
- Writing, editing, or refactoring code
- Reading, summarizing, or explaining code
- Debugging and fixing bugs
- Running tests or build commands
- Exploring project structure
- Any other task that involves interacting with a codebase
Do NOT attempt to do any of these yourself no reading files, no running commands, no writing code. You are the coordinator; the coding agent does the work. Your job is to translate the user's request into a clear prompt and pass it to the agent.
## Prerequisites
The user must have one of the following installed on their machine:
- **Claude Code** https://claude.ai/code
- **Codex** https://codex.openai.com
These are external tools that you cannot install for the user.
## Workflow
### Step 1: Gather requirements
Before running anything, confirm the following with the user:
1. **Working directory** Ask which folder the code should be written in, unless the user has already specified it. Example: "Which folder should I work in?"
2. **Agent choice** Ask whether to use **Claude Code** or **Codex**. Mention that the chosen agent must already be installed on their machine.
### Step 2: Confirm execution plan
Once you know the folder and agent, tell the user:
> I'll use [Claude Code / Codex] to [description of the task] in \`[folder]\`. Permission requests from the coding agent itself (file writes, command execution, etc.) will be automatically approved once it starts. Wait for the user's confirmation before you execute anything.
### Step 3: Execute with acpx
Use the \`executeCommand\` tool to run the coding agent via acpx. The command format is:
**For Claude Code:**
` + "`" + `
npx acpx@latest --approve-all --cwd <folder> claude exec "<prompt>"
` + "`" + `
**For Codex:**
` + "`" + `
npx acpx@latest --approve-all --cwd <folder> codex exec "<prompt>"
` + "`" + `
### Critical: flag order
The \`--approve-all\` and \`--cwd\` flags are global flags and MUST come before the agent name (\`claude\` or \`codex\`). This is the correct order:
` + "`" + `
npx acpx@latest [global flags] <agent> exec "<prompt>"
` + "`" + `
**Correct:**
` + "`" + `
npx acpx@latest --approve-all --cwd ~/projects/myapp claude exec "fix the bug"
` + "`" + `
**Wrong (will fail):**
` + "`" + `
npx acpx@latest claude --approve-all exec "fix the bug"
` + "`" + `
### Writing good prompts
When constructing the prompt for the coding agent:
- Be specific and detailed about what to build or fix
- Include file names, function signatures, and expected behavior
- Mention any constraints (language, framework, style)
- If the user gave you a short request, expand it into a clear, actionable prompt for the agent
### Step 4: Report results
After the command finishes, look for the summary that the coding agent produced at the end of its output and pass that along to the user as-is. Do not rewrite or add to it. Only add your own explanation if the command failed or the exit code is non-zero.
Do NOT use file reference blocks (e.g. \`\`\`file:path/to/file\`\`\`) when mentioning code files — they may not open correctly. Just refer to file paths as plain text.
- If the exit code is 5, it means permissions were denied this should not happen with \`--approve-all\`, but if it does, let the user know
`;
export default skill;

View file

@ -1,127 +0,0 @@
export const skill = String.raw`
# Composio Integration
**Load this skill** when the user asks to interact with ANY third-party service email, GitHub, Slack, LinkedIn, Notion, Jira, Google Sheets, calendar, etc. This skill provides the complete workflow for discovering, connecting, and executing Composio tools.
## Available Tools
| Tool | Purpose |
|------|---------|
| **composio-list-toolkits** | List all available integrations and their connection status |
| **composio-search-tools** | Search for tools by use case; returns slugs and input schemas |
| **composio-execute-tool** | Execute a tool by slug with parameters |
| **composio-connect-toolkit** | Connect a service via OAuth (opens browser) |
## Toolkit Slugs (exact values for toolkitSlug parameter)
| Service | Slug |
|---------|------|
| Gmail | \`gmail\` |
| Google Calendar | \`googlecalendar\` |
| Google Sheets | \`googlesheets\` |
| Google Docs | \`googledocs\` |
| Google Drive | \`googledrive\` |
| Slack | \`slack\` |
| GitHub | \`github\` |
| Notion | \`notion\` |
| Linear | \`linear\` |
| Jira | \`jira\` |
| Asana | \`asana\` |
| Trello | \`trello\` |
| HubSpot | \`hubspot\` |
| Salesforce | \`salesforce\` |
| LinkedIn | \`linkedin\` |
| X (Twitter) | \`twitter\` |
| Reddit | \`reddit\` |
| Dropbox | \`dropbox\` |
| OneDrive | \`onedrive\` |
| Microsoft Outlook | \`microsoft_outlook\` |
| Microsoft Teams | \`microsoft_teams\` |
| Calendly | \`calendly\` |
| Cal.com | \`cal\` |
| Intercom | \`intercom\` |
| Zendesk | \`zendesk\` |
| Airtable | \`airtable\` |
**IMPORTANT:** Always use these exact slugs. Do NOT guess e.g., Google Sheets is \`googlesheets\` (no underscore), not \`google_sheets\`.
## Critical: Check First, Connect Second
**BEFORE calling composio-connect-toolkit, ALWAYS check if the service is already connected.** The system prompt includes a "Currently connected" list. If the service is there, skip connecting and go straight to search + execute.
**Flow:**
1. Check if the service is in the "Currently connected" list (in the system prompt above)
2. If **connected** go directly to step 4
3. If **NOT connected** call \`composio-connect-toolkit\` once, wait for user to authenticate, then continue
4. Call \`composio-search-tools\` with SHORT keyword queries
5. Read the \`inputSchema\` from results — note \`required\` fields
6. Call \`composio-execute-tool\` with slug, toolkit, and all required arguments
**NEVER call composio-connect-toolkit for a service that's already connected.** This creates duplicate connect cards in the UI.
## Search Query Tips
Use **short keyword queries**, not full sentences:
| Good | Bad |
|---------|--------|
| "list issues" | "get all open issues for a GitHub repository" |
| "send email" | "send an email to someone using Gmail" |
| "get profile" | "fetch the authenticated user's profile details" |
| "create spreadsheet" | "create a new Google Sheets spreadsheet with data" |
If the first search returns 0 results, try a different short query (e.g., "issues" instead of "list issues").
## Passing Arguments
**ALWAYS include the \`arguments\` field** when calling \`composio-execute-tool\`, even if the tool has no required parameters.
- Read the \`inputSchema\` from search results carefully
- Extract user-provided values into the correct fields (e.g., "rowboatlabs/rowboat" \`owner: "rowboatlabs", repo: "rowboat"\`)
- For tools with empty \`properties: {}\`, pass \`arguments: {}\`
- For tools with required fields, pass all of them
### Example: GitHub Issues
User says: "Get me the open issues on rowboatlabs/rowboat"
1. \`composio-search-tools({ query: "list issues", toolkitSlug: "github" })\`
finds \`GITHUB_ISSUES_LIST_FOR_REPO\` with required: ["owner", "repo"]
2. \`composio-execute-tool({ toolSlug: "GITHUB_ISSUES_LIST_FOR_REPO", toolkitSlug: "github", arguments: { owner: "rowboatlabs", repo: "rowboat", state: "open", per_page: 100 } })\`
### Example: Gmail Fetch
User says: "What's my latest email?"
1. \`composio-search-tools({ query: "fetch emails", toolkitSlug: "gmail" })\`
finds \`GMAIL_FETCH_EMAILS\`
2. \`composio-execute-tool({ toolSlug: "GMAIL_FETCH_EMAILS", toolkitSlug: "gmail", arguments: { user_id: "me", max_results: 5 } })\`
### Example: LinkedIn Profile (no-arg tool)
User says: "Get my LinkedIn profile"
1. \`composio-search-tools({ query: "get profile", toolkitSlug: "linkedin" })\`
finds \`LINKEDIN_GET_MY_INFO\` with properties: {}
2. \`composio-execute-tool({ toolSlug: "LINKEDIN_GET_MY_INFO", toolkitSlug: "linkedin", arguments: {} })\`
## Error Recovery
- **If a tool call fails** (missing fields, 500 error): Fix the arguments and retry IMMEDIATELY. Do NOT stop and narrate the error to the user.
- **If search returns 0 results**: Try a different short query. If still 0, the tool may not exist for that service.
- **If a tool requires connection**: Call \`composio-connect-toolkit\` once, then retry after connection.
## Multi-Part Requests
When the user says "connect X and then do Y" complete BOTH parts in one turn:
1. If X is already connected (check the connected list), skip to Y immediately
2. If X needs connecting, connect it, then proceed to Y after authentication
## Confirmation Rules
- **Read-only actions** (fetch, list, get, search): Execute without asking
- **Mutating actions** (send email, create issue, post, delete): Show the user what you're about to do and confirm before executing
- **Connecting a toolkit**: Always safe just do it when needed
`;
export default skill;

View file

@ -1,24 +0,0 @@
export const skill = String.raw`
# Deletion Guardrails
Load this skill when a user asks to delete agents or workflows so you follow the required confirmation steps.
## Workflow deletion protocol
1. Read the workflow file to identify every agent it references.
2. Report those agents to the user and ask whether they should be deleted too.
3. Wait for explicit confirmation before deleting anything.
4. Only remove the workflow and/or agents the user authorizes.
## Agent deletion protocol
1. Inspect the agent file to discover which workflows reference it.
2. List those workflows to the user and ask whether they should be updated or deleted.
3. Pause for confirmation before modifying workflows or removing the agent.
4. Perform only the deletions the user approves.
## Safety checklist
- Never delete cascaded resources automatically.
- Keep a clear audit trail in your responses describing what was removed.
- If the users instructions are ambiguous, ask clarifying questions before taking action.
`;
export default skill;

View file

@ -1,307 +0,0 @@
import { KNOWLEDGE_NOTE_STYLE_GUIDE } from '../../../lib/knowledge-note-style.js';
export const skill = String.raw`
# Document Collaboration Skill
You are an expert document assistant helping the user create, edit, and refine documents in their knowledge base.
` + KNOWLEDGE_NOTE_STYLE_GUIDE + String.raw`
> The writing style above is non-negotiable for any content you author or edit in the knowledge base even small one-off edits. The user's whole knowledge base is built on it. The rest of this skill covers the *workflow* of collaboration; the style guide above covers the *output*.
## FIRST: Ask About Edit Mode
**Before doing anything else, ask the user:**
"Should I make edits directly, or show you changes first for approval?"
- **Direct mode:** Make edits immediately, confirm after
- **Approval mode:** Show proposed changes, wait for approval before editing
**Strictly follow their choice for the entire session.** Don't switch modes without asking.
## CRITICAL: Re-read Before Every Response
**Before every response, you MUST use workspace-readFile to re-read the current document.** The user may have edited the file manually outside of this conversation. Always work with the latest version of the file, never rely on a cached or previous version.
## Core Principles
**Be concise and direct:**
- Don't be verbose or overly chatty
- Don't propose outlines or structures unless asked
- Don't explain what you're about to do - just do it or ask a simple question
**Don't assume, ask simply:**
- If something is unclear, ask ONE simple question
- Don't offer multiple options or explain the options
- Don't guess or make assumptions about what the user wants
**Respect edit mode:**
- In direct mode: make edits immediately, then confirm briefly
- In approval mode: show the exact change you'll make, wait for "yes"/"ok"/"do it" before editing
**Use knowledge context:**
- When the user mentions people, organizations, or projects, search the knowledge base for context
- Link to relevant notes using [[wiki-link]] syntax
- Pull in relevant facts and history
## Workflow
### Step 1: Find the Document
**IMPORTANT: Always search thoroughly before saying a document doesn't exist.**
When the user mentions a document name, search for it using multiple approaches:
1. **Search by name pattern** (handles partial matches, different cases):
\`\`\`
workspace-glob({ pattern: "knowledge/**/*[name]*", path: "knowledge/" })
\`\`\`
2. **Search by content** (finds docs that mention the topic):
\`\`\`
workspace-grep({ pattern: "[name]", path: "knowledge/" })
\`\`\`
3. **Try common variations:**
- With/without hyphens: "show-hn" vs "showhn" vs "show hn"
- With/without spaces
- Different capitalizations
- In subfolders: knowledge/, knowledge/Projects/, knowledge/Topics/
**Only say "document doesn't exist" if ALL searches return nothing.**
**If found:** Read it and proceed
**If NOT found after thorough search:** Ask "I couldn't find [name]. Shall I create it?"
**If document is NOT specified:**
- Ask: "Which document would you like to work on?"
**Creating new documents:**
1. Ask simply: "Shall I create [filename]?" (don't ask about location - default to \`knowledge/Notes/\` unless the user specifies a different folder)
2. Create it with just a title - don't pre-populate with structure or outlines
3. Ask: "What would you like in this?"
\`\`\`
workspace-createFile({
path: "knowledge/Notes/[Document Name].md",
content: "# [Document Title]\n\n"
})
\`\`\`
**WRONG approach:**
- "Should this be in Projects/ or Topics/?" - don't ask, just use \`knowledge/Notes/\`
- "Here's a proposed outline..." - don't propose, let the user guide
- "I'll create a structure with sections for X, Y, Z" - don't assume structure
**RIGHT approach:**
- "Shall I create knowledge/Notes/roadmap.md?"
- *creates file with just the title*
- "Created. What would you like in this?"
### Step 2: Understand the Request
**IMPORTANT: Never make unsolicited edits.** If the user hasn't specified what they want to do with the document, ask them: "What would you like to change?" Do NOT proactively improve, restructure, or suggest edits unless the user has explicitly asked for changes.
**Types of requests:**
1. **Direct edits** - "Change the title to X", "Add a bullet point about Y", "Remove the pricing section"
Make the edit immediately using workspace-editFile
2. **Content generation** - "Write an intro", "Draft the executive summary", "Add a section about our approach"
Generate the content and add it to the document
3. **Review/feedback** - "What do you think?", "Is this clear?", "Any suggestions?"
Read the document and provide thoughtful feedback
4. **Research-backed additions** - "Add context about [Person]", "Include what we discussed with [Company]"
Search knowledge base first, then add relevant context
5. **No clear request** - User just says "let's work on X" with no specific ask
Read the document, then ask: "What would you like to change?"
### Step 3: Execute Changes
**For edits, use workspace-editFile:**
\`\`\`
workspace-editFile({
path: "knowledge/[path].md",
old_string: "[exact text to replace]",
new_string: "[new text]"
})
\`\`\`
**For additions at the end:**
\`\`\`
workspace-editFile({
path: "knowledge/[path].md",
old_string: "[last line or section]",
new_string: "[last line or section]\n\n[new content]"
})
\`\`\`
**For new sections:**
Find the right place in the document structure and insert the new section.
### Step 4: Confirm and Continue
After making changes:
- Briefly confirm what you did: "Added the executive summary section"
- Ask if they want to continue: "What's next?" or "Anything else to adjust?"
- Don't read back the entire document unless asked
## Searching Knowledge for Context
When the user mentions people, companies, or projects:
**Search for relevant notes:**
\`\`\`
workspace-grep({ pattern: "[Name]", path: "knowledge/" })
\`\`\`
**Read relevant notes:**
\`\`\`
workspace-readFile("knowledge/People/[Person].md")
workspace-readFile("knowledge/Organizations/[Company].md")
workspace-readFile("knowledge/Projects/[Project].md")
\`\`\`
**Use the context:**
- Reference specific facts, dates, and details
- Use [[wiki-links]] to connect to other notes
- Include relevant history and background
## Document Locations
Documents are stored in \`knowledge/\` within the workspace root, with subfolders:
- \`Notes/\` - **Default location for user notes. Create new notes here unless the user specifies a different folder.**
- \`People/\` - Notes about individuals
- \`Organizations/\` - Notes about companies, teams
- \`Projects/\` - Project documentation
- \`Topics/\` - Subject matter notes
## Rich Blocks
Notes support rich block types beyond standard Markdown. Blocks are fenced code blocks with a language identifier and a JSON body. Use these when the user asks for visual content like charts, tables, images, or embeds.
### Image Block
Displays an image with optional alt text and caption.
\`\`\`image
{"src": "https://example.com/photo.png", "alt": "Description", "caption": "Optional caption"}
\`\`\`
- \`src\` (required): URL or relative path to the image
- \`alt\` (optional): Alt text
- \`caption\` (optional): Caption displayed below the image
### Embed Block
Embeds external content (YouTube videos, Figma designs, tweets, or generic links).
\`\`\`embed
{"provider": "youtube", "url": "https://www.youtube.com/watch?v=VIDEO_ID", "caption": "Video title"}
\`\`\`
- \`provider\` (required): \`"youtube"\`, \`"figma"\`, \`"tweet"\`, or \`"generic"\`
- \`url\` (required): Full URL to the content
- \`caption\` (optional): Caption displayed below the embed
- YouTube and Figma render as iframes; tweet renders inline from the tweet URL; generic shows a link card
### Iframe Block
Embeds an arbitrary web page or a locally-served dashboard in the note.
\`\`\`iframe
{"url": "http://localhost:3210/sites/example-dashboard/", "title": "Trend Dashboard", "height": 640}
\`\`\`
- \`url\` (required): Full URL to render. Use \`https://\` for remote sites, or \`http://localhost:3210/sites/<slug>/\` for local dashboards
- \`title\` (optional): Title shown above the iframe
- \`height\` (optional): Height in pixels. Good dashboard defaults are 480-800
- \`allow\` (optional): Custom iframe \`allow\` attribute when the page needs extra browser capabilities
- Remote sites may refuse to render in iframes because of their own CSP / X-Frame-Options headers. When you need a reliable embed, create a local site in \`sites/<slug>/\` and use the localhost URL above
### Chart Block
Renders a chart from inline data.
\`\`\`chart
{"chart": "bar", "title": "Q1 Revenue", "data": [{"month": "Jan", "revenue": 50000}, {"month": "Feb", "revenue": 62000}], "x": "month", "y": "revenue"}
\`\`\`
- \`chart\` (required): \`"line"\`, \`"bar"\`, or \`"pie"\`
- \`title\` (optional): Chart title
- \`data\` (optional): Array of objects with the data points
- \`source\` (optional): Relative path to a JSON file containing the data array (alternative to inline data)
- \`x\` (required): Key name for the x-axis / label field
- \`y\` (required): Key name for the y-axis / value field
### Table Block
Renders a styled table from structured data.
\`\`\`table
{"title": "Team", "columns": ["name", "role"], "data": [{"name": "Alice", "role": "Eng"}, {"name": "Bob", "role": "Design"}]}
\`\`\`
- \`columns\` (required): Array of column names (determines display order)
- \`data\` (required): Array of row objects
- \`title\` (optional): Table title
### Block Guidelines
- The JSON must be valid and on a single line (no pretty-printing)
- Insert blocks using \`workspace-editFile\` just like any other content
- When the user asks for a chart, table, embed, or live dashboard use blocks rather than plain Markdown tables or image links
- When editing a note that already contains blocks, preserve them unless the user asks to change them
- For local dashboards and mini apps, put the site files in \`sites/<slug>/\` and point an \`iframe\` block at \`http://localhost:3210/sites/<slug>/\`
## Best Practices
**Writing style:** see "Knowledge-note writing style" at the top of this skill that's the canonical guide. Match the user's tone for prose-shaped content (their own narrative writing); for everything else apply the terse-and-scannable rules.
**Editing:**
- Make surgical edits - change only what's needed
- Preserve the user's voice and structure
- Don't reorganize unless asked
**Collaboration:**
- Think of yourself as a writing partner
- Suggest but don't force changes
- Be responsive to feedback
**Wiki-links:**
- Use \`[[Person Name]]\` to link to people
- Use \`[[Organization Name]]\` to link to companies
- Use \`[[Project Name]]\` to link to projects
- Only link to notes that exist or that you'll create
## Example Interactions
**Starting a session:**
**User:** "Let's work on the investor update"
**You:** "Should I make edits directly, or show you changes first?"
**User:** "directly is fine"
**You:** *Search for it, read it*
"Found knowledge/Investor Update Q1.md. What would you like to change?"
**Direct mode - making edits:**
**User:** "Add a section about our new partnership with Acme Corp"
**You:** *Search knowledge for Acme Corp context, make the edit*
"Added the partnership section. Anything else?"
**Approval mode - showing changes first:**
**User:** "Add a section about Acme Corp"
**You:** "I'll add this after the Overview section:
\`\`\`
## Partnership with Acme Corp
[content based on knowledge...]
\`\`\`
Ok to add?"
**User:** "yes"
**You:** *Makes the edit*
"Done. What's next?"
**Creating a new doc:**
**User:** "Create a doc for the roadmap"
**You:** "Shall I create knowledge/roadmap.md?"
**User:** "yes"
**You:** *Creates file with just title*
"Created. What would you like in this?"
**WRONG examples - don't do this:**
- "Nice, new doc time! Quick clarifier: should this be standalone or in Projects/?"
- "Here's a proposed outline for the doc..."
- "I'll assume this is a project-style doc and sketch an initial structure"
- "In the meantime, let me propose some sections..."
- Switching from approval mode to direct mode without asking
- In approval mode: making edits without showing the change first
`;
export default skill;

View file

@ -1,252 +0,0 @@
export const skill = String.raw`
# Email Draft Skill
You are helping the user draft email responses. Use their calendar and knowledge base for context.
## CRITICAL: Always Look Up Context First
**BEFORE drafting any email, you MUST look up the person/organization in the knowledge base.**
**PATH REQUIREMENT:** Always use \`knowledge/\` as the path (not empty, not the workspace root, not an absolute path).
- **WRONG:** \`path: ""\` or \`path: "."\`
- **CORRECT:** \`path: "knowledge/"\`
When the user says "draft an email to Monica" or mentions ANY person, organization, project, or topic:
1. **STOP** - Do not draft anything yet
2. **SEARCH** - Look them up in the knowledge base (path MUST be \`knowledge/\`):
\`\`\`
workspace-grep({ pattern: "Monica", path: "knowledge/" })
\`\`\`
3. **READ** - Read their note to understand who they are:
\`\`\`
workspace-readFile("knowledge/People/Monica Smith.md")
\`\`\`
4. **UNDERSTAND** - Extract their role, organization, relationship history, past interactions, open items
5. **THEN DRAFT** - Only now draft the email, using this context
**DO NOT** skip this step. **DO NOT** provide generic templates. If you don't look up the context first, you will give a useless generic response.
## Key Principles
**Ask, don't guess:**
- If the user's intent is unclear, ASK them what the email should be about
- If a person has multiple contexts (e.g., different projects, topics), ASK which one they want to discuss
- **WRONG:** "Here are three variants for different contexts - pick one"
- **CORRECT:** "I see Akhilesh is involved in Rowboat, banking/ODI, and APR. Which topic would you like to discuss in this email?"
**Be decisive, not generic:**
- Once you know the context, draft ONE email - no multiple versions or options
- Do NOT provide generic templates - every draft should be personalized based on knowledge base context
- Infer the right tone, content, and approach from the context you gather
- Do NOT hedge with "here are a few options" or "you could say X or Y" - either ask for clarification OR make a decision and draft ONE email
## State Management
All state is stored in \`pre-built/email-draft/\`:
- \`state.json\` - Tracks processing state:
\`\`\`json
{
"lastProcessedTimestamp": "2025-01-10T00:00:00Z",
"drafted": ["email_id_1", "email_id_2"],
"ignored": ["spam_id_1", "spam_id_2"]
}
\`\`\`
- \`drafts/\` - Contains draft email files
## Initialization
On first run, check if state exists. If not, create it:
1. Check if \`pre-built/email-draft/state.json\` exists
2. If not, create \`pre-built/email-draft/\` and \`pre-built/email-draft/drafts/\`
3. Initialize \`state.json\` with empty arrays and a timestamp of "1970-01-01T00:00:00Z"
## Processing Flow
### Step 1: Load State
Read \`pre-built/email-draft/state.json\` to get:
- \`lastProcessedTimestamp\` - Only process emails newer than this
- \`drafted\` - List of email IDs already drafted (skip these)
- \`ignored\` - List of email IDs marked as ignored (skip these)
### Step 2: Scan for New Emails
List emails in \`gmail_sync/\` folder.
For each email file:
1. Extract the email ID from filename (e.g., \`19048cf9c0317981.md\` -> \`19048cf9c0317981\`)
2. Skip if ID is in \`drafted\` or \`ignored\` lists
3. Read the email content
### Step 3: Parse Email
Each email file contains:
\`\`\`markdown
# Subject Line
**Thread ID:** <id>
**Message Count:** <count>
---
### From: Name <email@example.com>
**Date:** <date string>
<email body>
\`\`\`
Extract:
- Thread ID (this is the email ID)
- From (sender name and email)
- Date
- Subject (from the # heading)
- Body content
- Message count (to understand if it's a thread)
### Step 4: Classify Email
Determine the email type and action:
**IGNORE these (add to \`ignored\` list):**
- Newsletters (unsubscribe links, "View in browser", bulk sender indicators)
- Marketing emails (promotional language, no-reply senders)
- Automated notifications (GitHub, Jira, Slack, shipping updates)
- Spam or cold outreach that's clearly irrelevant
- Emails where you (the user) are the sender and it's outbound with no reply
**DRAFT response for:**
- Meeting requests or scheduling emails
- Personal emails from known contacts
- Business inquiries that seem legitimate
- Follow-ups on existing conversations
- Emails requesting information or action
### Step 5: Gather Context
Before drafting, gather relevant context. **Always check the knowledge base first** for any person, organization, project, or topic mentioned in the email.
**Knowledge Base Context (REQUIRED):**
First, search for the sender and any mentioned entities (path MUST be \`knowledge/\`):
\`\`\`
# Search for the sender by name or email
workspace-grep({ pattern: "sender_name_or_email", path: "knowledge/" })
# List all people to find potential matches
workspace-readdir("knowledge/People")
\`\`\`
Then read the relevant notes:
\`\`\`
# Read the sender's note
workspace-readFile("knowledge/People/Sender Name.md")
# Read their organization's note
workspace-readFile("knowledge/Organizations/Company Name.md")
\`\`\`
Extract from these notes:
- Their role, title, and organization
- History of past interactions and meetings
- Commitments made (by them or to them)
- Open items and pending actions
- Relationship context and rapport
Use this context to provide informed, personalized responses that demonstrate you remember past interactions.
**Calendar Context** (for scheduling emails):
- Read calendar events from \`calendar_sync/\` folder
- Look for events in the relevant time period
- Check for conflicts, availability
### Step 6: Create Draft
For emails that need a response, create a draft file in \`pre-built/email-draft/drafts/\`:
**Filename:** \`{email_id}_draft.md\`
**Content format:**
\`\`\`markdown
# Draft Response
**Original Email ID:** {email_id}
**Original Subject:** {subject}
**From:** {sender}
**Date Processed:** {current_date}
---
## Context Used
- Calendar: {relevant calendar info or "N/A"}
- Memory: {relevant notes or "N/A"}
---
## Draft Response
Subject: Re: {original_subject}
{draft email body}
---
## Notes
{any notes about why this response was crafted this way}
\`\`\`
**Drafting Guidelines:**
- Draft ONE email - do not offer multiple versions or options unless explicitly asked
- Be concise and professional
- For scheduling: propose specific times based on calendar availability
- For inquiries: answer directly or indicate what info is needed
- Reference any relevant context from memory naturally - show you remember past interactions
- Match the tone of the incoming email
- If it's a thread with multiple messages, read the full context
- Do NOT use generic templates or placeholder language - personalize based on knowledge base
- If you're unsure about the user's intent, ask a clarifying question first
### Step 7: Update State
After processing each email:
1. Add the email ID to either \`drafted\` or \`ignored\` list
2. Update \`lastProcessedTimestamp\` to the current time
3. Write updated state to \`pre-built/email-draft/state.json\`
## Output
After processing all new emails, provide a summary:
\`\`\`
## Processing Summary
**Emails Scanned:** X
**Drafts Created:** Y
**Ignored:** Z
### Drafts Created:
- {email_id}: {subject} - {brief reason}
### Ignored:
- {email_id}: {subject} - {reason for ignoring}
\`\`\`
## Error Handling
- If an email file is malformed, log it and continue
- If calendar/notes folders don't exist, proceed without that context
- Always save state after each email to avoid reprocessing on failure
## Important Notes
- Never actually send emails - only create drafts
- The user will review and send drafts manually
- Be conservative with ignore - when in doubt, create a draft
- For ambiguous emails, create a draft with a note explaining the ambiguity
`;
export default skill;

View file

@ -1,237 +0,0 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import builtinToolsSkill from "./builtin-tools/skill.js";
import deletionGuardrailsSkill from "./deletion-guardrails/skill.js";
import docCollabSkill from "./doc-collab/skill.js";
import draftEmailsSkill from "./draft-emails/skill.js";
import mcpIntegrationSkill from "./mcp-integration/skill.js";
import meetingPrepSkill from "./meeting-prep/skill.js";
import organizeFilesSkill from "./organize-files/skill.js";
import createPresentationsSkill from "./create-presentations/skill.js";
import appNavigationSkill from "./app-navigation/skill.js";
import browserControlSkill from "./browser-control/skill.js";
import codeWithAgentsSkill from "./code-with-agents/skill.js";
import composioIntegrationSkill from "./composio-integration/skill.js";
import liveNoteSkill from "./live-note/skill.js";
import backgroundTaskSkill from "./background-task/skill.js";
import notifyUserSkill from "./notify-user/skill.js";
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url));
const CATALOG_PREFIX = "src/application/assistant/skills";
// console.log(liveNoteSkill);
type SkillDefinition = {
id: string; // Also used as folder name
title: string;
summary: string;
content: string;
};
type ResolvedSkill = {
id: string;
catalogPath: string;
content: string;
};
const definitions: SkillDefinition[] = [
{
id: "create-presentations",
title: "Create Presentations",
summary: "Create PDF presentations and slide decks from natural language requests using knowledge base context.",
content: createPresentationsSkill,
},
{
id: "doc-collab",
title: "Document Collaboration",
summary: "Collaborate on documents - create, edit, and refine notes and documents in the knowledge base.",
content: docCollabSkill,
},
{
id: "draft-emails",
title: "Draft Emails",
summary: "Process incoming emails and create draft responses using calendar and knowledge base for context.",
content: draftEmailsSkill,
},
{
id: "meeting-prep",
title: "Meeting Prep",
summary: "Prepare for meetings by gathering context about attendees from the knowledge base.",
content: meetingPrepSkill,
},
{
id: "organize-files",
title: "Organize Files",
summary: "Find, organize, and tidy up files on the user's machine. Move files to folders, clean up Desktop/Downloads, locate specific files.",
content: organizeFilesSkill,
},
{
id: "builtin-tools",
title: "Builtin Tools Reference",
summary: "Understanding and using builtin tools (especially executeCommand for bash/shell) in agent definitions.",
content: builtinToolsSkill,
},
{
id: "mcp-integration",
title: "MCP Integration Guidance",
summary: "Discovering, executing, and integrating MCP tools. Use this to check what external capabilities are available and execute MCP tools on behalf of users.",
content: mcpIntegrationSkill,
},
{
id: "composio-integration",
title: "Composio Integration",
summary: "Interact with third-party services (Gmail, GitHub, Slack, LinkedIn, Notion, Jira, Google Sheets, etc.) via Composio. Search, connect, and execute tools.",
content: composioIntegrationSkill,
},
{
id: "deletion-guardrails",
title: "Deletion Guardrails",
summary: "Following the confirmation process before removing workflows or agents and their dependencies.",
content: deletionGuardrailsSkill,
},
{
id: "app-navigation",
title: "App Navigation",
summary: "Navigate the app UI - open notes, switch views, filter/search the knowledge base, and manage saved views.",
content: appNavigationSkill,
},
{
id: "code-with-agents",
title: "Code with Agents",
summary: "Write code, build projects, create scripts, or fix bugs by delegating to Claude Code or Codex via acpx.",
content: codeWithAgentsSkill,
},
{
id: "background-task",
title: "Background Tasks",
summary: "Set up a recurring background task — persistent instructions the agent fires on a schedule and/or on matching events (Gmail, Calendar). Either maintains an `index.md` digest (OUTPUT mode) or performs a recurring side-effect like drafting a reply / posting to Slack / calling an API (ACTION mode). Flagship surface for anything recurring.",
content: backgroundTaskSkill,
},
{
id: "live-note",
title: "Live Notes",
summary: "Make a specific markdown note self-updating — a single `live:` objective in the frontmatter that the live-note agent maintains on a schedule or on incoming events. Load only when the user explicitly says 'live note' / 'live-note'; for anything else recurring, prefer the background-task skill.",
content: liveNoteSkill,
},
{
id: "browser-control",
title: "Browser Control",
summary: "Control the embedded browser pane - open sites, inspect page state, and interact with indexed page elements.",
content: browserControlSkill,
},
{
id: "notify-user",
title: "Notify User",
summary: "Send native desktop notifications with optional clickable links — including rowboat:// deep links that open a specific note, chat, or view inside the app.",
content: notifyUserSkill,
},
];
const skillEntries = definitions.map((definition) => ({
...definition,
catalogPath: `${CATALOG_PREFIX}/${definition.id}/skill.ts`,
}));
const catalogSections = skillEntries.map((entry) => [
`## ${entry.title}`,
`- **Skill file:** \`${entry.catalogPath}\``,
`- **Use it for:** ${entry.summary}`,
].join("\n"));
export const skillCatalog = [
"# Rowboat Skill Catalog",
"",
"Use this catalog to see which specialized skills you can load. Each entry lists the exact skill file plus a short description of when it helps.",
"",
catalogSections.join("\n\n"),
].join("\n");
/**
* Build a skill catalog string, optionally excluding specific skills by ID.
*/
export function buildSkillCatalog(options?: { excludeIds?: string[] }): string {
const entries = options?.excludeIds
? skillEntries.filter(e => !options.excludeIds!.includes(e.id))
: skillEntries;
const sections = entries.map((entry) => [
`## ${entry.title}`,
`- **Skill file:** \`${entry.catalogPath}\``,
`- **Use it for:** ${entry.summary}`,
].join("\n"));
return [
"# Rowboat Skill Catalog",
"",
"Use this catalog to see which specialized skills you can load. Each entry lists the exact skill file plus a short description of when it helps.",
"",
sections.join("\n\n"),
].join("\n");
}
const normalizeIdentifier = (value: string) =>
value.trim().replace(/\\/g, "/").replace(/^\.\/+/, "");
const aliasMap = new Map<string, ResolvedSkill>();
const registerAlias = (alias: string, entry: ResolvedSkill) => {
const normalized = normalizeIdentifier(alias);
if (!normalized) return;
aliasMap.set(normalized, entry);
};
const registerAliasVariants = (alias: string, entry: ResolvedSkill) => {
const normalized = normalizeIdentifier(alias);
if (!normalized) return;
const variants = new Set<string>([normalized]);
if (/\.(ts|js)$/i.test(normalized)) {
variants.add(normalized.replace(/\.(ts|js)$/i, ""));
variants.add(
normalized.endsWith(".ts") ? normalized.replace(/\.ts$/i, ".js") : normalized.replace(/\.js$/i, ".ts"),
);
} else {
variants.add(`${normalized}.ts`);
variants.add(`${normalized}.js`);
}
for (const variant of variants) {
registerAlias(variant, entry);
}
};
for (const entry of skillEntries) {
const absoluteTs = path.join(CURRENT_DIR, entry.id, "skill.ts");
const absoluteJs = path.join(CURRENT_DIR, entry.id, "skill.js");
const resolvedEntry: ResolvedSkill = {
id: entry.id,
catalogPath: entry.catalogPath,
content: entry.content,
};
const baseAliases = [
entry.id,
`${entry.id}/skill`,
`${entry.id}/skill.ts`,
`${entry.id}/skill.js`,
`skills/${entry.id}/skill.ts`,
`skills/${entry.id}/skill.js`,
`${CATALOG_PREFIX}/${entry.id}/skill.ts`,
`${CATALOG_PREFIX}/${entry.id}/skill.js`,
absoluteTs,
absoluteJs,
];
for (const alias of baseAliases) {
registerAliasVariants(alias, resolvedEntry);
}
}
export const availableSkills = skillEntries.map((entry) => entry.id);
export function resolveSkill(identifier: string): ResolvedSkill | null {
const normalized = normalizeIdentifier(identifier);
if (!normalized) return null;
return aliasMap.get(normalized) ?? null;
}

View file

@ -1,639 +0,0 @@
import { z } from 'zod';
import { stringify as stringifyYaml } from 'yaml';
import { LiveNoteSchema } from '@x/shared/dist/live-note.js';
const schemaYaml = stringifyYaml(z.toJSONSchema(LiveNoteSchema)).trimEnd();
const richBlockMenu = `**5. Rich block render — when the data has a natural visual form.**
The live-note agent can emit *rich blocks* special fenced blocks the editor renders as styled UI (charts, calendars, embedded iframes, etc.). When the data fits one of these shapes, mention it in the objective so the agent doesn't fall back to plain markdown:
- \`table\` — multi-row data, scoreboards, leaderboards. *"Render the leaderboard as a \`table\` block with columns Rank, Title, Points, Comments."*
- \`chart\` — time series, breakdowns, share-of-total. *"Plot the rate as a \`chart\` block (line, bar, or pie) with x=date, y=rate."*
- \`mermaid\` — flowcharts, sequence/relationship diagrams, gantt charts. *"Render the dependency map as a \`mermaid\` diagram."*
- \`calendar\` — upcoming events / agenda. *"Show the agenda as a \`calendar\` block."*
- \`email\` — single email thread digest (subject, from, summary, latest body, optional draft). *"Render the most important unanswered thread as an \`email\` block."*
- \`image\` — single image with caption. *"Render the cover photo as an \`image\` block."*
- \`embed\` — YouTube or Figma. *"Render the demo as an \`embed\` block."*
- \`iframe\` — live dashboards, status pages, anything that benefits from being live not snapshotted. *"Embed the status page as an \`iframe\` block pointing to <url>."*
- \`transcript\` — long meeting transcripts (collapsible). *"Render the transcript as a \`transcript\` block."*
- \`prompt\` — a "next step" Copilot card the user can click to start a chat. *"End with a \`prompt\` block labeled '<short label>' that runs '<longer prompt to send to Copilot>'."*
You **do not** need to write the block body yourself describe the desired output inside the objective and the live-note agent will format it (it knows each block's exact schema). Avoid \`task\` block types — those are user-authored input, not agent output.
- Good: "Show today's calendar events. Render as a \`calendar\` block with \`showJoinButton: true\`."
- Good: "Plot USD/INR over the last 7 days as a \`chart\` block — line chart, x=date, y=rate."
- Bad: "Show today's calendar." (vague agent may produce a markdown bullet list when the user wants the rich block)`;
export const skill = String.raw`
# Live Notes Skill
A *live note* is a regular markdown note whose body is kept current by a background agent. The user expresses intent via a single \`live:\` block in the note's YAML frontmatter — one persistent **objective** plus an optional \`triggers\` object that says when the agent should fire (cron, time-of-day windows, and/or matching events). A note with no \`live:\` key is just static; adding one makes it live. Users manage live notes in the **Live Note panel** (Radio icon at the top-right of the editor).
When this skill is loaded, your job is: make a passive note live (or extend the objective on an already-live note), run the agent once so the user immediately sees content, and tell them where to manage it.
## Mode: act-first (non-negotiable on strong signals)
Live-note creation and editing are **action-first**. Strong-signal asks (see below) get *executed*, not discussed. Read the file, write the \`live:\` block via \`workspace-edit\`, run the agent once, and confirm in one line at the end. Past tense, not future tense.
What you must NOT do on a strong-signal ask:
- Don't ask "Should I make edits directly, or show changes first for approval?" that prompt belongs to generic doc editing, not live notes.
- Don't ask "where should this live?" pick a default folder (see below) and proceed.
- Don't say "I'll create knowledge/Notes/X.md" without the action attached. Either say "Done created" or just do it.
- Don't open with an explanation of what a live note is. The user already asked for one.
- **Don't ask "should I do this?" when the request is unambiguous, just do it.** A clarifying question is reserved for *genuine* ambiguity (see "When to ask one short question" below), not as a politeness gate.
If a previous skill or earlier turn was waiting on edit-mode permission, treat the live-note request as implicit "direct mode" and proceed.
The two **panel-driven** flows in "Exceptions" at the bottom of this skill are the only places where a first-turn explanation is wanted. Don't bleed that posture into normal asks.
## Reading the user's intent
You're loaded any time the user might be asking for something dynamic. Three postures, depending on signal strength:
### Strong signals act, then confirm (default behaviour)
The user used unambiguous language asking for something to be tracked. **Just do it** pick a default folder, look for an existing matching note, then either extend its objective or create a new live note. Run it once. Confirm in one line. No "should I?" gate.
- **Cadence words**: "every morning…", "daily…", "each Monday…", "hourly weather here"
- **Living-document verbs**: "keep a running summary of…", "maintain a digest of…", "build up notes on…", "roll up X here"
- **Watch/monitor verbs**: "watch X", "monitor Y", "keep an eye on Z", "follow the Acme deal", "stay on top of…"
- **Pin-live framings**: "pin live updates of…", "always show the latest X here", "keep this fresh"
- **Direct**: "set up a [feed / tracker / dashboard / live note] for X", "track X" / "make this live"
- **Event-conditional**: "whenever a relevant email comes in, update…", "if anyone mentions X, capture it here"
### Default folder picker (when no note is named)
When a strong signal lands without a specific note attached, pick the folder by topic shape. Don't ask the user pick.
| Topic shape | Default folder |
|---|---|
| News, headlines, market prices, weather, status pages, reference dashboards | \`knowledge/Notes/\` |
| Tasks, monitors, daily briefings, recurring digests of the user's own data, "background agent"-style work | \`knowledge/Tasks/\` |
| A specific person (e.g. "track everything about Sarah Chen") | \`knowledge/People/\` |
| A specific company / org | \`knowledge/Organizations/\` |
| A specific project or workstream | \`knowledge/Projects/\` |
| A topic / theme | \`knowledge/Topics/\` |
**Filename**: derive from the topic in title-case (\`News Feed.md\`, \`Coinbase News.md\`, \`SFO Weather.md\`).
**Before creating**: \`workspace-grep\` and \`workspace-glob\` the chosen folder for an existing note that already covers the topic. If one exists with a \`live:\` block, **extend its objective** (see "Already-live notes — extend, don't fork"). If one exists without a \`live:\` block, **make that note live** (don't create a duplicate). Only create a new file when no match is found.
### Default cadence picker (when the user didn't specify timing)
When the user names a topic but doesn't say *how often*, **pick a cadence** — don't ask. Use judgment based on the topic shape. The user can tweak it later in the panel.
| Topic shape | Default cadence |
|---|---|
| News / market summary / topic-following / weather / status | One morning **window** \`06:00\`\`12:00\`. Add an \`eventMatchCriteria\` when the topic could also surface in synced Gmail/Calendar. |
| Stock / crypto prices when the user says "real-time" or "throughout the day" | \`cronExpr\` hourly or every 15 min, depending on phrasing. |
| Daily briefings / dashboards | Two or three **windows** spanning the workday (morning, midday, post-lunch). |
| Email / calendar-driven topics (Q3 emails, customer reschedules) | \`eventMatchCriteria\` only — schedule is "when a relevant signal arrives". Add a single morning window if a fallback baseline refresh feels right. |
**When in doubt, default to a single morning window \`06:00\`\`12:00\`.** It's forgiving (fires whenever the user opens the app in the morning) and matches the casual "I'll check this in the morning" expectation.
Reach for a precise \`cronExpr\` only when the user explicitly demands a clock time ("at 9am sharp", "every 15 minutes"). Casual asks ("every morning", "daily") get windows.
### When to ask one short question
Only when the request is **genuinely** ambiguous not as a politeness gate. Examples:
- The user named a specific note that doesn't exist AND your search for similar names returned multiple plausible candidates ask "Did you mean A or B?"
- The new ask in an already-live note conflicts with the existing objective (replace, not extend) ask "Replace the existing objective, or add this on top?"
- The topic is too vague to derive a sensible filename or folder ("track stuff for me") ask one focusing question.
Pick a single question, get to the action on the next turn. Never stack questions.
### Medium signals answer the one-off, then offer
Answer the user's actual question first. Then add a single-line offer to keep it updated. **The offer is not optional on a medium signal — if you don't add it, you're failing the skill.** If the user says yes, make the note live. If they don't engage, leave it don't push twice.
- **Time-decaying one-offs**: "what's USD/INR right now?", "top HN stories?", "weather?", "status of service X?"
- **News / updates on a topic**: "what's the latest news on Coinbase?", "what's happening with the Q3 launch?", "any updates on Project Apollo?", "what's new with [person/company]?"
- **Note-anchored snapshots**: "show me my schedule today", "put my open tasks here", "drop the latest commits here" especially when in a note context
- **Recurring artifacts**: "I'm starting a weekly review note", "my morning briefing", "a dashboard for the Acme deal"
- **Topic-following / catch-up**: "catch me up on the migration project", "I want to follow Project Apollo"
**Catch-all heuristic:** if you reached for \`web-search\` or a news tool to answer a question about a person, company, project, or topic, the answer is exactly the kind of thing a live note would refresh on a schedule — **always offer** at the end. Same goes for any time-decaying lookup (prices, weather, status).
Offer line shape (one line, concrete):
> "Want me to keep this in a live note that refreshes every morning?"
Or, when there's a sensible default file already implied (e.g. a topic name):
> "I can drop this in \`knowledge/Notes/Coinbase News.md\` and refresh it every morning — want that?"
The offer goes at the **very end** of your response, on its own line, after the answer is fully delivered.
### Anti-signals do NOT make a note live
- Definitional questions ("what is X?")
- One-off lookups ("look up X for me")
- Manual document work ("help me write…", "edit this paragraph…")
- General how-to ("how do I do Y?")
## Already-live notes extend, don't fork
**This is the most important rule of the skill.** When the user asks you to track something *new* in a note that **already has a \`live:\` block**, edit the existing \`objective\` in natural language to absorb the new ask. Do **not** create a second \`live:\` block. Do **not** introduce some other key. There is exactly one objective per note.
- The user says "also keep an eye on Hacker News stories about this" read the current \`objective\`, append/integrate the new ask in natural-language prose, write it back.
- The objective ends up longer over time. That's fine. The agent treats it as one coherent intent.
- If the new ask conflicts with the old (e.g. user wants to *replace* what the note tracks), ask one short question to confirm before overwriting.
## What to say to the user
The user knows the feature as **live notes** and finds them in the **Live notes view**. Speak in those terms; don't expose internals like "frontmatter", "trigger", or "objective" in user-facing prose unless the user uses them first.
**Use past tense.** All of these messages are sent *after* the action no future-tense "I'll do this" or "I'm going to set this up". The action already happened.
After making a passive note live (or creating a new live note from scratch):
> Done created \`knowledge/Notes/News Feed.md\` and made it live, refreshing every morning. Running it once now so you see content right away. Manage it from the Live notes view (Radio icon in the sidebar).
After extending the objective on an already-live note:
> Updated the objective to also cover that. Re-running now so the new output shows up.
When skipping a re-run (because the user said not to or "later"):
> Updated. I'll let it run on its next trigger.
**Anti-patterns** don't write any of these:
- "I'll set up a live note for you. Should I create knowledge/Notes/News Feed.md?" (future tense, asking permission)
- "I need one thing to proceed: which note should this live in?" (asking when default-folder picker tells you the answer)
- "That's a live note use case! Here's what I can set up: ..." (preamble + lecture instead of action)
- "Here's a comprehensive setup..." or "I've prepared the following..." (decorative framing)
## Worked example strong signal, no note named
**User:** "i want to set up a news feed to track news for India and the world."
**Right behaviour** (one turn):
1. \`workspace-grep({ pattern: "News Feed", path: "knowledge/Notes/" })\` — search for an existing match.
2. \`workspace-grep({ pattern: "news", path: "knowledge/Notes/" })\` — broader search to catch variants.
3. No match found create \`knowledge/Notes/News Feed.md\` with a sensible \`live:\` block (objective covering India + world headlines, a windows trigger for "every morning"-style refresh, plus an \`eventMatchCriteria\` if news might come from synced data).
4. Call \`run-live-note-agent\` with a backfill \`context\` so the body isn't empty.
5. Reply: "Done — created \`knowledge/Notes/News Feed.md\` and made it live, refreshing every morning. Running it once now so you see content right away. Manage it from the Live notes view."
**Wrong behaviour:** running 2 lookup tools, then surfacing a paragraph saying "That's a live note use case, so the clean setup is a self-updating news note with: India headlines, world headlines, a refresh cadence like every morning. I need one thing to proceed: which note should this live in? If you don't already have one, I'll create knowledge/Notes/News Feed.md and make it live there." The user already gave you everything you need. Act.
## What is a live note (concretely)
**Concrete example** a note that shows the current Chicago time, refreshed hourly:
` + "```" + `markdown
---
live:
objective: |
Show the current time in Chicago, IL in 12-hour format. Keep it as one
short line, no extra prose.
active: true
triggers:
cronExpr: "0 * * * *"
---
# Chicago time
(empty the agent will fill this in on the first run)
` + "```" + `
After the first run, the body might become:
` + "```" + `markdown
# Chicago time
2:30 PM, Central Time
` + "```" + `
Good use cases:
- Weather / air quality for a location
- News digests or headlines
- Stock or crypto prices
- Sports scores
- Service status pages
- Personal dashboards (today's calendar, steps, focus stats)
- Living summaries fed by incoming events (emails, meeting notes)
- Any recurring content that decays fast
## Anatomy
A live note lives entirely in the note's frontmatter there is no inline marker in the body. The agent owns the entire body below the H1 and writes whatever content the objective demands.
The frontmatter block is fenced by ` + "`" + `---` + "`" + ` lines at the very top of the file:
` + "```" + `markdown
---
live:
objective: |
<what this note should keep being>
active: true
triggers:
cronExpr: "0 * * * *"
---
# Note body
` + "```" + `
A note has **at most one** \`live:\` block. Each block has exactly one \`objective\`. The objective can be long and cover several sub-topics — the agent reads it holistically. Omit \`triggers\` (or all three trigger fields) for a manual-only live note.
## Canonical Schema
Below is the authoritative schema for a \`live:\` block (generated at build time from the TypeScript source — never out of date). Use it to validate every field name, type, and constraint before writing YAML:
` + "```" + `yaml
${schemaYaml}
` + "```" + `
**Runtime-managed fields never write these yourself:** ` + "`" + `lastRunAt` + "`" + `, ` + "`" + `lastRunId` + "`" + `, ` + "`" + `lastRunSummary` + "`" + `.
## Do Not Set ` + "`" + `model` + "`" + ` or ` + "`" + `provider` + "`" + ` (almost always)
The schema includes optional ` + "`" + `model` + "`" + ` and ` + "`" + `provider` + "`" + ` fields. **Omit them.** A user-configurable global default already picks the right model and provider for live-note runs; setting per-note values bypasses that and is almost always wrong.
The only time these belong on a note:
- The user **explicitly** named a model or provider for *this specific note* in their request ("use Claude Opus for this one", "force this onto OpenAI"). Quote the user's wording back when confirming.
Things that are **not** reasons to set these:
- "It should be fast" / "I want a small model" that's a global preference, not a per-note one. Leave it; the global default exists.
- "This note is complex" write a clearer objective; don't reach for a different model.
- "Just to be safe" / "in case it matters" antipattern. Leave them out.
When in doubt: omit both fields. Never volunteer them. Never include them in a starter template you suggest.
## Writing a Good Objective
### The Frame: This Is a Personal Knowledge Tracker
Live-note output lives in a personal knowledge base the user scans frequently. Aim for data-forward, scannable output the answer to "what's current / what changed?" in the fewest words that carry real information. Not prose. Not decoration.
### Core Rules
- **Specific and actionable.** State exactly what to keep up to date, what to source from, and what shape the output should take.
- **Multi-faceted is OK.** Unlike the old per-track model, a single objective can cover several related sub-topics list them inside the objective text and let the agent organize the body. Don't fork a second objective.
- **Imperative voice.** "Keep this note updated with…", "Show…", "Maintain a section titled…".
- **Specify output shape when shape matters.** "One line: ` + "`" + `<temp>°F, <conditions>` + "`" + `", "3-column markdown table", "bulleted digest of 5 items", or pick a rich block (see "Rich block render" below).
### Self-Sufficiency (critical)
The objective runs later, in a background scheduler, with **no chat context and no memory of this conversation**. It must stand alone.
**Never use phrases that depend on prior conversation or prior runs:**
- "as before", "same style as before", "like last time"
- "keep the format we discussed", "matching the previous output"
- "continue from where you left off" (without stating the state)
If you want consistent style across runs, **describe the style inline** (e.g. "a 3-column markdown table with headers ` + "`" + `Location` + "`" + `, ` + "`" + `Local Time` + "`" + `, ` + "`" + `Offset` + "`" + `"). The live-note agent only sees the objective not this chat, not what it produced last time.
### Output Patterns Match the Data
Pick a shape that fits what the note is tracking. Five common patterns the first four are plain markdown; the fifth is a rich rendered block:
**1. Single metric / status line.**
- Good: "Fetch USD/INR. Return one line: ` + "`" + `USD/INR: <rate> (as of <HH:MM IST>)` + "`" + `."
- Bad: "Give me a nice update about the dollar rate."
**2. Compact table.**
- Good: "Show current local time for India, Chicago, Indianapolis as a 3-column markdown table: ` + "`" + `Location | Local Time | Offset vs India` + "`" + `. One row per location, no prose."
- Bad: "Show a polished, table-first world clock with a pleasant layout."
**3. Rolling digest.**
- Good: "Summarize the top 5 HN front-page stories as bullets: ` + "`" + `- <title> (<points> pts, <comments> comments)` + "`" + `. No commentary."
- Bad: "Give me the top HN stories with thoughtful takeaways."
**4. Status / threshold watch.**
- Good: "Check https://status.example.com. Return one line: ` + "`" + ` All systems operational` + "`" + ` or ` + "`" + ` <component>: <status>` + "`" + `. If degraded, add one bullet per affected component."
- Bad: "Keep an eye on the status page and tell me how it looks."
${richBlockMenu}
### Per-trigger guidance (advanced)
**Default behaviour:** one objective serves all triggers cron, window, event, and manual runs all see the same intent. **Don't reach for per-trigger branching unless the run actually needs to behave differently.**
The agent always receives a \`**Trigger:**\` line in its run message telling it which trigger fired:
- \`Manual run (user-triggered)\` — Run button or Copilot tool.
- \`Scheduled refresh — the cron expression \\\`<expr>\\\` matched\` — exact-time refresh.
- \`Scheduled refresh — fired inside the configured window\` — forgiving once-per-day baseline refresh.
- \`Event match — Pass 1 routing flagged this note\` — comes with the event payload and a Pass 2 decision directive.
**When to branch in the objective:** there's a meaningful difference between the work to do on a *baseline* refresh (cron/window pull a full snapshot from local data) and a *reactive* update (event integrate one new signal). The flagship case is the **Today.md emails section**: on a window run it scans \`gmail_sync/\` for everything worth attention; on an event run with an incoming email payload it integrates that one thread into the existing digest without re-listing previously-seen threads. Same objective, two branches.
How to write it use plain conditional language inside the objective:
\`\`\`yaml
live:
objective: |
Maintain a digest of email threads worth attention today, as a single \`emails\` block.
Without an event payload (cron / window / manual runs): scan \`gmail_sync/\` and emit the
full digest from scratch.
With an event payload (event run): integrate the new thread into the existing digest
add it if new, update its entry if the threadId is already shown and don't re-list
threads the user has already seen unless their state changed.
\`\`\`
Notice: the objective doesn't mention "cron" or "window" by name, just describes the conditions. The agent reads its \`**Trigger:**\` line and matches the right branch.
**Don't branch for stylistic reasons** ("on cron be terse, on event be verbose"). Branching is for *what data to look at* and *whether to do an incremental vs full update*, not for tone.
### Anti-Patterns
- **Decorative adjectives** describing the output: "polished", "clean", "beautiful", "pleasant", "nicely formatted" they tell the agent nothing concrete.
- **References to past state** without a mechanism to access it ("as before", "same as last time").
- **A second \`live:\` block** when one already exists — extend the existing objective instead.
- **Open-ended prose requests** ("tell me about X", "give me thoughts on X").
## YAML String Style (critical read before writing the ` + "`" + `objective` + "`" + ` or ` + "`" + `triggers.eventMatchCriteria` + "`" + `)
The two free-form fields \`objective\` and \`triggers.eventMatchCriteria\` — are where YAML parsing usually breaks. The runner re-emits the full frontmatter every time it writes \`lastRunAt\`, \`lastRunSummary\`, etc., and the YAML library may re-flow long plain (unquoted) strings onto multiple lines. Once that happens, any ` + "`" + `:` + "`" + ` **followed by a space** inside the value silently corrupts the entry: YAML interprets the ` + "`" + `:` + "`" + ` as a new key/value separator and the field gets truncated.
### The rule: always use a safe scalar style
**Default to the literal block scalar (` + "`" + `|` + "`" + `) for ` + "`" + `objective` + "`" + ` and ` + "`" + `eventMatchCriteria` + "`" + `, every time.**
### Preferred: literal block scalar (` + "`" + `|` + "`" + `)
` + "```" + `yaml
live:
objective: |
Show current local time for India, Chicago, and Indianapolis as a
3-column markdown table: Location | Local Time | Offset vs India.
One row per location, 24-hour time (HH:MM), no extra prose.
active: true
triggers:
cronExpr: "0 * * * *"
eventMatchCriteria: |
Emails from the finance team about Q3 budget or OKRs.
` + "```" + `
- ` + "`" + `|` + "`" + ` preserves line breaks verbatim. Colons, ` + "`" + `#` + "`" + `, quotes, leading ` + "`" + `-` + "`" + `, percent signs all literal. No escaping needed.
- **Indent every content line by 2 spaces** relative to the key. Use spaces, never tabs.
- Leave a real newline after ` + "`" + `|` + "`" + ` content starts on the next line.
### Acceptable alternative: double-quoted on a single line
Fine for short single-sentence fields:
` + "```" + `yaml
live:
objective: "Show the current time in Chicago, IL in 12-hour format."
active: true
` + "```" + `
### Do NOT use plain (unquoted) scalars for these two fields
Even if the current value looks safe, a future edit may introduce a ` + "`" + `:` + "`" + ` or ` + "`" + `#` + "`" + `, and a future re-emit may fold the line. The ` + "`" + `|` + "`" + ` style is safe under **all** future edits.
### Never-hand-write fields
\`lastRunAt\`, \`lastRunId\`, \`lastRunSummary\` are owned by the runner. Don't touch them — don't even try to style them. If your edit's ` + "`" + `oldString` + "`" + ` happens to include these, copy them byte-for-byte into ` + "`" + `newString` + "`" + ` unchanged.
## Triggers
The \`triggers\` object has three optional sub-fields. Mix freely; presence of a field is the marker that the note should fire on that channel.
- \`cronExpr\` — fires at an exact recurring time (5-field cron string).
- \`windows\` — list of \`{ startTime, endTime }\` bands; the agent fires once per day per window, anywhere inside the band.
- \`eventMatchCriteria\` — natural-language description of which incoming events (emails, calendar changes) should wake the note.
Omit ` + "`" + `triggers` + "`" + ` entirely (or omit all three sub-fields) for a **manual-only** live note the user runs it from the Run button in the panel.
### \`cronExpr\`
` + "```" + `yaml
triggers:
cronExpr: "0 * * * *"
` + "```" + `
Always quote the cron expression it contains spaces and ` + "`" + `*` + "`" + `.
### \`windows\`
` + "```" + `yaml
triggers:
windows:
- { startTime: "09:00", endTime: "12:00" }
- { startTime: "13:00", endTime: "15:00" }
` + "```" + `
Each window fires **at most once per day, anywhere inside the time-of-day band** (24-hour HH:MM, local). The day's cycle is anchored at \`startTime\` — once a fire lands at-or-after today's start, that window is done for the day. Use windows when the user wants something to happen "in the morning" rather than at an exact clock time. Forgiving by design: if the app isn't open at the band's start, it still fires the moment the user opens it inside the band.
### \`eventMatchCriteria\`
` + "```" + `yaml
triggers:
eventMatchCriteria: |
Emails about Q3 planning, roadmap decisions, or quarterly OKRs.
` + "```" + `
How event triggering works:
1. When a new event arrives, a fast LLM classifier checks each live note's \`eventMatchCriteria\` (and its objective) against the event content.
2. If it might match, the live-note agent receives both the event payload and the existing note body, and decides whether to actually update.
3. If the event isn't truly relevant on closer inspection, the agent skips the update no fabricated content.
### Combining trigger fields
Mix freely. Example a note that refreshes weekday mornings AND on incoming Q3 emails:
` + "```" + `yaml
live:
objective: |
Maintain a running summary of decisions and open questions about Q3 planning.
active: true
triggers:
cronExpr: "0 9 * * 1-5"
eventMatchCriteria: |
Emails about Q3 planning, roadmap decisions, or quarterly OKRs.
` + "```" + `
### Cron cookbook
- ` + "`" + `"*/15 * * * *"` + "`" + ` every 15 minutes
- ` + "`" + `"0 * * * *"` + "`" + ` every hour on the hour
- ` + "`" + `"0 8 * * *"` + "`" + ` daily at 8am
- ` + "`" + `"0 9 * * 1-5"` + "`" + ` weekdays at 9am
- ` + "`" + `"0 0 * * 0"` + "`" + ` Sundays at midnight
- ` + "`" + `"0 0 1 * *"` + "`" + ` first of month at midnight
## Insertion Workflow
**Reminder:** once you have enough to act, act. Do not pause to ask about edit mode.
### Making a passive note live (no \`live:\` block yet)
1. \`workspace-readFile({ path })\` — re-read fresh.
2. Inspect existing frontmatter (the ` + "`" + `---` + "`" + `-fenced block at the top, if any).
3. \`workspace-edit\`:
- **If the note has frontmatter without a \`live:\` block**: anchor on the closing \`---\` of the frontmatter and insert the \`live:\` block just before it.
- **If the note has no frontmatter at all**: anchor on the very first line of the file. Replace it with a new frontmatter block (\`---\\n\` ... \`\\n---\\n\` followed by the original first line).
### Extending an already-live note
1. \`workspace-readFile({ path })\` — fetch the current \`live.objective\`.
2. Edit the \`objective\` value via \`workspace-edit\` to absorb the new ask in natural language. Keep the \`|\` block scalar style.
3. Don't touch other \`live:\` fields unless the user explicitly asked (e.g. "also run this hourly" → add/edit \`triggers.cronExpr\`).
### Sidebar chat with a specific note
1. If a file is mentioned/attached, read it.
2. If ambiguous, ask one question: "Which note should this be in?"
3. Apply the workflow above (extend if already live, create if passive).
### No note context at all
If the user used a strong signal but didn't name a specific note: **don't ask** "which note?" use the Default folder picker (above) and proceed. Create the file with a sensible filename derived from the topic.
If the user used a medium signal with no note: answer the one-off, then offer to make it live somewhere (and pick the folder when they say yes).
## Exceptions first-turn confirmation only when
The two flows below are the **only** exceptions to the act-first default. They have explicit panel/card context that wants a brief explanation before the user commits. Don't bleed this posture into normal asks outside these flows, strong signals get acted on, not explained.
### Exception 1: Suggested Topics exploration flow
Sometimes the user arrives from the Suggested Topics panel with a prompt like:
- "I am exploring a suggested topic card from the Suggested Topics panel."
- a title, category, description, and target folder such as \`knowledge/Topics/\` or \`knowledge/People/\`
This is a *browse* gesture, not a commit gesture the user might back out. So:
1. On the first turn, **do not create or modify anything yet**. Briefly explain the live note you can set up and ask for confirmation.
2. If the user clearly confirms ("yes", "set it up", "do it"), treat that as explicit permission to proceed.
3. Before creating a new note, search the target folder for an existing matching note and update it (extend objective if already live; make it live otherwise).
4. If no matching note exists and the prompt gave you a target folder, create the new note there without bouncing back to ask.
5. Use the card title as the default note title / filename unless a small normalization is clearly needed.
6. Keep the surrounding note scaffolding minimal but useful. The \`live:\` block should be the core of the note.
### Exception 2: New-live-note panel flow (panel-driven, no note named)
The user clicks the "New live note" button in the **Live notes** view and the opening message is the canned "I want to set up a Live note / task." (no specific topic, no note named). This is the only case where you ask before acting but the ask is minimal.
On the first turn, reply with **just** a one-line prompt and 2-3 concrete examples. **Do not** explain what a live note is. **Do not** ask about cadence, folder, or format you'll pick those yourself once they name a topic. Examples to draw from (pick 2-3 that span different shapes):
- A daily news feed for a topic ("AI coding agents", "India + world news")
- A market summary ("BTC, ETH, SPY each morning")
- A weekly Q3-emails digest from your inbox
- A morning weather + commute-conditions briefing
- A live dashboard for an ongoing project
Shape your reply roughly like:
> What would you like to track? A few examples to spark ideas:
> - A daily news feed for a topic
> - A market summary
> - A digest of relevant emails
Once the user names a topic, **drop into the strong-signal flow**: use the Default folder picker for location, the Default cadence picker for timing, search for an existing match, extend or create, run once, confirm in one line. Don't bounce back with "great — and how often should it refresh?" pick.
**The trigger for Exception 2 is specifically the generic "I want to set up a Live note / task." opening.** A user asking "set up a news feed for India and the world" is *not* in this flow that's a strong signal, act on it.
## The Exact Frontmatter Shape
For a brand-new live note:
` + "```" + `markdown
---
live:
objective: |
<objective, indented 2 spaces, may span multiple lines>
active: true
triggers:
cronExpr: "0 * * * *"
---
# <Note title>
` + "```" + `
**Rules:**
- \`live:\` is at the top level of the frontmatter, never nested under other keys.
- There is **at most one** \`live:\` block per note.
- 2-space YAML indent throughout. No tabs.
- \`triggers:\` is an object, not an array. Each sub-field (\`cronExpr\`, \`windows\`, \`eventMatchCriteria\`) is independently optional. Omit \`triggers\` entirely for manual-only.
- **Always use the literal block scalar (\`|\`)** for \`objective\` and \`eventMatchCriteria\`.
- **Always quote cron expressions** in YAML they contain spaces and \`*\`.
- The note body below the frontmatter can start empty, with a heading, or with whatever scaffolding the user wants. The live-note agent edits the body on its first run.
## After Creating or Editing a Live Note
**Run it once.** Always. The only exception is when the user explicitly said *not* to ("don't run yet", "I'll run it later", "no need to run it now"). Use the \`run-live-note-agent\` tool — same as the user clicking Run in the panel.
Why default-on:
- For event-driven live notes (with \`eventMatchCriteria\`), the body stays empty until the next matching event arrives. Running once gives the user immediate content.
- For notes that pull from existing local data (synced emails, calendar, meeting notes), running with a backfill \`context\` (see below) seeds rich initial content.
- After an edit, the user expects to see the updated output without an extra round-trip.
Confirm in one line and tell the user where to find it:
> "Done — this note is live, refreshing hourly. Running it once now so you see content right away. You can manage it from the Live Note panel."
For an objective extension on an already-live note:
> "Updated the objective. Re-running now so you see the new output."
If you skipped the re-run (user said not to):
> "Updated — I'll let it run on its next trigger."
**Do not** write content into the note body yourself that's the live-note agent's job, delegated via \`run-live-note-agent\`.
## Using the \`run-live-note-agent\` tool
\`run-live-note-agent\` triggers a single run right now. You can pass an optional \`context\` string to bias *this run only* without modifying the objective — the difference between a stock refresh and a smart backfill.
### Backfill \`context\` examples
- A newly-live note watching Q3 emails run with:
> context: "Initial backfill — scan ` + "`" + `gmail_sync/` + "`" + ` for emails from the last 90 days about Q3 planning, OKRs, and roadmap, and synthesize the initial summary."
- A new note tracking this week's customer calls run with:
> context: "Backfill from this week's meeting notes in ` + "`" + `granola_sync/` + "`" + ` and ` + "`" + `fireflies_sync/` + "`" + `."
- Manual refresh after the user mentions a recent change:
> context: "Focus on changes from the last 7 days only."
- Plain refresh (user said "run it now"): **omit \`context\`**. Don't invent it.
### Reading the result
The tool returns ` + "`" + `{ success, runId, action, summary, contentAfter, error }` + "`" + `:
- \`action: 'replace'\` → body changed. Confirm in one line; optionally cite the first line of \`contentAfter\`.
- \`action: 'no_update'\` → agent decided nothing needed to change. Tell the user briefly; \`summary\` usually explains why.
- \`error: 'Already running'\` → another run is in flight; tell the user to retry shortly.
- Other \`error\` → surface concisely.
### Don'ts
- **Don't run more than once** per user-facing action one tool call per turn.
- **Don't pass \`context\`** for a plain refresh — it can mislead the agent.
- **Don't write content into the note body yourself** always delegate via \`run-live-note-agent\`.
## Don'ts
- **Don't create a second \`live:\` block** when one already exists — extend the existing \`objective\`.
- **Don't add \`triggers\`** if the user explicitly wants manual-only.
- **Don't write** \`lastRunAt\`, \`lastRunId\`, or \`lastRunSummary\` — runtime-managed.
- **Don't schedule** with ` + "`" + `"* * * * *"` + "`" + ` (every minute) unless the user explicitly asks.
- **Don't use \`workspace-writeFile\`** to rewrite the whole file — always \`workspace-edit\` with a unique anchor.
## Editing or Removing an Existing Live Note
**Change the objective:** \`workspace-edit\` the \`objective\` value (use \`|\` block scalar).
**Change triggers:** \`workspace-edit\` the relevant sub-field of the \`triggers\` object.
**Pause without removing:** flip \`active: false\`.
**Make passive (remove the \`live:\` block):** \`workspace-edit\` with \`oldString\` = the entire \`live:\` block (from the \`live:\` line down to the next top-level key or the closing \`---\`), \`newString\` = empty. The note body is left alone — if you want to clear leftover agent output, do that as a separate edit.
## Quick Reference
Minimal template (frontmatter only):
` + "```" + `yaml
live:
objective: |
<objective always use \`|\`, indented 2 spaces>
active: true
triggers:
cronExpr: "0 * * * *"
` + "```" + `
Top cron expressions: \`"0 * * * *"\` (hourly), \`"0 8 * * *"\` (daily 8am), \`"0 9 * * 1-5"\` (weekdays 9am), \`"*/15 * * * *"\` (every 15m).
YAML style reminder: \`objective\` and \`eventMatchCriteria\` are **always** \`|\` block scalars. Never plain. Never leave a plain scalar in place when editing.
`;
export default skill;

View file

@ -1,436 +0,0 @@
export const skill = String.raw`
# MCP Integration Guidance
**Load this skill proactively** when a user asks for ANY task that might require external capabilities (web search, internet access, APIs, data fetching, time/date, etc.). This skill provides complete guidance on discovering and executing MCP tools.
## CRITICAL: Composio Tools Take Priority Over MCP
**If a Composio toolkit is connected for the service the user wants (GitHub, Gmail, Slack, etc.), use the \`composio-search-tools\` and \`composio-execute-tool\` builtin tools — NOT MCP tools.** Composio integrations are already authenticated and ready to use. Only fall back to MCP tools if the service is NOT available through Composio.
## When to Check MCP Tools
**IMPORTANT**: When a user asks for a task that requires external capabilities AND no Composio toolkit covers it, check MCP tools:
1. **First check**: Call \`listMcpServers\` to see what's available
2. **Then list tools**: Call \`listMcpTools\` on relevant servers
3. **Execute if possible**: Use \`executeMcpTool\` if a tool matches the need
4. **Only then decline**: If no MCP tool can help, explain what's not possible
**DO NOT** immediately say "I can't do that" or "I don't have internet access" without checking MCP tools first!
### Common User Requests and MCP Tools
| User Request | Check For | Likely Tool |
|--------------|-----------|-------------|
| "Search the web/internet" | firecrawl, fetch | \`firecrawl_search\` |
| "Scrape this website" | firecrawl | \`firecrawl_scrape\` |
| "Read/write files" | filesystem | \`read_file\`, \`write_file\` |
| "Get current time/date" | time | \`get_current_time\` |
| "Make HTTP request" | fetch | \`fetch\`, \`post\` |
| "Generate audio/speech" | elevenLabs | \`text_to_speech\` |
## Key concepts
- MCP servers expose tools (web scraping, APIs, databases, etc.) declared in \`config/mcp.json\`.
- Agents reference MCP tools through the \`"tools"\` block by specifying \`type\`, \`name\`, \`description\`, \`mcpServerName\`, and a full \`inputSchema\`.
- Tool schemas can include optional property descriptions; only include \`"required"\` when parameters are mandatory.
## CRITICAL: Adding MCP Servers
**ALWAYS use the \`addMcpServer\` builtin tool** to add or update MCP server configurations. This tool validates the configuration before saving and prevents startup errors.
**NEVER manually create or edit \`config/mcp.json\`** using \`workspace-writeFile\` for MCP servers—this bypasses validation and will cause errors.
### MCP Server Configuration Schema
There are TWO types of MCP servers:
#### 1. STDIO (Command-based) Servers
For servers that run as local processes (Node.js, Python, etc.):
**Required fields:**
- \`command\`: string (e.g., "npx", "node", "python", "uvx")
**Optional fields:**
- \`args\`: array of strings (command arguments)
- \`env\`: object with string key-value pairs (environment variables)
- \`type\`: "stdio" (optional, inferred from presence of \`command\`)
**Schema:**
\`\`\`json
{
"type": "stdio",
"command": "string (REQUIRED)",
"args": ["string", "..."],
"env": {
"KEY": "value"
}
}
\`\`\`
**Valid STDIO examples:**
\`\`\`json
{
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/data"]
}
\`\`\`
\`\`\`json
{
"command": "python",
"args": ["-m", "mcp_server_git"],
"env": {
"GIT_REPO_PATH": "/path/to/repo"
}
}
\`\`\`
\`\`\`json
{
"command": "uvx",
"args": ["mcp-server-fetch"]
}
\`\`\`
#### 2. HTTP/SSE Servers
For servers that expose HTTP or Server-Sent Events endpoints:
**Required fields:**
- \`url\`: string (complete URL including protocol and path)
**Optional fields:**
- \`headers\`: object with string key-value pairs (HTTP headers)
- \`type\`: "http" (optional, inferred from presence of \`url\`)
**Schema:**
\`\`\`json
{
"type": "http",
"url": "string (REQUIRED)",
"headers": {
"Authorization": "Bearer token",
"Custom-Header": "value"
}
}
\`\`\`
**Valid HTTP examples:**
\`\`\`json
{
"url": "http://localhost:3000/sse"
}
\`\`\`
\`\`\`json
{
"url": "https://api.example.com/mcp",
"headers": {
"Authorization": "Bearer sk-1234567890"
}
}
\`\`\`
### Common Validation Errors to Avoid
**WRONG - Missing required field:**
\`\`\`json
{
"args": ["some-arg"]
}
\`\`\`
Error: Missing \`command\` for stdio OR \`url\` for http
**WRONG - Empty object:**
\`\`\`json
{}
\`\`\`
Error: Must have either \`command\` (stdio) or \`url\` (http)
**WRONG - Mixed types:**
\`\`\`json
{
"command": "npx",
"url": "http://localhost:3000"
}
\`\`\`
Error: Cannot have both \`command\` and \`url\`
**CORRECT - Minimal stdio:**
\`\`\`json
{
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-time"]
}
\`\`\`
**CORRECT - Minimal http:**
\`\`\`json
{
"url": "http://localhost:3000/sse"
}
\`\`\`
### Using addMcpServer Tool
**Example 1: Add stdio server**
\`\`\`json
{
"serverName": "filesystem",
"serverType": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/data"]
}
\`\`\`
**Example 2: Add HTTP server**
\`\`\`json
{
"serverName": "custom-api",
"serverType": "http",
"url": "https://api.example.com/mcp",
"headers": {
"Authorization": "Bearer token123"
}
}
\`\`\`
**Example 3: Add Python MCP server**
\`\`\`json
{
"serverName": "github",
"serverType": "stdio",
"command": "python",
"args": ["-m", "mcp_server_github"],
"env": {
"GITHUB_TOKEN": "ghp_xxxxx"
}
}
\`\`\`
## Operator actions
1. Use \`listMcpServers\` to enumerate configured servers.
2. Use \`addMcpServer\` to add or update MCP server configurations (with validation).
3. Use \`listMcpTools\` for a server to understand the available operations and schemas.
4. Use \`executeMcpTool\` to run MCP tools directly on behalf of the user.
5. Explain which MCP tools match the user's needs before editing agent definitions.
6. When adding a tool to an agent, document what it does and ensure the schema mirrors the MCP definition.
## Executing MCP Tools Directly (Copilot)
As the copilot, you can execute MCP tools directly on behalf of the user using the \`executeMcpTool\` builtin. This allows you to use MCP tools without creating an agent.
### When to Execute MCP Tools Directly
- User asks you to perform a task that an MCP tool can handle (web search, file operations, API calls, etc.)
- User wants immediate results from an MCP tool without setting up an agent
- You need to test or demonstrate an MCP tool's functionality
- You're helping the user accomplish a one-time task
### Workflow for Executing MCP Tools
1. **Discover available servers**: Use \`listMcpServers\` to see what MCP servers are configured
2. **List tools from a server**: Use \`listMcpTools\` with the server name to see available tools and their schemas
3. **CAREFULLY EXAMINE THE SCHEMA**: Look at the \`inputSchema\` to understand exactly what parameters are required
4. **Execute the tool**: Use \`executeMcpTool\` with the server name, tool name, and required arguments (matching the schema exactly)
5. **Return results**: Present the results to the user in a helpful format
### CRITICAL: Schema Matching
**ALWAYS** examine the \`inputSchema\` from \`listMcpTools\` before calling \`executeMcpTool\`.
The schema tells you:
- What parameters are required (check the \`"required"\` array)
- What type each parameter should be (string, number, boolean, object, array)
- Parameter descriptions and examples
**Example schema from listMcpTools:**
\`\`\`json
{
"name": "firecrawl_search",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query"
},
"limit": {
"type": "number",
"description": "Number of results"
}
},
"required": ["query"]
}
}
\`\`\`
**Correct executeMcpTool call:**
\`\`\`json
{
"serverName": "firecrawl",
"toolName": "firecrawl_search",
"arguments": {
"query": "latest AI news"
}
}
\`\`\`
**WRONG - Missing arguments:**
\`\`\`json
{
"serverName": "firecrawl",
"toolName": "firecrawl_search"
}
\`\`\`
**WRONG - Wrong parameter name:**
\`\`\`json
{
"serverName": "firecrawl",
"toolName": "firecrawl_search",
"arguments": {
"search": "latest AI news" // Wrong! Should be "query"
}
}
\`\`\`
### Example: Using Firecrawl to Search the Web
**Step 1: List servers**
\`\`\`json
// Call: listMcpServers
// Response: { "servers": [{"name": "firecrawl", "type": "stdio", ...}] }
\`\`\`
**Step 2: List tools**
\`\`\`json
// Call: listMcpTools with serverName: "firecrawl"
// Response: { "tools": [{"name": "firecrawl_search", "description": "Search the web", "inputSchema": {...}}] }
\`\`\`
**Step 3: Execute the tool**
\`\`\`json
{
"serverName": "firecrawl",
"toolName": "firecrawl_search",
"arguments": {
"query": "latest AI news",
"limit": 5
}
}
\`\`\`
### Example: Using Filesystem Tool
**Execute a filesystem read operation:**
\`\`\`json
{
"serverName": "filesystem",
"toolName": "read_file",
"arguments": {
"path": "/path/to/file.txt"
}
}
\`\`\`
### Tips for Executing MCP Tools
- Always check the \`inputSchema\` from \`listMcpTools\` to know what arguments are required
- Match argument types exactly (string, number, boolean, object, array)
- Provide helpful context to the user about what the tool is doing
- Handle errors gracefully and suggest alternatives if a tool fails
- For complex tasks, consider creating an agent instead of one-off tool calls
### Discovery Pattern (Recommended)
When a user asks for something that might be accomplished with an MCP tool:
1. **Identify the need**: "You want to search the web? Let me check what MCP tools are available..."
2. **List servers**: Call \`listMcpServers\`
3. **Check for relevant tools**: If you find a relevant server (e.g., "firecrawl" for web search), call \`listMcpTools\`
4. **Execute the tool**: Once you find the right tool and understand its schema, call \`executeMcpTool\`
5. **Present results**: Format and explain the results to the user
### Common MCP Servers and Their Tools
Based on typical configurations, you might find:
- **firecrawl**: Web scraping, search, crawling (\`firecrawl_search\`, \`firecrawl_scrape\`, \`firecrawl_crawl\`)
- **filesystem**: File operations (\`read_file\`, \`write_file\`, \`list_directory\`)
- **github**: GitHub operations (\`create_issue\`, \`create_pr\`, \`search_repositories\`)
- **fetch**: HTTP requests (\`fetch\`, \`post\`)
- **time**: Time/date operations (\`get_current_time\`, \`convert_timezone\`)
Always use \`listMcpServers\` and \`listMcpTools\` to discover what's actually available rather than assuming.
## Adding MCP Tools to Agents
Once an MCP server is configured, add its tools to agent definitions (Markdown files with YAML frontmatter):
### MCP Tool Format in Agent (YAML frontmatter)
\`\`\`yaml
tools:
descriptive_key:
type: mcp
name: actual_tool_name_from_server
description: What the tool does
mcpServerName: server_name_from_config
inputSchema:
type: object
properties:
param1:
type: string
description: What param1 means
required:
- param1
\`\`\`
### Tool Schema Rules
- Use \`listMcpTools\` to get the exact \`inputSchema\` from the server
- Copy the schema exactly as provided by the MCP server
- Only include \`required\` array if parameters are truly mandatory
- Add descriptions to help the agent understand parameter usage
### Example snippets to reference
- Firecrawl search (required param):
\`\`\`yaml
tools:
search:
type: mcp
name: firecrawl_search
description: Search the web
mcpServerName: firecrawl
inputSchema:
type: object
properties:
query:
type: string
description: Search query
limit:
type: number
description: Number of results
required:
- query
\`\`\`
- ElevenLabs text-to-speech (no required array):
\`\`\`yaml
tools:
text_to_speech:
type: mcp
name: text_to_speech
description: Generate audio from text
mcpServerName: elevenLabs
inputSchema:
type: object
properties:
text:
type: string
\`\`\`
## Safety reminders
- ALWAYS use \`addMcpServer\` to configure MCP servers—never manually edit config files
- Only recommend MCP tools that are actually configured (use \`listMcpServers\` first)
- Clarify any missing details (required parameters, server names) before modifying files
- Test server connection with \`listMcpTools\` after adding a new server
- Invalid MCP configs prevent agents from startingvalidation is critical
`;
export default skill;

View file

@ -1,165 +0,0 @@
export const skill = String.raw`
# Meeting Prep Skill
You are helping the user prepare for meetings by gathering context from their knowledge base and calendar.
## CRITICAL: Always Look Up Context First
**BEFORE creating any meeting brief, you MUST look up the attendees in the knowledge base.**
**PATH REQUIREMENT:** Always use \`knowledge/\` as the path (not empty, not the workspace root, not an absolute path).
- **WRONG:** \`path: ""\` or \`path: "."\`
- **CORRECT:** \`path: "knowledge/"\`
When the user asks to prep for a meeting or mentions attendees:
1. **STOP** - Do not create a generic brief
2. **SEARCH** - Look up each attendee in the knowledge base:
\`\`\`
workspace-grep({ pattern: "Attendee Name", path: "knowledge/" })
\`\`\`
3. **READ** - Read their notes to understand who they are:
\`\`\`
workspace-readFile("knowledge/People/Attendee Name.md")
workspace-readFile("knowledge/Organizations/Their Company.md")
\`\`\`
4. **UNDERSTAND** - Extract their role, organization, relationship history, past interactions, open items
5. **THEN BRIEF** - Only now create the meeting brief, using this context
**DO NOT** skip this step. **DO NOT** provide generic briefs. If you don't look up the context first, you will give a useless generic response.
## Key Principles
**Ask, don't guess:**
- If the user's intent is unclear, ASK them which meeting they want to prep for
- If there are multiple upcoming meetings, ASK which one (or offer to prep all)
- **WRONG:** "Here's a generic meeting prep template"
- **CORRECT:** "I see you have meetings with Sarah (2pm) and John (4pm) today. Which one would you like me to prep?"
**Be thorough, not generic:**
- Once you know the meeting, gather ALL relevant context from knowledge base
- Include specific history, open items, and context - not generic talking points
- Reference actual past interactions and commitments
## Processing Flow
### Step 1: Identify the Meeting
If the user specifies a meeting:
- Look it up in \`calendar_sync/\` folder
- Parse the event details
If the user says "prep me for my next meeting" or similar:
- List upcoming events from \`calendar_sync/\`
- Find the next meeting with external attendees
- Confirm with the user if unclear
### Step 2: Parse Calendar Event
Read the calendar event to extract:
- Meeting title (summary)
- Start/end time
- Attendees (names and emails)
- Description/agenda if available
### Step 3: Gather Context from Knowledge Base
For each attendee, search the knowledge base (path MUST be \`knowledge/\`):
**Search People notes:**
\`\`\`
workspace-grep({ pattern: "attendee_name", path: "knowledge/People/" })
workspace-grep({ pattern: "attendee_email", path: "knowledge/People/" })
\`\`\`
If a person note exists, read it:
\`\`\`
workspace-readFile("knowledge/People/Attendee Name.md")
\`\`\`
Extract:
- Their role/title
- Company/organization
- Key facts about them
- Previous interactions
- Open items
**Search Organization notes:**
\`\`\`
workspace-grep({ pattern: "company_name", path: "knowledge/Organizations/" })
\`\`\`
**Search Projects:**
\`\`\`
workspace-grep({ pattern: "attendee_name", path: "knowledge/Projects/" })
workspace-grep({ pattern: "company_name", path: "knowledge/Projects/" })
\`\`\`
### Step 4: Create Meeting Brief
Create a brief with this format:
\`\`\`markdown
📋
Meeting Brief: {Attendee Name}
{Time} today · {Company}
About {First Name}
{Role at company}. {Key background - 1-2 sentences}. {What they care about or focus on}.
Your History
- {Date}: {Brief description of interaction/outcome}
- {Date}: {Brief description}
- {Date}: {Brief description}
Open Items
- {Action item} (they asked {date})
- {Action item}
Suggested Talking Points
- {Concrete suggestion based on history}
- {Reference relevant entities with [[wiki-links]]}
\`\`\`
**Example:**
\`\`\`markdown
📋
Meeting Brief: Sarah Chen
2:00 PM today · Horizon Ventures
About Sarah
Partner at Horizon Ventures. Led investments in WorkOS and Segment. Very focused on unit economics.
Your History
- Jan 15: Partner meeting positive reception
- Jan 12: Sent updated deck with cohort analysis
- Jan 8: First pitch she loved the 125% NRR
Open Items
- Send updated financial model (she asked Jan 15)
- Discuss term sheet timeline
Suggested Talking Points
- Address her question about CAC by channel
- Mention [[TechFlow]] expansion closed ($120K ARR)
\`\`\`
**Briefing Guidelines:**
- Use \`[[Name]]\` wiki-link syntax for cross-references to people, projects, orgs
- Keep "About" section concise - 2-3 sentences max
- History should be reverse chronological (most recent first)
- Limit to 3-5 most relevant history items
- Open items should be actionable and specific
- Talking points should be concrete, not generic
- If no notes exist for a person, mention that and offer to create one
## Important Notes
- Only prep for meetings with external attendees
- Skip internal calendar blocks (DND, Focus Time, Lunch, etc.)
- For meetings with multiple attendees, create sections for each key person
- Prioritize recent interactions (last 30 days) in the history section
- If an attendee has no notes, suggest what you'd want to capture about them
`;
export default skill;

View file

@ -1,70 +0,0 @@
export const skill = String.raw`
# Notify User
Load this skill when you need to send a desktop notification to the user e.g. after a long-running task completes, when a track detects something noteworthy, or when an agent wants to ping the user with a clickable result.
## When to use
- **Use it for**: completion alerts, threshold breaches, status changes, new items the user asked you to watch for, anything time-sensitive.
- **Don't use it for**: routine progress updates, anything the user can already see in the chat, or repeated pings inside a loop (there is no built-in rate limit restraint is on you).
## The tool: \`notify-user\`
Triggers a native macOS notification. The call returns immediately; it does not block waiting for the user to click.
### Parameters
- **\`title\`** (optional, defaults to \`"Rowboat"\`) — bold headline at the top.
- **\`message\`** (required) — body text. Keep it short — macOS truncates after a couple of lines.
- **\`link\`** (optional) — URL to open when the user clicks the notification. Two kinds accepted:
- **\`https://...\` / \`http://...\`** — opens in the default browser
- **\`rowboat://...\`** — opens a view inside Rowboat (see deep links below)
- If omitted, clicking the notification focuses the Rowboat app.
### Examples
Plain alert (no link clicking focuses the app):
\`\`\`json
{
"title": "Backup complete",
"message": "All 142 files synced to iCloud."
}
\`\`\`
External link:
\`\`\`json
{
"title": "New email from Monica",
"message": "Re: Q4 planning — needs your input by Friday",
"link": "https://mail.google.com/mail/u/0/#inbox/abc123"
}
\`\`\`
Deep link into a Rowboat note:
\`\`\`json
{
"message": "Daily brief is ready",
"link": "rowboat://open?type=file&path=knowledge/Daily/2026-04-25.md"
}
\`\`\`
## Deep links: \`rowboat://\`
Use these as the \`link\` parameter to land the user on a specific view in Rowboat instead of an external site. URL-encode paths/names that contain spaces or special characters.
| Target | Format | Example |
|---|---|---|
| Open a file | \`rowboat://open?type=file&path=<workspace-relative path>\` | \`rowboat://open?type=file&path=knowledge/People/Acme.md\` |
| Open chat | \`rowboat://open?type=chat\` (optional \`&runId=<id>\`) | \`rowboat://open?type=chat&runId=abc123\` |
| Knowledge graph | \`rowboat://open?type=graph\` | — |
| Background task view | \`rowboat://open?type=task&name=<task-name>\` | \`rowboat://open?type=task&name=daily-brief\` |
| Suggested topics | \`rowboat://open?type=suggested-topics\` | — |
The \`type=file\` path is workspace-relative (the same path you'd pass to \`workspace-readFile\`).
## Anti-patterns
- **Don't notify per step** of a multi-step task. Notify on completion, not on progress.
- **Don't repeat what's already on screen.** If the result is already in the chat or in a note the user is viewing, skip the notification.
- **Don't dump the result into \`message\`.** Surface the headline; put the detail behind a deep link or external link.
- **Don't notify silently-failing things either.** If something failed, say so in the message — don't swallow the failure into a generic "done".
`;
export default skill;

View file

@ -1,180 +0,0 @@
export const skill = String.raw`
# Organize Files Skill
You are helping the user organize, tidy up, and find files on their local machine.
## Core Capabilities
1. **Find files** - Locate files by name, type, or content
2. **Organize files** - Move files into logical folders
3. **Tidy up** - Clean up cluttered directories (Desktop, Downloads, etc.)
4. **Create structure** - Set up folder hierarchies for projects
## Key Principles
**Always preview before acting:**
- Show the user what files will be affected BEFORE moving/deleting
- List the proposed changes and ask for confirmation
- **WRONG:** Immediately run \`mv\` commands without showing what will move
- **CORRECT:** "I found 23 screenshots on your Desktop. Here's the plan: [list]. Should I proceed?"
**Be conservative with destructive operations:**
- Never delete files without explicit confirmation
- Prefer moving to a "to-review" folder over deleting
- When in doubt, ask
**Handle paths safely:**
- Always quote paths to handle spaces: \`"$HOME/My Documents"\`
- Expand ~ to $HOME in commands
- Use absolute paths when possible
## Finding Files
**By name pattern:**
\`\`\`bash
# Find all PDFs in Downloads
find ~/Downloads -name "*.pdf" -type f
# Find files containing "AI" in the name
find ~/Downloads -iname "*AI*" -type f
# Find screenshots (common naming patterns)
find ~/Desktop -name "Screenshot*" -o -name "Screen Shot*"
\`\`\`
**By type:**
\`\`\`bash
# Images
find ~/Desktop -type f \( -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" -o -name "*.gif" -o -name "*.webp" \)
# Documents
find ~/Desktop -type f \( -name "*.pdf" -o -name "*.doc" -o -name "*.docx" -o -name "*.txt" \)
# Videos
find ~/Desktop -type f \( -name "*.mp4" -o -name "*.mov" -o -name "*.avi" -o -name "*.mkv" \)
\`\`\`
**By date:**
\`\`\`bash
# Files modified in last 7 days
find ~/Downloads -type f -mtime -7
# Files older than 30 days
find ~/Downloads -type f -mtime +30
\`\`\`
**By content (for text/PDF):**
\`\`\`bash
# Search inside files for text
grep -r "search term" ~/Documents --include="*.txt" --include="*.md"
# For PDFs, use pdfgrep if available, or list and let user check
find ~/Downloads -name "*.pdf" -exec basename {} \;
\`\`\`
**Extracting content from documents:**
When users want to read or summarize a document's contents (PDF, Excel, CSV, Word .docx), use the \`parseFile\` builtin tool. It extracts text from binary formats so you can answer questions about them.
- Accepts absolute paths (e.g., \`~/Downloads/report.pdf\`) or workspace-relative paths — no need to copy files first.
- Supported formats: \`.pdf\`, \`.xlsx\`, \`.xls\`, \`.csv\`, \`.docx\`
For scanned PDFs, images with text, complex layouts, or presentations where local parsing falls short, use the \`LLMParse\` builtin tool instead. It sends the file to the configured LLM as a multimodal attachment and returns well-structured markdown.
- Supports everything \`parseFile\` does plus images (\`.png\`, \`.jpg\`, \`.gif\`, \`.webp\`, \`.svg\`, \`.bmp\`, \`.tiff\`), PowerPoint (\`.pptx\`), HTML, and plain text.
- Also accepts an optional \`prompt\` parameter for custom extraction instructions.
## Organizing Files
**Create destination folder:**
\`\`\`bash
mkdir -p ~/Desktop/Screenshots
mkdir -p ~/Downloads/PDFs
mkdir -p ~/Documents/Projects/ProjectName
\`\`\`
**Move files:**
\`\`\`bash
# Move specific file
mv ~/Desktop/Screenshot\ 2024-01-15.png ~/Desktop/Screenshots/
# Move all matching files (after confirmation!)
find ~/Desktop -name "Screenshot*" -exec mv {} ~/Desktop/Screenshots/ \;
# Safer: move with verbose output
mv -v ~/Desktop/Screenshot*.png ~/Desktop/Screenshots/
\`\`\`
**Batch organization pattern:**
\`\`\`bash
# Create folders by file type
mkdir -p ~/Desktop/{Screenshots,Documents,Images,Videos,Other}
# Move by type (show user the plan first!)
find ~/Desktop -maxdepth 1 -name "*.png" -exec mv -v {} ~/Desktop/Images/ \;
find ~/Desktop -maxdepth 1 -name "*.pdf" -exec mv -v {} ~/Desktop/Documents/ \;
\`\`\`
## Common Organization Tasks
### Screenshots on Desktop
1. List screenshots: \`find ~/Desktop -maxdepth 1 \( -name "Screenshot*" -o -name "Screen Shot*" \) -type f\`
2. Count them: add \`| wc -l\`
3. Create folder: \`mkdir -p ~/Desktop/Screenshots\`
4. Show plan and get confirmation
5. Move: \`find ~/Desktop -maxdepth 1 \( -name "Screenshot*" -o -name "Screen Shot*" \) -exec mv -v {} ~/Desktop/Screenshots/ \;\`
### Clean up Downloads
1. Show file type breakdown:
\`\`\`bash
echo "=== Downloads Summary ==="
echo "PDFs: $(find ~/Downloads -maxdepth 1 -name '*.pdf' | wc -l)"
echo "Images: $(find ~/Downloads -maxdepth 1 \( -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' \) | wc -l)"
echo "DMGs: $(find ~/Downloads -maxdepth 1 -name '*.dmg' | wc -l)"
echo "ZIPs: $(find ~/Downloads -maxdepth 1 -name '*.zip' | wc -l)"
\`\`\`
2. Propose organization structure
3. Get confirmation
4. Execute moves
### Find a specific file
1. Ask clarifying questions if needed (file type, approximate name, when downloaded)
2. Search with appropriate find command
3. Show matches with full paths
4. Offer to open the containing folder: \`open ~/Downloads\` (macOS)
## Output Format
When presenting a plan:
\`\`\`
📁 Organization Plan: Desktop Cleanup
Found 47 files to organize:
- 23 screenshots ~/Desktop/Screenshots/
- 12 PDFs ~/Desktop/Documents/
- 8 images ~/Desktop/Images/
- 4 other files (leaving in place)
Should I proceed with this organization?
\`\`\`
When reporting results:
\`\`\`
Organization Complete
Moved 43 files:
- 23 screenshots to Screenshots/
- 12 PDFs to Documents/
- 8 images to Images/
4 files left in place (mixed types - review manually)
\`\`\`
## Safety Rules
1. **Never delete without explicit permission** - even "cleanup" means organize, not delete
2. **Don't touch system folders** - /System, /Library, /Applications, etc.
3. **Don't touch hidden files** - files starting with . unless explicitly asked
4. **Limit depth** - use \`-maxdepth 1\` unless user wants recursive organization
5. **Show before doing** - always preview the operation first
6. **Preserve originals when uncertain** - copy instead of move if unsure
`;
export default skill;

View file

@ -1,124 +0,0 @@
const skill = String.raw`
# Slack Integration Skill (agent-slack CLI)
You interact with Slack by running **agent-slack** commands through \`executeCommand\`.
---
## 1. Check Connection
Before any Slack operation, read \`config/slack.json\` from the workspace root. If \`enabled\` is \`false\` or the \`workspaces\` array is empty, simply tell the user: "Slack is not enabled. You can enable it in the Connectors settings." Do not attempt any agent-slack commands.
If enabled, use the workspace URLs from the config for all commands.
---
## 2. Core Commands
### Messages
| Action | Command |
|--------|---------|
| List recent messages | \`agent-slack message list "#channel-name" --limit 25\` |
| List thread replies | \`agent-slack message list "#channel" --thread-ts 1234567890.123456\` |
| Get a single message | \`agent-slack message get "https://team.slack.com/archives/C.../p..."\` |
| Send a message | \`agent-slack message send "#channel-name" "Hello team!"\` |
| Reply in thread | \`agent-slack message send "#channel-name" "Reply text" --thread-ts 1234567890.123456\` |
| Edit a message | \`agent-slack message edit "#channel-name" --ts 1234567890.123456 "Updated text"\` |
| Delete a message | \`agent-slack message delete "#channel-name" --ts 1234567890.123456\` |
**Targets** can be:
- A full Slack URL: \`https://team.slack.com/archives/C01234567/p1234567890123456\`
- A channel name: \`"#general"\` or \`"general"\`
- A channel ID: \`C01234567\`
### Reactions
\`\`\`
agent-slack message react add "<target>" <emoji> --ts <ts>
agent-slack message react remove "<target>" <emoji> --ts <ts>
\`\`\`
### Search
\`\`\`
agent-slack search messages "query text" --limit 20
agent-slack search messages "query" --channel "#channel-name" --user "@username"
agent-slack search messages "query" --after 2025-01-01 --before 2025-02-01
agent-slack search files "query" --limit 10
\`\`\`
### Channels
\`\`\`
agent-slack channel new --name "project-x" --workspace https://team.slack.com
agent-slack channel new --name "secret-project" --private
agent-slack channel invite --channel "#project-x" --users "@alice,@bob"
\`\`\`
### Users
\`\`\`
agent-slack user list --limit 200
agent-slack user get "@username"
agent-slack user get U01234567
\`\`\`
### Canvases
\`\`\`
agent-slack canvas get "https://team.slack.com/docs/F01234567"
agent-slack canvas get F01234567 --workspace https://team.slack.com
\`\`\`
---
## 3. Multi-Workspace
**Important:** The user has chosen which workspaces to use. Before your first Slack operation, read \`config/slack.json\` from the workspace root to see the selected workspaces. Only interact with workspaces listed in that config — ignore any other authenticated workspaces.
If the selected workspace list contains multiple entries, use \`--workspace <url>\` to disambiguate:
\`\`\`
agent-slack message list "#general" --workspace https://team.slack.com
\`\`\`
If only one workspace is selected, always use \`--workspace\` with its URL to avoid ambiguity with other authenticated workspaces.
---
## 4. Token Budget Control
Use \`--limit\` to control how many messages/results are returned. Use \`--max-body-chars\` or \`--max-content-chars\` to truncate long message bodies:
\`\`\`
agent-slack message list "#channel" --limit 10
agent-slack search messages "query" --limit 5 --max-content-chars 2000
\`\`\`
---
## 5. Discovering More Commands
For any command you're unsure about:
\`\`\`
agent-slack --help
agent-slack message --help
agent-slack search --help
agent-slack channel --help
\`\`\`
---
## Best Practices
- **Always show drafts before sending** Never send Slack messages without user confirmation
- **Summarize, don't dump** When showing channel history, summarize the key points rather than pasting everything
- **Prefer Slack URLs** When referring to messages, use Slack URLs over raw channel names when available
- **Use --limit** Always set reasonable limits to keep output concise and token-efficient
- **Resolve user IDs** Messages contain raw user IDs like \`U078AHJP341\`. Resolve them to real names before presenting to the user. Batch all lookups into a single \`executeCommand\` call using \`;\` separators, e.g. \`agent-slack user get U078AHJP341 --workspace ... ; agent-slack user get U090UEZCEQ0 --workspace ...\`
- **Cross-reference with knowledge base** Check if mentioned people have notes in the knowledge base
`;
export default skill;

View file

@ -1,52 +0,0 @@
export const skill = String.raw`
# Web Search Skill
You have access to two search tools for finding information on the internet. Choose the right one based on the user's intent.
## Tools
### web-search (Brave Search)
Quick, general-purpose web search. Returns titles, URLs, and short descriptions.
**Best for:**
- Quick lookups for things that change ("current price of Bitcoin", "weather in SF")
- Current events and breaking news
- Finding a specific website or page
- Simple questions with direct answers
- Checking a fact or date
### research-search (Exa Search)
Deep, research-oriented search. Returns full article text, highlights, and metadata (author, published date).
**Best for:**
- Exploring a topic in depth ("what are the latest advances in CRISPR")
- Finding articles, blog posts, papers, and quality sources
- Discovering companies, people, or organizations
- Research where you need rich context, not just links
- When the user says "research", "find articles about", "look into", "deep dive"
**Category filter:** Use the category parameter when the user's intent clearly maps to one: company, research paper, news, tweet, personal site, financial report, people.
## How Many Searches to Do
**CRITICAL: Always start with exactly ONE search call.** Pick the single best tool (\`web-search\` or \`research-search\`) and make one request. Wait for the result before deciding if more searches are needed.
**NEVER call multiple search tools simultaneously.** No parallel web-search + research-search. No firing off two web-searches at once. Always sequential: one search at a time.
Only make a follow-up search if:
- The first search returned truly uninformative or irrelevant results
- The query has clearly distinct sub-topics that the first search couldn't cover (e.g., "compare X and Y" after getting results for X only)
- The user explicitly asks you to dig deeper
One good search is almost always enough. Default to one and stop.
## Choosing Between the Two
If both tools are attached, prefer:
- \`web-search\` when the user wants a quick answer or specific link
- \`research-search\` when the user wants to learn, explore, or gather sources
If only one is attached, use whichever is available.
`;
export default skill;

View file

@ -6,7 +6,7 @@ import { createInterface } from "readline";
import { execSync } from "child_process";
import { glob } from "glob";
import { executeCommand, executeCommandAbortable } from "./command-executor.js";
import { resolveSkill, availableSkills } from "../assistant/skills/index.js";
import { ISkillResolver } from "../../skills/resolver.js";
import { executeTool, listServers, listTools } from "../../mcp/mcp.js";
import container from "../../di/container.js";
import { IMcpConfigRepo } from "../..//mcp/repo.js";
@ -135,27 +135,39 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
loadSkill: {
description: "Load a Rowboat skill definition into context by fetching its guidance string",
inputSchema: z.object({
skillName: z.string().describe("Skill identifier or path (e.g., 'workflow-run-ops' or 'src/application/assistant/skills/workflow-run-ops/skill.ts')"),
skillName: z.string().describe("Skill identifier (e.g., 'doc-collab', 'web-search')"),
}),
execute: async ({ skillName }: { skillName: string }) => {
const resolved = resolveSkill(skillName);
const resolver = container.resolve<ISkillResolver>("skillResolver");
const resolved = await resolver.resolve(skillName);
if (!resolved) {
const catalog = await resolver.getCatalog();
const available = catalog.map((s) => s.id).join(", ");
return {
success: false,
message: `Skill '${skillName}' not found. Available skills: ${availableSkills.join(", ")}`,
message: `Skill '${skillName}' not found. Available skills: ${available}`,
};
}
return {
success: true,
skillName: resolved.id,
path: resolved.catalogPath,
content: resolved.content,
};
},
},
listSkills: {
description: "List all available skills with their id, title, and one-line summary",
inputSchema: z.object({}),
execute: async () => {
const resolver = container.resolve<ISkillResolver>("skillResolver");
const catalog = await resolver.getCatalog();
return { success: true, skills: catalog };
},
},
'workspace-getRoot': {
description: 'Get the workspace root directory path',
inputSchema: z.object({}),

View file

@ -1,141 +0,0 @@
/**
* The canonical writing style for content written into the user's knowledge
* base. Imported by both the `doc-collab` skill (so Copilot picks it up on
* note edits) and the live-note run-agent prompt (so background runs use the
* same rules without having to load the skill on every fire). One source of
* truth, two consumers.
*
* If you change this guide, restart the dev server / rebuild both consumers
* inline it at module load.
*/
export const KNOWLEDGE_NOTE_STYLE_GUIDE = `# Knowledge-note writing style — terse and scannable
The user's knowledge base is a place they **scan**, not read. Every note competes for attention against many others. Optimize aggressively for **information density and signal-per-line**. These rules apply whether you're authoring a new note, refreshing a live note, or making a one-off edit they are not optional.
## The frame
- The reader wants the answer to "what's current / what changed?" in the fewest words that carry real information.
- A reader scanning ten notes in a row will give each one ~2 seconds. Format for that.
- Prose is the wrong shape for almost everything. Reach for it only when the content genuinely is a paragraph (user-written analysis, meeting reflection, qualitative narrative). Informational content facts, lists, status, news, prices, weather uses tighter shapes.
## Tightest shape that fits pick from this ladder
**1. Single line** when the answer is one fact.
- Weather: \`24°, Cloudy · NE 8mph · 12% PoP\`
- Price: \`BTC: $67,432 (+1.2% 24h)\`
- Time: \`2:30 PM IST\`
- Status: \`✓ All systems operational\` or \`⚠ db: degraded\`
**2. Compact table** for 2+ parallel items with the same shape.
\`\`\`
| Symbol | Price | Δ24h |
|--------|------:|------:|
| BTC | $67k | +1.2% |
| ETH | $3.2k | 0.8% |
\`\`\`
**3. Short bullets** for digests and lists. One line per item, 80 chars when possible. Lead with the value, push metadata to the end.
- News: \`- <headline> · <source> · <time>\`
- Tasks: \`- [ ] <task> · <due>\`
- HN: \`- <title> · 842 pts · 312 comments\`
**4. Status line + per-component bullets** when there's a top-level state plus details worth surfacing.
\`\`\`
db degraded
- api: 240ms p95 (vs 80ms baseline)
- db: connection pool saturated
\`\`\`
**5. Rich block** (\`table\`, \`chart\`, \`calendar\`, \`email\`, \`mermaid\`, etc.) when the data has a natural visual form. Don't render a calendar or chart in plain markdown when the rich block exists.
## Hard "no" list
- **No prose paragraphs** for informational content. Even if the topic is something a magazine would write 200 words about, the note version is bullets or a table.
- **No decorative adjectives**: "comprehensive", "balanced", "polished", "detailed", "high-quality", "carefully curated". They tell the reader nothing concrete.
- **No framing prose**: skip "Here's the latest update on…", "Below is a summary of…", "I've gathered the following…", "Quick rundown:". Get to the data on the first line.
- **No self-reference**: don't write "I updated this section at X" — the system records timestamps. Don't write "This note refreshes hourly" the user already knows.
- **No caveats unless the data is genuinely uncertain**: "Note: this is approximate", "As of last refresh", "Subject to change" are noise. If freshness matters, encode it inline: \`BTC: $67,432 (as of 14:05 IST)\`.
- **No preamble** no "Sure, here's…", "Got it, will do — here's the result." Just the result.
- **No filler headers** a note whose content is a single fact doesn't need a \`## Summary\` heading. Headings exist to break up content, not announce it.
## Bullet rules
- One line per bullet. No nesting beyond 2 levels if you reach for a third level, it should be a new section or a table.
- **Lead with the value.** "BTC at $67k" not "The current BTC price is approximately $67k".
- Use \`·\` (middle dot) as a separator for related fields when stacking 2+ items inline. \`<headline> · <source> · <time>\` reads better than \`(<source>, <time>)\`.
- Push metadata (time, source, status, score) to the **end** of the bullet, after a separator.
## Table rules
- Use a markdown table (or a \`table\` rich block) for ≥3 parallel items. For 1-2 items, use a single line or two bullets — a 2-row table is overhead with no benefit.
- Aim for 4 columns. More and the reader can't scan it.
- Right-align numeric columns when possible.
- No "Notes" column full of prose; if a row needs annotation, footnote it below the table.
## Sources and links make destinations clickable
Knowledge notes are entry points, not dead ends. **If the user might want to click through and read more, give them the link.** This applies to anything you pulled from outside the user's own data news, papers, blog posts, GitHub issues, status pages, search results, social posts, dashboards.
**Required when you have a URL:**
- Source attribution is non-negotiable for any item pulled from the web. Name the source (CNBC, Reuters, "GitHub", "company blog", "@<author> on X", etc.) **and** give a link to the canonical URL.
- Research / reference bullets that summarize external content.
- HN / front-page lists, paper digests, ranked items.
**Format:** make the **headline** the link that's what the user reaches for first.
- Preferred: \`- [<headline>](<url>) · <source> · <when>\`
- Acceptable: \`- <headline> · [<source>](<url>) · <when>\` when the headline isn't itself an article (e.g. a one-line insight you derived from the source).
If the bullet also carries a short description, the link still goes on the headline:
\`- [<headline>](<url>) · <source> · <when> · <one-line description>\`
**Not required:**
- Items pulled from the user's own data (calendar events, sent emails, meeting notes the user authored) the natural reference (event id, sender name, meeting filename) is enough.
- Pure point-in-time facts the user wouldn't drill into ("BTC: $67,432", "24°, Cloudy", "✓ All systems operational"). No link.
**Internal references:** use \`[[Note Name]]\` to link other knowledge-base notes. The editor renders these as clickable wiki-links — preferable to a flat path string.
**When you don't have a URL but it would be useful:** drop the link, keep the source name. Don't fabricate URLs. Don't write \`(link unavailable)\` — that's noise. If the source is a known publication, the source name alone is still informative.
## Genres cookbook
Common note types and the target shape for each:
- **Weather**: single line \`T°, Conditions · Wind · Precip\`. A 3-day micro-forecast as 3 lines if the user asks for it.
- **News digest**: bulleted list. Source attribution + link **required** when you have a URL see "Sources and links" above. Shape: \`- [<headline>](<url>) · <source> · <date>\` (optionally append \` · <one-line takeaway>\` when the headline alone isn't enough). Group by topic only when >10 items.
- **Stock / crypto prices**: table with \`Symbol | Price | Δ24h | Δ7d\`. Add a \`chart\` block for time series only when the user asks for trends. No links — these are point-in-time facts.
- **Service status**: a single status line; per-component bullets *only* when something is degraded. Link the status page when surfacing the top-level status (\`[✓ All systems operational](<status_url>)\`).
- **Calendar / agenda**: \`calendar\` rich block. Never plain markdown.
- **Email digest**: \`emails\` rich block (multi-thread) or \`email\` block (single thread). Plain markdown only for one-line summaries when there are >20 threads.
- **HN / front-page lists**: bullets \`- [<title>](<url>) · <points> pts · <comments> comments\`. Title is always the link.
- **Tasks / priorities**: ranked bullets with priority tag \`- [P0] <task> · <due>\`. \`[[wiki-link]]\` to a source note when one exists (e.g. the task came from a meeting note).
- **Research notes / search results**: bullets with **link**, source, 1-line gist \`- [<title>](<url>) · <source> · <gist>\`. Link is required when you found this via search. Don't synthesize into prose.
- **GitHub / issue digests**: \`- [<title>](<issue_url>) · <repo> · <state> · <updated>\`.
- **Tweets / social digests**: \`- [<truncated text or topic>](<post_url>) · @<author> · <when>\`.
## When prose IS appropriate
- A **1-3 sentence opening summary** at the top of a complex note (a "lede") concise enough to scan.
- A section the user explicitly authored as narrative (a journal entry, meeting reflection, qualitative analysis).
- The **user's own writing** never restructure it into bullets unless they ask.
For everything else: bullets, tables, single lines.
## A worked example
**Bad** wall of prose, decorative adjectives, framing, caveats:
> Here's a comprehensive update on today's most important news from India and around the world. The geopolitical landscape continues to evolve rapidly, with several significant developments worth highlighting. In India, the markets had a notable session today, with the Sensex closing higher on positive sentiment around the upcoming budget. Meanwhile, in global news, there have been important shifts in technology and finance.
**Good** bullets, lead with value, metadata at the end, no framing, **headline is a link to the source article**:
> ## India
> - [Sensex closes +0.6% at 73,420](https://www.livemint.com/...) · Mint · 4 PM
> - [Budget speech draft sets fiscal-deficit target at 4.5%](https://www.reuters.com/...) · Reuters · 2 PM
> - [Cabinet clears semiconductor mission Phase 2](https://economictimes.indiatimes.com/...) · ET · 11 AM
>
> ## World
> - [OpenAI launches GPT-5 mini for free tier](https://techcrunch.com/...) · TechCrunch · 9 AM PT
> - [Fed minutes signal one more cut this year](https://www.bloomberg.com/...) · Bloomberg · 2 PM ET
> - [EU passes AI Act amendment on training data](https://www.politico.eu/...) · Politico · 3 PM CET
Same information, ~80% fewer words, scannable in 5 seconds.
`;

View file

@ -1,10 +1,12 @@
import z from 'zod';
import { Agent, ToolAttachment } from '@x/shared/dist/agent.js';
import { BuiltinTools } from '../application/lib/builtin-tools.js';
import { KNOWLEDGE_NOTE_STYLE_GUIDE } from '../application/lib/knowledge-note-style.js';
import container from '../di/container.js';
import type { ISkillResolver } from '../skills/resolver.js';
import { WorkDir } from '../config/config.js';
export const BACKGROUND_TASK_AGENT_INSTRUCTIONS = `You are the background-task agent — a self-running agent that fires on a schedule and/or in response to incoming events to act on persistent **instructions** the user wrote.
function buildInstructions(knowledgeNoteStyle: string): string {
return `You are the background-task agent — a self-running agent that fires on a schedule and/or in response to incoming events to act on persistent **instructions** the user wrote.
You are running with **no user present** to clarify, approve, or watch.
- Do NOT ask clarifying questions make the most reasonable interpretation of the instructions and proceed.
@ -49,7 +51,7 @@ The run message tells you which trigger fired and how to interpret it:
# Workspace conventions
${KNOWLEDGE_NOTE_STYLE_GUIDE}
${knowledgeNoteStyle}
# Failure and fallback
@ -67,18 +69,23 @@ Avoid: "I updated the file.", "Done!", "Here is the update:". The summary is a d
The workspace lives at \`${WorkDir}\`.
`;
}
export function buildBackgroundTaskAgent(): z.infer<typeof Agent> {
export async function buildBackgroundTaskAgent(): Promise<z.infer<typeof Agent>> {
const tools: Record<string, z.infer<typeof ToolAttachment>> = {};
for (const name of Object.keys(BuiltinTools)) {
if (name === 'executeCommand') continue;
tools[name] = { type: 'builtin', name };
}
const resolver = container.resolve<ISkillResolver>('skillResolver');
const styleSkill = await resolver.resolve('knowledge-note-style');
const styleGuide = styleSkill?.content ?? '';
return {
name: 'background-task-agent',
description: 'Background agent that runs on a schedule/event and either keeps a task\'s index.md current (OUTPUT mode) or performs a recurring side-effect and journals it (ACTION mode).',
instructions: BACKGROUND_TASK_AGENT_INSTRUCTIONS,
instructions: buildInstructions(styleGuide),
tools,
};
}

View file

@ -15,6 +15,8 @@ import { IAbortRegistry, InMemoryAbortRegistry } from "../runs/abort-registry.js
import { FSAgentScheduleRepo, IAgentScheduleRepo } from "../agent-schedule/repo.js";
import { FSAgentScheduleStateRepo, IAgentScheduleStateRepo } from "../agent-schedule/state-repo.js";
import { FSSlackConfigRepo, ISlackConfigRepo } from "../slack/repo.js";
import { FSOfficialSkillsRepo, IOfficialSkillsRepo } from "../skills/official-repo.js";
import { SkillResolver, ISkillResolver } from "../skills/resolver.js";
import type { IBrowserControlService } from "../application/browser-control/service.js";
import type { INotificationService } from "../application/notification/service.js";
@ -41,6 +43,8 @@ container.register({
agentScheduleRepo: asClass<IAgentScheduleRepo>(FSAgentScheduleRepo).singleton(),
agentScheduleStateRepo: asClass<IAgentScheduleStateRepo>(FSAgentScheduleStateRepo).singleton(),
slackConfigRepo: asClass<ISlackConfigRepo>(FSSlackConfigRepo).singleton(),
officialSkillsRepo: asClass<IOfficialSkillsRepo>(FSOfficialSkillsRepo).singleton(),
skillResolver: asClass<ISkillResolver>(SkillResolver).singleton(),
});
export default container;
@ -56,3 +60,9 @@ export function registerNotificationService(service: INotificationService): void
notificationService: asValue(service),
});
}
export function registerSkillsDir(skillsDir: string): void {
container.register({
skillsDir: asValue(skillsDir),
});
}

View file

@ -1,10 +1,12 @@
import z from 'zod';
import { Agent, ToolAttachment } from '@x/shared/dist/agent.js';
import { BuiltinTools } from '../../application/lib/builtin-tools.js';
import { KNOWLEDGE_NOTE_STYLE_GUIDE } from '../../application/lib/knowledge-note-style.js';
import container from '../../di/container.js';
import type { ISkillResolver } from '../../skills/resolver.js';
import { WorkDir } from '../../config/config.js';
export const LIVE_NOTE_AGENT_INSTRUCTIONS = `You are the live-note agent — a background agent that keeps a *live note* in the user's personal knowledge base current with its objective.
function buildInstructions(knowledgeNoteStyle: string): string {
return `You are the live-note agent — a background agent that keeps a *live note* in the user's personal knowledge base current with its objective.
Your goal on each run: bring the body of the note in line with the user's persistent **objective** for that note. The user is maintaining a personal knowledge base and will scan this note alongside many others optimize for **information density and scannability**, not conversational prose.
@ -78,7 +80,7 @@ Unless the objective explicitly specifies a different structure, follow this def
If the objective says something specific about layout (e.g. "show the top 5 stories at the top, with a one-paragraph summary above them"), follow that exactly and ignore the defaults.
${KNOWLEDGE_NOTE_STYLE_GUIDE}
${knowledgeNoteStyle}
The style guide above is the canonical writing style for everything you emit into the body. The objective may specify a particular shape ("3-column markdown table: Location | Local Time | Offset") when it does, follow it exactly. When it doesn't, walk the ladder above and pick the tightest shape that fits the data.
@ -307,18 +309,23 @@ State the action and the substance. Good examples:
Avoid: "I updated the note.", "Done!", "Here is the update:". The summary is a data point, not a sign-off.
`;
}
export function buildLiveNoteAgent(): z.infer<typeof Agent> {
export async function buildLiveNoteAgent(): Promise<z.infer<typeof Agent>> {
const tools: Record<string, z.infer<typeof ToolAttachment>> = {};
for (const name of Object.keys(BuiltinTools)) {
if (name === 'executeCommand') continue;
tools[name] = { type: 'builtin', name };
}
const resolver = container.resolve<ISkillResolver>('skillResolver');
const styleSkill = await resolver.resolve('knowledge-note-style');
const styleGuide = styleSkill?.content ?? '';
return {
name: 'live-note-agent',
description: 'Background agent that keeps a live note up to date with its objective',
instructions: LIVE_NOTE_AGENT_INSTRUCTIONS,
instructions: buildInstructions(styleGuide),
tools,
};
}

View file

@ -0,0 +1,50 @@
import fs from "node:fs/promises";
import path from "node:path";
import { parseSkillMd } from "./skill-md-parser.js";
import type { SkillDefinition } from "./types.js";
export interface IOfficialSkillsRepo {
listOfficial(): Promise<SkillDefinition[]>;
getOfficial(id: string): Promise<SkillDefinition | null>;
}
export class FSOfficialSkillsRepo implements IOfficialSkillsRepo {
private readonly officialDir: string;
constructor({ skillsDir }: { skillsDir: string }) {
this.officialDir = skillsDir;
}
async listOfficial(): Promise<SkillDefinition[]> {
const result: SkillDefinition[] = [];
let entries: string[];
try {
entries = await fs.readdir(this.officialDir);
} catch {
return result;
}
for (const entry of entries) {
const skillMdPath = path.join(this.officialDir, entry, "SKILL.md");
try {
const raw = await fs.readFile(skillMdPath, "utf-8");
result.push(parseSkillMd(raw, entry));
} catch {
// Not a valid skill directory, skip
}
}
result.sort((a, b) => a.id.localeCompare(b.id));
return result;
}
async getOfficial(id: string): Promise<SkillDefinition | null> {
const skillMdPath = path.join(this.officialDir, id, "SKILL.md");
try {
const raw = await fs.readFile(skillMdPath, "utf-8");
return parseSkillMd(raw, id);
} catch {
return null;
}
}
}

View file

@ -0,0 +1,65 @@
import { ResolvedSkill, SkillCatalogEntry } from "@x/shared/dist/skill.js";
import { IOfficialSkillsRepo } from "./official-repo.js";
const INCLUDE_DIRECTIVE = /\{\{include:([a-z0-9][a-z0-9_-]*)\}\}/g;
export interface ISkillResolver {
getCatalog(): Promise<SkillCatalogEntry[]>;
resolve(id: string): Promise<ResolvedSkill | null>;
}
export class SkillResolver implements ISkillResolver {
private readonly officialSkillsRepo: IOfficialSkillsRepo;
constructor({ officialSkillsRepo }: { officialSkillsRepo: IOfficialSkillsRepo }) {
this.officialSkillsRepo = officialSkillsRepo;
}
async getCatalog(): Promise<SkillCatalogEntry[]> {
const all = await this.officialSkillsRepo.listOfficial();
return all
.filter((s) => !s.hidden)
.map(({ id, title, summary }) => ({ id, title, summary }));
}
async resolve(id: string): Promise<ResolvedSkill | null> {
return this.resolveInner(id, new Set());
}
private async resolveInner(id: string, seen: Set<string>): Promise<ResolvedSkill | null> {
if (seen.has(id)) {
// Cycle: emit a placeholder rather than infinite-looping.
return null;
}
const def = await this.officialSkillsRepo.getOfficial(id);
if (!def) return null;
const nextSeen = new Set(seen);
nextSeen.add(id);
const expanded = await this.expandIncludes(def.content, nextSeen);
return {
id: def.id,
title: def.title,
summary: def.summary,
content: expanded,
};
}
private async expandIncludes(content: string, seen: Set<string>): Promise<string> {
const matches = Array.from(content.matchAll(INCLUDE_DIRECTIVE));
if (matches.length === 0) return content;
const replacements = new Map<string, string>();
for (const match of matches) {
const directive = match[0];
const includeId = match[1];
if (replacements.has(directive)) continue;
const resolved = await this.resolveInner(includeId, seen);
replacements.set(
directive,
resolved?.content ?? `<!-- missing skill include: ${includeId} -->`,
);
}
return content.replace(INCLUDE_DIRECTIVE, (whole) => replacements.get(whole) ?? whole);
}
}

View file

@ -0,0 +1,33 @@
import { parse } from "yaml";
import { SkillFrontmatter } from "@x/shared/dist/skill.js";
import type { SkillDefinition } from "./types.js";
/**
* Parse a SKILL.md file (YAML frontmatter + markdown body) into a SkillDefinition.
* Follows the Agent Skills spec: frontmatter between --- markers.
*/
export function parseSkillMd(raw: string, fallbackId?: string): SkillDefinition {
const normalized = raw.replace(/\r\n/g, "\n");
if (!normalized.startsWith("---\n")) {
throw new Error("SKILL.md missing frontmatter (must start with ---)");
}
const end = normalized.indexOf("\n---\n", 4);
const lastEnd = normalized.endsWith("\n---") ? normalized.length - 4 : -1;
const closingIdx = end !== -1 ? end : lastEnd;
if (closingIdx === -1) {
throw new Error("SKILL.md has malformed frontmatter (missing closing ---)");
}
const fm = normalized.slice(4, closingIdx).trim();
const body = normalized.slice(closingIdx + 4).trim();
const parsed = SkillFrontmatter.parse(parse(fm));
return {
id: parsed.name ?? fallbackId ?? "unknown",
title: parsed.metadata?.title ?? parsed.name,
summary: parsed.description,
hidden: parsed.hidden ?? false,
content: body,
};
}

View file

@ -0,0 +1,7 @@
export type SkillDefinition = {
id: string;
title: string;
summary: string;
hidden: boolean;
content: string;
};

View file

@ -16,4 +16,5 @@ export * as promptBlock from './prompt-block.js';
export * as frontmatter from './frontmatter.js';
export * as bases from './bases.js';
export * as browserControl from './browser-control.js';
export * as skill from './skill.js';
export { PrefixLogger };

View file

@ -17,6 +17,7 @@ import { UserMessageContent } from './message.js';
import { RowboatApiConfig } from './rowboat-account.js';
import { ZListToolkitsResponse } from './composio.js';
import { BrowserStateSchema } from './browser-control.js';
import { ResolvedSkill, SkillCatalogEntry } from './skill.js';
// ============================================================================
// Runtime Validation Schemas (Single Source of Truth)
@ -871,6 +872,17 @@ const ipcSchemas = {
req: BrowserStateSchema,
res: z.null(),
},
// Skills channels (read-only)
'skills:list': {
req: z.null(),
res: z.object({
skills: z.array(SkillCatalogEntry),
}),
},
'skills:get': {
req: z.object({ id: z.string() }),
res: ResolvedSkill.nullable(),
},
// Billing channels
'billing:getInfo': {
req: z.null(),

View file

@ -0,0 +1,36 @@
import { z } from 'zod';
// SKILL.md frontmatter schema. `name` is the skill id (folder name) and
// `description` is the one-line catalog summary. `hidden: true` keeps a
// skill out of the public catalog while still allowing other skills to
// `{{include:<id>}}` it as content (e.g. shared style guides).
export const SkillFrontmatter = z.object({
name: z.string().max(64),
description: z.string().max(1024),
hidden: z.boolean().optional(),
license: z.string().optional(),
metadata: z.object({
title: z.string().optional(),
}).passthrough().optional(),
});
export type SkillFrontmatter = z.infer<typeof SkillFrontmatter>;
// Skill catalog entry seen by the agent and the renderer (no content body).
export const SkillCatalogEntry = z.object({
id: z.string(),
title: z.string(),
summary: z.string(),
});
export type SkillCatalogEntry = z.infer<typeof SkillCatalogEntry>;
// Fully-resolved skill: catalog metadata + body with all {{include:<id>}}
// directives expanded.
export const ResolvedSkill = z.object({
id: z.string(),
title: z.string(),
summary: z.string(),
content: z.string(),
});
export type ResolvedSkill = z.infer<typeof ResolvedSkill>;