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.
7.7 KiB
| name | description | metadata | ||
|---|---|---|---|---|
| draft-emails | Process incoming emails and create draft responses using calendar and knowledge base for context. |
|
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: ""orpath: "." - CORRECT:
path: "knowledge/"
When the user says "draft an email to Monica" or mentions ANY person, organization, project, or topic:
- STOP - Do not draft anything yet
- SEARCH - Look them up in the knowledge base (path MUST be
knowledge/):workspace-grep({ pattern: "Monica", path: "knowledge/" }) - READ - Read their note to understand who they are:
workspace-readFile("knowledge/People/Monica Smith.md") - UNDERSTAND - Extract their role, organization, relationship history, past interactions, open items
- 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:{ "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:
- Check if
pre-built/email-draft/state.jsonexists - If not, create
pre-built/email-draft/andpre-built/email-draft/drafts/ - Initialize
state.jsonwith 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 thisdrafted- 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:
- Extract the email ID from filename (e.g.,
19048cf9c0317981.md->19048cf9c0317981) - Skip if ID is in
draftedorignoredlists - Read the email content
Step 3: Parse Email
Each email file contains:
# 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:
# 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:
- Add the email ID to either
draftedorignoredlist - Update
lastProcessedTimestampto the current time - 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