oh-pi 0.1.0

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.
Files changed (65) hide show
  1. package/README.md +180 -0
  2. package/dist/bin/oh-pi.d.ts +2 -0
  3. package/dist/bin/oh-pi.js +3 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +62 -0
  6. package/dist/tui/agents-select.d.ts +1 -0
  7. package/dist/tui/agents-select.js +18 -0
  8. package/dist/tui/confirm-apply.d.ts +3 -0
  9. package/dist/tui/confirm-apply.js +90 -0
  10. package/dist/tui/extension-select.d.ts +1 -0
  11. package/dist/tui/extension-select.js +17 -0
  12. package/dist/tui/keybinding-select.d.ts +1 -0
  13. package/dist/tui/keybinding-select.js +16 -0
  14. package/dist/tui/mode-select.d.ts +3 -0
  15. package/dist/tui/mode-select.js +16 -0
  16. package/dist/tui/preset-select.d.ts +5 -0
  17. package/dist/tui/preset-select.js +81 -0
  18. package/dist/tui/provider-setup.d.ts +2 -0
  19. package/dist/tui/provider-setup.js +68 -0
  20. package/dist/tui/theme-select.d.ts +1 -0
  21. package/dist/tui/theme-select.js +16 -0
  22. package/dist/tui/welcome.d.ts +2 -0
  23. package/dist/tui/welcome.js +25 -0
  24. package/dist/types.d.ts +31 -0
  25. package/dist/types.js +41 -0
  26. package/dist/utils/detect.d.ts +11 -0
  27. package/dist/utils/detect.js +56 -0
  28. package/dist/utils/install.d.ts +5 -0
  29. package/dist/utils/install.js +130 -0
  30. package/package.json +54 -0
  31. package/pi-package/agents/colony-operator.md +32 -0
  32. package/pi-package/agents/data-ai-engineer.md +24 -0
  33. package/pi-package/agents/fullstack-developer.md +24 -0
  34. package/pi-package/agents/general-developer.md +22 -0
  35. package/pi-package/agents/security-researcher.md +29 -0
  36. package/pi-package/extensions/ant-colony/README.md +117 -0
  37. package/pi-package/extensions/ant-colony/concurrency.ts +115 -0
  38. package/pi-package/extensions/ant-colony/index.ts +338 -0
  39. package/pi-package/extensions/ant-colony/nest.ts +196 -0
  40. package/pi-package/extensions/ant-colony/queen.ts +356 -0
  41. package/pi-package/extensions/ant-colony/spawner.ts +328 -0
  42. package/pi-package/extensions/ant-colony/types.ts +117 -0
  43. package/pi-package/extensions/auto-session-name.ts +29 -0
  44. package/pi-package/extensions/git-guard.ts +45 -0
  45. package/pi-package/extensions/safe-guard.ts +46 -0
  46. package/pi-package/prompts/commit.md +18 -0
  47. package/pi-package/prompts/document.md +14 -0
  48. package/pi-package/prompts/explain.md +13 -0
  49. package/pi-package/prompts/fix.md +11 -0
  50. package/pi-package/prompts/optimize.md +14 -0
  51. package/pi-package/prompts/pr.md +24 -0
  52. package/pi-package/prompts/refactor.md +14 -0
  53. package/pi-package/prompts/review.md +18 -0
  54. package/pi-package/prompts/security.md +16 -0
  55. package/pi-package/prompts/test.md +12 -0
  56. package/pi-package/skills/ant-colony/SKILL.md +59 -0
  57. package/pi-package/skills/debug-helper/SKILL.md +43 -0
  58. package/pi-package/skills/git-workflow/SKILL.md +48 -0
  59. package/pi-package/skills/quick-setup/SKILL.md +44 -0
  60. package/pi-package/themes/catppuccin-mocha.json +31 -0
  61. package/pi-package/themes/cyberpunk.json +66 -0
  62. package/pi-package/themes/gruvbox-dark.json +29 -0
  63. package/pi-package/themes/nord.json +29 -0
  64. package/pi-package/themes/oh-p-dark.json +69 -0
  65. package/pi-package/themes/tokyo-night.json +29 -0
