pi-thread-engine 0.4.6 → 0.4.7
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/PLAN.md +53 -33
- package/extensions/index.ts +228 -3
- package/package.json +1 -1
- package/src/core/executor.ts +278 -220
- package/src/core/types.ts +1 -1
- package/src/core/worktree.ts +263 -0
- package/src/dashboard.ts +426 -421
package/src/core/executor.ts
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import type { ThreadRegistry } from "./registry.js";
|
|
3
|
-
import type { Thread, ThreadTask } from "./types.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Executor — runs thread tasks.
|
|
7
|
-
*
|
|
8
|
-
* Two backends:
|
|
9
|
-
* - "subagent": delegates to pi-subagents via sendUserMessage (inherits agent specialization, TUI, artifacts)
|
|
10
|
-
* - "native": spawns pi -p directly (for fusion/zero where we need raw control)
|
|
11
|
-
*/
|
|
12
|
-
export class ThreadExecutor {
|
|
13
|
-
constructor(
|
|
14
|
-
private pi: ExtensionAPI,
|
|
15
|
-
private registry: ThreadRegistry
|
|
16
|
-
) {}
|
|
17
|
-
|
|
18
|
-
// ── Native execution (pi -p) ────────────────────────────────
|
|
19
|
-
|
|
20
|
-
private async runTaskNative(thread: Thread, task: ThreadTask): Promise<void> {
|
|
21
|
-
const cwd = thread.config.cwd ?? process.cwd();
|
|
22
|
-
this.registry.startTask(thread.id, task.id);
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
const args = ["-p", task.prompt];
|
|
26
|
-
if (task.model) args.unshift("-m", task.model);
|
|
27
|
-
|
|
28
|
-
const result = await this.pi.exec("pi", args, {
|
|
29
|
-
cwd,
|
|
30
|
-
timeout: 10 * 60 * 1000,
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
if (result.code === 0) {
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { ThreadRegistry } from "./registry.js";
|
|
3
|
+
import type { Thread, ThreadTask } from "./types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Executor — runs thread tasks.
|
|
7
|
+
*
|
|
8
|
+
* Two backends:
|
|
9
|
+
* - "subagent": delegates to pi-subagents via sendUserMessage (inherits agent specialization, TUI, artifacts)
|
|
10
|
+
* - "native": spawns pi -p directly (for fusion/zero where we need raw control)
|
|
11
|
+
*/
|
|
12
|
+
export class ThreadExecutor {
|
|
13
|
+
constructor(
|
|
14
|
+
private pi: ExtensionAPI,
|
|
15
|
+
private registry: ThreadRegistry
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
// ── Native execution (pi -p) ────────────────────────────────
|
|
19
|
+
|
|
20
|
+
private async runTaskNative(thread: Thread, task: ThreadTask): Promise<void> {
|
|
21
|
+
const cwd = thread.config.cwd ?? process.cwd();
|
|
22
|
+
this.registry.startTask(thread.id, task.id);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const args = ["-p", task.prompt];
|
|
26
|
+
if (task.model) args.unshift("-m", task.model);
|
|
27
|
+
|
|
28
|
+
const result = await this.pi.exec("pi", args, {
|
|
29
|
+
cwd,
|
|
30
|
+
timeout: 10 * 60 * 1000,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (result.code === 0) {
|
|
34
34
|
this.registry.completeTask(thread.id, task.id, result.stdout);
|
|
35
35
|
// Parse --mode json output for token/cost data
|
|
36
36
|
try {
|
|
@@ -49,190 +49,248 @@ export class ThreadExecutor {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
} catch {}
|
|
53
|
-
} else {
|
|
54
|
-
this.registry.failTask(thread.id, task.id, result.stderr || `Exit code: ${result.code}`);
|
|
55
|
-
}
|
|
56
|
-
} catch (err: any) {
|
|
57
|
-
this.registry.failTask(thread.id, task.id, err.message ?? String(err));
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ── Subagent execution (via sendUserMessage) ────────────────
|
|
62
|
-
|
|
63
|
-
private launchSubagentParallel(thread: Thread): void {
|
|
64
|
-
const agent = thread.config.agent ?? "worker";
|
|
65
|
-
const tasks = thread.tasks.map((t) => ({
|
|
66
|
-
agent,
|
|
67
|
-
task: t.prompt,
|
|
68
|
-
...(t.model ? { model: t.model } : {}),
|
|
69
|
-
}));
|
|
70
|
-
|
|
71
|
-
// Send as a user message that pi-subagents will pick up
|
|
72
|
-
this.pi.sendUserMessage(
|
|
73
|
-
`Run these tasks in parallel using subagent:\n\`\`\`json\n${JSON.stringify({ tasks }, null, 2)}\n\`\`\``,
|
|
74
|
-
{ deliverAs: "followUp" }
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
// Mark all tasks as running (subagent handles the rest)
|
|
78
|
-
this.registry.startThread(thread.id);
|
|
79
|
-
for (const task of thread.tasks) {
|
|
80
|
-
this.registry.startTask(thread.id, task.id);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
private launchSubagentChain(thread: Thread): void {
|
|
85
|
-
const agent = thread.config.agent ?? "worker";
|
|
86
|
-
const chain = thread.tasks.map((t, i) => ({
|
|
87
|
-
agent,
|
|
88
|
-
task: i === 0 ? t.prompt : `Continue: ${t.prompt}. Previous context: {previous}`,
|
|
89
|
-
...(t.model ? { model: t.model } : {}),
|
|
90
|
-
}));
|
|
91
|
-
|
|
92
|
-
this.pi.sendUserMessage(
|
|
93
|
-
`Run this chain using subagent:\n\`\`\`json\n${JSON.stringify({ chain }, null, 2)}\n\`\`\``,
|
|
94
|
-
{ deliverAs: "followUp" }
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
this.registry.startThread(thread.id);
|
|
98
|
-
this.registry.startTask(thread.id, thread.tasks[0].id);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
private launchSubagentMeta(thread: Thread): void {
|
|
102
|
-
const chain = [
|
|
103
|
-
{ agent: "scout", task: thread.tasks[0]?.prompt ?? "Scout the codebase" },
|
|
104
|
-
{ agent: "planner", task: "{previous}" },
|
|
105
|
-
{ agent: "worker", task: "{previous}" },
|
|
106
|
-
{ agent: "reviewer", task: "{previous}" },
|
|
107
|
-
];
|
|
108
|
-
|
|
109
|
-
this.pi.sendUserMessage(
|
|
110
|
-
`Run this meta pipeline using subagent:\n\`\`\`json\n${JSON.stringify({ chain }, null, 2)}\n\`\`\``,
|
|
111
|
-
{ deliverAs: "followUp" }
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
this.registry.startThread(thread.id);
|
|
115
|
-
this.registry.startTask(thread.id, thread.tasks[0].id);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ── Fusion (native, multi-model) ────────────────────────────
|
|
119
|
-
|
|
120
|
-
async execFusion(thread: Thread): Promise<void> {
|
|
121
|
-
this.registry.startThread(thread.id);
|
|
122
|
-
// All tasks run in parallel with potentially different models
|
|
123
|
-
await Promise.allSettled(thread.tasks.map((task) => this.runTaskNative(thread, task)));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ── Zero-touch (native + verification) ──────────────────────
|
|
127
|
-
|
|
128
|
-
async execZero(thread: Thread): Promise<void> {
|
|
129
|
-
this.registry.startThread(thread.id);
|
|
130
|
-
const task = thread.tasks[0];
|
|
131
|
-
await this.runTaskNative(thread, task);
|
|
132
|
-
|
|
133
|
-
// If task succeeded and we have a verify command, run it
|
|
134
|
-
if (task.state === "completed" && thread.config.verifyCommand) {
|
|
135
|
-
const cwd = thread.config.cwd ?? process.cwd();
|
|
136
|
-
try {
|
|
137
|
-
const verify = await this.pi.exec("bash", ["-c", thread.config.verifyCommand], {
|
|
138
|
-
cwd,
|
|
139
|
-
timeout: 5 * 60 * 1000,
|
|
140
|
-
});
|
|
141
|
-
if (verify.code !== 0) {
|
|
142
|
-
// Override the completed state to failed
|
|
143
|
-
task.state = "failed";
|
|
144
|
-
task.error = `Verification failed (${thread.config.verifyCommand}): ${verify.stderr || verify.stdout}`;
|
|
145
|
-
thread.state = "failed";
|
|
146
|
-
thread.completedAt = Date.now();
|
|
147
|
-
thread.duration = thread.completedAt - (thread.startedAt ?? thread.createdAt);
|
|
148
|
-
}
|
|
149
|
-
} catch (err: any) {
|
|
150
|
-
task.state = "failed";
|
|
151
|
-
task.error = `Verification error: ${err.message}`;
|
|
152
|
-
thread.state = "failed";
|
|
153
|
-
thread.completedAt = Date.now();
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// ── Dispatch ─────────────────────────────────────────────────
|
|
159
|
-
|
|
160
|
-
async dispatch(
|
|
161
|
-
thread: Thread,
|
|
162
|
-
opts?: { onCheckpoint?: (phase: number, task: ThreadTask) => Promise<boolean> }
|
|
163
|
-
): Promise<void> {
|
|
164
|
-
const backend = thread.config.backend;
|
|
165
|
-
|
|
166
|
-
if (backend === "subagent") {
|
|
167
|
-
switch (thread.type) {
|
|
168
|
-
case "parallel":
|
|
169
|
-
return this.launchSubagentParallel(thread);
|
|
170
|
-
case "chained":
|
|
171
|
-
return this.launchSubagentChain(thread);
|
|
172
|
-
case "meta":
|
|
173
|
-
return this.launchSubagentMeta(thread);
|
|
174
|
-
default:
|
|
175
|
-
// Fall through to native for unsupported subagent types
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Native execution
|
|
181
|
-
switch (thread.type) {
|
|
182
|
-
case "base":
|
|
183
|
-
case "long":
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
this.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return
|
|
192
|
-
case "
|
|
193
|
-
return this.
|
|
194
|
-
case "
|
|
195
|
-
this.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
52
|
+
} catch {}
|
|
53
|
+
} else {
|
|
54
|
+
this.registry.failTask(thread.id, task.id, result.stderr || `Exit code: ${result.code}`);
|
|
55
|
+
}
|
|
56
|
+
} catch (err: any) {
|
|
57
|
+
this.registry.failTask(thread.id, task.id, err.message ?? String(err));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Subagent execution (via sendUserMessage) ────────────────
|
|
62
|
+
|
|
63
|
+
private launchSubagentParallel(thread: Thread): void {
|
|
64
|
+
const agent = thread.config.agent ?? "worker";
|
|
65
|
+
const tasks = thread.tasks.map((t) => ({
|
|
66
|
+
agent,
|
|
67
|
+
task: t.prompt,
|
|
68
|
+
...(t.model ? { model: t.model } : {}),
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
// Send as a user message that pi-subagents will pick up
|
|
72
|
+
this.pi.sendUserMessage(
|
|
73
|
+
`Run these tasks in parallel using subagent:\n\`\`\`json\n${JSON.stringify({ tasks }, null, 2)}\n\`\`\``,
|
|
74
|
+
{ deliverAs: "followUp" }
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Mark all tasks as running (subagent handles the rest)
|
|
78
|
+
this.registry.startThread(thread.id);
|
|
79
|
+
for (const task of thread.tasks) {
|
|
80
|
+
this.registry.startTask(thread.id, task.id);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private launchSubagentChain(thread: Thread): void {
|
|
85
|
+
const agent = thread.config.agent ?? "worker";
|
|
86
|
+
const chain = thread.tasks.map((t, i) => ({
|
|
87
|
+
agent,
|
|
88
|
+
task: i === 0 ? t.prompt : `Continue: ${t.prompt}. Previous context: {previous}`,
|
|
89
|
+
...(t.model ? { model: t.model } : {}),
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
this.pi.sendUserMessage(
|
|
93
|
+
`Run this chain using subagent:\n\`\`\`json\n${JSON.stringify({ chain }, null, 2)}\n\`\`\``,
|
|
94
|
+
{ deliverAs: "followUp" }
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
this.registry.startThread(thread.id);
|
|
98
|
+
this.registry.startTask(thread.id, thread.tasks[0].id);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private launchSubagentMeta(thread: Thread): void {
|
|
102
|
+
const chain = [
|
|
103
|
+
{ agent: "scout", task: thread.tasks[0]?.prompt ?? "Scout the codebase" },
|
|
104
|
+
{ agent: "planner", task: "{previous}" },
|
|
105
|
+
{ agent: "worker", task: "{previous}" },
|
|
106
|
+
{ agent: "reviewer", task: "{previous}" },
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
this.pi.sendUserMessage(
|
|
110
|
+
`Run this meta pipeline using subagent:\n\`\`\`json\n${JSON.stringify({ chain }, null, 2)}\n\`\`\``,
|
|
111
|
+
{ deliverAs: "followUp" }
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
this.registry.startThread(thread.id);
|
|
115
|
+
this.registry.startTask(thread.id, thread.tasks[0].id);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Fusion (native, multi-model) ────────────────────────────
|
|
119
|
+
|
|
120
|
+
async execFusion(thread: Thread): Promise<void> {
|
|
121
|
+
this.registry.startThread(thread.id);
|
|
122
|
+
// All tasks run in parallel with potentially different models
|
|
123
|
+
await Promise.allSettled(thread.tasks.map((task) => this.runTaskNative(thread, task)));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Zero-touch (native + verification) ──────────────────────
|
|
127
|
+
|
|
128
|
+
async execZero(thread: Thread): Promise<void> {
|
|
129
|
+
this.registry.startThread(thread.id);
|
|
130
|
+
const task = thread.tasks[0];
|
|
131
|
+
await this.runTaskNative(thread, task);
|
|
132
|
+
|
|
133
|
+
// If task succeeded and we have a verify command, run it
|
|
134
|
+
if (task.state === "completed" && thread.config.verifyCommand) {
|
|
135
|
+
const cwd = thread.config.cwd ?? process.cwd();
|
|
136
|
+
try {
|
|
137
|
+
const verify = await this.pi.exec("bash", ["-c", thread.config.verifyCommand], {
|
|
138
|
+
cwd,
|
|
139
|
+
timeout: 5 * 60 * 1000,
|
|
140
|
+
});
|
|
141
|
+
if (verify.code !== 0) {
|
|
142
|
+
// Override the completed state to failed
|
|
143
|
+
task.state = "failed";
|
|
144
|
+
task.error = `Verification failed (${thread.config.verifyCommand}): ${verify.stderr || verify.stdout}`;
|
|
145
|
+
thread.state = "failed";
|
|
146
|
+
thread.completedAt = Date.now();
|
|
147
|
+
thread.duration = thread.completedAt - (thread.startedAt ?? thread.createdAt);
|
|
148
|
+
}
|
|
149
|
+
} catch (err: any) {
|
|
150
|
+
task.state = "failed";
|
|
151
|
+
task.error = `Verification error: ${err.message}`;
|
|
152
|
+
thread.state = "failed";
|
|
153
|
+
thread.completedAt = Date.now();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Dispatch ─────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
async dispatch(
|
|
161
|
+
thread: Thread,
|
|
162
|
+
opts?: { onCheckpoint?: (phase: number, task: ThreadTask) => Promise<boolean> }
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
const backend = thread.config.backend;
|
|
165
|
+
|
|
166
|
+
if (backend === "subagent") {
|
|
167
|
+
switch (thread.type) {
|
|
168
|
+
case "parallel":
|
|
169
|
+
return this.launchSubagentParallel(thread);
|
|
170
|
+
case "chained":
|
|
171
|
+
return this.launchSubagentChain(thread);
|
|
172
|
+
case "meta":
|
|
173
|
+
return this.launchSubagentMeta(thread);
|
|
174
|
+
default:
|
|
175
|
+
// Fall through to native for unsupported subagent types
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Native execution
|
|
181
|
+
switch (thread.type) {
|
|
182
|
+
case "base":
|
|
183
|
+
case "long":
|
|
184
|
+
case "plan":
|
|
185
|
+
case "scheduled":
|
|
186
|
+
this.registry.startThread(thread.id);
|
|
187
|
+
return this.runTaskNative(thread, thread.tasks[0]);
|
|
188
|
+
case "parallel":
|
|
189
|
+
this.registry.startThread(thread.id);
|
|
190
|
+
await Promise.allSettled(thread.tasks.map((t) => this.runTaskNative(thread, t)));
|
|
191
|
+
return;
|
|
192
|
+
case "fusion":
|
|
193
|
+
return this.execFusion(thread);
|
|
194
|
+
case "zero":
|
|
195
|
+
return this.execZero(thread);
|
|
196
|
+
case "worktree":
|
|
197
|
+
return this.execWorktree(thread);
|
|
198
|
+
case "chained": {
|
|
199
|
+
this.registry.startThread(thread.id);
|
|
200
|
+
for (let i = 0; i < thread.tasks.length; i++) {
|
|
201
|
+
if (i > 0 && opts?.onCheckpoint) {
|
|
202
|
+
const proceed = await opts.onCheckpoint(i, thread.tasks[i]);
|
|
203
|
+
if (!proceed) {
|
|
204
|
+
this.registry.kill(thread.id);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
await this.runTaskNative(thread, thread.tasks[i]);
|
|
209
|
+
if (thread.tasks[i].state === "failed") return;
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
case "meta":
|
|
214
|
+
this.registry.startThread(thread.id);
|
|
215
|
+
for (const task of thread.tasks) {
|
|
216
|
+
await this.runTaskNative(thread, task);
|
|
217
|
+
if (task.state === "failed") return;
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Execute a thread in an isolated git worktree */
|
|
224
|
+
private async execWorktree(thread: Thread): Promise<void> {
|
|
225
|
+
const cwd = thread.config.cwd ?? process.cwd();
|
|
226
|
+
this.registry.startThread(thread.id);
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const { createWorktree, removeWorktree, findRepoRoot, pushWorktreeChanges } = await import("./worktree.js");
|
|
230
|
+
const repoPath = findRepoRoot(cwd);
|
|
231
|
+
|
|
232
|
+
if (!repoPath) {
|
|
233
|
+
this.registry.failTask(thread.id, thread.tasks[0].id, "Not in a git repository");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Create worktree
|
|
238
|
+
const wt = createWorktree(cwd, thread.id);
|
|
239
|
+
const task = thread.tasks[0];
|
|
240
|
+
|
|
241
|
+
// Write task prompt to worktree
|
|
242
|
+
const { writeFileSync } = await import("fs");
|
|
243
|
+
const { join } = await import("path");
|
|
244
|
+
writeFileSync(join(wt.path, ".pi-thread-task.md"), task.prompt, "utf8");
|
|
245
|
+
|
|
246
|
+
// Run the task inside the worktree using pi -p
|
|
247
|
+
try {
|
|
248
|
+
const result = await this.pi.exec("pi", ["-p", task.prompt], {
|
|
249
|
+
cwd: wt.path,
|
|
250
|
+
timeout: 30 * 60 * 1000, // 30 min max
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
if (result.code === 0) {
|
|
254
|
+
// Write result marker
|
|
255
|
+
writeFileSync(join(wt.path, ".pi-thread-result.json"), JSON.stringify({
|
|
256
|
+
threadId: thread.id,
|
|
257
|
+
status: "completed",
|
|
258
|
+
output: result.stdout,
|
|
259
|
+
timestamp: Date.now(),
|
|
260
|
+
}, null, 2), "utf8");
|
|
261
|
+
|
|
262
|
+
this.registry.completeTask(thread.id, task.id, result.stdout);
|
|
263
|
+
} else {
|
|
264
|
+
this.registry.failTask(thread.id, task.id, result.stderr || `Exit code: ${result.code}`);
|
|
265
|
+
}
|
|
266
|
+
} catch (err: any) {
|
|
267
|
+
this.registry.failTask(thread.id, task.id, err.message ?? String(err));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Always clean up worktree after execution
|
|
271
|
+
removeWorktree(cwd, thread.id);
|
|
272
|
+
} catch (err: any) {
|
|
273
|
+
this.registry.failTask(thread.id, thread.tasks[0].id, err.message ?? String(err));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Inject a reply into a running thread — sends message to its session */
|
|
278
|
+
injectReply(threadId: string, message: string): void {
|
|
279
|
+
const thread = this.registry.get(threadId);
|
|
280
|
+
if (!thread) return;
|
|
281
|
+
|
|
282
|
+
// Mark the blocked task as no longer needing input
|
|
283
|
+
for (const task of thread.tasks) {
|
|
284
|
+
if (task.state === "needs_input") {
|
|
285
|
+
task.state = "running";
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Forward the reply to the running subagent session via sendUserMessage
|
|
290
|
+
// The subagent will pick up the new context and continue
|
|
291
|
+
this.pi.sendUserMessage(
|
|
292
|
+
`[Thread ${threadId}] Reply to blocked thread: ${message}`,
|
|
293
|
+
{ deliverAs: "followUp" }
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
package/src/core/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Thread types from the thread engineering framework */
|
|
2
|
-
export type ThreadType = "base" | "parallel" | "chained" | "fusion" | "meta" | "long" | "zero";
|
|
2
|
+
export type ThreadType = "base" | "parallel" | "chained" | "fusion" | "meta" | "long" | "zero" | "worktree" | "plan" | "scheduled" | "monitor";
|
|
3
3
|
|
|
4
4
|
/** Thread lifecycle states */
|
|
5
5
|
export type ThreadState = "pending" | "running" | "paused" | "completed" | "failed" | "killed" | "needs_input";
|