oh-pi 0.1.48 → 0.1.49

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.48",
3
+ "version": "0.1.49",
4
4
  "description": "One-click setup for pi-coding-agent. Like oh-my-zsh for pi.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,10 +14,10 @@ const CPU_CORES = os.cpus().length;
14
14
 
15
15
  export function defaultConcurrency(): ConcurrencyConfig {
16
16
  return {
17
- current: 1,
17
+ current: 2,
18
18
  min: 1,
19
19
  max: Math.min(CPU_CORES, 8),
20
- optimal: 2,
20
+ optimal: 3,
21
21
  history: [],
22
22
  };
23
23
  }
@@ -68,8 +68,8 @@ export function adapt(config: ConcurrencyConfig, pendingTasks: number): Concurre
68
68
  const taskCap = Math.min(pendingTasks, config.max);
69
69
 
70
70
  if (samples.length < 3) {
71
- // 冷启动:保守起步
72
- next.current = Math.min(2, taskCap);
71
+ // 冷启动:直接给一半 max,快速利用并发
72
+ next.current = Math.min(Math.ceil(config.max / 2), taskCap);
73
73
  return next;
74
74
  }
75
75
 
@@ -12,7 +12,7 @@
12
12
  import { readFileSync, appendFileSync, existsSync } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
15
- import { Text, Container, Spacer } from "@mariozechner/pi-tui";
15
+ import { Text, Container, Spacer, matchesKey } from "@mariozechner/pi-tui";
16
16
  import { Type } from "@sinclair/typebox";
17
17
  import { runColony, type QueenCallbacks } from "./queen.js";
18
18
  import type { ColonyState, ColonyMetrics, AntStreamEvent } from "./types.js";
@@ -46,30 +46,6 @@ function casteIcon(caste: string): string {
46
46
  return caste === "scout" ? "🔍" : caste === "soldier" ? "🛡️" : "⚒️";
47
47
  }
48
48
 
49
- function progressBar(done: number, total: number, width: number, theme: any): string {
50
- if (total === 0) return "";
51
- const pct = Math.min(done / total, 1);
52
- const filled = Math.round(pct * width);
53
- const empty = width - filled;
54
- return theme.fg("success", "█".repeat(filled)) + theme.fg("muted", "░".repeat(empty)) + " " + theme.fg("accent", `${done}/${total}`);
55
- }
56
-
57
- function phasePipeline(status: string, theme: any): string {
58
- const phases = [
59
- { key: "scouting", icon: "🔍", label: "Scout" },
60
- { key: "working", icon: "⚒️", label: "Work" },
61
- { key: "reviewing", icon: "🛡️", label: "Review" },
62
- { key: "done", icon: "✅", label: "Done" },
63
- ];
64
- const idx = phases.findIndex(p => p.key === status);
65
- return phases.map((p, i) => {
66
- const label = `${p.icon} ${p.label}`;
67
- if (i < idx) return theme.fg("success", label);
68
- if (i === idx) return theme.fg("accent", theme.bold(label));
69
- return theme.fg("muted", label);
70
- }).join(theme.fg("muted", " → "));
71
- }
72
-
73
49
  // ═══ Background colony state ═══
74
50
 
