oh-pi 0.1.55 → 0.1.56

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.55",
3
+ "version": "0.1.56",
4
4
  "description": "One-click setup for pi-coding-agent. Like oh-my-zsh for pi.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,94 @@
1
+ /**
2
+ * 轻量 import graph — 静态分析 ts/js 文件的依赖关系
3
+ */
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+
7
+ export interface ImportGraph {
8
+ /** file → files it imports */
9
+ imports: Map<string, Set<string>>;
10
+ /** file → files that import it */
11
+ importedBy: Map<string, Set<string>>;
12
+ }
13
+
14
+ /** 从 import/require 语句中提取相对路径 */
15
+ const IMPORT_RE = /(?:import|export)\s+.*?from\s+['"](\.[^'"]+)['"]/g;
16
+ const REQUIRE_RE = /require\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g;
17
+
18
+ function resolveImport(from: string, specifier: string, cwd: string): string | null {
19
+ const dir = path.dirname(path.resolve(cwd, from));
20
+ const base = path.resolve(dir, specifier);
21
+ const exts = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.js"];
22
+ for (const ext of exts) {
23
+ const full = base + ext;
24
+ if (fs.existsSync(full)) return path.relative(cwd, full);
25
+ }
26
+ return null;
27
+ }
28
+
29
+ /**
30
+ * 构建项目的 import graph
31
+ * @param files - 相对于 cwd 的文件路径列表
32
+ * @param cwd - 项目根目录
33
+ */
34
+ export function buildImportGraph(files: string[], cwd: string): ImportGraph {
35
+ const imports = new Map<string, Set<string>>();
36
+ const importedBy = new Map<string, Set<string>>();
37
+
38
+ for (const file of files) {
39
+ const abs = path.resolve(cwd, file);
40
+ if (!fs.existsSync(abs)) continue;
41
+ let content: string;
42
+ try { content = fs.readFileSync(abs, "utf-8"); } catch { continue; }
43
+
44
+ const deps = new Set<string>();
45
+ for (const re of [IMPORT_RE, REQUIRE_RE]) {
46
+ re.lastIndex = 0;
47
+ for (const m of content.matchAll(re)) {
48
+ const resolved = resolveImport(file, m[1], cwd);
49
+ if (resolved) deps.add(resolved);
50
+ }
51
+ }
52
+
53
+ imports.set(file, deps);
54
+ for (const dep of deps) {
55
+ if (!importedBy.has(dep)) importedBy.set(dep, new Set());
56
+ importedBy.get(dep)!.add(file);
57
+ }
58
+ }
59
+
60
+ return { imports, importedBy };
61
+ }
62
+
63
+ /**
64
+ * 计算文件的依赖深度(被多少文件直接或间接依赖)
65
+ * 深度越高 = 越底层 = 应优先处理
66
+ */
67
+ export function dependencyDepth(file: string, graph: ImportGraph): number {
68
+ const visited = new Set<string>();
69
+ const queue = [file];
70
+ while (queue.length > 0) {
71
+ const f = queue.pop()!;
72
+ if (visited.has(f)) continue;
73
+ visited.add(f);
74
+ const dependents = graph.importedBy.get(f);
75
+ if (dependents) for (const d of dependents) queue.push(d);
76
+ }
77
+ return visited.size - 1; // 不算自己
78
+ }
79
+
80
+ /**
81
+ * 检查 taskA 的文件是否依赖 taskB 的文件
82
+ * 即 taskA 的某个文件 import 了 taskB 的某个文件
83
+ */
84
+ export function taskDependsOn(taskAFiles: string[], taskBFiles: string[], graph: ImportGraph): boolean {
85
+ for (const a of taskAFiles) {
86
+ const deps = graph.imports.get(a);
87
+ if (deps) {
88
+ for (const b of taskBFiles) {
89
+ if (deps.has(b)) return true;
90
+ }
91
+ }
92
+ }
93
+ return false;
94
+ }
@@ -22,6 +22,7 @@ import { DEFAULT_ANT_CONFIGS } from "./types.js";
22
22
  import { Nest } from "./nest.js";
23
23
  import { spawnAnt, runDrone, makeTaskId } from "./spawner.js";
24
24
  import { adapt, sampleSystem, defaultConcurrency } from "./concurrency.js";
