oh-pi 0.1.62 → 0.1.64

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.62",
3
+ "version": "0.1.64",
4
4
  "description": "One-click setup for pi-coding-agent. Like oh-my-zsh for pi.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,7 +14,8 @@ import { join } from "node:path";
14
14
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
15
15
  import { Text, Container, Spacer, matchesKey } from "@mariozechner/pi-tui";
16
16
  import { Type } from "@sinclair/typebox";
17
- import { runColony, type QueenCallbacks } from "./queen.js";
17
+ import { runColony, resumeColony, type QueenCallbacks } from "./queen.js";
18
+ import { Nest } from "./nest.js";
18
19
  import type { ColonyState, ColonyMetrics, AntStreamEvent } from "./types.js";
19
20
 
20
21
  // ═══ Helpers ═══
@@ -174,7 +175,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
174
175
  modelOverrides: Record<string, string>;
175
176
  cwd: string;
176
177
  modelRegistry?: any;
177
- }) {
178
+ }, resume = false) {
178
179
  if (activeColony) {
179
180
  pi.events.emit("ant-colony:notify", { msg: "A colony is already running. Use /colony-stop first.", level: "warning" });
180
181
  return;
@@ -190,9 +191,21 @@ export default function antColonyExtension(pi: ExtensionAPI) {
190
191
  promise: null as any, // set below
191
192
  };
192
193
 
194
+ let lastPhase = "";
195
+
193
196
  const callbacks: QueenCallbacks = {
194
197
  onSignal(signal) {
195
198
  colony.phase = signal.message;
199
+ // 阶段切换时注入消息到主进程对话流
200
+ if (signal.phase !== lastPhase) {
201
+ lastPhase = signal.phase;
202
+ const pct = Math.round(signal.progress * 100);
203
+ pi.sendMessage({
204
+ customType: "ant-colony-progress",
205
+ content: `[COLONY_SIGNAL:${signal.phase.toUpperCase()}] 🐜 ${signal.message} (${pct}%, ${formatCost(signal.cost)})`,
206
+ display: false,
207
+ }, { triggerTurn: false, deliverAs: "followUp" });
208
+ }
196
209
  throttledRender();
197
210
  },
198
211
  onPhase(phase, detail) {
@@ -236,7 +249,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
236
249
  appendFileSync(gitignorePath, `${gitContent.length && !gitContent.endsWith("\n") ? "\n" : ""}.ant-colony/\n`);
237
250
  }
238
251
 
239
- colony.promise = runColony({
252
+ const colonyOpts = {
240
253
  cwd: params.cwd,
241
254
  goal: params.goal,
242
255
  maxAnts: params.maxAnts,
@@ -247,7 +260,8 @@ export default function antColonyExtension(pi: ExtensionAPI) {
247
260
  callbacks,
248
261
  authStorage: undefined,
249
262
  modelRegistry: params.modelRegistry,
250
- });
263
+ };
264
+ colony.promise = resume ? resumeColony(colonyOpts) : runColony(colonyOpts);
251
265
 
252
266
  activeColony = colony;
253
267
  throttledRender();
@@ -569,6 +583,30 @@ export default function antColonyExtension(pi: ExtensionAPI) {
569
583
  },
570
584
  });
571
585
 
586
+ pi.registerCommand("colony-resume", {
587
+ description: "Resume a colony from its last checkpoint",
588
+ async handler(_args, ctx) {
589
+ if (activeColony) {
590
+ ctx.ui.notify("A colony is already running.", "warning");
591
+ return;
592
+ }
593
+ const found = Nest.findResumable(ctx.cwd);
594
+ if (!found) {
595
+ ctx.ui.notify("No resumable colony found.", "info");
596
+ return;
597
+ }
598
+ ctx.ui.notify(`🐜 Resuming colony: ${found.state.goal.slice(0, 60)}...`, "info");
599
+ launchBackgroundColony({
600
+ cwd: ctx.cwd,
601
+ goal: found.state.goal,
602
+ maxCost: found.state.maxCost ?? undefined,
603
+ currentModel: ctx.currentModel,
604
+ modelOverrides: {},
605
+ modelRegistry: ctx.modelRegistry,
606
+ }, true);
607
+ },
608
+ });
609
+
572
610
  // ═══ Cleanup on shutdown ═══
573
611
  pi.on("session_shutdown", async () => {
574
612
  if (activeColony) {
@@ -263,4 +263,33 @@ export class Nest {
263
263
  private readJson<T>(file: string): T {
264
264
  return JSON.parse(fs.readFileSync(file, "utf-8")) as T;
265
265
  }
266
+
267
+ /** 查找可恢复的蚁群(状态为 working/scouting/reviewing 且未完成) */
268
+ static findResumable(cwd: string): { colonyId: string; state: ColonyState } | null {
269
+ const parentDir = path.join(cwd, ".ant-colony");
270
+ try {
271
+ for (const dir of fs.readdirSync(parentDir)) {
272
+ const stateFile = path.join(parentDir, dir, "state.json");
273
+ if (!fs.existsSync(stateFile)) continue;
274
+ const state = JSON.parse(fs.readFileSync(stateFile, "utf-8")) as ColonyState;
275
+ if (!state.finishedAt && state.status !== "done" && state.status !== "failed" && state.status !== "budget_exceeded") {
276
+ return { colonyId: dir, state };
277
+ }
278
+ }
279
+ } catch { /* no .ant-colony dir */ }
280
+ return null;
281
+ }
282
+
283
+ /** 从已有文件恢复,不调用 init */
284
+ restore(): void {
285
+ this.stateCache = this.readJson<ColonyState>(this.stateFile);
286
+ // 将 claimed/active 任务重置为 pending(蚂蚁已死)
287
+ for (const task of this.getAllTasks()) {
288
+ if (task.status === "claimed" || task.status === "active") {
289
+ task.status = "pending";
290
+ task.claimedBy = undefined;
291
+ this.writeTask(task);
292
+ }
293
+ }
294
+ }
266
295
  }
@@ -542,3 +542,76 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
542
542
  cleanup();
543
543
  }
544
544
  }
545
+
546
+ /**
547
+ * 从检查点恢复蚁群 — 跳过已完成的阶段,继续执行未完成的任务
548
+ */
549
+ export async function resumeColony(opts: QueenOptions): Promise<ColonyState> {
550
+ const found = Nest.findResumable(opts.cwd);
551
+ if (!found) return runColony(opts); // 无可恢复状态,正常启动
552
+
553
+ const nest = new Nest(opts.cwd, found.colonyId);
554
+ nest.restore();
555
+
556
+ const { signal, callbacks } = opts;
557
+ const waveBase: Omit<WaveOptions, "caste"> = {
558
+ nest, cwd: opts.cwd, signal, callbacks,
559
+ currentModel: opts.currentModel,
560
+ modelOverrides: opts.modelOverrides,
561
+ authStorage: opts.authStorage,
562
+ modelRegistry: opts.modelRegistry,
563
+ };
564
+
565
+ const emitSignal = (phase: ColonyState["status"], message: string) => {
566
+ const m = nest.getState().metrics;
567
+ const active = nest.getState().ants.filter(a => a.status === "working").length;
568
+ const progress = m.tasksTotal > 0 ? m.tasksDone / m.tasksTotal : 0;
569
+ callbacks.onSignal?.({ phase, progress, active, cost: m.totalCost, message });
570
+ };
571
+
572
+ const cleanup = () => {
573
+ nest.destroy();
574
+ const parentDir = path.join(opts.cwd, ".ant-colony");
575
+ try {
576
+ const entries = fs.readdirSync(parentDir);
577
+ if (entries.length === 0) fs.rmdirSync(parentDir);
578
+ } catch { /* ignore */ }
579
+ };
580
+
581
+ callbacks.onPhase?.("working", "Resuming colony from checkpoint...");
582
+ emitSignal("working", "Resuming...");
583
+
584
+ try {
585
+ // 执行所有 pending 任务
586
+ const pendingDrones = nest.getAllTasks().filter(t => t.caste === "drone" && t.status === "pending");
587
+ if (pendingDrones.length > 0) await runAntWave({ ...waveBase, caste: "drone" });
588
+
589
+ const pendingWorkers = nest.getAllTasks().filter(t => t.caste === "worker" && t.status === "pending");
590
+ if (pendingWorkers.length > 0) {
591
+ const result = await runAntWave({ ...waveBase, caste: "worker" });
592
+ if (result === "budget") {
593
+ nest.updateState({ status: "budget_exceeded", finishedAt: Date.now() });
594
+ emitSignal("budget_exceeded", "Budget exhausted");
595
+ const s = nest.getState();
596
+ callbacks.onComplete?.(s);
597
+ return s;
598
+ }
599
+ }
600
+
601
+ const finalMetrics = updateMetrics(nest);
602
+ nest.updateState({ status: "done", finishedAt: Date.now(), metrics: finalMetrics });
603
+ const finalState = nest.getState();
604
+ callbacks.onComplete?.(finalState);
605
+ emitSignal("done", `Resumed: ${finalMetrics.tasksDone}/${finalMetrics.tasksTotal} tasks done`);
606
+ return finalState;
607
+
608
+ } catch (e) {
609
+ nest.updateState({ status: "failed", finishedAt: Date.now() });
610
+ const failState = nest.getState();
611
+ callbacks.onComplete?.(failState);
612
+ emitSignal("failed", String(e).slice(0, 100));
613
+ return failState;
614
+ } finally {
615
+ cleanup();
616
+ }
617
+ }