Merge branch 'main' into main

This commit is contained in:
Affaan Mustafa
2026-03-13 00:00:34 -07:00
70 changed files with 7128 additions and 239 deletions

View File

@@ -0,0 +1,337 @@
---
name: claude-api
description: Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.
origin: ECC
---
# Claude API
Build applications with the Anthropic Claude API and SDKs.
## When to Activate
- Building applications that call the Claude API
- Code imports `anthropic` (Python) or `@anthropic-ai/sdk` (TypeScript)
- User asks about Claude API patterns, tool use, streaming, or vision
- Implementing agent workflows with Claude Agent SDK
- Optimizing API costs, token usage, or latency
## Model Selection
| Model | ID | Best For |
|-------|-----|----------|
| Opus 4.6 | `claude-opus-4-6` | Complex reasoning, architecture, research |
| Sonnet 4.6 | `claude-sonnet-4-6` | Balanced coding, most development tasks |
| Haiku 4.5 | `claude-haiku-4-5-20251001` | Fast responses, high-volume, cost-sensitive |
Default to Sonnet 4.6 unless the task requires deep reasoning (Opus) or speed/cost optimization (Haiku).
## Python SDK
### Installation
```bash
pip install anthropic
```
### Basic Message
```python
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from env
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[
{"role": "user", "content": "Explain async/await in Python"}
]
)
print(message.content[0].text)
```
### Streaming
```python
with client.messages.stream(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": "Write a haiku about coding"}]
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
```
### System Prompt
```python
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system="You are a senior Python developer. Be concise.",
messages=[{"role": "user", "content": "Review this function"}]
)
```
## TypeScript SDK
### Installation
```bash
npm install @anthropic-ai/sdk
```
### Basic Message
```typescript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic(); // reads ANTHROPIC_API_KEY from env
const message = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [
{ role: "user", content: "Explain async/await in TypeScript" }
],
});
console.log(message.content[0].text);
```
### Streaming
```typescript
const stream = client.messages.stream({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{ role: "user", content: "Write a haiku" }],
});
for await (const event of stream) {
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
process.stdout.write(event.delta.text);
}
}
```
## Tool Use
Define tools and let Claude call them:
```python
tools = [
{
"name": "get_weather",
"description": "Get current weather for a location",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City name"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["location"]
}
}
]
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": "What's the weather in SF?"}]
)
# Handle tool use response
for block in message.content:
if block.type == "tool_use":
# Execute the tool with block.input
result = get_weather(**block.input)
# Send result back
follow_up = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=tools,
messages=[
{"role": "user", "content": "What's the weather in SF?"},
{"role": "assistant", "content": message.content},
{"role": "user", "content": [
{"type": "tool_result", "tool_use_id": block.id, "content": str(result)}
]}
]
)
```
## Vision
Send images for analysis:
```python
import base64
with open("diagram.png", "rb") as f:
image_data = base64.standard_b64encode(f.read()).decode("utf-8")
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": image_data}},
{"type": "text", "text": "Describe this diagram"}
]
}]
)
```
## Extended Thinking
For complex reasoning tasks:
```python
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=16000,
thinking={
"type": "enabled",
"budget_tokens": 10000
},
messages=[{"role": "user", "content": "Solve this math problem step by step..."}]
)
for block in message.content:
if block.type == "thinking":
print(f"Thinking: {block.thinking}")
elif block.type == "text":
print(f"Answer: {block.text}")
```
## Prompt Caching
Cache large system prompts or context to reduce costs:
```python
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=[
{"type": "text", "text": large_system_prompt, "cache_control": {"type": "ephemeral"}}
],
messages=[{"role": "user", "content": "Question about the cached context"}]
)
# Check cache usage
print(f"Cache read: {message.usage.cache_read_input_tokens}")
print(f"Cache creation: {message.usage.cache_creation_input_tokens}")
```
## Batches API
Process large volumes asynchronously at 50% cost reduction:
```python
import time
batch = client.messages.batches.create(
requests=[
{
"custom_id": f"request-{i}",
"params": {
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"messages": [{"role": "user", "content": prompt}]
}
}
for i, prompt in enumerate(prompts)
]
)
# Poll for completion
while True:
status = client.messages.batches.retrieve(batch.id)
if status.processing_status == "ended":
break
time.sleep(30)
# Get results
for result in client.messages.batches.results(batch.id):
print(result.result.message.content[0].text)
```
## Claude Agent SDK
Build multi-step agents:
```python
# Note: Agent SDK API surface may change — check official docs
import anthropic
# Define tools as functions
tools = [{
"name": "search_codebase",
"description": "Search the codebase for relevant code",
"input_schema": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"]
}
}]
# Run an agentic loop with tool use
client = anthropic.Anthropic()
messages = [{"role": "user", "content": "Review the auth module for security issues"}]
while True:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
tools=tools,
messages=messages,
)
if response.stop_reason == "end_turn":
break
# Handle tool calls and continue the loop
messages.append({"role": "assistant", "content": response.content})
# ... execute tools and append tool_result messages
```
## Cost Optimization
| Strategy | Savings | When to Use |
|----------|---------|-------------|
| Prompt caching | Up to 90% on cached tokens | Repeated system prompts or context |
| Batches API | 50% | Non-time-sensitive bulk processing |
| Haiku instead of Sonnet | ~75% | Simple tasks, classification, extraction |
| Shorter max_tokens | Variable | When you know output will be short |
| Streaming | None (same cost) | Better UX, same price |
## Error Handling
```python
import time
from anthropic import APIError, RateLimitError, APIConnectionError
try:
message = client.messages.create(...)
except RateLimitError:
# Back off and retry
time.sleep(60)
except APIConnectionError:
# Network issue, retry with backoff
pass
except APIError as e:
print(f"API error {e.status_code}: {e.message}")
```
## Environment Setup
```bash
# Required
export ANTHROPIC_API_KEY="your-api-key-here"
# Optional: set default model
export ANTHROPIC_MODEL="claude-sonnet-4-6"
```
Never hardcode API keys. Always use environment variables.

View File

@@ -0,0 +1,7 @@
interface:
display_name: "Claude API"
short_description: "Anthropic Claude API patterns and SDKs"
brand_color: "#D97706"
default_prompt: "Build applications with the Claude API using Messages, tool use, streaming, and Agent SDK"
policy:
allow_implicit_invocation: true

View File

@@ -0,0 +1,192 @@
---
name: crosspost
description: Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.
origin: ECC
---
# Crosspost
Distribute content across multiple social platforms with platform-native adaptation.
## When to Use
- User wants to post content to multiple platforms
- Publishing announcements, launches, or updates across social media
- Repurposing a post from one platform to others
- User says "crosspost", "post everywhere", "share on all platforms", or "distribute this"
## How It Works
### Core Rules
1. **Never post identical content cross-platform.** Each platform gets a native adaptation.
2. **Primary platform first.** Post to the main platform, then adapt for others.
3. **Respect platform conventions.** Length limits, formatting, link handling all differ.
4. **One idea per post.** If the source content has multiple ideas, split across posts.
5. **Attribution matters.** If crossposting someone else's content, credit the source.
### Platform Specifications
| Platform | Max Length | Link Handling | Hashtags | Media |
|----------|-----------|---------------|----------|-------|
| X | 280 chars (4000 for Premium) | Counted in length | Minimal (1-2 max) | Images, video, GIFs |
| LinkedIn | 3000 chars | Not counted in length | 3-5 relevant | Images, video, docs, carousels |
| Threads | 500 chars | Separate link attachment | None typical | Images, video |
| Bluesky | 300 chars | Via facets (rich text) | None (use feeds) | Images |
### Workflow
### Step 1: Create Source Content
Start with the core idea. Use `content-engine` skill for high-quality drafts:
- Identify the single core message
- Determine the primary platform (where the audience is biggest)
- Draft the primary platform version first
### Step 2: Identify Target Platforms
Ask the user or determine from context:
- Which platforms to target
- Priority order (primary gets the best version)
- Any platform-specific requirements (e.g., LinkedIn needs professional tone)
### Step 3: Adapt Per Platform
For each target platform, transform the content:
**X adaptation:**
- Open with a hook, not a summary
- Cut to the core insight fast
- Keep links out of main body when possible
- Use thread format for longer content
**LinkedIn adaptation:**
- Strong first line (visible before "see more")
- Short paragraphs with line breaks
- Frame around lessons, results, or professional takeaways
- More explicit context than X (LinkedIn audience needs framing)
**Threads adaptation:**
- Conversational, casual tone
- Shorter than LinkedIn, less compressed than X
- Visual-first if possible
**Bluesky adaptation:**
- Direct and concise (300 char limit)
- Community-oriented tone
- Use feeds/lists for topic targeting instead of hashtags
### Step 4: Post Primary Platform
Post to the primary platform first:
- Use `x-api` skill for X
- Use platform-specific APIs or tools for others
- Capture the post URL for cross-referencing
### Step 5: Post to Secondary Platforms
Post adapted versions to remaining platforms:
- Stagger timing (not all at once — 30-60 min gaps)
- Include cross-platform references where appropriate ("longer thread on X" etc.)
## Examples
### Source: Product Launch
**X version:**
```
We just shipped [feature].
[One specific thing it does that's impressive]
[Link]
```
**LinkedIn version:**
```
Excited to share: we just launched [feature] at [Company].
Here's why it matters:
[2-3 short paragraphs with context]
[Takeaway for the audience]
[Link]
```
**Threads version:**
```
just shipped something cool — [feature]
[casual explanation of what it does]
link in bio
```
### Source: Technical Insight
**X version:**
```
TIL: [specific technical insight]
[Why it matters in one sentence]
```
**LinkedIn version:**
```
A pattern I've been using that's made a real difference:
[Technical insight with professional framing]
[How it applies to teams/orgs]
#relevantHashtag
```
## API Integration
### Batch Crossposting Service (Example Pattern)
If using a crossposting service (e.g., Postbridge, Buffer, or a custom API), the pattern looks like:
```python
import os
import requests
resp = requests.post(
"https://api.postbridge.io/v1/posts",
headers={"Authorization": f"Bearer {os.environ['POSTBRIDGE_API_KEY']}"},
json={
"platforms": ["twitter", "linkedin", "threads"],
"content": {
"twitter": {"text": x_version},
"linkedin": {"text": linkedin_version},
"threads": {"text": threads_version}
}
},
timeout=30
)
resp.raise_for_status()
```
### Manual Posting
Without Postbridge, post to each platform using its native API:
- X: Use `x-api` skill patterns
- LinkedIn: LinkedIn API v2 with OAuth 2.0
- Threads: Threads API (Meta)
- Bluesky: AT Protocol API
## Quality Gate
Before posting:
- [ ] Each platform version reads naturally for that platform
- [ ] No identical content across platforms
- [ ] Length limits respected
- [ ] Links work and are placed appropriately
- [ ] Tone matches platform conventions
- [ ] Media is sized correctly for each platform
## Related Skills
- `content-engine` — Generate platform-native content
- `x-api` — X/Twitter API integration

View File

@@ -0,0 +1,7 @@
interface:
display_name: "Crosspost"
short_description: "Multi-platform content distribution with native adaptation"
brand_color: "#EC4899"
default_prompt: "Distribute content across X, LinkedIn, Threads, and Bluesky with platform-native adaptation"
policy:
allow_implicit_invocation: true

View File

@@ -0,0 +1,155 @@
---
name: deep-research
description: Multi-source deep research using firecrawl and exa MCPs. Searches the web, synthesizes findings, and delivers cited reports with source attribution. Use when the user wants thorough research on any topic with evidence and citations.
origin: ECC
---
# Deep Research
Produce thorough, cited research reports from multiple web sources using firecrawl and exa MCP tools.
## When to Activate
- User asks to research any topic in depth
- Competitive analysis, technology evaluation, or market sizing
- Due diligence on companies, investors, or technologies
- Any question requiring synthesis from multiple sources
- User says "research", "deep dive", "investigate", or "what's the current state of"
## MCP Requirements
At least one of:
- **firecrawl** — `firecrawl_search`, `firecrawl_scrape`, `firecrawl_crawl`
- **exa** — `web_search_exa`, `web_search_advanced_exa`, `crawling_exa`
Both together give the best coverage. Configure in `~/.claude.json` or `~/.codex/config.toml`.
## Workflow
### Step 1: Understand the Goal
Ask 1-2 quick clarifying questions:
- "What's your goal — learning, making a decision, or writing something?"
- "Any specific angle or depth you want?"
If the user says "just research it" — skip ahead with reasonable defaults.
### Step 2: Plan the Research
Break the topic into 3-5 research sub-questions. Example:
- Topic: "Impact of AI on healthcare"
- What are the main AI applications in healthcare today?
- What clinical outcomes have been measured?
- What are the regulatory challenges?
- What companies are leading this space?
- What's the market size and growth trajectory?
### Step 3: Execute Multi-Source Search
For EACH sub-question, search using available MCP tools:
**With firecrawl:**
```
firecrawl_search(query: "<sub-question keywords>", limit: 8)
```
**With exa:**
```
web_search_exa(query: "<sub-question keywords>", numResults: 8)
web_search_advanced_exa(query: "<keywords>", numResults: 5, startPublishedDate: "2025-01-01")
```
**Search strategy:**
- Use 2-3 different keyword variations per sub-question
- Mix general and news-focused queries
- Aim for 15-30 unique sources total
- Prioritize: academic, official, reputable news > blogs > forums
### Step 4: Deep-Read Key Sources
For the most promising URLs, fetch full content:
**With firecrawl:**
```
firecrawl_scrape(url: "<url>")
```
**With exa:**
```
crawling_exa(url: "<url>", tokensNum: 5000)
```
Read 3-5 key sources in full for depth. Do not rely only on search snippets.
### Step 5: Synthesize and Write Report
Structure the report:
```markdown
# [Topic]: Research Report
*Generated: [date] | Sources: [N] | Confidence: [High/Medium/Low]*
## Executive Summary
[3-5 sentence overview of key findings]
## 1. [First Major Theme]
[Findings with inline citations]
- Key point ([Source Name](url))
- Supporting data ([Source Name](url))
## 2. [Second Major Theme]
...
## 3. [Third Major Theme]
...
## Key Takeaways
- [Actionable insight 1]
- [Actionable insight 2]
- [Actionable insight 3]
## Sources
1. [Title](url) — [one-line summary]
2. ...
## Methodology
Searched [N] queries across web and news. Analyzed [M] sources.
Sub-questions investigated: [list]
```
### Step 6: Deliver
- **Short topics**: Post the full report in chat
- **Long reports**: Post the executive summary + key takeaways, save full report to a file
## Parallel Research with Subagents
For broad topics, use Claude Code's Task tool to parallelize:
```
Launch 3 research agents in parallel:
1. Agent 1: Research sub-questions 1-2
2. Agent 2: Research sub-questions 3-4
3. Agent 3: Research sub-question 5 + cross-cutting themes
```
Each agent searches, reads sources, and returns findings. The main session synthesizes into the final report.
## Quality Rules
1. **Every claim needs a source.** No unsourced assertions.
2. **Cross-reference.** If only one source says it, flag it as unverified.
3. **Recency matters.** Prefer sources from the last 12 months.
4. **Acknowledge gaps.** If you couldn't find good info on a sub-question, say so.
5. **No hallucination.** If you don't know, say "insufficient data found."
6. **Separate fact from inference.** Label estimates, projections, and opinions clearly.
## Examples
```
"Research the current state of nuclear fusion energy"
"Deep dive into Rust vs Go for backend services in 2026"
"Research the best strategies for bootstrapping a SaaS business"
"What's happening with the US housing market right now?"
"Investigate the competitive landscape for AI code editors"
```

View File

@@ -0,0 +1,7 @@
interface:
display_name: "Deep Research"
short_description: "Multi-source deep research with firecrawl and exa MCPs"
brand_color: "#6366F1"
default_prompt: "Research the given topic using firecrawl and exa, produce a cited report"
policy:
allow_implicit_invocation: true

View File

