ndomo 0.1.0 → 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/.env.example +4 -0
- package/README.es.md +29 -23
- package/README.md +64 -24
- package/bun.lock +447 -0
- package/docs/configuration.md +4 -4
- package/docs/installation.md +53 -34
- package/docs/installer.md +164 -0
- package/docs/integrations.md +1 -1
- package/docs/web-ui.md +124 -0
- package/package.json +43 -4
- package/scripts/install.sh +28 -0
- package/scripts/smoke-install.sh +47 -0
- package/scripts/smoke-web.sh +335 -0
- package/src/cli/__tests__/install.test.ts +733 -0
- package/src/cli/index.ts +8 -0
- package/src/cli/install.ts +1273 -0
- package/src/config/__tests__/schema.test.ts +223 -0
- package/src/config/schema.ts +129 -16
- package/src/http/__tests__/auth.test.ts +10 -10
- package/src/http/__tests__/spa.test.ts +296 -0
- package/src/http/auth.ts +8 -1
- package/src/http/server.ts +71 -2
- package/.bun-version +0 -1
- package/.dockerignore +0 -79
- package/.editorconfig +0 -18
- package/.github/CODEOWNERS +0 -8
- package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -62
- package/.github/ISSUE_TEMPLATE/config.yml +0 -2
- package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -34
- package/.github/dependabot.yml +0 -36
- package/.github/pull_request_template.md +0 -24
- package/.github/release.yml +0 -30
- package/.github/workflows/gitleaks.yml +0 -28
- package/.github/workflows/release-please.yml +0 -27
- package/.github/workflows/smoke.yml +0 -29
- package/.husky/commit-msg +0 -1
- package/CHANGELOG.md +0 -114
- package/Dockerfile +0 -32
- package/bin/ndomo-analyses.ts +0 -4
- package/bin/ndomo-status.ts +0 -4
- package/biome.json +0 -57
- package/commitlint.config.js +0 -3
- package/opencode.json +0 -5
- package/release-please-config.json +0 -11
- package/scripts/dev-bust-cache.sh +0 -164
- package/scripts/smoke-e2e.ts +0 -704
- package/scripts/smoke-hot.ts +0 -417
- package/scripts/smoke-v4.ts +0 -256
- package/scripts/smoke-v5.ts +0 -397
- package/scripts/uninstall.sh +0 -224
- package/src/index.ts +0 -37
- package/src/lib.ts +0 -65
- package/src/mem/scoped.ts +0 -65
- package/src/orchestrator/background.test.ts +0 -268
- package/src/orchestrator/background.ts +0 -293
- package/src/orchestrator/memory-hook.ts +0 -182
- package/src/orchestrator/reconciler.ts +0 -123
- package/src/orchestrator/scheduler.test.ts +0 -300
- package/src/orchestrator/scheduler.ts +0 -243
- package/src/plugin.test.ts +0 -2574
- package/src/plugin.ts +0 -1690
- package/src/worktrees/manager.ts +0 -236
- package/src/worktrees/state.ts +0 -87
- package/tests/integration/ranger-flow.test.ts +0 -257
- 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
|
-
});
|