@ifi/oh-pi-ant-colony 0.2.1
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/LICENSE +21 -0
- package/README.md +119 -0
- package/extensions/ant-colony/concurrency.ts +127 -0
- package/extensions/ant-colony/deps.ts +156 -0
- package/extensions/ant-colony/index.ts +969 -0
- package/extensions/ant-colony/nest.ts +542 -0
- package/extensions/ant-colony/parser.ts +236 -0
- package/extensions/ant-colony/prompts.ts +111 -0
- package/extensions/ant-colony/queen.ts +1041 -0
- package/extensions/ant-colony/spawner.ts +380 -0
- package/extensions/ant-colony/types.ts +152 -0
- package/extensions/ant-colony/ui.ts +139 -0
- package/package.json +26 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ifiok Jr.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# 🐜 Ant Colony — Multi-Agent Swarm Extension
|
|
2
|
+
|
|
3
|
+
> A self-organizing multi-agent system modeled after real ant colony ecology. Adaptive concurrency,
|
|
4
|
+
> pheromone communication, zero centralized scheduling.
|
|
5
|
+
|
|
6
|
+
## Architecture
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
Queen Main pi process, receives goals, orchestrates lifecycle
|
|
10
|
+
│
|
|
11
|
+
├─ 🔍 Scout Lightweight haiku, explores paths, marks food sources
|
|
12
|
+
├─ ⚒️ Worker Sonnet, executes tasks, may spawn sub-tasks
|
|
13
|
+
└─ 🛡️ Soldier Sonnet, reviews quality, may request rework
|
|
14
|
+
|
|
15
|
+
Pheromone .ant-colony/ file system, indirect ant-to-ant communication
|
|
16
|
+
Nest Shared state, atomic file operations, cross-process safe
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Lifecycle
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
Goal → Scouting → Task Pool → Workers Execute in Parallel → Soldiers Review → Fix (if needed) → Done
|
|
23
|
+
│ │
|
|
24
|
+
│ Pheromone decay (10min) │ Sub-tasks auto-spawned
|
|
25
|
+
└───────────────────────────┘
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Adaptive Concurrency
|
|
29
|
+
|
|
30
|
+
Models real ant colony dynamic recruitment:
|
|
31
|
+
|
|
32
|
+
- **Cold start**: 1–2 ants, gradual exploration
|
|
33
|
+
- **Exploration phase**: +1 each wave, monitoring throughput inflection point
|
|
34
|
+
- **Steady state**: fine-tune around optimal value
|
|
35
|
+
- **Overload protection**: CPU > 85% or memory < 500MB → auto-reduce
|
|
36
|
+
- **Elastic scaling**: more tasks → recruit; fewer tasks → shrink
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
### Auto-Trigger
|
|
41
|
+
|
|
42
|
+
The LLM automatically invokes the `ant_colony` tool when task complexity warrants it.
|
|
43
|
+
|
|
44
|
+
### Commands
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
/colony-stop Cancel a running colony
|
|
48
|
+
Ctrl+Shift+A Open colony details panel
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Examples
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
/colony Migrate the entire project from CommonJS to ESM, updating all imports/exports and tsconfig
|
|
55
|
+
|
|
56
|
+
/colony Add unit tests for all modules under src/, targeting 80% coverage
|
|
57
|
+
|
|
58
|
+
/colony Refactor auth system from session-based to JWT, maintaining API compatibility
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Pheromone System
|
|
62
|
+
|
|
63
|
+
Ants communicate indirectly through pheromones (stigmergy), not direct messages:
|
|
64
|
+
|
|
65
|
+
| Type | Released By | Meaning |
|
|
66
|
+
| ---------- | ----------- | --------------------------------------- |
|
|
67
|
+
| discovery | Scout | Discovered code structure, dependencies |
|
|
68
|
+
| progress | Worker | Completed changes, file modifications |
|
|
69
|
+
| warning | Soldier | Quality issues, conflict risks |
|
|
70
|
+
| completion | Worker | Task completion marker |
|
|
71
|
+
| dependency | Any | File dependency relationships |
|
|
72
|
+
|
|
73
|
+
Pheromones decay exponentially (10-minute half-life), preventing stale info from misleading
|
|
74
|
+
subsequent ants.
|
|
75
|
+
|
|
76
|
+
## File Locking
|
|
77
|
+
|
|
78
|
+
Each task declares the files it operates on. The queen guarantees:
|
|
79
|
+
|
|
80
|
+
- Only one ant modifies a given file at any time
|
|
81
|
+
- Conflicting tasks are automatically marked `blocked` and resume when locks release
|
|
82
|
+
|
|
83
|
+
## Nest Structure
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
.ant-colony/{colony-id}/
|
|
87
|
+
├── state.json Colony state
|
|
88
|
+
├── pheromone.jsonl Append-only pheromone log
|
|
89
|
+
└── tasks/ One file per task (atomic updates)
|
|
90
|
+
├── t-xxx.json
|
|
91
|
+
└── t-yyy.json
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Installation
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Option 1: Symlink to pi extensions directory
|
|
98
|
+
mkdir -p ~/.pi/agent/extensions/ant-colony
|
|
99
|
+
ln -sf "$(pwd)/pi-package/extensions/ant-colony/index.ts" ~/.pi/agent/extensions/ant-colony/index.ts
|
|
100
|
+
# ... (symlink all .ts files)
|
|
101
|
+
|
|
102
|
+
# Option 2: Install via oh-pi
|
|
103
|
+
npx oh-pi # Select "Full Power" preset
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Module Reference
|
|
107
|
+
|
|
108
|
+
| File | Lines | Responsibility |
|
|
109
|
+
| ---------------- | ----- | -------------------------------------------------------------------------- |
|
|
110
|
+
| `types.ts` | ~150 | Type system: ants, tasks, pheromones, colony state |
|
|
111
|
+
| `nest.ts` | ~500 | Nest: file-system shared state, atomic R/W, pheromone decay |
|
|
112
|
+
| `concurrency.ts` | ~120 | Adaptive concurrency: system sampling, exploration/steady-state adjustment |
|
|
113
|
+
| `spawner.ts` | ~370 | Ant spawning: session management, prompt construction, output parsing |
|
|
114
|
+
| `queen.ts` | ~1000 | Queen scheduling: lifecycle, task waves, multi-round iteration |
|
|
115
|
+
| `index.ts` | ~900 | Extension entry: tool/shortcut registration, TUI rendering |
|
|
116
|
+
| `deps.ts` | ~140 | Lightweight import graph for dependency-aware scheduling |
|
|
117
|
+
| `parser.ts` | ~180 | Sub-task and pheromone extraction from ant output |
|
|
118
|
+
| `prompts.ts` | ~90 | Per-caste system prompts and prompt builder |
|
|
119
|
+
| `ui.ts` | ~140 | Formatting helpers for status bar, overlay, and reports |
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adaptive concurrency control — modeled after ant colony dynamic recruitment.
|
|
3
|
+
*
|
|
4
|
+
* Real ant colonies: more food → more recruitment pheromones → more foragers leave the nest.
|
|
5
|
+
* Mapping: more tasks + idle system → increase concurrency; overloaded/few tasks → decrease.
|
|
6
|
+
*
|
|
7
|
+
* Explores boundaries: gradually increases concurrency at startup, monitoring throughput inflection points.
|
|
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: 2,
|
|
18
|
+
min: 1,
|
|
19
|
+
max: Math.min(CPU_CORES, 8),
|
|
20
|
+
optimal: 3,
|
|
21
|
+
history: [],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Sample current system load (CPU, memory, throughput). */
|
|
26
|
+
export function sampleSystem(activeTasks: number, completedRecently: number, windowMinutes: number): ConcurrencySample {
|
|
27
|
+
const cpus = os.cpus();
|
|
28
|
+
const cpuLoad =
|
|
29
|
+
cpus.reduce((sum, c) => {
|
|
30
|
+
const total = Object.values(c.times).reduce((a, b) => a + b, 0);
|
|
31
|
+
return sum + 1 - c.times.idle / total;
|
|
32
|
+
}, 0) / cpus.length;
|
|
33
|
+
|
|
34
|
+
const mem = os.freemem();
|
|
35
|
+
const throughput = windowMinutes > 0 ? completedRecently / windowMinutes : 0;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
timestamp: Date.now(),
|
|
39
|
+
concurrency: activeTasks,
|
|
40
|
+
cpuLoad,
|
|
41
|
+
memFree: mem,
|
|
42
|
+
throughput,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Adaptive adjustment algorithm.
|
|
48
|
+
*
|
|
49
|
+
* Phase 1 — Exploration (samples < 10): increment by 1 each wave, finding throughput inflection.
|
|
50
|
+
* Phase 2 — Steady state: fine-tune around the optimal value.
|
|
51
|
+
*
|
|
52
|
+
* Constraints:
|
|
53
|
+
* - CPU load > 85% → reduce
|
|
54
|
+
* - Free memory < 500MB → reduce
|
|
55
|
+
* - Throughput declining → revert to previous optimal
|
|
56
|
+
* - No pending tasks → drop to min
|
|
57
|
+
*/
|
|
58
|
+
export function adapt(config: ConcurrencyConfig, pendingTasks: number): ConcurrencyConfig {
|
|
59
|
+
const next = { ...config };
|
|
60
|
+
const samples = config.history;
|
|
61
|
+
|
|
62
|
+
// No pending tasks — drop to minimum
|
|
63
|
+
if (pendingTasks === 0) {
|
|
64
|
+
next.current = config.min;
|
|
65
|
+
return next;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Cap at pending task count
|
|
69
|
+
const taskCap = Math.min(pendingTasks, config.max);
|
|
70
|
+
|
|
71
|
+
if (samples.length < 2) {
|
|
72
|
+
// Cold start: use half of max for fast ramp-up
|
|
73
|
+
next.current = Math.min(Math.ceil(config.max / 2), taskCap);
|
|
74
|
+
return next;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const latest = samples[samples.length - 1];
|
|
78
|
+
const prev = samples[samples.length - 2];
|
|
79
|
+
|
|
80
|
+
// CPU sliding window: average of last 3 samples
|
|
81
|
+
const recentCpuSamples = samples.slice(-3);
|
|
82
|
+
const avgCpu = recentCpuSamples.reduce((s, x) => s + x.cpuLoad, 0) / recentCpuSamples.length;
|
|
83
|
+
|
|
84
|
+
// 429 cooldown: no concurrency increases within 30s of rate limit
|
|
85
|
+
const inRateLimitCooldown = config.lastRateLimitAt != null && Date.now() - config.lastRateLimitAt < 30000;
|
|
86
|
+
|
|
87
|
+
// Hard constraint: reduce immediately on overload (hysteresis: >85% reduce, 60-85% hold)
|
|
88
|
+
if (avgCpu > 0.85 || latest.memFree < 500 * 1024 * 1024) {
|
|
89
|
+
next.current = Math.max(config.min, config.current - 1);
|
|
90
|
+
return next;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Hysteresis band: CPU between 60-85% holds steady
|
|
94
|
+
const canIncrease = avgCpu < 0.6 && !inRateLimitCooldown;
|
|
95
|
+
|
|
96
|
+
// Exploration phase: insufficient samples, ramp up gradually
|
|
97
|
+
if (samples.length < 10) {
|
|
98
|
+
if (latest.throughput >= prev.throughput && canIncrease) {
|
|
99
|
+
next.current = Math.min(config.current + 1, taskCap);
|
|
100
|
+
} else if (latest.throughput < prev.throughput) {
|
|
101
|
+
// Throughput declining — inflection point found
|
|
102
|
+
next.optimal = prev.concurrency;
|
|
103
|
+
next.current = prev.concurrency;
|
|
104
|
+
}
|
|
105
|
+
return next;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Steady state: fine-tune around optimal
|
|
109
|
+
const recentThroughput = samples.slice(-5).reduce((s, x) => s + x.throughput, 0) / 5;
|
|
110
|
+
const olderThroughput = samples.slice(-10, -5).reduce((s, x) => s + x.throughput, 0) / 5;
|
|
111
|
+
|
|
112
|
+
if (recentThroughput > olderThroughput * 1.1 && canIncrease) {
|
|
113
|
+
next.current = Math.min(config.current + 1, taskCap);
|
|
114
|
+
if (recentThroughput > olderThroughput * 1.2) {
|
|
115
|
+
next.optimal = next.current;
|
|
116
|
+
}
|
|
117
|
+
} else if (recentThroughput < olderThroughput * 0.8) {
|
|
118
|
+
next.current = Math.max(config.min, config.optimal);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 429 recovery: restore to optimal when CPU is underutilized (e.g. after backoff)
|
|
122
|
+
if (avgCpu < 0.5 && next.current < config.optimal && !inRateLimitCooldown) {
|
|
123
|
+
next.current = config.optimal;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return next;
|
|
127
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight import graph builder — static analysis of ts/js file dependencies.
|
|
3
|
+
*
|
|
4
|
+
* Parses `import`/`export`/`require` statements to build a bidirectional
|
|
5
|
+
* dependency graph. Used by the queen scheduler to detect file-level
|
|
6
|
+
* dependencies between tasks, preventing workers from editing files that
|
|
7
|
+
* depend on each other simultaneously.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Bidirectional import graph mapping files to their imports and reverse-imports.
|
|
14
|
+
*
|
|
15
|
+
* - `imports`: file → set of files it directly imports
|
|
16
|
+
* - `importedBy`: file → set of files that import it (reverse edges)
|
|
17
|
+
*/
|
|
18
|
+
export interface ImportGraph {
|
|
19
|
+
/** Forward edges: file → files it imports. */
|
|
20
|
+
imports: Map<string, Set<string>>;
|
|
21
|
+
/** Reverse edges: file → files that import it. */
|
|
22
|
+
importedBy: Map<string, Set<string>>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Matches ESM `import ... from './path'` and `export ... from './path'` statements. */
|
|
26
|
+
const IMPORT_RE = /(?:import|export)\s+.*?from\s+['"](\.[^'"]+)['"]/g;
|
|
27
|
+
|
|
28
|
+
/** Matches CommonJS `require('./path')` calls. */
|
|
29
|
+
const REQUIRE_RE = /require\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a relative import specifier to an actual file path.
|
|
33
|
+
* Tries common extensions (.ts, .tsx, .js, .jsx) and index files.
|
|
34
|
+
*
|
|
35
|
+
* @param from - The file that contains the import statement (relative to cwd)
|
|
36
|
+
* @param specifier - The relative import specifier (e.g. `./foo`)
|
|
37
|
+
* @param cwd - Project root directory
|
|
38
|
+
* @returns Resolved path relative to cwd, or null if not found
|
|
39
|
+
*/
|
|
40
|
+
function resolveImport(from: string, specifier: string, cwd: string): string | null {
|
|
41
|
+
const dir = path.dirname(path.resolve(cwd, from));
|
|
42
|
+
const base = path.resolve(dir, specifier);
|
|
43
|
+
const exts = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.js"];
|
|
44
|
+
for (const ext of exts) {
|
|
45
|
+
const full = base + ext;
|
|
46
|
+
if (fs.existsSync(full)) {
|
|
47
|
+
return path.relative(cwd, full);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build a bidirectional import graph from a list of source files.
|
|
55
|
+
* Scans each file for import/require statements and resolves them to
|
|
56
|
+
* relative file paths within the project.
|
|
57
|
+
*
|
|
58
|
+
* @param files - List of file paths relative to `cwd`
|
|
59
|
+
* @param cwd - Project root directory
|
|
60
|
+
* @returns The constructed import graph
|
|
61
|
+
*/
|
|
62
|
+
export function buildImportGraph(files: string[], cwd: string): ImportGraph {
|
|
63
|
+
const imports = new Map<string, Set<string>>();
|
|
64
|
+
const importedBy = new Map<string, Set<string>>();
|
|
65
|
+
|
|
66
|
+
for (const file of files) {
|
|
67
|
+
const abs = path.resolve(cwd, file);
|
|
68
|
+
if (!fs.existsSync(abs)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
let content: string;
|
|
72
|
+
try {
|
|
73
|
+
content = fs.readFileSync(abs, "utf-8");
|
|
74
|
+
} catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const deps = new Set<string>();
|
|
79
|
+
for (const re of [IMPORT_RE, REQUIRE_RE]) {
|
|
80
|
+
re.lastIndex = 0;
|
|
81
|
+
for (const m of content.matchAll(re)) {
|
|
82
|
+
const resolved = resolveImport(file, m[1], cwd);
|
|
83
|
+
if (resolved) {
|
|
84
|
+
deps.add(resolved);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
imports.set(file, deps);
|
|
90
|
+
for (const dep of deps) {
|
|
91
|
+
if (!importedBy.has(dep)) {
|
|
92
|
+
importedBy.set(dep, new Set());
|
|
93
|
+
}
|
|
94
|
+
const dependents = importedBy.get(dep);
|
|
95
|
+
if (dependents) {
|
|
96
|
+
dependents.add(file);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { imports, importedBy };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Calculate the dependency depth of a file — how many files directly or
|
|
106
|
+
* indirectly depend on it. Higher depth = more foundational = should be
|
|
107
|
+
* processed first to avoid cascading breakage.
|
|
108
|
+
*
|
|
109
|
+
* Uses BFS traversal through the `importedBy` reverse edges.
|
|
110
|
+
*
|
|
111
|
+
* @param file - File path relative to cwd
|
|
112
|
+
* @param graph - The import graph to traverse
|
|
113
|
+
* @returns Number of transitive dependents (excluding the file itself)
|
|
114
|
+
*/
|
|
115
|
+
export function dependencyDepth(file: string, graph: ImportGraph): number {
|
|
116
|
+
const visited = new Set<string>();
|
|
117
|
+
const queue = [file];
|
|
118
|
+
while (queue.length > 0) {
|
|
119
|
+
const current = queue.pop();
|
|
120
|
+
if (!current || visited.has(current)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
visited.add(current);
|
|
124
|
+
const dependents = graph.importedBy.get(current);
|
|
125
|
+
if (dependents) {
|
|
126
|
+
for (const d of dependents) {
|
|
127
|
+
queue.push(d);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return visited.size - 1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check whether any file in taskA's scope imports any file in taskB's scope.
|
|
136
|
+
* Used by the queen to detect inter-task dependencies and block conflicting
|
|
137
|
+
* concurrent execution.
|
|
138
|
+
*
|
|
139
|
+
* @param taskAFiles - File paths assigned to task A
|
|
140
|
+
* @param taskBFiles - File paths assigned to task B
|
|
141
|
+
* @param graph - The import graph
|
|
142
|
+
* @returns `true` if task A depends on task B's files
|
|
143
|
+
*/
|
|
144
|
+
export function taskDependsOn(taskAFiles: string[], taskBFiles: string[], graph: ImportGraph): boolean {
|
|
145
|
+
for (const a of taskAFiles) {
|
|
146
|
+
const deps = graph.imports.get(a);
|
|
147
|
+
if (deps) {
|
|
148
|
+
for (const b of taskBFiles) {
|
|
149
|
+
if (deps.has(b)) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|