@@ -0,0 +1,144 @@
---
name: dmux-workflows
description: Multi-agent orchestration using dmux (tmux pane manager for AI agents). Patterns for parallel agent workflows across Claude Code, Codex, OpenCode, and other harnesses. Use when running multiple agent sessions in parallel or coordinating multi-agent development workflows.
origin: ECC
---
# dmux Workflows
Orchestrate parallel AI agent sessions using dmux, a tmux pane manager for agent harnesses.
## When to Activate
- Running multiple agent sessions in parallel
- Coordinating work across Claude Code, Codex, and other harnesses
- Complex tasks that benefit from divide-and-conquer parallelism
- User says "run in parallel", "split this work", "use dmux", or "multi-agent"
## What is dmux
dmux is a tmux-based orchestration tool that manages AI agent panes:
- Press `n` to create a new pane with a prompt
- Press `m` to merge pane output back to the main session
- Supports: Claude Code, Codex, OpenCode, Cline, Gemini, Qwen
**Install:** `npm install -g dmux` or see [github.com/standardagents/dmux](https://github.com/standardagents/dmux)
## Quick Start
```bash
# Start dmux session
dmux
# Create agent panes (press 'n' in dmux, then type prompt)
# Pane 1: "Implement the auth middleware in src/auth/"
# Pane 2: "Write tests for the user service"
# Pane 3: "Update API documentation"
# Each pane runs its own agent session
# Press 'm' to merge results back
```
## Workflow Patterns
### Pattern 1: Research + Implement
Split research and implementation into parallel tracks:
```
Pane 1 (Research): "Research best practices for rate limiting in Node.js.
Check current libraries, compare approaches, and write findings to
/tmp/rate-limit-research.md"
Pane 2 (Implement): "Implement rate limiting middleware for our Express API.
Start with a basic token bucket, we'll refine after research completes."
# After Pane 1 completes, merge findings into Pane 2's context
```
### Pattern 2: Multi-File Feature
Parallelize work across independent files:
```
Pane 1: "Create the database schema and migrations for the billing feature"
Pane 2: "Build the billing API endpoints in src/api/billing/"
Pane 3: "Create the billing dashboard UI components"
# Merge all, then do integration in main pane
```
### Pattern 3: Test + Fix Loop
Run tests in one pane, fix in another:
```
Pane 1 (Watcher): "Run the test suite in watch mode. When tests fail,
summarize the failures."
Pane 2 (Fixer): "Fix failing tests based on the error output from pane 1"
```
### Pattern 4: Cross-Harness
Use different AI tools for different tasks:
```
Pane 1 (Claude Code): "Review the security of the auth module"
Pane 2 (Codex): "Refactor the utility functions for performance"
Pane 3 (Claude Code): "Write E2E tests for the checkout flow"
```
### Pattern 5: Code Review Pipeline
Parallel review perspectives:
```
Pane 1: "Review src/api/ for security vulnerabilities"
Pane 2: "Review src/api/ for performance issues"
Pane 3: "Review src/api/ for test coverage gaps"
# Merge all reviews into a single report
```
## Best Practices
1. **Independent tasks only.** Don't parallelize tasks that depend on each other's output.
2. **Clear boundaries.** Each pane should work on distinct files or concerns.
3. **Merge strategically.** Review pane output before merging to avoid conflicts.
4. **Use git worktrees.** For file-conflict-prone work, use separate worktrees per pane.
5. **Resource awareness.** Each pane uses API tokens — keep total panes under 5-6.
## Git Worktree Integration
For tasks that touch overlapping files:
```bash
# Create worktrees for isolation
git worktree add ../feature-auth feat/auth
git worktree add ../feature-billing feat/billing
# Run agents in separate worktrees
# Pane 1: cd ../feature-auth && claude
# Pane 2: cd ../feature-billing && claude
# Merge branches when done
git merge feat/auth
git merge feat/billing
```
## Complementary Tools
| Tool | What It Does | When to Use |
|------|-------------|-------------|
| **dmux** | tmux pane management for agents | Parallel agent sessions |
| **Superset** | Terminal IDE for 10+ parallel agents | Large-scale orchestration |
| **Claude Code Task tool** | In-process subagent spawning | Programmatic parallelism within a session |
| **Codex multi-agent** | Built-in agent roles | Codex-specific parallel work |
## Troubleshooting
- **Pane not responding:** Check if the agent session is waiting for input. Use `m` to read output.
- **Merge conflicts:** Use git worktrees to isolate file changes per pane.
- **High token usage:** Reduce number of parallel panes. Each pane is a full agent session.
- **tmux not found:** Install with `brew install tmux` (macOS) or `apt install tmux` (Linux).

View File

@@ -0,0 +1,7 @@
interface:
display_name: "dmux Workflows"
short_description: "Multi-agent orchestration with dmux"
brand_color: "#14B8A6"
default_prompt: "Orchestrate parallel agent sessions using dmux pane manager"
policy:
allow_implicit_invocation: true

View File

@@ -0,0 +1,174 @@
---
name: exa-search
description: Neural search via Exa MCP for web, code, and company research. Use when the user needs web search, code examples, company intel, people lookup, or AI-powered deep research with Exa's neural search engine.
origin: ECC
---
# Exa Search
Neural search for web content, code, companies, and people via the Exa MCP server.
## When to Activate
- User needs current web information or news
- Searching for code examples, API docs, or technical references
- Researching companies, competitors, or market players
- Finding professional profiles or people in a domain
- Running background research for any development task
- User says "search for", "look up", "find", or "what's the latest on"
## MCP Requirement
Exa MCP server must be configured. Add to `~/.claude.json`:
```json
"exa-web-search": {
"command": "npx",
"args": [
"-y",
"exa-mcp-server",
"tools=web_search_exa,web_search_advanced_exa,get_code_context_exa,crawling_exa,company_research_exa,people_search_exa,deep_researcher_start,deep_researcher_check"
],
"env": { "EXA_API_KEY": "YOUR_EXA_API_KEY_HERE" }
}
```
Get an API key at [exa.ai](https://exa.ai).
## Core Tools
### web_search_exa
General web search for current information, news, or facts.
```
web_search_exa(query: "latest AI developments 2026", numResults: 5)
```
**Parameters:**
| Param | Type | Default | Notes |
|-------|------|---------|-------|
| `query` | string | required | Search query |
| `numResults` | number | 8 | Number of results |
### web_search_advanced_exa
Filtered search with domain and date constraints.
```
web_search_advanced_exa(
query: "React Server Components best practices",
numResults: 5,
includeDomains: ["github.com", "react.dev"],
startPublishedDate: "2025-01-01"
)
```
**Parameters:**
| Param | Type | Default | Notes |
|-------|------|---------|-------|
| `query` | string | required | Search query |
| `numResults` | number | 8 | Number of results |
| `includeDomains` | string[] | none | Limit to specific domains |
| `excludeDomains` | string[] | none | Exclude specific domains |
| `startPublishedDate` | string | none | ISO date filter (start) |
| `endPublishedDate` | string | none | ISO date filter (end) |
### get_code_context_exa
Find code examples and documentation from GitHub, Stack Overflow, and docs sites.
```
get_code_context_exa(query: "Python asyncio patterns", tokensNum: 3000)
```
**Parameters:**
| Param | Type | Default | Notes |
|-------|------|---------|-------|
| `query` | string | required | Code or API search query |
| `tokensNum` | number | 5000 | Content tokens (1000-50000) |
### company_research_exa
Research companies for business intelligence and news.
```
company_research_exa(companyName: "Anthropic", numResults: 5)
```
**Parameters:**
| Param | Type | Default | Notes |
|-------|------|---------|-------|
| `companyName` | string | required | Company name |
| `numResults` | number | 5 | Number of results |
### people_search_exa
Find professional profiles and bios.
```
people_search_exa(query: "AI safety researchers at Anthropic", numResults: 5)
```
### crawling_exa
Extract full page content from a URL.
```
crawling_exa(url: "https://example.com/article", tokensNum: 5000)
```
**Parameters:**
| Param | Type | Default | Notes |
|-------|------|---------|-------|
| `url` | string | required | URL to extract |
| `tokensNum` | number | 5000 | Content tokens |
### deep_researcher_start / deep_researcher_check
Start an AI research agent that runs asynchronously.
```
# Start research
deep_researcher_start(query: "comprehensive analysis of AI code editors in 2026")
# Check status (returns results when complete)
deep_researcher_check(researchId: "<id from start>")
```
## Usage Patterns
### Quick Lookup
```
web_search_exa(query: "Node.js 22 new features", numResults: 3)
```
### Code Research
```
get_code_context_exa(query: "Rust error handling patterns Result type", tokensNum: 3000)
```
### Company Due Diligence
```
company_research_exa(companyName: "Vercel", numResults: 5)
web_search_advanced_exa(query: "Vercel funding valuation 2026", numResults: 3)
```
### Technical Deep Dive
```
# Start async research
deep_researcher_start(query: "WebAssembly component model status and adoption")
# ... do other work ...
deep_researcher_check(researchId: "<id>")
```
## Tips
- Use `web_search_exa` for broad queries, `web_search_advanced_exa` for filtered results
- Lower `tokensNum` (1000-2000) for focused code snippets, higher (5000+) for comprehensive context
- Combine `company_research_exa` with `web_search_advanced_exa` for thorough company analysis
- Use `crawling_exa` to get full content from specific URLs found in search results
- `deep_researcher_start` is best for comprehensive topics that benefit from AI synthesis
## Related Skills
- `deep-research` — Full research workflow using firecrawl + exa together
- `market-research` — Business-oriented research with decision frameworks

View File

@@ -0,0 +1,7 @@
interface:
display_name: "Exa Search"
short_description: "Neural search via Exa MCP for web, code, and companies"
brand_color: "#8B5CF6"
default_prompt: "Search using Exa MCP tools for web content, code, or company research"
policy:
allow_implicit_invocation: true

View File

@@ -0,0 +1,277 @@
---
name: fal-ai-media
description: Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.
origin: ECC
---
# fal.ai Media Generation
Generate images, videos, and audio using fal.ai models via MCP.
## When to Activate
- User wants to generate images from text prompts
- Creating videos from text or images
- Generating speech, music, or sound effects
- Any media generation task
- User says "generate image", "create video", "text to speech", "make a thumbnail", or similar
## MCP Requirement
fal.ai MCP server must be configured. Add to `~/.claude.json`:
```json
"fal-ai": {
"command": "npx",
"args": ["-y", "fal-ai-mcp-server"],
"env": { "FAL_KEY": "YOUR_FAL_KEY_HERE" }
}
```
Get an API key at [fal.ai](https://fal.ai).
## MCP Tools
The fal.ai MCP provides these tools:
- `search` — Find available models by keyword
- `find` — Get model details and parameters
- `generate` — Run a model with parameters
- `result` — Check async generation status
- `status` — Check job status
- `cancel` — Cancel a running job
- `estimate_cost` — Estimate generation cost
- `models` — List popular models
- `upload` — Upload files for use as inputs
---
## Image Generation
### Nano Banana 2 (Fast)
Best for: quick iterations, drafts, text-to-image, image editing.
```
generate(
model_name: "fal-ai/nano-banana-2",
input: {
"prompt": "a futuristic cityscape at sunset, cyberpunk style",
"image_size": "landscape_16_9",
"num_images": 1,
"seed": 42
}
)
```
### Nano Banana Pro (High Fidelity)
Best for: production images, realism, typography, detailed prompts.
```
generate(
model_name: "fal-ai/nano-banana-pro",
input: {
"prompt": "professional product photo of wireless headphones on marble surface, studio lighting",
"image_size": "square",
"num_images": 1,
"guidance_scale": 7.5
}
)
```
### Common Image Parameters
| Param | Type | Options | Notes |
|-------|------|---------|-------|
| `prompt` | string | required | Describe what you want |
| `image_size` | string | `square`, `portrait_4_3`, `landscape_16_9`, `portrait_16_9`, `landscape_4_3` | Aspect ratio |
| `num_images` | number | 1-4 | How many to generate |
| `seed` | number | any integer | Reproducibility |
| `guidance_scale` | number | 1-20 | How closely to follow the prompt (higher = more literal) |
### Image Editing
Use Nano Banana 2 with an input image for inpainting, outpainting, or style transfer:
```
# First upload the source image
upload(file_path: "/path/to/image.png")
# Then generate with image input
generate(
model_name: "fal-ai/nano-banana-2",
input: {
"prompt": "same scene but in watercolor style",
"image_url": "<uploaded_url>",
"image_size": "landscape_16_9"
}
)
```
---
## Video Generation
### Seedance 1.0 Pro (ByteDance)
Best for: text-to-video, image-to-video with high motion quality.
```
generate(
model_name: "fal-ai/seedance-1-0-pro",
input: {
"prompt": "a drone flyover of a mountain lake at golden hour, cinematic",
"duration": "5s",
"aspect_ratio": "16:9",
"seed": 42
}
)
```
### Kling Video v3 Pro
Best for: text/image-to-video with native audio generation.
```
generate(
model_name: "fal-ai/kling-video/v3/pro",
input: {
"prompt": "ocean waves crashing on a rocky coast, dramatic clouds",
"duration": "5s",
"aspect_ratio": "16:9"
}
)
```
### Veo 3 (Google DeepMind)
Best for: video with generated sound, high visual quality.
```
generate(
model_name: "fal-ai/veo-3",
input: {
"prompt": "a bustling Tokyo street market at night, neon signs, crowd noise",
"aspect_ratio": "16:9"
}
)
```
### Image-to-Video
Start from an existing image:
```
generate(
model_name: "fal-ai/seedance-1-0-pro",
input: {
"prompt": "camera slowly zooms out, gentle wind moves the trees",
"image_url": "<uploaded_image_url>",
"duration": "5s"
}
)
```
### Video Parameters
| Param | Type | Options | Notes |
|-------|------|---------|-------|
| `prompt` | string | required | Describe the video |
| `duration` | string | `"5s"`, `"10s"` | Video length |
| `aspect_ratio` | string | `"16:9"`, `"9:16"`, `"1:1"` | Frame ratio |
| `seed` | number | any integer | Reproducibility |
| `image_url` | string | URL | Source image for image-to-video |
---
## Audio Generation
### CSM-1B (Conversational Speech)
Text-to-speech with natural, conversational quality.
```
generate(
model_name: "fal-ai/csm-1b",
input: {
"text": "Hello, welcome to the demo. Let me show you how this works.",
"speaker_id": 0
}
)
```
### ThinkSound (Video-to-Audio)
Generate matching audio from video content.
```
generate(
model_name: "fal-ai/thinksound",
input: {
"video_url": "<video_url>",
"prompt": "ambient forest sounds with birds chirping"
}
)
```
### ElevenLabs (via API, no MCP)
For professional voice synthesis, use ElevenLabs directly:
```python
import os
import requests
resp = requests.post(
"https://api.elevenlabs.io/v1/text-to-speech/<voice_id>",
headers={
"xi-api-key": os.environ["ELEVENLABS_API_KEY"],
"Content-Type": "application/json"
},
json={
"text": "Your text here",
"model_id": "eleven_turbo_v2_5",
"voice_settings": {"stability": 0.5, "similarity_boost": 0.75}
}
)
with open("output.mp3", "wb") as f:
f.write(resp.content)
```
### VideoDB Generative Audio
If VideoDB is configured, use its generative audio:
```python
# Voice generation
audio = coll.generate_voice(text="Your narration here", voice="alloy")
# Music generation
music = coll.generate_music(prompt="upbeat electronic background music", duration=30)
# Sound effects
sfx = coll.generate_sound_effect(prompt="thunder crack followed by rain")
```
---
## Cost Estimation
Before generating, check estimated cost:
```
estimate_cost(model_name: "fal-ai/nano-banana-pro", input: {...})
```
## Model Discovery
Find models for specific tasks:
```
search(query: "text to video")
find(model_name: "fal-ai/seedance-1-0-pro")
models()
```
## Tips
- Use `seed` for reproducible results when iterating on prompts
- Start with lower-cost models (Nano Banana 2) for prompt iteration, then switch to Pro for finals
- For video, keep prompts descriptive but concise — focus on motion and scene
- Image-to-video produces more controlled results than pure text-to-video
- Check `estimate_cost` before running expensive video generations
## Related Skills
- `videodb` — Video processing, editing, and streaming
- `video-editing` — AI-powered video editing workflows
- `content-engine` — Content creation for social platforms

View File

@@ -0,0 +1,7 @@
interface:
display_name: "fal.ai Media"
short_description: "AI image, video, and audio generation via fal.ai"
brand_color: "#F43F5E"
default_prompt: "Generate images, videos, or audio using fal.ai models"
policy:
allow_implicit_invocation: true

View File

@@ -0,0 +1,308 @@
---
name: video-editing
description: AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.
origin: ECC
---
# Video Editing
AI-assisted editing for real footage. Not generation from prompts. Editing existing video fast.
## When to Activate
- User wants to edit, cut, or structure video footage
- Turning long recordings into short-form content
- Building vlogs, tutorials, or demo videos from raw capture
- Adding overlays, subtitles, music, or voiceover to existing video
- Reframing video for different platforms (YouTube, TikTok, Instagram)
- User says "edit video", "cut this footage", "make a vlog", or "video workflow"
## Core Thesis
AI video editing is useful when you stop asking it to create the whole video and start using it to compress, structure, and augment real footage. The value is not generation. The value is compression.
## The Pipeline
```
Screen Studio / raw footage
→ Claude / Codex
→ FFmpeg
→ Remotion
→ ElevenLabs / fal.ai
→ Descript or CapCut
```
Each layer has a specific job. Do not skip layers. Do not try to make one tool do everything.
## Layer 1: Capture (Screen Studio / Raw Footage)
Collect the source material:
- **Screen Studio**: polished screen recordings for app demos, coding sessions, browser workflows
- **Raw camera footage**: vlog footage, interviews, event recordings
- **Desktop capture via VideoDB**: session recording with real-time context (see `videodb` skill)
Output: raw files ready for organization.
## Layer 2: Organization (Claude / Codex)
Use Claude Code or Codex to:
- **Transcribe and label**: generate transcript, identify topics and themes
- **Plan structure**: decide what stays, what gets cut, what order works
- **Identify dead sections**: find pauses, tangents, repeated takes
- **Generate edit decision list**: timestamps for cuts, segments to keep
- **Scaffold FFmpeg and Remotion code**: generate the commands and compositions
```
Example prompt:
"Here's the transcript of a 4-hour recording. Identify the 8 strongest segments
for a 24-minute vlog. Give me FFmpeg cut commands for each segment."
```
This layer is about structure, not final creative taste.
## Layer 3: Deterministic Cuts (FFmpeg)
FFmpeg handles the boring but critical work: splitting, trimming, concatenating, and preprocessing.
### Extract segment by timestamp
```bash
ffmpeg -i raw.mp4 -ss 00:12:30 -to 00:15:45 -c copy segment_01.mp4
```
### Batch cut from edit decision list
```bash
#!/bin/bash
# cuts.txt: start,end,label
while IFS=, read -r start end label; do
ffmpeg -i raw.mp4 -ss "$start" -to "$end" -c copy "segments/${label}.mp4"
done < cuts.txt
```
### Concatenate segments
```bash
# Create file list
for f in segments/*.mp4; do echo "file '$f'"; done > concat.txt
ffmpeg -f concat -safe 0 -i concat.txt -c copy assembled.mp4
```
### Create proxy for faster editing
```bash
ffmpeg -i raw.mp4 -vf "scale=960:-2" -c:v libx264 -preset ultrafast -crf 28 proxy.mp4
```
### Extract audio for transcription
```bash
ffmpeg -i raw.mp4 -vn -acodec pcm_s16le -ar 16000 audio.wav
```
### Normalize audio levels
```bash
ffmpeg -i segment.mp4 -af loudnorm=I=-16:TP=-1.5:LRA=11 -c:v copy normalized.mp4
```
## Layer 4: Programmable Composition (Remotion)
Remotion turns editing problems into composable code. Use it for things that traditional editors make painful:
### When to use Remotion
- Overlays: text, images, branding, lower thirds
- Data visualizations: charts, stats, animated numbers
- Motion graphics: transitions, explainer animations
- Composable scenes: reusable templates across videos
- Product demos: annotated screenshots, UI highlights
### Basic Remotion composition
```tsx
import { AbsoluteFill, Sequence, Video, useCurrentFrame } from "remotion";
export const VlogComposition: React.FC = () => {
const frame = useCurrentFrame();
return (
<AbsoluteFill>
{/* Main footage */}
<Sequence from={0} durationInFrames={300}>
<Video src="/segments/intro.mp4" />
</Sequence>
{/* Title overlay */}
<Sequence from={30} durationInFrames={90}>
<AbsoluteFill style={{
justifyContent: "center",
alignItems: "center",
}}>
<h1 style={{
fontSize: 72,
color: "white",
textShadow: "2px 2px 8px rgba(0,0,0,0.8)",
}}>
The AI Editing Stack
</h1>
</AbsoluteFill>
</Sequence>
{/* Next segment */}
<Sequence from={300} durationInFrames={450}>
<Video src="/segments/demo.mp4" />
</Sequence>
</AbsoluteFill>
);
};
```
### Render output
```bash
npx remotion render src/index.ts VlogComposition output.mp4
```
See the [Remotion docs](https://www.remotion.dev/docs) for detailed patterns and API reference.
## Layer 5: Generated Assets (ElevenLabs / fal.ai)
Generate only what you need. Do not generate the whole video.
### Voiceover with ElevenLabs
```python
import os
import requests
resp = requests.post(
f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}",
headers={
"xi-api-key": os.environ["ELEVENLABS_API_KEY"],
"Content-Type": "application/json"
},
json={
"text": "Your narration text here",
"model_id": "eleven_turbo_v2_5",
"voice_settings": {"stability": 0.5, "similarity_boost": 0.75}
}
)
with open("voiceover.mp3", "wb") as f:
f.write(resp.content)
```
### Music and SFX with fal.ai
Use the `fal-ai-media` skill for:
- Background music generation
- Sound effects (ThinkSound model for video-to-audio)
- Transition sounds
### Generated visuals with fal.ai
Use for insert shots, thumbnails, or b-roll that doesn't exist:
```
generate(model_name: "fal-ai/nano-banana-pro", input: {
"prompt": "professional thumbnail for tech vlog, dark background, code on screen",
"image_size": "landscape_16_9"
})
```
### VideoDB generative audio
If VideoDB is configured:
```python
voiceover = coll.generate_voice(text="Narration here", voice="alloy")
music = coll.generate_music(prompt="lo-fi background for coding vlog", duration=120)
sfx = coll.generate_sound_effect(prompt="subtle whoosh transition")
```
## Layer 6: Final Polish (Descript / CapCut)
The last layer is human. Use a traditional editor for:
- **Pacing**: adjust cuts that feel too fast or slow
- **Captions**: auto-generated, then manually cleaned
- **Color grading**: basic correction and mood
- **Final audio mix**: balance voice, music, and SFX levels
- **Export**: platform-specific formats and quality settings
This is where taste lives. AI clears the repetitive work. You make the final calls.
## Social Media Reframing
Different platforms need different aspect ratios:
| Platform | Aspect Ratio | Resolution |
|----------|-------------|------------|
| YouTube | 16:9 | 1920x1080 |
| TikTok / Reels | 9:16 | 1080x1920 |
| Instagram Feed | 1:1 | 1080x1080 |
| X / Twitter | 16:9 or 1:1 | 1280x720 or 720x720 |
### Reframe with FFmpeg
```bash
# 16:9 to 9:16 (center crop)
ffmpeg -i input.mp4 -vf "crop=ih*9/16:ih,scale=1080:1920" vertical.mp4
# 16:9 to 1:1 (center crop)
ffmpeg -i input.mp4 -vf "crop=ih:ih,scale=1080:1080" square.mp4
```
### Reframe with VideoDB
```python
# Smart reframe (AI-guided subject tracking)
reframed = video.reframe(start=0, end=60, target="vertical", mode=ReframeMode.smart)
```
## Scene Detection and Auto-Cut
### FFmpeg scene detection
```bash
# Detect scene changes (threshold 0.3 = moderate sensitivity)
ffmpeg -i input.mp4 -vf "select='gt(scene,0.3)',showinfo" -vsync vfr -f null - 2>&1 | grep showinfo
```
### Silence detection for auto-cut
```bash
# Find silent segments (useful for cutting dead air)
ffmpeg -i input.mp4 -af silencedetect=noise=-30dB:d=2 -f null - 2>&1 | grep silence
```
### Highlight extraction
Use Claude to analyze transcript + scene timestamps:
```
"Given this transcript with timestamps and these scene change points,
identify the 5 most engaging 30-second clips for social media."
```
## What Each Tool Does Best
| Tool | Strength | Weakness |
|------|----------|----------|
| Claude / Codex | Organization, planning, code generation | Not the creative taste layer |
| FFmpeg | Deterministic cuts, batch processing, format conversion | No visual editing UI |
| Remotion | Programmable overlays, composable scenes, reusable templates | Learning curve for non-devs |
| Screen Studio | Polished screen recordings immediately | Only screen capture |
| ElevenLabs | Voice, narration, music, SFX | Not the center of the workflow |
| Descript / CapCut | Final pacing, captions, polish | Manual, not automatable |
## Key Principles
1. **Edit, don't generate.** This workflow is for cutting real footage, not creating from prompts.
2. **Structure before style.** Get the story right in Layer 2 before touching anything visual.
3. **FFmpeg is the backbone.** Boring but critical. Where long footage becomes manageable.
4. **Remotion for repeatability.** If you'll do it more than once, make it a Remotion component.
5. **Generate selectively.** Only use AI generation for assets that don't exist, not for everything.
6. **Taste is the last layer.** AI clears repetitive work. You make the final creative calls.
## Related Skills
- `fal-ai-media` — AI image, video, and audio generation
- `videodb` — Server-side video processing, indexing, and streaming
- `content-engine` — Platform-native content distribution

View File

@@ -0,0 +1,7 @@
interface:
display_name: "Video Editing"
short_description: "AI-assisted video editing for real footage"
brand_color: "#EF4444"
default_prompt: "Edit video using AI-assisted pipeline: organize, cut, compose, generate assets, polish"
policy:
allow_implicit_invocation: true

View File

@@ -0,0 +1,214 @@
---
name: x-api
description: X/Twitter API integration for posting tweets, threads, reading timelines, search, and analytics. Covers OAuth auth patterns, rate limits, and platform-native content posting. Use when the user wants to interact with X programmatically.
origin: ECC
---
# X API
Programmatic interaction with X (Twitter) for posting, reading, searching, and analytics.
## When to Activate
- User wants to post tweets or threads programmatically
- Reading timeline, mentions, or user data from X
- Searching X for content, trends, or conversations
- Building X integrations or bots
- Analytics and engagement tracking
- User says "post to X", "tweet", "X API", or "Twitter API"
## Authentication
### OAuth 2.0 (App-Only / User Context)
Best for: read-heavy operations, search, public data.
```bash
# Environment setup
export X_BEARER_TOKEN="your-bearer-token"
```
```python
import os
import requests
bearer = os.environ["X_BEARER_TOKEN"]
headers = {"Authorization": f"Bearer {bearer}"}
# Search recent tweets
resp = requests.get(
"https://api.x.com/2/tweets/search/recent",
headers=headers,
params={"query": "claude code", "max_results": 10}
)
tweets = resp.json()
```
### OAuth 1.0a (User Context)
Required for: posting tweets, managing account, DMs.
```bash
# Environment setup — source before use
export X_API_KEY="your-api-key"
export X_API_SECRET="your-api-secret"
export X_ACCESS_TOKEN="your-access-token"
export X_ACCESS_SECRET="your-access-secret"
```
```python
import os
from requests_oauthlib import OAuth1Session
oauth = OAuth1Session(
os.environ["X_API_KEY"],
client_secret=os.environ["X_API_SECRET"],
resource_owner_key=os.environ["X_ACCESS_TOKEN"],
resource_owner_secret=os.environ["X_ACCESS_SECRET"],
)
```
## Core Operations
### Post a Tweet
```python
resp = oauth.post(
"https://api.x.com/2/tweets",
json={"text": "Hello from Claude Code"}
)
resp.raise_for_status()
tweet_id = resp.json()["data"]["id"]
```
### Post a Thread
```python
def post_thread(oauth, tweets: list[str]) -> list[str]:
ids = []
reply_to = None
for text in tweets:
payload = {"text": text}
if reply_to:
payload["reply"] = {"in_reply_to_tweet_id": reply_to}
resp = oauth.post("https://api.x.com/2/tweets", json=payload)
resp.raise_for_status()
tweet_id = resp.json()["data"]["id"]
ids.append(tweet_id)
reply_to = tweet_id
return ids
```
### Read User Timeline
```python
resp = requests.get(
f"https://api.x.com/2/users/{user_id}/tweets",
headers=headers,
params={
"max_results": 10,
"tweet.fields": "created_at,public_metrics",
}
)
```
### Search Tweets
```python
resp = requests.get(
"https://api.x.com/2/tweets/search/recent",
headers=headers,
params={
"query": "from:affaanmustafa -is:retweet",
"max_results": 10,
"tweet.fields": "public_metrics,created_at",
}
)
```
### Get User by Username
```python
resp = requests.get(
"https://api.x.com/2/users/by/username/affaanmustafa",
headers=headers,
params={"user.fields": "public_metrics,description,created_at"}
)
```
### Upload Media and Post
```python
# Media upload uses v1.1 endpoint
# Step 1: Upload media
media_resp = oauth.post(
"https://upload.twitter.com/1.1/media/upload.json",
files={"media": open("image.png", "rb")}
)
media_id = media_resp.json()["media_id_string"]
# Step 2: Post with media
resp = oauth.post(
"https://api.x.com/2/tweets",
json={"text": "Check this out", "media": {"media_ids": [media_id]}}
)
```
## Rate Limits Reference
| Endpoint | Limit | Window |
|----------|-------|--------|
| POST /2/tweets | 200 | 15 min |
| GET /2/tweets/search/recent | 450 | 15 min |
| GET /2/users/:id/tweets | 1500 | 15 min |
| GET /2/users/by/username | 300 | 15 min |
| POST media/upload | 415 | 15 min |
Always check `x-rate-limit-remaining` and `x-rate-limit-reset` headers.
```python
import time
remaining = int(resp.headers.get("x-rate-limit-remaining", 0))
if remaining < 5:
reset = int(resp.headers.get("x-rate-limit-reset", 0))
wait = max(0, reset - int(time.time()))
print(f"Rate limit approaching. Resets in {wait}s")
```
## Error Handling
```python
resp = oauth.post("https://api.x.com/2/tweets", json={"text": content})
if resp.status_code == 201:
return resp.json()["data"]["id"]
elif resp.status_code == 429:
reset = int(resp.headers["x-rate-limit-reset"])
raise Exception(f"Rate limited. Resets at {reset}")
elif resp.status_code == 403:
raise Exception(f"Forbidden: {resp.json().get('detail', 'check permissions')}")
else:
raise Exception(f"X API error {resp.status_code}: {resp.text}")
```
## Security
- **Never hardcode tokens.** Use environment variables or `.env` files.
- **Never commit `.env` files.** Add to `.gitignore`.
- **Rotate tokens** if exposed. Regenerate at developer.x.com.
- **Use read-only tokens** when write access is not needed.
- **Store OAuth secrets securely** — not in source code or logs.
## Integration with Content Engine
Use `content-engine` skill to generate platform-native content, then post via X API:
1. Generate content with content-engine (X platform format)
2. Validate length (280 chars for single tweet)
3. Post via X API using patterns above
4. Track engagement via public_metrics
## Related Skills
- `content-engine` — Generate platform-native content for X
- `crosspost` — Distribute content across X, LinkedIn, and other platforms

View File

@@ -0,0 +1,7 @@
interface:
display_name: "X API"
short_description: "X/Twitter API integration for posting, threads, and analytics"
brand_color: "#000000"
default_prompt: "Use X API to post tweets, threads, or retrieve timeline and search data"
policy:
allow_implicit_invocation: true

View File

@@ -34,10 +34,17 @@ Available skills:
- strategic-compact — Context management
- api-design — REST API design patterns
- verification-loop — Build, test, lint, typecheck, security
- deep-research — Multi-source research with firecrawl and exa MCPs
- exa-search — Neural search via Exa MCP for web, code, and companies
- claude-api — Anthropic Claude API patterns and SDKs
- x-api — X/Twitter API integration for posting, threads, and analytics
- crosspost — Multi-platform content distribution
- fal-ai-media — AI image/video/audio generation via fal.ai
- dmux-workflows — Multi-agent orchestration with dmux
## MCP Servers
Configure in `~/.codex/config.toml` under `[mcp_servers]`. See `.codex/config.toml` for reference configuration with GitHub, Context7, Memory, and Sequential Thinking servers.
Treat the project-local `.codex/config.toml` as the default Codex baseline for ECC. The current ECC baseline enables GitHub, Context7, Exa, Memory, Playwright, and Sequential Thinking; add heavier extras in `~/.codex/config.toml` only when a task actually needs them.
## Multi-Agent Support
@@ -63,7 +70,7 @@ Sample role configs in this repo:
| Commands | `/slash` commands | Instruction-based |
| Agents | Subagent Task tool | Multi-agent via `/agent` and `[agents.<name>]` roles |
| Security | Hook-based enforcement | Instruction + sandbox |
| MCP | Full support | Command-based only |
| MCP | Full support | Supported via `config.toml` and `codex mcp add` |
## Security Without Hooks

View File

@@ -1,4 +1,4 @@
model = "o4-mini"
model = "gpt-5.4"
model_reasoning_effort = "medium"
sandbox_mode = "read-only"

View File

@@ -10,8 +10,9 @@
# - https://developers.openai.com/codex/multi-agent
# Model selection
model = "o4-mini"
model_provider = "openai"
# Leave `model` and `model_provider` unset so Codex CLI uses its current
# built-in defaults. Uncomment and pin them only if you intentionally want
# repo-local or global model overrides.
# Top-level runtime settings (current Codex schema)
approval_policy = "on-request"
@@ -32,6 +33,8 @@ notify = [
# model_instructions_file = "/absolute/path/to/instructions.md"
# MCP servers
# Keep the default project set lean. API-backed servers inherit credentials from
# the launching environment or can be supplied by a user-level ~/.codex/config.toml.
[mcp_servers.github]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]
@@ -40,10 +43,17 @@ args = ["-y", "@modelcontextprotocol/server-github"]
command = "npx"
args = ["-y", "@upstash/context7-mcp@latest"]
[mcp_servers.exa]
url = "https://mcp.exa.ai/mcp"
[mcp_servers.memory]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-memory"]
[mcp_servers.playwright]
command = "npx"
args = ["-y", "@playwright/mcp@latest", "--extension"]
[mcp_servers.sequential-thinking]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]
@@ -57,6 +67,10 @@ args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]
# command = "npx"
# args = ["-y", "firecrawl-mcp"]
#
# [mcp_servers.fal-ai]
# command = "npx"
# args = ["-y", "fal-ai-mcp-server"]
#
# [mcp_servers.cloudflare]
# command = "npx"
# args = ["-y", "@cloudflare/mcp-server-cloudflare"]
@@ -76,22 +90,18 @@ approval_policy = "never"
sandbox_mode = "workspace-write"
web_search = "live"
# Optional project-local multi-agent roles.
# Keep these commented in global config, because config_file paths are resolved
# relative to the config.toml file and must exist at load time.
#
# [agents]
# max_threads = 6
# max_depth = 1
#
# [agents.explorer]
# description = "Read-only codebase explorer for gathering evidence before changes are proposed."
# config_file = "agents/explorer.toml"
#
# [agents.reviewer]
# description = "PR reviewer focused on correctness, security, and missing tests."
# config_file = "agents/reviewer.toml"
#
# [agents.docs_researcher]
# description = "Documentation specialist that verifies APIs, framework behavior, and release notes."
# config_file = "agents/docs-researcher.toml"
[agents]
max_threads = 6
max_depth = 1
[agents.explorer]
description = "Read-only codebase explorer for gathering evidence before changes are proposed."
config_file = "agents/explorer.toml"
[agents.reviewer]
description = "PR reviewer focused on correctness, security, and missing tests."
config_file = "agents/reviewer.toml"
[agents.docs_researcher]
description = "Documentation specialist that verifies APIs, framework behavior, and release notes."
config_file = "agents/docs-researcher.toml"

1
.gitignore vendored
View File

@@ -41,3 +41,4 @@ examples/sessions/*.tmp
# Local drafts
marketing/
.dmux/

View File

@@ -116,7 +116,7 @@ the community.
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
<https://www.contributor-covenant.org/version/2/0/code_of_conduct.html>.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
@@ -124,5 +124,5 @@ enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
<https://www.contributor-covenant.org/faq>. Translations are available at
<https://www.contributor-covenant.org/translations>.

View File

@@ -940,6 +940,7 @@ Codex macOS app:
- Open this repository as your workspace.
- The root `AGENTS.md` is auto-detected.
- `.codex/config.toml` and `.codex/agents/*.toml` work best when kept project-local.
- The reference `.codex/config.toml` intentionally does not pin `model` or `model_provider`, so Codex uses its own current default unless you override it.
- Optional: copy `.codex/config.toml` to `~/.codex/config.toml` for global defaults; keep the multi-agent role files project-local unless you also copy `.codex/agents/`.
### What's Included

164
commands/aside.md Normal file
View File

@@ -0,0 +1,164 @@
---
description: Answer a quick side question without interrupting or losing context from the current task. Resume work automatically after answering.
---
# Aside Command
Ask a question mid-task and get an immediate, focused answer — then continue right where you left off. The current task, files, and context are never modified.
## When to Use
- You're curious about something while Claude is working and don't want to lose momentum
- You need a quick explanation of code Claude is currently editing
- You want a second opinion or clarification on a decision without derailing the task
- You need to understand an error, concept, or pattern before Claude proceeds
- You want to ask something unrelated to the current task without starting a new session
## Usage
```
/aside <your question>
/aside what does this function actually return?
/aside is this pattern thread-safe?
/aside why are we using X instead of Y here?
/aside what's the difference between foo() and bar()?
/aside should we be worried about the N+1 query we just added?
```
## Process
### Step 1: Freeze the current task state
Before answering anything, mentally note:
- What is the active task? (what file, feature, or problem was being worked on)
- What step was in progress at the moment `/aside` was invoked?
- What was about to happen next?
Do NOT touch, edit, create, or delete any files during the aside.
### Step 2: Answer the question directly
Answer the question in the most concise form that is still complete and useful.
- Lead with the answer, not the reasoning
- Keep it short — if a full explanation is needed, offer to go deeper after the task
- If the question is about the current file or code being worked on, reference it precisely (file path and line number if relevant)
- If answering requires reading a file, read it — but read only, never write
Format the response as:
```
ASIDE: [restate the question briefly]
[Your answer here]
— Back to task: [one-line description of what was being done]
```
### Step 3: Resume the main task
After delivering the answer, immediately continue the active task from the exact point it was paused. Do not ask for permission to resume unless the aside answer revealed a blocker or a reason to reconsider the current approach (see Edge Cases).
---
## Edge Cases
**No question provided (`/aside` with nothing after it):**
Respond:
```
ASIDE: no question provided
What would you like to know? (ask your question and I'll answer without losing the current task context)
— Back to task: [one-line description of what was being done]
```
**Question reveals a potential problem with the current task:**
Flag it clearly before resuming:
```
ASIDE: [answer]
⚠️ Note: This answer suggests [issue] with the current approach. Want to address this before continuing, or proceed as planned?
```
Wait for the user's decision before resuming.
**Question is actually a task redirect (not a side question):**
If the question implies changing what is being built (e.g., `/aside actually, let's use Redis instead`), clarify:
```
ASIDE: That sounds like a direction change, not just a side question.
Do you want to:
(a) Answer this as information only and keep the current plan
(b) Pause the current task and change approach
```
Wait for the user's answer — do not make assumptions.
**Question is about the currently open file or code:**
Answer from the live context. If the file was read earlier in the session, reference it directly. If not, read it now (read-only) and answer with a file:line reference.
**No active task (nothing in progress when `/aside` is invoked):**
Still use the standard wrapper so the response shape stays consistent:
```
ASIDE: [restate the question briefly]
[Your answer here]
— Back to task: no active task to resume
```
**Question requires a long answer:**
Give the essential answer concisely, then offer:
```
That's the short version. Want a deeper explanation after we finish [current task]?
```
**Multiple `/aside` questions in a row:**
Answer each one in sequence. After the last answer, resume the main task. Do not lose task state across a chain of asides.
**Aside answer implies a code change is needed:**
Note the change needed but do not make it during the aside:
```
ASIDE: [answer]
📝 Worth fixing: [what should be changed]. I'll flag this after the current task unless you want to address it now.
```
**Question is ambiguous or too vague:**
Ask one clarifying question — the shortest question that gets the information needed to answer. Do not ask multiple questions.
---
## Example Output
```
User: /aside what does fetchWithRetry() actually do?
ASIDE: what does fetchWithRetry() do?
fetchWithRetry() (src/api/retry.ts:12) attempts the request up to 3 times with
exponential backoff (250ms → 500ms → 1s). It only retries on 5xx responses and
network errors — 4xx errors are treated as final and not retried.
— Back to task: refactoring the auth middleware in src/middleware/auth.ts
```
```
User: /aside is the approach we're taking thread-safe?
ASIDE: is the current approach thread-safe?
No — the shared cache object in src/cache/store.ts:34 is mutated without locking.
Under concurrent requests this is a race condition. It's low risk in a single-process
Node.js server but would be a real problem with worker threads or clustering.
⚠️ Note: This could affect the feature we're building. Want to address this now or continue and fix it in a follow-up?
```
---
## Notes
- Never modify files during an aside — read-only access only
- The aside is a conversation pause, not a new task — the original task must always resume
- Keep answers focused: the goal is to unblock the user quickly, not to deliver a lecture
- If an aside sparks a larger discussion, finish the current task first unless the aside reveals a blocker
- Asides are not saved to session files unless explicitly relevant to the task outcome

View File

@@ -101,6 +101,14 @@ TaskOutput({ task_id: "<task_id>", block: true, timeout: 600000 })
4. Force stop when score < 7 or user does not approve.
5. Use `AskUserQuestion` tool for user interaction when needed (e.g., confirmation/selection/approval).
## When to Use External Orchestration
Use external tmux/worktree orchestration when the work must be split across parallel workers that need isolated git state, independent terminals, or separate build/test execution. Use in-process subagents for lightweight analysis, planning, or review where the main session remains the only writer.
```bash
node scripts/orchestrate-worktrees.js .claude/plan/workflow-e2e-test.json --execute
```
---
## Execution Workflow

View File

@@ -148,6 +148,61 @@ Run simultaneously:
Combine outputs into single report
```
For external tmux-pane workers with separate git worktrees, use `node scripts/orchestrate-worktrees.js plan.json --execute`. The built-in orchestration pattern stays in-process; the helper is for long-running or cross-harness sessions.
When workers need to see dirty or untracked local files from the main checkout, add `seedPaths` to the plan file. ECC overlays only those selected paths into each worker worktree after `git worktree add`, which keeps the branch isolated while still exposing in-flight local scripts, plans, or docs.
```json
{
"sessionName": "workflow-e2e",
"seedPaths": [
"scripts/orchestrate-worktrees.js",
"scripts/lib/tmux-worktree-orchestrator.js",
".claude/plan/workflow-e2e-test.json"
],
"workers": [
{ "name": "docs", "task": "Update orchestration docs." }
]
}
```
To export a control-plane snapshot for a live tmux/worktree session, run:
```bash
node scripts/orchestration-status.js .claude/plan/workflow-visual-proof.json
```
The snapshot includes session activity, tmux pane metadata, worker states, objectives, seeded overlays, and recent handoff summaries in JSON form.
## Operator Command-Center Handoff
When the workflow spans multiple sessions, worktrees, or tmux panes, append a control-plane block to the final handoff:
```markdown
CONTROL PLANE
-------------
Sessions:
- active session ID or alias
- branch + worktree path for each active worker
- tmux pane or detached session name when applicable
Diffs:
- git status summary
- git diff --stat for touched files
- merge/conflict risk notes
Approvals:
- pending user approvals
- blocked steps awaiting confirmation
Telemetry:
- last activity timestamp or idle signal
- estimated token or cost drift
- policy events raised by hooks or reviewers
```
This keeps planner, implementer, reviewer, and loop workers legible from the operator surface.
## Arguments
$ARGUMENTS:

View File

@@ -0,0 +1,38 @@
---
description: Analyze a draft prompt and output an optimized, ECC-enriched version ready to paste and run. Does NOT execute the task — outputs advisory analysis only.
---
# /prompt-optimize
Analyze and optimize the following prompt for maximum ECC leverage.
## Your Task
Apply the **prompt-optimizer** skill to the user's input below. Follow the 6-phase analysis pipeline:
0. **Project Detection** — Read CLAUDE.md, detect tech stack from project files (package.json, go.mod, pyproject.toml, etc.)
1. **Intent Detection** — Classify the task type (new feature, bug fix, refactor, research, testing, review, documentation, infrastructure, design)
2. **Scope Assessment** — Evaluate complexity (TRIVIAL / LOW / MEDIUM / HIGH / EPIC), using codebase size as signal if detected
3. **ECC Component Matching** — Map to specific skills, commands, agents, and model tier
4. **Missing Context Detection** — Identify gaps. If 3+ critical items missing, ask the user to clarify before generating
5. **Workflow & Model** — Determine lifecycle position, recommend model tier, and split into multiple prompts if HIGH/EPIC
## Output Requirements
- Present diagnosis, recommended ECC components, and an optimized prompt using the Output Format from the prompt-optimizer skill
- Provide both **Full Version** (detailed) and **Quick Version** (compact, varied by intent type)
- Respond in the same language as the user's input
- The optimized prompt must be complete and ready to copy-paste into a new session
- End with a footer offering adjustment or a clear next step for starting a separate execution request
## CRITICAL
Do NOT execute the user's task. Output ONLY the analysis and optimized prompt.
If the user asks for direct execution, explain that `/prompt-optimize` only produces advisory output and tell them to start a normal task request instead.
Note: `blueprint` is a **skill**, not a slash command. Write "Use the blueprint skill"
instead of presenting it as a `/...` command.
## User Input
$ARGUMENTS

View File

@@ -12,6 +12,8 @@ Manage Claude Code session history - list, load, alias, and edit sessions stored
Display all sessions with metadata, filtering, and pagination.
Use `/sessions info` when you need operator-surface context for a swarm: branch, worktree path, and session recency.
```bash
/sessions # List all sessions (default)
/sessions list # Same as above
@@ -25,6 +27,7 @@ Display all sessions with metadata, filtering, and pagination.
node -e "
const sm = require((process.env.CLAUDE_PLUGIN_ROOT||require('path').join(require('os').homedir(),'.claude'))+'/scripts/lib/session-manager');
const aa = require((process.env.CLAUDE_PLUGIN_ROOT||require('path').join(require('os').homedir(),'.claude'))+'/scripts/lib/session-aliases');
const path = require('path');
const result = sm.getAllSessions({ limit: 20 });
const aliases = aa.listAliases();
@@ -33,17 +36,18 @@ for (const a of aliases) aliasMap[a.sessionPath] = a.name;
console.log('Sessions (showing ' + result.sessions.length + ' of ' + result.total + '):');
console.log('');
console.log('ID Date Time Size Lines Alias');
console.log('────────────────────────────────────────────────────');
console.log('ID Date Time Branch Worktree Alias');
console.log('────────────────────────────────────────────────────────────────────');
for (const s of result.sessions) {
const alias = aliasMap[s.filename] || '';
const size = sm.getSessionSize(s.sessionPath);
const stats = sm.getSessionStats(s.sessionPath);
const metadata = sm.parseSessionMetadata(sm.getSessionContent(s.sessionPath));
const id = s.shortId === 'no-id' ? '(none)' : s.shortId.slice(0, 8);
const time = s.modifiedTime.toTimeString().slice(0, 5);
const branch = (metadata.branch || '-').slice(0, 12);
const worktree = metadata.worktree ? path.basename(metadata.worktree).slice(0, 18) : '-';
console.log(id.padEnd(8) + ' ' + s.date + ' ' + time + ' ' + size.padEnd(7) + ' ' + String(stats.lineCount).padEnd(5) + ' ' + alias);
console.log(id.padEnd(8) + ' ' + s.date + ' ' + time + ' ' + branch.padEnd(12) + ' ' + worktree.padEnd(18) + ' ' + alias);
}
"
```
@@ -108,6 +112,18 @@ if (session.metadata.started) {
if (session.metadata.lastUpdated) {
console.log('Last Updated: ' + session.metadata.lastUpdated);
}
if (session.metadata.project) {
console.log('Project: ' + session.metadata.project);
}
if (session.metadata.branch) {
console.log('Branch: ' + session.metadata.branch);
}
if (session.metadata.worktree) {
console.log('Worktree: ' + session.metadata.worktree);
}
" "$ARGUMENTS"
```
@@ -215,6 +231,9 @@ console.log('ID: ' + (session.shortId === 'no-id' ? '(none)' : session.
console.log('Filename: ' + session.filename);
console.log('Date: ' + session.date);
console.log('Modified: ' + session.modifiedTime.toISOString().slice(0, 19).replace('T', ' '));
console.log('Project: ' + (session.metadata.project || '-'));
console.log('Branch: ' + (session.metadata.branch || '-'));
console.log('Worktree: ' + (session.metadata.worktree || '-'));
console.log('');
console.log('Content:');
console.log(' Lines: ' + stats.lineCount);
@@ -236,6 +255,11 @@ Show all session aliases.
/sessions aliases # List all aliases
```
## Operator Notes
- Session files persist `Project`, `Branch`, and `Worktree` in the header so `/sessions info` can disambiguate parallel tmux/worktree runs.
- For command-center style monitoring, combine `/sessions info`, `git diff --stat`, and the cost metrics emitted by `scripts/hooks/cost-tracker.js`.
**Script:**
```bash
node -e "

View File

@@ -581,7 +581,7 @@ LOGGING = {
| 強力なシークレット | SECRET_KEYに環境変数を使用 |
| パスワード検証 | すべてのパスワードバリデータを有効化 |
| CSRF保護 | デフォルトで有効、無効にしない |
| XSS防止 | Djangoは自動エスケープ、ユーザー入力で`|safe`を使用しない |
| XSS防止 | Djangoは自動エスケープ、ユーザー入力で<code>\|safe</code>を使用しない |
| SQLインジェクション | ORMを使用、クエリで文字列を連結しない |
| ファイルアップロード | ファイルタイプとサイズを検証 |
| レート制限 | APIエンドポイントをスロットル |

View File

@@ -71,14 +71,14 @@
## 归属
本《行为准则》改编自 \[Contributor Covenant]\[homepage]
本《行为准则》改编自 [Contributor Covenant][homepage]
版本 2.0,可在
https://www.contributor-covenant.org/version/2/0/code\_of\_conduct.html 获取。
<https://www.contributor-covenant.org/version/2/0/code_of_conduct.html> 获取。
社区影响指南的灵感来源于 [Mozilla 的行为准则执行阶梯](https://github.com/mozilla/diversity)。
[homepage]: https://www.contributor-covenant.org
有关本行为准则常见问题的解答,请参阅常见问题解答:
https://www.contributor-covenant.org/faq。翻译版本可在
https://www.contributor-covenant.org/translations 获取。
<https://www.contributor-covenant.org/faq>。翻译版本可在
<https://www.contributor-covenant.org/translations> 获取。

View File

@@ -939,13 +939,14 @@ Codex macOS 应用:
* 将此仓库作为您的工作区打开。
* 根目录的 `AGENTS.md` 会被自动检测。
* 参考 `.codex/config.toml` 故意不固定 `model``model_provider`,因此 Codex 会使用它自己的当前默认值,除非您显式覆盖。
* 可选:将 `.codex/config.toml` 复制到 `~/.codex/config.toml` 以实现 CLI/应用行为一致性。
### 包含内容
| 组件 | 数量 | 详情 |
|-----------|-------|---------|
| 配置 | 1 | `.codex/config.toml`模型、权限、MCP 服务器、持久指令 |
| 配置 | 1 | `.codex/config.toml` — 权限、MCP 服务器、通知和配置文件 |
| AGENTS.md | 2 | 根目录(通用)+ `.codex/AGENTS.md`Codex 特定补充) |
| 技能 | 16 | `.agents/skills/` — 每个技能包含 SKILL.md + agents/openai.yaml |
| MCP 服务器 | 4 | GitHub、Context7、Memory、Sequential Thinking基于命令 |

View File

@@ -315,6 +315,6 @@ result = "".join(str(item) for item in items)
| 海象运算符 (`:=`) | 3.8+ |
| 仅限位置参数 | 3.8+ |
| Match 语句 | 3.10+ |
| 类型联合 (\`x | None\`) | 3.10+ |
| 类型联合 (\`x \| None\`) | 3.10+ |
确保你的项目 `pyproject.toml``setup.py` 指定了正确的最低 Python 版本。

View File

@@ -40,7 +40,7 @@
要在会话间共享记忆,最好的方法是使用一个技能或命令来总结和检查进度,然后保存到 `.claude` 文件夹中的一个 `.tmp` 文件中,并在会话结束前不断追加内容。第二天,它可以将其用作上下文,并从中断处继续。为每个会话创建一个新文件,这样你就不会将旧的上下文污染到新的工作中。
![Session Storage File Tree](../../assets/images/longform/03-session-storage.png)
*会话存储示例 -> https://github.com/affaan-m/everything-claude-code/tree/main/examples/sessions*
*会话存储示例 -> <https://github.com/affaan-m/everything-claude-code/tree/main/examples/sessions>*
Claude 创建一个总结当前状态的文件。审阅它,如果需要则要求编辑,然后重新开始。对于新的对话,只需提供文件路径。当你达到上下文限制并需要继续复杂工作时,这尤其有用。这些文件应包含:
@@ -130,7 +130,7 @@ alias claude-research='claude --system-prompt "$(cat ~/.claude/contexts/research
**定价参考:**
![Claude Model Pricing](../../assets/images/longform/05-pricing-table.png)
*来源https://platform.claude.com/docs/en/about-claude/pricing*
*来源:<https://platform.claude.com/docs/en/about-claude/pricing>*
**工具特定优化:**

View File

@@ -211,7 +211,7 @@ git worktree add ../feature-branch feature-branch
流式传输和监视 Claude 运行的日志/bash 进程:
https://github.com/user-attachments/assets/shortform/07-tmux-video.mp4
<https://github.com/user-attachments/assets/shortform/07-tmux-video.mp4>
```bash
tmux new -s dev

View File

@@ -129,7 +129,7 @@ CREATE INDEX orders_customer_id_idx ON orders (customer_id);
| 索引類型 | 使用場景 | 運算子 |
|----------|----------|--------|
| **B-tree**(預設)| 等於、範圍 | `=``<``>``BETWEEN``IN` |
| **GIN** | 陣列、JSONB、全文搜尋 | `@>``?``?&``?|``@@` |
| **GIN** | 陣列、JSONB、全文搜尋 | `@>``?``?&`<code>?\|</code>`@@` |
| **BRIN** | 大型時序表 | 排序資料的範圍查詢 |
| **Hash** | 僅等於 | `=`(比 B-tree 略快)|

View File

@@ -72,11 +72,11 @@
"env": {
"EXA_API_KEY": "YOUR_EXA_API_KEY_HERE"
},
"description": "Web search, research, and data ingestion via Exa API — recommended for research-first development workflow"
"description": "Web search, research, and data ingestion via Exa API — prefer task-scoped use for broader research after GitHub search and primary docs"
},
"context7": {
"command": "npx",
"args": ["-y", "@context7/mcp-server"],
"args": ["-y", "@upstash/context7-mcp@latest"],
"description": "Live documentation lookup"
},
"magic": {
@@ -93,6 +93,50 @@
"command": "python3",
"args": ["-m", "insa_its.mcp_server"],
"description": "AI-to-AI security monitoring — anomaly detection, credential exposure, hallucination checks, forensic tracing. 23 anomaly types, OWASP MCP Top 10 coverage. 100% local. Install: pip install insa-its"
},
"playwright": {
"command": "npx",
"args": ["-y", "@playwright/mcp", "--browser", "chrome"],
"description": "Browser automation and testing via Playwright"
},
"fal-ai": {
"command": "npx",
"args": ["-y", "fal-ai-mcp-server"],
"env": {
"FAL_KEY": "YOUR_FAL_KEY_HERE"
},
"description": "AI image/video/audio generation via fal.ai models"
},
"browserbase": {
"command": "npx",
"args": ["-y", "@browserbasehq/mcp-server-browserbase"],
"env": {
"BROWSERBASE_API_KEY": "YOUR_BROWSERBASE_KEY_HERE"
},
"description": "Cloud browser sessions via Browserbase"
},
"browser-use": {
"type": "http",
"url": "https://api.browser-use.com/mcp",
"headers": {
"x-browser-use-api-key": "YOUR_BROWSER_USE_KEY_HERE"
},
"description": "AI browser agent for web tasks"
},
"token-optimizer": {
"command": "npx",
"args": ["-y", "token-optimizer-mcp"],
"description": "Token optimization for 95%+ context reduction via content deduplication and compression"
},
"confluence": {
"command": "npx",
"args": ["-y", "confluence-mcp-server"],
"env": {
"CONFLUENCE_BASE_URL": "YOUR_CONFLUENCE_URL_HERE",
"CONFLUENCE_EMAIL": "YOUR_EMAIL_HERE",
"CONFLUENCE_API_TOKEN": "YOUR_CONFLUENCE_TOKEN_HERE"
},
"description": "Confluence Cloud integration — search pages, retrieve content, explore spaces"
}
},
"_comments": {

109
package-lock.json generated
View File

@@ -18,7 +18,7 @@
"c8": "^10.1.2",
"eslint": "^9.39.2",
"globals": "^17.1.0",
"markdownlint-cli": "^0.47.0"
"markdownlint-cli": "^0.48.0"
},
"engines": {
"node": ">=18"
@@ -267,29 +267,6 @@
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -714,11 +691,10 @@
"license": "MIT"
},
"node_modules/commander": {
"version": "14.0.2",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
"integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==",
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
}
@@ -844,7 +820,6 @@
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
@@ -1588,7 +1563,6 @@
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"uc.micro": "^2.0.0"
}
@@ -1640,11 +1614,10 @@
}
},
"node_modules/markdown-it": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
"integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
@@ -1682,23 +1655,22 @@
}
},
"node_modules/markdownlint-cli": {
"version": "0.47.0",
"resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.47.0.tgz",
"integrity": "sha512-HOcxeKFAdDoldvoYDofd85vI8LgNWy8vmYpCwnlLV46PJcodmGzD7COSSBlhHwsfT4o9KrAStGodImVBus31Bg==",
"version": "0.48.0",
"resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.48.0.tgz",
"integrity": "sha512-NkZQNu2E0Q5qLEEHwWj674eYISTLD4jMHkBzDobujXd1kv+yCxi8jOaD/rZoQNW1FBBMMGQpuW5So8B51N/e0A==",
"dev": true,
"license": "MIT",
"dependencies": {
"commander": "~14.0.2",
"commander": "~14.0.3",
"deep-extend": "~0.6.0",
"ignore": "~7.0.5",
"js-yaml": "~4.1.1",
"jsonc-parser": "~3.3.1",
"jsonpointer": "~5.0.1",
"markdown-it": "~14.1.0",
"markdown-it": "~14.1.1",
"markdownlint": "~0.40.0",
"minimatch": "~10.1.1",
"minimatch": "~10.2.4",
"run-con": "~1.3.2",
"smol-toml": "~1.5.2",
"smol-toml": "~1.6.0",
"tinyglobby": "~0.2.15"
},
"bin": {
@@ -1708,6 +1680,27 @@
"node": ">=20"
}
},
"node_modules/markdownlint-cli/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/markdownlint-cli/node_modules/brace-expansion": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"dev": true,
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/markdownlint-cli/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
@@ -1719,16 +1712,15 @@
}
},
"node_modules/markdownlint-cli/node_modules/minimatch": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
"brace-expansion": "^5.0.2"
},
"engines": {
"node": "20 || >=22"
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -1738,8 +1730,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/micromark": {
"version": "4.0.2",
@@ -2278,11 +2269,10 @@
"license": "MIT"
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -2489,7 +2479,6 @@
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
@@ -2590,11 +2579,10 @@
}
},
"node_modules/smol-toml": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.5.2.tgz",
"integrity": "sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz",
"integrity": "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">= 18"
},
@@ -2812,8 +2800,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/uri-js": {
"version": "4.4.1",

View File

@@ -67,6 +67,9 @@
"scripts/hooks/",
"scripts/lib/",
"scripts/claw.js",
"scripts/orchestration-status.js",
"scripts/orchestrate-codex-worker.sh",
"scripts/orchestrate-worktrees.js",
"scripts/setup-package-manager.js",
"scripts/skill-create-output.js",
"skills/",
@@ -83,6 +86,9 @@
"postinstall": "echo '\\n ecc-universal installed!\\n Run: npx ecc-install typescript\\n Docs: https://github.com/affaan-m/everything-claude-code\\n'",
"lint": "eslint . && markdownlint '**/*.md' --ignore node_modules",
"claw": "node scripts/claw.js",
"orchestrate:status": "node scripts/orchestration-status.js",
"orchestrate:worker": "bash scripts/orchestrate-codex-worker.sh",
"orchestrate:tmux": "node scripts/orchestrate-worktrees.js",
"test": "node scripts/ci/validate-agents.js && node scripts/ci/validate-commands.js && node scripts/ci/validate-rules.js && node scripts/ci/validate-skills.js && node scripts/ci/validate-hooks.js && node scripts/ci/validate-no-personal-paths.js && node tests/run-all.js",
"coverage": "c8 --all --include=\"scripts/**/*.js\" --check-coverage --lines 80 --functions 80 --branches 80 --statements 80 --reporter=text --reporter=lcov node tests/run-all.js"
},
@@ -92,7 +98,7 @@
"c8": "^10.1.2",
"eslint": "^9.39.2",
"globals": "^17.1.0",
"markdownlint-cli": "^0.47.0"
"markdownlint-cli": "^0.48.0"
},
"engines": {
"node": ">=18"

View File

@@ -8,7 +8,8 @@ The Feature Implementation Workflow describes the development pipeline: research
0. **Research & Reuse** _(mandatory before any new implementation)_
- **GitHub code search first:** Run `gh search repos` and `gh search code` to find existing implementations, templates, and patterns before writing anything new.
- **Exa MCP for research:** Use `exa-web-search` MCP during the planning phase for broader research, data ingestion, and discovering prior art.
- **Library docs second:** Use Context7 or primary vendor docs to confirm API behavior, package usage, and version-specific details before implementing.
- **Exa only when the first two are insufficient:** Use Exa for broader web research or discovery after GitHub search and primary docs.
- **Check package registries:** Search npm, PyPI, crates.io, and other registries before writing utility code. Prefer battle-tested libraries over hand-rolled solutions.
- **Search for adaptable implementations:** Look for open-source projects that solve 80%+ of the problem and can be forked, ported, or wrapped.
- Prefer adopting or porting a proven approach over writing net-new code when it meets the requirement.

View File

@@ -16,15 +16,17 @@ const {
getDateString,
getTimeString,
getSessionIdShort,
getProjectName,
ensureDir,
readFile,
writeFile,
replaceInFile,
runCommand,
log
} = require('../lib/utils');
const SUMMARY_START_MARKER = '<!-- ECC:SUMMARY:START -->';
const SUMMARY_END_MARKER = '<!-- ECC:SUMMARY:END -->';
const SESSION_SEPARATOR = '\n---\n';
/**
* Extract a meaningful summary from the session transcript.
@@ -128,6 +130,51 @@ function runMain() {
});
}
function getSessionMetadata() {
const branchResult = runCommand('git rev-parse --abbrev-ref HEAD');
return {
project: getProjectName() || 'unknown',
branch: branchResult.success ? branchResult.output : 'unknown',
worktree: process.cwd()
};
}
function extractHeaderField(header, label) {
const match = header.match(new RegExp(`\\*\\*${escapeRegExp(label)}:\\*\\*\\s*(.+)$`, 'm'));
return match ? match[1].trim() : null;
}
function buildSessionHeader(today, currentTime, metadata, existingContent = '') {
const headingMatch = existingContent.match(/^#\s+.+$/m);
const heading = headingMatch ? headingMatch[0] : `# Session: ${today}`;
const date = extractHeaderField(existingContent, 'Date') || today;
const started = extractHeaderField(existingContent, 'Started') || currentTime;
return [
heading,
`**Date:** ${date}`,
`**Started:** ${started}`,
`**Last Updated:** ${currentTime}`,
`**Project:** ${metadata.project}`,
`**Branch:** ${metadata.branch}`,
`**Worktree:** ${metadata.worktree}`,
''
].join('\n');
}
function mergeSessionHeader(content, today, currentTime, metadata) {
const separatorIndex = content.indexOf(SESSION_SEPARATOR);
if (separatorIndex === -1) {
return null;
}
const existingHeader = content.slice(0, separatorIndex);
const body = content.slice(separatorIndex + SESSION_SEPARATOR.length);
const nextHeader = buildSessionHeader(today, currentTime, metadata, existingHeader);
return `${nextHeader}${SESSION_SEPARATOR}${body}`;
}
async function main() {
// Parse stdin JSON to get transcript_path
let transcriptPath = null;
@@ -143,6 +190,7 @@ async function main() {
const today = getDateString();
const shortId = getSessionIdShort();
const sessionFile = path.join(sessionsDir, `${today}-${shortId}-session.tmp`);
const sessionMetadata = getSessionMetadata();
ensureDir(sessionsDir);
@@ -160,42 +208,42 @@ async function main() {
}
if (fs.existsSync(sessionFile)) {
// Update existing session file
const updated = replaceInFile(
sessionFile,
/\*\*Last Updated:\*\*.*/,
`**Last Updated:** ${currentTime}`
);
if (!updated) {
log(`[SessionEnd] Failed to update timestamp in ${sessionFile}`);
const existing = readFile(sessionFile);
let updatedContent = existing;
if (existing) {
const merged = mergeSessionHeader(existing, today, currentTime, sessionMetadata);
if (merged) {
updatedContent = merged;
} else {
log(`[SessionEnd] Failed to normalize header in ${sessionFile}`);
}
}
// If we have a new summary, update only the generated summary block.
// This keeps repeated Stop invocations idempotent and preserves
// user-authored sections in the same session file.
if (summary) {
const existing = readFile(sessionFile);
if (existing) {
const summaryBlock = buildSummaryBlock(summary);
let updatedContent = existing;
if (summary && updatedContent) {
const summaryBlock = buildSummaryBlock(summary);
if (existing.includes(SUMMARY_START_MARKER) && existing.includes(SUMMARY_END_MARKER)) {
updatedContent = existing.replace(
new RegExp(`${escapeRegExp(SUMMARY_START_MARKER)}[\\s\\S]*?${escapeRegExp(SUMMARY_END_MARKER)}`),
summaryBlock
);
} else {
// Migration path for files created before summary markers existed.
updatedContent = existing.replace(
/## (?:Session Summary|Current State)[\s\S]*?$/,
`${summaryBlock}\n\n### Notes for Next Session\n-\n\n### Context to Load\n\`\`\`\n[relevant files]\n\`\`\`\n`
);
}
writeFile(sessionFile, updatedContent);
if (updatedContent.includes(SUMMARY_START_MARKER) && updatedContent.includes(SUMMARY_END_MARKER)) {
updatedContent = updatedContent.replace(
new RegExp(`${escapeRegExp(SUMMARY_START_MARKER)}[\\s\\S]*?${escapeRegExp(SUMMARY_END_MARKER)}`),
summaryBlock
);
} else {
// Migration path for files created before summary markers existed.
updatedContent = updatedContent.replace(
/## (?:Session Summary|Current State)[\s\S]*?$/,
`${summaryBlock}\n\n### Notes for Next Session\n-\n\n### Context to Load\n\`\`\`\n[relevant files]\n\`\`\`\n`
);
}
}
if (updatedContent) {
writeFile(sessionFile, updatedContent);
}
log(`[SessionEnd] Updated session file: ${sessionFile}`);
} else {
// Create new session file
@@ -203,14 +251,7 @@ async function main() {
? `${buildSummaryBlock(summary)}\n\n### Notes for Next Session\n-\n\n### Context to Load\n\`\`\`\n[relevant files]\n\`\`\``
: `## Current State\n\n[Session context goes here]\n\n### Completed\n- [ ]\n\n### In Progress\n- [ ]\n\n### Notes for Next Session\n-\n\n### Context to Load\n\`\`\`\n[relevant files]\n\`\`\``;
const template = `# Session: ${today}
**Date:** ${today}
**Started:** ${currentTime}
**Last Updated:** ${currentTime}
---
${summarySection}
const template = `${buildSessionHeader(today, currentTime, sessionMetadata)}${SESSION_SEPARATOR}${summarySection}
`;
writeFile(sessionFile, template);

View File

@@ -0,0 +1,363 @@
'use strict';
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
function stripCodeTicks(value) {
if (typeof value !== 'string') {
return value;
}
const trimmed = value.trim();
if (trimmed.startsWith('`') && trimmed.endsWith('`') && trimmed.length >= 2) {
return trimmed.slice(1, -1);
}
return trimmed;
}
function normalizeSessionName(value, fallback = 'session') {
const normalized = String(value || '')
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
return normalized || fallback;
}
function parseSection(content, heading) {
if (typeof content !== 'string' || content.length === 0) {
return '';
}
const lines = content.split('\n');
const headingLines = new Set([`## ${heading}`, `**${heading}**`]);
const startIndex = lines.findIndex(line => headingLines.has(line.trim()));
if (startIndex === -1) {
return '';
}
const collected = [];
for (let index = startIndex + 1; index < lines.length; index += 1) {
const line = lines[index];
const trimmed = line.trim();
if (trimmed.startsWith('## ') || (/^\*\*.+\*\*$/.test(trimmed) && !headingLines.has(trimmed))) {
break;
}
collected.push(line);
}
return collected.join('\n').trim();
}
function parseBullets(section) {
if (!section) {
return [];
}
return section
.split('\n')
.map(line => line.trim())
.filter(line => line.startsWith('- '))
.map(line => stripCodeTicks(line.replace(/^- /, '').trim()));
}
function parseWorkerStatus(content) {
const status = {
state: null,
updated: null,
branch: null,
worktree: null,
taskFile: null,
handoffFile: null
};
if (typeof content !== 'string' || content.length === 0) {
return status;
}
for (const line of content.split('\n')) {
const match = line.match(/^- ([A-Za-z ]+):\s*(.+)$/);
if (!match) {
continue;
}
const key = match[1].trim().toLowerCase().replace(/\s+/g, '');
const value = stripCodeTicks(match[2]);
if (key === 'state') status.state = value;
if (key === 'updated') status.updated = value;
if (key === 'branch') status.branch = value;
if (key === 'worktree') status.worktree = value;
if (key === 'taskfile') status.taskFile = value;
if (key === 'handofffile') status.handoffFile = value;
}
return status;
}
function parseWorkerTask(content) {
return {
objective: parseSection(content, 'Objective'),
seedPaths: parseBullets(parseSection(content, 'Seeded Local Overlays'))
};
}
function parseFirstSection(content, headings) {
for (const heading of headings) {
const section = parseSection(content, heading);
if (section) {
return section;
}
}
return '';
}
function parseWorkerHandoff(content) {
return {
summary: parseBullets(parseFirstSection(content, ['Summary'])),
validation: parseBullets(parseFirstSection(content, [
'Validation',
'Tests / Verification',
'Tests',
'Verification'
])),
remainingRisks: parseBullets(parseFirstSection(content, [
'Remaining Risks',
'Follow-ups',
'Follow Ups'
]))
};
}
function readTextIfExists(filePath) {
if (!filePath || !fs.existsSync(filePath)) {
return '';
}
return fs.readFileSync(filePath, 'utf8');
}
function listWorkerDirectories(coordinationDir) {
if (!coordinationDir || !fs.existsSync(coordinationDir)) {
return [];
}
return fs.readdirSync(coordinationDir, { withFileTypes: true })
.filter(entry => entry.isDirectory())
.filter(entry => {
const workerDir = path.join(coordinationDir, entry.name);
return ['status.md', 'task.md', 'handoff.md']
.some(filename => fs.existsSync(path.join(workerDir, filename)));
})
.map(entry => entry.name)
.sort();
}
function loadWorkerSnapshots(coordinationDir) {
return listWorkerDirectories(coordinationDir).map(workerSlug => {
const workerDir = path.join(coordinationDir, workerSlug);
const statusPath = path.join(workerDir, 'status.md');
const taskPath = path.join(workerDir, 'task.md');
const handoffPath = path.join(workerDir, 'handoff.md');
const status = parseWorkerStatus(readTextIfExists(statusPath));
const task = parseWorkerTask(readTextIfExists(taskPath));
const handoff = parseWorkerHandoff(readTextIfExists(handoffPath));
return {
workerSlug,
workerDir,
status,
task,
handoff,
files: {
status: statusPath,
task: taskPath,
handoff: handoffPath
}
};
});
}
function listTmuxPanes(sessionName) {
const format = [
'#{pane_id}',
'#{window_index}',
'#{pane_index}',
'#{pane_title}',
'#{pane_current_command}',
'#{pane_current_path}',
'#{pane_active}',
'#{pane_dead}',
'#{pane_pid}'
].join('\t');
const result = spawnSync('tmux', ['list-panes', '-t', sessionName, '-F', format], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
return [];
}
return (result.stdout || '')
.split('\n')
.map(line => line.trim())
.filter(Boolean)
.map(line => {
const [
paneId,
windowIndex,
paneIndex,
title,
currentCommand,
currentPath,
active,
dead,
pid
] = line.split('\t');
return {
paneId,
windowIndex: Number(windowIndex),
paneIndex: Number(paneIndex),
title,
currentCommand,
currentPath,
active: active === '1',
dead: dead === '1',
pid: pid ? Number(pid) : null
};
});
}
function summarizeWorkerStates(workers) {
return workers.reduce((counts, worker) => {
const state = worker.status.state || 'unknown';
counts[state] = (counts[state] || 0) + 1;
return counts;
}, {});
}
function buildSessionSnapshot({ sessionName, coordinationDir, panes }) {
const workerSnapshots = loadWorkerSnapshots(coordinationDir);
const paneMap = new Map(panes.map(pane => [pane.title, pane]));
const workers = workerSnapshots.map(worker => ({
...worker,
pane: paneMap.get(worker.workerSlug) || null
}));
return {
sessionName,
coordinationDir,
sessionActive: panes.length > 0,
paneCount: panes.length,
workerCount: workers.length,
workerStates: summarizeWorkerStates(workers),
panes,
workers
};
}
function readPlanConfig(absoluteTarget) {
let config;
try {
config = JSON.parse(fs.readFileSync(absoluteTarget, 'utf8'));
} catch (_error) {
throw new Error(`Invalid orchestration plan JSON: ${absoluteTarget}`);
}
if (!config || Array.isArray(config) || typeof config !== 'object') {
throw new Error(`Invalid orchestration plan: expected a JSON object (${absoluteTarget})`);
}
return config;
}
function readPlanString(config, key, absoluteTarget) {
const value = config[key];
if (value === undefined) {
return undefined;
}
if (typeof value !== 'string') {
throw new Error(`Invalid orchestration plan: ${key} must be a string when provided (${absoluteTarget})`);
}
const normalized = value.trim();
return normalized.length > 0 ? normalized : undefined;
}
function resolveSnapshotTarget(targetPath, cwd = process.cwd()) {
const absoluteTarget = path.resolve(cwd, targetPath);
if (fs.existsSync(absoluteTarget) && fs.statSync(absoluteTarget).isFile()) {
const config = readPlanConfig(absoluteTarget);
const repoRoot = path.resolve(readPlanString(config, 'repoRoot', absoluteTarget) || cwd);
const sessionName = normalizeSessionName(
readPlanString(config, 'sessionName', absoluteTarget) || path.basename(repoRoot),
'session'
);
const coordinationRoot = path.resolve(
readPlanString(config, 'coordinationRoot', absoluteTarget) || path.join(repoRoot, '.orchestration')
);
return {
sessionName,
coordinationDir: path.join(coordinationRoot, sessionName),
repoRoot,
targetType: 'plan'
};
}
const repoRoot = path.resolve(cwd);
const sessionName = normalizeSessionName(targetPath, path.basename(repoRoot));
return {
sessionName,
coordinationDir: path.join(repoRoot, '.orchestration', sessionName),
repoRoot,
targetType: 'session'
};
}
function collectSessionSnapshot(targetPath, cwd = process.cwd()) {
const target = resolveSnapshotTarget(targetPath, cwd);
const panes = listTmuxPanes(target.sessionName);
const snapshot = buildSessionSnapshot({
sessionName: target.sessionName,
coordinationDir: target.coordinationDir,
panes
});
return {
...snapshot,
repoRoot: target.repoRoot,
targetType: target.targetType
};
}
module.exports = {
buildSessionSnapshot,
collectSessionSnapshot,
listTmuxPanes,
loadWorkerSnapshots,
normalizeText: stripCodeTicks,
parseWorkerHandoff,
parseWorkerStatus,
parseWorkerTask,
resolveSnapshotTarget
};

View File

@@ -17,11 +17,12 @@ const {
} = require('./utils');
// Session filename pattern: YYYY-MM-DD-[session-id]-session.tmp
// The session-id is optional (old format) and can include lowercase
// alphanumeric characters and hyphens, with a minimum length of 8.
// The session-id is optional (old format) and can include letters, digits,
// underscores, and hyphens, but must not start with a hyphen.
// Matches: "2026-02-01-session.tmp", "2026-02-01-a1b2c3d4-session.tmp",
// and "2026-02-01-frontend-worktree-1-session.tmp"
const SESSION_FILENAME_REGEX = /^(\d{4}-\d{2}-\d{2})(?:-([a-z0-9-]{8,}))?-session\.tmp$/;
// "2026-02-01-frontend-worktree-1-session.tmp", and
// "2026-02-01-ChezMoi_2-session.tmp"
const SESSION_FILENAME_REGEX = /^(\d{4}-\d{2}-\d{2})(?:-([a-zA-Z0-9_][a-zA-Z0-9_-]*))?-session\.tmp$/;
/**
* Parse session filename to extract metadata
@@ -85,6 +86,9 @@ function parseSessionMetadata(content) {
date: null,
started: null,
lastUpdated: null,
project: null,
branch: null,
worktree: null,
completed: [],
inProgress: [],
notes: '',
@@ -117,6 +121,22 @@ function parseSessionMetadata(content) {
metadata.lastUpdated = updatedMatch[1];
}
// Extract control-plane metadata
const projectMatch = content.match(/\*\*Project:\*\*\s*(.+)$/m);
if (projectMatch) {
metadata.project = projectMatch[1].trim();
}
const branchMatch = content.match(/\*\*Branch:\*\*\s*(.+)$/m);
if (branchMatch) {
metadata.branch = branchMatch[1].trim();
}
const worktreeMatch = content.match(/\*\*Worktree:\*\*\s*(.+)$/m);
if (worktreeMatch) {
metadata.worktree = worktreeMatch[1].trim();
}
// Extract completed items
const completedSection = content.match(/### Completed\s*\n([\s\S]*?)(?=###|\n\n|$)/);
if (completedSection) {

View File

@@ -0,0 +1,521 @@
'use strict';
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
function slugify(value, fallback = 'worker') {
const normalized = String(value || '')
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
return normalized || fallback;
}
function renderTemplate(template, variables) {
if (typeof template !== 'string' || template.trim().length === 0) {
throw new Error('launcherCommand must be a non-empty string');
}
return template.replace(/\{([a-z_]+)\}/g, (match, key) => {
if (!(key in variables)) {
throw new Error(`Unknown template variable: ${key}`);
}
return String(variables[key]);
});
}
function shellQuote(value) {
return `'${String(value).replace(/'/g, `'\\''`)}'`;
}
function formatCommand(program, args) {
return [program, ...args.map(shellQuote)].join(' ');
}
function buildTemplateVariables(values) {
return Object.entries(values).reduce((accumulator, [key, value]) => {
const stringValue = String(value);
const quotedValue = shellQuote(stringValue);
accumulator[key] = stringValue;
accumulator[`${key}_raw`] = stringValue;
accumulator[`${key}_sh`] = quotedValue;
return accumulator;
}, {});
}
function normalizeSeedPaths(seedPaths, repoRoot) {
const resolvedRepoRoot = path.resolve(repoRoot);
const entries = Array.isArray(seedPaths) ? seedPaths : [];
const seen = new Set();
const normalized = [];
for (const entry of entries) {
if (typeof entry !== 'string' || entry.trim().length === 0) {
continue;
}
const absolutePath = path.resolve(resolvedRepoRoot, entry);
const relativePath = path.relative(resolvedRepoRoot, absolutePath);
if (
relativePath.startsWith('..') ||
path.isAbsolute(relativePath)
) {
throw new Error(`seedPaths entries must stay inside repoRoot: ${entry}`);
}
const normalizedPath = relativePath.split(path.sep).join('/');
if (!normalizedPath || normalizedPath === '.') {
throw new Error('seedPaths entries must not target the repo root');
}
if (normalizedPath === '.git' || normalizedPath.startsWith('.git/')) {
throw new Error(`seedPaths entries must not target git metadata: ${entry}`);
}
if (seen.has(normalizedPath)) {
continue;
}
seen.add(normalizedPath);
normalized.push(normalizedPath);
}
return normalized;
}
function overlaySeedPaths({ repoRoot, seedPaths, worktreePath }) {
const normalizedSeedPaths = normalizeSeedPaths(seedPaths, repoRoot);
for (const seedPath of normalizedSeedPaths) {
const sourcePath = path.join(repoRoot, seedPath);
const destinationPath = path.join(worktreePath, seedPath);
if (!fs.existsSync(sourcePath)) {
throw new Error(`Seed path does not exist in repoRoot: ${seedPath}`);
}
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.rmSync(destinationPath, { force: true, recursive: true });
fs.cpSync(sourcePath, destinationPath, {
dereference: false,
force: true,
preserveTimestamps: true,
recursive: true
});
}
}
function buildWorkerArtifacts(workerPlan) {
const seededPathsSection = workerPlan.seedPaths.length > 0
? [
'',
'## Seeded Local Overlays',
...workerPlan.seedPaths.map(seedPath => `- \`${seedPath}\``)
]
: [];
return {
dir: workerPlan.coordinationDir,
files: [
{
path: workerPlan.taskFilePath,
content: [
`# Worker Task: ${workerPlan.workerName}`,
'',
`- Session: \`${workerPlan.sessionName}\``,
`- Repo root: \`${workerPlan.repoRoot}\``,
`- Worktree: \`${workerPlan.worktreePath}\``,
`- Branch: \`${workerPlan.branchName}\``,
`- Launcher status file: \`${workerPlan.statusFilePath}\``,
`- Launcher handoff file: \`${workerPlan.handoffFilePath}\``,
...seededPathsSection,
'',
'## Objective',
workerPlan.task,
'',
'## Completion',
'Do not spawn subagents or external agents for this task.',
'Report results in your final response.',
'Respond with these exact sections so orchestration parsing can succeed:',
'## Summary',
'- ...',
'## Validation',
'- ...',
'## Remaining Risks',
'- ...',
`The worker launcher captures your response in \`${workerPlan.handoffFilePath}\` automatically.`,
`The worker launcher updates \`${workerPlan.statusFilePath}\` automatically.`
].join('\n')
},
{
path: workerPlan.handoffFilePath,
content: [
`# Handoff: ${workerPlan.workerName}`,
'',
'## Summary',
'- Pending',
'',
'## Validation',
'- Pending',
'',
'## Remaining Risks',
'- Pending'
].join('\n')
},
{
path: workerPlan.statusFilePath,
content: [
`# Status: ${workerPlan.workerName}`,
'',
'- State: not started',
`- Worktree: \`${workerPlan.worktreePath}\``,
`- Branch: \`${workerPlan.branchName}\``
].join('\n')
}
]
};
}
function buildOrchestrationPlan(config = {}) {
const repoRoot = path.resolve(config.repoRoot || process.cwd());
const repoName = path.basename(repoRoot);
const workers = Array.isArray(config.workers) ? config.workers : [];
const globalSeedPaths = normalizeSeedPaths(config.seedPaths, repoRoot);
const sessionName = slugify(config.sessionName || repoName, 'session');
const worktreeRoot = path.resolve(config.worktreeRoot || path.dirname(repoRoot));
const coordinationRoot = path.resolve(
config.coordinationRoot || path.join(repoRoot, '.orchestration')
);
const coordinationDir = path.join(coordinationRoot, sessionName);
const baseRef = config.baseRef || 'HEAD';
const defaultLauncher = config.launcherCommand || '';
if (workers.length === 0) {
throw new Error('buildOrchestrationPlan requires at least one worker');
}
const seenWorkerSlugs = new Map();
const workerPlans = workers.map((worker, index) => {
if (!worker || typeof worker.task !== 'string' || worker.task.trim().length === 0) {
throw new Error(`Worker ${index + 1} is missing a task`);
}
const workerName = worker.name || `worker-${index + 1}`;
const workerSlug = slugify(workerName, `worker-${index + 1}`);
if (seenWorkerSlugs.has(workerSlug)) {
const firstWorkerName = seenWorkerSlugs.get(workerSlug);
throw new Error(
`Worker names must map to unique slugs: ${workerSlug} (${firstWorkerName}, ${workerName})`
);
}
seenWorkerSlugs.set(workerSlug, workerName);
const branchName = `orchestrator-${sessionName}-${workerSlug}`;
const worktreePath = path.join(worktreeRoot, `${repoName}-${sessionName}-${workerSlug}`);
const workerCoordinationDir = path.join(coordinationDir, workerSlug);
const taskFilePath = path.join(workerCoordinationDir, 'task.md');
const handoffFilePath = path.join(workerCoordinationDir, 'handoff.md');
const statusFilePath = path.join(workerCoordinationDir, 'status.md');
const launcherCommand = worker.launcherCommand || defaultLauncher;
const workerSeedPaths = normalizeSeedPaths(worker.seedPaths, repoRoot);
const seedPaths = normalizeSeedPaths([...globalSeedPaths, ...workerSeedPaths], repoRoot);
const templateVariables = buildTemplateVariables({
branch_name: branchName,
handoff_file: handoffFilePath,
repo_root: repoRoot,
session_name: sessionName,
status_file: statusFilePath,
task_file: taskFilePath,
worker_name: workerName,
worker_slug: workerSlug,
worktree_path: worktreePath
});
if (!launcherCommand) {
throw new Error(`Worker ${workerName} is missing a launcherCommand`);
}
const gitArgs = ['worktree', 'add', '-b', branchName, worktreePath, baseRef];
return {
branchName,
coordinationDir: workerCoordinationDir,
gitArgs,
gitCommand: formatCommand('git', gitArgs),
handoffFilePath,
launchCommand: renderTemplate(launcherCommand, templateVariables),
repoRoot,
sessionName,
seedPaths,
statusFilePath,
task: worker.task.trim(),
taskFilePath,
workerName,
workerSlug,
worktreePath
};
});
const tmuxCommands = [
{
cmd: 'tmux',
args: ['new-session', '-d', '-s', sessionName, '-n', 'orchestrator', '-c', repoRoot],
description: 'Create detached tmux session'
},
{
cmd: 'tmux',
args: [
'send-keys',
'-t',
sessionName,
`printf '%s\\n' 'Session: ${sessionName}' 'Coordination: ${coordinationDir}'`,
'C-m'
],
description: 'Print orchestrator session details'
}
];
for (const workerPlan of workerPlans) {
tmuxCommands.push(
{
cmd: 'tmux',
args: ['split-window', '-d', '-t', sessionName, '-c', workerPlan.worktreePath],
description: `Create pane for ${workerPlan.workerName}`
},
{
cmd: 'tmux',
args: ['select-layout', '-t', sessionName, 'tiled'],
description: 'Arrange panes in tiled layout'
},
{
cmd: 'tmux',
args: ['select-pane', '-t', '<pane-id>', '-T', workerPlan.workerSlug],
description: `Label pane ${workerPlan.workerSlug}`
},
{
cmd: 'tmux',
args: [
'send-keys',
'-t',
'<pane-id>',
`cd ${shellQuote(workerPlan.worktreePath)} && ${workerPlan.launchCommand}`,
'C-m'
],
description: `Launch worker ${workerPlan.workerName}`
}
);
}
return {
baseRef,
coordinationDir,
replaceExisting: Boolean(config.replaceExisting),
repoRoot,
sessionName,
tmuxCommands,
workerPlans
};
}
function materializePlan(plan) {
for (const workerPlan of plan.workerPlans) {
const artifacts = buildWorkerArtifacts(workerPlan);
fs.mkdirSync(artifacts.dir, { recursive: true });
for (const file of artifacts.files) {
fs.writeFileSync(file.path, file.content + '\n', 'utf8');
}
}
}
function runCommand(program, args, options = {}) {
const result = spawnSync(program, args, {
cwd: options.cwd,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
const stderr = (result.stderr || '').trim();
throw new Error(`${program} ${args.join(' ')} failed${stderr ? `: ${stderr}` : ''}`);
}
return result;
}
function commandSucceeds(program, args, options = {}) {
const result = spawnSync(program, args, {
cwd: options.cwd,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
});
return result.status === 0;
}
function canonicalizePath(targetPath) {
const resolvedPath = path.resolve(targetPath);
try {
return fs.realpathSync.native(resolvedPath);
} catch (_error) {
const parentPath = path.dirname(resolvedPath);
try {
return path.join(fs.realpathSync.native(parentPath), path.basename(resolvedPath));
} catch (_parentError) {
return resolvedPath;
}
}
}
function branchExists(repoRoot, branchName) {
return commandSucceeds('git', ['show-ref', '--verify', '--quiet', `refs/heads/${branchName}`], {
cwd: repoRoot
});
}
function listWorktrees(repoRoot) {
const listed = runCommand('git', ['worktree', 'list', '--porcelain'], { cwd: repoRoot });
const lines = (listed.stdout || '').split('\n');
const worktrees = [];
for (const line of lines) {
if (line.startsWith('worktree ')) {
const listedPath = line.slice('worktree '.length).trim();
worktrees.push({
listedPath,
canonicalPath: canonicalizePath(listedPath)
});
}
}
return worktrees;
}
function cleanupExisting(plan) {
runCommand('git', ['worktree', 'prune', '--expire', 'now'], { cwd: plan.repoRoot });
const hasSession = spawnSync('tmux', ['has-session', '-t', plan.sessionName], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
});
if (hasSession.status === 0) {
runCommand('tmux', ['kill-session', '-t', plan.sessionName], { cwd: plan.repoRoot });
}
for (const workerPlan of plan.workerPlans) {
const expectedWorktreePath = canonicalizePath(workerPlan.worktreePath);
const existingWorktree = listWorktrees(plan.repoRoot).find(
worktree => worktree.canonicalPath === expectedWorktreePath
);
if (existingWorktree) {
runCommand('git', ['worktree', 'remove', '--force', existingWorktree.listedPath], {
cwd: plan.repoRoot
});
}
if (fs.existsSync(workerPlan.worktreePath)) {
fs.rmSync(workerPlan.worktreePath, { force: true, recursive: true });
}
runCommand('git', ['worktree', 'prune', '--expire', 'now'], { cwd: plan.repoRoot });
if (branchExists(plan.repoRoot, workerPlan.branchName)) {
runCommand('git', ['branch', '-D', workerPlan.branchName], { cwd: plan.repoRoot });
}
}
}
function executePlan(plan) {
runCommand('git', ['rev-parse', '--is-inside-work-tree'], { cwd: plan.repoRoot });
runCommand('tmux', ['-V']);
if (plan.replaceExisting) {
cleanupExisting(plan);
} else {
const hasSession = spawnSync('tmux', ['has-session', '-t', plan.sessionName], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
});
if (hasSession.status === 0) {
throw new Error(`tmux session already exists: ${plan.sessionName}`);
}
}
materializePlan(plan);
for (const workerPlan of plan.workerPlans) {
runCommand('git', workerPlan.gitArgs, { cwd: plan.repoRoot });
overlaySeedPaths({
repoRoot: plan.repoRoot,
seedPaths: workerPlan.seedPaths,
worktreePath: workerPlan.worktreePath
});
}
runCommand(
'tmux',
['new-session', '-d', '-s', plan.sessionName, '-n', 'orchestrator', '-c', plan.repoRoot],
{ cwd: plan.repoRoot }
);
runCommand(
'tmux',
[
'send-keys',
'-t',
plan.sessionName,
`printf '%s\\n' 'Session: ${plan.sessionName}' 'Coordination: ${plan.coordinationDir}'`,
'C-m'
],
{ cwd: plan.repoRoot }
);
for (const workerPlan of plan.workerPlans) {
const splitResult = runCommand(
'tmux',
['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', plan.sessionName, '-c', workerPlan.worktreePath],
{ cwd: plan.repoRoot }
);
const paneId = splitResult.stdout.trim();
if (!paneId) {
throw new Error(`tmux split-window did not return a pane id for ${workerPlan.workerName}`);
}
runCommand('tmux', ['select-layout', '-t', plan.sessionName, 'tiled'], { cwd: plan.repoRoot });
runCommand('tmux', ['select-pane', '-t', paneId, '-T', workerPlan.workerSlug], {
cwd: plan.repoRoot
});
runCommand(
'tmux',
[
'send-keys',
'-t',
paneId,
`cd ${shellQuote(workerPlan.worktreePath)} && ${workerPlan.launchCommand}`,
'C-m'
],
{ cwd: plan.repoRoot }
);
}
return {
coordinationDir: plan.coordinationDir,
sessionName: plan.sessionName,
workerCount: plan.workerPlans.length
};
}
module.exports = {
buildOrchestrationPlan,
executePlan,
materializePlan,
normalizeSeedPaths,
overlaySeedPaths,
renderTemplate,
slugify
};

View File

@@ -0,0 +1,92 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 3 ]]; then
echo "Usage: bash scripts/orchestrate-codex-worker.sh <task-file> <handoff-file> <status-file>" >&2
exit 1
fi
task_file="$1"
handoff_file="$2"
status_file="$3"
timestamp() {
date -u +"%Y-%m-%dT%H:%M:%SZ"
}
write_status() {
local state="$1"
local details="$2"
cat > "$status_file" <<EOF
# Status
- State: $state
- Updated: $(timestamp)
- Branch: $(git rev-parse --abbrev-ref HEAD)
- Worktree: \`$(pwd)\`
$details
EOF
}
mkdir -p "$(dirname "$handoff_file")" "$(dirname "$status_file")"
write_status "running" "- Task file: \`$task_file\`"
prompt_file="$(mktemp)"
output_file="$(mktemp)"
cleanup() {
rm -f "$prompt_file" "$output_file"
}
trap cleanup EXIT
cat > "$prompt_file" <<EOF
You are one worker in an ECC tmux/worktree swarm.
Rules:
- Work only in the current git worktree.
- Do not touch sibling worktrees or the parent repo checkout.
- Complete the task from the task file below.
- Do not spawn subagents or external agents for this task.
- Report progress and final results in stdout only.
- Do not write handoff or status files yourself; the launcher manages those artifacts.
- If you change code or docs, keep the scope narrow and defensible.
- In your final response, include these exact sections:
1. Summary
2. Validation
3. Remaining Risks
- You may include Files Changed if useful, but keep the three sections above exact.
Task file: $task_file
$(cat "$task_file")
EOF
if codex exec -p yolo -m gpt-5.4 --color never -C "$(pwd)" -o "$output_file" - < "$prompt_file"; then
{
echo "# Handoff"
echo
echo "- Completed: $(timestamp)"
echo "- Branch: \`$(git rev-parse --abbrev-ref HEAD)\`"
echo "- Worktree: \`$(pwd)\`"
echo
cat "$output_file"
echo
echo "## Git Status"
echo
git status --short
} > "$handoff_file"
write_status "completed" "- Handoff file: \`$handoff_file\`"
else
{
echo "# Handoff"
echo
echo "- Failed: $(timestamp)"
echo "- Branch: \`$(git rev-parse --abbrev-ref HEAD)\`"
echo "- Worktree: \`$(pwd)\`"
echo
echo "The Codex worker exited with a non-zero status."
} > "$handoff_file"
write_status "failed" "- Handoff file: \`$handoff_file\`"
exit 1
fi

View File

@@ -0,0 +1,111 @@
#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
const {
buildOrchestrationPlan,
executePlan,
materializePlan
} = require('./lib/tmux-worktree-orchestrator');
function usage() {
console.log([
'Usage:',
' node scripts/orchestrate-worktrees.js <plan.json> [--execute]',
' node scripts/orchestrate-worktrees.js <plan.json> [--write-only]',
'',
'Placeholders supported in launcherCommand:',
' Raw defaults: {worker_name} {worker_slug} {session_name} {repo_root}',
' Raw defaults: {worktree_path} {branch_name} {task_file} {handoff_file} {status_file}',
' Shell-safe aliases: {worker_name_sh} {worker_slug_sh} {session_name_sh} {repo_root_sh}',
' Shell-safe aliases: {worktree_path_sh} {branch_name_sh} {task_file_sh} {handoff_file_sh} {status_file_sh}',
' Explicit raw aliases also exist with the _raw suffix.',
'',
'Without flags the script prints a dry-run plan only.'
].join('\n'));
}
function parseArgs(argv) {
const args = argv.slice(2);
const planPath = args.find(arg => !arg.startsWith('--'));
return {
execute: args.includes('--execute'),
planPath,
writeOnly: args.includes('--write-only')
};
}
function loadPlanConfig(planPath) {
const absolutePath = path.resolve(planPath);
const raw = fs.readFileSync(absolutePath, 'utf8');
const config = JSON.parse(raw);
config.repoRoot = config.repoRoot || process.cwd();
return { absolutePath, config };
}
function printDryRun(plan, absolutePath) {
const preview = {
planFile: absolutePath,
sessionName: plan.sessionName,
repoRoot: plan.repoRoot,
coordinationDir: plan.coordinationDir,
workers: plan.workerPlans.map(worker => ({
workerName: worker.workerName,
branchName: worker.branchName,
worktreePath: worker.worktreePath,
seedPaths: worker.seedPaths,
taskFilePath: worker.taskFilePath,
handoffFilePath: worker.handoffFilePath,
launchCommand: worker.launchCommand
})),
commands: [
...plan.workerPlans.map(worker => worker.gitCommand),
...plan.tmuxCommands.map(command => [command.cmd, ...command.args].join(' '))
]
};
console.log(JSON.stringify(preview, null, 2));
}
function main() {
const { execute, planPath, writeOnly } = parseArgs(process.argv);
if (!planPath) {
usage();
process.exit(1);
}
const { absolutePath, config } = loadPlanConfig(planPath);
const plan = buildOrchestrationPlan(config);
if (writeOnly) {
materializePlan(plan);
console.log(`Wrote orchestration files to ${plan.coordinationDir}`);
return;
}
if (!execute) {
printDryRun(plan, absolutePath);
return;
}
const result = executePlan(plan);
console.log([
`Started tmux session '${result.sessionName}' with ${result.workerCount} worker panes.`,
`Coordination files: ${result.coordinationDir}`,
`Attach with: tmux attach -t ${result.sessionName}`
].join('\n'));
}
if (require.main === module) {
try {
main();
} catch (error) {
console.error(`[orchestrate-worktrees] ${error.message}`);
process.exit(1);
}
}
module.exports = { main };

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
const { collectSessionSnapshot } = require('./lib/orchestration-session');
function usage() {
console.log([
'Usage:',
' node scripts/orchestration-status.js <session-name|plan.json> [--write <output.json>]',
'',
'Examples:',
' node scripts/orchestration-status.js workflow-visual-proof',
' node scripts/orchestration-status.js .claude/plan/workflow-visual-proof.json',
' node scripts/orchestration-status.js .claude/plan/workflow-visual-proof.json --write /tmp/snapshot.json'
].join('\n'));
}
function parseArgs(argv) {
const args = argv.slice(2);
let target = null;
let writePath = null;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === '--write') {
const candidate = args[index + 1];
if (!candidate || candidate.startsWith('--')) {
throw new Error('--write requires an output path');
}
writePath = candidate;
index += 1;
continue;
}
if (arg.startsWith('--')) {
throw new Error(`Unknown flag: ${arg}`);
}
if (target) {
throw new Error('Expected a single session name or plan path');
}
target = arg;
}
return { target, writePath };
}
function main() {
const { target, writePath } = parseArgs(process.argv);
if (!target) {
usage();
process.exit(1);
}
const snapshot = collectSessionSnapshot(target, process.cwd());
const json = JSON.stringify(snapshot, null, 2);
if (writePath) {
const absoluteWritePath = path.resolve(writePath);
fs.mkdirSync(path.dirname(absoluteWritePath), { recursive: true });
fs.writeFileSync(absoluteWritePath, json + '\n', 'utf8');
}
console.log(json);
}
if (require.main === module) {
try {
main();
} catch (error) {
console.error(`[orchestration-status] ${error.message}`);
process.exit(1);
}
}
module.exports = { main, parseArgs };

337
skills/claude-api/SKILL.md Normal file
View File

@@ -0,0 +1,337 @@
---
name: claude-api
description: Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.
origin: ECC
---
# Claude API
Build applications with the Anthropic Claude API and SDKs.
## When to Activate
- Building applications that call the Claude API
- Code imports `anthropic` (Python) or `@anthropic-ai/sdk` (TypeScript)
- User asks about Claude API patterns, tool use, streaming, or vision
- Implementing agent workflows with Claude Agent SDK
- Optimizing API costs, token usage, or latency
## Model Selection
| Model | ID | Best For |
|-------|-----|----------|
| Opus 4.1 | `claude-opus-4-1` | Complex reasoning, architecture, research |
| Sonnet 4 | `claude-sonnet-4-0` | Balanced coding, most development tasks |
| Haiku 3.5 | `claude-3-5-haiku-latest` | Fast responses, high-volume, cost-sensitive |
Default to Sonnet 4 unless the task requires deep reasoning (Opus) or speed/cost optimization (Haiku). For production, prefer pinned snapshot IDs over aliases.
## Python SDK
### Installation
```bash
pip install anthropic
```
### Basic Message
```python
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from env
message = client.messages.create(
model="claude-sonnet-4-0",
max_tokens=1024,
messages=[
{"role": "user", "content": "Explain async/await in Python"}
]
)
print(message.content[0].text)
```
### Streaming
```python
with client.messages.stream(
model="claude-sonnet-4-0",
max_tokens=1024,
messages=[{"role": "user", "content": "Write a haiku about coding"}]
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
```
### System Prompt
```python
message = client.messages.create(
model="claude-sonnet-4-0",
max_tokens=1024,
system="You are a senior Python developer. Be concise.",
messages=[{"role": "user", "content": "Review this function"}]
)
```
## TypeScript SDK
### Installation
```bash
npm install @anthropic-ai/sdk
```
### Basic Message
```typescript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic(); // reads ANTHROPIC_API_KEY from env
const message = await client.messages.create({
model: "claude-sonnet-4-0",
max_tokens: 1024,
messages: [
{ role: "user", content: "Explain async/await in TypeScript" }
],
});
console.log(message.content[0].text);
```
### Streaming
```typescript
const stream = client.messages.stream({
model: "claude-sonnet-4-0",
max_tokens: 1024,
messages: [{ role: "user", content: "Write a haiku" }],
});
for await (const event of stream) {
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
process.stdout.write(event.delta.text);
}
}
```
## Tool Use
Define tools and let Claude call them:
```python
tools = [
{
"name": "get_weather",
"description": "Get current weather for a location",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City name"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["location"]
}
}
]
message = client.messages.create(
model="claude-sonnet-4-0",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": "What's the weather in SF?"}]
)
# Handle tool use response
for block in message.content:
if block.type == "tool_use":
# Execute the tool with block.input
result = get_weather(**block.input)
# Send result back
follow_up = client.messages.create(
model="claude-sonnet-4-0",
max_tokens=1024,
tools=tools,
messages=[
{"role": "user", "content": "What's the weather in SF?"},
{"role": "assistant", "content": message.content},
{"role": "user", "content": [
{"type": "tool_result", "tool_use_id": block.id, "content": str(result)}
]}
]
)
```
## Vision
Send images for analysis:
```python
import base64
with open("diagram.png", "rb") as f:
image_data = base64.standard_b64encode(f.read()).decode("utf-8")
message = client.messages.create(
model="claude-sonnet-4-0",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": image_data}},
{"type": "text", "text": "Describe this diagram"}
]
}]
)
```
## Extended Thinking
For complex reasoning tasks:
```python
message = client.messages.create(
model="claude-sonnet-4-0",
max_tokens=16000,
thinking={
"type": "enabled",
"budget_tokens": 10000
},
messages=[{"role": "user", "content": "Solve this math problem step by step..."}]
)
for block in message.content:
if block.type == "thinking":
print(f"Thinking: {block.thinking}")
elif block.type == "text":
print(f"Answer: {block.text}")
```
## Prompt Caching
Cache large system prompts or context to reduce costs:
```python
message = client.messages.create(
model="claude-sonnet-4-0",
max_tokens=1024,
system=[
{"type": "text", "text": large_system_prompt, "cache_control": {"type": "ephemeral"}}
],
messages=[{"role": "user", "content": "Question about the cached context"}]
)
# Check cache usage
print(f"Cache read: {message.usage.cache_read_input_tokens}")
print(f"Cache creation: {message.usage.cache_creation_input_tokens}")
```
## Batches API
Process large volumes asynchronously at 50% cost reduction:
```python
import time
batch = client.messages.batches.create(
requests=[
{
"custom_id": f"request-{i}",
"params": {
"model": "claude-sonnet-4-0",
"max_tokens": 1024,
"messages": [{"role": "user", "content": prompt}]
}
}
for i, prompt in enumerate(prompts)
]
)
# Poll for completion
while True:
status = client.messages.batches.retrieve(batch.id)
if status.processing_status == "ended":
break
time.sleep(30)
# Get results
for result in client.messages.batches.results(batch.id):
print(result.result.message.content[0].text)
```
## Claude Agent SDK
Build multi-step agents:
```python
# Note: Agent SDK API surface may change — check official docs
import anthropic
# Define tools as functions
tools = [{
"name": "search_codebase",
"description": "Search the codebase for relevant code",
"input_schema": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"]
}
}]
# Run an agentic loop with tool use
client = anthropic.Anthropic()
messages = [{"role": "user", "content": "Review the auth module for security issues"}]
while True:
response = client.messages.create(
model="claude-sonnet-4-0",
max_tokens=4096,
tools=tools,
messages=messages,
)
if response.stop_reason == "end_turn":
break
# Handle tool calls and continue the loop
messages.append({"role": "assistant", "content": response.content})
# ... execute tools and append tool_result messages
```
## Cost Optimization
| Strategy | Savings | When to Use |
|----------|---------|-------------|
| Prompt caching | Up to 90% on cached tokens | Repeated system prompts or context |
| Batches API | 50% | Non-time-sensitive bulk processing |
| Haiku instead of Sonnet | ~75% | Simple tasks, classification, extraction |
| Shorter max_tokens | Variable | When you know output will be short |
| Streaming | None (same cost) | Better UX, same price |
## Error Handling
```python
import time
from anthropic import APIError, RateLimitError, APIConnectionError
try:
message = client.messages.create(...)
except RateLimitError:
# Back off and retry
time.sleep(60)
except APIConnectionError:
# Network issue, retry with backoff
pass
except APIError as e:
print(f"API error {e.status_code}: {e.message}")
```
## Environment Setup
```bash
# Required
export ANTHROPIC_API_KEY="your-api-key-here"
# Optional: set default model
export ANTHROPIC_MODEL="claude-sonnet-4-0"
```
Never hardcode API keys. Always use environment variables.

View File

@@ -82,7 +82,7 @@ If the user chooses niche or core + niche, continue to category selection below
### 2b: Choose Skill Categories
There are 27 skills organized into 4 categories. Use `AskUserQuestion` with `multiSelect: true`:
There are 41 skills organized into 8 categories. Use `AskUserQuestion` with `multiSelect: true`:
```
Question: "Which skill categories do you want to install?"
@@ -90,6 +90,11 @@ Options:
- "Framework & Language" — "Django, Spring Boot, Go, Python, Java, Frontend, Backend patterns"
- "Database" — "PostgreSQL, ClickHouse, JPA/Hibernate patterns"
- "Workflow & Quality" — "TDD, verification, learning, security review, compaction"
- "Business & Content" — "Article writing, content engine, market research, investor materials, outreach"
- "Research & APIs" — "Deep research, Exa search, Claude API patterns"
- "Social & Content Distribution" — "X/Twitter API, crossposting alongside content-engine"
- "Media Generation" — "fal.ai image/video/audio alongside VideoDB"
- "Orchestration" — "dmux multi-agent workflows"
- "All skills" — "Install every available skill"
```
@@ -150,6 +155,34 @@ For each selected category, print the full list of skills below and ask the user
| `investor-materials` | Pitch decks, one-pagers, investor memos, and financial models |
| `investor-outreach` | Personalized investor cold emails, warm intros, and follow-ups |
**Category: Research & APIs (3 skills)**
| Skill | Description |
|-------|-------------|
| `deep-research` | Multi-source deep research using firecrawl and exa MCPs with cited reports |
| `exa-search` | Neural search via Exa MCP for web, code, company, and people research |
| `claude-api` | Anthropic Claude API patterns: Messages, streaming, tool use, vision, batches, Agent SDK |
**Category: Social & Content Distribution (2 skills)**
| Skill | Description |
|-------|-------------|
| `x-api` | X/Twitter API integration for posting, threads, search, and analytics |
| `crosspost` | Multi-platform content distribution with platform-native adaptation |
**Category: Media Generation (2 skills)**
| Skill | Description |
|-------|-------------|
| `fal-ai-media` | Unified AI media generation (image, video, audio) via fal.ai MCP |
| `video-editing` | AI-assisted video editing for cutting, structuring, and augmenting real footage |
**Category: Orchestration (1 skill)**
| Skill | Description |
|-------|-------------|
| `dmux-workflows` | Multi-agent orchestration using dmux for parallel agent sessions |
**Standalone**
| Skill | Description |
@@ -230,6 +263,10 @@ Some skills reference others. Verify these dependencies:
- `continuous-learning-v2` references `~/.claude/homunculus/` directory
- `python-testing` may reference `python-patterns`
- `golang-testing` may reference `golang-patterns`
- `crosspost` references `content-engine` and `x-api`
- `deep-research` references `exa-search` (complementary MCP tools)
- `fal-ai-media` references `videodb` (complementary media skill)
- `x-api` references `content-engine` and `crosspost`
- Language-specific rules reference `common/` counterparts
### 4d: Report Issues

View File

@@ -38,6 +38,12 @@ analyze_observations() {
return
fi
# session-guardian: gate observer cycle (active hours, cooldown, idle detection)
if ! bash "$(dirname "$0")/session-guardian.sh"; then
echo "[$(date)] Observer cycle skipped by session-guardian" >> "$LOG_FILE"
return
fi
prompt_file="$(mktemp "${TMPDIR:-/tmp}/ecc-observer-prompt.XXXXXX")"
cat > "$prompt_file" <<PROMPT
Read ${OBSERVATIONS_FILE} and identify patterns for the project ${PROJECT_NAME} (user corrections, error resolutions, repeated workflows, tool preferences).
@@ -91,7 +97,8 @@ PROMPT
max_turns=10
fi
claude --model haiku --max-turns "$max_turns" --print < "$prompt_file" >> "$LOG_FILE" 2>&1 &
# Prevent observe.sh from recording this automated Haiku session as observations
ECC_SKIP_OBSERVE=1 ECC_HOOK_PROFILE=minimal claude --model haiku --max-turns "$max_turns" --print < "$prompt_file" >> "$LOG_FILE" 2>&1 &
claude_pid=$!
(

View File

@@ -0,0 +1,150 @@
#!/usr/bin/env bash
# session-guardian.sh — Observer session guard
# Exit 0 = proceed. Exit 1 = skip this observer cycle.
# Called by observer-loop.sh before spawning any Claude session.
#
# Config (env vars, all optional):
# OBSERVER_INTERVAL_SECONDS default: 300 (per-project cooldown)
# OBSERVER_LAST_RUN_LOG default: ~/.claude/observer-last-run.log
# OBSERVER_ACTIVE_HOURS_START default: 800 (8:00 AM local, set to 0 to disable)
# OBSERVER_ACTIVE_HOURS_END default: 2300 (11:00 PM local, set to 0 to disable)
# OBSERVER_MAX_IDLE_SECONDS default: 1800 (30 min; set to 0 to disable)
#
# Gate execution order (cheapest first):
# Gate 1: Time window check (~0ms, string comparison)
# Gate 2: Project cooldown log (~1ms, file read + mkdir lock)
# Gate 3: Idle detection (~5-50ms, OS syscall; fail open)
set -euo pipefail
INTERVAL="${OBSERVER_INTERVAL_SECONDS:-300}"
LOG_PATH="${OBSERVER_LAST_RUN_LOG:-$HOME/.claude/observer-last-run.log}"
ACTIVE_START="${OBSERVER_ACTIVE_HOURS_START:-800}"
ACTIVE_END="${OBSERVER_ACTIVE_HOURS_END:-2300}"
MAX_IDLE="${OBSERVER_MAX_IDLE_SECONDS:-1800}"
# ── Gate 1: Time Window ───────────────────────────────────────────────────────
# Skip observer cycles outside configured active hours (local system time).
# Uses HHMM integer comparison. Works on BSD date (macOS) and GNU date (Linux).
# Supports overnight windows such as 2200-0600.
# Set both ACTIVE_START and ACTIVE_END to 0 to disable this gate.
if [ "$ACTIVE_START" -ne 0 ] || [ "$ACTIVE_END" -ne 0 ]; then
current_hhmm=$(date +%k%M | tr -d ' ')
current_hhmm_num=$(( 10#${current_hhmm:-0} ))
active_start_num=$(( 10#${ACTIVE_START:-800} ))
active_end_num=$(( 10#${ACTIVE_END:-2300} ))
within_active_hours=0
if [ "$active_start_num" -lt "$active_end_num" ]; then
if [ "$current_hhmm_num" -ge "$active_start_num" ] && [ "$current_hhmm_num" -lt "$active_end_num" ]; then
within_active_hours=1
fi
else
if [ "$current_hhmm_num" -ge "$active_start_num" ] || [ "$current_hhmm_num" -lt "$active_end_num" ]; then
within_active_hours=1
fi
fi
if [ "$within_active_hours" -ne 1 ]; then
echo "session-guardian: outside active hours (${current_hhmm}, window ${ACTIVE_START}-${ACTIVE_END})" >&2
exit 1
fi
fi
# ── Gate 2: Project Cooldown Log ─────────────────────────────────────────────
# Prevent the same project being observed faster than OBSERVER_INTERVAL_SECONDS.
# Key: PROJECT_DIR when provided by the observer, otherwise git root path.
# Uses mkdir-based lock for safe concurrent access. Skips the cycle on lock contention.
# stderr uses basename only — never prints the full absolute path.
project_root="${PROJECT_DIR:-}"
if [ -z "$project_root" ] || [ ! -d "$project_root" ]; then
project_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")"
fi
project_name="$(basename "$project_root")"
now="$(date +%s)"
mkdir -p "$(dirname "$LOG_PATH")" || {
echo "session-guardian: cannot create log dir, proceeding" >&2
exit 0
}
_lock_dir="${LOG_PATH}.lock"
if ! mkdir "$_lock_dir" 2>/dev/null; then
# Another observer holds the lock — skip this cycle to avoid double-spawns
echo "session-guardian: log locked by concurrent process, skipping cycle" >&2
exit 1
else
trap 'rm -rf "$_lock_dir"' EXIT INT TERM
last_spawn=0
last_spawn=$(awk -F '\t' -v key="$project_root" '$1 == key { value = $2 } END { if (value != "") print value }' "$LOG_PATH" 2>/dev/null) || true
last_spawn="${last_spawn:-0}"
[[ "$last_spawn" =~ ^[0-9]+$ ]] || last_spawn=0
elapsed=$(( now - last_spawn ))
if [ "$elapsed" -lt "$INTERVAL" ]; then
rm -rf "$_lock_dir"
trap - EXIT INT TERM
echo "session-guardian: cooldown active for '${project_name}' (last spawn ${elapsed}s ago, interval ${INTERVAL}s)" >&2
exit 1
fi
# Update log: remove old entry for this project, append new timestamp (tab-delimited)
tmp_log="$(mktemp "$(dirname "$LOG_PATH")/observer-last-run.XXXXXX")"
awk -F '\t' -v key="$project_root" '$1 != key' "$LOG_PATH" > "$tmp_log" 2>/dev/null || true
printf '%s\t%s\n' "$project_root" "$now" >> "$tmp_log"
mv "$tmp_log" "$LOG_PATH"
rm -rf "$_lock_dir"
trap - EXIT INT TERM
fi
# ── Gate 3: Idle Detection ────────────────────────────────────────────────────
# Skip cycles when no user input received for too long. Fail open if idle time
# cannot be determined (Linux without xprintidle, headless, unknown OS).
# Set OBSERVER_MAX_IDLE_SECONDS=0 to disable this gate.
get_idle_seconds() {
local _raw
case "$(uname -s)" in
Darwin)
_raw=$( { /usr/sbin/ioreg -c IOHIDSystem \
| /usr/bin/awk '/HIDIdleTime/ {print int($NF/1000000000); exit}'; } \
2>/dev/null ) || true
printf '%s\n' "${_raw:-0}" | head -n1
;;
Linux)
if command -v xprintidle >/dev/null 2>&1; then
_raw=$(xprintidle 2>/dev/null) || true
echo $(( ${_raw:-0} / 1000 ))
else
echo 0 # fail open: xprintidle not installed
fi
;;
*MINGW*|*MSYS*|*CYGWIN*)
_raw=$(powershell.exe -NoProfile -NonInteractive -Command \
"try { \
Add-Type -MemberDefinition '[DllImport(\"user32.dll\")] public static extern bool GetLastInputInfo(ref LASTINPUTINFO p); [StructLayout(LayoutKind.Sequential)] public struct LASTINPUTINFO { public uint cbSize; public int dwTime; }' -Name WinAPI -Namespace PInvoke; \
\$l = New-Object PInvoke.WinAPI+LASTINPUTINFO; \$l.cbSize = 8; \
[PInvoke.WinAPI]::GetLastInputInfo([ref]\$l) | Out-Null; \
[int][Math]::Max(0, [long]([Environment]::TickCount - [long]\$l.dwTime) / 1000) \
} catch { 0 }" \
2>/dev/null | tr -d '\r') || true
printf '%s\n' "${_raw:-0}" | head -n1
;;
*)
echo 0 # fail open: unknown platform
;;
esac
}
if [ "$MAX_IDLE" -gt 0 ]; then
idle_seconds=$(get_idle_seconds)
if [ "$idle_seconds" -gt "$MAX_IDLE" ]; then
echo "session-guardian: user idle ${idle_seconds}s (threshold ${MAX_IDLE}s), skipping" >&2
exit 1
fi
fi
exit 0

View File

@@ -113,8 +113,56 @@ if [ -f "$CONFIG_DIR/disabled" ]; then
exit 0
fi
# FIX: Skip if a previous run already aborted due to confirmation/permission prompt
# This is the circuit-breaker — stops retrying after a non-interactive failure
# ─────────────────────────────────────────────
# Automated session guards
# Prevents observe.sh from firing on non-human sessions to avoid:
# - ECC observing its own Haiku observer sessions (self-loop)
# - ECC observing other tools' automated sessions (e.g. claude-mem)
# - All-night Haiku usage with no human activity
# ─────────────────────────────────────────────
# Env-var checks first (cheapest — no subprocess spawning):
# Layer 1: CLAUDE_CODE_ENTRYPOINT — set by Claude Code itself to indicate how
# it was invoked. Only interactive terminal sessions should continue; treat any
# explicit non-cli entrypoint as automated so future entrypoint types fail closed
# without requiring updates here.
case "${CLAUDE_CODE_ENTRYPOINT:-cli}" in
cli) ;;
*) exit 0 ;;
esac
# Layer 2: Respect ECC_HOOK_PROFILE=minimal — suppresses non-essential hooks
[ "${ECC_HOOK_PROFILE:-standard}" = "minimal" ] && exit 0
# Layer 3: Cooperative skip env var — tools like claude-mem can set this
# (export ECC_SKIP_OBSERVE=1) before spawning their automated sessions
[ "${ECC_SKIP_OBSERVE:-0}" = "1" ] && exit 0
# Layer 4: Skip subagent sessions — agent_id is only present when a hook fires
# inside a subagent (automated by definition, never a human interactive session).
# Placed after env-var checks to avoid a Python subprocess on sessions that
# already exit via Layers 1-3.
_ECC_AGENT_ID=$(echo "$INPUT_JSON" | "$PYTHON_CMD" -c "import json,sys; print(json.load(sys.stdin).get('agent_id',''))" 2>/dev/null || true)
[ -n "$_ECC_AGENT_ID" ] && exit 0
# Layer 5: CWD path exclusions — skip known observer-session directories.
# Add custom paths via ECC_OBSERVE_SKIP_PATHS (comma-separated substrings).
# Whitespace is trimmed from each pattern; empty patterns are skipped to
# prevent an empty-string glob from matching every path.
_ECC_SKIP_PATHS="${ECC_OBSERVE_SKIP_PATHS:-observer-sessions,.claude-mem}"
if [ -n "$STDIN_CWD" ]; then
IFS=',' read -ra _ECC_SKIP_ARRAY <<< "$_ECC_SKIP_PATHS"
for _pattern in "${_ECC_SKIP_ARRAY[@]}"; do
_pattern="${_pattern#"${_pattern%%[![:space:]]*}"}" # trim leading whitespace
_pattern="${_pattern%"${_pattern##*[![:space:]]}"}" # trim trailing whitespace
[ -z "$_pattern" ] && continue
case "$STDIN_CWD" in *"$_pattern"*) exit 0 ;; esac
done
fi
# Skip if a previous run already aborted due to confirmation/permission prompt.
# This is the circuit-breaker — stops retrying after a non-interactive failure.
if [ -f "$SENTINEL_FILE" ]; then
echo "[observe] Skipping: previous run aborted due to confirmation/permission prompt. Remove ${SENTINEL_FILE} to re-enable." >&2
exit 0

192
skills/crosspost/SKILL.md Normal file
View File

@@ -0,0 +1,192 @@
---
name: crosspost
description: Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.
origin: ECC
---
# Crosspost
Distribute content across multiple social platforms with platform-native adaptation.
## When to Use
- User wants to post content to multiple platforms
- Publishing announcements, launches, or updates across social media
- Repurposing a post from one platform to others
- User says "crosspost", "post everywhere", "share on all platforms", or "distribute this"
## How It Works
### Core Rules
1. **Never post identical content cross-platform.** Each platform gets a native adaptation.
2. **Primary platform first.** Post to the main platform, then adapt for others.
3. **Respect platform conventions.** Length limits, formatting, link handling all differ.
4. **One idea per post.** If the source content has multiple ideas, split across posts.
5. **Attribution matters.** If crossposting someone else's content, credit the source.
### Platform Specifications
| Platform | Max Length | Link Handling | Hashtags | Media |
|----------|-----------|---------------|----------|-------|
| X | 280 chars (4000 for Premium) | Counted in length | Minimal (1-2 max) | Images, video, GIFs |
| LinkedIn | 3000 chars | Not counted in length | 3-5 relevant | Images, video, docs, carousels |
| Threads | 500 chars | Separate link attachment | None typical | Images, video |
| Bluesky | 300 chars | Via facets (rich text) | None (use feeds) | Images |
### Workflow
### Step 1: Create Source Content
Start with the core idea. Use `content-engine` skill for high-quality drafts:
- Identify the single core message
- Determine the primary platform (where the audience is biggest)
- Draft the primary platform version first
### Step 2: Identify Target Platforms
Ask the user or determine from context:
- Which platforms to target
- Priority order (primary gets the best version)
- Any platform-specific requirements (e.g., LinkedIn needs professional tone)
### Step 3: Adapt Per Platform
For each target platform, transform the content:
**X adaptation:**
- Open with a hook, not a summary
- Cut to the core insight fast
- Keep links out of main body when possible
- Use thread format for longer content
**LinkedIn adaptation:**
- Strong first line (visible before "see more")
- Short paragraphs with line breaks
- Frame around lessons, results, or professional takeaways
- More explicit context than X (LinkedIn audience needs framing)
**Threads adaptation:**
- Conversational, casual tone
- Shorter than LinkedIn, less compressed than X
- Visual-first if possible
**Bluesky adaptation:**
- Direct and concise (300 char limit)
- Community-oriented tone
- Use feeds/lists for topic targeting instead of hashtags
### Step 4: Post Primary Platform
Post to the primary platform first:
- Use `x-api` skill for X
- Use platform-specific APIs or tools for others
- Capture the post URL for cross-referencing
### Step 5: Post to Secondary Platforms
Post adapted versions to remaining platforms:
- Stagger timing (not all at once — 30-60 min gaps)
- Include cross-platform references where appropriate ("longer thread on X" etc.)
## Examples
### Source: Product Launch
**X version:**
```
We just shipped [feature].
[One specific thing it does that's impressive]
[Link]
```
**LinkedIn version:**
```
Excited to share: we just launched [feature] at [Company].
Here's why it matters:
[2-3 short paragraphs with context]
[Takeaway for the audience]
[Link]
```
**Threads version:**
```
just shipped something cool — [feature]
[casual explanation of what it does]
link in bio
```
### Source: Technical Insight
**X version:**
```
TIL: [specific technical insight]
[Why it matters in one sentence]
```
**LinkedIn version:**
```
A pattern I've been using that's made a real difference:
[Technical insight with professional framing]
[How it applies to teams/orgs]
#relevantHashtag
```
## API Integration
### Batch Crossposting Service (Example Pattern)
If using a crossposting service (e.g., Postbridge, Buffer, or a custom API), the pattern looks like:
```python
import os
import requests
resp = requests.post(
"https://your-crosspost-service.example/api/posts",
headers={"Authorization": f"Bearer {os.environ['POSTBRIDGE_API_KEY']}"},
json={
"platforms": ["twitter", "linkedin", "threads"],
"content": {
"twitter": {"text": x_version},
"linkedin": {"text": linkedin_version},
"threads": {"text": threads_version}
}
},
timeout=30
)
resp.raise_for_status()
```
### Manual Posting
Without Postbridge, post to each platform using its native API:
- X: Use `x-api` skill patterns
- LinkedIn: LinkedIn API v2 with OAuth 2.0
- Threads: Threads API (Meta)
- Bluesky: AT Protocol API
## Quality Gate
Before posting:
- [ ] Each platform version reads naturally for that platform
- [ ] No identical content across platforms
- [ ] Length limits respected
- [ ] Links work and are placed appropriately
- [ ] Tone matches platform conventions
- [ ] Media is sized correctly for each platform
## Related Skills
- `content-engine` — Generate platform-native content
- `x-api` — X/Twitter API integration

View File

@@ -0,0 +1,155 @@
---
name: deep-research
description: Multi-source deep research using firecrawl and exa MCPs. Searches the web, synthesizes findings, and delivers cited reports with source attribution. Use when the user wants thorough research on any topic with evidence and citations.
origin: ECC
---
# Deep Research
Produce thorough, cited research reports from multiple web sources using firecrawl and exa MCP tools.
## When to Activate
- User asks to research any topic in depth
- Competitive analysis, technology evaluation, or market sizing
- Due diligence on companies, investors, or technologies
- Any question requiring synthesis from multiple sources
- User says "research", "deep dive", "investigate", or "what's the current state of"
## MCP Requirements
At least one of:
- **firecrawl** — `firecrawl_search`, `firecrawl_scrape`, `firecrawl_crawl`
- **exa** — `web_search_exa`, `web_search_advanced_exa`, `crawling_exa`
Both together give the best coverage. Configure in `~/.claude.json` or `~/.codex/config.toml`.
## Workflow
### Step 1: Understand the Goal
Ask 1-2 quick clarifying questions:
- "What's your goal — learning, making a decision, or writing something?"
- "Any specific angle or depth you want?"
If the user says "just research it" — skip ahead with reasonable defaults.
### Step 2: Plan the Research
Break the topic into 3-5 research sub-questions. Example:
- Topic: "Impact of AI on healthcare"
- What are the main AI applications in healthcare today?
- What clinical outcomes have been measured?
- What are the regulatory challenges?
- What companies are leading this space?
- What's the market size and growth trajectory?
### Step 3: Execute Multi-Source Search
For EACH sub-question, search using available MCP tools:
**With firecrawl:**
```
firecrawl_search(query: "<sub-question keywords>", limit: 8)
```
**With exa:**
```
web_search_exa(query: "<sub-question keywords>", numResults: 8)
web_search_advanced_exa(query: "<keywords>", numResults: 5, startPublishedDate: "2025-01-01")
```
**Search strategy:**
- Use 2-3 different keyword variations per sub-question
- Mix general and news-focused queries
- Aim for 15-30 unique sources total
- Prioritize: academic, official, reputable news > blogs > forums
### Step 4: Deep-Read Key Sources
For the most promising URLs, fetch full content:
**With firecrawl:**
```
firecrawl_scrape(url: "<url>")
```
**With exa:**
```
crawling_exa(url: "<url>", tokensNum: 5000)
```
Read 3-5 key sources in full for depth. Do not rely only on search snippets.
### Step 5: Synthesize and Write Report
Structure the report:
```markdown
# [Topic]: Research Report
*Generated: [date] | Sources: [N] | Confidence: [High/Medium/Low]*
## Executive Summary
[3-5 sentence overview of key findings]
## 1. [First Major Theme]
[Findings with inline citations]
- Key point ([Source Name](url))
- Supporting data ([Source Name](url))
## 2. [Second Major Theme]
...
## 3. [Third Major Theme]
...
## Key Takeaways
- [Actionable insight 1]
- [Actionable insight 2]
- [Actionable insight 3]
## Sources
1. [Title](url) — [one-line summary]
2. ...
## Methodology
Searched [N] queries across web and news. Analyzed [M] sources.
Sub-questions investigated: [list]
```
### Step 6: Deliver
- **Short topics**: Post the full report in chat
- **Long reports**: Post the executive summary + key takeaways, save full report to a file
## Parallel Research with Subagents
For broad topics, use Claude Code's Task tool to parallelize:
```
Launch 3 research agents in parallel:
1. Agent 1: Research sub-questions 1-2
2. Agent 2: Research sub-questions 3-4
3. Agent 3: Research sub-question 5 + cross-cutting themes
```
Each agent searches, reads sources, and returns findings. The main session synthesizes into the final report.
## Quality Rules
1. **Every claim needs a source.** No unsourced assertions.
2. **Cross-reference.** If only one source says it, flag it as unverified.
3. **Recency matters.** Prefer sources from the last 12 months.
4. **Acknowledge gaps.** If you couldn't find good info on a sub-question, say so.
5. **No hallucination.** If you don't know, say "insufficient data found."
6. **Separate fact from inference.** Label estimates, projections, and opinions clearly.
## Examples
```
"Research the current state of nuclear fusion energy"
"Deep dive into Rust vs Go for backend services in 2026"
"Research the best strategies for bootstrapping a SaaS business"
"What's happening with the US housing market right now?"
"Investigate the competitive landscape for AI code editors"
```

View File

@@ -0,0 +1,191 @@
---
name: dmux-workflows
description: Multi-agent orchestration using dmux (tmux pane manager for AI agents). Patterns for parallel agent workflows across Claude Code, Codex, OpenCode, and other harnesses. Use when running multiple agent sessions in parallel or coordinating multi-agent development workflows.
origin: ECC
---
# dmux Workflows
Orchestrate parallel AI agent sessions using dmux, a tmux pane manager for agent harnesses.
## When to Activate
- Running multiple agent sessions in parallel
- Coordinating work across Claude Code, Codex, and other harnesses
- Complex tasks that benefit from divide-and-conquer parallelism
- User says "run in parallel", "split this work", "use dmux", or "multi-agent"
## What is dmux
dmux is a tmux-based orchestration tool that manages AI agent panes:
- Press `n` to create a new pane with a prompt
- Press `m` to merge pane output back to the main session
- Supports: Claude Code, Codex, OpenCode, Cline, Gemini, Qwen
**Install:** `npm install -g dmux` or see [github.com/standardagents/dmux](https://github.com/standardagents/dmux)
## Quick Start
```bash
# Start dmux session
dmux
# Create agent panes (press 'n' in dmux, then type prompt)
# Pane 1: "Implement the auth middleware in src/auth/"
# Pane 2: "Write tests for the user service"
# Pane 3: "Update API documentation"
# Each pane runs its own agent session
# Press 'm' to merge results back
```
## Workflow Patterns
### Pattern 1: Research + Implement
Split research and implementation into parallel tracks:
```
Pane 1 (Research): "Research best practices for rate limiting in Node.js.
Check current libraries, compare approaches, and write findings to
/tmp/rate-limit-research.md"
Pane 2 (Implement): "Implement rate limiting middleware for our Express API.
Start with a basic token bucket, we'll refine after research completes."
# After Pane 1 completes, merge findings into Pane 2's context
```
### Pattern 2: Multi-File Feature
Parallelize work across independent files:
```
Pane 1: "Create the database schema and migrations for the billing feature"
Pane 2: "Build the billing API endpoints in src/api/billing/"
Pane 3: "Create the billing dashboard UI components"
# Merge all, then do integration in main pane
```
### Pattern 3: Test + Fix Loop
Run tests in one pane, fix in another:
```
Pane 1 (Watcher): "Run the test suite in watch mode. When tests fail,
summarize the failures."
Pane 2 (Fixer): "Fix failing tests based on the error output from pane 1"
```
### Pattern 4: Cross-Harness
Use different AI tools for different tasks:
```
Pane 1 (Claude Code): "Review the security of the auth module"
Pane 2 (Codex): "Refactor the utility functions for performance"
Pane 3 (Claude Code): "Write E2E tests for the checkout flow"
```
### Pattern 5: Code Review Pipeline
Parallel review perspectives:
```
Pane 1: "Review src/api/ for security vulnerabilities"
Pane 2: "Review src/api/ for performance issues"
Pane 3: "Review src/api/ for test coverage gaps"
# Merge all reviews into a single report
```
## Best Practices
1. **Independent tasks only.** Don't parallelize tasks that depend on each other's output.
2. **Clear boundaries.** Each pane should work on distinct files or concerns.
3. **Merge strategically.** Review pane output before merging to avoid conflicts.
4. **Use git worktrees.** For file-conflict-prone work, use separate worktrees per pane.
5. **Resource awareness.** Each pane uses API tokens — keep total panes under 5-6.
## Git Worktree Integration
For tasks that touch overlapping files:
```bash
# Create worktrees for isolation
git worktree add -b feat/auth ../feature-auth HEAD
git worktree add -b feat/billing ../feature-billing HEAD
# Run agents in separate worktrees
# Pane 1: cd ../feature-auth && claude
# Pane 2: cd ../feature-billing && claude
# Merge branches when done
git merge feat/auth
git merge feat/billing
```
## Complementary Tools
| Tool | What It Does | When to Use |
|------|-------------|-------------|
| **dmux** | tmux pane management for agents | Parallel agent sessions |
| **Superset** | Terminal IDE for 10+ parallel agents | Large-scale orchestration |
| **Claude Code Task tool** | In-process subagent spawning | Programmatic parallelism within a session |
| **Codex multi-agent** | Built-in agent roles | Codex-specific parallel work |
## ECC Helper
ECC now includes a helper for external tmux-pane orchestration with separate git worktrees:
```bash
node scripts/orchestrate-worktrees.js plan.json --execute
```
Example `plan.json`:
```json
{
"sessionName": "skill-audit",
"baseRef": "HEAD",
"launcherCommand": "codex exec --cwd {worktree_path_sh} --task-file {task_file_sh}",
"workers": [
{ "name": "docs-a", "task": "Fix skills 1-4 and write handoff notes." },
{ "name": "docs-b", "task": "Fix skills 5-8 and write handoff notes." }
]
}
```
The helper:
- Creates one branch-backed git worktree per worker
- Optionally overlays selected `seedPaths` from the main checkout into each worker worktree
- Writes per-worker `task.md`, `handoff.md`, and `status.md` files under `.orchestration/<session>/`
- Starts a tmux session with one pane per worker
- Launches each worker command in its own pane
- Leaves the main pane free for the orchestrator
Use `seedPaths` when workers need access to dirty or untracked local files that are not yet part of `HEAD`, such as local orchestration scripts, draft plans, or docs:
```json
{
"sessionName": "workflow-e2e",
"seedPaths": [
"scripts/orchestrate-worktrees.js",
"scripts/lib/tmux-worktree-orchestrator.js",
".claude/plan/workflow-e2e-test.json"
],
"launcherCommand": "bash {repo_root_sh}/scripts/orchestrate-codex-worker.sh {task_file_sh} {handoff_file_sh} {status_file_sh}",
"workers": [
{ "name": "seed-check", "task": "Verify seeded files are present before starting work." }
]
}
```
## Troubleshooting
- **Pane not responding:** Switch to the pane directly or inspect it with `tmux capture-pane -pt <session>:0.<pane-index>`.
- **Merge conflicts:** Use git worktrees to isolate file changes per pane.
- **High token usage:** Reduce number of parallel panes. Each pane is a full agent session.
- **tmux not found:** Install with `brew install tmux` (macOS) or `apt install tmux` (Linux).

175
skills/exa-search/SKILL.md Normal file
View File

@@ -0,0 +1,175 @@
---
name: exa-search
description: Neural search via Exa MCP for web, code, and company research. Use when the user needs web search, code examples, company intel, people lookup, or AI-powered deep research with Exa's neural search engine.
origin: ECC
---
# Exa Search
Neural search for web content, code, companies, and people via the Exa MCP server.
## When to Activate
- User needs current web information or news
- Searching for code examples, API docs, or technical references
- Researching companies, competitors, or market players
- Finding professional profiles or people in a domain
- Running background research for any development task
- User says "search for", "look up", "find", or "what's the latest on"
## MCP Requirement
Exa MCP server must be configured. Add to `~/.claude.json`:
```json
"exa-web-search": {
"command": "npx",
"args": [
"-y",
"exa-mcp-server",
"tools=web_search_exa,web_search_advanced_exa,get_code_context_exa,crawling_exa,company_research_exa,people_search_exa,deep_researcher_start,deep_researcher_check"
],
"env": { "EXA_API_KEY": "YOUR_EXA_API_KEY_HERE" }
}
```
Get an API key at [exa.ai](https://exa.ai).
If you omit the `tools=...` argument, only a smaller default tool set may be enabled.
## Core Tools
### web_search_exa
General web search for current information, news, or facts.
```
web_search_exa(query: "latest AI developments 2026", numResults: 5)
```
**Parameters:**
| Param | Type | Default | Notes |
|-------|------|---------|-------|
| `query` | string | required | Search query |
| `numResults` | number | 8 | Number of results |
### web_search_advanced_exa
Filtered search with domain and date constraints.
```
web_search_advanced_exa(
query: "React Server Components best practices",
numResults: 5,
includeDomains: ["github.com", "react.dev"],
startPublishedDate: "2025-01-01"
)
```
**Parameters:**
| Param | Type | Default | Notes |
|-------|------|---------|-------|
| `query` | string | required | Search query |
| `numResults` | number | 8 | Number of results |
| `includeDomains` | string[] | none | Limit to specific domains |
| `excludeDomains` | string[] | none | Exclude specific domains |
| `startPublishedDate` | string | none | ISO date filter (start) |
| `endPublishedDate` | string | none | ISO date filter (end) |
### get_code_context_exa
Find code examples and documentation from GitHub, Stack Overflow, and docs sites.
```
get_code_context_exa(query: "Python asyncio patterns", tokensNum: 3000)
```
**Parameters:**
| Param | Type | Default | Notes |
|-------|------|---------|-------|
| `query` | string | required | Code or API search query |
| `tokensNum` | number | 5000 | Content tokens (1000-50000) |
### company_research_exa
Research companies for business intelligence and news.
```
company_research_exa(companyName: "Anthropic", numResults: 5)
```
**Parameters:**
| Param | Type | Default | Notes |
|-------|------|---------|-------|
| `companyName` | string | required | Company name |
| `numResults` | number | 5 | Number of results |
### people_search_exa
Find professional profiles and bios.
```
people_search_exa(query: "AI safety researchers at Anthropic", numResults: 5)
```
### crawling_exa
Extract full page content from a URL.
```
crawling_exa(url: "https://example.com/article", tokensNum: 5000)
```
**Parameters:**
| Param | Type | Default | Notes |
|-------|------|---------|-------|
| `url` | string | required | URL to extract |
| `tokensNum` | number | 5000 | Content tokens |
### deep_researcher_start / deep_researcher_check
Start an AI research agent that runs asynchronously.
```
# Start research
deep_researcher_start(query: "comprehensive analysis of AI code editors in 2026")
# Check status (returns results when complete)
deep_researcher_check(researchId: "<id from start>")
```
## Usage Patterns
### Quick Lookup
```
web_search_exa(query: "Node.js 22 new features", numResults: 3)
```
### Code Research
```
get_code_context_exa(query: "Rust error handling patterns Result type", tokensNum: 3000)
```
### Company Due Diligence
```
company_research_exa(companyName: "Vercel", numResults: 5)
web_search_advanced_exa(query: "Vercel funding valuation 2026", numResults: 3)
```
### Technical Deep Dive
```
# Start async research
deep_researcher_start(query: "WebAssembly component model status and adoption")
# ... do other work ...
deep_researcher_check(researchId: "<id>")
```
## Tips
- Use `web_search_exa` for broad queries, `web_search_advanced_exa` for filtered results
- Lower `tokensNum` (1000-2000) for focused code snippets, higher (5000+) for comprehensive context
- Combine `company_research_exa` with `web_search_advanced_exa` for thorough company analysis
- Use `crawling_exa` to get full content from specific URLs found in search results
- `deep_researcher_start` is best for comprehensive topics that benefit from AI synthesis
## Related Skills
- `deep-research` — Full research workflow using firecrawl + exa together
- `market-research` — Business-oriented research with decision frameworks

View File

@@ -0,0 +1,284 @@
---
name: fal-ai-media
description: Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.
origin: ECC
---
# fal.ai Media Generation
Generate images, videos, and audio using fal.ai models via MCP.
## When to Activate
- User wants to generate images from text prompts
- Creating videos from text or images
- Generating speech, music, or sound effects
- Any media generation task
- User says "generate image", "create video", "text to speech", "make a thumbnail", or similar
## MCP Requirement
fal.ai MCP server must be configured. Add to `~/.claude.json`:
```json
"fal-ai": {
"command": "npx",
"args": ["-y", "fal-ai-mcp-server"],
"env": { "FAL_KEY": "YOUR_FAL_KEY_HERE" }
}
```
Get an API key at [fal.ai](https://fal.ai).
## MCP Tools
The fal.ai MCP provides these tools:
- `search` — Find available models by keyword
- `find` — Get model details and parameters
- `generate` — Run a model with parameters
- `result` — Check async generation status
- `status` — Check job status
- `cancel` — Cancel a running job
- `estimate_cost` — Estimate generation cost
- `models` — List popular models
- `upload` — Upload files for use as inputs
---
## Image Generation
### Nano Banana 2 (Fast)
Best for: quick iterations, drafts, text-to-image, image editing.
```
generate(
app_id: "fal-ai/nano-banana-2",
input_data: {
"prompt": "a futuristic cityscape at sunset, cyberpunk style",
"image_size": "landscape_16_9",
"num_images": 1,
"seed": 42
}
)
```
### Nano Banana Pro (High Fidelity)
Best for: production images, realism, typography, detailed prompts.
```
generate(
app_id: "fal-ai/nano-banana-pro",
input_data: {
"prompt": "professional product photo of wireless headphones on marble surface, studio lighting",
"image_size": "square",
"num_images": 1,
"guidance_scale": 7.5
}
)
```
### Common Image Parameters
| Param | Type | Options | Notes |
|-------|------|---------|-------|
| `prompt` | string | required | Describe what you want |
| `image_size` | string | `square`, `portrait_4_3`, `landscape_16_9`, `portrait_16_9`, `landscape_4_3` | Aspect ratio |
| `num_images` | number | 1-4 | How many to generate |
| `seed` | number | any integer | Reproducibility |
| `guidance_scale` | number | 1-20 | How closely to follow the prompt (higher = more literal) |
### Image Editing
Use Nano Banana 2 with an input image for inpainting, outpainting, or style transfer:
```
# First upload the source image
upload(file_path: "/path/to/image.png")
# Then generate with image input
generate(
app_id: "fal-ai/nano-banana-2",
input_data: {
"prompt": "same scene but in watercolor style",
"image_url": "<uploaded_url>",
"image_size": "landscape_16_9"
}
)
```
---
## Video Generation
### Seedance 1.0 Pro (ByteDance)
Best for: text-to-video, image-to-video with high motion quality.
```
generate(
app_id: "fal-ai/seedance-1-0-pro",
input_data: {
"prompt": "a drone flyover of a mountain lake at golden hour, cinematic",
"duration": "5s",
"aspect_ratio": "16:9",
"seed": 42
}
)
```
### Kling Video v3 Pro
Best for: text/image-to-video with native audio generation.
```
generate(
app_id: "fal-ai/kling-video/v3/pro",
input_data: {
"prompt": "ocean waves crashing on a rocky coast, dramatic clouds",
"duration": "5s",
"aspect_ratio": "16:9"
}
)
```
### Veo 3 (Google DeepMind)
Best for: video with generated sound, high visual quality.
```
generate(
app_id: "fal-ai/veo-3",
input_data: {
"prompt": "a bustling Tokyo street market at night, neon signs, crowd noise",
"aspect_ratio": "16:9"
}
)
```
### Image-to-Video
Start from an existing image:
```
generate(
app_id: "fal-ai/seedance-1-0-pro",
input_data: {
"prompt": "camera slowly zooms out, gentle wind moves the trees",
"image_url": "<uploaded_image_url>",
"duration": "5s"
}
)
```
### Video Parameters
| Param | Type | Options | Notes |
|-------|------|---------|-------|
| `prompt` | string | required | Describe the video |
| `duration` | string | `"5s"`, `"10s"` | Video length |
| `aspect_ratio` | string | `"16:9"`, `"9:16"`, `"1:1"` | Frame ratio |
| `seed` | number | any integer | Reproducibility |
| `image_url` | string | URL | Source image for image-to-video |
---
## Audio Generation
### CSM-1B (Conversational Speech)
Text-to-speech with natural, conversational quality.
```
generate(
app_id: "fal-ai/csm-1b",
input_data: {
"text": "Hello, welcome to the demo. Let me show you how this works.",
"speaker_id": 0
}
)
```
### ThinkSound (Video-to-Audio)
Generate matching audio from video content.
```
generate(
app_id: "fal-ai/thinksound",
input_data: {
"video_url": "<video_url>",
"prompt": "ambient forest sounds with birds chirping"
}
)
```
### ElevenLabs (via API, no MCP)
For professional voice synthesis, use ElevenLabs directly:
```python
import os
import requests
resp = requests.post(
"https://api.elevenlabs.io/v1/text-to-speech/<voice_id>",
headers={
"xi-api-key": os.environ["ELEVENLABS_API_KEY"],
"Content-Type": "application/json"
},
json={
"text": "Your text here",
"model_id": "eleven_turbo_v2_5",
"voice_settings": {"stability": 0.5, "similarity_boost": 0.75}
}
)
with open("output.mp3", "wb") as f:
f.write(resp.content)
```
### VideoDB Generative Audio
If VideoDB is configured, use its generative audio:
```python
# Voice generation
audio = coll.generate_voice(text="Your narration here", voice="alloy")
# Music generation
music = coll.generate_music(prompt="upbeat electronic background music", duration=30)
# Sound effects
sfx = coll.generate_sound_effect(prompt="thunder crack followed by rain")
```
---
## Cost Estimation
Before generating, check estimated cost:
```
estimate_cost(
estimate_type: "unit_price",
endpoints: {
"fal-ai/nano-banana-pro": {
"unit_quantity": 1
}
}
)
```
## Model Discovery
Find models for specific tasks:
```
search(query: "text to video")
find(endpoint_ids: ["fal-ai/seedance-1-0-pro"])
models()
```
## Tips
- Use `seed` for reproducible results when iterating on prompts
- Start with lower-cost models (Nano Banana 2) for prompt iteration, then switch to Pro for finals
- For video, keep prompts descriptive but concise — focus on motion and scene
- Image-to-video produces more controlled results than pure text-to-video
- Check `estimate_cost` before running expensive video generations
## Related Skills
- `videodb` — Video processing, editing, and streaming
- `video-editing` — AI-powered video editing workflows
- `content-engine` — Content creation for social platforms

View File

@@ -0,0 +1,397 @@
---
name: prompt-optimizer
description: >-
Analyze raw prompts, identify intent and gaps, match ECC components
(skills/commands/agents/hooks), and output a ready-to-paste optimized
prompt. Advisory role only — never executes the task itself.
TRIGGER when: user says "optimize prompt", "improve my prompt",
"how to write a prompt for", "help me prompt", "rewrite this prompt",
or explicitly asks to enhance prompt quality. Also triggers on Chinese
equivalents: "优化prompt", "改进prompt", "怎么写prompt", "帮我优化这个指令".
DO NOT TRIGGER when: user wants the task executed directly, or says
"just do it" / "直接做". DO NOT TRIGGER when user says "优化代码",
"优化性能", "optimize performance", "optimize this code" — those are
refactoring/performance tasks, not prompt optimization.
origin: community
metadata:
author: YannJY02
version: "1.0.0"
---
# Prompt Optimizer
Analyze a draft prompt, critique it, match it to ECC ecosystem components,
and output a complete optimized prompt the user can paste and run.
## When to Use
- User says "optimize this prompt", "improve my prompt", "rewrite this prompt"
- User says "help me write a better prompt for..."
- User says "what's the best way to ask Claude Code to..."
- User says "优化prompt", "改进prompt", "怎么写prompt", "帮我优化这个指令"
- User pastes a draft prompt and asks for feedback or enhancement
- User says "I don't know how to prompt for this"
- User says "how should I use ECC for..."
- User explicitly invokes `/prompt-optimize`
### Do Not Use When
- User wants the task done directly (just execute it)
- User says "优化代码", "优化性能", "optimize this code", "optimize performance" — these are refactoring tasks, not prompt optimization
- User is asking about ECC configuration (use `configure-ecc` instead)
- User wants a skill inventory (use `skill-stocktake` instead)
- User says "just do it" or "直接做"
## How It Works
**Advisory only — do not execute the user's task.**
Do NOT write code, create files, run commands, or take any implementation
action. Your ONLY output is an analysis plus an optimized prompt.
If the user says "just do it", "直接做", or "don't optimize, just execute",
do not switch into implementation mode inside this skill. Tell the user this
skill only produces optimized prompts, and instruct them to make a normal
task request if they want execution instead.
Run this 6-phase pipeline sequentially. Present results using the Output Format below.
### Analysis Pipeline
### Phase 0: Project Detection
Before analyzing the prompt, detect the current project context:
1. Check if a `CLAUDE.md` exists in the working directory — read it for project conventions
2. Detect tech stack from project files:
- `package.json` → Node.js / TypeScript / React / Next.js
- `go.mod` → Go
- `pyproject.toml` / `requirements.txt` → Python
- `Cargo.toml` → Rust
- `build.gradle` / `pom.xml` → Java / Kotlin / Spring Boot
- `Package.swift` → Swift
- `Gemfile` → Ruby
- `composer.json` → PHP
- `*.csproj` / `*.sln` → .NET
- `Makefile` / `CMakeLists.txt` → C / C++
- `cpanfile` / `Makefile.PL` → Perl
3. Note detected tech stack for use in Phase 3 and Phase 4
If no project files are found (e.g., the prompt is abstract or for a new project),
skip detection and flag "tech stack unknown" in Phase 4.
### Phase 1: Intent Detection
Classify the user's task into one or more categories:
| Category | Signal Words | Example |
|----------|-------------|---------|
| New Feature | build, create, add, implement, 创建, 实现, 添加 | "Build a login page" |
| Bug Fix | fix, broken, not working, error, 修复, 报错 | "Fix the auth flow" |
| Refactor | refactor, clean up, restructure, 重构, 整理 | "Refactor the API layer" |
| Research | how to, what is, explore, investigate, 怎么, 如何 | "How to add SSO" |
| Testing | test, coverage, verify, 测试, 覆盖率 | "Add tests for the cart" |
| Review | review, audit, check, 审查, 检查 | "Review my PR" |
| Documentation | document, update docs, 文档 | "Update the API docs" |
| Infrastructure | deploy, CI, docker, database, 部署, 数据库 | "Set up CI/CD pipeline" |
| Design | design, architecture, plan, 设计, 架构 | "Design the data model" |
### Phase 2: Scope Assessment
If Phase 0 detected a project, use codebase size as a signal. Otherwise, estimate
from the prompt description alone and mark the estimate as uncertain.
| Scope | Heuristic | Orchestration |
|-------|-----------|---------------|
| TRIVIAL | Single file, < 50 lines | Direct execution |
| LOW | Single component or module | Single command or skill |
| MEDIUM | Multiple components, same domain | Command chain + /verify |
| HIGH | Cross-domain, 5+ files | /plan first, then phased execution |
| EPIC | Multi-session, multi-PR, architectural shift | Use blueprint skill for multi-session plan |
### Phase 3: ECC Component Matching
Map intent + scope + tech stack (from Phase 0) to specific ECC components.
#### By Intent Type
| Intent | Commands | Skills | Agents |
|--------|----------|--------|--------|
| New Feature | /plan, /tdd, /code-review, /verify | tdd-workflow, verification-loop | planner, tdd-guide, code-reviewer |
| Bug Fix | /tdd, /build-fix, /verify | tdd-workflow | tdd-guide, build-error-resolver |
| Refactor | /refactor-clean, /code-review, /verify | verification-loop | refactor-cleaner, code-reviewer |
| Research | /plan | search-first, iterative-retrieval | — |
| Testing | /tdd, /e2e, /test-coverage | tdd-workflow, e2e-testing | tdd-guide, e2e-runner |
| Review | /code-review | security-review | code-reviewer, security-reviewer |
| Documentation | /update-docs, /update-codemaps | — | doc-updater |
| Infrastructure | /plan, /verify | docker-patterns, deployment-patterns, database-migrations | architect |
| Design (MEDIUM-HIGH) | /plan | — | planner, architect |
| Design (EPIC) | — | blueprint (invoke as skill) | planner, architect |
#### By Tech Stack
| Tech Stack | Skills to Add | Agent |
|------------|--------------|-------|
| Python / Django | django-patterns, django-tdd, django-security, django-verification, python-patterns, python-testing | python-reviewer |
| Go | golang-patterns, golang-testing | go-reviewer, go-build-resolver |
| Spring Boot / Java | springboot-patterns, springboot-tdd, springboot-security, springboot-verification, java-coding-standards, jpa-patterns | code-reviewer |
| Kotlin / Android | kotlin-coroutines-flows, compose-multiplatform-patterns, android-clean-architecture | kotlin-reviewer |
| TypeScript / React | frontend-patterns, backend-patterns, coding-standards | code-reviewer |
| Swift / iOS | swiftui-patterns, swift-concurrency-6-2, swift-actor-persistence, swift-protocol-di-testing | code-reviewer |
| PostgreSQL | postgres-patterns, database-migrations | database-reviewer |
| Perl | perl-patterns, perl-testing, perl-security | code-reviewer |
| C++ | cpp-coding-standards, cpp-testing | code-reviewer |
| Other / Unlisted | coding-standards (universal) | code-reviewer |
### Phase 4: Missing Context Detection
Scan the prompt for missing critical information. Check each item and mark
whether Phase 0 auto-detected it or the user must supply it:
- [ ] **Tech stack** — Detected in Phase 0, or must user specify?
- [ ] **Target scope** — Files, directories, or modules mentioned?
- [ ] **Acceptance criteria** — How to know the task is done?
- [ ] **Error handling** — Edge cases and failure modes addressed?
- [ ] **Security requirements** — Auth, input validation, secrets?
- [ ] **Testing expectations** — Unit, integration, E2E?
- [ ] **Performance constraints** — Load, latency, resource limits?
- [ ] **UI/UX requirements** — Design specs, responsive, a11y? (if frontend)
- [ ] **Database changes** — Schema, migrations, indexes? (if data layer)
- [ ] **Existing patterns** — Reference files or conventions to follow?
- [ ] **Scope boundaries** — What NOT to do?
**If 3+ critical items are missing**, ask the user up to 3 clarification
questions before generating the optimized prompt. Then incorporate the
answers into the optimized prompt.
### Phase 5: Workflow & Model Recommendation
Determine where this prompt sits in the development lifecycle:
```
Research → Plan → Implement (TDD) → Review → Verify → Commit
```
For MEDIUM+ tasks, always start with /plan. For EPIC tasks, use blueprint skill.
**Model recommendation** (include in output):
| Scope | Recommended Model | Rationale |
|-------|------------------|-----------|
| TRIVIAL-LOW | Sonnet 4.6 | Fast, cost-efficient for simple tasks |
| MEDIUM | Sonnet 4.6 | Best coding model for standard work |
| HIGH | Sonnet 4.6 (main) + Opus 4.6 (planning) | Opus for architecture, Sonnet for implementation |
| EPIC | Opus 4.6 (blueprint) + Sonnet 4.6 (execution) | Deep reasoning for multi-session planning |
**Multi-prompt splitting** (for HIGH/EPIC scope):
For tasks that exceed a single session, split into sequential prompts:
- Prompt 1: Research + Plan (use search-first skill, then /plan)
- Prompt 2-N: Implement one phase per prompt (each ends with /verify)
- Final Prompt: Integration test + /code-review across all phases
- Use /save-session and /resume-session to preserve context between sessions
---
## Output Format
Present your analysis in this exact structure. Respond in the same language
as the user's input.
### Section 1: Prompt Diagnosis
**Strengths:** List what the original prompt does well.
**Issues:**
| Issue | Impact | Suggested Fix |
|-------|--------|---------------|
| (problem) | (consequence) | (how to fix) |
**Needs Clarification:** Numbered list of questions the user should answer.
If Phase 0 auto-detected the answer, state it instead of asking.
### Section 2: Recommended ECC Components
| Type | Component | Purpose |
|------|-----------|---------|
| Command | /plan | Plan architecture before coding |
| Skill | tdd-workflow | TDD methodology guidance |
| Agent | code-reviewer | Post-implementation review |
| Model | Sonnet 4.6 | Recommended for this scope |
### Section 3: Optimized Prompt — Full Version
Present the complete optimized prompt inside a single fenced code block.
The prompt must be self-contained and ready to copy-paste. Include:
- Clear task description with context
- Tech stack (detected or specified)
- /command invocations at the right workflow stages
- Acceptance criteria
- Verification steps
- Scope boundaries (what NOT to do)
For items that reference blueprint, write: "Use the blueprint skill to..."
(not `/blueprint`, since blueprint is a skill, not a command).
### Section 4: Optimized Prompt — Quick Version
A compact version for experienced ECC users. Vary by intent type:
| Intent | Quick Pattern |
|--------|--------------|
| New Feature | `/plan [feature]. /tdd to implement. /code-review. /verify.` |
| Bug Fix | `/tdd — write failing test for [bug]. Fix to green. /verify.` |
| Refactor | `/refactor-clean [scope]. /code-review. /verify.` |
| Research | `Use search-first skill for [topic]. /plan based on findings.` |
| Testing | `/tdd [module]. /e2e for critical flows. /test-coverage.` |
| Review | `/code-review. Then use security-reviewer agent.` |
| Docs | `/update-docs. /update-codemaps.` |
| EPIC | `Use blueprint skill for "[objective]". Execute phases with /verify gates.` |
### Section 5: Enhancement Rationale
| Enhancement | Reason |
|-------------|--------|
| (what was added) | (why it matters) |
### Footer
> Not what you need? Tell me what to adjust, or make a normal task request
> if you want execution instead of prompt optimization.
---
## Examples
### Trigger Examples
- "Optimize this prompt for ECC"
- "Rewrite this prompt so Claude Code uses the right commands"
- "帮我优化这个指令"
- "How should I prompt ECC for this task?"
### Example 1: Vague Chinese Prompt (Project Detected)
**User input:**
```
帮我写一个用户登录页面
```
**Phase 0 detects:** `package.json` with Next.js 15, TypeScript, Tailwind CSS
**Optimized Prompt (Full):**
```
使用项目现有技术栈Next.js 15 + TypeScript + Tailwind CSS实现用户登录页面。
技术要求:
- 沿用项目现有的组件结构和路由约定
- 表单验证使用项目中已有的验证方案(检查是否已用 Zod/Yup/其他)
- 认证方式:沿用项目现有认证方案(如无,默认 JWT
- 包含:邮箱/密码登录表单、表单验证、错误提示、加载状态、响应式布局
工作流:
1. /plan 先规划组件结构和认证流程,参考现有页面的模式
2. /tdd 测试先行:编写登录表单的单元测试和认证流程的集成测试
3. 实现登录页面和认证逻辑
4. /code-review 审查实现
5. /verify 验证所有测试通过且页面正常渲染
安全要求:
- 密码不明文传输
- 防止暴力破解rate limiting
- XSS 防护
- CSRF token
验收标准:
- 所有测试通过,覆盖率 80%+
- 页面在移动端和桌面端正常渲染
- 登录成功跳转到 dashboard失败显示错误信息
不要做:
- 不要实现注册页面
- 不要实现忘记密码功能
- 不要修改现有的路由结构
```
### Example 2: Moderate English Prompt
**User input:**
```
Add a REST API endpoint for user profile updates with validation
```
**Phase 0 detects:** `go.mod` with Go 1.22, Chi router
**Optimized Prompt (Full):**
```
Add a REST API endpoint for user profile updates (PATCH /api/users/:id).
Tech stack: Go 1.22 + Chi router (detected from project)
Requirements:
- PATCH /api/users/:id — partial update of user profile
- Input validation for fields: name, email, avatar_url, bio
- Auth: require valid token, users can only update own profile
- Return 200 with updated user on success
- Return 400 with validation errors on invalid input
- Return 401/403 for auth failures
- Follow existing API patterns in the codebase
Workflow:
1. /plan the endpoint structure, middleware chain, and validation logic
2. /tdd — write table-driven tests for success, validation failure, auth failure, not-found
3. Implement following existing handler patterns
4. /go-review
5. /verify — run full test suite, confirm no regressions
Do not:
- Modify existing endpoints
- Change the database schema (use existing user table)
- Add new dependencies without checking existing ones first (use search-first skill)
```
### Example 3: EPIC Project
**User input:**
```
Migrate our monolith to microservices
```
**Optimized Prompt (Full):**
```
Use the blueprint skill to plan: "Migrate monolith to microservices architecture"
Before executing, answer these questions in the blueprint:
1. Which domain boundaries exist in the current monolith?
2. Which service should be extracted first (lowest coupling)?
3. Communication pattern: REST APIs, gRPC, or event-driven (Kafka/RabbitMQ)?
4. Database strategy: shared DB initially or database-per-service from start?
5. Deployment target: Kubernetes, Docker Compose, or serverless?
The blueprint should produce phases like:
- Phase 1: Identify service boundaries and create domain map
- Phase 2: Set up infrastructure (API gateway, service mesh, CI/CD per service)
- Phase 3: Extract first service (strangler fig pattern)
- Phase 4: Verify with integration tests, then extract next service
- Phase N: Decommission monolith
Each phase = 1 PR, with /verify gates between phases.
Use /save-session between phases. Use /resume-session to continue.
Use git worktrees for parallel service extraction when dependencies allow.
Recommended: Opus 4.6 for blueprint planning, Sonnet 4.6 for phase execution.
```
---
## Related Components
| Component | When to Reference |
|-----------|------------------|
| `configure-ecc` | User hasn't set up ECC yet |
| `skill-stocktake` | Audit which components are installed (use instead of hardcoded catalog) |
| `search-first` | Research phase in optimized prompts |
| `blueprint` | EPIC-scope optimized prompts (invoke as skill, not command) |
| `strategic-compact` | Long session context management |
| `cost-aware-llm-pipeline` | Token optimization recommendations |

View File

@@ -96,6 +96,34 @@ Understanding what persists helps you compact with confidence:
5. **Write before compacting** — Save important context to files or memory before compacting
6. **Use `/compact` with a summary** — Add a custom message: `/compact Focus on implementing auth middleware next`
## Token Optimization Patterns
### Trigger-Table Lazy Loading
Instead of loading full skill content at session start, use a trigger table that maps keywords to skill paths. Skills load only when triggered, reducing baseline context by 50%+:
| Trigger | Skill | Load When |
|---------|-------|-----------|
| "test", "tdd", "coverage" | tdd-workflow | User mentions testing |
| "security", "auth", "xss" | security-review | Security-related work |
| "deploy", "ci/cd" | deployment-patterns | Deployment context |
### Context Composition Awareness
Monitor what's consuming your context window:
- **CLAUDE.md files** — Always loaded, keep lean
- **Loaded skills** — Each skill adds 1-5K tokens
- **Conversation history** — Grows with each exchange
- **Tool results** — File reads, search results add bulk
### Duplicate Instruction Detection
Common sources of duplicate context:
- Same rules in both `~/.claude/rules/` and project `.claude/rules/`
- Skills that repeat CLAUDE.md instructions
- Multiple skills covering overlapping domains
### Context Optimization Tools
- `token-optimizer` MCP — Automated 95%+ token reduction via content deduplication
- `context-mode` — Context virtualization (315KB to 5.4KB demonstrated)
## Related
- [The Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) — Token optimization section

View File

@@ -0,0 +1,310 @@
---
name: video-editing
description: AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.
origin: ECC
---
# Video Editing
AI-assisted editing for real footage. Not generation from prompts. Editing existing video fast.
## When to Activate
- User wants to edit, cut, or structure video footage
- Turning long recordings into short-form content
- Building vlogs, tutorials, or demo videos from raw capture
- Adding overlays, subtitles, music, or voiceover to existing video
- Reframing video for different platforms (YouTube, TikTok, Instagram)
- User says "edit video", "cut this footage", "make a vlog", or "video workflow"
## Core Thesis
AI video editing is useful when you stop asking it to create the whole video and start using it to compress, structure, and augment real footage. The value is not generation. The value is compression.
## The Pipeline
```
Screen Studio / raw footage
→ Claude / Codex
→ FFmpeg
→ Remotion
→ ElevenLabs / fal.ai
→ Descript or CapCut
```
Each layer has a specific job. Do not skip layers. Do not try to make one tool do everything.
## Layer 1: Capture (Screen Studio / Raw Footage)
Collect the source material:
- **Screen Studio**: polished screen recordings for app demos, coding sessions, browser workflows
- **Raw camera footage**: vlog footage, interviews, event recordings
- **Desktop capture via VideoDB**: session recording with real-time context (see `videodb` skill)
Output: raw files ready for organization.
## Layer 2: Organization (Claude / Codex)
Use Claude Code or Codex to:
- **Transcribe and label**: generate transcript, identify topics and themes
- **Plan structure**: decide what stays, what gets cut, what order works
- **Identify dead sections**: find pauses, tangents, repeated takes
- **Generate edit decision list**: timestamps for cuts, segments to keep
- **Scaffold FFmpeg and Remotion code**: generate the commands and compositions
```
Example prompt:
"Here's the transcript of a 4-hour recording. Identify the 8 strongest segments
for a 24-minute vlog. Give me FFmpeg cut commands for each segment."
```
This layer is about structure, not final creative taste.
## Layer 3: Deterministic Cuts (FFmpeg)
FFmpeg handles the boring but critical work: splitting, trimming, concatenating, and preprocessing.
### Extract segment by timestamp
```bash
ffmpeg -i raw.mp4 -ss 00:12:30 -to 00:15:45 -c copy segment_01.mp4
```
### Batch cut from edit decision list
```bash
#!/bin/bash
# cuts.txt: start,end,label
while IFS=, read -r start end label; do
ffmpeg -i raw.mp4 -ss "$start" -to "$end" -c copy "segments/${label}.mp4"
done < cuts.txt
```
### Concatenate segments
```bash
# Create file list
for f in segments/*.mp4; do echo "file '$f'"; done > concat.txt
ffmpeg -f concat -safe 0 -i concat.txt -c copy assembled.mp4
```
### Create proxy for faster editing
```bash
ffmpeg -i raw.mp4 -vf "scale=960:-2" -c:v libx264 -preset ultrafast -crf 28 proxy.mp4
```
### Extract audio for transcription
```bash
ffmpeg -i raw.mp4 -vn -acodec pcm_s16le -ar 16000 audio.wav
```
### Normalize audio levels
```bash
ffmpeg -i segment.mp4 -af loudnorm=I=-16:TP=-1.5:LRA=11 -c:v copy normalized.mp4
```
## Layer 4: Programmable Composition (Remotion)
Remotion turns editing problems into composable code. Use it for things that traditional editors make painful:
### When to use Remotion
- Overlays: text, images, branding, lower thirds
- Data visualizations: charts, stats, animated numbers
- Motion graphics: transitions, explainer animations
- Composable scenes: reusable templates across videos
- Product demos: annotated screenshots, UI highlights
### Basic Remotion composition
```tsx
import { AbsoluteFill, Sequence, Video, useCurrentFrame } from "remotion";
export const VlogComposition: React.FC = () => {
const frame = useCurrentFrame();
return (
<AbsoluteFill>
{/* Main footage */}
<Sequence from={0} durationInFrames={300}>
<Video src="/segments/intro.mp4" />
</Sequence>
{/* Title overlay */}
<Sequence from={30} durationInFrames={90}>
<AbsoluteFill style={{
justifyContent: "center",
alignItems: "center",
}}>
<h1 style={{
fontSize: 72,
color: "white",
textShadow: "2px 2px 8px rgba(0,0,0,0.8)",
}}>
The AI Editing Stack
</h1>
</AbsoluteFill>
</Sequence>
{/* Next segment */}
<Sequence from={300} durationInFrames={450}>
<Video src="/segments/demo.mp4" />
</Sequence>
</AbsoluteFill>
);
};
```
### Render output
```bash
npx remotion render src/index.ts VlogComposition output.mp4
```
See the [Remotion docs](https://www.remotion.dev/docs) for detailed patterns and API reference.
## Layer 5: Generated Assets (ElevenLabs / fal.ai)
Generate only what you need. Do not generate the whole video.
### Voiceover with ElevenLabs
```python
import os
import requests
resp = requests.post(
f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}",
headers={
"xi-api-key": os.environ["ELEVENLABS_API_KEY"],
"Content-Type": "application/json"
},
json={
"text": "Your narration text here",
"model_id": "eleven_turbo_v2_5",
"voice_settings": {"stability": 0.5, "similarity_boost": 0.75}
}
)
with open("voiceover.mp3", "wb") as f:
f.write(resp.content)
```
### Music and SFX with fal.ai
Use the `fal-ai-media` skill for:
- Background music generation
- Sound effects (ThinkSound model for video-to-audio)
- Transition sounds
### Generated visuals with fal.ai
Use for insert shots, thumbnails, or b-roll that doesn't exist:
```
generate(app_id: "fal-ai/nano-banana-pro", input_data: {
"prompt": "professional thumbnail for tech vlog, dark background, code on screen",
"image_size": "landscape_16_9"
})
```
### VideoDB generative audio
If VideoDB is configured:
```python
voiceover = coll.generate_voice(text="Narration here", voice="alloy")
music = coll.generate_music(prompt="lo-fi background for coding vlog", duration=120)
sfx = coll.generate_sound_effect(prompt="subtle whoosh transition")
```
## Layer 6: Final Polish (Descript / CapCut)
The last layer is human. Use a traditional editor for:
- **Pacing**: adjust cuts that feel too fast or slow
- **Captions**: auto-generated, then manually cleaned
- **Color grading**: basic correction and mood
- **Final audio mix**: balance voice, music, and SFX levels
- **Export**: platform-specific formats and quality settings
This is where taste lives. AI clears the repetitive work. You make the final calls.
## Social Media Reframing
Different platforms need different aspect ratios:
| Platform | Aspect Ratio | Resolution |
|----------|-------------|------------|
| YouTube | 16:9 | 1920x1080 |
| TikTok / Reels | 9:16 | 1080x1920 |
| Instagram Feed | 1:1 | 1080x1080 |
| X / Twitter | 16:9 or 1:1 | 1280x720 or 720x720 |
### Reframe with FFmpeg
```bash
# 16:9 to 9:16 (center crop)
ffmpeg -i input.mp4 -vf "crop=ih*9/16:ih,scale=1080:1920" vertical.mp4
# 16:9 to 1:1 (center crop)
ffmpeg -i input.mp4 -vf "crop=ih:ih,scale=1080:1080" square.mp4
```
### Reframe with VideoDB
```python
from videodb import ReframeMode
# Smart reframe (AI-guided subject tracking)
reframed = video.reframe(start=0, end=60, target="vertical", mode=ReframeMode.smart)
```
## Scene Detection and Auto-Cut
### FFmpeg scene detection
```bash
# Detect scene changes (threshold 0.3 = moderate sensitivity)
ffmpeg -i input.mp4 -vf "select='gt(scene,0.3)',showinfo" -vsync vfr -f null - 2>&1 | grep showinfo
```
### Silence detection for auto-cut
```bash
# Find silent segments (useful for cutting dead air)
ffmpeg -i input.mp4 -af silencedetect=noise=-30dB:d=2 -f null - 2>&1 | grep silence
```
### Highlight extraction
Use Claude to analyze transcript + scene timestamps:
```
"Given this transcript with timestamps and these scene change points,
identify the 5 most engaging 30-second clips for social media."
```
## What Each Tool Does Best
| Tool | Strength | Weakness |
|------|----------|----------|
| Claude / Codex | Organization, planning, code generation | Not the creative taste layer |
| FFmpeg | Deterministic cuts, batch processing, format conversion | No visual editing UI |
| Remotion | Programmable overlays, composable scenes, reusable templates | Learning curve for non-devs |
| Screen Studio | Polished screen recordings immediately | Only screen capture |
| ElevenLabs | Voice, narration, music, SFX | Not the center of the workflow |
| Descript / CapCut | Final pacing, captions, polish | Manual, not automatable |
## Key Principles
1. **Edit, don't generate.** This workflow is for cutting real footage, not creating from prompts.
2. **Structure before style.** Get the story right in Layer 2 before touching anything visual.
3. **FFmpeg is the backbone.** Boring but critical. Where long footage becomes manageable.
4. **Remotion for repeatability.** If you'll do it more than once, make it a Remotion component.
5. **Generate selectively.** Only use AI generation for assets that don't exist, not for everything.
6. **Taste is the last layer.** AI clears repetitive work. You make the final creative calls.
## Related Skills
- `fal-ai-media` — AI image, video, and audio generation
- `videodb` — Server-side video processing, indexing, and streaming
- `content-engine` — Platform-native content distribution

View File

@@ -108,7 +108,7 @@ The user must set `VIDEO_DB_API_KEY` using **either** method:
- **Export in terminal** (before starting Claude): `export VIDEO_DB_API_KEY=your-key`
- **Project `.env` file**: Save `VIDEO_DB_API_KEY=your-key` in the project's `.env` file
Get a free API key at https://console.videodb.io (50 free uploads, no credit card).
Get a free API key at [console.videodb.io](https://console.videodb.io) (50 free uploads, no credit card).
**Do NOT** read, write, or handle the API key yourself. Always let the user set it.
@@ -354,7 +354,6 @@ Reference documentation is in the `reference/` directory adjacent to this SKILL.
- [reference/capture-reference.md](reference/capture-reference.md) - Capture SDK and WebSocket events
- [reference/use-cases.md](reference/use-cases.md) - Common video processing patterns and examples
**Do not use ffmpeg, moviepy, or local encoding tools** when VideoDB supports the operation. The following are all handled server-side by VideoDB — trimming, combining clips, overlaying audio or music, adding subtitles, text/image overlays, transcoding, resolution changes, aspect-ratio conversion, resizing for platform requirements, transcription, and media generation. Only fall back to local tools for operations listed under Limitations in reference/editor.md (transitions, speed changes, crop/zoom, colour grading, volume mixing).
### When to use what

View File

@@ -380,7 +380,7 @@ results = video.search(
```
> **Note:** `filter` is an explicit named parameter in `video.search()`. `scene_index_id` is passed through `**kwargs` to the API.
>
> **Important:** `video.search()` raises `InvalidRequestError` with message `"No results found"` when there are no matches. Always wrap search calls in try/except. For scene search, use `score_threshold=0.3` or higher to filter low-relevance noise.
For scene search, use `search_type=SearchType.semantic` with `index_type=IndexType.scene`. Pass `scene_index_id` when targeting a specific scene index. See [search.md](search.md) for details.

