oh-pi 0.1.54 → 0.1.55

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.54",
3
+ "version": "0.1.55",
4
4
  "description": "One-click setup for pi-coding-agent. Like oh-my-zsh for pi.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,7 +43,7 @@ function statusIcon(status: string): string {
43
43
  }
44
44
 
45
45
  function casteIcon(caste: string): string {
46
- return caste === "scout" ? "🔍" : caste === "soldier" ? "🛡️" : "⚒️";
46
+ return caste === "scout" ? "🔍" : caste === "soldier" ? "🛡️" : caste === "drone" ? "⚙️" : "⚒️";
47
47
  }
48
48
 
49
49
  // ═══ Background colony state ═══
@@ -20,7 +20,7 @@ import type {
20
20
  } from "./types.js";
21
21
  import { DEFAULT_ANT_CONFIGS } from "./types.js";
22
22
  import { Nest } from "./nest.js";
23
- import { spawnAnt, makeTaskId } from "./spawner.js";
23
+ import { spawnAnt, runDrone, makeTaskId } from "./spawner.js";
24
24
  import { adapt, sampleSystem, defaultConcurrency } from "./concurrency.js";
25
25
  import type { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
26
26
 
@@ -177,7 +177,9 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
177
177
  callbacks.onAntSpawn(ant, task);
178
178
 
179
179
  try {
180
- const result = await spawnAnt(cwd, nest, task, config, signal, callbacks.onAntStream, opts.authStorage, opts.modelRegistry);
180
+ const result = caste === "drone"
181
+ ? await runDrone(cwd, nest, task)
182
+ : await spawnAnt(cwd, nest, task, config, signal, callbacks.onAntStream, opts.authStorage, opts.modelRegistry);
181
183
  callbacks.onAntDone(result.ant, task, result.output);
182
184
 
183
185
  if (result.rateLimited) {
@@ -344,7 +346,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
344
346
  callbacks.onPhase("scouting", "Dispatching scout ant to explore codebase...");
345
347
  await runAntWave({ ...waveBase, caste: "scout" });
346
348
 
347
- let workerTasks = nest.getAllTasks().filter(t => t.caste === "worker" && t.status === "pending");
349
+ let workerTasks = nest.getAllTasks().filter(t => (t.caste === "worker" || t.caste === "drone") && t.status === "pending");
348
350
 
349
351
  // 只在完全没有 worker 任务时才重试一次
350
352
  if (workerTasks.length === 0) {
@@ -372,7 +374,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
372
374
  nest.writeTask(relayTask);
373
375
  callbacks.onPhase("scouting", "Scout relay: generating worker tasks...");
374
376
  await runAntWave({ ...waveBase, caste: "scout" });
375
- workerTasks = nest.getAllTasks().filter(t => t.caste === "worker" && t.status === "pending");
377
+ workerTasks = nest.getAllTasks().filter(t => (t.caste === "worker" || t.caste === "drone") && t.status === "pending");
376
378
  }
377
379
 
378
380
  if (workerTasks.length === 0) {
@@ -384,12 +386,24 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
384
386
 
385
387
  // ═══ Phase 2: 工作 ═══
386
388
  nest.updateState({ status: "working" });
389
+
390
+ // 先执行 drone 任务(零 LLM 成本)
391
+ const droneTasks = nest.getAllTasks().filter(t => t.caste === "drone" && t.status === "pending");
392
+ if (droneTasks.length > 0) {
393
+ callbacks.onPhase("working", `${droneTasks.length} drone tasks. Executing rules...`);
394
+ await runAntWave({ ...waveBase, caste: "drone" });
395
+ }
396
+
387
397
  callbacks.onPhase("working", `${workerTasks.length} tasks discovered. Dispatching worker ants...`);
388
398
  await runAntWave({ ...waveBase, caste: "worker" });
389
399
 
390
400
  // 处理工蚁产生的子任务(可能有多轮)
391
401
  let rounds = 0;
392
402
  while (rounds < 3) {
403
+ // 先跑 drone 子任务
404
+ const pendingDrones = nest.getAllTasks().filter(t => t.caste === "drone" && t.status === "pending");
405
+ if (pendingDrones.length > 0) await runAntWave({ ...waveBase, caste: "drone" });
406
+
393
407
  const remaining = nest.getAllTasks().filter(t =>
394
408
  t.caste === "worker" && (t.status === "pending" || t.status === "blocked")
395
409
  );
@@ -83,6 +83,8 @@ For each task the colony should do next:
83
83
  - priority: <1-5, 1=highest>
84
84
  - context: <relevant code snippets that the worker will need, with file:line references>
85
85
 
86
+ Use caste "drone" instead of "worker" for simple tasks that can be done with a single bash command (file copy, find-replace, formatting, running tests). Drone description should be the exact bash command to execute.
87
+
86
88
  ## Warnings
87
89
  Any risks, blockers, or conflicts detected.`,
88
90
 
@@ -250,6 +252,49 @@ function makeMinimalResourceLoader(systemPrompt: string): ResourceLoader {
250
252
  };
251
253
  }
252
254
 
255
+ /**
256
+ * 运行 Drone — 纯规则执行,零 LLM 成本
257
+ * 任务描述即为要执行的 bash 命令
258
+ */
259
+ export async function runDrone(
260
+ cwd: string,
261
+ nest: Nest,
262
+ task: Task,
263
+ ): Promise<AntResult> {
264
+ const antId = makeAntId("drone");
265
+ const ant: Ant = {
266
+ id: antId, caste: "drone", status: "working", taskId: task.id,
267
+ pid: null, model: "none",
268
+ usage: { input: 0, output: 0, cost: 0, turns: 1 },
269
+ startedAt: Date.now(), finishedAt: null,
270
+ };
271
+ nest.updateAnt(ant);
272
+ nest.updateTaskStatus(task.id, "active");
273
+
274
+ try {
275
+ const { execSync } = await import("node:child_process");
276
+ const cmd = task.description;
277
+ const output = execSync(cmd, { cwd, encoding: "utf-8", timeout: 30000, stdio: "pipe" }).trim();
278
+
279
+ ant.status = "done";
280
+ ant.finishedAt = Date.now();
281
+ nest.updateAnt(ant);
282
+ nest.updateTaskStatus(task.id, "done", `## Completed\n${output || "(no output)"}`);
283
+ nest.dropPheromone({
284
+ id: makePheromoneId(), type: "completion", antId, antCaste: "drone",
285
+ taskId: task.id, content: `Drone executed: ${cmd.slice(0, 100)}`, files: task.files, strength: 1, createdAt: Date.now(),
286
+ });
287
+ return { ant, output, newTasks: [], pheromones: [], rateLimited: false };
288
+ } catch (e: any) {
289
+ const errStr = e.stderr?.toString() || String(e);
290
+ ant.status = "failed";
291
+ ant.finishedAt = Date.now();
292
+ nest.updateAnt(ant);
293
+ nest.updateTaskStatus(task.id, "failed", undefined, errStr.slice(0, 500));
294
+ return { ant, output: errStr, newTasks: [], pheromones: [], rateLimited: false };
295
+ }
296
+ }
297
+
253
298
  /**
254
299
  * 孵化并运行一只蚂蚁 — SDK 内嵌版
255
300
  */
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  // ═══ 蚂蚁角色 ═══
6
- export type AntCaste = "scout" | "worker" | "soldier";
6
+ export type AntCaste = "scout" | "worker" | "soldier" | "drone";
7
7
 
8
8
  export interface AntConfig {
9
9
  caste: AntCaste;
@@ -17,6 +17,7 @@ export const DEFAULT_ANT_CONFIGS: Record<AntCaste, Omit<AntConfig, "systemPrompt
17
17
  scout: { caste: "scout", model: "", tools: ["read", "bash", "grep", "find", "ls"], maxTurns: 8 },
18
18
  worker: { caste: "worker", model: "", tools: ["read", "bash", "edit", "write", "grep", "find", "ls"], maxTurns: 15 },
19
19
  soldier: { caste: "soldier", model: "", tools: ["read", "bash", "grep", "find", "ls"], maxTurns: 8 },
20
+ drone: { caste: "drone", model: "", tools: ["bash"], maxTurns: 1 },
20
21
  };
21
22
 
22
23
  /** Per-caste model overrides from user config */