oh-pi 0.1.53 → 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.53",
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
 
@@ -72,7 +72,7 @@ function makeInitialScoutTask(goal: string): Task {
72
72
 
73
73
  function childTaskFromParsed(
74
74
  parentId: string,
75
- parsed: { title: string; description: string; files: string[]; caste: AntCaste; priority: TaskPriority },
75
+ parsed: { title: string; description: string; files: string[]; caste: AntCaste; priority: TaskPriority; context?: string },
76
76
  ): Task {
77
77
  return {
78
78
  id: makeTaskId(),
@@ -83,6 +83,7 @@ function childTaskFromParsed(
83
83
  status: "pending",
84
84
  priority: parsed.priority,
85
85
  files: parsed.files,
86
+ context: parsed.context || undefined,
86
87
  claimedBy: null,
87
88
  result: null,
88
89
  error: null,
@@ -176,7 +177,9 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
176
177
  callbacks.onAntSpawn(ant, task);
177
178
 
178
179
  try {
179
- 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);
180
183
  callbacks.onAntDone(result.ant, task, result.output);
181
184
 
182
185
  if (result.rateLimited) {
@@ -343,7 +346,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
343
346
  callbacks.onPhase("scouting", "Dispatching scout ant to explore codebase...");
344
347
  await runAntWave({ ...waveBase, caste: "scout" });
345
348
 
346
- 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");
347
350
 
348
351
  // 只在完全没有 worker 任务时才重试一次
349
352
  if (workerTasks.length === 0) {
@@ -371,7 +374,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
371
374
  nest.writeTask(relayTask);
372
375
  callbacks.onPhase("scouting", "Scout relay: generating worker tasks...");
373
376
  await runAntWave({ ...waveBase, caste: "scout" });
374
- 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");
375
378
  }
376
379
 
377
380
  if (workerTasks.length === 0) {
@@ -383,12 +386,24 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
383
386
 
384
387
  // ═══ Phase 2: 工作 ═══
385
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
+
386
397
  callbacks.onPhase("working", `${workerTasks.length} tasks discovered. Dispatching worker ants...`);
387
398
  await runAntWave({ ...waveBase, caste: "worker" });
388
399
 
389
400
  // 处理工蚁产生的子任务(可能有多轮)
390
401
  let rounds = 0;
391
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
+
392
407
  const remaining = nest.getAllTasks().filter(t =>
393
408
  t.caste === "worker" && (t.status === "pending" || t.status === "blocked")
394
409
  );
@@ -398,9 +413,18 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
398
413
  await runAntWave({ ...waveBase, caste: "worker" });
399
414
  }
400
415
 
416
+ // ═══ Auto-check: run tsc before soldier review ═══
417
+ let tscPassed = true;
418
+ try {
419
+ const { execSync } = await import("node:child_process");
420
+ execSync("npx tsc --noEmit", { cwd: opts.cwd, timeout: 30000, stdio: "pipe" });
421
+ } catch {
422
+ tscPassed = false;
423
+ }
424
+
401
425
  // ═══ Phase 3: 审查 ═══
402
426
  const completedWorkerTasks = nest.getAllTasks().filter(t => t.caste === "worker" && t.status === "done");
403
- if (completedWorkerTasks.length > 0) {
427
+ if (completedWorkerTasks.length > 0 && (!tscPassed || completedWorkerTasks.length > 3)) {
404
428
  nest.updateState({ status: "reviewing" });
405
429
  callbacks.onPhase("reviewing", "Dispatching soldier ants to review changes...");
406
430
  const reviewTask = makeReviewTask(completedWorkerTasks);
@@ -57,6 +57,7 @@ export interface ParsedSubTask {
57
57
  files: string[];
58
58
  caste: AntCaste;
59
59
  priority: 1 | 2 | 3 | 4 | 5;
60
+ context?: string;
60
61
  }
61
62
 
62
63
  const CASTE_PROMPTS: Record<AntCaste, string> = {
@@ -67,6 +68,7 @@ Behavior:
67
68
  - Identify files, functions, dependencies related to the goal
68
69
  - IMPORTANT: After EACH tool call, summarize what you found so far. Do NOT wait until the end.
69
70
  - Report findings as structured intelligence for Worker Ants
71
+ - For each recommended task, include the KEY code snippets (with file:line) the worker will need — this saves workers from re-reading files
70
72
 
71
73
  Output format (MUST follow exactly):
72
74
  ## Discoveries
@@ -79,6 +81,9 @@ For each task the colony should do next:
79
81
  - files: <comma-separated file paths>
80
82
  - caste: worker
81
83
  - priority: <1-5, 1=highest>
84
+ - context: <relevant code snippets that the worker will need, with file:line references>
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.
82
87
 
83
88
  ## Warnings
84
89
  Any risks, blockers, or conflicts detected.`,
@@ -142,6 +147,9 @@ function buildPrompt(task: Task, pheromoneContext: string, castePrompt: string,
142
147
  if (task.files.length > 0) {
143
148
  prompt += `**Files scope:** ${task.files.join(", ")}\n`;
144
149
  }
150
+ if (task.context) {
151
+ prompt += `\n## Pre-loaded Context (from scout)\n${task.context}\n`;
152
+ }
145
153
  if (/[\u4e00-\u9fff]/.test(task.description)) {
146
154
  prompt += '\nIMPORTANT: Follow the language requirements specified in the task description. If the task says to write in Chinese, write in Chinese.\n';
147
155
  }
@@ -152,13 +160,18 @@ function buildPrompt(task: Task, pheromoneContext: string, castePrompt: string,
152
160
  function parseSubTasks(output: string): ParsedSubTask[] {
153
161
  const tasks: ParsedSubTask[] = [];
154
162
  const regex = /### TASK:\s*(.+)\n(?:- description:\s*(.+)\n)?(?:- files:\s*(.+)\n)?(?:- caste:\s*(\w+)\n)?(?:- priority:\s*(\d))?/g;
163
+ const taskBlocks = output.split(/(?=### TASK:)/);
155
164
  for (const m of output.matchAll(regex)) {
165
+ const block = taskBlocks.find(b => b.includes(`### TASK: ${m[1]?.trim()}`)) || "";
166
+ const ctxMatch = block.match(/- context:\s*([\s\S]*?)(?=### TASK:|## |\n\n|$)/);
167
+ const context = ctxMatch?.[1]?.trim() || undefined;
156
168
  tasks.push({
157
169
  title: m[1]?.trim() || "Untitled",
158
170
  description: m[2]?.trim() || m[1]?.trim() || "",
159
171
  files: (m[3]?.trim() || "").split(",").map(f => f.trim()).filter(Boolean),
160
172
  caste: (m[4]?.trim() as AntCaste) || "worker",
161
173
  priority: (parseInt(m[5] || "3") as 1 | 2 | 3 | 4 | 5) || 3,
174
+ context,
162
175
  });
163
176
  }
164
177
  return tasks;
@@ -239,6 +252,49 @@ function makeMinimalResourceLoader(systemPrompt: string): ResourceLoader {
239
252
  };
240
253
  }
241
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
+
242
298
  /**
243
299
  * 孵化并运行一只蚂蚁 — SDK 内嵌版
244
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 */
@@ -35,6 +36,7 @@ export interface Task {
35
36
  status: TaskStatus;
36
37
  priority: TaskPriority;
37
38
  files: string[]; // 锁定的文件
39
+ context?: string; // Scout 预加载的代码片段
38
40
  claimedBy: string | null; // ant id
39
41
  result: string | null;
40
42
  error: string | null;