25
+ import { buildImportGraph, taskDependsOn, type ImportGraph } from "./deps.js";
25
26
  import type { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
26
27
 
27
28
  export interface QueenCallbacks {
@@ -149,6 +150,7 @@ interface WaveOptions {
149
150
  callbacks: QueenCallbacks;
150
151
  authStorage?: AuthStorage;
151
152
  modelRegistry?: ModelRegistry;
153
+ importGraph?: ImportGraph;
152
154
  }
153
155
 
154
156
  /**
@@ -188,11 +190,13 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
188
190
 
189
191
  // 蚂蚁产生的子任务加入巢穴
190
192
  for (const sub of result.newTasks) {
191
- // 检查文件锁冲突
193
+ // 检查文件锁冲突和依赖冲突
192
194
  const allTasks = nest.getAllTasks();
193
195
  const conflicting = allTasks.find(t =>
194
- t.status === "active" &&
195
- t.files.some(f => sub.files.includes(f))
196
+ t.status === "active" && (
197
+ t.files.some(f => sub.files.includes(f)) ||
198
+ (opts.importGraph && taskDependsOn(sub.files, t.files, opts.importGraph))
199
+ )
196
200
  );
197
201
  const child = childTaskFromParsed(task.id, sub);
198
202
  if (conflicting) {
@@ -230,11 +234,15 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
230
234
  await new Promise(r => setTimeout(r, backoffMs));
231
235
  }
232
236
 
233
- // 解除 blocked 任务(如果锁定文件已释放)
237
+ // 解除 blocked 任务(如果锁定文件和依赖文件都已释放)
234
238
  const activeTasks = state.tasks.filter(t => t.status === "active");
235
239
  const activeFiles = new Set(activeTasks.flatMap(t => t.files));
236
240
  for (const t of state.tasks.filter(t => t.status === "blocked" && t.caste === caste)) {
237
- if (!t.files.some(f => activeFiles.has(f))) {
241
+ const fileConflict = t.files.some(f => activeFiles.has(f));
242
+ const depConflict = opts.importGraph && activeTasks.some(at =>
243
+ taskDependsOn(t.files, at.files, opts.importGraph!)
244
+ );
245
+ if (!fileConflict && !depConflict) {
238
246
  nest.updateTaskStatus(t.id, "pending");
239
247
  }
240
248
  }
@@ -324,7 +332,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
324
332
 
325
333
  nest.init(initialState);
326
334
  const { signal, callbacks } = opts;
327
- const waveBase: Omit<WaveOptions, "caste"> = {
335
+ const waveBase: Omit<WaveOptions, "caste"> & { importGraph?: ImportGraph } = {
328
336
  nest, cwd: opts.cwd, signal, callbacks,
329
337
  currentModel: opts.currentModel,
330
338
  modelOverrides: opts.modelOverrides,
@@ -387,6 +395,16 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
387
395
  // ═══ Phase 2: 工作 ═══
388
396
  nest.updateState({ status: "working" });
389
397
 
398
+ // 构建 import graph 用于依赖感知调度
399
+ let importGraph: ImportGraph | undefined;
400
+ try {
401
+ const allFiles = nest.getAllTasks().flatMap(t => t.files).filter(f => /\.[tj]sx?$/.test(f));
402
+ if (allFiles.length > 0) {
403
+ importGraph = buildImportGraph([...new Set(allFiles)], opts.cwd);
404
+ waveBase.importGraph = importGraph;
405
+ }
406
+ } catch { /* graph build failed, proceed without */ }
407
+
390
408
  // 先执行 drone 任务(零 LLM 成本)
391
409
  const droneTasks = nest.getAllTasks().filter(t => t.caste === "drone" && t.status === "pending");
392
410
  if (droneTasks.length > 0) {
@@ -168,7 +168,7 @@ function parseSubTasks(output: string): ParsedSubTask[] {
168
168
  tasks.push({
169
169
  title: m[1]?.trim() || "Untitled",
170
170
  description: m[2]?.trim() || m[1]?.trim() || "",
171
- files: (m[3]?.trim() || "").split(",").map(f => f.trim()).filter(Boolean),
171
+ files: (m[3]?.trim() || "").split(",").map((f: string) => f.trim()).filter(Boolean),
172
172
  caste: (m[4]?.trim() as AntCaste) || "worker",
173
173
  priority: (parseInt(m[5] || "3") as 1 | 2 | 3 | 4 | 5) || 3,
174
174
  context,
@@ -361,7 +361,7 @@ export async function spawnAnt(
361
361
  });
362
362
 
363
363
  // 订阅实时事件
364
- session.subscribe((event) => {
364
+ session.subscribe((event: any) => {
365
365
  if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
366
366
  const delta = event.assistantMessageEvent.delta;
367
367
  accumulatedText += delta;