ndomo 0.1.0 → 0.2.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/.env.example +4 -0
  2. package/README.es.md +29 -23
  3. package/README.md +64 -24
  4. package/bun.lock +447 -0
  5. package/docs/configuration.md +4 -4
  6. package/docs/installation.md +53 -34
  7. package/docs/installer.md +164 -0
  8. package/docs/integrations.md +1 -1
  9. package/docs/web-ui.md +124 -0
  10. package/package.json +43 -4
  11. package/scripts/install.sh +28 -0
  12. package/scripts/smoke-install.sh +47 -0
  13. package/scripts/smoke-web.sh +335 -0
  14. package/src/cli/__tests__/install.test.ts +733 -0
  15. package/src/cli/index.ts +8 -0
  16. package/src/cli/install.ts +1292 -0
  17. package/src/config/__tests__/schema.test.ts +223 -0
  18. package/src/config/schema.ts +129 -16
  19. package/src/http/__tests__/auth.test.ts +10 -10
  20. package/src/http/__tests__/spa.test.ts +296 -0
  21. package/src/http/auth.ts +8 -1
  22. package/src/http/server.ts +71 -2
  23. package/.bun-version +0 -1
  24. package/.dockerignore +0 -79
  25. package/.editorconfig +0 -18
  26. package/.github/CODEOWNERS +0 -8
  27. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -62
  28. package/.github/ISSUE_TEMPLATE/config.yml +0 -2
  29. package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -34
  30. package/.github/dependabot.yml +0 -36
  31. package/.github/pull_request_template.md +0 -24
  32. package/.github/release.yml +0 -30
  33. package/.github/workflows/gitleaks.yml +0 -28
  34. package/.github/workflows/release-please.yml +0 -27
  35. package/.github/workflows/smoke.yml +0 -29
  36. package/.husky/commit-msg +0 -1
  37. package/CHANGELOG.md +0 -114
  38. package/Dockerfile +0 -32
  39. package/bin/ndomo-analyses.ts +0 -4
  40. package/bin/ndomo-status.ts +0 -4
  41. package/biome.json +0 -57
  42. package/commitlint.config.js +0 -3
  43. package/opencode.json +0 -5
  44. package/release-please-config.json +0 -11
  45. package/scripts/dev-bust-cache.sh +0 -164
  46. package/scripts/smoke-e2e.ts +0 -704
  47. package/scripts/smoke-hot.ts +0 -417
  48. package/scripts/smoke-v4.ts +0 -256
  49. package/scripts/smoke-v5.ts +0 -397
  50. package/scripts/uninstall.sh +0 -224
  51. package/src/index.ts +0 -37
  52. package/src/lib.ts +0 -65
  53. package/src/mem/scoped.ts +0 -65
  54. package/src/orchestrator/background.test.ts +0 -268
  55. package/src/orchestrator/background.ts +0 -293
  56. package/src/orchestrator/memory-hook.ts +0 -182
  57. package/src/orchestrator/reconciler.ts +0 -123
  58. package/src/orchestrator/scheduler.test.ts +0 -300
  59. package/src/orchestrator/scheduler.ts +0 -243
  60. package/src/plugin.test.ts +0 -2574
  61. package/src/plugin.ts +0 -1690
  62. package/src/worktrees/manager.ts +0 -236
  63. package/src/worktrees/state.ts +0 -87
  64. package/tests/integration/ranger-flow.test.ts +0 -257
  65. package/tsconfig.json +0 -31