View File

@@ -107,7 +107,7 @@ Use [scripts/ws_listener.py](../scripts/ws_listener.py) to connect and dump even
}
```
> For latest details, see https://docs.videodb.io/pages/ingest/capture-sdks/realtime-context.md
> For latest details, see [VideoDB Realtime Context docs](https://docs.videodb.io/pages/ingest/capture-sdks/realtime-context.md).
---

209
skills/x-api/SKILL.md Normal file
View File

@@ -0,0 +1,209 @@
---
name: x-api
description: X/Twitter API integration for posting tweets, threads, reading timelines, search, and analytics. Covers OAuth auth patterns, rate limits, and platform-native content posting. Use when the user wants to interact with X programmatically.
origin: ECC
---
# X API
Programmatic interaction with X (Twitter) for posting, reading, searching, and analytics.
## When to Activate
- User wants to post tweets or threads programmatically
- Reading timeline, mentions, or user data from X
- Searching X for content, trends, or conversations
- Building X integrations or bots
- Analytics and engagement tracking
- User says "post to X", "tweet", "X API", or "Twitter API"
## Authentication
### OAuth 2.0 Bearer Token (App-Only)
Best for: read-heavy operations, search, public data.
```bash
# Environment setup
export X_BEARER_TOKEN="your-bearer-token"
```
```python
import os
import requests
bearer = os.environ["X_BEARER_TOKEN"]
headers = {"Authorization": f"Bearer {bearer}"}
# Search recent tweets
resp = requests.get(
"https://api.x.com/2/tweets/search/recent",
headers=headers,
params={"query": "claude code", "max_results": 10}
)
tweets = resp.json()
```
### OAuth 1.0a (User Context)
Required for: posting tweets, managing account, DMs.
```bash
# Environment setup — source before use
export X_API_KEY="your-api-key"
export X_API_SECRET="your-api-secret"
export X_ACCESS_TOKEN="your-access-token"
export X_ACCESS_SECRET="your-access-secret"
```
```python
import os
from requests_oauthlib import OAuth1Session
oauth = OAuth1Session(
os.environ["X_API_KEY"],
client_secret=os.environ["X_API_SECRET"],
resource_owner_key=os.environ["X_ACCESS_TOKEN"],
resource_owner_secret=os.environ["X_ACCESS_SECRET"],
)
```
## Core Operations
### Post a Tweet
```python
resp = oauth.post(
"https://api.x.com/2/tweets",
json={"text": "Hello from Claude Code"}
)
resp.raise_for_status()
tweet_id = resp.json()["data"]["id"]
```
### Post a Thread
```python
def post_thread(oauth, tweets: list[str]) -> list[str]:
ids = []
reply_to = None
for text in tweets:
payload = {"text": text}
if reply_to:
payload["reply"] = {"in_reply_to_tweet_id": reply_to}
resp = oauth.post("https://api.x.com/2/tweets", json=payload)
resp.raise_for_status()
tweet_id = resp.json()["data"]["id"]
ids.append(tweet_id)
reply_to = tweet_id
return ids
```
### Read User Timeline
```python
resp = requests.get(
f"https://api.x.com/2/users/{user_id}/tweets",
headers=headers,
params={
"max_results": 10,
"tweet.fields": "created_at,public_metrics",
}
)
```
### Search Tweets
```python
resp = requests.get(
"https://api.x.com/2/tweets/search/recent",
headers=headers,
params={
"query": "from:affaanmustafa -is:retweet",
"max_results": 10,
"tweet.fields": "public_metrics,created_at",
}
)
```
### Get User by Username
```python
resp = requests.get(
"https://api.x.com/2/users/by/username/affaanmustafa",
headers=headers,
params={"user.fields": "public_metrics,description,created_at"}
)
```
### Upload Media and Post
```python
# Media upload uses v1.1 endpoint
# Step 1: Upload media
media_resp = oauth.post(
"https://upload.twitter.com/1.1/media/upload.json",
files={"media": open("image.png", "rb")}
)
media_id = media_resp.json()["media_id_string"]
# Step 2: Post with media
resp = oauth.post(
"https://api.x.com/2/tweets",
json={"text": "Check this out", "media": {"media_ids": [media_id]}}
)
```
## Rate Limits
X API rate limits vary by endpoint, auth method, and account tier, and they change over time. Always:
- Check the current X developer docs before hardcoding assumptions
- Read `x-rate-limit-remaining` and `x-rate-limit-reset` headers at runtime
- Back off automatically instead of relying on static tables in code
```python
import time
remaining = int(resp.headers.get("x-rate-limit-remaining", 0))
if remaining < 5:
reset = int(resp.headers.get("x-rate-limit-reset", 0))
wait = max(0, reset - int(time.time()))
print(f"Rate limit approaching. Resets in {wait}s")
```
## Error Handling
```python
resp = oauth.post("https://api.x.com/2/tweets", json={"text": content})
if resp.status_code == 201:
return resp.json()["data"]["id"]
elif resp.status_code == 429:
reset = int(resp.headers["x-rate-limit-reset"])
raise Exception(f"Rate limited. Resets at {reset}")
elif resp.status_code == 403:
raise Exception(f"Forbidden: {resp.json().get('detail', 'check permissions')}")
else:
raise Exception(f"X API error {resp.status_code}: {resp.text}")
```
## Security
- **Never hardcode tokens.** Use environment variables or `.env` files.
- **Never commit `.env` files.** Add to `.gitignore`.
- **Rotate tokens** if exposed. Regenerate at developer.x.com.
- **Use read-only tokens** when write access is not needed.
- **Store OAuth secrets securely** — not in source code or logs.
## Integration with Content Engine
Use `content-engine` skill to generate platform-native content, then post via X API:
1. Generate content with content-engine (X platform format)
2. Validate length (280 chars for single tweet)
3. Post via X API using patterns above
4. Track engagement via public_metrics
## Related Skills
- `content-engine` — Generate platform-native content for X
- `crosspost` — Distribute content across X, LinkedIn, and other platforms

View File

@@ -0,0 +1,70 @@
/**
* Tests for `.codex/config.toml` reference defaults.
*
* Run with: node tests/codex-config.test.js
*/
const assert = require('assert');
const fs = require('fs');
const path = require('path');
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
const repoRoot = path.join(__dirname, '..');
const configPath = path.join(repoRoot, '.codex', 'config.toml');
const config = fs.readFileSync(configPath, 'utf8');
const codexAgentsDir = path.join(repoRoot, '.codex', 'agents');
let passed = 0;
let failed = 0;
if (
test('reference config does not pin a top-level model', () => {
assert.ok(!/^model\s*=/m.test(config), 'Expected `.codex/config.toml` to inherit the CLI default model');
})
)
passed++;
else failed++;
if (
test('reference config does not pin a top-level model provider', () => {
assert.ok(
!/^model_provider\s*=/m.test(config),
'Expected `.codex/config.toml` to inherit the CLI default provider',
);
})
)
passed++;
else failed++;
if (
test('sample Codex role configs do not use o4-mini', () => {
const roleFiles = fs.readdirSync(codexAgentsDir).filter(file => file.endsWith('.toml'));
assert.ok(roleFiles.length > 0, 'Expected sample role config files under `.codex/agents`');
for (const roleFile of roleFiles) {
const rolePath = path.join(codexAgentsDir, roleFile);
const roleConfig = fs.readFileSync(rolePath, 'utf8');
assert.ok(
!/^model\s*=\s*"o4-mini"$/m.test(roleConfig),
`Expected sample role config to avoid o4-mini: ${roleFile}`,
);
}
})
)
passed++;
else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);

View File

@@ -98,6 +98,44 @@ function cleanupTestDir(testDir) {
fs.rmSync(testDir, { recursive: true, force: true });
}
function normalizeComparablePath(targetPath) {
if (!targetPath) return '';
let normalizedPath = String(targetPath).trim().replace(/\\/g, '/');
if (/^\/[a-zA-Z]\//.test(normalizedPath)) {
normalizedPath = `${normalizedPath[1]}:/${normalizedPath.slice(3)}`;
}
if (/^[a-zA-Z]:\//.test(normalizedPath)) {
normalizedPath = `${normalizedPath[0].toUpperCase()}:${normalizedPath.slice(2)}`;
}
try {
normalizedPath = fs.realpathSync(normalizedPath);
} catch {
// Fall through to string normalization when the path cannot be resolved directly.
}
return path.normalize(normalizedPath).replace(/\\/g, '/').replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
}
function pathsReferToSameLocation(leftPath, rightPath) {
const normalizedLeftPath = normalizeComparablePath(leftPath);
const normalizedRightPath = normalizeComparablePath(rightPath);
if (!normalizedLeftPath || !normalizedRightPath) return false;
if (normalizedLeftPath === normalizedRightPath) return true;
try {
const leftStats = fs.statSync(normalizedLeftPath);
const rightStats = fs.statSync(normalizedRightPath);
return leftStats.dev === rightStats.dev && leftStats.ino === rightStats.ino;
} catch {
return false;
}
}
function createCommandShim(binDir, baseName, logFile) {
fs.mkdirSync(binDir, { recursive: true });
@@ -155,6 +193,7 @@ async function runTests() {
let passed = 0;
let failed = 0;
let skipped = 0;
const scriptsDir = path.join(__dirname, '..', '..', 'scripts', 'hooks');
@@ -360,22 +399,30 @@ async function runTests() {
if (
await asyncTest('creates or updates session file', async () => {
// Run the script
await runScript(path.join(scriptsDir, 'session-end.js'));
const isoHome = path.join(os.tmpdir(), `ecc-session-create-${Date.now()}`);
// Check if session file was created
// Note: Without CLAUDE_SESSION_ID, falls back to project name (not 'default')
// Use local time to match the script's getDateString() function
const sessionsDir = path.join(os.homedir(), '.claude', 'sessions');
const now = new Date();
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
try {
await runScript(path.join(scriptsDir, 'session-end.js'), '', {
HOME: isoHome,
USERPROFILE: isoHome
});
// Get the expected session ID (project name fallback)
const utils = require('../../scripts/lib/utils');
const expectedId = utils.getSessionIdShort();
const sessionFile = path.join(sessionsDir, `${today}-${expectedId}-session.tmp`);
// Check if session file was created
// Note: Without CLAUDE_SESSION_ID, falls back to project/worktree name (not 'default')
// Use local time to match the script's getDateString() function
const sessionsDir = path.join(isoHome, '.claude', 'sessions');
const now = new Date();
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
assert.ok(fs.existsSync(sessionFile), `Session file should exist: ${sessionFile}`);
// Get the expected session ID (project name fallback)
const utils = require('../../scripts/lib/utils');
const expectedId = utils.getSessionIdShort();
const sessionFile = path.join(sessionsDir, `${today}-${expectedId}-session.tmp`);
assert.ok(fs.existsSync(sessionFile), `Session file should exist: ${sessionFile}`);
} finally {
fs.rmSync(isoHome, { recursive: true, force: true });
}
})
)
passed++;
@@ -404,6 +451,39 @@ async function runTests() {
passed++;
else failed++;
if (
await asyncTest('writes project, branch, and worktree metadata into new session files', async () => {
const isoHome = path.join(os.tmpdir(), `ecc-session-metadata-${Date.now()}`);
const testSessionId = 'test-session-meta1234';
const expectedShortId = testSessionId.slice(-8);
const topLevel = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).stdout.trim();
const branch = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { encoding: 'utf8' }).stdout.trim();
const project = path.basename(topLevel);
try {
const result = await runScript(path.join(scriptsDir, 'session-end.js'), '', {
HOME: isoHome,
USERPROFILE: isoHome,
CLAUDE_SESSION_ID: testSessionId
});
assert.strictEqual(result.code, 0, 'Hook should exit 0');
const now = new Date();
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
const sessionFile = path.join(isoHome, '.claude', 'sessions', `${today}-${expectedShortId}-session.tmp`);
const content = fs.readFileSync(sessionFile, 'utf8');
assert.ok(content.includes(`**Project:** ${project}`), 'Should persist project metadata');
assert.ok(content.includes(`**Branch:** ${branch}`), 'Should persist branch metadata');
assert.ok(content.includes(`**Worktree:** ${process.cwd()}`), 'Should persist worktree metadata');
} finally {
fs.rmSync(isoHome, { recursive: true, force: true });
}
})
)
passed++;
else failed++;
// pre-compact.js tests
console.log('\npre-compact.js:');
@@ -1218,7 +1298,10 @@ async function runTests() {
fs.writeFileSync(transcriptPath, lines.join('\n'));
const stdinJson = JSON.stringify({ transcript_path: transcriptPath });
const result = await runScript(path.join(scriptsDir, 'session-end.js'), stdinJson);
const result = await runScript(path.join(scriptsDir, 'session-end.js'), stdinJson, {
HOME: testDir,
USERPROFILE: testDir
});
assert.strictEqual(result.code, 0);
// Session file should contain summary with tools used
assert.ok(result.stderr.includes('Created session file') || result.stderr.includes('Updated session file'), 'Should create/update session file');
@@ -2148,7 +2231,11 @@ async function runTests() {
passed++;
else failed++;
if (
if (process.platform === 'win32') {
console.log(' - detect-project writes project metadata to the registry and project directory');
console.log(' (skipped — bash script paths are not Windows-compatible)');
skipped++;
} else if (
await asyncTest('detect-project writes project metadata to the registry and project directory', async () => {
const testRoot = createTestDir();
const homeDir = path.join(testRoot, 'home');
@@ -2185,9 +2272,9 @@ async function runTests() {
assert.strictEqual(code, 0, `detect-project should source cleanly, stderr: ${stderr}`);
const [projectId, projectDir] = stdout.trim().split(/\r?\n/);
const [projectId] = stdout.trim().split(/\r?\n/);
const registryPath = path.join(homeDir, '.claude', 'homunculus', 'projects.json');
const projectMetadataPath = path.join(projectDir, 'project.json');
const projectMetadataPath = path.join(homeDir, '.claude', 'homunculus', 'projects', projectId, 'project.json');
assert.ok(projectId, 'detect-project should emit a project id');
assert.ok(fs.existsSync(registryPath), 'projects.json should be created');
@@ -2199,7 +2286,13 @@ async function runTests() {
assert.ok(registry[projectId], 'registry should contain the detected project');
assert.strictEqual(metadata.id, projectId, 'project.json should include the detected id');
assert.strictEqual(metadata.name, path.basename(repoDir), 'project.json should include the repo name');
assert.strictEqual(fs.realpathSync(metadata.root), fs.realpathSync(repoDir), 'project.json should include the repo root');
const normalizedMetadataRoot = normalizeComparablePath(metadata.root);
const normalizedRepoDir = normalizeComparablePath(repoDir);
assert.ok(normalizedMetadataRoot, 'project.json should include a non-empty repo root');
assert.ok(
pathsReferToSameLocation(normalizedMetadataRoot, normalizedRepoDir),
`project.json should include the repo root (expected ${normalizedRepoDir}, got ${normalizedMetadataRoot})`,
);
assert.strictEqual(metadata.remote, 'https://github.com/example/ecc-test.git', 'project.json should include the sanitized remote');
assert.ok(metadata.created_at, 'project.json should include created_at');
assert.ok(metadata.last_seen, 'project.json should include last_seen');
@@ -2521,6 +2614,42 @@ async function runTests() {
passed++;
else failed++;
if (
await asyncTest('normalizes existing session headers with project, branch, and worktree metadata', async () => {
const testDir = createTestDir();
const sessionsDir = path.join(testDir, '.claude', 'sessions');
fs.mkdirSync(sessionsDir, { recursive: true });
const utils = require('../../scripts/lib/utils');
const today = utils.getDateString();
const shortId = 'update04';
const sessionFile = path.join(sessionsDir, `${today}-${shortId}-session.tmp`);
const branch = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { encoding: 'utf8' }).stdout.trim();
const project = path.basename(spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).stdout.trim());
fs.writeFileSync(
sessionFile,
`# Session: ${today}\n**Date:** ${today}\n**Started:** 09:00\n**Last Updated:** 09:00\n\n---\n\n## Current State\n\n[Session context goes here]\n`
);
const result = await runScript(path.join(scriptsDir, 'session-end.js'), '', {
HOME: testDir,
USERPROFILE: testDir,
CLAUDE_SESSION_ID: `session-${shortId}`
});
assert.strictEqual(result.code, 0);
const updated = fs.readFileSync(sessionFile, 'utf8');
assert.ok(updated.includes(`**Project:** ${project}`), 'Should inject project metadata into existing headers');
assert.ok(updated.includes(`**Branch:** ${branch}`), 'Should inject branch metadata into existing headers');
assert.ok(updated.includes(`**Worktree:** ${process.cwd()}`), 'Should inject worktree metadata into existing headers');
cleanupTestDir(testDir);
})
)
passed++;
else failed++;
if (
await asyncTest('replaces blank template with summary when updating existing file', async () => {
const testDir = createTestDir();
@@ -3888,6 +4017,8 @@ async function runTests() {
try {
const result = await runScript(path.join(scriptsDir, 'session-end.js'), oversizedPayload, {
HOME: testDir,
USERPROFILE: testDir,
CLAUDE_TRANSCRIPT_PATH: transcriptPath
});
assert.strictEqual(result.code, 0, 'Should exit 0 even with oversized stdin');
@@ -4311,12 +4442,12 @@ async function runTests() {
// ── Round 74: session-start.js main().catch handler ──
console.log('\nRound 74: session-start.js (main catch — unrecoverable error):');
if (
if (process.platform === 'win32') {
console.log(' - session-start exits 0 with error message when HOME is non-directory');
console.log(' (skipped — /dev/null not available on Windows)');
skipped++;
} else if (
await asyncTest('session-start exits 0 with error message when HOME is non-directory', async () => {
if (process.platform === 'win32') {
console.log(' (skipped — /dev/null not available on Windows)');
return;
}
// HOME=/dev/null makes ensureDir(sessionsDir) throw ENOTDIR,
// which propagates to main().catch — the top-level error boundary
const result = await runScript(path.join(scriptsDir, 'session-start.js'), '', {
@@ -4333,12 +4464,12 @@ async function runTests() {
// ── Round 75: pre-compact.js main().catch handler ──
console.log('\nRound 75: pre-compact.js (main catch — unrecoverable error):');
if (
if (process.platform === 'win32') {
console.log(' - pre-compact exits 0 with error message when HOME is non-directory');
console.log(' (skipped — /dev/null not available on Windows)');
skipped++;
} else if (
await asyncTest('pre-compact exits 0 with error message when HOME is non-directory', async () => {
if (process.platform === 'win32') {
console.log(' (skipped — /dev/null not available on Windows)');
return;
}
// HOME=/dev/null makes ensureDir(sessionsDir) throw ENOTDIR,
// which propagates to main().catch — the top-level error boundary
const result = await runScript(path.join(scriptsDir, 'pre-compact.js'), '', {
@@ -4355,12 +4486,12 @@ async function runTests() {
// ── Round 75: session-end.js main().catch handler ──
console.log('\nRound 75: session-end.js (main catch — unrecoverable error):');
if (
if (process.platform === 'win32') {
console.log(' - session-end exits 0 with error message when HOME is non-directory');
console.log(' (skipped — /dev/null not available on Windows)');
skipped++;
} else if (
await asyncTest('session-end exits 0 with error message when HOME is non-directory', async () => {
if (process.platform === 'win32') {
console.log(' (skipped — /dev/null not available on Windows)');
return;
}
// HOME=/dev/null makes ensureDir(sessionsDir) throw ENOTDIR inside main(),
// which propagates to runMain().catch — the top-level error boundary
const result = await runScript(path.join(scriptsDir, 'session-end.js'), '{}', {
@@ -4377,12 +4508,12 @@ async function runTests() {
// ── Round 76: evaluate-session.js main().catch handler ──
console.log('\nRound 76: evaluate-session.js (main catch — unrecoverable error):');
if (
if (process.platform === 'win32') {
console.log(' - evaluate-session exits 0 with error message when HOME is non-directory');
console.log(' (skipped — /dev/null not available on Windows)');
skipped++;
} else if (
await asyncTest('evaluate-session exits 0 with error message when HOME is non-directory', async () => {
if (process.platform === 'win32') {
console.log(' (skipped — /dev/null not available on Windows)');
return;
}
// HOME=/dev/null makes ensureDir(learnedSkillsPath) throw ENOTDIR,
// which propagates to main().catch — the top-level error boundary
const result = await runScript(path.join(scriptsDir, 'evaluate-session.js'), '{}', {
@@ -4399,12 +4530,12 @@ async function runTests() {
// ── Round 76: suggest-compact.js main().catch handler ──
console.log('\nRound 76: suggest-compact.js (main catch — double-failure):');
if (
if (process.platform === 'win32') {
console.log(' - suggest-compact exits 0 with error when TMPDIR is non-directory');
console.log(' (skipped — /dev/null not available on Windows)');
skipped++;
} else if (
await asyncTest('suggest-compact exits 0 with error when TMPDIR is non-directory', async () => {
if (process.platform === 'win32') {
console.log(' (skipped — /dev/null not available on Windows)');
return;
}
// TMPDIR=/dev/null causes openSync to fail (ENOTDIR), then the catch
// fallback writeFile also fails, propagating to main().catch
const result = await runScript(path.join(scriptsDir, 'suggest-compact.js'), '', {
@@ -4517,10 +4648,20 @@ async function runTests() {
const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith('.tmp'));
assert.ok(files.length > 0, 'Should create session file');
const content = fs.readFileSync(path.join(sessionsDir, files[0]), 'utf8');
const summaryMatch = content.match(
/<!-- ECC:SUMMARY:START -->([\s\S]*?)<!-- ECC:SUMMARY:END -->/
);
// The real string message should appear
assert.ok(content.includes('Real user message'), 'Should include the string content user message');
// Numeric/boolean/object content should NOT appear as text
assert.ok(!content.includes('42'), 'Numeric content should be skipped (else branch → empty string → filtered)');
assert.ok(summaryMatch, 'Should include a generated summary block');
const summaryBlock = summaryMatch[1];
// Numeric/boolean/object content should NOT appear as task bullets
assert.ok(
!summaryBlock.includes('\n- 42\n'),
'Numeric content should be skipped (else branch → empty string → filtered)'
);
assert.ok(!summaryBlock.includes('\n- true\n'), 'Boolean content should be skipped');
assert.ok(!summaryBlock.includes('[object Object]'), 'Object content should be skipped');
} finally {
fs.rmSync(isoHome, { recursive: true, force: true });
}
@@ -4876,7 +5017,8 @@ Some random content without the expected ### Context to Load section
console.log('\n=== Test Results ===');
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
console.log(`Total: ${passed + failed}\n`);
console.log(`Skipped: ${skipped}`);
console.log(`Total: ${passed + failed + skipped}\n`);
process.exit(failed > 0 ? 1 : 0);
}

View File

@@ -0,0 +1,318 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
buildSessionSnapshot,
loadWorkerSnapshots,
parseWorkerHandoff,
parseWorkerStatus,
parseWorkerTask,
resolveSnapshotTarget
} = require('../../scripts/lib/orchestration-session');
console.log('=== Testing orchestration-session.js ===\n');
let passed = 0;
let failed = 0;
function test(desc, fn) {
try {
fn();
console.log(`${desc}`);
passed++;
} catch (error) {
console.log(`${desc}: ${error.message}`);
failed++;
}
}
test('parseWorkerStatus extracts structured status fields', () => {
const status = parseWorkerStatus([
'# Status',
'',
'- State: completed',
'- Updated: 2026-03-12T14:09:15Z',
'- Branch: feature-branch',
'- Worktree: `/tmp/worktree`',
'',
'- Handoff file: `/tmp/handoff.md`'
].join('\n'));
assert.deepStrictEqual(status, {
state: 'completed',
updated: '2026-03-12T14:09:15Z',
branch: 'feature-branch',
worktree: '/tmp/worktree',
taskFile: null,
handoffFile: '/tmp/handoff.md'
});
});
test('parseWorkerTask extracts objective and seeded overlays', () => {
const task = parseWorkerTask([
'# Worker Task',
'',
'## Seeded Local Overlays',
'- `scripts/orchestrate-worktrees.js`',
'- `commands/orchestrate.md`',
'',
'## Objective',
'Verify seeded files and summarize status.'
].join('\n'));
assert.deepStrictEqual(task.seedPaths, [
'scripts/orchestrate-worktrees.js',
'commands/orchestrate.md'
]);
assert.strictEqual(task.objective, 'Verify seeded files and summarize status.');
});
test('parseWorkerHandoff extracts summary, validation, and risks', () => {
const handoff = parseWorkerHandoff([
'# Handoff',
'',
'## Summary',
'- Worker completed successfully',
'',
'## Validation',
'- Ran tests',
'',
'## Remaining Risks',
'- No runtime screenshot'
].join('\n'));
assert.deepStrictEqual(handoff.summary, ['Worker completed successfully']);
assert.deepStrictEqual(handoff.validation, ['Ran tests']);
assert.deepStrictEqual(handoff.remainingRisks, ['No runtime screenshot']);
});
test('parseWorkerHandoff also supports bold section headers', () => {
const handoff = parseWorkerHandoff([
'# Handoff',
'',
'**Summary**',
'- Worker completed successfully',
'',
'**Validation**',
'- Ran tests',
'',
'**Remaining Risks**',
'- No runtime screenshot'
].join('\n'));
assert.deepStrictEqual(handoff.summary, ['Worker completed successfully']);
assert.deepStrictEqual(handoff.validation, ['Ran tests']);
assert.deepStrictEqual(handoff.remainingRisks, ['No runtime screenshot']);
});
test('parseWorkerHandoff accepts legacy verification and follow-up headings', () => {
const handoff = parseWorkerHandoff([
'# Handoff',
'',
'## Summary',
'- Worker completed successfully',
'',
'## Tests / Verification',
'- Ran tests',
'',
'## Follow-ups',
'- Re-run screenshots after deploy'
].join('\n'));
assert.deepStrictEqual(handoff.summary, ['Worker completed successfully']);
assert.deepStrictEqual(handoff.validation, ['Ran tests']);
assert.deepStrictEqual(handoff.remainingRisks, ['Re-run screenshots after deploy']);
});
test('loadWorkerSnapshots reads coordination worker directories', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orch-session-'));
const coordinationDir = path.join(tempRoot, 'coordination');
const workerDir = path.join(coordinationDir, 'seed-check');
const proofDir = path.join(coordinationDir, 'proof');
fs.mkdirSync(workerDir, { recursive: true });
fs.mkdirSync(proofDir, { recursive: true });
try {
fs.writeFileSync(path.join(workerDir, 'status.md'), [
'# Status',
'',
'- State: running',
'- Branch: seed-branch',
'- Worktree: `/tmp/seed-worktree`'
].join('\n'));
fs.writeFileSync(path.join(workerDir, 'task.md'), [
'# Worker Task',
'',
'## Objective',
'Inspect seed paths.'
].join('\n'));
fs.writeFileSync(path.join(workerDir, 'handoff.md'), [
'# Handoff',
'',
'## Summary',
'- Pending'
].join('\n'));
const workers = loadWorkerSnapshots(coordinationDir);
assert.strictEqual(workers.length, 1);
assert.strictEqual(workers[0].workerSlug, 'seed-check');
assert.strictEqual(workers[0].status.branch, 'seed-branch');
assert.strictEqual(workers[0].task.objective, 'Inspect seed paths.');
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
test('buildSessionSnapshot merges tmux panes with worker metadata', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orch-snapshot-'));
const coordinationDir = path.join(tempRoot, 'coordination');
const workerDir = path.join(coordinationDir, 'seed-check');
fs.mkdirSync(workerDir, { recursive: true });
try {
fs.writeFileSync(path.join(workerDir, 'status.md'), '- State: completed\n- Branch: seed-branch\n');
fs.writeFileSync(path.join(workerDir, 'task.md'), '## Objective\nInspect seed paths.\n');
fs.writeFileSync(path.join(workerDir, 'handoff.md'), '## Summary\n- ok\n');
const snapshot = buildSessionSnapshot({
sessionName: 'workflow-visual-proof',
coordinationDir,
panes: [
{
paneId: '%95',
windowIndex: 1,
paneIndex: 2,
title: 'seed-check',
currentCommand: 'codex',
currentPath: '/tmp/worktree',
active: false,
dead: false,
pid: 1234
}
]
});
assert.strictEqual(snapshot.sessionActive, true);
assert.strictEqual(snapshot.workerCount, 1);
assert.strictEqual(snapshot.workerStates.completed, 1);
assert.strictEqual(snapshot.workers[0].pane.paneId, '%95');
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
test('resolveSnapshotTarget handles plan files and direct session names', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orch-target-'));
const repoRoot = path.join(tempRoot, 'repo');
fs.mkdirSync(repoRoot, { recursive: true });
const planPath = path.join(repoRoot, 'plan.json');
fs.writeFileSync(planPath, JSON.stringify({
sessionName: 'workflow-visual-proof',
repoRoot,
coordinationRoot: path.join(repoRoot, '.claude', 'orchestration')
}));
try {
const fromPlan = resolveSnapshotTarget(planPath, repoRoot);
assert.strictEqual(fromPlan.targetType, 'plan');
assert.strictEqual(fromPlan.sessionName, 'workflow-visual-proof');
const fromSession = resolveSnapshotTarget('workflow-visual-proof', repoRoot);
assert.strictEqual(fromSession.targetType, 'session');
assert.ok(fromSession.coordinationDir.endsWith(path.join('.orchestration', 'workflow-visual-proof')));
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
test('resolveSnapshotTarget normalizes plan session names and defaults to the repo name', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orch-target-'));
const repoRoot = path.join(tempRoot, 'My Repo');
fs.mkdirSync(repoRoot, { recursive: true });
const namedPlanPath = path.join(repoRoot, 'named-plan.json');
const defaultPlanPath = path.join(repoRoot, 'default-plan.json');
fs.writeFileSync(namedPlanPath, JSON.stringify({
sessionName: 'Workflow Visual Proof',
repoRoot
}));
fs.writeFileSync(defaultPlanPath, JSON.stringify({ repoRoot }));
try {
const namedPlan = resolveSnapshotTarget(namedPlanPath, repoRoot);
assert.strictEqual(namedPlan.sessionName, 'workflow-visual-proof');
assert.ok(namedPlan.coordinationDir.endsWith(path.join('.orchestration', 'workflow-visual-proof')));
const defaultPlan = resolveSnapshotTarget(defaultPlanPath, repoRoot);
assert.strictEqual(defaultPlan.sessionName, 'my-repo');
assert.ok(defaultPlan.coordinationDir.endsWith(path.join('.orchestration', 'my-repo')));
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
test('resolveSnapshotTarget rejects malformed plan files and invalid config fields', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orch-target-'));
const repoRoot = path.join(tempRoot, 'repo');
fs.mkdirSync(repoRoot, { recursive: true });
const invalidJsonPath = path.join(repoRoot, 'invalid-json.json');
const blankFieldsPath = path.join(repoRoot, 'blank-fields.json');
const invalidSessionNamePath = path.join(repoRoot, 'invalid-session.json');
const invalidRepoRootPath = path.join(repoRoot, 'invalid-repo-root.json');
const invalidCoordinationRootPath = path.join(repoRoot, 'invalid-coordination-root.json');
fs.writeFileSync(invalidJsonPath, '{not valid json');
fs.writeFileSync(blankFieldsPath, JSON.stringify({
sessionName: ' ',
repoRoot: ' ',
coordinationRoot: ' '
}));
fs.writeFileSync(invalidSessionNamePath, JSON.stringify({
sessionName: 42,
repoRoot
}));
fs.writeFileSync(invalidRepoRootPath, JSON.stringify({
sessionName: 'workflow',
repoRoot: ['not-a-string']
}));
fs.writeFileSync(invalidCoordinationRootPath, JSON.stringify({
sessionName: 'workflow',
repoRoot,
coordinationRoot: false
}));
try {
const blankFields = resolveSnapshotTarget(blankFieldsPath, repoRoot);
assert.strictEqual(blankFields.sessionName, 'repo');
assert.strictEqual(blankFields.repoRoot, repoRoot);
assert.ok(blankFields.coordinationDir.endsWith(path.join('.orchestration', 'repo')));
assert.throws(
() => resolveSnapshotTarget(invalidJsonPath, repoRoot),
/Invalid orchestration plan JSON/
);
assert.throws(
() => resolveSnapshotTarget(invalidSessionNamePath, repoRoot),
/sessionName must be a string when provided/
);
assert.throws(
() => resolveSnapshotTarget(invalidRepoRootPath, repoRoot),
/repoRoot must be a string when provided/
);
assert.throws(
() => resolveSnapshotTarget(invalidCoordinationRootPath, repoRoot),
/coordinationRoot must be a string when provided/
);
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
if (failed > 0) process.exit(1);

View File

@@ -80,9 +80,10 @@ function runTests() {
assert.strictEqual(result.shortId, 'abcdef12345678');
})) passed++; else failed++;
if (test('rejects short IDs less than 8 chars', () => {
if (test('accepts short IDs under 8 chars', () => {
const result = sessionManager.parseSessionFilename('2026-02-01-abc-session.tmp');
assert.strictEqual(result, null);
assert.ok(result);
assert.strictEqual(result.shortId, 'abc');
})) passed++; else failed++;
// parseSessionMetadata tests
@@ -94,6 +95,9 @@ function runTests() {
**Date:** 2026-02-01
**Started:** 10:30
**Last Updated:** 14:45
**Project:** everything-claude-code
**Branch:** feature/session-metadata
**Worktree:** /tmp/ecc-worktree
### Completed
- [x] Set up project
@@ -114,6 +118,9 @@ src/main.ts
assert.strictEqual(meta.date, '2026-02-01');
assert.strictEqual(meta.started, '10:30');
assert.strictEqual(meta.lastUpdated, '14:45');
assert.strictEqual(meta.project, 'everything-claude-code');
assert.strictEqual(meta.branch, 'feature/session-metadata');
assert.strictEqual(meta.worktree, '/tmp/ecc-worktree');
assert.strictEqual(meta.completed.length, 2);
assert.strictEqual(meta.completed[0], 'Set up project');
assert.strictEqual(meta.inProgress.length, 1);
@@ -578,9 +585,16 @@ src/main.ts
// parseSessionFilename edge cases
console.log('\nparseSessionFilename (additional edge cases):');
if (test('rejects uppercase letters in short ID', () => {
if (test('accepts uppercase letters in short ID', () => {
const result = sessionManager.parseSessionFilename('2026-02-01-ABCD1234-session.tmp');
assert.strictEqual(result, null, 'Uppercase letters should be rejected');
assert.ok(result, 'Uppercase letters should be accepted');
assert.strictEqual(result.shortId, 'ABCD1234');
})) passed++; else failed++;
if (test('accepts underscores in short ID', () => {
const result = sessionManager.parseSessionFilename('2026-02-01-ChezMoi_2-session.tmp');
assert.ok(result, 'Underscores should be accepted');
assert.strictEqual(result.shortId, 'ChezMoi_2');
})) passed++; else failed++;
if (test('accepts hyphenated short IDs (extra segments)', () => {
@@ -1910,20 +1924,22 @@ file.ts
'Year 100+ is not affected by the 0-99 → 1900-1999 mapping');
})) passed++; else failed++;
// ── Round 110: parseSessionFilename rejects uppercase IDs (regex is [a-z0-9]) ──
console.log('\nRound 110: parseSessionFilename (uppercase ID — regex [a-z0-9]{8,} rejects [A-Z]):');
if (test('parseSessionFilename rejects filenames with uppercase characters in short ID', () => {
// SESSION_FILENAME_REGEX uses [a-z0-9]{8,} — strictly lowercase
// ── Round 110: parseSessionFilename accepts mixed-case IDs ──
console.log('\nRound 110: parseSessionFilename (mixed-case IDs are accepted):');
if (test('parseSessionFilename accepts filenames with uppercase characters in short ID', () => {
const upperResult = sessionManager.parseSessionFilename('2026-01-15-ABCD1234-session.tmp');
assert.strictEqual(upperResult, null,
'All-uppercase ID should be rejected by [a-z0-9]{8,}');
assert.notStrictEqual(upperResult, null,
'All-uppercase ID should be accepted');
assert.strictEqual(upperResult.shortId, 'ABCD1234');
const mixedResult = sessionManager.parseSessionFilename('2026-01-15-AbCd1234-session.tmp');
assert.strictEqual(mixedResult, null,
'Mixed-case ID should be rejected by [a-z0-9]{8,}');
// Confirm lowercase is accepted
assert.notStrictEqual(mixedResult, null,
'Mixed-case ID should be accepted');
assert.strictEqual(mixedResult.shortId, 'AbCd1234');
const lowerResult = sessionManager.parseSessionFilename('2026-01-15-abcd1234-session.tmp');
assert.notStrictEqual(lowerResult, null,
'All-lowercase ID should be accepted');
'All-lowercase ID should still be accepted');
assert.strictEqual(lowerResult.shortId, 'abcd1234');
})) passed++; else failed++;
@@ -2190,36 +2206,34 @@ file.ts
}
})) passed++; else failed++;
// ── Round 117: parseSessionFilename with uppercase short ID — regex rejects [A-Z] ──
console.log('\nRound 117: parseSessionFilename (uppercase short ID — regex [a-z0-9] rejects uppercase):');
if (test('parseSessionFilename rejects uppercase short IDs because regex uses [a-z0-9] not [a-zA-Z0-9]', () => {
// The regex: /^(\d{4}-\d{2}-\d{2})(?:-([a-z0-9]{8,}))?-session\.tmp$/
// Note: [a-z0-9] — lowercase only
// All uppercase — rejected
// ── Round 117: parseSessionFilename accepts uppercase, underscores, and short IDs ──
console.log('\nRound 117: parseSessionFilename (uppercase, underscores, and short IDs are accepted):');
if (test('parseSessionFilename accepts uppercase short IDs, underscores, and 7-char names', () => {
const upper = sessionManager.parseSessionFilename('2026-01-15-ABCDEFGH-session.tmp');
assert.strictEqual(upper, null,
'All-uppercase ID should be rejected (regex uses [a-z0-9])');
assert.notStrictEqual(upper, null,
'All-uppercase ID should be accepted');
assert.strictEqual(upper.shortId, 'ABCDEFGH');
// Mixed case — rejected
const mixed = sessionManager.parseSessionFilename('2026-01-15-AbCdEfGh-session.tmp');
assert.strictEqual(mixed, null,
'Mixed-case ID should be rejected (uppercase chars not in [a-z0-9])');
assert.notStrictEqual(mixed, null,
'Mixed-case ID should be accepted');
assert.strictEqual(mixed.shortId, 'AbCdEfGh');
// All lowercase — accepted
const lower = sessionManager.parseSessionFilename('2026-01-15-abcdefgh-session.tmp');
assert.notStrictEqual(lower, null, 'All-lowercase ID should be accepted');
assert.strictEqual(lower.shortId, 'abcdefgh');
// Uppercase hex-like (common in UUIDs) — rejected
const hexUpper = sessionManager.parseSessionFilename('2026-01-15-A1B2C3D4-session.tmp');
assert.strictEqual(hexUpper, null,
'Uppercase hex ID should be rejected');
assert.notStrictEqual(hexUpper, null, 'Uppercase hex ID should be accepted');
assert.strictEqual(hexUpper.shortId, 'A1B2C3D4');
// Lowercase hex — accepted
const hexLower = sessionManager.parseSessionFilename('2026-01-15-a1b2c3d4-session.tmp');
assert.notStrictEqual(hexLower, null, 'Lowercase hex ID should be accepted');
assert.strictEqual(hexLower.shortId, 'a1b2c3d4');
const underscored = sessionManager.parseSessionFilename('2026-01-15-ChezMoi_2-session.tmp');
assert.notStrictEqual(underscored, null, 'IDs with underscores should be accepted');
assert.strictEqual(underscored.shortId, 'ChezMoi_2');
const shortName = sessionManager.parseSessionFilename('2026-01-15-homelab-session.tmp');
assert.notStrictEqual(shortName, null, '7-character names should be accepted');
assert.strictEqual(shortName.shortId, 'homelab');
})) passed++; else failed++;
// ── Round 119: parseSessionMetadata "Context to Load" code block extraction ──

View File

@@ -0,0 +1,303 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
slugify,
renderTemplate,
buildOrchestrationPlan,
materializePlan,
normalizeSeedPaths,
overlaySeedPaths
} = require('../../scripts/lib/tmux-worktree-orchestrator');
console.log('=== Testing tmux-worktree-orchestrator.js ===\n');
let passed = 0;
let failed = 0;
function test(desc, fn) {
try {
fn();
console.log(`${desc}`);
passed++;
} catch (error) {
console.log(`${desc}: ${error.message}`);
failed++;
}
}
console.log('Helpers:');
test('slugify normalizes mixed punctuation and casing', () => {
assert.strictEqual(slugify('Feature Audit: Docs + Tmux'), 'feature-audit-docs-tmux');
});
test('renderTemplate replaces supported placeholders', () => {
const rendered = renderTemplate('run {worker_name} in {worktree_path}', {
worker_name: 'Docs Fixer',
worktree_path: '/tmp/repo-worker'
});
assert.strictEqual(rendered, 'run Docs Fixer in /tmp/repo-worker');
});
test('renderTemplate rejects unknown placeholders', () => {
assert.throws(
() => renderTemplate('missing {unknown}', { worker_name: 'docs' }),
/Unknown template variable/
);
});
console.log('\nPlan generation:');
test('buildOrchestrationPlan creates worktrees, branches, and tmux commands', () => {
const repoRoot = path.join('/tmp', 'ecc');
const plan = buildOrchestrationPlan({
repoRoot,
sessionName: 'Skill Audit',
baseRef: 'main',
launcherCommand: 'codex exec --cwd {worktree_path_sh} --task-file {task_file_sh}',
workers: [
{ name: 'Docs A', task: 'Fix skills 1-4' },
{ name: 'Docs B', task: 'Fix skills 5-8' }
]
});
assert.strictEqual(plan.sessionName, 'skill-audit');
assert.strictEqual(plan.workerPlans.length, 2);
assert.strictEqual(plan.workerPlans[0].branchName, 'orchestrator-skill-audit-docs-a');
assert.strictEqual(plan.workerPlans[1].branchName, 'orchestrator-skill-audit-docs-b');
assert.deepStrictEqual(
plan.workerPlans[0].gitArgs.slice(0, 4),
['worktree', 'add', '-b', 'orchestrator-skill-audit-docs-a'],
'Should create branch-backed worktrees'
);
assert.ok(
plan.workerPlans[0].worktreePath.endsWith(path.join('ecc-skill-audit-docs-a')),
'Should create sibling worktree path'
);
assert.ok(
plan.workerPlans[0].taskFilePath.endsWith(path.join('.orchestration', 'skill-audit', 'docs-a', 'task.md')),
'Should create per-worker task file'
);
assert.ok(
plan.workerPlans[0].handoffFilePath.endsWith(path.join('.orchestration', 'skill-audit', 'docs-a', 'handoff.md')),
'Should create per-worker handoff file'
);
assert.ok(
plan.workerPlans[0].launchCommand.includes(plan.workerPlans[0].taskFilePath),
'Launch command should interpolate task file'
);
assert.ok(
plan.workerPlans[0].launchCommand.includes(plan.workerPlans[0].worktreePath),
'Launch command should interpolate worktree path'
);
assert.ok(
plan.tmuxCommands.some(command => command.args.includes('split-window')),
'Should include tmux split commands'
);
assert.ok(
plan.tmuxCommands.some(command => command.args.includes('select-layout')),
'Should include tiled layout command'
);
});
test('buildOrchestrationPlan requires at least one worker', () => {
assert.throws(
() => buildOrchestrationPlan({
repoRoot: '/tmp/ecc',
sessionName: 'empty',
launcherCommand: 'codex exec --task-file {task_file}',
workers: []
}),
/at least one worker/
);
});
test('buildOrchestrationPlan normalizes global and worker seed paths', () => {
const plan = buildOrchestrationPlan({
repoRoot: '/tmp/ecc',
sessionName: 'seeded',
launcherCommand: 'echo run',
seedPaths: ['scripts/orchestrate-worktrees.js', './.claude/plan/workflow-e2e-test.json'],
workers: [
{
name: 'Docs',
task: 'Update docs',
seedPaths: ['commands/multi-workflow.md']
}
]
});
assert.deepStrictEqual(plan.workerPlans[0].seedPaths, [
'scripts/orchestrate-worktrees.js',
'.claude/plan/workflow-e2e-test.json',
'commands/multi-workflow.md'
]);
});
test('buildOrchestrationPlan rejects worker names that collapse to the same slug', () => {
assert.throws(
() => buildOrchestrationPlan({
repoRoot: '/tmp/ecc',
sessionName: 'duplicates',
launcherCommand: 'echo run',
workers: [
{ name: 'Docs A', task: 'Fix skill docs' },
{ name: 'Docs/A', task: 'Fix tests' }
]
}),
/unique slugs/
);
});
test('buildOrchestrationPlan exposes shell-safe launcher aliases alongside raw defaults', () => {
const repoRoot = path.join('/tmp', 'My Repo');
const plan = buildOrchestrationPlan({
repoRoot,
sessionName: 'Spacing Audit',
launcherCommand: 'bash {repo_root_sh}/scripts/orchestrate-codex-worker.sh {task_file_sh} {handoff_file_sh} {status_file_sh} {worker_name_sh} {worker_name}',
workers: [{ name: 'Docs Fixer', task: 'Update docs' }]
});
const quote = value => `'${String(value).replace(/'/g, `'\\''`)}'`;
const resolvedRepoRoot = plan.workerPlans[0].repoRoot;
assert.ok(
plan.workerPlans[0].launchCommand.includes(`bash ${quote(resolvedRepoRoot)}/scripts/orchestrate-codex-worker.sh`),
'repo_root_sh should provide a shell-safe path'
);
assert.ok(
plan.workerPlans[0].launchCommand.includes(quote(plan.workerPlans[0].taskFilePath)),
'task_file_sh should provide a shell-safe path'
);
assert.ok(
plan.workerPlans[0].launchCommand.includes(`${quote(plan.workerPlans[0].workerName)} ${plan.workerPlans[0].workerName}`),
'raw defaults should remain available alongside shell-safe aliases'
);
});
test('normalizeSeedPaths rejects paths outside the repo root', () => {
assert.throws(
() => normalizeSeedPaths(['../outside.txt'], '/tmp/ecc'),
/inside repoRoot/
);
});
test('normalizeSeedPaths rejects repo root and git metadata paths', () => {
assert.throws(
() => normalizeSeedPaths(['.'], '/tmp/ecc'),
/must not target the repo root/
);
assert.throws(
() => normalizeSeedPaths(['.git/config'], '/tmp/ecc'),
/must not target git metadata/
);
});
test('materializePlan keeps worker instructions inside the worktree boundary', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orchestrator-test-'));
try {
const plan = buildOrchestrationPlan({
repoRoot: tempRoot,
coordinationRoot: path.join(tempRoot, '.claude', 'orchestration'),
sessionName: 'Workflow E2E',
launcherCommand: 'bash {repo_root_sh}/scripts/orchestrate-codex-worker.sh {task_file_sh} {handoff_file_sh} {status_file_sh}',
workers: [{ name: 'Docs', task: 'Update the workflow docs.' }]
});
materializePlan(plan);
const taskFile = fs.readFileSync(plan.workerPlans[0].taskFilePath, 'utf8');
const handoffFile = fs.readFileSync(plan.workerPlans[0].handoffFilePath, 'utf8');
assert.ok(
taskFile.includes('Report results in your final response.'),
'Task file should tell the worker to report in stdout'
);
assert.ok(
taskFile.includes('## Summary') &&
taskFile.includes('## Validation') &&
taskFile.includes('## Remaining Risks'),
'Task file should require parser-compatible headings'
);
assert.ok(
taskFile.includes('Do not spawn subagents or external agents for this task.'),
'Task file should keep nested workers single-session'
);
assert.ok(
!taskFile.includes('Write results and handoff notes to'),
'Task file should not require writing handoff files outside the worktree'
);
assert.ok(
!taskFile.includes('Update `'),
'Task file should not instruct the nested worker to update orchestration status files'
);
assert.ok(
handoffFile.includes('## Summary') &&
handoffFile.includes('## Validation') &&
handoffFile.includes('## Remaining Risks'),
'Handoff placeholder should seed parser-compatible headings'
);
assert.ok(
!handoffFile.includes('## Files Changed') &&
!handoffFile.includes('## Tests / Verification') &&
!handoffFile.includes('## Follow-ups'),
'Handoff placeholder should not use legacy headings'
);
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
test('overlaySeedPaths copies local overlays into the worker worktree', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orchestrator-overlay-'));
const repoRoot = path.join(tempRoot, 'repo');
const worktreePath = path.join(tempRoot, 'worktree');
try {
fs.mkdirSync(path.join(repoRoot, 'scripts'), { recursive: true });
fs.mkdirSync(path.join(repoRoot, '.claude', 'plan'), { recursive: true });
fs.mkdirSync(path.join(worktreePath, 'scripts'), { recursive: true });
fs.writeFileSync(
path.join(repoRoot, 'scripts', 'orchestrate-worktrees.js'),
'local-version\n',
'utf8'
);
fs.writeFileSync(
path.join(repoRoot, '.claude', 'plan', 'workflow-e2e-test.json'),
'{"seeded":true}\n',
'utf8'
);
fs.writeFileSync(
path.join(worktreePath, 'scripts', 'orchestrate-worktrees.js'),
'head-version\n',
'utf8'
);
overlaySeedPaths({
repoRoot,
seedPaths: [
'scripts/orchestrate-worktrees.js',
'.claude/plan/workflow-e2e-test.json'
],
worktreePath
});
assert.strictEqual(
fs.readFileSync(path.join(worktreePath, 'scripts', 'orchestrate-worktrees.js'), 'utf8'),
'local-version\n'
);
assert.strictEqual(
fs.readFileSync(path.join(worktreePath, '.claude', 'plan', 'workflow-e2e-test.json'), 'utf8'),
'{"seeded":true}\n'
);
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
if (failed > 0) process.exit(1);

View File

@@ -0,0 +1,89 @@
'use strict';
const assert = require('assert');
const { parseArgs } = require('../../scripts/orchestration-status');
console.log('=== Testing orchestration-status.js ===\n');
let passed = 0;
let failed = 0;
function test(desc, fn) {
try {
fn();
console.log(`${desc}`);
passed++;
} catch (error) {
console.log(`${desc}: ${error.message}`);
failed++;
}
}
test('parseArgs reads a target with an optional write path', () => {
assert.deepStrictEqual(
parseArgs([
'node',
'scripts/orchestration-status.js',
'workflow-visual-proof',
'--write',
'/tmp/snapshot.json'
]),
{
target: 'workflow-visual-proof',
writePath: '/tmp/snapshot.json'
}
);
});
test('parseArgs does not treat the write path as the target', () => {
assert.deepStrictEqual(
parseArgs([
'node',
'scripts/orchestration-status.js',
'--write',
'/tmp/snapshot.json',
'workflow-visual-proof'
]),
{
target: 'workflow-visual-proof',
writePath: '/tmp/snapshot.json'
}
);
});
test('parseArgs rejects missing write values and unknown flags', () => {
assert.throws(
() => parseArgs([
'node',
'scripts/orchestration-status.js',
'workflow-visual-proof',
'--write'
]),
/--write requires an output path/
);
assert.throws(
() => parseArgs([
'node',
'scripts/orchestration-status.js',
'workflow-visual-proof',
'--unknown'
]),
/Unknown flag/
);
});
test('parseArgs rejects multiple positional targets', () => {
assert.throws(
() => parseArgs([
'node',
'scripts/orchestration-status.js',
'first',
'second'
]),
/Expected a single session name or plan path/
);
});
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
if (failed > 0) process.exit(1);

View File

@@ -40,7 +40,7 @@ With lazy loading, the context window issue is mostly solved. But token usage an
For sharing memory across sessions, a skill or command that summarizes and checks in on progress then saves to a `.tmp` file in your `.claude` folder and appends to it until the end of your session is the best bet. The next day it can use that as context and pick up where you left off, create a new file for each session so you don't pollute old context into new work.
![Session Storage File Tree](./assets/images/longform/03-session-storage.png)
*Example of session storage -> https://github.com/affaan-m/everything-claude-code/tree/main/examples/sessions*
*Example of session storage -> <https://github.com/affaan-m/everything-claude-code/tree/main/examples/sessions>*
Claude creates a file summarizing current state. Review it, ask for edits if needed, then start fresh. For the new conversation, just provide the file path. Particularly useful when you're hitting context limits and need to continue complex work. These files should contain:
- What approaches worked (verifiably with evidence)
@@ -129,7 +129,7 @@ Default to Sonnet for 90% of coding tasks. Upgrade to Opus when first attempt fa
**Pricing Reference:**
![Claude Model Pricing](./assets/images/longform/05-pricing-table.png)
*Source: https://platform.claude.com/docs/en/about-claude/pricing*
*Source: <https://platform.claude.com/docs/en/about-claude/pricing>*
**Tool-Specific Optimizations:**

View File

@@ -211,7 +211,7 @@ git worktree add ../feature-branch feature-branch
Stream and watch logs/bash processes Claude runs:
https://github.com/user-attachments/assets/shortform/07-tmux-video.mp4
<https://github.com/user-attachments/assets/shortform/07-tmux-video.mp4>
```bash
tmux new -s dev