@@ -0,0 +1,356 @@
1
+ /**
2
+ * 蚁后 (Queen) — 蚁群调度核心
3
+ *
4
+ * 生命周期:
5
+ * 1. 接收目标 → 派侦察蚁探路
6
+ * 2. 侦察蚁返回 → 根据发现生成任务池
7
+ * 3. 自适应派工蚁执行任务
8
+ * 4. 任务完成 → 派兵蚁审查
9
+ * 5. 有问题 → 生成修复任务回到步骤3
10
+ * 6. 全部通过 → 汇总报告
11
+ *
12
+ * 调度循环模拟真实蚁群:蚂蚁不断出巢→觅食→回巢→再出巢
13
+ */
14
+
15
+ import type {
16
+ ColonyState, Task, Ant, AntCaste, ColonyMetrics,
17
+ ConcurrencyConfig, TaskPriority,
18
+ } from "./types.js";
19
+ import { DEFAULT_ANT_CONFIGS } from "./types.js";
20
+ import { Nest } from "./nest.js";
21
+ import { spawnAnt, makeTaskId } from "./spawner.js";
22
+ import { adapt, sampleSystem, defaultConcurrency } from "./concurrency.js";
23
+
24
+ export interface QueenCallbacks {
25
+ onPhase(phase: ColonyState["status"], detail: string): void;
26
+ onAntSpawn(ant: Ant, task: Task): void;
27
+ onAntDone(ant: Ant, task: Task, output: string): void;
28
+ onProgress(metrics: ColonyMetrics): void;
29
+ onComplete(state: ColonyState): void;
30
+ }
31
+
32
+ export interface QueenOptions {
33
+ cwd: string;
34
+ goal: string;
35
+ maxAnts?: number;
36
+ signal?: AbortSignal;
37
+ callbacks: QueenCallbacks;
38
+ }
39
+
40
+ function makeColonyId(): string {
41
+ return `colony-${Date.now().toString(36)}`;
42
+ }
43
+
44
+ function makeInitialScoutTask(goal: string): Task {
45
+ return {
46
+ id: makeTaskId(),
47
+ parentId: null,
48
+ title: "Scout: explore codebase for goal",
49
+ description: `Explore the codebase and identify all files, modules, and dependencies relevant to this goal:\n\n${goal}\n\nBe thorough. The colony depends on your intelligence.`,
50
+ caste: "scout",
51
+ status: "pending",
52
+ priority: 1,
53
+ files: [],
54
+ claimedBy: null,
55
+ result: null,
56
+ error: null,
57
+ spawnedTasks: [],
58
+ createdAt: Date.now(),
59
+ startedAt: null,
60
+ finishedAt: null,
61
+ };
62
+ }
63
+
64
+ function childTaskFromParsed(
65
+ parentId: string,
66
+ parsed: { title: string; description: string; files: string[]; caste: AntCaste; priority: TaskPriority },
67
+ ): Task {
68
+ return {
69
+ id: makeTaskId(),
70
+ parentId,
71
+ title: parsed.title,
72
+ description: parsed.description,
73
+ caste: parsed.caste,
74
+ status: "pending",
75
+ priority: parsed.priority,
76
+ files: parsed.files,
77
+ claimedBy: null,
78
+ result: null,
79
+ error: null,
80
+ spawnedTasks: [],
81
+ createdAt: Date.now(),
82
+ startedAt: null,
83
+ finishedAt: null,
84
+ };
85
+ }
86
+
87
+ function makeReviewTask(completedTasks: Task[]): Task {
88
+ const files = [...new Set(completedTasks.flatMap(t => t.files))];
89
+ return {
90
+ id: makeTaskId(),
91
+ parentId: null,
92
+ title: "Soldier: review all changes",
93
+ description: `Review all changes made by worker ants. Files changed:\n${files.map(f => `- ${f}`).join("\n")}`,
94
+ caste: "soldier",
95
+ status: "pending",
96
+ priority: 1,
97
+ files,
98
+ claimedBy: null,
99
+ result: null,
100
+ error: null,
101
+ spawnedTasks: [],
102
+ createdAt: Date.now(),
103
+ startedAt: null,
104
+ finishedAt: null,
105
+ };
106
+ }
107
+
108
+ function updateMetrics(nest: Nest): ColonyMetrics {
109
+ const tasks = nest.getAllTasks();
110
+ const state = nest.getState();
111
+ const now = Date.now();
112
+ const elapsed = (now - state.metrics.startTime) / 60000; // minutes
113
+
114
+ const metrics: ColonyMetrics = {
115
+ tasksTotal: tasks.length,
116
+ tasksDone: tasks.filter(t => t.status === "done").length,
117
+ tasksFailed: tasks.filter(t => t.status === "failed").length,
118
+ antsSpawned: state.ants.length,
119
+ totalCost: state.ants.reduce((s, a) => s + a.usage.cost, 0),
120
+ totalTokens: state.ants.reduce((s, a) => s + a.usage.input + a.usage.output, 0),
121
+ startTime: state.metrics.startTime,
122
+ throughputHistory: [
123
+ ...state.metrics.throughputHistory,
124
+ elapsed > 0 ? tasks.filter(t => t.status === "done").length / elapsed : 0,
125
+ ].slice(-20),
126
+ };
127
+
128
+ nest.updateState({ metrics });
129
+ return metrics;
130
+ }
131
+
132
+ /**
133
+ * 并发执行一批蚂蚁,自适应调节并发度
134
+ */
135
+ async function runAntWave(
136
+ nest: Nest,
137
+ cwd: string,
138
+ caste: AntCaste,
139
+ signal: AbortSignal | undefined,
140
+ callbacks: QueenCallbacks,
141
+ ): Promise<void> {
142
+ const config = DEFAULT_ANT_CONFIGS[caste];
143
+
144
+ let backoffMs = 0; // 429 退避时间
145
+
146
+ const runOne = async (): Promise<"done" | "empty" | "rate_limited"> => {
147
+ const task = nest.nextPendingTask(caste);
148
+ if (!task) return "empty";
149
+ if (!nest.claimTask(task.id, "queen")) return "empty";
150
+
151
+ const ant: Ant = {
152
+ id: "", caste, status: "idle", taskId: task.id,
153
+ pid: null, usage: { input: 0, output: 0, cost: 0, turns: 0 },
154
+ startedAt: Date.now(), finishedAt: null,
155
+ };
156
+ callbacks.onAntSpawn(ant, task);
157
+
158
+ try {
159
+ const result = await spawnAnt(cwd, nest, task, config, signal);
160
+ callbacks.onAntDone(result.ant, task, result.output);
161
+
162
+ if (result.rateLimited) {
163
+ return "rate_limited";
164
+ }
165
+
166
+ // 蚂蚁产生的子任务加入巢穴
167
+ for (const sub of result.newTasks) {
168
+ // 检查文件锁冲突
169
+ const allTasks = nest.getAllTasks();
170
+ const conflicting = allTasks.find(t =>
171
+ t.status === "active" &&
172
+ t.files.some(f => sub.files.includes(f))
173
+ );
174
+ const child = childTaskFromParsed(task.id, sub);
175
+ if (conflicting) {
176
+ child.status = "blocked";
177
+ }
178
+ nest.addSubTask(task.id, child);
179
+ }
180
+
181
+ // 更新指标
182
+ const metrics = updateMetrics(nest);
183
+ callbacks.onProgress(metrics);
184
+
185
+ return "done";
186
+ } catch (e) {
187
+ nest.updateTaskStatus(task.id, "failed", undefined, String(e));
188
+ return "done";
189
+ }
190
+ };
191
+
192
+ // 调度循环:持续派蚂蚁直到没有待处理任务
193
+ while (!signal?.aborted) {
194
+ const state = nest.getState();
195
+ const pending = state.tasks.filter(t => t.status === "pending" && t.caste === caste);
196
+ if (pending.length === 0) break;
197
+
198
+ // 429 退避:等待后恢复
199
+ if (backoffMs > 0) {
200
+ callbacks.onPhase("working", `Rate limited (429). Backing off ${Math.round(backoffMs / 1000)}s...`);
201
+ await new Promise(r => setTimeout(r, backoffMs));
202
+ backoffMs = 0;
203
+ }
204
+
205
+ // 解除 blocked 任务(如果锁定文件已释放)
206
+ const activeTasks = state.tasks.filter(t => t.status === "active");
207
+ const activeFiles = new Set(activeTasks.flatMap(t => t.files));
208
+ for (const t of state.tasks.filter(t => t.status === "blocked" && t.caste === caste)) {
209
+ if (!t.files.some(f => activeFiles.has(f))) {
210
+ nest.updateTaskStatus(t.id, "pending");
211
+ }
212
+ }
213
+
214
+ // 自适应并发
215
+ const completedRecently = state.tasks.filter(t =>
216
+ t.status === "done" && t.finishedAt && t.finishedAt > Date.now() - 120000
217
+ ).length;
218
+ const sample = sampleSystem(
219
+ state.ants.filter(a => a.status === "working").length,
220
+ completedRecently,
221
+ 2,
222
+ );
223
+ nest.recordSample(sample);
224
+
225
+ const concurrency = adapt(state.concurrency, pending.length);
226
+ nest.updateState({ concurrency });
227
+
228
+ // 派出蚂蚁(并发数由 adapt 决定)
229
+ const activeAnts = state.ants.filter(a => a.status === "working").length;
230
+ const slotsAvailable = Math.max(0, concurrency.current - activeAnts);
231
+
232
+ if (slotsAvailable === 0) {
233
+ // 等待一下再检查
234
+ await new Promise(r => setTimeout(r, 2000));
235
+ continue;
236
+ }
237
+
238
+ const batch = Math.min(slotsAvailable, pending.length);
239
+ const promises: Promise<"done" | "empty" | "rate_limited">[] = [];
240
+ for (let i = 0; i < batch; i++) {
241
+ promises.push(runOne());
242
+ }
243
+ const results = await Promise.all(promises);
244
+
245
+ // 429 处理:任何蚂蚁遇到限流 → 降低并发 + 指数退避
246
+ if (results.includes("rate_limited")) {
247
+ const cur = nest.getState().concurrency;
248
+ const reduced = Math.max(cur.min, Math.floor(cur.current / 2));
249
+ nest.updateState({ concurrency: { ...cur, current: reduced } });
250
+ // 指数退避:15s → 30s → 60s,上限 60s
251
+ backoffMs = backoffMs === 0 ? 15000 : Math.min(backoffMs * 2, 60000);
252
+ } else {
253
+ // 成功时逐步恢复退避计数
254
+ backoffMs = 0;
255
+ }
256
+ }
257
+ }
258
+
259
+ /**
260
+ * 蚁后主循环
261
+ */
262
+ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
263
+ const colonyId = makeColonyId();
264
+ const nest = new Nest(opts.cwd, colonyId);
265
+
266
+ const initialState: ColonyState = {
267
+ id: colonyId,
268
+ goal: opts.goal,
269
+ status: "scouting",
270
+ tasks: [makeInitialScoutTask(opts.goal)],
271
+ ants: [],
272
+ pheromones: [],
273
+ concurrency: defaultConcurrency(),
274
+ metrics: {
275
+ tasksTotal: 1, tasksDone: 0, tasksFailed: 0,
276
+ antsSpawned: 0, totalCost: 0, totalTokens: 0,
277
+ startTime: Date.now(), throughputHistory: [],
278
+ },
279
+ createdAt: Date.now(),
280
+ finishedAt: null,
281
+ };
282
+
283
+ if (opts.maxAnts) {
284
+ initialState.concurrency.max = opts.maxAnts;
285
+ }
286
+
287
+ nest.init(initialState);
288
+ const { signal, callbacks } = opts;
289
+
290
+ try {
291
+ // ═══ Phase 1: 侦察 ═══
292
+ callbacks.onPhase("scouting", "Dispatching scout ants to explore codebase...");
293
+ await runAntWave(nest, opts.cwd, "scout", signal, callbacks);
294
+
295
+ // 检查侦察结果,如果没有产生工蚁任务,用侦察结果让女王自己拆
296
+ const postScout = nest.getAllTasks();
297
+ const workerTasks = postScout.filter(t => t.caste === "worker" && t.status === "pending");
298
+ if (workerTasks.length === 0) {
299
+ // 侦察蚁没产生任务,标记失败
300
+ nest.updateState({ status: "failed", finishedAt: Date.now() });
301
+ const finalState = nest.getState();
302
+ callbacks.onComplete(finalState);
303
+ return finalState;
304
+ }
305
+
306
+ // ═══ Phase 2: 工作 ═══
307
+ nest.updateState({ status: "working" });
308
+ callbacks.onPhase("working", `${workerTasks.length} tasks discovered. Dispatching worker ants...`);
309
+ await runAntWave(nest, opts.cwd, "worker", signal, callbacks);
310
+
311
+ // 处理工蚁产生的子任务(可能有多轮)
312
+ let rounds = 0;
313
+ while (rounds < 3) {
314
+ const remaining = nest.getAllTasks().filter(t =>
315
+ t.caste === "worker" && (t.status === "pending" || t.status === "blocked")
316
+ );
317
+ if (remaining.length === 0) break;
318
+ rounds++;
319
+ callbacks.onPhase("working", `Round ${rounds + 1}: ${remaining.length} sub-tasks from workers...`);
320
+ await runAntWave(nest, opts.cwd, "worker", signal, callbacks);
321
+ }
322
+
323
+ // ═══ Phase 3: 审查 ═══
324
+ const completedWorkerTasks = nest.getAllTasks().filter(t => t.caste === "worker" && t.status === "done");
325
+ if (completedWorkerTasks.length > 0) {
326
+ nest.updateState({ status: "reviewing" });
327
+ callbacks.onPhase("reviewing", "Dispatching soldier ants to review changes...");
328
+ const reviewTask = makeReviewTask(completedWorkerTasks);
329
+ nest.writeTask(reviewTask);
330
+ await runAntWave(nest, opts.cwd, "soldier", signal, callbacks);
331
+
332
+ // 兵蚁产生的修复任务
333
+ const fixTasks = nest.getAllTasks().filter(t =>
334
+ t.caste === "worker" && t.status === "pending" && t.parentId !== null
335
+ );
336
+ if (fixTasks.length > 0) {
337
+ nest.updateState({ status: "working" });
338
+ callbacks.onPhase("working", `${fixTasks.length} fix tasks from review. Dispatching workers...`);
339
+ await runAntWave(nest, opts.cwd, "worker", signal, callbacks);
340
+ }
341
+ }
342
+
343
+ // ═══ Phase 4: 完成 ═══
344
+ const finalMetrics = updateMetrics(nest);
345
+ nest.updateState({ status: "done", finishedAt: Date.now(), metrics: finalMetrics });
346
+ const finalState = nest.getState();
347
+ callbacks.onComplete(finalState);
348
+ return finalState;
349
+
350
+ } catch (e) {
351
+ nest.updateState({ status: "failed", finishedAt: Date.now() });
352
+ const failState = nest.getState();
353
+ callbacks.onComplete(failState);
354
+ return failState;
355
+ }
356
+ }
@@ -0,0 +1,328 @@
1
+ /**
2
+ * 蚂蚁 Spawner — 每只蚂蚁是一个独立 pi --mode json 进程
3
+ *
4
+ * 蚂蚁的 system prompt 中注入:
5
+ * - 自己的角色和任务
6
+ * - 巢穴中的信息素上下文
7
+ * - 完成后输出结构化结果的指令
8
+ */
9
+
10
+ import { spawn as nodeSpawn } from "node:child_process";
11
+ import * as fs from "node:fs";
12
+ import * as os from "node:os";
13
+ import * as path from "node:path";
14
+ import type { Ant, AntCaste, AntConfig, Task, Pheromone, DEFAULT_ANT_CONFIGS } from "./types.js";
15
+ import type { Nest } from "./nest.js";
16
+
17
+ let antCounter = 0;
18
+
19
+ export function makeAntId(caste: AntCaste): string {
20
+ return `${caste}-${++antCounter}-${Date.now().toString(36)}`;
21
+ }
22
+
23
+ export function makePheromoneId(): string {
24
+ return `p-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
25
+ }
26
+
27
+ export function makeTaskId(): string {
28
+ return `t-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
29
+ }
30
+
31
+ interface AntResult {
32
+ ant: Ant;
33
+ output: string;
34
+ messages: any[];
35
+ newTasks: ParsedSubTask[];
36
+ pheromones: Pheromone[];
37
+ rateLimited: boolean;
38
+ }
39
+
40
+ interface ParsedSubTask {
41
+ title: string;
42
+ description: string;
43
+ files: string[];
44
+ caste: AntCaste;
45
+ priority: 1 | 2 | 3 | 4 | 5;
46
+ }
47
+
48
+ const CASTE_PROMPTS: Record<AntCaste, string> = {
49
+ scout: `You are a Scout Ant. Your job is to explore and gather intelligence, NOT to make changes.
50
+
51
+ Behavior:
52
+ - Quickly scan the codebase to understand structure and locate relevant code
53
+ - Identify files, functions, dependencies related to the goal
54
+ - Report findings as structured intelligence for Worker Ants
55
+
56
+ Output format (MUST follow exactly):
57
+ ## Discoveries
58
+ - What you found, with file:line references
59
+
60
+ ## Recommended Tasks
61
+ For each task the colony should do next:
62
+ ### TASK: <title>
63
+ - description: <what to do>
64
+ - files: <comma-separated file paths>
65
+ - caste: worker
66
+ - priority: <1-5, 1=highest>
67
+
68
+ ## Warnings
69
+ Any risks, blockers, or conflicts detected.`,
70
+
71
+ worker: `You are a Worker Ant. You execute tasks autonomously and leave traces for the colony.
72
+
73
+ Behavior:
74
+ - Read the pheromone context to understand what scouts and other workers discovered
75
+ - Execute your assigned task completely
76
+ - If you discover sub-tasks needed, declare them (do NOT execute them yourself)
77
+ - Minimize file conflicts — only touch files assigned to you
78
+
79
+ Output format (MUST follow exactly):
80
+ ## Completed
81
+ What was done, with file:line references for all changes.
82
+
83
+ ## Files Changed
84
+ - path/to/file.ts — what changed
85
+
86
+ ## Sub-Tasks (if any)
87
+ ### TASK: <title>
88
+ - description: <what to do>
89
+ - files: <comma-separated file paths>
90
+ - caste: <worker|soldier>
91
+ - priority: <1-5>
92
+
93
+ ## Pheromone
94
+ Key information other ants should know about your changes.`,
95
+
96
+ soldier: `You are a Soldier Ant (Reviewer). You guard colony quality — you do NOT make changes.
97
+
98
+ Behavior:
99
+ - Review the files changed by worker ants
100
+ - Check for bugs, security issues, conflicts between workers
101
+ - Report issues that need fixing
102
+
103
+ Output format (MUST follow exactly):
104
+ ## Review
105
+ - file:line — issue description (severity: critical|warning|info)
106
+
107
+ ## Fix Tasks (if critical issues found)
108
+ ### TASK: <title>
109
+ - description: <what to fix>
110
+ - files: <comma-separated file paths>
111
+ - caste: worker
112
+ - priority: 1
113
+
114
+ ## Verdict
115
+ PASS or FAIL with summary.`,
116
+ };
117
+
118
+ function buildPrompt(task: Task, pheromoneContext: string, castePrompt: string): string {
119
+ let prompt = castePrompt + "\n\n";
120
+ if (pheromoneContext) {
121
+ prompt += `## Colony Pheromone Trail (intelligence from other ants)\n${pheromoneContext}\n\n`;
122
+ }
123
+ prompt += `## Your Assignment\n**Task:** ${task.title}\n**Description:** ${task.description}\n`;
124
+ if (task.files.length > 0) {
125
+ prompt += `**Files scope:** ${task.files.join(", ")}\n`;
126
+ }
127
+ return prompt;
128
+ }
129
+
130
+ function writePromptFile(antId: string, prompt: string): { dir: string; file: string } {
131
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ant-"));
132
+ const file = path.join(dir, `${antId}.md`);
133
+ fs.writeFileSync(file, prompt, { mode: 0o600 });
134
+ return { dir, file };
135
+ }
136
+
137
+ /** 从蚂蚁输出中解析子任务声明 */
138
+ function parseSubTasks(output: string): ParsedSubTask[] {
139
+ const tasks: ParsedSubTask[] = [];
140
+ const regex = /### TASK:\s*(.+)\n(?:- description:\s*(.+)\n)?(?:- files:\s*(.+)\n)?(?:- caste:\s*(\w+)\n)?(?:- priority:\s*(\d))?/g;
141
+ for (const m of output.matchAll(regex)) {
142
+ tasks.push({
143
+ title: m[1]?.trim() || "Untitled",
144
+ description: m[2]?.trim() || m[1]?.trim() || "",
145
+ files: (m[3]?.trim() || "").split(",").map(f => f.trim()).filter(Boolean),
146
+ caste: (m[4]?.trim() as AntCaste) || "worker",
147
+ priority: (parseInt(m[5] || "3") as 1 | 2 | 3 | 4 | 5) || 3,
148
+ });
149
+ }
150
+ return tasks;
151
+ }
152
+
153
+ /** 从蚂蚁输出中提取信息素 */
154
+ function extractPheromones(antId: string, caste: AntCaste, taskId: string, output: string, files: string[]): Pheromone[] {
155
+ const pheromones: Pheromone[] = [];
156
+ const now = Date.now();
157
+
158
+ // 提取 ## Discoveries 或 ## Pheromone 段落
159
+ const sections = ["Discoveries", "Pheromone", "Files Changed", "Warnings", "Review"];
160
+ for (const section of sections) {
161
+ const regex = new RegExp(`## ${section}\\n([\\s\\S]*?)(?=\\n## |$)`, "i");
162
+ const match = output.match(regex);
163
+ if (match?.[1]?.trim()) {
164
+ const type: import("./types.js").PheromoneType =
165
+ section === "Discoveries" ? "discovery" :
166
+ section === "Warnings" || section === "Review" ? "warning" :
167
+ section === "Files Changed" ? "completion" : "progress";
168
+ pheromones.push({
169
+ id: makePheromoneId(),
170
+ type,
171
+ antId,
172
+ antCaste: caste,
173
+ taskId,
174
+ content: match[1].trim().slice(0, 2000), // 限制大小
175
+ files,
176
+ strength: 1.0,
177
+ createdAt: now,
178
+ });
179
+ }
180
+ }
181
+ return pheromones;
182
+ }
183
+
184
+ /** 获取蚂蚁输出的最终文本 */
185
+ function getFinalOutput(messages: any[]): string {
186
+ for (let i = messages.length - 1; i >= 0; i--) {
187
+ const msg = messages[i];
188
+ if (msg.role === "assistant") {
189
+ for (const part of msg.content) {
190
+ if (part.type === "text") return part.text;
191
+ }
192
+ }
193
+ }
194
+ return "";
195
+ }
196
+
197
+ /**
198
+ * 孵化并运行一只蚂蚁
199
+ */
200
+ export async function spawnAnt(
201
+ cwd: string,
202
+ nest: Nest,
203
+ task: Task,
204
+ antConfig: Omit<AntConfig, "systemPrompt">,
205
+ signal?: AbortSignal,
206
+ ): Promise<AntResult> {
207
+ const antId = makeAntId(antConfig.caste);
208
+ const ant: Ant = {
209
+ id: antId,
210
+ caste: antConfig.caste,
211
+ status: "working",
212
+ taskId: task.id,
213
+ pid: null,
214
+ usage: { input: 0, output: 0, cost: 0, turns: 0 },
215
+ startedAt: Date.now(),
216
+ finishedAt: null,
217
+ };
218
+
219
+ nest.updateAnt(ant);
220
+ nest.updateTaskStatus(task.id, "active");
221
+
222
+ // 构建 prompt
223
+ const pheromoneCtx = nest.getPheromoneContext(task.files);
224
+ const castePrompt = CASTE_PROMPTS[antConfig.caste];
225
+ const fullPrompt = buildPrompt(task, pheromoneCtx, castePrompt);
226
+ const { dir: tmpDir, file: tmpFile } = writePromptFile(antId, fullPrompt);
227
+
228
+ const args = [
229
+ "--mode", "json",
230
+ "-p",
231
+ "--no-session",
232
+ "--model", antConfig.model,
233
+ "--tools", antConfig.tools.join(","),
234
+ "--append-system-prompt", tmpFile,
235
+ `Execute this task: ${task.title}\n\n${task.description}`,
236
+ ];
237
+
238
+ const messages: any[] = [];
239
+ let stderr = "";
240
+
241
+ try {
242
+ const exitCode = await new Promise<number>((resolve) => {
243
+ const proc = nodeSpawn("pi", args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
244
+ ant.pid = proc.pid ?? null;
245
+ nest.updateAnt(ant);
246
+
247
+ let buffer = "";
248
+
249
+ proc.stdout.on("data", (data) => {
250
+ buffer += data.toString();
251
+ const lines = buffer.split("\n");
252
+ buffer = lines.pop() || "";
253
+ for (const line of lines) {
254
+ if (!line.trim()) continue;
255
+ try {
256
+ const event = JSON.parse(line);
257
+ if (event.type === "message_end" && event.message) {
258
+ messages.push(event.message);
259
+ if (event.message.role === "assistant") {
260
+ ant.usage.turns++;
261
+ const u = event.message.usage;
262
+ if (u) {
263
+ ant.usage.input += u.input || 0;
264
+ ant.usage.output += u.output || 0;
265
+ ant.usage.cost += u.cost?.total || 0;
266
+ }
267
+ }
268
+ }
269
+ if (event.type === "tool_result_end" && event.message) {
270
+ messages.push(event.message);
271
+ }
272
+ } catch { /* skip non-json */ }
273
+ }
274
+ });
275
+
276
+ proc.stderr.on("data", (d) => { stderr += d.toString(); });
277
+ proc.on("close", (code) => {
278
+ if (buffer.trim()) {
279
+ try {
280
+ const event = JSON.parse(buffer);
281
+ if (event.type === "message_end" && event.message) messages.push(event.message);
282
+ } catch { /* skip */ }
283
+ }
284
+ resolve(code ?? 1);
285
+ });
286
+ proc.on("error", () => resolve(1));
287
+
288
+ if (signal) {
289
+ const kill = () => {
290
+ proc.kill("SIGTERM");
291
+ setTimeout(() => { if (!proc.killed) proc.kill("SIGKILL"); }, 3000);
292
+ };
293
+ if (signal.aborted) kill();
294
+ else signal.addEventListener("abort", kill, { once: true });
295
+ }
296
+ });
297
+
298
+ const output = getFinalOutput(messages);
299
+ const success = exitCode === 0;
300
+ const rateLimited = stderr.includes("429") || stderr.includes("rate limit") || stderr.includes("Rate limit")
301
+ || output.includes("429") || output.includes("rate_limit");
302
+
303
+ ant.status = success ? "done" : "failed";
304
+ ant.finishedAt = Date.now();
305
+ ant.pid = null;
306
+ nest.updateAnt(ant);
307
+
308
+ if (rateLimited) {
309
+ // 429: 不标记任务为 failed,回退为 pending 以便重试
310
+ nest.updateTaskStatus(task.id, "pending");
311
+ ant.status = "failed";
312
+ nest.updateAnt(ant);
313
+ return { ant, output, messages, newTasks: [], pheromones: [], rateLimited: true };
314
+ }
315
+
316
+ nest.updateTaskStatus(task.id, success ? "done" : "failed", output, success ? undefined : stderr || output);
317
+
318
+ // 解析子任务和信息素
319
+ const newTasks = parseSubTasks(output);
320
+ const pheromones = extractPheromones(antId, antConfig.caste, task.id, output, task.files);
321
+ for (const p of pheromones) nest.dropPheromone(p);
322
+
323
+ return { ant, output, messages, newTasks, pheromones, rateLimited: false };
324
+ } finally {
325
+ try { fs.unlinkSync(tmpFile); } catch { /* ignore */ }
326
+ try { fs.rmdirSync(tmpDir); } catch { /* ignore */ }
327
+ }
328
+ }