Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions skills/subagents/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
name: subagents
description: Launching and managing coding subagents via tmux. Use when delegating tasks to parallel agents, monitoring their progress, or coordinating multi-agent work. Triggers include "launch an agent", "start a subagent", "delegate this task", "run this in parallel", or any task coordination involving multiple agents.
---

# Subagent Management

Run coding subagents in tmux sessions so both the orchestrator and the human have visibility. The orchestrator (you) acts as project manager — writing tasks, launching agents, monitoring progress, and validating results. You don't code directly.

## Launching a subagent

```bash
tmux new-session -d -s <session-name> -x 220 -y 50 \
"pi --model <model> '<prompt>' 2>&1; echo '[AGENT DONE]'; sleep 99999"
```

- **Session name**: short, descriptive (e.g. `agent1`, `refactor-data`, `fix-tests`).
- **Prompt**: tell the agent what task file to read, what guidelines to follow (e.g. `AGENTS.md`), and what the success criteria are.
- The `echo '[AGENT DONE]'; sleep 99999` tail keeps the session alive after the agent finishes so you can review its final output.

## Monitoring progress

```bash
# Peek at the last N lines of output
tmux capture-pane -t <session-name> -p | tail -40

# The human can watch live
tmux attach -t <session-name>
```

Check in periodically. Don't just launch and forget — catch issues early.

## Key rules

### Never kill a working agent

An agent accumulates deep context over many minutes of reading, reasoning, and coding. Killing it mid-task destroys all of that. A new agent starting from scratch will:

- Waste time re-reading everything
- Miss implicit decisions the previous agent made
- Likely produce worse or inconsistent results

**If you need to do something in the repo while an agent is running** (create a branch, install a dep, check types), do it in your own terminal. The filesystem is shared — you can work alongside the agent without disrupting it.

### One task per agent

Each agent gets a single task file from `tasks/`. Don't overload an agent with multiple unrelated goals. If a task turns out to be bigger than expected, split it.

### Give agents the right starting context

A good launch prompt includes:

1. Which task file to read
2. Which project guidelines to follow (e.g. `AGENTS.md`)
3. Orientation on where to start in the codebase
4. What "done" looks like

A bad launch prompt is vague ("fix the app") or over-specified with implementation details (let the agent figure out the how).

### Parallel agents

Multiple agents can work simultaneously on independent tasks. Use distinct tmux session names and make sure their tasks touch different files to avoid conflicts.

If tasks are sequential (agent B depends on agent A's output), wait for A to finish and validate before launching B.

### Validation

When an agent signals it's done (or you see `[AGENT DONE]` in the session):

1. Check the output: `tmux capture-pane -t <session-name> -p | tail -80`
2. Run the validation criteria from the task file (typically `npm run check`, `npm test`, manual grep checks)
3. Review the diff: `git diff`
4. If it passes, update the task status to `done`
5. If it fails, either relaunch with specific fix instructions or fix manually

### Cleanup

```bash
# Kill a finished session
tmux kill-session -t <session-name>

# List all sessions
tmux list-sessions
```
86 changes: 43 additions & 43 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,55 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useRoute } from './ui/routing';
// Top-level application component. Instantiates the data layer once and
// provides a thin routing adapter that syncs URL ↔ data state.
import React, { useEffect, useRef } from 'react';
import { useRoute, parseRoute } from './ui/routing';
import { RepoView } from './ui/RepoView';
import { HomeView } from './ui/HomeView';
import { listRecentRepos, recordRecentRepo, type RecentRepo } from './storage/local';
import { useAppData } from './data';
import { listRecentRepos } from './storage/local';

export function App() {
// Data layer: instantiated once at the app level.
// The initial route is read from the URL synchronously so the first render
// already reflects the correct slug/path without waiting for a dispatch.
const { state, dispatch } = useAppData(parseRoute(window.location.pathname));

// URL routing: parse/navigate the browser history.
const { route, navigate } = useRoute();

// Adjust page title based on route
// URL → data: inform the data layer whenever the browser route changes.
useEffect(() => {
document.title = route.kind === 'repo' ? `${route.owner}/${route.repo}` : 'VibeNote';
dispatch({ type: 'route-changed', route });
}, [route]);

// redirects
// Data → URL: when the data layer sets a pendingNavigation (e.g. after a
// rename or sync), reflect it in the URL via navigate().
let lastNavRef = useRef(state.pendingNavigation);
useEffect(() => {
let nav = state.pendingNavigation;
// Skip if no pending nav, or if we already processed this exact object.
if (!nav || nav === lastNavRef.current) return;
lastNavRef.current = nav;

// Build the target route from the current active route + new path.
let activeRoute = state.activeRoute;
if (activeRoute.kind === 'repo') {
navigate({ ...activeRoute, notePath: nav.path }, { replace: nav.replace });
} else if (activeRoute.kind === 'new') {
navigate({ kind: 'new', notePath: nav.path }, { replace: nav.replace });
}
}, [state.pendingNavigation]);

// Adjust page title based on active route.
useEffect(() => {
let r = state.activeRoute;
document.title = r.kind === 'repo' ? `${r.owner}/${r.repo}` : 'VibeNote';
}, [state.activeRoute]);

// Redirects based on the URL route (not the data layer route).
useEffect(() => {
// if the route is /start, redirect to the most recent repo or /home
if (route.kind === 'start') {
let recents = state.recents;
let candidate = recents.find((entry) => entry.owner !== undefined && entry.repo !== undefined);

if (candidate !== undefined) {
Expand All @@ -33,52 +67,18 @@ export function App() {
}
}, [route]);

// list of recent repos, kept in local storage and updated when navigating to a new repo
// or updating information about an existing one
const [recents, recordRecent] = useRecents();

if (route.kind === 'home') {
return <HomeView recents={recents} navigate={navigate} />;
return <HomeView recents={state.recents} navigate={navigate} />;
}

if (route.kind === 'start') {
// will redirect immediately
return null;
}

if (route.kind === 'new') {
return <RepoView slug="new" route={route} navigate={navigate} recordRecent={recordRecent} />;
}

if (route.kind === 'repo') {
return (
<RepoView
slug={`${route.owner}/${route.repo}`}
route={route}
navigate={navigate}
recordRecent={recordRecent}
/>
);
if (route.kind === 'new' || route.kind === 'repo') {
return <RepoView state={state} dispatch={dispatch} navigate={navigate} />;
}

return null;
}

function useRecents() {
const [recents, setRecents] = useState<RecentRepo[]>(() => listRecentRepos());

useEffect(() => {
const onStorage = () => setRecents(listRecentRepos());
window.addEventListener('storage', onStorage);
return () => window.removeEventListener('storage', onStorage);
}, []);

const recordRecent = useCallback(
(entry: { slug: string; owner?: string; repo?: string; title?: string; connected?: boolean }) => {
recordRecentRepo(entry);
setRecents(listRecentRepos());
},
[]
);
return [recents, recordRecent] as const;
}
Loading