pi-thread-engine 0.4.7 → 0.4.9
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 +30 -53
- package/README.md +221 -214
- package/_lib/contract.ts +116 -0
- package/docs/HELLO_PYTHON.md +68 -0
- package/extensions/index.ts +111 -78
- package/package.json +15 -6
- package/src/core/executor.ts +12 -6
- package/src/core/registry.test.ts +32 -0
- package/src/core/registry.ts +290 -290
- package/src/core/types.ts +107 -107
- package/src/core/worktree.ts +309 -263
- package/src/hello-world.test.ts +30 -0
- package/src/hello-world.ts +25 -0
- package/src/worktree-lifecycle.test.ts +124 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Python `hello_world()` Utility
|
|
2
|
+
|
|
3
|
+
> A lightweight Python entry point for pi-thread-engine — useful for
|
|
4
|
+
> Pi extension scripts that prefer Python over TypeScript.
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
|
|
8
|
+
Mirrors the TypeScript `helloWorld()` function (`src/hello.ts`) as a
|
|
9
|
+
simple verification utility. Calling `hello_world()` returns the
|
|
10
|
+
canonical greeting string `"Hello, World!"` with zero side effects.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
### Import
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from scripts.hello import hello_world
|
|
18
|
+
|
|
19
|
+
greeting = hello_world()
|
|
20
|
+
print(greeting) # Hello, World!
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### CLI invocation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
python scripts/hello.py
|
|
27
|
+
# Hello, World!
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Tests
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
python -m unittest scripts/test_hello.py -v
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Expected output:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
test_returns_hello_world_string (scripts.test_hello.TestHelloWorld.test_returns_hello_world_string)
|
|
40
|
+
hello_world() should return exactly 'Hello, World!'. ... ok
|
|
41
|
+
test_returns_string_type (scripts.test_hello.TestHelloWorld.test_returns_string_type)
|
|
42
|
+
hello_world() should return a str instance. ... ok
|
|
43
|
+
|
|
44
|
+
----------------------------------------------------------------------
|
|
45
|
+
Ran 2 tests in 0.000s
|
|
46
|
+
|
|
47
|
+
OK
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Implementation
|
|
51
|
+
|
|
52
|
+
| Aspect | Detail |
|
|
53
|
+
|--------|--------|
|
|
54
|
+
| **File** | `scripts/hello.py` |
|
|
55
|
+
| **Function** | `hello_world() -> str` |
|
|
56
|
+
| **Return value** | `"Hello, World!"` |
|
|
57
|
+
| **Framework** | Python 3 (stdlib only) |
|
|
58
|
+
| **Test framework** | `unittest` (stdlib) |
|
|
59
|
+
| **Test file** | `scripts/test_hello.py` |
|
|
60
|
+
|
|
61
|
+
## Integration
|
|
62
|
+
|
|
63
|
+
The Python test is integrated into the project's test suite via
|
|
64
|
+
`scripts/test.sh` (step 4). Run the full suite with:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
bash scripts/test.sh
|
|
68
|
+
```
|
package/extensions/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI,
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { Type, type TUnsafe } from "@sinclair/typebox";
|
|
3
3
|
import { Text } from "@mariozechner/pi-tui";
|
|
4
4
|
|
|
@@ -21,6 +21,7 @@ import type { Thread, ThreadType, Story, StoryPhase } from "../src/core/types.js
|
|
|
21
21
|
import { writeFileSync, readFileSync, existsSync, mkdirSync } from "fs";
|
|
22
22
|
import { join, resolve } from "path";
|
|
23
23
|
import { createWorktree, removeWorktree, listWorktrees, pushWorktreeChanges, cleanupAll, isGitRepo, branchName, findRepoRoot } from "../src/core/worktree.js";
|
|
24
|
+
import { helloWorld } from "../src/hello-world.js";
|
|
24
25
|
|
|
25
26
|
// ── Export helper ───────────────────────────────────────────
|
|
26
27
|
function exportThread(id: string, r: ThreadRegistry, cwd: string): string | null {
|
|
@@ -55,7 +56,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
55
56
|
const threads = registry.all().filter((t) => t.state !== "killed");
|
|
56
57
|
const stories = registry.allStories();
|
|
57
58
|
if (threads.length > 0 || stories.length > 0) {
|
|
58
|
-
|
|
59
|
+
try {
|
|
60
|
+
pi.appendEntry("pi-threads-state", { threads, stories, timestamp: Date.now() });
|
|
61
|
+
} catch {
|
|
62
|
+
// Extension contexts can go stale after session replacement; persistence is best-effort.
|
|
63
|
+
}
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
|
|
@@ -81,8 +86,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
81
86
|
|
|
82
87
|
// ── Status bar ───────────────────────────────────────────────
|
|
83
88
|
|
|
89
|
+
let clearStatusHandler: (() => void) | undefined;
|
|
84
90
|
pi.on("session_start", async (_event, ctx) => {
|
|
85
|
-
|
|
91
|
+
clearStatusHandler?.();
|
|
92
|
+
clearStatusHandler = registry.on(() => {
|
|
86
93
|
const running = registry.byState("running");
|
|
87
94
|
const stories = registry.allStories().filter((s) => s.state === "executing" || s.state === "planning");
|
|
88
95
|
const parts: string[] = [];
|
|
@@ -99,7 +106,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
99
106
|
);
|
|
100
107
|
}
|
|
101
108
|
|
|
102
|
-
|
|
109
|
+
try {
|
|
110
|
+
ctx.ui.setStatus("pi-threads", parts.length > 0 ? parts.join(" ") : undefined);
|
|
111
|
+
} catch {
|
|
112
|
+
// Status updates are best-effort; command/session contexts may go stale.
|
|
113
|
+
}
|
|
103
114
|
});
|
|
104
115
|
});
|
|
105
116
|
|
|
@@ -125,6 +136,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
125
136
|
return tasks;
|
|
126
137
|
}
|
|
127
138
|
|
|
139
|
+
|
|
128
140
|
function stateIcon(state: string): string {
|
|
129
141
|
switch (state) {
|
|
130
142
|
case "running": return "⟳";
|
|
@@ -138,16 +150,54 @@ export default function (pi: ExtensionAPI) {
|
|
|
138
150
|
|
|
139
151
|
function typeIcon(type: string): string {
|
|
140
152
|
switch (type) {
|
|
153
|
+
case "base": return "•";
|
|
141
154
|
case "parallel": return "⫘";
|
|
142
155
|
case "chained": return "⟶";
|
|
143
156
|
case "fusion": return "⊕";
|
|
144
157
|
case "meta": return "◎";
|
|
145
158
|
case "long": return "∞";
|
|
146
159
|
case "zero": return "⊘";
|
|
160
|
+
case "worktree": return "🌳";
|
|
161
|
+
case "plan": return "⌁";
|
|
162
|
+
case "scheduled": return "⏰";
|
|
147
163
|
default: return "·";
|
|
148
164
|
}
|
|
149
165
|
}
|
|
150
166
|
|
|
167
|
+
async function openDashboard(ctx: ExtensionCommandContext) {
|
|
168
|
+
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
169
|
+
const dashboard = createDashboard(
|
|
170
|
+
registry,
|
|
171
|
+
theme,
|
|
172
|
+
() => done(),
|
|
173
|
+
(id) => {
|
|
174
|
+
registry.kill(id);
|
|
175
|
+
ctx.ui.notify(`Killed ${id}`, "warning");
|
|
176
|
+
tui.requestRender();
|
|
177
|
+
},
|
|
178
|
+
(id) => {
|
|
179
|
+
const t = registry.get(id);
|
|
180
|
+
if (t) {
|
|
181
|
+
const preview = t.tasks.map((tk) => `${tk.id}: ${tk.result?.slice(0, 100) ?? tk.error ?? "(pending)"}`).join("\n");
|
|
182
|
+
ctx.ui.notify(`Thread ${id} results:\n${preview}`, "info");
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
(id, message) => {
|
|
186
|
+
executor.injectReply(id, message);
|
|
187
|
+
ctx.ui.notify(`Replied to ${id}: ${message.slice(0, 50)}...`, "info");
|
|
188
|
+
tui.requestRender();
|
|
189
|
+
},
|
|
190
|
+
(id) => { const p = exportThread(id, registry, ctx.cwd); if (p) ctx.ui.notify(`Exported to ${p}`, "info"); }
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
render: (w: number) => dashboard.render(w),
|
|
195
|
+
invalidate: () => dashboard.invalidate(),
|
|
196
|
+
handleInput: (data: string) => { dashboard.handleInput(data); tui.requestRender(); },
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
151
201
|
// ── /threads — unified TUI dashboard ─────────────────────────
|
|
152
202
|
|
|
153
203
|
pi.registerCommand("threads", {
|
|
@@ -241,69 +291,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
241
291
|
}
|
|
242
292
|
const ts = new Date().toISOString().slice(0, 19).replace(/[T:]/g, "-");
|
|
243
293
|
const fn = "threads-" + ts + ".md";
|
|
244
|
-
const outPath =
|
|
245
|
-
|
|
294
|
+
const outPath = join(ctx.cwd, fn);
|
|
295
|
+
writeFileSync(outPath, md.join("\n"), "utf8");
|
|
246
296
|
ctx.ui.notify("Exported to " + outPath, "info");
|
|
247
297
|
return;
|
|
248
298
|
}
|
|
249
299
|
|
|
250
|
-
|
|
251
|
-
// ── /agents alias (Claude-style muscle memory) ──────────────
|
|
252
|
-
pi.registerCommand("agents", {
|
|
253
|
-
description: "Alias for /threads — Claude-style Agent View dashboard",
|
|
254
|
-
handler: async (a, c) => {
|
|
255
|
-
// Forward to /threads
|
|
256
|
-
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
257
|
-
const dashboard = createDashboard(
|
|
258
|
-
registry,
|
|
259
|
-
theme,
|
|
260
|
-
() => done(),
|
|
261
|
-
(id) => { registry.kill(id); ctx.ui.notify(`Killed ${id}`, "warning"); tui.requestRender(); },
|
|
262
|
-
(id) => { const t = registry.get(id); if (t) { ctx.ui.notify(`Thread ${id}: ${t.tasks.map((tk) => `${tk.id}: ${tk.result?.slice(0,100) ?? tk.error ?? "(pending)"}`).join("\n")}`, "info"); } },
|
|
263
|
-
(id, message) => { executor.injectReply(id, message); ctx.ui.notify(`Replied to ${id}: ${message.slice(0, 50)}...`, "info"); tui.requestRender(); },
|
|
264
|
-
(id) => { const p = exportThread(id, registry, ctx.cwd); if (p) ctx.ui.notify(`Exported to ${p}`, "info"); }
|
|
265
|
-
);
|
|
266
|
-
return { render: (w: number) => dashboard.render(w), invalidate: () => dashboard.invalidate(), handleInput: (data: string) => { dashboard.handleInput(data); tui.requestRender(); } };
|
|
267
|
-
});
|
|
268
|
-
},
|
|
269
|
-
});
|
|
270
|
-
// ── End /agents alias ───────────────────────────────────────
|
|
271
|
-
|
|
272
|
-
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
273
|
-
const dashboard = createDashboard(
|
|
274
|
-
registry,
|
|
275
|
-
theme,
|
|
276
|
-
() => done(),
|
|
277
|
-
(id) => {
|
|
278
|
-
registry.kill(id);
|
|
279
|
-
ctx.ui.notify(`Killed ${id}`, "warning");
|
|
280
|
-
tui.requestRender();
|
|
281
|
-
},
|
|
282
|
-
(id) => {
|
|
283
|
-
const t = registry.get(id);
|
|
284
|
-
if (t) {
|
|
285
|
-
const preview = t.tasks.map((tk) => `${tk.id}: ${tk.result?.slice(0, 100) ?? tk.error ?? "(pending)"}`).join("\n");
|
|
286
|
-
ctx.ui.notify(`Thread ${id} results:\n${preview}`, "info");
|
|
287
|
-
}
|
|
288
|
-
},
|
|
289
|
-
(id, message) => {
|
|
290
|
-
// Inline reply — send message to blocked thread
|
|
291
|
-
executor.injectReply(id, message);
|
|
292
|
-
ctx.ui.notify(`Replied to ${id}: ${message.slice(0, 50)}...`, "info");
|
|
293
|
-
tui.requestRender();
|
|
294
|
-
},
|
|
295
|
-
(id) => { const p = exportThread(id, registry, ctx.cwd); if (p) ctx.ui.notify(`Exported to ${p}`, "info"); }
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
return {
|
|
299
|
-
render: (w: number) => dashboard.render(w),
|
|
300
|
-
invalidate: () => dashboard.invalidate(),
|
|
301
|
-
handleInput: (data: string) => { dashboard.handleInput(data); tui.requestRender(); },
|
|
302
|
-
};
|
|
303
|
-
});
|
|
300
|
+
await openDashboard(ctx);
|
|
304
301
|
},
|
|
305
302
|
});
|
|
306
303
|
|
|
304
|
+
pi.registerCommand("agents", {
|
|
305
|
+
description: "Alias for /threads — Claude-style Agent View dashboard",
|
|
306
|
+
handler: async (_args, ctx) => openDashboard(ctx),
|
|
307
|
+
});
|
|
308
|
+
|
|
307
309
|
// ── /pthread — parallel via subagent ─────────────────────────
|
|
308
310
|
|
|
309
311
|
pi.registerCommand("pthread", {
|
|
@@ -321,19 +323,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
321
323
|
},
|
|
322
324
|
});
|
|
323
325
|
|
|
324
|
-
// ── /cthread — chained
|
|
326
|
+
// ── /cthread — chained with human checkpoints ─────────────────
|
|
325
327
|
|
|
326
328
|
pi.registerCommand("cthread", {
|
|
327
|
-
description: 'C-Thread: sequential phases
|
|
329
|
+
description: 'C-Thread: sequential phases with human checkpoints. Usage: /cthread "phase 1" "phase 2"',
|
|
328
330
|
handler: async (args, ctx) => {
|
|
329
331
|
if (!args?.trim()) { ctx.ui.notify('Usage: /cthread "phase 1" "phase 2"', "error"); return; }
|
|
330
332
|
const phases = parseTaskArgs(args);
|
|
331
333
|
if (phases.length < 2) { ctx.ui.notify("Need at least 2 phases", "error"); return; }
|
|
332
334
|
|
|
333
|
-
const thread = registry.create("chained", `Chain: ${phases.length} phases`, phases, { cwd: ctx.cwd, backend: "
|
|
335
|
+
const thread = registry.create("chained", `Chain: ${phases.length} phases`, phases, { cwd: ctx.cwd, backend: "native" });
|
|
334
336
|
|
|
335
|
-
ctx.ui.notify(`🧵 C-Thread ${thread.id}: ${phases.length} phases
|
|
336
|
-
executor.dispatch(thread
|
|
337
|
+
ctx.ui.notify(`🧵 C-Thread ${thread.id}: ${phases.length} phases with checkpoints...`, "info");
|
|
338
|
+
executor.dispatch(thread, {
|
|
339
|
+
onCheckpoint: async (phase, task) => ctx.ui.confirm(
|
|
340
|
+
"C-Thread checkpoint",
|
|
341
|
+
`Phase ${phase} complete. Continue to phase ${phase + 1}: ${task.label}?`
|
|
342
|
+
),
|
|
343
|
+
});
|
|
337
344
|
},
|
|
338
345
|
});
|
|
339
346
|
|
|
@@ -472,10 +479,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
472
479
|
// ── /wthread — worktree isolation (port from Grok CLI) ────
|
|
473
480
|
|
|
474
481
|
pi.registerCommand("wthread", {
|
|
475
|
-
description: 'W-Thread: run task in isolated git worktree. Usage: /wthread "task" or /wthread list
|
|
482
|
+
description: 'W-Thread: run task in isolated git worktree. Usage: /wthread "task" or /wthread list, push, discard, cleanup',
|
|
476
483
|
handler: async (args, ctx) => {
|
|
477
484
|
if (!args?.trim()) {
|
|
478
|
-
ctx.ui.notify('Usage: /wthread "task" | /wthread list | /wthread cleanup', "error");
|
|
485
|
+
ctx.ui.notify('Usage: /wthread "task" | /wthread list | /wthread push <id> [message] | /wthread discard <id> | /wthread cleanup', "error");
|
|
479
486
|
return;
|
|
480
487
|
}
|
|
481
488
|
|
|
@@ -501,7 +508,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
501
508
|
return;
|
|
502
509
|
}
|
|
503
510
|
|
|
504
|
-
if (sub === "push"
|
|
511
|
+
if (sub === "push") {
|
|
512
|
+
if (parts.length < 2) { ctx.ui.notify('Usage: /wthread push <id> [message]', "error"); return; }
|
|
505
513
|
const threadId = parts[1];
|
|
506
514
|
const msg = parts.slice(2).join(" ") || undefined;
|
|
507
515
|
const ok = pushWorktreeChanges(ctx.cwd, threadId, msg);
|
|
@@ -509,6 +517,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
509
517
|
return;
|
|
510
518
|
}
|
|
511
519
|
|
|
520
|
+
if (sub === "discard" || sub === "remove") {
|
|
521
|
+
if (parts.length < 2) { ctx.ui.notify('Usage: /wthread discard <id>', "error"); return; }
|
|
522
|
+
const threadId = parts[1];
|
|
523
|
+
const ok = removeWorktree(ctx.cwd, threadId);
|
|
524
|
+
ctx.ui.notify(ok ? `Discarded ${threadId} worktree` : "Discard failed", ok ? "info" : "error");
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
512
528
|
// Default: create worktree and run task
|
|
513
529
|
if (!isGitRepo(ctx.cwd)) { ctx.ui.notify("Not in a git repo — can't create worktree", "error"); return; }
|
|
514
530
|
|
|
@@ -778,6 +794,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
778
794
|
},
|
|
779
795
|
});
|
|
780
796
|
|
|
797
|
+
// ── /hello — Greeting command ───────────────────────────────
|
|
798
|
+
|
|
799
|
+
pi.registerCommand("hello", {
|
|
800
|
+
description: 'Say hello. Usage: /hello [name]',
|
|
801
|
+
handler: async (args, ctx) => {
|
|
802
|
+
const name = args?.trim() || undefined;
|
|
803
|
+
ctx.ui.notify(helloWorld(name), "info");
|
|
804
|
+
},
|
|
805
|
+
});
|
|
806
|
+
|
|
781
807
|
// ── LLM-callable tools ───────────────────────────────────────
|
|
782
808
|
|
|
783
809
|
pi.registerTool({
|
|
@@ -785,18 +811,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
785
811
|
label: "Thread Spawn",
|
|
786
812
|
description: [
|
|
787
813
|
"Spawn a thread. Types:",
|
|
814
|
+
"- base: one prompt -> tool calls -> review (native)",
|
|
788
815
|
"- parallel: N independent tasks in parallel (via subagent)",
|
|
789
|
-
"- chained: sequential phases with checkpoints (
|
|
790
|
-
"- meta: scout
|
|
816
|
+
"- chained: sequential phases with human checkpoints (native)",
|
|
817
|
+
"- meta: scout -> plan -> build -> review pipeline (via subagent)",
|
|
791
818
|
"- fusion: same prompt to N agents/models, compare results (native, UNIQUE)",
|
|
792
819
|
"- zero: autonomous + verification command gate (native, UNIQUE)",
|
|
793
820
|
"- long: extended autonomous run (native)",
|
|
794
|
-
"- worktree: run in isolated git worktree (native,
|
|
795
|
-
"- plan: structured plan
|
|
796
|
-
"- scheduled: recurring scheduled task (native,
|
|
821
|
+
"- worktree: run in isolated git worktree (native, extension beyond article)",
|
|
822
|
+
"- plan: structured plan -> approve -> execute gate (native, extension beyond article)",
|
|
823
|
+
"- scheduled: recurring scheduled task (native, extension beyond article)",
|
|
797
824
|
].join("\n"),
|
|
798
825
|
parameters: Type.Object({
|
|
799
|
-
type: StringEnum(["parallel", "fusion", "chained", "meta", "long", "zero", "worktree", "plan", "scheduled"] as const),
|
|
826
|
+
type: StringEnum(["base", "parallel", "fusion", "chained", "meta", "long", "zero", "worktree", "plan", "scheduled"] as const),
|
|
800
827
|
prompts: Type.Array(Type.String(), { description: "Task prompts" }),
|
|
801
828
|
models: Type.Optional(Type.Array(Type.String(), { description: "Models for fusion (e.g. ['anthropic/claude-sonnet-4', 'google/gemini-2.5-pro'])" })),
|
|
802
829
|
count: Type.Optional(Type.Number({ description: "Agent count for fusion (default 3)" })),
|
|
@@ -817,8 +844,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
817
844
|
taskPrompts = [prompts[0]]; // Meta delegates to subagent chain internally
|
|
818
845
|
}
|
|
819
846
|
|
|
820
|
-
// Auto-select backend
|
|
821
|
-
const
|
|
847
|
+
// Auto-select backend: P/B use subagents; C/F/L/Z and extension types run natively.
|
|
848
|
+
const nativeTypes = new Set(["base", "chained", "fusion", "zero", "long", "worktree", "plan", "scheduled"]);
|
|
849
|
+
const backend: import("../src/core/types.js").ExecutionBackend = (backendOverride as import("../src/core/types.js").ExecutionBackend) ?? (nativeTypes.has(String(type)) ? "native" as const : "subagent" as const);
|
|
822
850
|
|
|
823
851
|
const label = type === "fusion"
|
|
824
852
|
? `Fusion: ${prompts[0]?.slice(0, 40)}`
|
|
@@ -833,7 +861,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
833
861
|
});
|
|
834
862
|
|
|
835
863
|
// Dispatch (async — runs in background)
|
|
836
|
-
executor.dispatch(thread
|
|
864
|
+
executor.dispatch(thread, type === "chained" ? {
|
|
865
|
+
onCheckpoint: async (phase, task) => ctx.ui.confirm(
|
|
866
|
+
"C-Thread checkpoint",
|
|
867
|
+
`Phase ${phase} complete. Continue to phase ${phase + 1}: ${task.label}?`
|
|
868
|
+
),
|
|
869
|
+
} : undefined);
|
|
837
870
|
|
|
838
871
|
const modelInfo = models ? ` Models: ${models.join(", ")}` : "";
|
|
839
872
|
const verifyInfo = verify ? ` Verify: ${verify}` : "";
|
|
@@ -847,7 +880,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
847
880
|
details: { threadId: thread.id, type, taskCount: taskPrompts.length, backend },
|
|
848
881
|
};
|
|
849
882
|
},
|
|
850
|
-
renderCall(args, theme) {
|
|
883
|
+
renderCall(args: any, theme: any) {
|
|
851
884
|
return new Text(
|
|
852
885
|
theme.fg("toolTitle", theme.bold("thread_spawn ")) +
|
|
853
886
|
theme.fg("accent", args.type) +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-thread-engine",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9",
|
|
4
4
|
"description": "Thread-Based Engineering for pi — all 7 thread types + stories + fusion + zero-touch + TUI dashboard. Based on @IndyDevDan framework from agenticengineer.com.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,22 +21,31 @@
|
|
|
21
21
|
"pi": {
|
|
22
22
|
"extensions": [
|
|
23
23
|
"./extensions/index.ts"
|
|
24
|
-
]
|
|
24
|
+
],
|
|
25
|
+
"commands": ["threads","agents","pthread","cthread","bthread","fthread","zthread","lthread","wthread","plan","tloop","story","stories","hello"],
|
|
26
|
+
"tools": ["thread_spawn","thread_status","thread_kill"]
|
|
25
27
|
},
|
|
26
28
|
"files": [
|
|
29
|
+
"_lib/",
|
|
27
30
|
"extensions/",
|
|
28
31
|
"src/",
|
|
29
32
|
"README.md",
|
|
30
33
|
"PLAN.md",
|
|
31
34
|
"docs/"
|
|
32
35
|
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "bash scripts/build.sh",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:watch": "vitest"
|
|
40
|
+
},
|
|
33
41
|
"devDependencies": {
|
|
34
|
-
"typescript": "^6.0.3"
|
|
42
|
+
"typescript": "^6.0.3",
|
|
43
|
+
"vitest": "^4.1.6"
|
|
35
44
|
},
|
|
36
45
|
"peerDependencies": {
|
|
37
|
-
"@
|
|
38
|
-
"@
|
|
39
|
-
"@
|
|
46
|
+
"@earendil-works/pi-ai": ">=0.73.0",
|
|
47
|
+
"@earendil-works/pi-coding-agent": ">=0.73.0",
|
|
48
|
+
"@earendil-works/pi-tui": ">=0.73.0",
|
|
40
49
|
"@sinclair/typebox": "^0.34.48"
|
|
41
50
|
}
|
|
42
51
|
}
|
package/src/core/executor.ts
CHANGED
|
@@ -17,15 +17,22 @@ export class ThreadExecutor {
|
|
|
17
17
|
|
|
18
18
|
// ── Native execution (pi -p) ────────────────────────────────
|
|
19
19
|
|
|
20
|
+
private async execPi(args: string[], opts: { cwd: string; timeout: number }) {
|
|
21
|
+
if (process.platform === "win32") {
|
|
22
|
+
return this.pi.exec("cmd.exe", ["/d", "/s", "/c", "pi", ...args], opts);
|
|
23
|
+
}
|
|
24
|
+
return this.pi.exec("pi", args, opts);
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
private async runTaskNative(thread: Thread, task: ThreadTask): Promise<void> {
|
|
21
28
|
const cwd = thread.config.cwd ?? process.cwd();
|
|
22
29
|
this.registry.startTask(thread.id, task.id);
|
|
23
30
|
|
|
24
31
|
try {
|
|
25
|
-
const args = ["-p", task.prompt];
|
|
32
|
+
const args = ["--no-session", "--no-extensions", "--no-skills", "--no-context-files", "-p", task.prompt];
|
|
26
33
|
if (task.model) args.unshift("-m", task.model);
|
|
27
34
|
|
|
28
|
-
const result = await this.
|
|
35
|
+
const result = await this.execPi(args, {
|
|
29
36
|
cwd,
|
|
30
37
|
timeout: 10 * 60 * 1000,
|
|
31
38
|
});
|
|
@@ -226,7 +233,7 @@ export class ThreadExecutor {
|
|
|
226
233
|
this.registry.startThread(thread.id);
|
|
227
234
|
|
|
228
235
|
try {
|
|
229
|
-
const { createWorktree,
|
|
236
|
+
const { createWorktree, findRepoRoot } = await import("./worktree.js");
|
|
230
237
|
const repoPath = findRepoRoot(cwd);
|
|
231
238
|
|
|
232
239
|
if (!repoPath) {
|
|
@@ -245,7 +252,7 @@ export class ThreadExecutor {
|
|
|
245
252
|
|
|
246
253
|
// Run the task inside the worktree using pi -p
|
|
247
254
|
try {
|
|
248
|
-
const result = await this.
|
|
255
|
+
const result = await this.execPi(["--no-session", "--no-extensions", "--no-skills", "--no-context-files", "-p", task.prompt], {
|
|
249
256
|
cwd: wt.path,
|
|
250
257
|
timeout: 30 * 60 * 1000, // 30 min max
|
|
251
258
|
});
|
|
@@ -267,8 +274,7 @@ export class ThreadExecutor {
|
|
|
267
274
|
this.registry.failTask(thread.id, task.id, err.message ?? String(err));
|
|
268
275
|
}
|
|
269
276
|
|
|
270
|
-
//
|
|
271
|
-
removeWorktree(cwd, thread.id);
|
|
277
|
+
// Preserve the worktree/branch for explicit /wthread push, discard, or cleanup.
|
|
272
278
|
} catch (err: any) {
|
|
273
279
|
this.registry.failTask(thread.id, thread.tasks[0].id, err.message ?? String(err));
|
|
274
280
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { ThreadRegistry } from "./registry.js";
|
|
3
|
+
|
|
4
|
+
describe("ThreadRegistry kill semantics", () => {
|
|
5
|
+
it("does not resurrect killed threads when async work completes late", () => {
|
|
6
|
+
const registry = new ThreadRegistry();
|
|
7
|
+
const thread = registry.create("base", "late finish", ["prompt"], { backend: "native" });
|
|
8
|
+
|
|
9
|
+
registry.startThread(thread.id);
|
|
10
|
+
registry.startTask(thread.id, thread.tasks[0].id);
|
|
11
|
+
registry.kill(thread.id);
|
|
12
|
+
registry.completeTask(thread.id, thread.tasks[0].id, "late result");
|
|
13
|
+
|
|
14
|
+
expect(thread.state).toBe("killed");
|
|
15
|
+
expect(thread.tasks[0].state).toBe("killed");
|
|
16
|
+
expect(thread.tasks[0].result).toBeUndefined();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("does not resurrect killed threads when async work fails late", () => {
|
|
20
|
+
const registry = new ThreadRegistry();
|
|
21
|
+
const thread = registry.create("base", "late failure", ["prompt"], { backend: "native" });
|
|
22
|
+
|
|
23
|
+
registry.startThread(thread.id);
|
|
24
|
+
registry.startTask(thread.id, thread.tasks[0].id);
|
|
25
|
+
registry.kill(thread.id);
|
|
26
|
+
registry.failTask(thread.id, thread.tasks[0].id, "late error");
|
|
27
|
+
|
|
28
|
+
expect(thread.state).toBe("killed");
|
|
29
|
+
expect(thread.tasks[0].state).toBe("killed");
|
|
30
|
+
expect(thread.tasks[0].error).toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
});
|