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,115 @@
1
+ /**
2
+ * 自适应并发控制 — 模拟蚁群的动态招募机制
3
+ *
4
+ * 真实蚁群:食物多→释放更多招募信息素→更多工蚁出巢
5
+ * 映射:任务多+系统空闲→提高并发;系统过载/任务少→降低并发
6
+ *
7
+ * 探索边界:启动时逐步提升并发,监测吞吐量拐点
8
+ */
9
+
10
+ import * as os from "node:os";
11
+ import type { ConcurrencyConfig, ConcurrencySample } from "./types.js";
12
+
13
+ const CPU_CORES = os.cpus().length;
14
+
15
+ export function defaultConcurrency(): ConcurrencyConfig {
16
+ return {
17
+ current: 1,
18
+ min: 1,
19
+ max: Math.min(CPU_CORES, 8),
20
+ optimal: 2,
21
+ history: [],
22
+ };
23
+ }
24
+
25
+ /** 采样当前系统负载 */
26
+ export function sampleSystem(activeTasks: number, completedRecently: number, windowMinutes: number): ConcurrencySample {
27
+ const cpus = os.cpus();
28
+ const cpuLoad = cpus.reduce((sum, c) => {
29
+ const total = Object.values(c.times).reduce((a, b) => a + b, 0);
30
+ return sum + 1 - c.times.idle / total;
31
+ }, 0) / cpus.length;
32
+
33
+ const mem = os.freemem();
34
+ const throughput = windowMinutes > 0 ? completedRecently / windowMinutes : 0;
35
+
36
+ return {
37
+ timestamp: Date.now(),
38
+ concurrency: activeTasks,
39
+ cpuLoad,
40
+ memFree: mem,
41
+ throughput,
42
+ };
43
+ }
44
+
45
+ /**
46
+ * 自适应调节算法
47
+ *
48
+ * 阶段1 - 探索期(样本<10):每次+1,寻找吞吐量拐点
49
+ * 阶段2 - 稳态期:围绕最优值微调
50
+ *
51
+ * 约束:
52
+ * - CPU 负载 > 85% → 减少
53
+ * - 可用内存 < 500MB → 减少
54
+ * - 吞吐量下降 → 回退到上一个最优值
55
+ * - 待处理任务为 0 → 降到 min
56
+ */
57
+ export function adapt(config: ConcurrencyConfig, pendingTasks: number): ConcurrencyConfig {
58
+ const next = { ...config };
59
+ const samples = config.history;
60
+
61
+ // 没有待处理任务,降到最低
62
+ if (pendingTasks === 0) {
63
+ next.current = config.min;
64
+ return next;
65
+ }
66
+
67
+ // 不超过待处理任务数
68
+ const taskCap = Math.min(pendingTasks, config.max);
69
+
70
+ if (samples.length < 3) {
71
+ // 冷启动:保守起步
72
+ next.current = Math.min(2, taskCap);
73
+ return next;
74
+ }
75
+
76
+ const latest = samples[samples.length - 1];
77
+ const prev = samples[samples.length - 2];
78
+
79
+ // 硬约束:系统过载立即减少
80
+ if (latest.cpuLoad > 0.85 || latest.memFree < 500 * 1024 * 1024) {
81
+ next.current = Math.max(config.min, config.current - 1);
82
+ return next;
83
+ }
84
+
85
+ // 探索期:样本不足,逐步提升
86
+ if (samples.length < 10) {
87
+ if (latest.throughput >= prev.throughput) {
88
+ // 吞吐量还在涨,继续探索
89
+ next.current = Math.min(config.current + 1, taskCap);
90
+ } else {
91
+ // 吞吐量下降,找到拐点
92
+ next.optimal = prev.concurrency;
93
+ next.current = prev.concurrency;
94
+ }
95
+ return next;
96
+ }
97
+
98
+ // 稳态期:围绕最优值微调
99
+ const recentThroughput = samples.slice(-5).reduce((s, x) => s + x.throughput, 0) / 5;
100
+ const olderThroughput = samples.slice(-10, -5).reduce((s, x) => s + x.throughput, 0) / 5;
101
+
102
+ if (recentThroughput > olderThroughput * 1.1 && latest.cpuLoad < 0.7) {
103
+ // 吞吐量上升且 CPU 有余量,尝试+1
104
+ next.current = Math.min(config.current + 1, taskCap);
105
+ if (recentThroughput > olderThroughput * 1.2) {
106
+ next.optimal = next.current; // 更新最优值
107
+ }
108
+ } else if (recentThroughput < olderThroughput * 0.8) {
109
+ // 吞吐量下降,回退
110
+ next.current = Math.max(config.min, config.optimal);
111
+ }
112
+ // 否则保持不变
113
+
114
+ return next;
115
+ }
@@ -0,0 +1,338 @@
1
+ /**
2
+ * 🐜 蚁群模式 (Ant Colony) — pi 扩展入口
3
+ *
4
+ * 注册:
5
+ * - ant_colony tool:LLM 可调用启动蚁群
6
+ * - /colony command:用户手动启动
7
+ * - TUI 渲染:实时显示蚁群状态
8
+ */
9
+
10
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
11
+ import { Text, Container, Spacer } from "@mariozechner/pi-tui";
12
+ import { Type } from "@sinclair/typebox";
13
+ import { runColony, type QueenCallbacks } from "./queen.js";
14
+ import type { ColonyState, ColonyMetrics, Ant, Task } from "./types.js";
15
+
16
+ interface ColonyDetails {
17
+ state: ColonyState | null;
18
+ phase: string;
19
+ log: string[];
20
+ }
21
+
22
+ function formatDuration(ms: number): string {
23
+ const s = Math.floor(ms / 1000);
24
+ if (s < 60) return `${s}s`;
25
+ const m = Math.floor(s / 60);
26
+ return `${m}m${s % 60}s`;
27
+ }
28
+
29
+ function formatCost(cost: number): string {
30
+ return cost < 0.01 ? `$${cost.toFixed(4)}` : `$${cost.toFixed(2)}`;
31
+ }
32
+
33
+ function formatTokens(n: number): string {
34
+ return n < 1000 ? `${n}` : n < 1000000 ? `${(n / 1000).toFixed(1)}k` : `${(n / 1000000).toFixed(1)}M`;
35
+ }
36
+
37
+ function statusIcon(status: string): string {
38
+ const icons: Record<string, string> = {
39
+ scouting: "🔍", working: "⚒️", reviewing: "🛡️",
40
+ done: "✅", failed: "❌",
41
+ };
42
+ return icons[status] || "🐜";
43
+ }
44
+
45
+ function casteIcon(caste: string): string {
46
+ return caste === "scout" ? "🔍" : caste === "soldier" ? "🛡️" : "⚒️";
47
+ }
48
+
49
+ export default function antColonyExtension(pi: ExtensionAPI) {
50
+
51
+ // ═══ Auto-trigger: 注入蚁群意识,LLM 自动判断何时启动 ═══
52
+ pi.on("before_agent_start", async () => {
53
+ return {
54
+ message: {
55
+ customType: "ant-colony-context",
56
+ content: `[ANT COLONY AVAILABLE]
57
+ You have the ant_colony tool. Use it automatically when:
58
+ - Task involves ≥3 files
59
+ - Task needs parallel workstreams (refactor, migration, feature across modules)
60
+ - Task is complex enough that serial execution is inefficient
61
+
62
+ Do NOT ask for confirmation. Call ant_colony directly with a clear goal.
63
+ For simple single-file tasks, work directly without the colony.`,
64
+ display: false,
65
+ },
66
+ };
67
+ });
68
+
69
+ // ═══ Tool: ant_colony ═══
70
+ pi.registerTool({
71
+ name: "ant_colony",
72
+ label: "Ant Colony",
73
+ description: [
74
+ "Launch an autonomous ant colony to accomplish a complex goal.",
75
+ "Scouts explore the codebase, workers execute tasks in parallel, soldiers review quality.",
76
+ "Concurrency auto-adapts to system load. Use for multi-file changes, large refactors, or complex features.",
77
+ "The colony self-organizes: scouts discover tasks, workers can spawn sub-tasks, soldiers can request fixes.",
78
+ ].join(" "),
79
+ parameters: Type.Object({
80
+ goal: Type.String({ description: "What the colony should accomplish" }),
81
+ maxAnts: Type.Optional(Type.Number({ description: "Max concurrent ants (default: auto-adapt)", minimum: 1, maximum: 8 })),
82
+ }),
83
+
84
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
85
+ const details: ColonyDetails = { state: null, phase: "initializing", log: [] };
86
+
87
+ const emit = () => {
88
+ const summary = details.state
89
+ ? `${statusIcon(details.state.status)} Colony: ${details.phase}`
90
+ : "🐜 Colony initializing...";
91
+ onUpdate?.({
92
+ content: [{ type: "text", text: summary }],
93
+ details: { ...details },
94
+ });
95
+ };
96
+
97
+ const callbacks: QueenCallbacks = {
98
+ onPhase(phase, detail) {
99
+ details.phase = detail;
100
+ details.log.push(`[${new Date().toLocaleTimeString()}] ${statusIcon(phase)} ${detail}`);
101
+ emit();
102
+ },
103
+ onAntSpawn(ant, task) {
104
+ details.log.push(` ${casteIcon(ant.caste)} ${ant.caste} ant dispatched → ${task.title.slice(0, 50)}`);
105
+ emit();
106
+ },
107
+ onAntDone(ant, task, output) {
108
+ const dur = ant.finishedAt ? formatDuration(ant.finishedAt - ant.startedAt) : "?";
109
+ const icon = ant.status === "done" ? "✓" : "✗";
110
+ details.log.push(` ${icon} ${ant.caste} ant finished (${dur}, ${formatCost(ant.usage.cost)}) → ${task.title.slice(0, 50)}`);
111
+ emit();
112
+ },
113
+ onProgress(metrics) {
114
+ if (details.state) details.state.metrics = metrics;
115
+ emit();
116
+ },
117
+ onComplete(state) {
118
+ details.state = state;
119
+ details.phase = state.status === "done" ? "Colony mission complete" : "Colony failed";
120
+ emit();
121
+ },
122
+ };
123
+
124
+ try {
125
+ const state = await runColony({
126
+ cwd: ctx.cwd,
127
+ goal: params.goal,
128
+ maxAnts: params.maxAnts,
129
+ signal: signal ?? undefined,
130
+ callbacks,
131
+ });
132
+
133
+ details.state = state;
134
+ const m = state.metrics;
135
+ const elapsed = state.finishedAt ? formatDuration(state.finishedAt - state.createdAt) : "?";
136
+
137
+ const report = [
138
+ `## 🐜 Ant Colony Report`,
139
+ ``,
140
+ `**Goal:** ${state.goal}`,
141
+ `**Status:** ${statusIcon(state.status)} ${state.status}`,
142
+ `**Duration:** ${elapsed}`,
143
+ ``,
144
+ `### Metrics`,
145
+ `- Tasks: ${m.tasksDone}/${m.tasksTotal} done, ${m.tasksFailed} failed`,
146
+ `- Ants spawned: ${m.antsSpawned}`,
147
+ `- Tokens: ${formatTokens(m.totalTokens)}`,
148
+ `- Cost: ${formatCost(m.totalCost)}`,
149
+ `- Peak concurrency: ${state.concurrency.optimal}`,
150
+ ``,
151
+ `### Task Results`,
152
+ ...state.tasks.filter(t => t.status === "done").map(t =>
153
+ `- ✓ **${t.title}** (${t.caste})${t.result ? `\n ${t.result.split("\n")[0]?.slice(0, 100)}` : ""}`
154
+ ),
155
+ ...state.tasks.filter(t => t.status === "failed").map(t =>
156
+ `- ✗ **${t.title}** — ${t.error?.slice(0, 100) || "unknown error"}`
157
+ ),
158
+ ``,
159
+ `### Pheromone Trail`,
160
+ ...state.pheromones.slice(-10).map(p =>
161
+ `- [${p.type}] ${p.content.split("\n")[0]?.slice(0, 80)}`
162
+ ),
163
+ ].join("\n");
164
+
165
+ return {
166
+ content: [{ type: "text", text: report }],
167
+ details: { ...details },
168
+ isError: state.status === "failed",
169
+ };
170
+ } catch (e) {
171
+ return {
172
+ content: [{ type: "text", text: `Colony failed: ${e}` }],
173
+ details: { ...details },
174
+ isError: true,
175
+ };
176
+ }
177
+ },
178
+
179
+ // ═══ TUI Rendering ═══
180
+
181
+ renderCall(args, theme) {
182
+ let text = theme.fg("toolTitle", theme.bold("ant_colony "));
183
+ text += theme.fg("accent", "🐜");
184
+ const goal = args.goal?.length > 60 ? args.goal.slice(0, 57) + "..." : args.goal;
185
+ text += "\n " + theme.fg("dim", goal || "...");
186
+ if (args.maxAnts) text += theme.fg("muted", ` (max: ${args.maxAnts})`);
187
+ return new Text(text, 0, 0);
188
+ },
189
+
190
+ renderResult(result, { expanded }, theme) {
191
+ const details = result.details as ColonyDetails | undefined;
192
+ if (!details?.state) {
193
+ // Still running or no state
194
+ const log = details?.log ?? [];
195
+ let text = theme.fg("warning", "🐜 ") + theme.fg("toolTitle", details?.phase || "initializing...");
196
+ const recent = log.slice(expanded ? -30 : -8);
197
+ if (recent.length > 0) {
198
+ text += "\n" + recent.map(l => theme.fg("dim", l)).join("\n");
199
+ }
200
+ if (!expanded && log.length > 8) {
201
+ text += "\n" + theme.fg("muted", `... ${log.length - 8} more (Ctrl+O to expand)`);
202
+ }
203
+ return new Text(text, 0, 0);
204
+ }
205
+
206
+ const state = details.state;
207
+ const m = state.metrics;
208
+ const icon = state.status === "done" ? theme.fg("success", "✓") : theme.fg("error", "✗");
209
+ const elapsed = state.finishedAt ? formatDuration(state.finishedAt - state.createdAt) : "?";
210
+
211
+ if (!expanded) {
212
+ let text = `${icon} ${theme.fg("toolTitle", theme.bold("ant colony "))}`;
213
+ text += theme.fg("accent", `${m.tasksDone}/${m.tasksTotal} tasks`);
214
+ text += theme.fg("muted", ` | ${m.antsSpawned} ants | ${elapsed} | ${formatCost(m.totalCost)}`);
215
+ text += theme.fg("muted", ` | peak ×${state.concurrency.optimal}`);
216
+
217
+ // Compact task list
218
+ for (const t of state.tasks.slice(0, 5)) {
219
+ const ti = t.status === "done" ? theme.fg("success", "✓") : t.status === "failed" ? theme.fg("error", "✗") : theme.fg("muted", "○");
220
+ text += `\n ${ti} ${theme.fg("dim", `[${t.caste}]`)} ${t.title.slice(0, 60)}`;
221
+ }
222
+ if (state.tasks.length > 5) text += `\n ${theme.fg("muted", `... +${state.tasks.length - 5} more`)}`;
223
+ text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
224
+ return new Text(text, 0, 0);
225
+ }
226
+
227
+ // Expanded view
228
+ const container = new Container();
229
+ container.addChild(new Text(
230
+ `${icon} ${theme.fg("toolTitle", theme.bold("ant colony "))}` +
231
+ theme.fg("accent", state.status) +
232
+ theme.fg("muted", ` | ${elapsed} | ${formatCost(m.totalCost)} | ${formatTokens(m.totalTokens)} tokens | peak ×${state.concurrency.optimal}`),
233
+ 0, 0,
234
+ ));
235
+ container.addChild(new Text(theme.fg("dim", state.goal), 0, 0));
236
+ container.addChild(new Spacer(1));
237
+
238
+ // Tasks
239
+ container.addChild(new Text(theme.fg("muted", `─── Tasks (${m.tasksDone}/${m.tasksTotal}) ───`), 0, 0));
240
+ for (const t of state.tasks) {
241
+ const ti = t.status === "done" ? theme.fg("success", "✓")
242
+ : t.status === "failed" ? theme.fg("error", "✗")
243
+ : t.status === "active" ? theme.fg("warning", "⏳")
244
+ : theme.fg("muted", "○");
245
+ let line = `${ti} ${theme.fg("accent", `[${t.caste}]`)} ${t.title}`;
246
+ if (t.finishedAt && t.startedAt) line += theme.fg("dim", ` (${formatDuration(t.finishedAt - t.startedAt)})`);
247
+ container.addChild(new Text(line, 0, 0));
248
+ if (t.status === "done" && t.result) {
249
+ const preview = t.result.split("\n").slice(0, 2).join("\n").slice(0, 120);
250
+ container.addChild(new Text(theme.fg("dim", ` ${preview}`), 0, 0));
251
+ }
252
+ if (t.status === "failed" && t.error) {
253
+ container.addChild(new Text(theme.fg("error", ` ${t.error.slice(0, 120)}`), 0, 0));
254
+ }
255
+ }
256
+
257
+ // Ants
258
+ container.addChild(new Spacer(1));
259
+ container.addChild(new Text(theme.fg("muted", `─── Ants (${m.antsSpawned}) ───`), 0, 0));
260
+ for (const a of state.ants) {
261
+ const ai = a.status === "done" ? theme.fg("success", "✓") : a.status === "failed" ? theme.fg("error", "✗") : theme.fg("warning", "⏳");
262
+ const dur = a.finishedAt ? formatDuration(a.finishedAt - a.startedAt) : "...";
263
+ container.addChild(new Text(
264
+ `${ai} ${casteIcon(a.caste)} ${theme.fg("accent", a.id)} ${theme.fg("dim", `${dur} ${formatCost(a.usage.cost)} ${a.usage.turns}t`)}`,
265
+ 0, 0,
266
+ ));
267
+ }
268
+
269
+ // Concurrency
270
+ container.addChild(new Spacer(1));
271
+ const c = state.concurrency;
272
+ container.addChild(new Text(
273
+ theme.fg("muted", `─── Concurrency ───`) + `\n` +
274
+ theme.fg("dim", `current: ${c.current} | optimal: ${c.optimal} | range: ${c.min}-${c.max} | samples: ${c.history.length}`),
275
+ 0, 0,
276
+ ));
277
+
278
+ // Activity log
279
+ container.addChild(new Spacer(1));
280
+ container.addChild(new Text(theme.fg("muted", "─── Log ───"), 0, 0));
281
+ for (const l of details.log.slice(-20)) {
282
+ container.addChild(new Text(theme.fg("dim", l), 0, 0));
283
+ }
284
+
285
+ return container;
286
+ },
287
+ });
288
+
289
+ // ═══ Command: /colony — 直接执行,零确认 ═══
290
+ pi.registerCommand("colony", {
291
+ description: "Launch an ant colony. Usage: /colony <goal>",
292
+ async handler(args, ctx) {
293
+ if (!args?.trim()) {
294
+ ctx.ui.notify("Usage: /colony <goal>", "warning");
295
+ return;
296
+ }
297
+ pi.sendUserMessage(`Use the ant_colony tool with goal: ${args.trim()}`);
298
+ },
299
+ });
300
+
301
+ // ═══ Command: /colony-status ═══
302
+ pi.registerCommand("colony-status", {
303
+ description: "Show status of the last ant colony run",
304
+ async handler(_args, ctx) {
305
+ // 从 session 中找最近的 ant_colony tool result
306
+ const entries = ctx.sessionManager.getEntries();
307
+ for (let i = entries.length - 1; i >= 0; i--) {
308
+ const e = entries[i] as any;
309
+ if (e.type === "message" && e.message?.role === "toolResult" && e.message?.toolName === "ant_colony") {
310
+ const details = e.message.details as ColonyDetails | undefined;
311
+ if (details?.state) {
312
+ const s = details.state;
313
+ const m = s.metrics;
314
+ ctx.ui.notify(
315
+ `🐜 Colony: ${s.status} | ${m.tasksDone}/${m.tasksTotal} tasks | ${m.antsSpawned} ants | ${formatCost(m.totalCost)}`,
316
+ s.status === "done" ? "success" : "warning",
317
+ );
318
+ return;
319
+ }
320
+ }
321
+ }
322
+ ctx.ui.notify("No colony run found in this session.", "info");
323
+ },
324
+ });
325
+
326
+ // ═══ Shortcut: Ctrl+Alt+A ═══
327
+ pi.registerShortcut("ctrl+alt+a", {
328
+ description: "Quick launch ant colony from editor content",
329
+ async handler(ctx) {
330
+ const text = await ctx.ui.input("Ant Colony Goal", "What should the colony accomplish?");
331
+ if (text?.trim()) {
332
+ pi.sendUserMessage(
333
+ `Use the ant_colony tool to accomplish this goal: ${text.trim()}`,
334
+ );
335
+ }
336
+ },
337
+ });
338
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * 巢穴 (Nest) — 蚁群共享状态,基于文件系统的跨进程协调
3
+ *
4
+ * .ant-colony/{colonyId}/
5
+ * state.json — 蚁巢主状态
6
+ * pheromone.jsonl — 信息素追加日志
7
+ * tasks/ — 每个任务一个文件,原子更新
8
+ */
9
+
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+ import * as os from "node:os";
13
+ import type { ColonyState, Task, Pheromone, Ant, TaskStatus, ConcurrencySample } from "./types.js";
14
+
15
+ export class Nest {
16
+ readonly dir: string;
17
+ private stateFile: string;
18
+ private pheromoneFile: string;
19
+ private tasksDir: string;
20
+
21
+ constructor(private cwd: string, private colonyId: string) {
22
+ this.dir = path.join(cwd, ".ant-colony", colonyId);
23
+ this.stateFile = path.join(this.dir, "state.json");
24
+ this.pheromoneFile = path.join(this.dir, "pheromone.jsonl");
25
+ this.tasksDir = path.join(this.dir, "tasks");
26
+ fs.mkdirSync(this.tasksDir, { recursive: true });
27
+ }
28
+
29
+ // ═══ State ═══
30
+
31
+ init(state: ColonyState): void {
32
+ this.writeJson(this.stateFile, state);
33
+ for (const t of state.tasks) this.writeTask(t);
34
+ }
35
+
36
+ getState(): ColonyState {
37
+ const base = this.readJson<ColonyState>(this.stateFile);
38
+ // 从 tasks/ 目录重建最新任务状态(原子性保证)
39
+ base.tasks = this.getAllTasks();
40
+ base.pheromones = this.getAllPheromones();
41
+ return base;
42
+ }
43
+
44
+ updateState(patch: Partial<Pick<ColonyState, "status" | "concurrency" | "metrics" | "ants" | "finishedAt">>): void {
45
+ const state = this.readJson<ColonyState>(this.stateFile);
46
+ Object.assign(state, patch);
47
+ this.writeJson(this.stateFile, state);
48
+ }
49
+
50
+ // ═══ Tasks (Food) ═══
51
+
52
+ writeTask(task: Task): void {
53
+ this.writeJson(path.join(this.tasksDir, `${task.id}.json`), task);
54
+ }
55
+
56
+ getTask(id: string): Task | null {
57
+ const f = path.join(this.tasksDir, `${id}.json`);
58
+ return fs.existsSync(f) ? this.readJson<Task>(f) : null;
59
+ }
60
+
61
+ getAllTasks(): Task[] {
62
+ try {
63
+ return fs.readdirSync(this.tasksDir)
64
+ .filter(f => f.endsWith(".json"))
65
+ .map(f => this.readJson<Task>(path.join(this.tasksDir, f)));
66
+ } catch { return []; }
67
+ }
68
+
69
+ claimTask(taskId: string, antId: string): boolean {
70
+ const task = this.getTask(taskId);
71
+ if (!task || task.status !== "pending") return false;
72
+ task.status = "claimed";
73
+ task.claimedBy = antId;
74
+ this.writeTask(task);
75
+ return true;
76
+ }
77
+
78
+ updateTaskStatus(taskId: string, status: TaskStatus, result?: string, error?: string): void {
79
+ const task = this.getTask(taskId);
80
+ if (!task) return;
81
+ task.status = status;
82
+ if (status === "active") task.startedAt = Date.now();
83
+ if (status === "done" || status === "failed") task.finishedAt = Date.now();
84
+ if (result !== undefined) task.result = result;
85
+ if (error !== undefined) task.error = error;
86
+ this.writeTask(task);
87
+ }
88
+
89
+ addSubTask(parentId: string, child: Task): void {
90
+ this.writeTask(child);
91
+ const parent = this.getTask(parentId);
92
+ if (parent) {
93
+ parent.spawnedTasks.push(child.id);
94
+ this.writeTask(parent);
95
+ }
96
+ }
97
+
98
+ /** 获取下一个可领取的任务(按优先级 + 信息素强度排序) */
99
+ nextPendingTask(caste: "scout" | "worker" | "soldier"): Task | null {
100
+ const tasks = this.getAllTasks()
101
+ .filter(t => t.status === "pending" && t.caste === caste);
102
+ if (tasks.length === 0) return null;
103
+
104
+ // 按优先级排序,同优先级按创建时间
105
+ tasks.sort((a, b) => a.priority - b.priority || a.createdAt - b.createdAt);
106
+
107
+ // 信息素加权:有相关 discovery 信息素的任务优先
108
+ const pheromones = this.getAllPheromones();
109
+ const scored = tasks.map(t => {
110
+ const relevantP = pheromones.filter(p =>
111
+ p.type === "discovery" &&
112
+ p.files.some(f => t.files.includes(f)) &&
113
+ p.strength > 0.1
114
+ );
115
+ const pScore = relevantP.reduce((sum, p) => sum + p.strength, 0);
116
+ return { task: t, score: (6 - t.priority) + pScore };
117
+ });
118
+ scored.sort((a, b) => b.score - a.score);
119
+ return scored[0]?.task ?? null;
120
+ }
121
+
122
+ // ═══ Pheromones ═══
123
+
124
+ dropPheromone(p: Pheromone): void {
125
+ fs.appendFileSync(this.pheromoneFile, JSON.stringify(p) + "\n");
126
+ }
127
+
128
+ getAllPheromones(): Pheromone[] {
129
+ if (!fs.existsSync(this.pheromoneFile)) return [];
130
+ const now = Date.now();
131
+ const HALF_LIFE = 10 * 60 * 1000; // 10 分钟半衰期
132
+ return fs.readFileSync(this.pheromoneFile, "utf-8")
133
+ .split("\n")
134
+ .filter(Boolean)
135
+ .map(line => {
136
+ const p = JSON.parse(line) as Pheromone;
137
+ // 信息素挥发:指数衰减
138
+ const age = now - p.createdAt;
139
+ p.strength = p.strength * Math.pow(0.5, age / HALF_LIFE);
140
+ return p;
141
+ })
142
+ .filter(p => p.strength > 0.05); // 过弱的丢弃
143
+ }
144
+
145
+ /** 读取与特定文件相关的信息素摘要 */
146
+ getPheromoneContext(files: string[], limit = 20): string {
147
+ const relevant = this.getAllPheromones()
148
+ .filter(p => p.files.some(f => files.includes(f)) || files.length === 0)
149
+ .sort((a, b) => b.strength - a.strength)
150
+ .slice(0, limit);
151
+ if (relevant.length === 0) return "";
152
+ return relevant.map(p =>
153
+ `[${p.type}|${p.antCaste}|str:${p.strength.toFixed(2)}] ${p.content}`
154
+ ).join("\n");
155
+ }
156
+
157
+ // ═══ Ants ═══
158
+
159
+ updateAnt(ant: Ant): void {
160
+ const state = this.readJson<ColonyState>(this.stateFile);
161
+ const idx = state.ants.findIndex(a => a.id === ant.id);
162
+ if (idx >= 0) state.ants[idx] = ant;
163
+ else state.ants.push(ant);
164
+ this.writeJson(this.stateFile, state);
165
+ }
166
+
167
+ // ═══ Concurrency Sampling ═══
168
+
169
+ recordSample(sample: ConcurrencySample): void {
170
+ const state = this.readJson<ColonyState>(this.stateFile);
171
+ state.concurrency.history.push(sample);
172
+ // 只保留最近 30 个样本
173
+ if (state.concurrency.history.length > 30) {
174
+ state.concurrency.history = state.concurrency.history.slice(-30);
175
+ }
176
+ this.writeJson(this.stateFile, state);
177
+ }
178
+
179
+ // ═══ Cleanup ═══
180
+
181
+ destroy(): void {
182
+ try { fs.rmSync(this.dir, { recursive: true, force: true }); } catch { /* ignore */ }
183
+ }
184
+
185
+ // ═══ Internal ═══
186
+
187
+ private writeJson(file: string, data: unknown): void {
188
+ const tmp = file + ".tmp";
189
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2));
190
+ fs.renameSync(tmp, file); // 原子写入
191
+ }
192
+
193
+ private readJson<T>(file: string): T {
194
+ return JSON.parse(fs.readFileSync(file, "utf-8")) as T;
195
+ }
196
+ }