@chuckssmith/agentloom 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +38 -0
- package/dist/commands/crew.d.ts +1 -0
- package/dist/commands/crew.js +103 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +40 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +34 -0
- package/dist/state/session.d.ts +39 -0
- package/dist/state/session.js +40 -0
- package/dist/team/orchestrator.d.ts +12 -0
- package/dist/team/orchestrator.js +101 -0
- package/dist/team/queue.d.ts +6 -0
- package/dist/team/queue.js +62 -0
- package/package.json +38 -0
- package/skills/architect.md +53 -0
- package/skills/crew.md +56 -0
- package/skills/grind.md +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# agentloom
|
|
2
|
+
|
|
3
|
+
A workflow layer for Claude Code. Better routing, reusable roles, and multi-agent crew coordination — built natively on what Claude Code already provides.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install -g agentloom
|
|
7
|
+
loom setup
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## What this is
|
|
13
|
+
|
|
14
|
+
Claude Code is the execution engine. agentloom adds:
|
|
15
|
+
- **`$grind`** — persistence loop that keeps working until a task is verified complete
|
|
16
|
+
- **`$crew`** — parallel workers that decompose and execute simultaneously
|
|
17
|
+
- **`$architect`** — deep analysis mode before major decisions
|
|
18
|
+
- **`loom crew`** — CLI to spawn a crew of workers from your terminal
|
|
19
|
+
|
|
20
|
+
It does not replace Claude Code. It wraps it.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g agentloom
|
|
28
|
+
loom setup # installs skills, validates deps
|
|
29
|
+
|
|
30
|
+
# From your terminal:
|
|
31
|
+
loom crew "audit every API endpoint for security issues"
|
|
32
|
+
loom crew 2:explore+1:code-reviewer "review the payment flow"
|
|
33
|
+
|
|
34
|
+
# Or from inside a Claude Code session:
|
|
35
|
+
# $grind "port the auth module to the new interface"
|
|
36
|
+
# $crew "analyze all three data pipeline stages in parallel"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Skills
|
|
42
|
+
|
|
43
|
+
Install with `loom setup`. Then use inside any Claude Code session:
|
|
44
|
+
|
|
45
|
+
| Skill | What it does |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `$grind` | Persistence loop with mandatory verification gate |
|
|
48
|
+
| `$crew` | Parallel workers — decomposes task, runs simultaneously, verifies |
|
|
49
|
+
| `$architect` | Deep analysis — maps system, finds real problems, recommends approach |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## CLI
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
loom crew [N] "<task>" Spawn N general-purpose workers
|
|
57
|
+
loom crew 3 "<task>" Spawn 3 workers
|
|
58
|
+
loom crew 2:explore "<task>" Spawn 2 Explore-type workers
|
|
59
|
+
loom crew 2:explore+1:code-reviewer Spawn typed crew
|
|
60
|
+
loom status Show active session
|
|
61
|
+
loom setup Install skills + validate
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Worker types
|
|
65
|
+
|
|
66
|
+
Matches Claude Code's built-in subagent types:
|
|
67
|
+
|
|
68
|
+
| Type | Best for |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `explore` | Read-only research, codebase mapping |
|
|
71
|
+
| `plan` | Architecture decisions, approach planning |
|
|
72
|
+
| `code-reviewer` | Audits, security reviews, quality checks |
|
|
73
|
+
| `frontend-developer` | UI and component work |
|
|
74
|
+
| `general-purpose` | General implementation (default) |
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## State directory
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
.agentloom/
|
|
82
|
+
tasks/ Task queue — workers claim atomically
|
|
83
|
+
workers/ Worker status and results
|
|
84
|
+
context/ Shared context snapshots
|
|
85
|
+
session.json Active session metadata
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Requirements
|
|
91
|
+
|
|
92
|
+
- Node.js 20+
|
|
93
|
+
- Claude Code CLI (`claude`)
|
|
94
|
+
- tmux (optional — used for crew mode on Mac/Linux; falls back to background processes on WSL/Windows)
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { setup } from './commands/setup.js';
|
|
3
|
+
import { crew } from './commands/crew.js';
|
|
4
|
+
import { status } from './commands/status.js';
|
|
5
|
+
const [, , command, ...args] = process.argv;
|
|
6
|
+
const usage = `
|
|
7
|
+
agentloom (loom) — workflow layer for Claude Code
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
loom setup Install skills and initialize state dir
|
|
11
|
+
loom crew [N] "<task>" Spawn N parallel workers on a task
|
|
12
|
+
loom crew 2:explore "<task>" Spawn typed workers (explore/plan/code-reviewer)
|
|
13
|
+
loom status Show active crew session
|
|
14
|
+
|
|
15
|
+
Modes (use $grind or $crew inside a Claude Code session):
|
|
16
|
+
$grind Persistence loop — keeps working until verified complete
|
|
17
|
+
$crew Parallel workers — decompose and execute simultaneously
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
loom setup
|
|
21
|
+
loom crew "refactor the auth module"
|
|
22
|
+
loom crew 3 "audit every API endpoint for security issues"
|
|
23
|
+
loom crew 2:explore+1:code-reviewer "review the payment flow"
|
|
24
|
+
`;
|
|
25
|
+
switch (command) {
|
|
26
|
+
case 'setup':
|
|
27
|
+
await setup();
|
|
28
|
+
break;
|
|
29
|
+
case 'crew':
|
|
30
|
+
await crew(args);
|
|
31
|
+
break;
|
|
32
|
+
case 'status':
|
|
33
|
+
await status();
|
|
34
|
+
break;
|
|
35
|
+
default:
|
|
36
|
+
console.log(usage);
|
|
37
|
+
process.exit(command ? 1 : 0);
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function crew(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { execSync, spawn } from 'child_process';
|
|
2
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { parseWorkerSpec, initSession, writeContextSnapshot, decomposeTasks, } from '../team/orchestrator.js';
|
|
5
|
+
import { STATE_DIR } from '../state/session.js';
|
|
6
|
+
const hasTmux = () => {
|
|
7
|
+
try {
|
|
8
|
+
execSync('tmux -V', { stdio: 'ignore' });
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const isWSL = () => process.platform === 'linux' && !!process.env.WSL_DISTRO_NAME;
|
|
16
|
+
export async function crew(args) {
|
|
17
|
+
if (args.length === 0) {
|
|
18
|
+
console.error('Usage: loom crew [N] "<task>"');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const { specs, task } = parseWorkerSpec(args);
|
|
22
|
+
const totalWorkers = specs.reduce((sum, s) => sum + s.count, 0);
|
|
23
|
+
const slug = task.slice(0, 30).toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
|
24
|
+
console.log(`\nagentloom crew`);
|
|
25
|
+
console.log(`Task: ${task}`);
|
|
26
|
+
console.log(`Workers: ${totalWorkers}`);
|
|
27
|
+
console.log(`Mode: ${hasTmux() && !isWSL() ? 'tmux' : 'background processes'}\n`);
|
|
28
|
+
const session = await initSession(task, totalWorkers);
|
|
29
|
+
const contextPath = await writeContextSnapshot(slug, task);
|
|
30
|
+
const tasks = await decomposeTasks(task, specs);
|
|
31
|
+
console.log(`Session: ${session.id}`);
|
|
32
|
+
console.log(`Tasks: ${tasks.length} created`);
|
|
33
|
+
console.log(`Context: ${contextPath}\n`);
|
|
34
|
+
const workerPrompt = buildWorkerPrompt(task, contextPath, session.id);
|
|
35
|
+
if (hasTmux() && !isWSL()) {
|
|
36
|
+
await launchTmux(session.id, totalWorkers, specs, workerPrompt);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
await launchBackground(session.id, totalWorkers, specs, workerPrompt);
|
|
40
|
+
}
|
|
41
|
+
console.log(`\nWorkers launched. Monitor with: loom status`);
|
|
42
|
+
console.log(`State dir: ${STATE_DIR}/`);
|
|
43
|
+
}
|
|
44
|
+
function buildWorkerPrompt(task, contextPath, sessionId) {
|
|
45
|
+
return `You are a worker agent in an agentloom crew session (${sessionId}).
|
|
46
|
+
|
|
47
|
+
Your job: help complete this task: "${task}"
|
|
48
|
+
|
|
49
|
+
## Your protocol
|
|
50
|
+
|
|
51
|
+
1. Read the shared context at: ${contextPath}
|
|
52
|
+
2. Check ${STATE_DIR}/tasks/ for unclaimed work (files ending in -pending.json)
|
|
53
|
+
3. Claim a task by reading it and noting the task ID
|
|
54
|
+
4. Do the work thoroughly using all tools available to you
|
|
55
|
+
5. Write your result back to ${STATE_DIR}/workers/
|
|
56
|
+
6. Repeat until no pending tasks remain
|
|
57
|
+
|
|
58
|
+
## Rules
|
|
59
|
+
- Claim only one task at a time
|
|
60
|
+
- Write your findings to the context file so other workers can see them
|
|
61
|
+
- Do not stop until you have completed at least one task
|
|
62
|
+
- If all tasks are claimed, do exploratory work relevant to the main task
|
|
63
|
+
|
|
64
|
+
Begin now. Check for pending tasks and start working.`;
|
|
65
|
+
}
|
|
66
|
+
async function launchBackground(sessionId, count, specs, prompt) {
|
|
67
|
+
await mkdir(join(STATE_DIR, 'workers'), { recursive: true });
|
|
68
|
+
let workerIdx = 0;
|
|
69
|
+
for (const spec of specs) {
|
|
70
|
+
for (let i = 0; i < spec.count; i++) {
|
|
71
|
+
const workerId = `w${String(workerIdx).padStart(2, '0')}`;
|
|
72
|
+
workerIdx++;
|
|
73
|
+
const promptFile = join(STATE_DIR, 'workers', `${workerId}-prompt.md`);
|
|
74
|
+
await writeFile(promptFile, prompt);
|
|
75
|
+
const child = spawn('claude', ['--print', '--dangerously-skip-permissions', '-p', prompt], {
|
|
76
|
+
detached: true,
|
|
77
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
78
|
+
env: { ...process.env, AGENTLOOM_WORKER_ID: workerId, AGENTLOOM_SESSION: sessionId },
|
|
79
|
+
});
|
|
80
|
+
child.unref();
|
|
81
|
+
console.log(` ✓ Worker ${workerId} (${spec.agentType}) launched [pid ${child.pid}]`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function launchTmux(sessionId, count, specs, prompt) {
|
|
86
|
+
const tmuxSession = `loom-${sessionId}`;
|
|
87
|
+
execSync(`tmux new-session -d -s ${tmuxSession} -x 220 -y 50`);
|
|
88
|
+
let workerIdx = 0;
|
|
89
|
+
for (const spec of specs) {
|
|
90
|
+
for (let i = 0; i < spec.count; i++) {
|
|
91
|
+
const workerId = `w${String(workerIdx).padStart(2, '0')}`;
|
|
92
|
+
workerIdx++;
|
|
93
|
+
if (workerIdx > 1) {
|
|
94
|
+
execSync(`tmux split-window -h -t ${tmuxSession}`);
|
|
95
|
+
execSync(`tmux select-layout -t ${tmuxSession} tiled`);
|
|
96
|
+
}
|
|
97
|
+
const cmd = `AGENTLOOM_WORKER_ID=${workerId} AGENTLOOM_SESSION=${sessionId} claude --print --dangerously-skip-permissions -p '${prompt.replace(/'/g, "'\"'\"'")}'; echo '[worker done]'; read`;
|
|
98
|
+
execSync(`tmux send-keys -t ${tmuxSession} "${cmd}" Enter`);
|
|
99
|
+
console.log(` ✓ Worker ${workerId} (${spec.agentType}) launched in tmux pane`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
execSync(`tmux attach-session -t ${tmuxSession}`);
|
|
103
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function setup(): Promise<void>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { mkdir, copyFile, readdir } from 'fs/promises';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const SKILLS_SRC = join(__dirname, '../../skills');
|
|
7
|
+
const SKILLS_DEST = join(homedir(), '.claude', 'skills');
|
|
8
|
+
export async function setup() {
|
|
9
|
+
console.log('agentloom setup\n');
|
|
10
|
+
// 1. Validate claude CLI exists
|
|
11
|
+
const { execSync } = await import('child_process');
|
|
12
|
+
try {
|
|
13
|
+
execSync('claude --version', { stdio: 'ignore' });
|
|
14
|
+
console.log('✓ claude CLI found');
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
console.error('✗ claude CLI not found — install Claude Code first');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
// 2. Install skills
|
|
21
|
+
await mkdir(SKILLS_DEST, { recursive: true });
|
|
22
|
+
const skills = await readdir(SKILLS_SRC);
|
|
23
|
+
for (const skill of skills.filter(f => f.endsWith('.md'))) {
|
|
24
|
+
const dest = join(SKILLS_DEST, skill);
|
|
25
|
+
await copyFile(join(SKILLS_SRC, skill), dest);
|
|
26
|
+
console.log(`✓ skill installed: ${skill}`);
|
|
27
|
+
}
|
|
28
|
+
// 3. Check tmux (optional)
|
|
29
|
+
try {
|
|
30
|
+
execSync('tmux -V', { stdio: 'ignore' });
|
|
31
|
+
console.log('✓ tmux found — crew mode will use split panes');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
console.log('~ tmux not found — crew mode will use background processes');
|
|
35
|
+
}
|
|
36
|
+
console.log('\nSetup complete.');
|
|
37
|
+
console.log('\nGet started:');
|
|
38
|
+
console.log(' loom crew "your task here"');
|
|
39
|
+
console.log(' or use $grind / $crew inside a Claude Code session');
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function status(): Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readSession, readTasks, readWorkers } from '../state/session.js';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { STATE_DIR } from '../state/session.js';
|
|
4
|
+
export async function status() {
|
|
5
|
+
if (!existsSync(STATE_DIR)) {
|
|
6
|
+
console.log('No active session. Run: loom crew "<task>"');
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
const session = await readSession();
|
|
10
|
+
if (!session) {
|
|
11
|
+
console.log('No session found.');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const tasks = await readTasks();
|
|
15
|
+
const workers = await readWorkers();
|
|
16
|
+
const pending = tasks.filter(t => t.status === 'pending').length;
|
|
17
|
+
const claimed = tasks.filter(t => t.status === 'claimed').length;
|
|
18
|
+
const done = tasks.filter(t => t.status === 'done').length;
|
|
19
|
+
const failed = tasks.filter(t => t.status === 'failed').length;
|
|
20
|
+
console.log(`\nSession: ${session.id}`);
|
|
21
|
+
console.log(`Status: ${session.status}`);
|
|
22
|
+
console.log(`Task: ${session.description}`);
|
|
23
|
+
console.log(`Started: ${session.createdAt}`);
|
|
24
|
+
console.log(`\nTasks: ${pending} pending ${claimed} active ${done} done ${failed} failed`);
|
|
25
|
+
console.log(`Workers: ${workers.length} (${session.workerCount} total)`);
|
|
26
|
+
if (workers.length > 0) {
|
|
27
|
+
console.log('\nWorker status:');
|
|
28
|
+
for (const w of workers) {
|
|
29
|
+
const task = w.currentTaskId ? tasks.find(t => t.id === w.currentTaskId) : null;
|
|
30
|
+
const desc = task ? ` → ${task.description.slice(0, 60)}` : '';
|
|
31
|
+
console.log(` [${w.id}] ${w.status}${desc}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare const STATE_DIR = ".claude-team";
|
|
2
|
+
export type TaskStatus = 'pending' | 'claimed' | 'done' | 'failed';
|
|
3
|
+
export type Task = {
|
|
4
|
+
id: string;
|
|
5
|
+
description: string;
|
|
6
|
+
agentType?: string;
|
|
7
|
+
status: TaskStatus;
|
|
8
|
+
workerId?: string;
|
|
9
|
+
result?: string;
|
|
10
|
+
error?: string;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
claimedAt?: string;
|
|
13
|
+
completedAt?: string;
|
|
14
|
+
};
|
|
15
|
+
export type Worker = {
|
|
16
|
+
id: string;
|
|
17
|
+
agentType: string;
|
|
18
|
+
status: 'idle' | 'working' | 'done';
|
|
19
|
+
currentTaskId?: string;
|
|
20
|
+
startedAt: string;
|
|
21
|
+
completedAt?: string;
|
|
22
|
+
};
|
|
23
|
+
export type Session = {
|
|
24
|
+
id: string;
|
|
25
|
+
description: string;
|
|
26
|
+
status: 'running' | 'verifying' | 'done' | 'failed';
|
|
27
|
+
workerCount: number;
|
|
28
|
+
createdAt: string;
|
|
29
|
+
completedAt?: string;
|
|
30
|
+
verificationResult?: 'pass' | 'fail';
|
|
31
|
+
verificationNotes?: string;
|
|
32
|
+
};
|
|
33
|
+
export declare function ensureStateDir(): Promise<void>;
|
|
34
|
+
export declare function writeSession(session: Session): Promise<void>;
|
|
35
|
+
export declare function readSession(): Promise<Session | null>;
|
|
36
|
+
export declare function writeTask(task: Task): Promise<void>;
|
|
37
|
+
export declare function writeWorker(worker: Worker): Promise<void>;
|
|
38
|
+
export declare function readWorkers(): Promise<Worker[]>;
|
|
39
|
+
export declare function readTasks(): Promise<Task[]>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
export const STATE_DIR = '.claude-team';
|
|
5
|
+
export async function ensureStateDir() {
|
|
6
|
+
await mkdir(join(STATE_DIR, 'tasks'), { recursive: true });
|
|
7
|
+
await mkdir(join(STATE_DIR, 'workers'), { recursive: true });
|
|
8
|
+
await mkdir(join(STATE_DIR, 'context'), { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
export async function writeSession(session) {
|
|
11
|
+
await writeFile(join(STATE_DIR, 'session.json'), JSON.stringify(session, null, 2));
|
|
12
|
+
}
|
|
13
|
+
export async function readSession() {
|
|
14
|
+
const path = join(STATE_DIR, 'session.json');
|
|
15
|
+
if (!existsSync(path))
|
|
16
|
+
return null;
|
|
17
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
18
|
+
}
|
|
19
|
+
export async function writeTask(task) {
|
|
20
|
+
await writeFile(join(STATE_DIR, 'tasks', `${task.id}-${task.status}.json`), JSON.stringify(task, null, 2));
|
|
21
|
+
}
|
|
22
|
+
export async function writeWorker(worker) {
|
|
23
|
+
await writeFile(join(STATE_DIR, 'workers', `${worker.id}.json`), JSON.stringify(worker, null, 2));
|
|
24
|
+
}
|
|
25
|
+
export async function readWorkers() {
|
|
26
|
+
const { readdir } = await import('fs/promises');
|
|
27
|
+
const dir = join(STATE_DIR, 'workers');
|
|
28
|
+
if (!existsSync(dir))
|
|
29
|
+
return [];
|
|
30
|
+
const files = await readdir(dir);
|
|
31
|
+
return Promise.all(files.filter(f => f.endsWith('.json')).map(async (f) => JSON.parse(await readFile(join(dir, f), 'utf8'))));
|
|
32
|
+
}
|
|
33
|
+
export async function readTasks() {
|
|
34
|
+
const { readdir } = await import('fs/promises');
|
|
35
|
+
const dir = join(STATE_DIR, 'tasks');
|
|
36
|
+
if (!existsSync(dir))
|
|
37
|
+
return [];
|
|
38
|
+
const files = await readdir(dir);
|
|
39
|
+
return Promise.all(files.filter(f => f.endsWith('.json')).map(async (f) => JSON.parse(await readFile(join(dir, f), 'utf8'))));
|
|
40
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Task, type Session } from '../state/session.js';
|
|
2
|
+
export type WorkerSpec = {
|
|
3
|
+
count: number;
|
|
4
|
+
agentType: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function parseWorkerSpec(args: string[]): {
|
|
7
|
+
specs: WorkerSpec[];
|
|
8
|
+
task: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function initSession(description: string, workerCount: number): Promise<Session>;
|
|
11
|
+
export declare function writeContextSnapshot(slug: string, task: string): Promise<string>;
|
|
12
|
+
export declare function decomposeTasks(task: string, specs: WorkerSpec[]): Promise<Task[]>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { writeFile } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { STATE_DIR, ensureStateDir, writeSession, writeTask } from '../state/session.js';
|
|
6
|
+
export function parseWorkerSpec(args) {
|
|
7
|
+
// Formats:
|
|
8
|
+
// omc team "task description"
|
|
9
|
+
// omc team 3 "task description"
|
|
10
|
+
// omc team 2:explore "task description"
|
|
11
|
+
// omc team 2:explore+1:code-reviewer "task description"
|
|
12
|
+
const task = args[args.length - 1] ?? '';
|
|
13
|
+
const specArg = args.length > 1 ? args[0] ?? '' : '';
|
|
14
|
+
if (!specArg) {
|
|
15
|
+
return { specs: [{ count: 2, agentType: 'general-purpose' }], task };
|
|
16
|
+
}
|
|
17
|
+
// Plain number: "3"
|
|
18
|
+
if (/^\d+$/.test(specArg)) {
|
|
19
|
+
return { specs: [{ count: parseInt(specArg), agentType: 'general-purpose' }], task };
|
|
20
|
+
}
|
|
21
|
+
// Typed specs: "2:explore+1:code-reviewer"
|
|
22
|
+
const parts = specArg.split('+');
|
|
23
|
+
const specs = parts.map(part => {
|
|
24
|
+
const [countStr, agentType] = part.split(':');
|
|
25
|
+
return {
|
|
26
|
+
count: parseInt(countStr ?? '1'),
|
|
27
|
+
agentType: agentType ?? 'general-purpose',
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
return { specs, task };
|
|
31
|
+
}
|
|
32
|
+
export async function initSession(description, workerCount) {
|
|
33
|
+
await ensureStateDir();
|
|
34
|
+
const session = {
|
|
35
|
+
id: randomUUID().slice(0, 8),
|
|
36
|
+
description,
|
|
37
|
+
status: 'running',
|
|
38
|
+
workerCount,
|
|
39
|
+
createdAt: new Date().toISOString(),
|
|
40
|
+
};
|
|
41
|
+
await writeSession(session);
|
|
42
|
+
return session;
|
|
43
|
+
}
|
|
44
|
+
export async function writeContextSnapshot(slug, task) {
|
|
45
|
+
const path = join(STATE_DIR, 'context', `${slug}.md`);
|
|
46
|
+
const content = `# Task Context\n\n**Task:** ${task}\n\n**Started:** ${new Date().toISOString()}\n\n## Notes\n\n_Workers will append findings here._\n`;
|
|
47
|
+
await writeFile(path, content);
|
|
48
|
+
return path;
|
|
49
|
+
}
|
|
50
|
+
export async function decomposeTasks(task, specs) {
|
|
51
|
+
const totalWorkers = specs.reduce((sum, s) => sum + s.count, 0);
|
|
52
|
+
const subtasks = await callClaudeDecompose(task, totalWorkers);
|
|
53
|
+
const tasks = [];
|
|
54
|
+
let idx = 0;
|
|
55
|
+
for (const spec of specs) {
|
|
56
|
+
for (let i = 0; i < spec.count; i++) {
|
|
57
|
+
const t = {
|
|
58
|
+
id: randomUUID().slice(0, 8),
|
|
59
|
+
description: subtasks[idx] ?? task,
|
|
60
|
+
agentType: spec.agentType,
|
|
61
|
+
status: 'pending',
|
|
62
|
+
createdAt: new Date().toISOString(),
|
|
63
|
+
};
|
|
64
|
+
tasks.push(t);
|
|
65
|
+
await writeTask(t);
|
|
66
|
+
idx++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return tasks;
|
|
70
|
+
}
|
|
71
|
+
function callClaudeDecompose(task, n) {
|
|
72
|
+
if (n <= 1)
|
|
73
|
+
return [task];
|
|
74
|
+
const prompt = `Decompose this task into exactly ${n} independent subtasks that can run in parallel. Each must be specific and actionable. Respond with a JSON array of ${n} strings — no explanation, no markdown, just the array.
|
|
75
|
+
|
|
76
|
+
Task: "${task}"`;
|
|
77
|
+
try {
|
|
78
|
+
const escaped = prompt.replace(/'/g, `'"'"'`);
|
|
79
|
+
const raw = execSync(`claude --print -p '${escaped}'`, {
|
|
80
|
+
encoding: 'utf8',
|
|
81
|
+
timeout: 30_000,
|
|
82
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
83
|
+
}).trim();
|
|
84
|
+
// Extract JSON array from the response (strip any surrounding prose)
|
|
85
|
+
const match = raw.match(/\[[\s\S]*\]/);
|
|
86
|
+
if (!match)
|
|
87
|
+
throw new Error('No JSON array in response');
|
|
88
|
+
const parsed = JSON.parse(match[0]);
|
|
89
|
+
if (!Array.isArray(parsed) || parsed.length === 0)
|
|
90
|
+
throw new Error('Empty array');
|
|
91
|
+
const subtasks = parsed.map(String);
|
|
92
|
+
// Pad or trim to exactly n
|
|
93
|
+
while (subtasks.length < n)
|
|
94
|
+
subtasks.push(task);
|
|
95
|
+
return subtasks.slice(0, n);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Fallback: every worker gets the same task description
|
|
99
|
+
return Array(n).fill(task);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Task } from '../state/session.js';
|
|
2
|
+
export declare function claimTask(workerId: string): Promise<Task | null>;
|
|
3
|
+
export declare function completeTask(task: Task, result: string): Promise<void>;
|
|
4
|
+
export declare function failTask(task: Task, error: string): Promise<void>;
|
|
5
|
+
export declare function pendingCount(): Promise<number>;
|
|
6
|
+
export declare function allDone(): Promise<boolean>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { readdir, readFile, rename, writeFile } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { STATE_DIR } from '../state/session.js';
|
|
5
|
+
const TASKS_DIR = join(STATE_DIR, 'tasks');
|
|
6
|
+
export async function claimTask(workerId) {
|
|
7
|
+
if (!existsSync(TASKS_DIR))
|
|
8
|
+
return null;
|
|
9
|
+
const files = await readdir(TASKS_DIR);
|
|
10
|
+
const pending = files.filter(f => f.endsWith('-pending.json'));
|
|
11
|
+
for (const file of pending) {
|
|
12
|
+
const oldPath = join(TASKS_DIR, file);
|
|
13
|
+
const task = JSON.parse(await readFile(oldPath, 'utf8'));
|
|
14
|
+
const newFile = file.replace('-pending.json', `-claimed-${workerId}.json`);
|
|
15
|
+
const newPath = join(TASKS_DIR, newFile);
|
|
16
|
+
// Atomic rename = claim. First writer wins.
|
|
17
|
+
try {
|
|
18
|
+
await rename(oldPath, newPath);
|
|
19
|
+
task.status = 'claimed';
|
|
20
|
+
task.workerId = workerId;
|
|
21
|
+
task.claimedAt = new Date().toISOString();
|
|
22
|
+
await writeFile(newPath, JSON.stringify(task, null, 2));
|
|
23
|
+
return task;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Another worker claimed it first — try next
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
export async function completeTask(task, result) {
|
|
33
|
+
const claimedFile = join(TASKS_DIR, `${task.id}-claimed-${task.workerId}.json`);
|
|
34
|
+
const doneFile = join(TASKS_DIR, `${task.id}-done-${task.workerId}.json`);
|
|
35
|
+
task.status = 'done';
|
|
36
|
+
task.result = result;
|
|
37
|
+
task.completedAt = new Date().toISOString();
|
|
38
|
+
await writeFile(claimedFile, JSON.stringify(task, null, 2));
|
|
39
|
+
await rename(claimedFile, doneFile);
|
|
40
|
+
}
|
|
41
|
+
export async function failTask(task, error) {
|
|
42
|
+
const claimedFile = join(TASKS_DIR, `${task.id}-claimed-${task.workerId}.json`);
|
|
43
|
+
const failedFile = join(TASKS_DIR, `${task.id}-failed-${task.workerId}.json`);
|
|
44
|
+
task.status = 'failed';
|
|
45
|
+
task.error = error;
|
|
46
|
+
task.completedAt = new Date().toISOString();
|
|
47
|
+
await writeFile(claimedFile, JSON.stringify(task, null, 2));
|
|
48
|
+
await rename(claimedFile, failedFile);
|
|
49
|
+
}
|
|
50
|
+
export async function pendingCount() {
|
|
51
|
+
if (!existsSync(TASKS_DIR))
|
|
52
|
+
return 0;
|
|
53
|
+
const files = await readdir(TASKS_DIR);
|
|
54
|
+
return files.filter(f => f.includes('-pending.json')).length;
|
|
55
|
+
}
|
|
56
|
+
export async function allDone() {
|
|
57
|
+
if (!existsSync(TASKS_DIR))
|
|
58
|
+
return true;
|
|
59
|
+
const files = await readdir(TASKS_DIR);
|
|
60
|
+
const active = files.filter(f => f.includes('-pending.json') || f.includes('-claimed-'));
|
|
61
|
+
return active.length === 0;
|
|
62
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chuckssmith/agentloom",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A workflow layer for Claude Code — reusable roles, persistence loops, and multi-agent crew coordination",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"agent",
|
|
8
|
+
"workflow",
|
|
9
|
+
"multi-agent"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/cssmith615/agentloom",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/cssmith615/agentloom.git"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "cssmith615",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"skills"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"bin": {
|
|
24
|
+
"loom": "dist/cli.js"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"dev": "tsc --watch",
|
|
29
|
+
"start": "node dist/cli.js"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=20"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^20.0.0",
|
|
36
|
+
"typescript": "^5.4.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: architect
|
|
3
|
+
description: Deep analysis and architecture review. Use before major implementation decisions or to audit existing systems. Returns a structured assessment with specific recommendations.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# $architect — Deep Analysis
|
|
7
|
+
|
|
8
|
+
You are now in **architect mode**. You will analyze deeply, not implement.
|
|
9
|
+
|
|
10
|
+
## Your job
|
|
11
|
+
Produce a structured architectural assessment. Be specific, cite file paths and line numbers, identify actual problems — not hypothetical ones.
|
|
12
|
+
|
|
13
|
+
## Process
|
|
14
|
+
|
|
15
|
+
### Step 1 — Explore (parallel)
|
|
16
|
+
Spawn `Explore` subagents in parallel to map the relevant systems simultaneously.
|
|
17
|
+
Use `run_in_background: true`.
|
|
18
|
+
|
|
19
|
+
### Step 2 — Synthesize
|
|
20
|
+
From the exploration results, identify:
|
|
21
|
+
- What exists and how it works
|
|
22
|
+
- Boundaries and contracts between components
|
|
23
|
+
- Actual problems (with evidence)
|
|
24
|
+
- Risks (specific, not generic)
|
|
25
|
+
- Recommended approach with tradeoffs
|
|
26
|
+
|
|
27
|
+
### Step 3 — Deliver
|
|
28
|
+
Return a structured report:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
## System map
|
|
32
|
+
[What exists, how it's connected]
|
|
33
|
+
|
|
34
|
+
## Findings
|
|
35
|
+
[Numbered list, each with: what, where (file:line), why it matters]
|
|
36
|
+
|
|
37
|
+
## Recommendation
|
|
38
|
+
[Specific approach, not "consider refactoring"]
|
|
39
|
+
|
|
40
|
+
## Risks
|
|
41
|
+
[What could go wrong with the recommendation]
|
|
42
|
+
|
|
43
|
+
## Open questions
|
|
44
|
+
[What you'd need to know before proceeding]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Rules
|
|
48
|
+
- Cite specific files and line numbers for every finding
|
|
49
|
+
- Do not recommend changes you haven't verified are needed
|
|
50
|
+
- "Consider refactoring" is not a recommendation — say what to refactor and why
|
|
51
|
+
- If the system is fine, say so
|
|
52
|
+
|
|
53
|
+
Begin analysis now.
|
package/skills/crew.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: crew
|
|
3
|
+
description: Spawn parallel workers on a task. Decomposes the work, runs workers simultaneously, then runs a verification pass. Use when the task is clearly decomposable into independent streams.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# $crew — Parallel Workers
|
|
7
|
+
|
|
8
|
+
You are now in **crew mode**. You will decompose this task, run workers in parallel, and verify the result.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
- Task has clearly independent work streams (e.g. audit 5 modules, port 3 subsystems)
|
|
12
|
+
- Work would benefit from simultaneous execution
|
|
13
|
+
- You want one worker per concern (explore / implement / verify)
|
|
14
|
+
|
|
15
|
+
## When NOT to use
|
|
16
|
+
- Task must be done sequentially (each step depends on the last) — use `$grind` instead
|
|
17
|
+
- Task is a quick single fix — just do it directly
|
|
18
|
+
|
|
19
|
+
## Execution rules
|
|
20
|
+
- Decompose into **truly independent** subtasks only
|
|
21
|
+
- Default crew: 1 `Explore` + 1 `general-purpose` + 1 `code-reviewer`
|
|
22
|
+
- For large tasks: up to 4 workers; beyond that, coordination cost exceeds benefit
|
|
23
|
+
- Use `run_in_background: true` for all worker spawns
|
|
24
|
+
- Collect all results before running the verification pass
|
|
25
|
+
|
|
26
|
+
## Crew structure
|
|
27
|
+
|
|
28
|
+
### Step 1 — Decompose
|
|
29
|
+
Break the task into N independent subtasks. Write them to `.agentloom/tasks/`.
|
|
30
|
+
|
|
31
|
+
For each subtask, decide the right agent type:
|
|
32
|
+
- `Explore` — research, read files, understand structure (read-only)
|
|
33
|
+
- `Plan` — architecture decisions, approach planning
|
|
34
|
+
- `general-purpose` — implementation work
|
|
35
|
+
- `code-reviewer` — audit, security review, quality check
|
|
36
|
+
- `frontend-developer` — UI/component work
|
|
37
|
+
|
|
38
|
+
### Step 2 — Launch crew (parallel)
|
|
39
|
+
Spawn all workers simultaneously with `run_in_background: true`.
|
|
40
|
+
Each worker gets: the subtask description + the shared context path.
|
|
41
|
+
|
|
42
|
+
### Step 3 — Collect results
|
|
43
|
+
Wait for all background workers to complete.
|
|
44
|
+
Read each result from `.agentloom/workers/`.
|
|
45
|
+
|
|
46
|
+
### Step 4 — Verify
|
|
47
|
+
Spawn a `code-reviewer` subagent across all completed work.
|
|
48
|
+
Return PASS or FAIL with evidence.
|
|
49
|
+
|
|
50
|
+
### Step 5 — Report
|
|
51
|
+
Summarize what each worker did and the final verification result.
|
|
52
|
+
|
|
53
|
+
## Completion contract
|
|
54
|
+
Same as `$grind` — only done when verification passes with evidence.
|
|
55
|
+
|
|
56
|
+
Begin now. Decompose the task, then launch the crew.
|
package/skills/grind.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: grind
|
|
3
|
+
description: Persistence loop — keeps working on a task until it is fully complete and verified. Use when you need guaranteed completion, not "do your best".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# $grind — Persistence Loop
|
|
7
|
+
|
|
8
|
+
You are now in **grind mode**. You will not stop until this task is genuinely complete and passes verification.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
- Task requires guaranteed completion with verification
|
|
12
|
+
- Multi-step work that may span many iterations
|
|
13
|
+
- User says "don't stop", "keep going", "finish this", "must work"
|
|
14
|
+
|
|
15
|
+
## Execution rules
|
|
16
|
+
- Fire independent sub-agent calls **simultaneously** using `run_in_background: true`
|
|
17
|
+
- Use the `Explore` subagent type for research/read-only work
|
|
18
|
+
- Use the `Plan` subagent type before major implementation decisions
|
|
19
|
+
- Use the `code-reviewer` subagent type for verification
|
|
20
|
+
- Do NOT declare done until verification passes
|
|
21
|
+
- Do NOT reduce scope to make the task easier
|
|
22
|
+
- Do NOT delete or skip tests to make them pass
|
|
23
|
+
|
|
24
|
+
## Loop structure
|
|
25
|
+
|
|
26
|
+
### Step 0 — Context snapshot
|
|
27
|
+
Before starting, write a context file to `.agentloom/context/{task-slug}.md` with:
|
|
28
|
+
- Task statement
|
|
29
|
+
- Desired outcome
|
|
30
|
+
- Known facts
|
|
31
|
+
- Constraints
|
|
32
|
+
- Unknowns
|
|
33
|
+
|
|
34
|
+
### Step 1 — Plan
|
|
35
|
+
Spawn a `Plan` subagent to map the implementation approach. Save the plan.
|
|
36
|
+
|
|
37
|
+
### Step 2 — Execute (parallel)
|
|
38
|
+
Spawn parallel subagents for independent work streams. Use `run_in_background: true`.
|
|
39
|
+
|
|
40
|
+
### Step 3 — Verify
|
|
41
|
+
Spawn a `code-reviewer` subagent with this prompt:
|
|
42
|
+
> "Review the completed work. Run all tests. Check that the original task requirements are met. Return PASS or FAIL with specific evidence."
|
|
43
|
+
|
|
44
|
+
### Step 4 — Iterate or complete
|
|
45
|
+
- **PASS** → report completion with evidence
|
|
46
|
+
- **FAIL** → go back to Step 2 with the reviewer's specific findings
|
|
47
|
+
|
|
48
|
+
## Completion contract
|
|
49
|
+
You may only declare this task done when:
|
|
50
|
+
1. A `code-reviewer` subagent has returned PASS
|
|
51
|
+
2. You can cite specific evidence (test output, file paths, behavior observed)
|
|
52
|
+
|
|
53
|
+
Begin now. State the task, write the context snapshot, then start the loop.
|