75
51
  interface AntStreamState {
@@ -94,64 +70,41 @@ export default function antColonyExtension(pi: ExtensionAPI) {
94
70
  // 当前运行中的后台蚁群(同时只允许一个)
95
71
  let activeColony: BackgroundColony | null = null;
96
72
 
97
- // ─── Widget/Status 渲染 ───
73
+ // ─── Status 渲染 ───
98
74
 
99
75
  let lastRender = 0;
100
76
  const throttledRender = () => {
101
77
  const now = Date.now();
102
- if (now - lastRender < 200) return;
78
+ if (now - lastRender < 500) return;
103
79
  lastRender = now;
104
- renderWidget();
105
- renderStatus();
80
+ pi.events.emit("ant-colony:render");
106
81
  };
107
82
 
108
- const renderWidget = () => {
109
- if (!activeColony) return;
110
- const { state, phase, antStreams } = activeColony;
111
- const streams = Array.from(antStreams.values());
112
- const lines: string[] = [];
113
-
114
- const elapsed = state ? formatDuration(Date.now() - state.createdAt) : "0s";
115
- const cost = state ? formatCost(state.metrics.totalCost) : "$0";
116
- lines.push(`🐜 Colony: ${phase} │ ${elapsed} │ ${cost}`);
117
-
118
- if (state && state.metrics.tasksTotal > 0) {
119
- const m = state.metrics;
120
- const pct = Math.round((m.tasksDone / m.tasksTotal) * 100);
121
- const filled = Math.round(pct / 5);
122
- lines.push(` ${"█".repeat(filled)}${"░".repeat(20 - filled)} ${m.tasksDone}/${m.tasksTotal} (${pct}%)`);
123
- }
124
-
125
- for (const s of streams.slice(-4)) {
126
- const icon = casteIcon(s.caste);
127
- const line = s.lastLine.length > 60 ? s.lastLine.slice(0, 57) + "..." : s.lastLine;
128
- lines.push(` ${icon} ${s.antId.slice(0, 15)} ▸ ${line || "..."}`);
129
- }
130
-
131
- pi.events.emit("ant-colony:widget", lines);
132
- };
133
-
134
- const renderStatus = () => {
135
- if (!activeColony) return;
136
- const { state, antStreams } = activeColony;
137
- if (!state) return;
138
- const m = state.metrics;
139
- const active = antStreams.size;
140
- pi.events.emit("ant-colony:status",
141
- `🐜 ${statusIcon(state.status)} ${m.tasksDone}/${m.tasksTotal} tasks │ ${active} active │ ${formatCost(m.totalCost)}`
142
- );
143
- };
144
-
145
- // 监听自己的事件来更新 UI(确保在有 ctx 的上下文中)
83
+ // 监听事件来更新 UI(确保在有 ctx 的上下文中)
146
84
  pi.on("session_start", async (_event, ctx) => {
147
- pi.events.on("ant-colony:widget", (lines: string[]) => {
148
- ctx.ui.setWidget("ant-colony", lines);
149
- });
150
- pi.events.on("ant-colony:status", (status: string) => {
151
- ctx.ui.setStatus("ant-colony", status);
85
+ pi.events.on("ant-colony:render", () => {
86
+ if (!activeColony) return;
87
+ const { state, antStreams } = activeColony;
88
+ const active = antStreams.size;
89
+ const elapsed = state ? formatDuration(Date.now() - state.createdAt) : "0s";
90
+ const m = state?.metrics;
91
+ 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
+
96
+ const parts = [`🐜 ${statusIcon(colonyStatus)}`];
97
+ if (m) parts.push(`${m.tasksDone}/${m.tasksTotal}`);
98
+ parts.push(`${active}⚡`);
99
+ parts.push(`${turns}↻`);
100
+ parts.push(formatTokens(outTok) + "↑");
101
+ if (m) parts.push(formatCost(m.totalCost));
102
+ parts.push(elapsed);
103
+
104
+ ctx.ui.setStatus("ant-colony", parts.join(" │ "));
152
105
  });
106
+
153
107
  pi.events.on("ant-colony:clear-ui", () => {
154
- ctx.ui.setWidget("ant-colony", undefined);
155
108
  ctx.ui.setStatus("ant-colony", undefined);
156
109
  });
157
110
  pi.events.on("ant-colony:notify", (data: { msg: string; level: "info" | "success" | "warning" | "error" }) => {
@@ -361,7 +314,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
361
314
  // 注入结果到对话
362
315
  pi.sendMessage({
363
316
  customType: "ant-colony-report",
364
- content: report,
317
+ content: `[COLONY_SIGNAL:COMPLETE]\n${report}`,
365
318
  display: true,
366
319
  }, { triggerTurn: true, deliverAs: "followUp" });
367
320
 
@@ -373,6 +326,11 @@ export default function antColonyExtension(pi: ExtensionAPI) {
373
326
  pi.events.emit("ant-colony:clear-ui");
374
327
  activeColony = null;
375
328
  pi.events.emit("ant-colony:notify", { msg: `🐜 Colony crashed: ${e}`, level: "error" });
329
+ pi.sendMessage({
330
+ customType: "ant-colony-report",
331
+ content: `[COLONY_SIGNAL:FAILED]\n## 🐜 Colony Crashed\n${e}`,
332
+ display: true,
333
+ }, { triggerTurn: true, deliverAs: "followUp" });
376
334
  });
377
335
  }
378
336
 
@@ -390,7 +348,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
390
348
  } catch { /* ignore */ }
391
349
 
392
350
  const colonyStatus = activeColony
393
- ? `\n[COLONY RUNNING] A colony is already running in the background. Do NOT launch another. Use /colony-stop to cancel it first.`
351
+ ? `\n[COLONY_SIGNAL:RUNNING] A colony is currently running in the background for goal: "${activeColony.goal.slice(0, 100)}". Do NOT launch another colony. Do NOT check progress or assume failure. You will receive [COLONY_SIGNAL:COMPLETE] automatically when it finishes. If the user asks about colony status, tell them it's still running. Use /colony-stop to cancel.`
394
352
  : "";
395
353
 
396
354
  return {
@@ -405,8 +363,17 @@ You have the ant_colony tool. Use it automatically when:
405
363
  Do NOT ask for confirmation. Call ant_colony directly with a clear goal.
406
364
  For simple single-file tasks, work directly without the colony.
407
365
 
408
- The colony runs in the BACKGROUND — you can continue chatting while it works.
409
- Results are injected into the conversation when the colony finishes.
366
+ [COLONY SIGNAL PROTOCOL]
367
+ The colony communicates via signals. You MUST obey these:
368
+ - [COLONY_SIGNAL:LAUNCHED] — Colony started. Do NOT poll, check, or assume failure. Wait for completion signal.
369
+ - [COLONY_SIGNAL:RUNNING] — Colony is active. Do NOT launch another or check progress.
370
+ - [COLONY_SIGNAL:COMPLETE] — Colony finished. Review the report and summarize results to the user.
371
+ - [COLONY_SIGNAL:FAILED] — Colony crashed. Report the error to the user.
372
+
373
+ After launching a colony, your ONLY correct behavior is:
374
+ 1. Tell the user the colony is running
375
+ 2. Continue chatting about OTHER topics if the user asks
376
+ 3. Wait for [COLONY_SIGNAL:COMPLETE] or [COLONY_SIGNAL:FAILED] — do NOT guess the outcome
410
377
  ${modelList ? `
411
378
  [COLONY MODEL SELECTION]
412
379
  Available models: ${modelList}
@@ -482,7 +449,7 @@ Strategy for choosing per-caste models:
482
449
  launchBackgroundColony(colonyParams);
483
450
 
484
451
  return {
485
- content: [{ type: "text", text: `🐜 Colony launched in background!\n\n**Goal:** ${params.goal}\n\nThe colony is now running. You can continue chatting results will be injected when it finishes.\n\nUse \`/colony-stop\` to cancel, \`/colony-status\` to check progress.` }],
452
+ content: [{ type: "text", text: `[COLONY_SIGNAL:LAUNCHED]\n🐜 Colony launched in background.\nGoal: ${params.goal}\n\n⚠️ IMPORTANT: The colony is now running autonomously. Do NOT check progress, do NOT ask about status, do NOT assume failure. You will receive a [COLONY_SIGNAL:COMPLETE] message automatically when it finishes. Continue chatting about other topics or wait silently.` }],
486
453
  };
487
454
  },
488
455
 
@@ -508,7 +475,7 @@ Strategy for choosing per-caste models:
508
475
  ));
509
476
  if (activeColony) {
510
477
  container.addChild(new Text(theme.fg("dim", ` Goal: ${activeColony.goal.slice(0, 70)}`), 0, 0));
511
- container.addChild(new Text(theme.fg("muted", ` Use /colony-status or check the widget above`), 0, 0));
478
+ container.addChild(new Text(theme.fg("muted", ` Ctrl+Shift+A for details │ /colony-stop to cancel`), 0, 0));
512
479
  }
513
480
  return container;
514
481
  },
