pi-thread-engine 0.4.6 → 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.
@@ -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
+ ```
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
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
 
@@ -18,8 +18,10 @@ import { ThreadRegistry, formatElapsed } from "../src/core/registry.js";
18
18
  import { ThreadExecutor } from "../src/core/executor.js";
19
19
  import { createDashboard } from "../src/dashboard.js";
20
20
  import type { Thread, ThreadType, Story, StoryPhase } from "../src/core/types.js";
21
- import { writeFileSync } from "fs";
22
- import { join } from "path";
21
+ import { writeFileSync, readFileSync, existsSync, mkdirSync } from "fs";
22
+ import { join, resolve } from "path";
23
+ import { createWorktree, removeWorktree, listWorktrees, pushWorktreeChanges, cleanupAll, isGitRepo, branchName, findRepoRoot } from "../src/core/worktree.js";
24
+ import { helloWorld } from "../src/hello-world.js";
23
25
 
24
26
  // ── Export helper ───────────────────────────────────────────
25
27
  function exportThread(id: string, r: ThreadRegistry, cwd: string): string | null {
@@ -54,7 +56,11 @@ export default function (pi: ExtensionAPI) {
54
56
  const threads = registry.all().filter((t) => t.state !== "killed");
55
57
  const stories = registry.allStories();
56
58
  if (threads.length > 0 || stories.length > 0) {
57
- pi.appendEntry("pi-threads-state", { threads, stories, timestamp: Date.now() });
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
+ }
58
64
  }
59
65
  }
60
66
 
@@ -80,8 +86,10 @@ export default function (pi: ExtensionAPI) {
80
86
 
81
87
  // ── Status bar ───────────────────────────────────────────────
82
88
 
89
+ let clearStatusHandler: (() => void) | undefined;
83
90
  pi.on("session_start", async (_event, ctx) => {
84
- registry.on(() => {
91
+ clearStatusHandler?.();
92
+ clearStatusHandler = registry.on(() => {
85
93
  const running = registry.byState("running");
86
94
  const stories = registry.allStories().filter((s) => s.state === "executing" || s.state === "planning");
87
95
  const parts: string[] = [];
@@ -98,7 +106,11 @@ export default function (pi: ExtensionAPI) {
98
106
  );
99
107
  }
100
108
 
101
- ctx.ui.setStatus("pi-threads", parts.length > 0 ? parts.join(" ") : undefined);
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
+ }
102
114
  });
103
115
  });
104
116
 
@@ -124,6 +136,7 @@ export default function (pi: ExtensionAPI) {
124
136
  return tasks;
125
137
  }
126
138
 
139
+
127
140
  function stateIcon(state: string): string {
128
141
  switch (state) {
129
142
  case "running": return "⟳";
@@ -137,16 +150,54 @@ export default function (pi: ExtensionAPI) {
137
150
 
138
151
  function typeIcon(type: string): string {
139
152
  switch (type) {
153
+ case "base": return "•";
140
154
  case "parallel": return "⫘";
141
155
  case "chained": return "⟶";
142
156
  case "fusion": return "⊕";
143
157
  case "meta": return "◎";
144
158
  case "long": return "∞";
145
159
  case "zero": return "⊘";
160
+ case "worktree": return "🌳";
161
+ case "plan": return "⌁";
162
+ case "scheduled": return "⏰";
146
163
  default: return "·";
147
164
  }
148
165
  }
149
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
+
150
201
  // ── /threads — unified TUI dashboard ─────────────────────────
151
202
 
152
203
  pi.registerCommand("threads", {
@@ -240,69 +291,21 @@ export default function (pi: ExtensionAPI) {
240
291
  }
241
292
  const ts = new Date().toISOString().slice(0, 19).replace(/[T:]/g, "-");
242
293
  const fn = "threads-" + ts + ".md";
243
- const outPath = require("path").join(ctx.cwd, fn);
244
- require("fs").writeFileSync(outPath, md.join("\n"), "utf8");
294
+ const outPath = join(ctx.cwd, fn);
295
+ writeFileSync(outPath, md.join("\n"), "utf8");
245
296
  ctx.ui.notify("Exported to " + outPath, "info");
246
297
  return;
247
298
  }
248
299
 
249
- // Default: open interactive TUI dashboard
250
- // ── /agents alias (Claude-style muscle memory) ──────────────
251
- pi.registerCommand("agents", {
252
- description: "Alias for /threads — Claude-style Agent View dashboard",
253
- handler: async (a, c) => {
254
- // Forward to /threads
255
- await ctx.ui.custom<void>((tui, theme, _kb, done) => {
256
- const dashboard = createDashboard(
257
- registry,
258
- theme,
259
- () => done(),
260
- (id) => { registry.kill(id); ctx.ui.notify(`Killed ${id}`, "warning"); tui.requestRender(); },
261
- (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"); } },
262
- (id, message) => { executor.injectReply(id, message); ctx.ui.notify(`Replied to ${id}: ${message.slice(0, 50)}...`, "info"); tui.requestRender(); },
263
- (id) => { const p = exportThread(id, registry, ctx.cwd); if (p) ctx.ui.notify(`Exported to ${p}`, "info"); }
264
- );
265
- return { render: (w: number) => dashboard.render(w), invalidate: () => dashboard.invalidate(), handleInput: (data: string) => { dashboard.handleInput(data); tui.requestRender(); } };
266
- });
267
- },
268
- });
269
- // ── End /agents alias ───────────────────────────────────────
270
-
271
- await ctx.ui.custom<void>((tui, theme, _kb, done) => {
272
- const dashboard = createDashboard(
273
- registry,
274
- theme,
275
- () => done(),
276
- (id) => {
277
- registry.kill(id);
278
- ctx.ui.notify(`Killed ${id}`, "warning");
279
- tui.requestRender();
280
- },
281
- (id) => {
282
- const t = registry.get(id);
283
- if (t) {
284
- const preview = t.tasks.map((tk) => `${tk.id}: ${tk.result?.slice(0, 100) ?? tk.error ?? "(pending)"}`).join("\n");
285
- ctx.ui.notify(`Thread ${id} results:\n${preview}`, "info");
286
- }
287
- },
288
- (id, message) => {
289
- // Inline reply — send message to blocked thread
290
- executor.injectReply(id, message);
291
- ctx.ui.notify(`Replied to ${id}: ${message.slice(0, 50)}...`, "info");
292
- tui.requestRender();
293
- },
294
- (id) => { const p = exportThread(id, registry, ctx.cwd); if (p) ctx.ui.notify(`Exported to ${p}`, "info"); }
295
- );
296
-
297
- return {
298
- render: (w: number) => dashboard.render(w),
299
- invalidate: () => dashboard.invalidate(),
300
- handleInput: (data: string) => { dashboard.handleInput(data); tui.requestRender(); },
301
- };
302
- });
300
+ await openDashboard(ctx);
303
301
  },
304
302
  });
305
303
 
304
+ pi.registerCommand("agents", {
305
+ description: "Alias for /threads — Claude-style Agent View dashboard",
306
+ handler: async (_args, ctx) => openDashboard(ctx),
307
+ });
308
+
306
309
  // ── /pthread — parallel via subagent ─────────────────────────
307
310
 
308
311
  pi.registerCommand("pthread", {
@@ -320,19 +323,24 @@ export default function (pi: ExtensionAPI) {
320
323
  },
321
324
  });
322
325
 
323
- // ── /cthread — chained via subagent ──────────────────────────
326
+ // ── /cthread — chained with human checkpoints ─────────────────
324
327
 
325
328
  pi.registerCommand("cthread", {
326
- description: 'C-Thread: sequential phases via subagent chain. Usage: /cthread "phase 1" "phase 2"',
329
+ description: 'C-Thread: sequential phases with human checkpoints. Usage: /cthread "phase 1" "phase 2"',
327
330
  handler: async (args, ctx) => {
328
331
  if (!args?.trim()) { ctx.ui.notify('Usage: /cthread "phase 1" "phase 2"', "error"); return; }
329
332
  const phases = parseTaskArgs(args);
330
333
  if (phases.length < 2) { ctx.ui.notify("Need at least 2 phases", "error"); return; }
331
334
 
332
- const thread = registry.create("chained", `Chain: ${phases.length} phases`, phases, { cwd: ctx.cwd, backend: "subagent" });
335
+ const thread = registry.create("chained", `Chain: ${phases.length} phases`, phases, { cwd: ctx.cwd, backend: "native" });
333
336
 
334
- ctx.ui.notify(`🧵 C-Thread ${thread.id}: ${phases.length} phases via subagent chain...`, "info");
335
- 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
+ });
336
344
  },
337
345
  });
338
346
 
@@ -468,6 +476,236 @@ export default function (pi: ExtensionAPI) {
468
476
  },
469
477
  });
470
478
 
479
+ // ── /wthread — worktree isolation (port from Grok CLI) ────
480
+
481
+ pi.registerCommand("wthread", {
482
+ description: 'W-Thread: run task in isolated git worktree. Usage: /wthread "task" or /wthread list, push, discard, cleanup',
483
+ handler: async (args, ctx) => {
484
+ if (!args?.trim()) {
485
+ ctx.ui.notify('Usage: /wthread "task" | /wthread list | /wthread push <id> [message] | /wthread discard <id> | /wthread cleanup', "error");
486
+ return;
487
+ }
488
+
489
+ // Subcommands
490
+ const parts = args.trim().split(/\s+/);
491
+ const sub = parts[0].toLowerCase();
492
+
493
+ if (sub === "list") {
494
+ if (!isGitRepo(ctx.cwd)) { ctx.ui.notify("Not in a git repo", "error"); return; }
495
+ const wts = listWorktrees(ctx.cwd);
496
+ if (wts.length === 0) { ctx.ui.notify("No active worktrees", "info"); return; }
497
+ const lines = wts.map((w) =>
498
+ ` ${w.threadId} → ${w.branch} @ ${w.path} ${w.dirty ? "⚠ dirty" : ""} (↑${w.ahead} ↓${w.behind})`
499
+ );
500
+ ctx.ui.notify(`Worktrees (${wts.length}):\n${lines.join("\n")}`, "info");
501
+ return;
502
+ }
503
+
504
+ if (sub === "cleanup") {
505
+ if (!isGitRepo(ctx.cwd)) { ctx.ui.notify("Not in a git repo", "error"); return; }
506
+ const result = cleanupAll(ctx.cwd);
507
+ ctx.ui.notify(`Cleaned: ${result.removed} worktrees (${result.failed} failed)`, result.failed > 0 ? "warning" : "info");
508
+ return;
509
+ }
510
+
511
+ if (sub === "push") {
512
+ if (parts.length < 2) { ctx.ui.notify('Usage: /wthread push <id> [message]', "error"); return; }
513
+ const threadId = parts[1];
514
+ const msg = parts.slice(2).join(" ") || undefined;
515
+ const ok = pushWorktreeChanges(ctx.cwd, threadId, msg);
516
+ ctx.ui.notify(ok ? `Pushed ${threadId} changes` : "Push failed", ok ? "info" : "error");
517
+ return;
518
+ }
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
+
528
+ // Default: create worktree and run task
529
+ if (!isGitRepo(ctx.cwd)) { ctx.ui.notify("Not in a git repo — can't create worktree", "error"); return; }
530
+
531
+ const task = args.trim();
532
+ const thread = registry.create("worktree", `W: ${task.slice(0, 40)}`, [task], {
533
+ cwd: ctx.cwd,
534
+ backend: "native",
535
+ });
536
+
537
+ ctx.ui.notify(`🌳 W-Thread ${thread.id}: Creating worktree for "${task.slice(0, 50)}"...`, "info");
538
+
539
+ executor.dispatch(thread).then(() => {
540
+ if (thread.state === "completed") {
541
+ ctx.ui.notify(`✅ W-Thread ${thread.id} done in worktree! Use /wthread push ${thread.id} to merge.`, "info");
542
+ } else {
543
+ ctx.ui.notify(`❌ W-Thread ${thread.id} ${thread.state}`, "error");
544
+ }
545
+ });
546
+ },
547
+ });
548
+
549
+ // ── /plan — Plan mode (port from Grok CLI) ────────────────
550
+
551
+ let planFile: string | null = null;
552
+ let planApproved = false;
553
+
554
+ pi.registerCommand("plan", {
555
+ description: 'Plan mode: write, review, and approve a plan before execution. Usage: /plan "goal" | /plan approve | /plan reject | /plan status',
556
+ handler: async (args, ctx) => {
557
+ if (!args?.trim()) {
558
+ ctx.ui.notify("Usage: /plan <goal> | /plan approve | /plan reject | /plan status", "error");
559
+ return;
560
+ }
561
+
562
+ const sub = args.trim().toLowerCase();
563
+
564
+ if (sub === "approve") {
565
+ if (!planFile) { ctx.ui.notify("No active plan. Use /plan \"goal\" first.", "error"); return; }
566
+ planApproved = true;
567
+ ctx.ui.notify("✅ Plan approved! You can now execute the planned work.", "info");
568
+ return;
569
+ }
570
+
571
+ if (sub === "reject") {
572
+ planFile = null;
573
+ planApproved = false;
574
+ ctx.ui.notify("✗ Plan rejected. Revise and re-submit with /plan \"goal\".", "warning");
575
+ return;
576
+ }
577
+
578
+ if (sub === "status") {
579
+ if (planFile && existsSync(planFile)) {
580
+ const content = readFileSync(planFile, "utf8");
581
+ const status = planApproved ? "✅ Approved" : "⏳ Awaiting approval";
582
+ ctx.ui.notify(`Plan status: ${status}\n\n${content.slice(0, 1000)}`, "info");
583
+ } else {
584
+ ctx.ui.notify("No active plan.", "info");
585
+ }
586
+ return;
587
+ }
588
+
589
+ // Create a new plan
590
+ const goal = args.trim();
591
+ planApproved = false;
592
+
593
+ // Create plan directory
594
+ const planDir = join(ctx.cwd, ".pi", "plans");
595
+ if (!existsSync(planDir)) mkdirSync(planDir, { recursive: true });
596
+ const ts = new Date().toISOString().slice(0, 19).replace(/[T:]/g, "-");
597
+ planFile = join(planDir, `plan-${ts}.md`);
598
+
599
+ const planMd = [
600
+ `# Plan: ${goal}`,
601
+ "",
602
+ "## Goal",
603
+ goal,
604
+ "",
605
+ "## Approach",
606
+ "(Generated by agent — review and edit before approving)",
607
+ "",
608
+ "## Steps",
609
+ "1. ",
610
+ "2. ",
611
+ "3. ",
612
+ "",
613
+ "## Verification",
614
+ "- ",
615
+ "",
616
+ "---",
617
+ `Created: ${new Date().toISOString()}`,
618
+ "Status: draft (run `/plan approve` to approve, `/plan reject` to reject)",
619
+ ].join("\n");
620
+
621
+ writeFileSync(planFile, planMd, "utf8");
622
+ ctx.ui.notify(
623
+ `📋 Plan created at ${planFile}\nReview it, then run /plan approve or /plan reject.`,
624
+ "info"
625
+ );
626
+ },
627
+ });
628
+
629
+ // ── /loop — Scheduled recurring tasks (port from Grok CLI) ─
630
+
631
+ const scheduler = new Map<string, { interval: number; prompt: string; timer: ReturnType<typeof setInterval> | null }>();
632
+
633
+ function parseInterval(input: string): number | null {
634
+ const m = input.match(/^(\d+)(s|m|h|d)$/);
635
+ if (!m) return null;
636
+ const n = parseInt(m[1]);
637
+ switch (m[2]) {
638
+ case "s": return n * 1000;
639
+ case "m": return n * 60 * 1000;
640
+ case "h": return n * 3600 * 1000;
641
+ case "d": return n * 86400 * 1000;
642
+ default: return null;
643
+ }
644
+ }
645
+
646
+ pi.registerCommand("loop", {
647
+ description: 'Schedule a recurring task. Usage: /loop 5m "check test status" | /loop list | /loop stop <id>',
648
+ handler: async (args, ctx) => {
649
+ if (!args?.trim()) {
650
+ ctx.ui.notify("Usage: /loop <interval> <prompt> | /loop list | /loop stop <id>", "error");
651
+ return;
652
+ }
653
+
654
+ const parts = args.trim().split(/\s+/);
655
+ const sub = parts[0].toLowerCase();
656
+
657
+ if (sub === "list") {
658
+ if (scheduler.size === 0) { ctx.ui.notify("No scheduled tasks.", "info"); return; }
659
+ const lines: string[] = [];
660
+ for (const [id, task] of scheduler) {
661
+ const intervalStr = `${task.interval / 1000}s`;
662
+ lines.push(` ${id}: every ${intervalStr} — "${task.prompt.slice(0, 50)}"`);
663
+ }
664
+ ctx.ui.notify(`Scheduled tasks (${scheduler.size}):\n${lines.join("\n")}`, "info");
665
+ return;
666
+ }
667
+
668
+ if (sub === "stop" && parts.length >= 2) {
669
+ const id = parts[1];
670
+ const task = scheduler.get(id);
671
+ if (!task) { ctx.ui.notify(`No scheduled task with id ${id}`, "error"); return; }
672
+ if (task.timer) clearInterval(task.timer);
673
+ scheduler.delete(id);
674
+ ctx.ui.notify(`Stopped ${id}`, "info");
675
+ return;
676
+ }
677
+
678
+ // Parse: /loop 5m "prompt"
679
+ const intervalStr = parts[0];
680
+ const interval = parseInterval(intervalStr);
681
+ if (!interval) { ctx.ui.notify(`Invalid interval: ${intervalStr}. Use format: 30s, 5m, 2h, 1d`, "error"); return; }
682
+
683
+ const prompt = parts.slice(1).join(" ");
684
+ if (!prompt) { ctx.ui.notify("No prompt specified", "error"); return; }
685
+
686
+ const id = `loop-${Date.now()}`;
687
+ ctx.ui.notify(`⏰ /loop ${id}: every ${intervalStr} — "${prompt.slice(0, 50)}"`, "info");
688
+
689
+ // Fire immediately
690
+ const thread = registry.create("scheduled", `Loop: ${prompt.slice(0, 40)}`, [prompt], {
691
+ cwd: ctx.cwd,
692
+ backend: "native",
693
+ });
694
+ executor.dispatch(thread);
695
+
696
+ // Schedule recurring
697
+ const timer = setInterval(() => {
698
+ const t = registry.create("scheduled", `Loop: ${prompt.slice(0, 40)}`, [prompt], {
699
+ cwd: ctx.cwd,
700
+ backend: "native",
701
+ });
702
+ executor.dispatch(t);
703
+ }, interval);
704
+
705
+ scheduler.set(id, { interval, prompt, timer });
706
+ },
707
+ });
708
+
471
709
  // ── /story — STORIES (the unique layer) ──────────────────────
