oh-pi 0.1.58 → 0.1.59

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-pi",
3
- "version": "0.1.58",
3
+ "version": "0.1.59",
4
4
  "description": "One-click setup for pi-coding-agent. Like oh-my-zsh for pi.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -60,7 +60,6 @@ interface BackgroundColony {
60
60
  abortController: AbortController;
61
61
  state: ColonyState | null;
62
62
  phase: string;
63
- log: string[];
64
63
  antStreams: Map<string, AntStreamState>;
65
64
  promise: Promise<ColonyState>;
66
65
  }
@@ -84,20 +83,13 @@ export default function antColonyExtension(pi: ExtensionAPI) {
84
83
  pi.on("session_start", async (_event, ctx) => {
85
84
  pi.events.on("ant-colony:render", () => {
86
85
  if (!activeColony) return;
87
- const { state, antStreams } = activeColony;
88
- const active = antStreams.size;
86
+ const { state } = activeColony;
89
87
  const elapsed = state ? formatDuration(Date.now() - state.createdAt) : "0s";
90
88
  const m = state?.metrics;
91
89
  const colonyStatus = state?.status || "scouting";
92
- const ants = state?.ants || [];
93
- const turns = ants.reduce((s, a) => s + a.usage.turns, 0);
94
- const outTok = ants.reduce((s, a) => s + a.usage.output, 0);
95
90
 
96
91
  const parts = [`🐜 ${statusIcon(colonyStatus)}`];
97
92
  if (m) parts.push(`${m.tasksDone}/${m.tasksTotal}`);
98
- parts.push(`${active}⚡`);
99
- parts.push(`${turns}↻`);
100
- parts.push(formatTokens(outTok) + "↑");
101
93
  if (m) parts.push(formatCost(m.totalCost));
102
94
  parts.push(elapsed);
103
95
 
@@ -129,18 +121,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
129
121
  appendFileSync(gitignorePath, `${gitContent.length && !gitContent.endsWith("\n") ? "\n" : ""}.ant-colony/\n`);
130
122
  }
131
123
 
132
- const log: string[] = [];
133
- const callbacks: QueenCallbacks = {
134
- onPhase(_phase, detail) { log.push(detail); },
135
- onAntSpawn(ant, task) { log.push(` ${casteIcon(ant.caste)} ${ant.caste} → ${task.title.slice(0, 50)}`); },
136
- onAntDone(ant, task) {
137
- const dur = ant.finishedAt ? formatDuration(ant.finishedAt - ant.startedAt) : "?";
138
- log.push(` ${ant.status === "done" ? "✓" : "✗"} ${ant.caste} (${dur}) → ${task.title.slice(0, 50)}`);
139
- },
140
- onAntStream() {},
141
- onProgress() {},
142
- onComplete() {},
143
- };
124
+ const callbacks: QueenCallbacks = {};
144
125
 
145
126
  try {
146
127
  const state = await runColony({
@@ -160,14 +141,14 @@ export default function antColonyExtension(pi: ExtensionAPI) {
160
141
  const report = [
161
142
  `## 🐜 Ant Colony Report`,
162
143
  `**Goal:** ${state.goal}`,
163
- `**Status:** ${statusIcon(state.status)} ${state.status} │ ${elapsed}`,
164
- `**Metrics:** ${m.tasksDone}/${m.tasksTotal} tasks │ ${m.antsSpawned} ants ${formatTokens(m.totalTokens)} ${formatCost(m.totalCost)}`,
144
+ `**Status:** ${statusIcon(state.status)} ${state.status} │ ${elapsed} │ ${formatCost(m.totalCost)}`,
145
+ `**Tasks:** ${m.tasksDone}/${m.tasksTotal} done${m.tasksFailed > 0 ? `, ${m.tasksFailed} failed` : ""}`,
165
146
  ``,
166
147
  ...state.tasks.filter(t => t.status === "done").map(t =>
167
- `- ✓ **${t.title}**${t.result ? `: ${t.result.split("\n")[0]?.slice(0, 100)}` : ""}`
148
+ `- ✓ **${t.title}**`
168
149
  ),
169
150
  ...state.tasks.filter(t => t.status === "failed").map(t =>
170
- `- ✗ **${t.title}** — ${t.error?.slice(0, 100) || "unknown"}`
151
+ `- ✗ **${t.title}** — ${t.error?.slice(0, 80) || "unknown"}`
171
152
  ),
172
153
  ].join("\n");
173
154
 
@@ -205,32 +186,27 @@ export default function antColonyExtension(pi: ExtensionAPI) {
205
186
  abortController,
206
187
  state: null,
207
188
  phase: "initializing",
208
- log: [],
209
189
  antStreams: new Map(),
210
190
  promise: null as any, // set below
211
191
  };
212
192
 
213
193
  const callbacks: QueenCallbacks = {
194
+ onSignal(signal) {
195
+ colony.phase = signal.message;
196
+ throttledRender();
197
+ },
214
198
  onPhase(phase, detail) {
215
199
  colony.phase = detail;
216
- colony.log.push(`[${new Date().toLocaleTimeString()}] ${statusIcon(phase)} ${detail}`);
217
200
  throttledRender();
218
201
  },
219
202
  onAntSpawn(ant, task) {
220
203
  colony.antStreams.set(ant.id, {
221
- antId: ant.id,
222
- caste: ant.caste,
223
- lastLine: "starting...",
224
- tokens: 0,
204
+ antId: ant.id, caste: ant.caste, lastLine: "starting...", tokens: 0,
225
205
  });
226
- colony.log.push(` ${casteIcon(ant.caste)} ${ant.caste} ant dispatched → ${task.title.slice(0, 50)}`);
227
206
  throttledRender();
228
207
  },
229
- onAntDone(ant, task) {
208
+ onAntDone(ant) {
230
209
  colony.antStreams.delete(ant.id);
231
- const dur = ant.finishedAt ? formatDuration(ant.finishedAt - ant.startedAt) : "?";
232
- const icon = ant.status === "done" ? "✓" : "✗";
233
- colony.log.push(` ${icon} ${ant.caste} finished (${dur}, ${formatCost(ant.usage.cost)}) → ${task.title.slice(0, 50)}`);
234
210
  throttledRender();
235
211
  },
236
212
  onAntStream(event: AntStreamEvent) {
@@ -240,7 +216,6 @@ export default function antColonyExtension(pi: ExtensionAPI) {
240
216
  const lines = event.totalText.split("\n").filter(l => l.trim());
241
217
  stream.lastLine = lines[lines.length - 1]?.trim() || "...";
242
218
  }
243
- throttledRender();
244
219
  },
245
220
  onProgress(metrics) {
246
221
  if (colony.state) colony.state.metrics = metrics;
@@ -285,25 +260,15 @@ export default function antColonyExtension(pi: ExtensionAPI) {
285
260
 
286
261
  const report = [
287
262
  `## 🐜 Ant Colony Report`,
288
- ``,
289
263
  `**Goal:** ${state.goal}`,
290
- `**Status:** ${statusIcon(state.status)} ${state.status}`,
291
- `**Duration:** ${elapsed}`,
292
- ...(state.maxCost != null ? [`**Budget:** ${formatCost(m.totalCost)} / ${formatCost(state.maxCost)}`] : []),
293
- ``,
294
- `### Metrics`,
295
- `- Tasks: ${m.tasksDone}/${m.tasksTotal} done, ${m.tasksFailed} failed`,
296
- `- Ants spawned: ${m.antsSpawned}`,
297
- `- Tokens: ${formatTokens(m.totalTokens)}`,
298
- `- Cost: ${formatCost(m.totalCost)}`,
299
- `- Peak concurrency: ${state.concurrency.optimal}`,
264
+ `**Status:** ${statusIcon(state.status)} ${state.status} │ ${elapsed} │ ${formatCost(m.totalCost)}`,
265
+ `**Tasks:** ${m.tasksDone}/${m.tasksTotal} done${m.tasksFailed > 0 ? `, ${m.tasksFailed} failed` : ""}`,
300
266
  ``,
301
- `### Task Results`,
302
267
  ...state.tasks.filter(t => t.status === "done").map(t =>
303
- `- ✓ **${t.title}** (${t.caste})${t.result ? `\n ${t.result.split("\n")[0]?.slice(0, 100)}` : ""}`
268
+ `- ✓ **${t.title}**`
304
269
  ),
305
270
  ...state.tasks.filter(t => t.status === "failed").map(t =>
306
- `- ✗ **${t.title}** — ${t.error?.slice(0, 100) || "unknown error"}`
271
+ `- ✗ **${t.title}** — ${t.error?.slice(0, 80) || "unknown"}`
307
272
  ),
308
273
  ].join("\n");
309
274
 
@@ -420,22 +385,10 @@ export default function antColonyExtension(pi: ExtensionAPI) {
420
385
  // ── Active Ants ──
421
386
  const streams = Array.from(c.antStreams.values());
422
387
  if (streams.length > 0) {
423
- lines.push(theme.fg("accent", " Active Ants"));
424
- for (const s of streams) {
425
- const line = s.lastLine.length > w - 20 ? s.lastLine.slice(0, w - 23) + "..." : s.lastLine;
426
- lines.push(` ${casteIcon(s.caste)} ${theme.fg("accent", s.antId.slice(0, 14))} ${theme.fg("dim", `${s.tokens}tok`)} ${theme.fg("muted", "▸")} ${line}`);
427
- }
388
+ lines.push(theme.fg("accent", ` Active: ${streams.length} ants working`));
428
389
  lines.push("");
429
390
  }
430
391
 
431
- // ── Log (last 8) ──
432
- if (c.log.length > 0) {
433
- lines.push(theme.fg("accent", " Log"));
434
- for (const l of c.log.slice(-8)) {
435
- lines.push(theme.fg("dim", ` ${l.slice(0, w - 2)}`));
436
- }
437
- }
438
-
439
392
  lines.push("");
440
393
  lines.push(theme.fg("muted", " esc close"));
441
394
  return lines;
@@ -566,38 +519,14 @@ export default function antColonyExtension(pi: ExtensionAPI) {
566
519
  const state = c.state;
567
520
  const elapsed = state ? formatDuration(Date.now() - state.createdAt) : "0s";
568
521
  const m = state?.metrics;
569
- const tasks = state?.tasks || [];
570
- const ants = state?.ants || [];
571
- const streams = Array.from(c.antStreams.values());
522
+ const phase = state?.status || "scouting";
572
523
 
573
524
  const lines: string[] = [
574
- `## 🐜 Colony Status`,
575
- `**Goal:** ${c.goal}`,
576
- `**Phase:** ${c.phase}`,
577
- `**Duration:** ${elapsed}`,
525
+ `🐜 ${statusIcon(phase)} ${c.goal.slice(0, 80)}`,
526
+ `${phase} ${m ? `${m.tasksDone}/${m.tasksTotal} tasks` : "starting"} │ ${m ? formatCost(m.totalCost) : "$0"} │ ${elapsed}`,
578
527
  ];
579
528
 
580
- if (m) {
581
- lines.push(`**Tasks:** ${m.tasksDone}/${m.tasksTotal} done, ${m.tasksFailed} failed`);
582
- lines.push(`**Ants spawned:** ${m.antsSpawned} | **Active:** ${streams.length}`);
583
- lines.push(`**Cost:** ${formatCost(m.totalCost)} | **Tokens:** ${formatTokens(m.totalTokens)}`);
584
- }
585
-
586
- if (tasks.length > 0) {
587
- lines.push("", "### Tasks");
588
- for (const t of tasks) {
589
- const icon = t.status === "done" ? "✓" : t.status === "failed" ? "✗" : t.status === "active" ? "●" : "○";
590
- const dur = t.finishedAt && t.startedAt ? ` (${formatDuration(t.finishedAt - t.startedAt)})` : "";
591
- lines.push(`- ${icon} [${t.caste}] ${t.title}${dur}`);
592
- }
593
- }
594
-
595
- if (streams.length > 0) {
596
- lines.push("", "### Active Ants");
597
- for (const s of streams) {
598
- lines.push(`- ${casteIcon(s.caste)} ${s.antId.slice(0, 14)} | ${s.tokens}tok | ${s.lastLine.slice(0, 60)}`);
599
- }
600
- }
529
+ if (m && m.tasksFailed > 0) lines.push(`⚠ ${m.tasksFailed} failed`);
601
530
 
602
531
  return lines.join("\n");
603
532
  }
@@ -15,7 +15,7 @@
15
15
  import * as fs from "node:fs";
16
16
  import * as path from "node:path";
17
17
  import type {
18
- ColonyState, Task, Ant, AntCaste, ColonyMetrics,
18
+ ColonyState, Task, Ant, AntCaste, ColonyMetrics, ColonySignal,
19
19
  ConcurrencyConfig, TaskPriority, ModelOverrides, AntStreamEvent,
20
20
  } from "./types.js";
21
21
  import { DEFAULT_ANT_CONFIGS } from "./types.js";
@@ -26,12 +26,15 @@ import { buildImportGraph, taskDependsOn, type ImportGraph } from "./deps.js";
26
26
  import type { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
27
27
 
28
28
  export interface QueenCallbacks {
29
- onPhase(phase: ColonyState["status"], detail: string): void;
30
- onAntSpawn(ant: Ant, task: Task): void;
31
- onAntDone(ant: Ant, task: Task, output: string): void;
29
+ /** 抽象信号 观察者只需实现这一个 */
30
+ onSignal?(signal: ColonySignal): void;
31
+ /** 以下为细粒度回调(verbose 模式,可选) */
32
+ onPhase?(phase: ColonyState["status"], detail: string): void;
33
+ onAntSpawn?(ant: Ant, task: Task): void;
34
+ onAntDone?(ant: Ant, task: Task, output: string): void;
32
35
  onAntStream?(event: AntStreamEvent): void;
33
- onProgress(metrics: ColonyMetrics): void;
34
- onComplete(state: ColonyState): void;
36
+ onProgress?(metrics: ColonyMetrics): void;
37
+ onComplete?(state: ColonyState): void;
35
38
  }
36
39
 
37
40
  export interface QueenOptions {
@@ -176,13 +179,13 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
176
179
  usage: { input: 0, output: 0, cost: 0, turns: 0 },
177
180
  startedAt: Date.now(), finishedAt: null,
178
181
  };
179
- callbacks.onAntSpawn(ant, task);
182
+ callbacks.onAntSpawn?.(ant, task);
180
183
 
181
184
  try {
182
185
  const result = caste === "drone"
183
186
  ? await runDrone(cwd, nest, task)
184
187
  : await spawnAnt(cwd, nest, task, config, signal, callbacks.onAntStream, opts.authStorage, opts.modelRegistry);
185
- callbacks.onAntDone(result.ant, task, result.output);
188
+ callbacks.onAntDone?.(result.ant, task, result.output);
186
189
 
187
190
  if (result.rateLimited) {
188
191
  return "rate_limited";
@@ -207,7 +210,8 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
207
210
 
208
211
  // 更新指标
209
212
  const metrics = updateMetrics(nest);
210
- callbacks.onProgress(metrics);
213
+ callbacks.onProgress?.(metrics);
214
+ emitSignal("working", `${metrics.tasksDone}/${metrics.tasksTotal} tasks done`);
211
215
 
212
216
  return "done";
213
217
  } catch (e) {
@@ -230,7 +234,7 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
230
234
 
231
235
  // 429 退避:短暂等待后恢复,连续限流才逐步加长
232
236
  if (backoffMs > 0) {
233
- callbacks.onPhase("working", `Rate limited (429). Waiting ${Math.round(backoffMs / 1000)}s...`);
237
+ callbacks.onPhase?.("working", `Rate limited (429). Waiting ${Math.round(backoffMs / 1000)}s...`);
234
238
  await new Promise(r => setTimeout(r, backoffMs));
235
239
  }
236
240
 
@@ -349,9 +353,17 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
349
353
  } catch { /* ignore */ }
350
354
  };
351
355
 
356
+ const emitSignal = (phase: ColonyState["status"], message: string) => {
357
+ const m = nest.getState().metrics;
358
+ const active = nest.getState().ants.filter(a => a.status === "working").length;
359
+ const progress = m.tasksTotal > 0 ? m.tasksDone / m.tasksTotal : 0;
360
+ callbacks.onSignal?.({ phase, progress, active, cost: m.totalCost, message });
361
+ };
362
+
352
363
  try {
353
364
  // ═══ Phase 1: 侦察(快速单次,不再多轮接力) ═══
354
- callbacks.onPhase("scouting", "Dispatching scout ant to explore codebase...");
365
+ callbacks.onPhase?.("scouting", "Dispatching scout ant to explore codebase...");
366
+ emitSignal("scouting", "Exploring codebase...");
355
367
  await runAntWave({ ...waveBase, caste: "scout" });
356
368
 
357
369
  let workerTasks = nest.getAllTasks().filter(t => (t.caste === "worker" || t.caste === "drone") && t.status === "pending");
@@ -380,7 +392,8 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
380
392
  finishedAt: null,
381
393
  };
382
394
  nest.writeTask(relayTask);
383
- callbacks.onPhase("scouting", "Scout relay: generating worker tasks...");
395
+ callbacks.onPhase?.("scouting", "Scout relay: generating worker tasks...");
396
+ emitSignal("scouting", "Retrying scout...");
384
397
  await runAntWave({ ...waveBase, caste: "scout" });
385
398
  workerTasks = nest.getAllTasks().filter(t => (t.caste === "worker" || t.caste === "drone") && t.status === "pending");
386
399
  }
@@ -388,7 +401,8 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
388
401
  if (workerTasks.length === 0) {
389
402
  nest.updateState({ status: "failed", finishedAt: Date.now() });
390
403
  const finalState = nest.getState();
391
- callbacks.onComplete(finalState);
404
+ callbacks.onComplete?.(finalState);
405
+ emitSignal("failed", "No tasks generated");
392
406
  return finalState;
393
407
  }
394
408
 
@@ -408,11 +422,13 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
408
422
  // 先执行 drone 任务(零 LLM 成本)
409
423
  const droneTasks = nest.getAllTasks().filter(t => t.caste === "drone" && t.status === "pending");
410
424
  if (droneTasks.length > 0) {
411
- callbacks.onPhase("working", `${droneTasks.length} drone tasks. Executing rules...`);
425
+ callbacks.onPhase?.("working", `${droneTasks.length} drone tasks. Executing rules...`);
426
+ emitSignal("working", `${droneTasks.length} drone tasks`);
412
427
  await runAntWave({ ...waveBase, caste: "drone" });
413
428
  }
414
429
 
415
- callbacks.onPhase("working", `${workerTasks.length} tasks discovered. Dispatching worker ants...`);
430
+ callbacks.onPhase?.("working", `${workerTasks.length} tasks discovered. Dispatching worker ants...`);
431
+ emitSignal("working", `${workerTasks.length} tasks to do`);
416
432
  await runAntWave({ ...waveBase, caste: "worker" });
417
433
 
418
434
  // 处理工蚁产生的子任务(可能有多轮)
@@ -427,7 +443,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
427
443
  );
428
444
  if (remaining.length === 0) break;
429
445
  rounds++;
430
- callbacks.onPhase("working", `Round ${rounds + 1}: ${remaining.length} sub-tasks from workers...`);
446
+ callbacks.onPhase?.("working", `Round ${rounds + 1}: ${remaining.length} sub-tasks from workers...`);
431
447
  await runAntWave({ ...waveBase, caste: "worker" });
432
448
  }
433
449
 
@@ -444,7 +460,8 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
444
460
  const completedWorkerTasks = nest.getAllTasks().filter(t => t.caste === "worker" && t.status === "done");
445
461
  if (completedWorkerTasks.length > 0 && (!tscPassed || completedWorkerTasks.length > 3)) {
446
462
  nest.updateState({ status: "reviewing" });
447
- callbacks.onPhase("reviewing", "Dispatching soldier ants to review changes...");
463
+ callbacks.onPhase?.("reviewing", "Dispatching soldier ants to review changes...");
464
+ emitSignal("reviewing", "Reviewing changes...");
448
465
  const reviewTask = makeReviewTask(completedWorkerTasks);
449
466
  nest.writeTask(reviewTask);
450
467
  await runAntWave({ ...waveBase, caste: "soldier" });
@@ -455,7 +472,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
455
472
  );
456
473
  if (fixTasks.length > 0) {
457
474
  nest.updateState({ status: "working" });
458
- callbacks.onPhase("working", `${fixTasks.length} fix tasks from review. Dispatching workers...`);
475
+ callbacks.onPhase?.("working", `${fixTasks.length} fix tasks from review. Dispatching workers...`);
459
476
  await runAntWave({ ...waveBase, caste: "worker" });
460
477
  }
461
478
  }
@@ -464,13 +481,15 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
464
481
  const finalMetrics = updateMetrics(nest);
465
482
  nest.updateState({ status: "done", finishedAt: Date.now(), metrics: finalMetrics });
466
483
  const finalState = nest.getState();
467
- callbacks.onComplete(finalState);
484
+ callbacks.onComplete?.(finalState);
485
+ emitSignal("done", `${finalMetrics.tasksDone}/${finalMetrics.tasksTotal} tasks done`);
468
486
  return finalState;
469
487
 
470
488
  } catch (e) {
471
489
  nest.updateState({ status: "failed", finishedAt: Date.now() });
472
490
  const failState = nest.getState();
473
- callbacks.onComplete(failState);
491
+ callbacks.onComplete?.(failState);
492
+ emitSignal("failed", String(e).slice(0, 100));
474
493
  return failState;
475
494
  } finally {
476
495
  cleanup();
@@ -132,3 +132,12 @@ export interface ColonyMetrics {
132
132
  startTime: number;
133
133
  throughputHistory: number[]; // tasks/min 滑动窗口
134
134
  }
135
+
136
+ /** 蚁群抽象信号 — 观察者只需关注这一个 */
137
+ export interface ColonySignal {
138
+ phase: ColonyState["status"];
139
+ progress: number; // 0-1
140
+ active: number; // 当前工作中的蚂蚁数
141
+ cost: number;
142
+ message: string; // 一句话描述
143
+ }