oh-pi 0.1.76 → 0.1.78

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/README.md CHANGED
@@ -157,7 +157,7 @@ Use `/colony-stop` to abort a running colony.
157
157
 
158
158
  ### Signal Protocol
159
159
 
160
- The colony communicates with the main conversation via structured signals, so the model never has to guess background state:
160
+ The colony communicates with the main conversation via structured signals, so the model never has to guess background state. Updates are passively pushed (non-blocking), so polling is optional:
161
161
 
162
162
  | Signal | Meaning |
163
163
  |--------|---------|
package/README.zh.md CHANGED
@@ -163,7 +163,7 @@ pi(主进程)
163
163
 
164
164
  ### 信号协议
165
165
 
166
- 蚁群通过结构化信号与主对话通信,让模型无需猜测后台状态:
166
+ 蚁群通过结构化信号与主对话通信,让模型无需猜测后台状态。更新采用被动推送(非阻塞),轮询仅在手动排障时需要:
167
167
 
168
168
  | 信号 | 含义 |
169
169
  |------|------|
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ async function quickFlow(env) {
43
43
  providers,
44
44
  theme: "dark",
45
45
  keybindings: "default",
46
- extensions: ["safe-guard", "git-guard", "auto-session-name", "custom-footer", "compact-header", "auto-update", "smart-compact"],
46
+ extensions: ["safe-guard", "git-guard", "auto-session-name", "custom-footer", "compact-header", "auto-update"],
47
47
  prompts: ["review", "fix", "explain", "commit", "test"],
48
48
  agents: "general-developer",
49
49
  thinking: "medium",
package/dist/registry.js CHANGED
@@ -49,7 +49,6 @@ export const EXTENSIONS = [
49
49
  { name: "compact-header", label: "⚡ Compact Header — Dense startup info replacing verbose output", default: true },
50
50
  { name: "ant-colony", label: "🐜 Ant Colony — Autonomous multi-agent swarm with adaptive concurrency", default: false },
51
51
  { name: "auto-update", label: "🔄 Auto Update — Check for oh-pi updates on startup and notify", default: true },
52
- { name: "smart-compact", label: "🗜️ Smart Compact — Trim large tool outputs and old messages in-flight", default: true },
53
52
  { name: "bg-process", label: "⏳ Bg Process — Auto-background long-running commands (dev servers, etc.)", default: false },
54
53
  ];
55
54
  /** 快捷键绑定方案(default / vim / emacs) */
@@ -3,6 +3,15 @@ import { join } from "node:path";
3
3
  import { KEYBINDING_SCHEMES, MODEL_CAPABILITIES, PROVIDERS } from "../registry.js";
4
4
  import { resources } from "./resources.js";
5
5
  import { ensureDir, syncDir } from "./install.js";
6
+ const ANT_COLONY_AUTOTRIGGER_GUIDE = `## Ant Colony Auto-Trigger
7
+ If the ant_colony tool is available, automatically use it when the task is complex:
8
+ - 3 or more files likely need changes
9
+ - 2 or more independent workstreams exist
10
+ - large refactor / migration / feature implementation
11
+
12
+ For simple one-file tasks, execute directly without colony overhead.
13
+ After launching ant_colony, use passive mode: wait for COLONY_SIGNAL updates and do not poll bg_colony_status unless the user explicitly asks for a manual snapshot.
14
+ `;
6
15
  /** 步骤 1-2: 生成 auth.json + settings.json */
7
16
  export function writeProviderEnv(agentDir, config) {
8
17
  // auth.json
@@ -103,6 +112,9 @@ export function writeAgents(agentDir, config) {
103
112
  const lang = langNames[config.locale] ?? config.locale;
104
113
  content = `## Language\nAlways respond in ${lang}. Use the user's language for all conversations and explanations. Code, commands, and technical terms can remain in English.\n\n${content}`;
105
114
  }
115
+ if (config.extensions.includes("ant-colony") && config.agents !== "colony-operator") {
116
+ content = `${content.trimEnd()}\n\n${ANT_COLONY_AUTOTRIGGER_GUIDE}`;
117
+ }
106
118
  writeFileSync(join(agentDir, "AGENTS.md"), content);
107
119
  }
108
120
  catch { /* template not found, skip */ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { writeAgents } from "./writers.js";
6
+ const tempDirs = [];
7
+ function makeTempDir() {
8
+ const dir = mkdtempSync(join(tmpdir(), "oh-pi-writers-"));
9
+ tempDirs.push(dir);
10
+ return dir;
11
+ }
12
+ function makeConfig(overrides) {
13
+ return {
14
+ providers: [],
15
+ theme: "dark",
16
+ keybindings: "default",
17
+ extensions: [],
18
+ prompts: [],
19
+ agents: "general-developer",
20
+ thinking: "medium",
21
+ ...overrides,
22
+ };
23
+ }
24
+ afterEach(() => {
25
+ for (const dir of tempDirs.splice(0)) {
26
+ rmSync(dir, { recursive: true, force: true });
27
+ }
28
+ });
29
+ describe("writeAgents", () => {
30
+ it("appends ant-colony auto-trigger guidance for non-colony operator agents", () => {
31
+ const dir = makeTempDir();
32
+ writeAgents(dir, makeConfig({
33
+ agents: "general-developer",
34
+ extensions: ["ant-colony"],
35
+ }));
36
+ const content = readFileSync(join(dir, "AGENTS.md"), "utf8");
37
+ expect(content).toContain("## Ant Colony Auto-Trigger");
38
+ expect(content).toContain("automatically use it when the task is complex");
39
+ expect(content).toContain("COLONY_SIGNAL");
40
+ });
41
+ it("does not append guidance when ant-colony extension is disabled", () => {
42
+ const dir = makeTempDir();
43
+ writeAgents(dir, makeConfig({
44
+ agents: "general-developer",
45
+ extensions: [],
46
+ }));
47
+ const content = readFileSync(join(dir, "AGENTS.md"), "utf8");
48
+ expect(content).not.toContain("## Ant Colony Auto-Trigger");
49
+ });
50
+ it("does not append duplicate guidance for colony-operator template", () => {
51
+ const dir = makeTempDir();
52
+ writeAgents(dir, makeConfig({
53
+ agents: "colony-operator",
54
+ extensions: ["ant-colony"],
55
+ }));
56
+ const content = readFileSync(join(dir, "AGENTS.md"), "utf8");
57
+ expect(content).not.toContain("## Ant Colony Auto-Trigger");
58
+ expect(content).toContain("You command an autonomous ant colony");
59
+ });
60
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-pi",
3
- "version": "0.1.76",
3
+ "version": "0.1.78",
4
4
  "description": "One-click setup for pi-coding-agent. Like oh-my-zsh for pi.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,9 @@
10
10
  "files": [
11
11
  "dist",
12
12
  "pi-package",
13
- "README.md"
13
+ "README.md",
14
+ "!**/*.test.ts",
15
+ "!**/*.spec.ts"
14
16
  ],
15
17
  "scripts": {
16
18
  "build": "tsc",
@@ -17,8 +17,9 @@ You command an autonomous ant colony. Complex tasks are delegated to the swarm,
17
17
  ## Workflow
18
18
  1. Assess task scope
19
19
  2. If colony-worthy → use `ant_colony` tool with clear goal
20
- 3. If simple do it directly
21
- 4. Review colony output, fix gaps manually if needed
20
+ 3. After launch, use passive mode: wait for `COLONY_SIGNAL:*` updates; do not poll `bg_colony_status` unless user explicitly asks
21
+ 4. If simple do it directly
22
+ 5. Review colony output, fix gaps manually if needed
22
23
 
23
24
  ## Code Standards
24
25
  - Follow existing conventions
@@ -59,6 +59,44 @@ export default function antColonyExtension(pi: ExtensionAPI) {
59
59
  // 当前运行中的后台蚁群(同时只允许一个)
60
60
  let activeColony: BackgroundColony | null = null;
61
61
 
62
+ // 防止主进程主动轮询导致阻塞:仅允许显式请求的手动快照,并加冷却
63
+ let lastBgStatusSnapshotAt = 0;
64
+ const STATUS_SNAPSHOT_COOLDOWN_MS = 15_000;
65
+
66
+ const extractMessageText = (message: any): string => {
67
+ const c = message?.content;
68
+ if (typeof c === "string") return c;
69
+ if (Array.isArray(c)) {
70
+ return c.map((p: any) => {
71
+ if (typeof p === "string") return p;
72
+ if (typeof p?.text === "string") return p.text;
73
+ if (typeof p?.content === "string") return p.content;
74
+ return "";
75
+ }).join("\n");
76
+ }
77
+ return "";
78
+ };
79
+
80
+ const lastUserMessageText = (ctx: any): string => {
81
+ try {
82
+ const branch = ctx?.sessionManager?.getBranch?.() ?? [];
83
+ for (let i = branch.length - 1; i >= 0; i--) {
84
+ const e = branch[i];
85
+ if (e?.type === "message" && e.message?.role === "user") {
86
+ return extractMessageText(e.message).trim();
87
+ }
88
+ }
89
+ } catch {
90
+ // ignore
91
+ }
92
+ return "";
93
+ };
94
+
95
+ const isExplicitStatusRequest = (ctx: any): boolean => {
96
+ const text = lastUserMessageText(ctx);
97
+ return /(?:\/colony-status|bg_colony_status)|(?:(?:蚁群|colony).{0,20}(?:状态|进度|进展|汇报|快照|status|progress|snapshot|update|check))|(?:(?:状态|进度|进展|汇报|快照|status|progress|snapshot|update|check).{0,20}(?:蚁群|colony))/i.test(text);
98
+ };
99
+
62
100
  const calcProgress = (m?: ColonyMetrics | null) => {
63
101
  if (!m || m.tasksTotal <= 0) return 0;
64
102
  return Math.max(0, Math.min(1, m.tasksDone / m.tasksTotal));
@@ -280,6 +318,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
280
318
  colony.promise = resume ? resumeColony(colonyOpts) : runColony(colonyOpts);
281
319
 
282
320
  activeColony = colony;
321
+ lastBgStatusSnapshotAt = 0;
283
322
  throttledRender();
284
323
 
285
324
  // 后台等待完成,注入结果
@@ -627,7 +666,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
627
666
  launchBackgroundColony(colonyParams);
628
667
 
629
668
  return {
630
- content: [{ type: "text", text: `[COLONY_SIGNAL:LAUNCHED]\n🐜 Colony launched in background.\nGoal: ${params.goal}\n\nThe colony is now running autonomously. Results will be injected when it finishes.` }],
669
+ content: [{ type: "text", text: `[COLONY_SIGNAL:LAUNCHED]\n🐜 Colony launched in background.\nGoal: ${params.goal}\n\nThe colony runs autonomously in passive mode. Progress is pushed via [COLONY_SIGNAL:*] follow-up messages. Do not poll bg_colony_status unless the user explicitly asks for a manual snapshot.` }],
631
670
  };
632
671
  },
633
672
 
@@ -689,9 +728,40 @@ export default function antColonyExtension(pi: ExtensionAPI) {
689
728
  pi.registerTool({
690
729
  name: "bg_colony_status",
691
730
  label: "Colony Status",
692
- description: "Check the status of a running background ant colony. Use this instead of bg_status to monitor colony progress.",
731
+ description: "Optional manual snapshot for a running colony. Progress is pushed passively via COLONY_SIGNAL follow-up messages; call this only when the user explicitly asks.",
693
732
  parameters: Type.Object({}),
694
- async execute() {
733
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
734
+ if (!activeColony) {
735
+ return {
736
+ content: [{ type: "text" as const, text: "No colony is currently running." }],
737
+ };
738
+ }
739
+
740
+ const explicit = isExplicitStatusRequest(ctx);
741
+ if (!explicit) {
742
+ return {
743
+ content: [{
744
+ type: "text" as const,
745
+ text: "Passive mode is active. Colony progress is already pushed via [COLONY_SIGNAL:*] follow-up messages. Skipping bg_colony_status polling to avoid blocking the main process. Ask explicitly for a manual snapshot if needed.",
746
+ }],
747
+ isError: true,
748
+ };
749
+ }
750
+
751
+ const now = Date.now();
752
+ const delta = now - lastBgStatusSnapshotAt;
753
+ if (delta < STATUS_SNAPSHOT_COOLDOWN_MS) {
754
+ const waitSec = Math.ceil((STATUS_SNAPSHOT_COOLDOWN_MS - delta) / 1000);
755
+ return {
756
+ content: [{
757
+ type: "text" as const,
758
+ text: `Manual status snapshot is rate-limited. Please wait ${waitSec}s to avoid active polling loops.`,
759
+ }],
760
+ isError: true,
761
+ };
762
+ }
763
+
764
+ lastBgStatusSnapshotAt = now;
695
765
  return {
696
766
  content: [{ type: "text" as const, text: buildStatusText() }],
697
767
  };
@@ -1,70 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { defaultConcurrency, adapt } from "./concurrency.js";
3
- import type { ConcurrencyConfig, ConcurrencySample } from "./types.js";
4
-
5
- const mkSample = (o: Partial<ConcurrencySample> = {}): ConcurrencySample => ({
6
- timestamp: Date.now(), concurrency: 2, cpuLoad: 0.3, memFree: 4e9, throughput: 1, ...o,
7
- });
8
-
9
- describe("defaultConcurrency", () => {
10
- it("returns valid config", () => {
11
- const c = defaultConcurrency();
12
- expect(c.current).toBe(2);
13
- expect(c.min).toBe(1);
14
- expect(c.max).toBeGreaterThanOrEqual(1);
15
- expect(c.max).toBeLessThanOrEqual(8);
16
- expect(c.history).toEqual([]);
17
- });
18
- });
19
-
20
- describe("adapt", () => {
21
- it("drops to min when no pending tasks", () => {
22
- const cfg: ConcurrencyConfig = { current: 4, min: 1, max: 8, optimal: 3, history: [mkSample(), mkSample()] };
23
- expect(adapt(cfg, 0).current).toBe(1);
24
- });
25
-
26
- it("cold start gives half max", () => {
27
- const cfg: ConcurrencyConfig = { current: 2, min: 1, max: 8, optimal: 3, history: [mkSample()] };
28
- expect(adapt(cfg, 10).current).toBe(4);
29
- });
30
-
31
- it("reduces when CPU > 85%", () => {
32
- const s = mkSample({ cpuLoad: 0.9 });
33
- const cfg: ConcurrencyConfig = { current: 4, min: 1, max: 8, optimal: 3, history: [s, s, s] };
34
- expect(adapt(cfg, 10).current).toBeLessThan(4);
35
- });
36
-
37
- it("reduces when memory low", () => {
38
- const s = mkSample({ memFree: 100 * 1024 * 1024 });
39
- const cfg: ConcurrencyConfig = { current: 4, min: 1, max: 8, optimal: 3, history: [s, s] };
40
- expect(adapt(cfg, 10).current).toBeLessThan(4);
41
- });
42
-
43
- it("increases during exploration when throughput rising", () => {
44
- const s1 = mkSample({ throughput: 1 });
45
- const s2 = mkSample({ throughput: 2 });
46
- const cfg: ConcurrencyConfig = { current: 3, min: 1, max: 8, optimal: 3, history: [s1, s2] };
47
- expect(adapt(cfg, 10).current).toBeGreaterThanOrEqual(3);
48
- });
49
-
50
- it("does not exceed max", () => {
51
- const s1 = mkSample({ throughput: 1 });
52
- const s2 = mkSample({ throughput: 5 });
53
- const cfg: ConcurrencyConfig = { current: 8, min: 1, max: 8, optimal: 3, history: [s1, s2] };
54
- expect(adapt(cfg, 100).current).toBeLessThanOrEqual(8);
55
- });
56
-
57
- it("does not exceed pending task count", () => {
58
- const s1 = mkSample({ throughput: 1 });
59
- const s2 = mkSample({ throughput: 5 });
60
- const cfg: ConcurrencyConfig = { current: 3, min: 1, max: 8, optimal: 3, history: [s1, s2] };
61
- expect(adapt(cfg, 2).current).toBeLessThanOrEqual(2);
62
- });
63
-
64
- it("respects rate limit cooldown", () => {
65
- const s1 = mkSample({ throughput: 1 });
66
- const s2 = mkSample({ throughput: 2 });
67
- const cfg: ConcurrencyConfig = { current: 3, min: 1, max: 8, optimal: 3, history: [s1, s2], lastRateLimitAt: Date.now() };
68
- expect(adapt(cfg, 10).current).toBeLessThanOrEqual(3);
69
- });
70
- });
@@ -1,62 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { buildImportGraph, dependencyDepth, taskDependsOn } from "./deps.js";
3
- import type { ImportGraph } from "./deps.js";
4
-
5
- describe("buildImportGraph", () => {
6
- it("returns empty graph for empty files", () => {
7
- const graph = buildImportGraph([], "/tmp");
8
- expect(graph.imports.size).toBe(0);
9
- expect(graph.importedBy.size).toBe(0);
10
- });
11
-
12
- it("returns empty graph for nonexistent files", () => {
13
- const graph = buildImportGraph(["nonexistent.ts"], "/tmp");
14
- expect(graph.imports.size).toBe(0);
15
- });
16
- });
17
-
18
- describe("dependencyDepth", () => {
19
- it("returns 0 for file with no dependents", () => {
20
- const graph: ImportGraph = { imports: new Map(), importedBy: new Map() };
21
- expect(dependencyDepth("a.ts", graph)).toBe(0);
22
- });
23
-
24
- it("counts direct dependents", () => {
25
- const graph: ImportGraph = {
26
- imports: new Map([["b.ts", new Set(["a.ts"])]]),
27
- importedBy: new Map([["a.ts", new Set(["b.ts"])]]),
28
- };
29
- expect(dependencyDepth("a.ts", graph)).toBe(1);
30
- });
31
-
32
- it("counts transitive dependents", () => {
33
- const graph: ImportGraph = {
34
- imports: new Map([["b.ts", new Set(["a.ts"])], ["c.ts", new Set(["b.ts"])]]),
35
- importedBy: new Map([["a.ts", new Set(["b.ts"])], ["b.ts", new Set(["c.ts"])]]),
36
- };
37
- expect(dependencyDepth("a.ts", graph)).toBe(2);
38
- });
39
- });
40
-
41
- describe("taskDependsOn", () => {
42
- it("returns true when taskA imports taskB file", () => {
43
- const graph: ImportGraph = {
44
- imports: new Map([["a.ts", new Set(["b.ts"])]]),
45
- importedBy: new Map([["b.ts", new Set(["a.ts"])]]),
46
- };
47
- expect(taskDependsOn(["a.ts"], ["b.ts"], graph)).toBe(true);
48
- });
49
-
50
- it("returns false when no dependency", () => {
51
- const graph: ImportGraph = {
52
- imports: new Map([["a.ts", new Set(["c.ts"])]]),
53
- importedBy: new Map(),
54
- };
55
- expect(taskDependsOn(["a.ts"], ["b.ts"], graph)).toBe(false);
56
- });
57
-
58
- it("returns false for empty file lists", () => {
59
- const graph: ImportGraph = { imports: new Map(), importedBy: new Map() };
60
- expect(taskDependsOn([], [], graph)).toBe(false);
61
- });
62
- });
@@ -1,130 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import * as os from "node:os";
5
- import { Nest } from "./nest.js";
6
- import type { ColonyState, Pheromone } from "./types.js";
7
-
8
- const mkState = (overrides: Partial<ColonyState> = {}): ColonyState => ({
9
- id: "test-colony", goal: "test", status: "working",
10
- tasks: [], ants: [], pheromones: [],
11
- concurrency: { current: 2, min: 1, max: 4, optimal: 3, history: [] },
12
- metrics: { tasksTotal: 0, tasksDone: 0, tasksFailed: 0, antsSpawned: 0, totalCost: 0, totalTokens: 0, startTime: Date.now(), throughputHistory: [] },
13
- maxCost: null, modelOverrides: {}, createdAt: Date.now(), finishedAt: null,
14
- ...overrides,
15
- });
16
-
17
- const mkPheromone = (overrides: Partial<Pheromone> = {}): Pheromone => ({
18
- id: `p-${Math.random().toString(36).slice(2)}`, type: "warning", antId: "ant-1", antCaste: "worker",
19
- taskId: "t-1", content: "test", files: ["a.ts"], strength: 1.0, createdAt: Date.now(),
20
- ...overrides,
21
- });
22
-
23
- let tmpDir: string;
24
- let nest: Nest;
25
-
26
- beforeEach(() => {
27
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nest-test-"));
28
- nest = new Nest(tmpDir, "test-colony");
29
- nest.init(mkState());
30
- });
31
-
32
- afterEach(() => {
33
- try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
34
- });
35
-
36
- describe("getStateLight", () => {
37
- it("returns state without triggering pheromone read", () => {
38
- nest.dropPheromone(mkPheromone());
39
- const light = nest.getStateLight();
40
- expect(light.id).toBe("test-colony");
41
- expect(light.tasks).toEqual([]);
42
- // pheromones should not be populated by getStateLight
43
- // (it returns stateCache which has empty pheromones from init)
44
- });
45
-
46
- it("includes tasks from cache", () => {
47
- nest.writeTask({
48
- id: "t-1", parentId: null, title: "Test", description: "desc",
49
- caste: "worker", status: "pending", priority: 3, files: [],
50
- claimedBy: null, result: null, error: null, spawnedTasks: [],
51
- createdAt: Date.now(), startedAt: null, finishedAt: null,
52
- });
53
- const light = nest.getStateLight();
54
- expect(light.tasks).toHaveLength(1);
55
- expect(light.tasks[0].id).toBe("t-1");
56
- });
57
- });
58
-
59
- describe("countWarnings", () => {
60
- it("returns 0 when no pheromones", () => {
61
- expect(nest.countWarnings(["a.ts"])).toBe(0);
62
- });
63
-
64
- it("counts warning pheromones for matching files", () => {
65
- nest.dropPheromone(mkPheromone({ type: "warning", files: ["a.ts"] }));
66
- nest.dropPheromone(mkPheromone({ type: "warning", files: ["a.ts"] }));
67
- nest.dropPheromone(mkPheromone({ type: "completion", files: ["a.ts"] }));
68
- expect(nest.countWarnings(["a.ts"])).toBe(2);
69
- });
70
-
71
- it("counts repellent pheromones", () => {
72
- nest.dropPheromone(mkPheromone({ type: "repellent", files: ["b.ts"] }));
73
- expect(nest.countWarnings(["b.ts"])).toBe(1);
74
- });
75
-
76
- it("returns 0 for unrelated files", () => {
77
- nest.dropPheromone(mkPheromone({ type: "warning", files: ["a.ts"] }));
78
- expect(nest.countWarnings(["c.ts"])).toBe(0);
79
- });
80
- });
81
-
82
- describe("pheromone dirty flag", () => {
83
- it("rebuilds index after dropPheromone", () => {
84
- nest.dropPheromone(mkPheromone({ type: "discovery", files: ["x.ts"] }));
85
- const pheromones = nest.getAllPheromones();
86
- expect(pheromones.length).toBe(1);
87
- expect(pheromones[0].type).toBe("discovery");
88
- });
89
-
90
- it("does not rebuild index when nothing changed", () => {
91
- nest.dropPheromone(mkPheromone({ files: ["x.ts"] }));
92
- nest.getAllPheromones(); // builds index, clears dirty
93
- // Second call should use cached index (no new data, no GC)
94
- const p2 = nest.getAllPheromones();
95
- expect(p2.length).toBe(1);
96
- });
97
- });
98
-
99
- describe("claimNextTask", () => {
100
- it("claims highest scored pending task", () => {
101
- nest.writeTask({
102
- id: "t-low", parentId: null, title: "Low", description: "",
103
- caste: "worker", status: "pending", priority: 5, files: [],
104
- claimedBy: null, result: null, error: null, spawnedTasks: [],
105
- createdAt: Date.now(), startedAt: null, finishedAt: null,
106
- });
107
- nest.writeTask({
108
- id: "t-high", parentId: null, title: "High", description: "",
109
- caste: "worker", status: "pending", priority: 1, files: [],
110
- claimedBy: null, result: null, error: null, spawnedTasks: [],
111
- createdAt: Date.now(), startedAt: null, finishedAt: null,
112
- });
113
- const claimed = nest.claimNextTask("worker", "ant-1");
114
- expect(claimed).not.toBeNull();
115
- expect(claimed!.id).toBe("t-high");
116
- expect(claimed!.status).toBe("claimed");
117
- });
118
-
119
- it("returns null when no pending tasks", () => {
120
- expect(nest.claimNextTask("worker", "ant-1")).toBeNull();
121
- });
122
- });
123
-
124
- describe("withStateLock spin", () => {
125
- it("updateState works under normal conditions", () => {
126
- nest.updateState({ status: "reviewing" });
127
- const state = nest.getStateLight();
128
- expect(state.status).toBe("reviewing");
129
- });
130
- });
@@ -1,143 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
-
3
- vi.mock("@mariozechner/pi-coding-agent", () => ({
4
- AuthStorage: class {},
5
- createAgentSession: vi.fn(),
6
- createReadTool: vi.fn(), createBashTool: vi.fn(), createEditTool: vi.fn(),
7
- createWriteTool: vi.fn(), createGrepTool: vi.fn(), createFindTool: vi.fn(),
8
- createLsTool: vi.fn(), ModelRegistry: class {}, SessionManager: { inMemory: vi.fn() },
9
- SettingsManager: { inMemory: vi.fn() }, createExtensionRuntime: vi.fn(),
10
- }));
11
- vi.mock("@mariozechner/pi-ai", () => ({ getModel: vi.fn() }));
12
- vi.mock("./spawner.js", async () => {
13
- const actual = await vi.importActual<any>("./spawner.js");
14
- return { ...actual, makePheromoneId: () => "p-test" };
15
- });
16
-
17
- import { parseSubTasks, extractPheromones } from "./parser.js";
18
-
19
- describe("parseSubTasks", () => {
20
- it("parses markdown TASK blocks", () => {
21
- const output = `## Recommended Tasks
22
- ### TASK: Fix login
23
- - description: Fix the login bug
24
- - files: src/auth.ts
25
- - caste: worker
26
- - priority: 2`;
27
- const tasks = parseSubTasks(output);
28
- expect(tasks).toHaveLength(1);
29
- expect(tasks[0].title).toBe("Fix login");
30
- expect(tasks[0].description).toBe("Fix the login bug");
31
- expect(tasks[0].files).toEqual(["src/auth.ts"]);
32
- expect(tasks[0].caste).toBe("worker");
33
- expect(tasks[0].priority).toBe(2);
34
- });
35
-
36
- it("parses JSON block", () => {
37
- const output = '```json\n[{"title":"Task A","description":"Do A","files":["a.ts"],"caste":"scout","priority":1}]\n```';
38
- const tasks = parseSubTasks(output);
39
- expect(tasks).toHaveLength(1);
40
- expect(tasks[0].title).toBe("Task A");
41
- expect(tasks[0].caste).toBe("scout");
42
- });
43
-
44
- it("defaults caste to worker for invalid", () => {
45
- const tasks = parseSubTasks('```json\n[{"title":"X","caste":"invalid"}]\n```');
46
- expect(tasks[0].caste).toBe("worker");
47
- });
48
-
49
- it("defaults priority to 3", () => {
50
- const tasks = parseSubTasks('```json\n[{"title":"X"}]\n```');
51
- expect(tasks[0].priority).toBe(3);
52
- });
53
-
54
- it("returns empty for no tasks", () => {
55
- expect(parseSubTasks("no tasks here")).toEqual([]);
56
- });
57
-
58
- it("parses multiple markdown tasks", () => {
59
- const output = `### TASK: A
60
- - description: Do A
61
- - files: a.ts
62
- - caste: worker
63
- - priority: 1
64
-
65
- ### TASK: B
66
- - description: Do B
67
- - files: b.ts
68
- - caste: soldier
69
- - priority: 2`;
70
- const tasks = parseSubTasks(output);
71
- expect(tasks).toHaveLength(2);
72
- });
73
-
74
- it("parses context field", () => {
75
- const output = `### TASK: Fix it
76
- - description: Fix bug
77
- - files: x.ts
78
- - caste: worker
79
- - priority: 3
80
- - context: some relevant code`;
81
- const tasks = parseSubTasks(output);
82
- expect(tasks[0].context).toBeTruthy();
83
- });
84
-
85
- it("parses chinese task format with full-width colon", () => {
86
- const output = `### 任务:生成重启检查报告
87
- - 描述:创建重启检查文档
88
- - 文件:docs/ant-colony-restart-check.md
89
- - 角色:worker
90
- - 优先级:1`;
91
- const tasks = parseSubTasks(output);
92
- expect(tasks).toHaveLength(1);
93
- expect(tasks[0].title).toContain("重启检查报告");
94
- expect(tasks[0].files).toEqual(["docs/ant-colony-restart-check.md"]);
95
- expect(tasks[0].caste).toBe("worker");
96
- expect(tasks[0].priority).toBe(1);
97
- });
98
-
99
- it("does not infer tasks from plain next-step narrative", () => {
100
- const output = `目前发现如下\n\n下一步我会继续定位:
101
- - 写入 docs/ant-colony-restart-check.md
102
- - 校验 src/index.ts 的入口流程`;
103
- const tasks = parseSubTasks(output);
104
- expect(tasks).toEqual([]);
105
- });
106
-
107
- it("parses bold markdown field keys", () => {
108
- const output = `### TASK: Harden parser
109
- - **description**: support bold fields
110
- - **files**: pi-package/extensions/ant-colony/parser.ts
111
- - **caste**: worker
112
- - **priority**: 2`;
113
- const tasks = parseSubTasks(output);
114
- expect(tasks).toHaveLength(1);
115
- expect(tasks[0].files).toEqual(["pi-package/extensions/ant-colony/parser.ts"]);
116
- });
117
- });
118
-
119
- describe("extractPheromones", () => {
120
- it("extracts discovery section", () => {
121
- const p = extractPheromones("ant-1", "scout", "t-1", "## Discoveries\n- Found auth\n\n## Other\nstuff", ["a.ts"]);
122
- expect(p.some(x => x.type === "discovery")).toBe(true);
123
- });
124
-
125
- it("extracts warning section", () => {
126
- const p = extractPheromones("ant-1", "scout", "t-1", "## Warnings\n- Conflict\n", []);
127
- expect(p.some(x => x.type === "warning")).toBe(true);
128
- });
129
-
130
- it("adds repellent on failure", () => {
131
- const p = extractPheromones("ant-1", "worker", "t-1", "output", ["a.ts"], true);
132
- expect(p.some(x => x.type === "repellent")).toBe(true);
133
- });
134
-
135
- it("returns empty for no matching sections", () => {
136
- expect(extractPheromones("ant-1", "worker", "t-1", "nothing", [])).toEqual([]);
137
- });
138
-
139
- it("extracts Files Changed as completion", () => {
140
- const p = extractPheromones("ant-1", "worker", "t-1", "## Files Changed\n- src/foo.ts\n", ["src/foo.ts"]);
141
- expect(p.some(x => x.type === "completion")).toBe(true);
142
- });
143
- });