@chuckssmith/agentloom 0.3.0 → 0.5.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 +80 -37
- package/dist/cli.js +26 -3
- package/dist/commands/collect.d.ts +1 -0
- package/dist/commands/collect.js +79 -0
- package/dist/commands/crew.js +60 -31
- package/dist/commands/reset.d.ts +1 -0
- package/dist/commands/reset.js +17 -0
- package/dist/commands/status.js +43 -11
- package/dist/commands/watch.d.ts +1 -0
- package/dist/commands/watch.js +69 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# agentloom
|
|
2
2
|
|
|
3
|
-
A workflow layer for Claude Code
|
|
3
|
+
A workflow layer for Claude Code — persistence loops, parallel crews, and typed agent roles, built natively on what Claude Code already provides.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install -g agentloom
|
|
6
|
+
npm install -g @chuckssmith/agentloom
|
|
7
7
|
loom setup
|
|
8
8
|
```
|
|
9
9
|
|
|
@@ -12,10 +12,11 @@ loom setup
|
|
|
12
12
|
## What this is
|
|
13
13
|
|
|
14
14
|
Claude Code is the execution engine. agentloom adds:
|
|
15
|
+
|
|
15
16
|
- **`$grind`** — persistence loop that keeps working until a task is verified complete
|
|
16
|
-
- **`$crew`** — parallel workers that decompose and execute simultaneously
|
|
17
|
+
- **`$crew`** — parallel workers that decompose and execute simultaneously
|
|
17
18
|
- **`$architect`** — deep analysis mode before major decisions
|
|
18
|
-
- **`loom crew`** — CLI to spawn a crew
|
|
19
|
+
- **`loom crew`** — CLI to spawn and monitor a crew from your terminal
|
|
19
20
|
|
|
20
21
|
It does not replace Claude Code. It wraps it.
|
|
21
22
|
|
|
@@ -24,14 +25,24 @@ It does not replace Claude Code. It wraps it.
|
|
|
24
25
|
## Quick start
|
|
25
26
|
|
|
26
27
|
```bash
|
|
27
|
-
npm install -g agentloom
|
|
28
|
-
loom setup # installs
|
|
28
|
+
npm install -g @chuckssmith/agentloom
|
|
29
|
+
loom setup # installs $grind, $crew, $architect skills + validates deps
|
|
29
30
|
|
|
30
|
-
#
|
|
31
|
+
# Spawn workers from your terminal:
|
|
31
32
|
loom crew "audit every API endpoint for security issues"
|
|
32
33
|
loom crew 2:explore+1:code-reviewer "review the payment flow"
|
|
34
|
+
loom crew --dry-run 3 "migrate the database schema" # preview before launching
|
|
35
|
+
|
|
36
|
+
# Monitor:
|
|
37
|
+
loom watch # live tail all worker logs
|
|
38
|
+
loom status # session overview + stale worker detection
|
|
39
|
+
loom logs w00 # full output for one worker
|
|
40
|
+
|
|
41
|
+
# After workers finish:
|
|
42
|
+
loom collect # synthesize results with Claude
|
|
43
|
+
loom reset --force # clear state for next run
|
|
33
44
|
|
|
34
|
-
# Or
|
|
45
|
+
# Or use inside any Claude Code session:
|
|
35
46
|
# $grind "port the auth module to the new interface"
|
|
36
47
|
# $crew "analyze all three data pipeline stages in parallel"
|
|
37
48
|
```
|
|
@@ -40,49 +51,81 @@ loom crew 2:explore+1:code-reviewer "review the payment flow"
|
|
|
40
51
|
|
|
41
52
|
## Skills
|
|
42
53
|
|
|
43
|
-
Install with `loom setup`.
|
|
54
|
+
Install with `loom setup`. Use inside any Claude Code session:
|
|
44
55
|
|
|
45
|
-
| Skill | What it does |
|
|
46
|
-
|
|
47
|
-
| `$grind` | Persistence loop
|
|
48
|
-
| `$crew` |
|
|
49
|
-
| `$architect` | Deep analysis — maps system, finds real problems, recommends approach |
|
|
56
|
+
| Skill | Trigger | What it does |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `$grind` | `$grind "<task>"` | Persistence loop — plans, executes in parallel, verifies. Won't stop until a code-reviewer subagent returns PASS |
|
|
59
|
+
| `$crew` | `$crew "<task>"` | Decomposes task into independent streams, runs workers simultaneously, verifies result |
|
|
60
|
+
| `$architect` | `$architect "<task>"` | Deep analysis — maps the system, finds real problems, recommends approach before you write code |
|
|
50
61
|
|
|
51
62
|
---
|
|
52
63
|
|
|
53
|
-
## CLI
|
|
64
|
+
## CLI reference
|
|
65
|
+
|
|
66
|
+
### Spawning workers
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
loom crew "<task>" 2 general-purpose workers (default)
|
|
70
|
+
loom crew 3 "<task>" 3 workers
|
|
71
|
+
loom crew 2:explore "<task>" 2 explore-type workers
|
|
72
|
+
loom crew 2:explore+1:code-reviewer "<task>" typed crew
|
|
73
|
+
loom crew --dry-run 3 "<task>" preview decomposed subtasks, no launch
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Monitoring
|
|
54
77
|
|
|
55
78
|
```
|
|
56
|
-
loom
|
|
57
|
-
loom
|
|
58
|
-
loom
|
|
59
|
-
loom
|
|
60
|
-
loom status Show active session
|
|
61
|
-
loom setup Install skills + validate
|
|
79
|
+
loom watch Live tail all worker logs with color-coded output
|
|
80
|
+
loom status Session overview, task counts, stale worker detection
|
|
81
|
+
loom logs Summary of all workers (status + last line)
|
|
82
|
+
loom logs <workerId> Full log + result for one worker (e.g. loom logs w00)
|
|
62
83
|
```
|
|
63
84
|
|
|
64
|
-
###
|
|
85
|
+
### After workers finish
|
|
65
86
|
|
|
66
|
-
|
|
87
|
+
```
|
|
88
|
+
loom collect Read worker results + synthesize summary with Claude
|
|
89
|
+
loom collect --no-ai Concatenate results without Claude synthesis
|
|
90
|
+
```
|
|
67
91
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
### Housekeeping
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
loom setup Install skills to ~/.claude/skills/, validate deps
|
|
96
|
+
loom reset --force Wipe .claude-team/ state
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Worker types
|
|
102
|
+
|
|
103
|
+
Each type gets a role-specific system prompt that shapes its behavior:
|
|
104
|
+
|
|
105
|
+
| Type | Role | Modifies files? |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `explore` | Maps code, documents structure and connections | No |
|
|
108
|
+
| `plan` | Reasons about approach, produces ordered action plan | No |
|
|
109
|
+
| `code-reviewer` | Audits for correctness, security, quality; assigns severity | No |
|
|
110
|
+
| `frontend-developer` | UI, components, styling, client-side logic | Yes |
|
|
111
|
+
| `general-purpose` | Does whatever the subtask requires (default) | Yes |
|
|
75
112
|
|
|
76
113
|
---
|
|
77
114
|
|
|
78
115
|
## State directory
|
|
79
116
|
|
|
117
|
+
Session state lives in `.claude-team/` (gitignored):
|
|
118
|
+
|
|
80
119
|
```
|
|
81
|
-
.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
120
|
+
.claude-team/
|
|
121
|
+
session.json Active session metadata
|
|
122
|
+
context/ Shared context snapshots (workers read + append)
|
|
123
|
+
tasks/ Task queue — workers claim atomically via file rename
|
|
124
|
+
workers/
|
|
125
|
+
w00.log Live stdout from worker 00
|
|
126
|
+
w00-prompt.md Prompt sent to worker 00
|
|
127
|
+
w00-result.md Result summary written by worker 00 on completion
|
|
128
|
+
summary.md Final synthesis from loom collect
|
|
86
129
|
```
|
|
87
130
|
|
|
88
131
|
---
|
|
@@ -90,8 +133,8 @@ Matches Claude Code's built-in subagent types:
|
|
|
90
133
|
## Requirements
|
|
91
134
|
|
|
92
135
|
- Node.js 20+
|
|
93
|
-
- Claude Code CLI (`claude`)
|
|
94
|
-
- tmux (optional — used
|
|
136
|
+
- Claude Code CLI (`claude`) — authenticated
|
|
137
|
+
- tmux (optional — used on Mac/Linux; falls back to background processes on Windows/WSL)
|
|
95
138
|
|
|
96
139
|
---
|
|
97
140
|
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,9 @@ import { setup } from './commands/setup.js';
|
|
|
3
3
|
import { crew } from './commands/crew.js';
|
|
4
4
|
import { status } from './commands/status.js';
|
|
5
5
|
import { logs } from './commands/logs.js';
|
|
6
|
+
import { collect } from './commands/collect.js';
|
|
7
|
+
import { reset } from './commands/reset.js';
|
|
8
|
+
import { watch } from './commands/watch.js';
|
|
6
9
|
const [, , command, ...args] = process.argv;
|
|
7
10
|
const usage = `
|
|
8
11
|
agentloom (loom) — workflow layer for Claude Code
|
|
@@ -12,9 +15,20 @@ Usage:
|
|
|
12
15
|
loom crew [N] "<task>" Spawn N parallel workers on a task
|
|
13
16
|
loom crew 2:explore "<task>" Spawn typed workers (explore/plan/code-reviewer)
|
|
14
17
|
loom crew --dry-run [N] "<task>" Preview decomposed subtasks without launching
|
|
15
|
-
loom
|
|
18
|
+
loom watch Live tail all worker logs (Ctrl+C to stop)
|
|
19
|
+
loom status Show active crew session + stale worker detection
|
|
16
20
|
loom logs Show worker output summary
|
|
17
21
|
loom logs <workerId> Show full log for a specific worker
|
|
22
|
+
loom collect Synthesize worker results into a summary
|
|
23
|
+
loom collect --no-ai Collect results without Claude synthesis
|
|
24
|
+
loom reset --force Clear all session state
|
|
25
|
+
|
|
26
|
+
Agent types (use with crew):
|
|
27
|
+
explore Read-only research and mapping
|
|
28
|
+
plan Architecture and approach planning
|
|
29
|
+
code-reviewer Audit for correctness, security, quality
|
|
30
|
+
frontend-developer UI and component work
|
|
31
|
+
general-purpose Default — does whatever the subtask requires
|
|
18
32
|
|
|
19
33
|
Modes (use $grind or $crew inside a Claude Code session):
|
|
20
34
|
$grind Persistence loop — keeps working until verified complete
|
|
@@ -26,8 +40,8 @@ Examples:
|
|
|
26
40
|
loom crew 3 "audit every API endpoint for security issues"
|
|
27
41
|
loom crew 2:explore+1:code-reviewer "review the payment flow"
|
|
28
42
|
loom crew --dry-run 3 "migrate the database schema"
|
|
29
|
-
loom
|
|
30
|
-
loom
|
|
43
|
+
loom watch
|
|
44
|
+
loom collect
|
|
31
45
|
`;
|
|
32
46
|
switch (command) {
|
|
33
47
|
case 'setup':
|
|
@@ -36,12 +50,21 @@ switch (command) {
|
|
|
36
50
|
case 'crew':
|
|
37
51
|
await crew(args);
|
|
38
52
|
break;
|
|
53
|
+
case 'watch':
|
|
54
|
+
await watch(args);
|
|
55
|
+
break;
|
|
39
56
|
case 'status':
|
|
40
57
|
await status();
|
|
41
58
|
break;
|
|
42
59
|
case 'logs':
|
|
43
60
|
await logs(args);
|
|
44
61
|
break;
|
|
62
|
+
case 'collect':
|
|
63
|
+
await collect(args);
|
|
64
|
+
break;
|
|
65
|
+
case 'reset':
|
|
66
|
+
await reset(args);
|
|
67
|
+
break;
|
|
45
68
|
default:
|
|
46
69
|
console.log(usage);
|
|
47
70
|
process.exit(command ? 1 : 0);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function collect(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readFile, readdir, writeFile } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
5
|
+
import { STATE_DIR, readSession, readTasks } from '../state/session.js';
|
|
6
|
+
const WORKERS_DIR = join(STATE_DIR, 'workers');
|
|
7
|
+
export async function collect(args) {
|
|
8
|
+
if (!existsSync(WORKERS_DIR)) {
|
|
9
|
+
console.log('No session found. Run: loom crew "<task>"');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const session = await readSession();
|
|
13
|
+
const tasks = await readTasks();
|
|
14
|
+
const files = await readdir(WORKERS_DIR);
|
|
15
|
+
const resultFiles = files.filter(f => f.endsWith('-result.md')).sort();
|
|
16
|
+
if (resultFiles.length === 0) {
|
|
17
|
+
console.log('No worker results yet. Check status with: loom status');
|
|
18
|
+
console.log('Workers still running? Check: loom logs');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
console.log(`\nCollecting results from ${resultFiles.length} worker(s)...\n`);
|
|
22
|
+
const results = [];
|
|
23
|
+
for (const f of resultFiles) {
|
|
24
|
+
const workerId = f.replace('-result.md', '');
|
|
25
|
+
const content = await readFile(join(WORKERS_DIR, f), 'utf8');
|
|
26
|
+
results.push({ workerId, content });
|
|
27
|
+
console.log(` ✓ ${workerId}`);
|
|
28
|
+
}
|
|
29
|
+
const taskDesc = session?.description ?? 'unknown task';
|
|
30
|
+
const summaryPath = join(STATE_DIR, 'summary.md');
|
|
31
|
+
// Build raw summary
|
|
32
|
+
const raw = results.map(r => `## ${r.workerId}\n\n${r.content.trim()}`).join('\n\n---\n\n');
|
|
33
|
+
// Optionally synthesize with Claude
|
|
34
|
+
const synthesize = !args.includes('--no-ai');
|
|
35
|
+
let synthesis = '';
|
|
36
|
+
if (synthesize) {
|
|
37
|
+
console.log('\nSynthesizing with Claude...');
|
|
38
|
+
const prompt = `You are summarizing the results of a multi-agent crew that worked on this task:
|
|
39
|
+
|
|
40
|
+
"${taskDesc}"
|
|
41
|
+
|
|
42
|
+
Here are the individual worker results:
|
|
43
|
+
|
|
44
|
+
${raw}
|
|
45
|
+
|
|
46
|
+
Write a concise synthesis (under 300 words) that:
|
|
47
|
+
1. States what was accomplished overall
|
|
48
|
+
2. Highlights the key findings or changes from each worker
|
|
49
|
+
3. Calls out any blockers, conflicts, or follow-up work needed
|
|
50
|
+
|
|
51
|
+
Be direct and specific. No filler.`;
|
|
52
|
+
const result = spawnSync('claude', ['--print', '-p', prompt], {
|
|
53
|
+
encoding: 'utf8',
|
|
54
|
+
timeout: 60_000,
|
|
55
|
+
});
|
|
56
|
+
if (result.status === 0 && result.stdout.trim()) {
|
|
57
|
+
synthesis = result.stdout.trim();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const summaryContent = [
|
|
61
|
+
`# Crew Summary`,
|
|
62
|
+
``,
|
|
63
|
+
`**Task:** ${taskDesc}`,
|
|
64
|
+
`**Workers:** ${resultFiles.length}`,
|
|
65
|
+
`**Collected:** ${new Date().toISOString()}`,
|
|
66
|
+
``,
|
|
67
|
+
synthesis ? `## Synthesis\n\n${synthesis}` : '',
|
|
68
|
+
synthesis ? `\n---\n` : '',
|
|
69
|
+
`## Individual Results`,
|
|
70
|
+
``,
|
|
71
|
+
raw,
|
|
72
|
+
].filter(l => l !== undefined).join('\n');
|
|
73
|
+
await writeFile(summaryPath, summaryContent);
|
|
74
|
+
console.log(`\nSummary written to: ${summaryPath}`);
|
|
75
|
+
if (synthesis) {
|
|
76
|
+
console.log('\n── Synthesis ─────────────────────────────────');
|
|
77
|
+
console.log(synthesis);
|
|
78
|
+
}
|
|
79
|
+
}
|
package/dist/commands/crew.js
CHANGED
|
@@ -13,6 +13,53 @@ const hasTmux = () => {
|
|
|
13
13
|
}
|
|
14
14
|
};
|
|
15
15
|
const isWSL = () => process.platform === 'linux' && !!process.env.WSL_DISTRO_NAME;
|
|
16
|
+
// Role-specific instructions injected into each worker prompt
|
|
17
|
+
const AGENT_ROLE = {
|
|
18
|
+
'explore': `Your role is EXPLORER. You are read-only. Do not modify any files.
|
|
19
|
+
- Map out the relevant code, files, and structure
|
|
20
|
+
- Document what exists, how it connects, and what's notable
|
|
21
|
+
- Your output feeds the other workers — be thorough and specific`,
|
|
22
|
+
'plan': `Your role is PLANNER. You are read-only. Do not modify any files.
|
|
23
|
+
- Reason about the best approach to the subtask
|
|
24
|
+
- Identify risks, dependencies, and open questions
|
|
25
|
+
- Produce a concrete, ordered action plan other workers can execute`,
|
|
26
|
+
'code-reviewer': `Your role is CODE REVIEWER. You are read-only. Do not modify any files.
|
|
27
|
+
- Audit the relevant code for correctness, security, and quality
|
|
28
|
+
- Flag specific lines, patterns, or logic that are problematic
|
|
29
|
+
- Assign severity (critical / high / medium / low) to each finding`,
|
|
30
|
+
'frontend-developer': `Your role is FRONTEND DEVELOPER.
|
|
31
|
+
- Focus on UI, components, styling, and client-side logic
|
|
32
|
+
- Follow existing conventions in the codebase
|
|
33
|
+
- Write clean, accessible code`,
|
|
34
|
+
'general-purpose': `Your role is GENERAL PURPOSE WORKER.
|
|
35
|
+
- Do whatever the subtask requires — research, implementation, or both
|
|
36
|
+
- Use all tools available to you`,
|
|
37
|
+
};
|
|
38
|
+
function buildWorkerPrompt(subtask, contextPath, sessionId, workerId, agentType) {
|
|
39
|
+
const resultFile = join(STATE_DIR, 'workers', `${workerId}-result.md`);
|
|
40
|
+
const roleInstructions = AGENT_ROLE[agentType] ?? AGENT_ROLE['general-purpose'];
|
|
41
|
+
return `You are worker ${workerId} in an agentloom crew session (${sessionId}).
|
|
42
|
+
|
|
43
|
+
${roleInstructions}
|
|
44
|
+
|
|
45
|
+
## Your assigned subtask
|
|
46
|
+
|
|
47
|
+
"${subtask}"
|
|
48
|
+
|
|
49
|
+
## Protocol
|
|
50
|
+
|
|
51
|
+
1. Read the shared context: ${contextPath}
|
|
52
|
+
2. Do the work thoroughly using all tools available to you
|
|
53
|
+
3. Append key findings to the context file so other workers can see them
|
|
54
|
+
4. When done, write a result summary to: ${resultFile}
|
|
55
|
+
Format: brief markdown — what you did, what you found, any blockers
|
|
56
|
+
|
|
57
|
+
## Rules
|
|
58
|
+
- Stay focused on your assigned subtask and role
|
|
59
|
+
- Do not stop until your subtask is complete or you have hit a genuine blocker
|
|
60
|
+
|
|
61
|
+
Begin now.`;
|
|
62
|
+
}
|
|
16
63
|
export async function crew(args) {
|
|
17
64
|
if (args.length === 0) {
|
|
18
65
|
console.error('Usage: loom crew [--dry-run] [N] "<task>"');
|
|
@@ -49,47 +96,28 @@ export async function crew(args) {
|
|
|
49
96
|
console.log(`Tasks: ${tasks.length} created`);
|
|
50
97
|
console.log(`Context: ${contextPath}\n`);
|
|
51
98
|
if (hasTmux() && !isWSL()) {
|
|
52
|
-
await launchTmux(session.id,
|
|
99
|
+
await launchTmux(session.id, specs, tasks, contextPath);
|
|
53
100
|
}
|
|
54
101
|
else {
|
|
55
|
-
await launchBackground(session.id, specs, tasks
|
|
102
|
+
await launchBackground(session.id, specs, tasks, contextPath);
|
|
56
103
|
}
|
|
57
104
|
console.log(`\nWorkers launched. Monitor with:`);
|
|
58
105
|
console.log(` loom status`);
|
|
59
106
|
console.log(` loom logs`);
|
|
107
|
+
console.log(` loom crew --watch (live tail)`);
|
|
60
108
|
console.log(`State dir: ${STATE_DIR}/`);
|
|
61
109
|
}
|
|
62
|
-
function
|
|
63
|
-
const resultFile = join(STATE_DIR, 'workers', `${workerId}-result.md`);
|
|
64
|
-
return `You are worker ${workerId} in an agentloom crew session (${sessionId}).
|
|
65
|
-
|
|
66
|
-
Your assigned subtask: "${subtask}"
|
|
67
|
-
|
|
68
|
-
## Protocol
|
|
69
|
-
|
|
70
|
-
1. Read the shared context: ${contextPath}
|
|
71
|
-
2. Do the work thoroughly using all tools available to you
|
|
72
|
-
3. When done, write a result summary to: ${resultFile}
|
|
73
|
-
Format: brief markdown — what you did, what you found, any blockers
|
|
74
|
-
|
|
75
|
-
## Rules
|
|
76
|
-
- Focus only on your assigned subtask
|
|
77
|
-
- Write findings to the context file (${contextPath}) so other workers can see them
|
|
78
|
-
- Do not stop until your subtask is complete or you have hit a genuine blocker
|
|
79
|
-
|
|
80
|
-
Begin now.`;
|
|
81
|
-
}
|
|
82
|
-
async function launchBackground(sessionId, specs, subtasks, contextPath) {
|
|
110
|
+
async function launchBackground(sessionId, specs, tasks, contextPath) {
|
|
83
111
|
await mkdir(join(STATE_DIR, 'workers'), { recursive: true });
|
|
84
112
|
let workerIdx = 0;
|
|
85
113
|
for (const spec of specs) {
|
|
86
114
|
for (let i = 0; i < spec.count; i++) {
|
|
87
115
|
const workerId = `w${String(workerIdx).padStart(2, '0')}`;
|
|
88
|
-
const subtask =
|
|
116
|
+
const subtask = tasks[workerIdx]?.description ?? tasks[0]?.description ?? '';
|
|
117
|
+
const agentType = tasks[workerIdx]?.agentType ?? spec.agentType;
|
|
89
118
|
workerIdx++;
|
|
90
|
-
const prompt = buildWorkerPrompt(subtask, contextPath, sessionId, workerId);
|
|
119
|
+
const prompt = buildWorkerPrompt(subtask, contextPath, sessionId, workerId, agentType);
|
|
91
120
|
const logFile = join(STATE_DIR, 'workers', `${workerId}.log`);
|
|
92
|
-
// Write prompt to disk for inspection
|
|
93
121
|
await writeFile(join(STATE_DIR, 'workers', `${workerId}-prompt.md`), prompt);
|
|
94
122
|
const log = await open(logFile, 'w');
|
|
95
123
|
const child = spawn('claude', ['--print', '--dangerously-skip-permissions', '-p', prompt], {
|
|
@@ -99,27 +127,28 @@ async function launchBackground(sessionId, specs, subtasks, contextPath) {
|
|
|
99
127
|
});
|
|
100
128
|
child.on('close', () => log.close());
|
|
101
129
|
child.unref();
|
|
102
|
-
console.log(` ✓ Worker ${workerId} (${
|
|
130
|
+
console.log(` ✓ Worker ${workerId} (${agentType}) launched [pid ${child.pid}] → ${logFile}`);
|
|
103
131
|
}
|
|
104
132
|
}
|
|
105
133
|
}
|
|
106
|
-
async function launchTmux(sessionId,
|
|
134
|
+
async function launchTmux(sessionId, specs, tasks, contextPath) {
|
|
107
135
|
const tmuxSession = `loom-${sessionId}`;
|
|
108
136
|
execSync(`tmux new-session -d -s ${tmuxSession} -x 220 -y 50`);
|
|
109
137
|
let workerIdx = 0;
|
|
110
138
|
for (const spec of specs) {
|
|
111
139
|
for (let i = 0; i < spec.count; i++) {
|
|
112
140
|
const workerId = `w${String(workerIdx).padStart(2, '0')}`;
|
|
113
|
-
const subtask =
|
|
141
|
+
const subtask = tasks[workerIdx]?.description ?? tasks[0]?.description ?? '';
|
|
142
|
+
const agentType = tasks[workerIdx]?.agentType ?? spec.agentType;
|
|
114
143
|
workerIdx++;
|
|
115
|
-
const prompt = buildWorkerPrompt(subtask, contextPath, sessionId, workerId);
|
|
144
|
+
const prompt = buildWorkerPrompt(subtask, contextPath, sessionId, workerId, agentType);
|
|
116
145
|
if (workerIdx > 1) {
|
|
117
146
|
execSync(`tmux split-window -h -t ${tmuxSession}`);
|
|
118
147
|
execSync(`tmux select-layout -t ${tmuxSession} tiled`);
|
|
119
148
|
}
|
|
120
149
|
const cmd = `AGENTLOOM_WORKER_ID=${workerId} AGENTLOOM_SESSION=${sessionId} claude --print --dangerously-skip-permissions -p '${prompt.replace(/'/g, "'\"'\"'")}'; echo '[worker done]'; read`;
|
|
121
150
|
execSync(`tmux send-keys -t ${tmuxSession} "${cmd}" Enter`);
|
|
122
|
-
console.log(` ✓ Worker ${workerId} (${
|
|
151
|
+
console.log(` ✓ Worker ${workerId} (${agentType}) launched in tmux pane`);
|
|
123
152
|
}
|
|
124
153
|
}
|
|
125
154
|
execSync(`tmux attach-session -t ${tmuxSession}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function reset(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { rm } from 'fs/promises';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { STATE_DIR } from '../state/session.js';
|
|
4
|
+
export async function reset(args) {
|
|
5
|
+
if (!existsSync(STATE_DIR)) {
|
|
6
|
+
console.log('Nothing to reset.');
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
const force = args.includes('--force') || args.includes('-f');
|
|
10
|
+
if (!force) {
|
|
11
|
+
console.log(`This will delete all session state in ${STATE_DIR}/`);
|
|
12
|
+
console.log('Run with --force to confirm: loom reset --force');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
await rm(STATE_DIR, { recursive: true, force: true });
|
|
16
|
+
console.log(`✓ Session state cleared (${STATE_DIR}/)`);
|
|
17
|
+
}
|
package/dist/commands/status.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { readSession, readTasks,
|
|
2
|
-
import { existsSync } from 'fs';
|
|
3
|
-
import {
|
|
1
|
+
import { readSession, readTasks, STATE_DIR } from '../state/session.js';
|
|
2
|
+
import { existsSync, statSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { readdir } from 'fs/promises';
|
|
5
|
+
const STALE_THRESHOLD_MS = 10 * 60 * 1000; // 10 minutes with no log growth = stale
|
|
4
6
|
export async function status() {
|
|
5
7
|
if (!existsSync(STATE_DIR)) {
|
|
6
8
|
console.log('No active session. Run: loom crew "<task>"');
|
|
@@ -12,7 +14,6 @@ export async function status() {
|
|
|
12
14
|
return;
|
|
13
15
|
}
|
|
14
16
|
const tasks = await readTasks();
|
|
15
|
-
const workers = await readWorkers();
|
|
16
17
|
const pending = tasks.filter(t => t.status === 'pending').length;
|
|
17
18
|
const claimed = tasks.filter(t => t.status === 'claimed').length;
|
|
18
19
|
const done = tasks.filter(t => t.status === 'done').length;
|
|
@@ -22,13 +23,44 @@ export async function status() {
|
|
|
22
23
|
console.log(`Task: ${session.description}`);
|
|
23
24
|
console.log(`Started: ${session.createdAt}`);
|
|
24
25
|
console.log(`\nTasks: ${pending} pending ${claimed} active ${done} done ${failed} failed`);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
// Worker status from log files
|
|
27
|
+
const workersDir = join(STATE_DIR, 'workers');
|
|
28
|
+
if (!existsSync(workersDir))
|
|
29
|
+
return;
|
|
30
|
+
const files = await readdir(workersDir);
|
|
31
|
+
const logFiles = files.filter(f => f.endsWith('.log')).sort();
|
|
32
|
+
if (logFiles.length === 0)
|
|
33
|
+
return;
|
|
34
|
+
console.log(`\nWorkers: ${logFiles.length}`);
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
for (const logFile of logFiles) {
|
|
37
|
+
const workerId = logFile.replace('.log', '');
|
|
38
|
+
const logPath = join(workersDir, logFile);
|
|
39
|
+
const resultPath = join(workersDir, `${workerId}-result.md`);
|
|
40
|
+
const hasResult = existsSync(resultPath);
|
|
41
|
+
if (hasResult) {
|
|
42
|
+
console.log(` [${workerId}] done ✓`);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
// Check if log is growing (worker is alive) or stale
|
|
46
|
+
const logStat = statSync(logPath);
|
|
47
|
+
const msSinceWrite = now - logStat.mtimeMs;
|
|
48
|
+
const isStale = msSinceWrite > STALE_THRESHOLD_MS;
|
|
49
|
+
const logSize = logStat.size;
|
|
50
|
+
if (logSize === 0) {
|
|
51
|
+
console.log(` [${workerId}] starting...`);
|
|
32
52
|
}
|
|
53
|
+
else if (isStale) {
|
|
54
|
+
const mins = Math.round(msSinceWrite / 60000);
|
|
55
|
+
console.log(` [${workerId}] STALE — no activity for ${mins}m (log: ${logPath})`);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
const secs = Math.round(msSinceWrite / 1000);
|
|
59
|
+
console.log(` [${workerId}] running (last activity ${secs}s ago)`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const allDone = logFiles.every(f => existsSync(join(workersDir, f.replace('.log', '-result.md'))));
|
|
63
|
+
if (allDone && logFiles.length > 0) {
|
|
64
|
+
console.log(`\nAll workers done. Run: loom collect`);
|
|
33
65
|
}
|
|
34
66
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function watch(_args: string[]): Promise<void>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { readdir, stat, readFile } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { STATE_DIR } from '../state/session.js';
|
|
5
|
+
const WORKERS_DIR = join(STATE_DIR, 'workers');
|
|
6
|
+
const POLL_MS = 800;
|
|
7
|
+
// A rotating set of ANSI colors for worker prefixes
|
|
8
|
+
const COLORS = ['\x1b[36m', '\x1b[33m', '\x1b[35m', '\x1b[32m', '\x1b[34m', '\x1b[31m'];
|
|
9
|
+
const RESET = '\x1b[0m';
|
|
10
|
+
const DIM = '\x1b[2m';
|
|
11
|
+
export async function watch(_args) {
|
|
12
|
+
if (!existsSync(WORKERS_DIR)) {
|
|
13
|
+
console.log('No active session. Run: loom crew "<task>"');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
console.log(`${DIM}Watching worker logs. Ctrl+C to stop.${RESET}\n`);
|
|
17
|
+
// Track how many bytes we've read from each log file
|
|
18
|
+
const offsets = {};
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
// eslint-disable-next-line no-constant-condition
|
|
21
|
+
while (true) {
|
|
22
|
+
if (!existsSync(WORKERS_DIR))
|
|
23
|
+
break;
|
|
24
|
+
const files = await readdir(WORKERS_DIR);
|
|
25
|
+
const logFiles = files.filter(f => f.endsWith('.log')).sort();
|
|
26
|
+
for (const logFile of logFiles) {
|
|
27
|
+
const workerId = logFile.replace('.log', '');
|
|
28
|
+
const color = COLORS[parseInt(workerId.replace('w', ''), 10) % COLORS.length] ?? COLORS[0];
|
|
29
|
+
const filePath = join(WORKERS_DIR, logFile);
|
|
30
|
+
if (!seen.has(workerId)) {
|
|
31
|
+
seen.add(workerId);
|
|
32
|
+
const resultExists = existsSync(join(WORKERS_DIR, `${workerId}-result.md`));
|
|
33
|
+
console.log(`${color}[${workerId}]${RESET} ${DIM}started${resultExists ? ' (already done)' : ''}${RESET}`);
|
|
34
|
+
}
|
|
35
|
+
const currentSize = (await stat(filePath)).size;
|
|
36
|
+
const offset = offsets[workerId] ?? 0;
|
|
37
|
+
if (currentSize > offset) {
|
|
38
|
+
const buf = await readFile(filePath);
|
|
39
|
+
const newContent = buf.slice(offset).toString('utf8');
|
|
40
|
+
offsets[workerId] = currentSize;
|
|
41
|
+
const lines = newContent.split('\n');
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
if (line.trim()) {
|
|
44
|
+
process.stdout.write(`${color}[${workerId}]${RESET} ${line}\n`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Check if worker just finished (result file appeared)
|
|
49
|
+
const resultPath = join(WORKERS_DIR, `${workerId}-result.md`);
|
|
50
|
+
const doneKey = `${workerId}-done`;
|
|
51
|
+
if (existsSync(resultPath) && !seen.has(doneKey)) {
|
|
52
|
+
seen.add(doneKey);
|
|
53
|
+
console.log(`${color}[${workerId}]${RESET} ${DIM}✓ result written${RESET}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Exit when all known workers have results
|
|
57
|
+
if (logFiles.length > 0) {
|
|
58
|
+
const allDone = logFiles.every(f => {
|
|
59
|
+
const id = f.replace('.log', '');
|
|
60
|
+
return existsSync(join(WORKERS_DIR, `${id}-result.md`));
|
|
61
|
+
});
|
|
62
|
+
if (allDone) {
|
|
63
|
+
console.log(`\n${DIM}All workers done. Run: loom collect${RESET}`);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
await new Promise(resolve => setTimeout(resolve, POLL_MS));
|
|
68
|
+
}
|
|
69
|
+
}
|