@@ -550,6 +517,99 @@ Strategy for choosing per-caste models:
550
517
  return container;
551
518
  });
552
519
 
520
+ // ═══ Shortcut: Ctrl+Shift+A 展开蚁群详情 ═══
521
+ pi.registerShortcut("ctrl+shift+a", {
522
+ description: "Show ant colony details",
523
+ async handler(ctx) {
524
+ if (!activeColony) {
525
+ ctx.ui.notify("No colony is currently running.", "info");
526
+ return;
527
+ }
528
+
529
+ await ctx.ui.custom<void>((tui, theme, _kb, done) => {
530
+ let cachedWidth: number | undefined;
531
+ let cachedLines: string[] | undefined;
532
+
533
+ const buildLines = (width: number): string[] => {
534
+ const c = activeColony;
535
+ if (!c) return [theme.fg("muted", " No colony running.")];
536
+
537
+ const lines: string[] = [];
538
+ const w = width - 2; // padding
539
+
540
+ // ── Header ──
541
+ const elapsed = c.state ? formatDuration(Date.now() - c.state.createdAt) : "0s";
542
+ const cost = c.state ? formatCost(c.state.metrics.totalCost) : "$0";
543
+ lines.push(theme.fg("accent", theme.bold(` 🐜 Colony Details`)) + theme.fg("muted", ` │ ${elapsed} │ ${cost}`));
544
+ lines.push(theme.fg("dim", ` Goal: ${c.goal.slice(0, w - 8)}`));
545
+ lines.push("");
546
+
547
+ // ── Tasks ──
548
+ const tasks = c.state?.tasks || [];
549
+ if (tasks.length > 0) {
550
+ lines.push(theme.fg("accent", " Tasks"));
551
+ for (const t of tasks.slice(0, 15)) {
552
+ const icon = t.status === "done" ? theme.fg("success", "✓")
553
+ : t.status === "failed" ? theme.fg("error", "✗")
554
+ : t.status === "active" ? theme.fg("warning", "●")
555
+ : theme.fg("dim", "○");
556
+ const dur = t.finishedAt && t.startedAt ? theme.fg("dim", ` ${formatDuration(t.finishedAt - t.startedAt)}`) : "";
557
+ lines.push(` ${icon} ${casteIcon(t.caste)} ${theme.fg("text", t.title.slice(0, w - 12))}${dur}`);
558
+ }
559
+ if (tasks.length > 15) lines.push(theme.fg("muted", ` ⋯ +${tasks.length - 15} more`));
560
+ lines.push("");
561
+ }
562
+
563
+ // ── Active Ants ──
564
+ const streams = Array.from(c.antStreams.values());
565
+ if (streams.length > 0) {
566
+ lines.push(theme.fg("accent", " Active Ants"));
567
+ for (const s of streams) {
568
+ const line = s.lastLine.length > w - 20 ? s.lastLine.slice(0, w - 23) + "..." : s.lastLine;
569
+ lines.push(` ${casteIcon(s.caste)} ${theme.fg("accent", s.antId.slice(0, 14))} ${theme.fg("dim", `${s.tokens}tok`)} ${theme.fg("muted", "▸")} ${line}`);
570
+ }
571
+ lines.push("");
572
+ }
573
+
574
+ // ── Log (last 8) ──
575
+ if (c.log.length > 0) {
576
+ lines.push(theme.fg("accent", " Log"));
577
+ for (const l of c.log.slice(-8)) {
578
+ lines.push(theme.fg("dim", ` ${l.slice(0, w - 2)}`));
579
+ }
580
+ }
581
+
582
+ lines.push("");
583
+ lines.push(theme.fg("muted", " esc close"));
584
+ return lines;
585
+ };
586
+
587
+ // 定时刷新
588
+ const timer = setInterval(() => {
589
+ cachedWidth = undefined;
590
+ cachedLines = undefined;
591
+ tui.requestRender();
592
+ }, 1000);
593
+
594
+ return {
595
+ render(width: number): string[] {
596
+ if (cachedLines && cachedWidth === width) return cachedLines;
597
+ cachedLines = buildLines(width);
598
+ cachedWidth = width;
599
+ return cachedLines;
600
+ },
601
+ invalidate() { cachedWidth = undefined; cachedLines = undefined; },
602
+ handleInput(data: string) {
603
+ if (matchesKey(data, "escape")) {
604
+ clearInterval(timer);
605
+ done(undefined);
606
+ }
607
+ },
608
+ };
609
+ }, { overlay: true, overlayOptions: { anchor: "center", width: "80%", maxHeight: "80%" } });
610
+ },
611
+ });
612
+
553
613
  // ═══ Command: /colony-stop ═══