@@ -1,182 +0,0 @@
1
- /**
2
- * Pre-add caveman compression for memory entries.
3
- * Transforms verbose text into compressed caveman format before
4
- * storing in opencode-mem, saving tokens on retrieval.
5
- *
6
- * All compression is regex-based (0 LLM tokens).
7
- */
8
-
9
- /** A memory entry ready for storage. */
10
- export interface MemoryEntry {
11
- /** The content to store. */
12
- content: string;
13
- /** Topic/category for the memory. */
14
- topic: string;
15
- /** Whether this is project-scoped or global. */
16
- scope: "project" | "all-projects";
17
- /** Optional tags for filtering. */
18
- tags?: string[];
19
- }
20
-
21
- /**
22
- * Regex patterns for caveman compression.
23
- * Each pattern is applied sequentially to the text.
24
- */
25
- const COMPRESSION_PATTERNS = [
26
- // Drop leading conjunctions: "And then...", "But actually...", "So basically..."
27
- { pattern: /^(?:and|but|or|so|then|also|well)\s+/gi, replacement: "" },
28
-
29
- // Drop filler adverbs anywhere in the sentence
30
- {
31
- pattern:
32
- /\b(?:just|really|basically|actually|simply|literally|honestly|seriously|obviously|definitely|probably|certainly|essentially|fundamentally|effectively)\b\s*/gi,
33
- replacement: "",
34
- },
35
-
36
- // Drop articles: English
37
- { pattern: /\b(?:the|a|an)\b\s*/gi, replacement: "" },
38
-
39
- // Drop articles: Spanish (for bilingual contexts)
40
- { pattern: /\b(?:el|la|los|las|un|una|unos|unas)\b\s*/gi, replacement: "" },
41
-
42
- // Drop filler phrases
43
- {
44
- pattern:
45
- /\b(?:in order to|due to the fact that|it is important to note that|it should be noted that|as a matter of fact|at the end of the day|for what it's worth|the thing is|what I mean is)\b\s*/gi,
46
- replacement: "",
47
- },
48
-
49
- // Collapse multiple spaces into one
50
- { pattern: / {2,}/g, replacement: " " },
51
-
52
- // Collapse multiple newlines into max two
53
- { pattern: /\n{3,}/g, replacement: "\n\n" },
54
- ] as const;
55
-
56
- /**
57
- * URL pattern to protect URLs from compression.
58
- * Matches http://, https://, and common git/ssh URLs.
59
- */
60
- const URL_PATTERN = /(?:https?:\/\/|git@|ssh:\/\/)[^\s`)\]]+/g;
61
-
62
- /**
63
- * Code block pattern to protect code from compression.
64
- * Matches fenced code blocks: ```...```
65
- */
66
- const CODE_BLOCK_PATTERN = /```[\s\S]*?```/g;
67
-
68
- /**
69
- * Compress text into caveman format using regex transformations.
70
- *
71
- * Preserves:
72
- * - Fenced code blocks (```...```)
73
- * - URLs (http://, https://, git@, ssh://)
74
- *
75
- * Removes:
76
- * - Articles (a, an, the, el, la, los, las, un, una)
77
- * - Filler words (just, really, basically, actually, simply, etc.)
78
- * - Leading conjunctions (and, but, or, so, then, also)
79
- * - Filler phrases ("in order to", "it is important to note that", etc.)
80
- * - Excess whitespace
81
- *
82
- * @param text - Input text to compress.
83
- * @returns Compressed caveman text.
84
- */
85
- export function cavemanCompress(text: string): string {
86
- if (!text || text.trim().length === 0) return text;
87
-
88
- // Step 1: Extract protected regions (code blocks and URLs)
89
- const protectedRegions: Array<{ start: number; end: number; text: string }> = [];
90
-
91
- // Extract code blocks first
92
- for (const match of text.matchAll(CODE_BLOCK_PATTERN)) {
93
- if (match.index !== undefined) {
94
- protectedRegions.push({
95
- start: match.index,
96
- end: match.index + match[0].length,
97
- text: match[0],
98
- });
99
- }
100
- }
101
-
102
- // Extract URLs (skip those inside code blocks)
103
- for (const match of text.matchAll(URL_PATTERN)) {
104
- if (match.index !== undefined) {
105
- const idx = match.index;
106
- const isInsideCodeBlock = protectedRegions.some((r) => idx >= r.start && idx < r.end);
107
- if (!isInsideCodeBlock) {
108
- protectedRegions.push({
109
- start: idx,
110
- end: idx + match[0].length,
111
- text: match[0],
112
- });
113
- }
114
- }
115
- }
116
-
117
- // Sort by start position (descending) for safe replacement
118
- protectedRegions.sort((a, b) => b.start - a.start);
119
-
120
- // Step 2: Replace protected regions with placeholders
121
- let workingText = text;
122
- const placeholders: string[] = [];
123
-
124
- for (const region of protectedRegions) {
125
- const placeholder = `\x00PROTECTED_${placeholders.length}\x00`;
126
- placeholders.push(region.text);
127
- workingText = workingText.slice(0, region.start) + placeholder + workingText.slice(region.end);
128
- }
129
-
130
- // Step 3: Apply compression patterns
131
- for (const { pattern, replacement } of COMPRESSION_PATTERNS) {
132
- workingText = workingText.replace(pattern, replacement);
133
- }
134
-
135
- // Step 4: Restore protected regions
136
- for (const [i, region] of placeholders.entries()) {
137
- workingText = workingText.replace(`\x00PROTECTED_${i}\x00`, region);
138
- }
139
-
140
- // Step 5: Final trim
141
- return workingText.trim();
142
- }
143
-
144
- /**
145
- * Prepare a memory entry for storage by compressing its content.
146
- * Returns a new MemoryEntry — does not mutate the original.
147
- *
148
- * @param entry - Raw memory entry.
149
- * @returns New MemoryEntry with compressed content.
150
- */
151
- export function prepareForMemory(entry: MemoryEntry): MemoryEntry {
152
- return {
153
- ...entry,
154
- content: cavemanCompress(entry.content),
155
- };
156
- }
157
-
158
- /**
159
- * Determine whether content is worth storing in memory.
160
- *
161
- * Filters out:
162
- * - Trivial content (< 20 chars after compression)
163
- * - Pure code blocks with no prose to summarize
164
- *
165
- * @param content - Content to evaluate.
166
- * @returns `true` if the content should be stored.
167
- */
168
- export function shouldStoreMemory(content: string): boolean {
169
- const compressed = cavemanCompress(content);
170
-
171
- // Too short after compression — not worth storing
172
- if (compressed.length < 20) return false;
173
-
174
- // Check if content is purely code blocks
175
- const withoutCodeBlocks = compressed.replace(CODE_BLOCK_PATTERN, "").trim();
176
- if (withoutCodeBlocks.length < 10) {
177
- // Almost all code, very little prose — not useful for memory search
178
- return false;
179
- }
180
-
181
- return true;
182
- }
@@ -1,123 +0,0 @@
1
- /**
2
- * Result reconciliation for parallel agent tasks.
3
- * Merges outputs from multiple specialist agents into a unified report.
4
- */
5
-
6
- /** Result from a single completed task. */
7
- export interface TaskResult {
8
- /** Task ID from the dispatcher. */
9
- taskId: string;
10
- /** Agent that produced this result. */
11
- agent: string;
12
- /** Agent's output text. */
13
- output: string;
14
- /** Files the agent modified or created. */
15
- filesModified: string[];
16
- /** Whether the task succeeded. */
17
- success: boolean;
18
- }
19
-
20
- /** Unified report after reconciling multiple task results. */
21
- export interface ReconciliationReport {
22
- /** Merged summary of all agent outputs. */
23
- mergedSummary: string;
24
- /** File paths where two or more agents wrote (potential conflicts). */
25
- conflicts: string[];
26
- /** Deduplicated list of all files touched across all tasks. */
27
- allFilesModified: string[];
28
- /** Actionable next steps based on the results. */
29
- recommendations: string[];
30
- }
31
-
32
- /**
33
- * Reconcile results from multiple parallel agent tasks.
34
- *
35
- * Detects file conflicts (two agents modified the same file),
36
- * merges output summaries, and generates recommendations.
37
- *
38
- * @param results - Array of task results to reconcile.
39
- * @returns A unified reconciliation report.
40
- */
41
- export function reconcileResults(results: TaskResult[]): ReconciliationReport {
42
- const conflicts: string[] = [];
43
- const allFilesModified: string[] = [];
44
- const recommendations: string[] = [];
45
- const summaryParts: string[] = [];
46
-
47
- // Track which agents modified each file
48
- const fileAgents = new Map<string, Set<string>>();
49
-
50
- for (const result of results) {
51
- // Build summary
52
- const status = result.success ? "ok" : "FAILED";
53
- summaryParts.push(`[${result.agent}:${status}] ${truncate(result.output, 200)}`);
54
-
55
- // Track file ownership
56
- for (const file of result.filesModified) {
57
- allFilesModified.push(file);
58
-
59
- const agents = fileAgents.get(file);
60
- if (agents) {
61
- agents.add(result.agent);
62
- } else {
63
- fileAgents.set(file, new Set([result.agent]));
64
- }
65
- }
66
-
67
- // Flag failures as recommendations
68
- if (!result.success) {
69
- recommendations.push(
70
- `Task ${result.taskId} (${result.agent}) failed: ${truncate(result.output, 100)}`,
71
- );
72
- }
73
- }
74
-
75
- // Detect conflicts: files modified by multiple agents
76
- for (const [file, agents] of fileAgents.entries()) {
77
- if (agents.size > 1) {
78
- const agentList = Array.from(agents).join(", ");
79
- conflicts.push(`CONFLICT: ${file} modified by ${agentList}`);
80
- recommendations.push(
81
- `Review merge conflict in ${file} — touched by: ${agentList}. Manual resolution may be needed.`,
82
- );
83
- }
84
- }
85
-
86
- // Deduplicate file list
87
- const uniqueFiles = [...new Set(allFilesModified)];
88
-
89
- // General recommendations
90
- if (conflicts.length === 0 && results.length > 1) {
91
- recommendations.push("No file conflicts detected. Safe to merge.");
92
- }
93
-
94
- if (results.every((r) => r.success)) {
95
- recommendations.push("All tasks succeeded. Run tests to verify integration.");
96
- }
97
-
98
- const failedCount = results.filter((r) => !r.success).length;
99
- if (failedCount > 0) {
100
- recommendations.push(
101
- `${failedCount}/${results.length} tasks failed. Review failures before proceeding.`,
102
- );
103
- }
104
-
105
- return {
106
- mergedSummary: summaryParts.join("\n"),
107
- conflicts,
108
- allFilesModified: uniqueFiles,
109
- recommendations,
110
- };
111
- }
112
-
113
- /**
114
- * Truncate text to a maximum length, appending "…" if truncated.
115
- *
116
- * @param text - Text to truncate.
117
- * @param maxLength - Maximum character count.
118
- * @returns Truncated string.
119
- */
120
- function truncate(text: string, maxLength: number): string {
121
- if (text.length <= maxLength) return text;
122
- return `${text.slice(0, maxLength - 1)}…`;
123
- }
@@ -1,300 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type { RoutedTask, RoutingDecision, TaskRequest } from "./scheduler.ts";
3
- import { canRunParallel, routeTask } from "./scheduler.ts";
4
-
5
- const mk = (overrides: Partial<TaskRequest> & Pick<TaskRequest, "type">): TaskRequest => ({
6
- description: "test task",
7
- risk: "low",
8
- ...overrides,
9
- });
10
-
11
- // ─── routeTask ────────────────────────────────────────────────────────
12
-
13
- describe("routeTask", () => {
14
- describe("priority rules", () => {
15
- test("explore → scout", () => {
16
- const d = routeTask(mk({ type: "explore" }));
17
- expect(d.agent).toBe("scout");
18
- expect(d.parallel).toBe(true);
19
- });
20
-
21
- test("research → scribe", () => {
22
- const d = routeTask(mk({ type: "research" }));
23
- expect(d.agent).toBe("scribe");
24
- expect(d.parallel).toBe(true);
25
- });
26
-
27
- test("design + vue → painter", () => {
28
- const d = routeTask(mk({ type: "design", stack: "vue" }));
29
- expect(d.agent).toBe("painter");
30
- expect(d.parallel).toBe(true);
31
- });
32
-
33
- test("design + non-vue stack → smith (default)", () => {
34
- const d = routeTask(mk({ type: "design", stack: "js" }));
35
- expect(d.agent).toBe("smith");
36
- });
37
-
38
- test("audit → inspector", () => {
39
- const d = routeTask(mk({ type: "audit" }));
40
- expect(d.agent).toBe("inspector");
41
- expect(d.parallel).toBe(true);
42
- });
43
-
44
- test("document → chronicler", () => {
45
- const d = routeTask(mk({ type: "document" }));
46
- expect(d.agent).toBe("chronicler");
47
- expect(d.parallel).toBe(true);
48
- });
49
-
50
- test("debate → guild, not parallel", () => {
51
- const d = routeTask(mk({ type: "debate" }));
52
- expect(d.agent).toBe("guild");
53
- expect(d.parallel).toBe(false);
54
- });
55
-
56
- test("debug + high risk → sage, not parallel", () => {
57
- const d = routeTask(mk({ type: "debug", risk: "high" }));
58
- expect(d.agent).toBe("sage");
59
- expect(d.parallel).toBe(false);
60
- });
61
-
62
- test("debug + low risk → smith (default)", () => {
63
- const d = routeTask(mk({ type: "debug", risk: "low" }));
64
- expect(d.agent).toBe("smith");
65
- });
66
-
67
- test("implement + js → js-smith", () => {
68
- const d = routeTask(mk({ type: "implement", stack: "js" }));
69
- expect(d.agent).toBe("js-smith");
70
- expect(d.parallel).toBe(true);
71
- });
72
-
73
- test("implement + go → go-smith", () => {
74
- const d = routeTask(mk({ type: "implement", stack: "go" }));
75
- expect(d.agent).toBe("go-smith");
76
- });
77
-
78
- test("implement + python → python-smith", () => {
79
- const d = routeTask(mk({ type: "implement", stack: "python" }));
80
- expect(d.agent).toBe("python-smith");
81
- });
82
-
83
- test("implement + zig → zig-smith", () => {
84
- const d = routeTask(mk({ type: "implement", stack: "zig" }));
85
- expect(d.agent).toBe("zig-smith");
86
- });
87
-
88
- test("implement + vue → vue-smith", () => {
89
- const d = routeTask(mk({ type: "implement", stack: "vue" }));
90
- expect(d.agent).toBe("vue-smith");
91
- });
92
-
93
- test("implement + generic → smith", () => {
94
- const d = routeTask(mk({ type: "implement", stack: "generic" }));
95
- expect(d.agent).toBe("smith");
96
- });
97
-
98
- test("implement + unknown → smith", () => {
99
- const d = routeTask(mk({ type: "implement", stack: "unknown" }));
100
- expect(d.agent).toBe("smith");
101
- });
102
-
103
- test("implement + no stack → smith", () => {
104
- const d = routeTask(mk({ type: "implement" }));
105
- expect(d.agent).toBe("smith");
106
- });
107
- });
108
-
109
- describe("dependencies semantics", () => {
110
- test("all routeTask results have empty dependencies (task IDs set externally)", () => {
111
- const cases: TaskRequest[] = [
112
- mk({ type: "explore" }),
113
- mk({ type: "research" }),
114
- mk({ type: "design", stack: "vue" }),
115
- mk({ type: "audit" }),
116
- mk({ type: "document" }),
117
- mk({ type: "debate" }),
118
- mk({ type: "debug", risk: "high" }),
119
- mk({ type: "implement", stack: "js" }),
120
- ];
121
- for (const req of cases) {
122
- const d = routeTask(req);
123
- expect(d.dependencies).toEqual([]);
124
- }
125
- });
126
-
127
- test("dependencies are task IDs, never agent names", () => {
128
- const d = routeTask(mk({ type: "implement", stack: "js", risk: "high" }));
129
- // Bug was: dependencies: ["sage"] — agent name in task ID field
130
- expect(d.dependencies).toEqual([]);
131
- expect(d.dependencies).not.toContain("sage");
132
- });
133
- });
134
-
135
- describe("requiresReview (sage advisory)", () => {
136
- test("high-risk implement sets requiresReview to sage", () => {
137
- const d = routeTask(mk({ type: "implement", stack: "js", risk: "high" }));
138
- expect(d.requiresReview).toBe("sage");
139
- expect(d.agent).toBe("js-smith");
140
- expect(d.parallel).toBe(true);
141
- });
142
-
143
- test("high-risk implement + go → go-smith with sage review", () => {
144
- const d = routeTask(mk({ type: "implement", stack: "go", risk: "high" }));
145
- expect(d.agent).toBe("go-smith");
146
- expect(d.requiresReview).toBe("sage");
147
- });
148
-
149
- test("low-risk implement has no requiresReview", () => {
150
- const d = routeTask(mk({ type: "implement", stack: "js" }));
151
- expect(d.requiresReview).toBeUndefined();
152
- });
153
-
154
- test("non-implement tasks have no requiresReview", () => {
155
- const d = routeTask(mk({ type: "explore", risk: "high" }));
156
- expect(d.requiresReview).toBeUndefined();
157
- });
158
-
159
- test("debug + high risk has no requiresReview (goes to sage directly)", () => {
160
- const d = routeTask(mk({ type: "debug", risk: "high" }));
161
- expect(d.requiresReview).toBeUndefined();
162
- expect(d.agent).toBe("sage");
163
- });
164
- });
165
- });
166
-
167
- // ─── canRunParallel ───────────────────────────────────────────────────
168
-
169
- describe("canRunParallel", () => {
170
- const makeRouted = (
171
- id: string,
172
- agent: string,
173
- opts: Partial<Pick<RoutingDecision, "parallel" | "dependencies">> = {},
174
- ): RoutedTask => ({
175
- id,
176
- decision: {
177
- agent,
178
- reason: "test",
179
- parallel: opts.parallel ?? true,
180
- dependencies: opts.dependencies ?? [],
181
- },
182
- });
183
-
184
- test("all parallel, no deps → true", () => {
185
- expect(
186
- canRunParallel([
187
- makeRouted("t1", "scout"),
188
- makeRouted("t2", "scribe"),
189
- makeRouted("t3", "js-smith"),
190
- ]),
191
- ).toBe(true);
192
- });
193
-
194
- test("one non-parallel task → false", () => {
195
- expect(
196
- canRunParallel([makeRouted("t1", "scout"), makeRouted("t2", "guild", { parallel: false })]),
197
- ).toBe(false);
198
- });
199
-
200
- test("dependency on external task (not in batch) → true", () => {
201
- expect(
202
- canRunParallel([
203
- makeRouted("t1", "js-smith", { dependencies: ["t-external"] }),
204
- makeRouted("t2", "scout"),
205
- ]),
206
- ).toBe(true);
207
- });
208
-
209
- test("dependency on task in same batch → false", () => {
210
- expect(
211
- canRunParallel([
212
- makeRouted("t1", "scout"),
213
- makeRouted("t2", "js-smith", { dependencies: ["t1"] }),
214
- ]),
215
- ).toBe(false);
216
- });
217
-
218
- test("circular dependency in batch → false", () => {
219
- const result = canRunParallel([
220
- makeRouted("t1", "js-smith", { dependencies: ["t2"] }),
221
- makeRouted("t2", "scout", { dependencies: ["t1"] }),
222
- ]);
223
- expect(result).toBe(false);
224
- });
225
-
226
- test("empty batch → true", () => {
227
- expect(canRunParallel([])).toBe(true);
228
- });
229
-
230
- test("single task, parallel, no deps → true", () => {
231
- expect(canRunParallel([makeRouted("t1", "scout")])).toBe(true);
232
- });
233
-
234
- test("single task, non-parallel → false", () => {
235
- expect(canRunParallel([makeRouted("t1", "guild", { parallel: false })])).toBe(false);
236
- });
237
-
238
- test("multiple deps, only one in batch → false", () => {
239
- expect(
240
- canRunParallel([
241
- makeRouted("t1", "scout"),
242
- makeRouted("t2", "scribe"),
243
- makeRouted("t3", "js-smith", { dependencies: ["t-external", "t1"] }),
244
- ]),
245
- ).toBe(false);
246
- });
247
-
248
- test("requiresReview does not affect parallelism", () => {
249
- // sage review is advisory, not a blocking dependency
250
- const tasks: RoutedTask[] = [
251
- {
252
- id: "t1",
253
- decision: {
254
- agent: "js-smith",
255
- reason: "test",
256
- parallel: true,
257
- dependencies: [],
258
- requiresReview: "sage",
259
- },
260
- },
261
- makeRouted("t2", "scout"),
262
- ];
263
- expect(canRunParallel(tasks)).toBe(true);
264
- });
265
- });
266
-
267
- // ─── canRunParallel (legacy RoutingDecision[] path) ───────────────────
268
-
269
- describe("canRunParallel (legacy RoutingDecision[])", () => {
270
- test("all parallel, no deps → true", () => {
271
- const decisions: RoutingDecision[] = [
272
- { agent: "scout", reason: "test", parallel: true, dependencies: [] },
273
- { agent: "scribe", reason: "test", parallel: true, dependencies: [] },
274
- ];
275
- expect(canRunParallel(decisions)).toBe(true);
276
- });
277
-
278
- test("one non-parallel → false", () => {
279
- const decisions: RoutingDecision[] = [
280
- { agent: "scout", reason: "test", parallel: true, dependencies: [] },
281
- { agent: "guild", reason: "test", parallel: false, dependencies: [] },
282
- ];
283
- expect(canRunParallel(decisions)).toBe(false);
284
- });
285
-
286
- test("agent-name dependency in batch → false (legacy semantic)", () => {
287
- const decisions: RoutingDecision[] = [
288
- { agent: "scout", reason: "test", parallel: true, dependencies: [] },
289
- { agent: "js-smith", reason: "test", parallel: true, dependencies: ["scout"] },
290
- ];
291
- expect(canRunParallel(decisions)).toBe(false);
292
- });
293
-
294
- test("agent-name dependency not in batch → true", () => {
295
- const decisions: RoutingDecision[] = [
296
- { agent: "js-smith", reason: "test", parallel: true, dependencies: ["sage"] },
297
- ];
298
- expect(canRunParallel(decisions)).toBe(true);
299
- });
300
- });