472
710
 
473
711
  pi.registerCommand("story", {
@@ -556,6 +794,16 @@ export default function (pi: ExtensionAPI) {
556
794
  },
557
795
  });
558
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
+
559
807
  // ── LLM-callable tools ───────────────────────────────────────
560
808
 
561
809
  pi.registerTool({
@@ -563,15 +811,19 @@ export default function (pi: ExtensionAPI) {
563
811
  label: "Thread Spawn",
564
812
  description: [
565
813
  "Spawn a thread. Types:",
814
+ "- base: one prompt -> tool calls -> review (native)",
566
815
  "- parallel: N independent tasks in parallel (via subagent)",
567
- "- chained: sequential phases with checkpoints (via subagent)",
568
- "- meta: scoutplanbuildreview pipeline (via subagent)",
816
+ "- chained: sequential phases with human checkpoints (native)",
817
+ "- meta: scout -> plan -> build -> review pipeline (via subagent)",
569
818
  "- fusion: same prompt to N agents/models, compare results (native, UNIQUE)",
570
819
  "- zero: autonomous + verification command gate (native, UNIQUE)",
571
820
  "- long: extended autonomous run (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)",
572
824
  ].join("\n"),
573
825
  parameters: Type.Object({
574
- type: StringEnum(["parallel", "fusion", "chained", "meta", "long", "zero"] as const),
826
+ type: StringEnum(["base", "parallel", "fusion", "chained", "meta", "long", "zero", "worktree", "plan", "scheduled"] as const),
575
827
  prompts: Type.Array(Type.String(), { description: "Task prompts" }),
576
828
  models: Type.Optional(Type.Array(Type.String(), { description: "Models for fusion (e.g. ['anthropic/claude-sonnet-4', 'google/gemini-2.5-pro'])" })),
577
829
  count: Type.Optional(Type.Number({ description: "Agent count for fusion (default 3)" })),
@@ -592,8 +844,9 @@ export default function (pi: ExtensionAPI) {
592
844
  taskPrompts = [prompts[0]]; // Meta delegates to subagent chain internally
593
845
  }
594
846
 
595
- // Auto-select backend
596
- const backend = backendOverride ?? (type === "fusion" || type === "zero" || type === "long" ? "native" : "subagent");
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);
597
850
 
598
851
  const label = type === "fusion"
599
852
  ? `Fusion: ${prompts[0]?.slice(0, 40)}`
@@ -608,7 +861,12 @@ export default function (pi: ExtensionAPI) {
608
861
  });
609
862
 
610
863
  // Dispatch (async — runs in background)
611
- 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);
612
870
 
613
871
  const modelInfo = models ? ` Models: ${models.join(", ")}` : "";
614
872
  const verifyInfo = verify ? ` Verify: ${verify}` : "";
@@ -622,7 +880,7 @@ export default function (pi: ExtensionAPI) {
622
880
  details: { threadId: thread.id, type, taskCount: taskPrompts.length, backend },
623
881
  };
624
882
  },
625
- renderCall(args, theme) {
883
+ renderCall(args: any, theme: any) {
626
884
  return new Text(
627
885
  theme.fg("toolTitle", theme.bold("thread_spawn ")) +
628
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.6",
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
- "@mariozechner/pi-coding-agent": ">=0.73.0",
38
- "@mariozechner/pi-tui": ">=0.73.0",
39
- "@mariozechner/pi-ai": ">=0.73.0",
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
  }