554
614
  pi.registerCommand("colony-stop", {
555
615
  description: "Stop the running background colony",
@@ -21,6 +21,7 @@ export class Nest {
21
21
  private pheromoneCache: Pheromone[] = [];
22
22
  private pheromoneOffset: number = 0;
23
23
  private taskCache: Map<string, Task> = new Map();
24
+ private stateCache: ColonyState | null = null;
24
25
 
25
26
  constructor(private cwd: string, private colonyId: string) {
26
27
  this.dir = path.join(cwd, ".ant-colony", colonyId);
@@ -35,13 +36,16 @@ export class Nest {
35
36
 
36
37
  init(state: ColonyState): void {
37
38
  this.writeJson(this.stateFile, state);
39
+ this.stateCache = state;
38
40
  this.taskCache.clear();
39
41
  for (const t of state.tasks) this.writeTask(t);
40
42
  }
41
43
 
42
44
  getState(): ColonyState {
43
- const base = this.readJson<ColonyState>(this.stateFile);
44
- // tasks/ 目录重建最新任务状态(原子性保证)
45
+ if (!this.stateCache) {
46
+ this.stateCache = this.readJson<ColonyState>(this.stateFile);
47
+ }
48
+ const base = { ...this.stateCache };
45
49
  base.tasks = this.getAllTasks();
46
50
  base.pheromones = this.getAllPheromones();
47
51
  return base;
@@ -49,9 +53,11 @@ export class Nest {
49
53
 
50
54
  updateState(patch: Partial<Pick<ColonyState, "status" | "concurrency" | "metrics" | "ants" | "finishedAt">>): void {
51
55
  this.withStateLock(() => {
52
- const state = this.readJson<ColonyState>(this.stateFile);
53
- Object.assign(state, patch);
54
- this.writeJson(this.stateFile, state);
56
+ if (!this.stateCache) {
57
+ this.stateCache = this.readJson<ColonyState>(this.stateFile);
58
+ }
59
+ Object.assign(this.stateCache, patch);
60
+ this.writeJson(this.stateFile, this.stateCache);
55
61
  });
56
62
  }
57
63
 
@@ -186,11 +192,13 @@ export class Nest {
186
192
 
187
193
  updateAnt(ant: Ant): void {
188
194
  this.withStateLock(() => {
189
- const state = this.readJson<ColonyState>(this.stateFile);
190
- const idx = state.ants.findIndex(a => a.id === ant.id);
191
- if (idx >= 0) state.ants[idx] = ant;
192
- else state.ants.push(ant);
193
- this.writeJson(this.stateFile, state);
195
+ if (!this.stateCache) {
196
+ this.stateCache = this.readJson<ColonyState>(this.stateFile);
197
+ }
198
+ const idx = this.stateCache.ants.findIndex(a => a.id === ant.id);
199
+ if (idx >= 0) this.stateCache.ants[idx] = ant;
200
+ else this.stateCache.ants.push(ant);
201
+ this.writeJson(this.stateFile, this.stateCache);
194
202
  });
195
203
  }
196
204
 
@@ -198,12 +206,14 @@ export class Nest {
198
206
 
199
207
  recordSample(sample: ConcurrencySample): void {
200
208
  this.withStateLock(() => {
201
- const state = this.readJson<ColonyState>(this.stateFile);
202
- state.concurrency.history.push(sample);
203
- if (state.concurrency.history.length > 30) {
204
- state.concurrency.history = state.concurrency.history.slice(-30);
209
+ if (!this.stateCache) {
210
+ this.stateCache = this.readJson<ColonyState>(this.stateFile);
211
+ }
212
+ this.stateCache.concurrency.history.push(sample);
213
+ if (this.stateCache.concurrency.history.length > 30) {
214
+ this.stateCache.concurrency.history = this.stateCache.concurrency.history.slice(-30);
205
215
  }
206
- this.writeJson(this.stateFile, state);
216
+ this.writeJson(this.stateFile, this.stateCache);
207
217
  });
208
218
  }
209
219
 
@@ -236,9 +236,9 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
236
236
  }
237
237
  }
238
238
 
239
- // 自适应并发(每 5000ms 采样一次)
239
+ // 自适应并发(每 2000ms 采样一次)
240
240
  const now = Date.now();
241
- if (now - lastSampleTime >= 5000) {
241
+ if (now - lastSampleTime >= 2000) {
242
242
  lastSampleTime = now;
243
243
  const completedRecently = state.tasks.filter(t =>
244
244
  t.status === "done" && t.finishedAt && t.finishedAt > now - 120000
@@ -260,7 +260,7 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
260
260
 
261
261
  if (slotsAvailable === 0) {
262
262
  // 等待一下再检查
263
- await new Promise(r => setTimeout(r, 2000));
263
+ await new Promise(r => setTimeout(r, 500));
264
264
  continue;
265
265
  }
266
266
 
@@ -336,51 +336,39 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
336
336
  };
337
337
 
338
338
  try {
339
- // ═══ Phase 1: 侦察(接力机制) ═══
340
- let scoutAttempt = 0;
341
- const MAX_SCOUT_RETRIES = 2;
342
- let workerTasks: Task[] = [];
339
+ // ═══ Phase 1: 侦察(快速单次,不再多轮接力) ═══
340
+ callbacks.onPhase("scouting", "Dispatching scout ant to explore codebase...");
341
+ await runAntWave({ ...waveBase, caste: "scout" });
343
342
 
344
- while (scoutAttempt <= MAX_SCOUT_RETRIES) {
345
- callbacks.onPhase("scouting", scoutAttempt === 0
346
- ? "Dispatching scout ants to explore codebase..."
347
- : `Scout relay ${scoutAttempt}/${MAX_SCOUT_RETRIES} (building on previous discoveries)...`);
343
+ let workerTasks = nest.getAllTasks().filter(t => t.caste === "worker" && t.status === "pending");
348
344
 
345
+ // 只在完全没有 worker 任务时才重试一次
346
+ if (workerTasks.length === 0) {
347
+ const pheromones = nest.getAllPheromones();
348
+ const hasDiscoveries = pheromones.some(p => p.type === "discovery");
349
+ const relayTask: Task = {
350
+ id: makeTaskId(),
351
+ parentId: null,
352
+ title: "Scout relay: generate worker tasks",
353
+ description: hasDiscoveries
354
+ ? `Previous scout found information but didn't generate worker tasks. Generate concrete worker tasks based on discoveries.\n\nGoal:\n${opts.goal}`
355
+ : `Explore the codebase for this goal and generate worker tasks:\n\n${opts.goal}`,
356
+ caste: "scout",
357
+ status: "pending",
358
+ priority: 1,
359
+ files: [],
360
+ claimedBy: null,
361
+ result: null,
362
+ error: null,
363
+ spawnedTasks: [],
364
+ createdAt: Date.now(),
365
+ startedAt: null,
366
+ finishedAt: null,
367
+ };
368
+ nest.writeTask(relayTask);
369
+ callbacks.onPhase("scouting", "Scout relay: generating worker tasks...");
349
370
  await runAntWave({ ...waveBase, caste: "scout" });
350
-
351
371
  workerTasks = nest.getAllTasks().filter(t => t.caste === "worker" && t.status === "pending");
352
- if (workerTasks.length > 0) break;
353
-
354
- scoutAttempt++;
355
- if (scoutAttempt <= MAX_SCOUT_RETRIES) {
356
- // 接力:检查是否有信息素(前一只 scout 的部分发现)
357
- const pheromones = nest.getAllPheromones();
358
- const hasDiscoveries = pheromones.some(p => p.type === "discovery");
359
-
360
- // 创建接力 scout 任务(而非重置旧任务)
361
- const relayDescription = hasDiscoveries
362
- ? `Continue exploring the codebase. Previous scouts made partial discoveries (see pheromone trail). Focus on areas NOT yet explored and generate worker tasks.\n\nOriginal goal:\n${opts.goal}`
363
- : `Explore the codebase and identify all files, modules, and dependencies relevant to this goal:\n\n${opts.goal}\n\nBe thorough. The colony depends on your intelligence.`;
364
-
365
- const relayTask: Task = {
366
- id: makeTaskId(),
367
- parentId: null,
368
- title: hasDiscoveries ? "Scout relay: continue exploration" : "Scout: explore codebase for goal",
369
- description: relayDescription,
370
- caste: "scout",
371
- status: "pending",
372
- priority: 1,
373
- files: [],
374
- claimedBy: null,
375
- result: null,
376
- error: null,
377
- spawnedTasks: [],
378
- createdAt: Date.now(),
379
- startedAt: null,
380
- finishedAt: null,
381
- };
382
- nest.writeTask(relayTask);
383
- }
384
372
  }
385
373
 
386
374
  if (workerTasks.length === 0) {