swarm-code 0.1.4 → 0.1.6
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/dist/config.js +1 -1
- package/dist/hooks/runner.d.ts +35 -0
- package/dist/hooks/runner.js +95 -0
- package/dist/interactive-swarm.js +1 -2
- package/dist/interactive.js +65 -22
- package/dist/main.d.ts +1 -1
- package/dist/main.js +11 -8
- package/dist/prompts/orchestrator.js +4 -3
- package/dist/swarm.js +40 -0
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -23,7 +23,7 @@ const DEFAULTS = {
|
|
|
23
23
|
max_session_budget_usd: 10.0,
|
|
24
24
|
default_agent: "opencode",
|
|
25
25
|
default_model: "anthropic/claude-sonnet-4-6",
|
|
26
|
-
auto_model_selection:
|
|
26
|
+
auto_model_selection: true,
|
|
27
27
|
compression_strategy: "structured",
|
|
28
28
|
compression_max_tokens: 1000,
|
|
29
29
|
worktree_base_dir: ".swarm-worktrees",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook runner — executes user-defined commands at lifecycle points.
|
|
3
|
+
*
|
|
4
|
+
* Hooks provide deterministic control flow (not LLM-decided).
|
|
5
|
+
* They run shell commands and surface only errors — success is silent.
|
|
6
|
+
*
|
|
7
|
+
* Lifecycle points:
|
|
8
|
+
* - post_thread: After a thread commits (before compression)
|
|
9
|
+
* - post_merge: After merge_threads() completes
|
|
10
|
+
* - post_session: When the session ends
|
|
11
|
+
*/
|
|
12
|
+
export interface HookConfig {
|
|
13
|
+
command: string;
|
|
14
|
+
on_failure: "warn" | "block";
|
|
15
|
+
}
|
|
16
|
+
export interface HooksConfig {
|
|
17
|
+
post_thread: HookConfig[];
|
|
18
|
+
post_merge: HookConfig[];
|
|
19
|
+
post_session: HookConfig[];
|
|
20
|
+
}
|
|
21
|
+
export interface HookResult {
|
|
22
|
+
success: boolean;
|
|
23
|
+
output: string;
|
|
24
|
+
command: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Load hooks from swarm_config.yaml hooks section, or from .swarm/hooks.yaml.
|
|
28
|
+
*/
|
|
29
|
+
export declare function loadHooks(projectDir: string): HooksConfig;
|
|
30
|
+
/**
|
|
31
|
+
* Run hooks for a lifecycle point.
|
|
32
|
+
* Returns results for each hook. On "block" failure, throws.
|
|
33
|
+
* Success output is swallowed — only errors are surfaced.
|
|
34
|
+
*/
|
|
35
|
+
export declare function runHooks(hooks: HookConfig[], cwd: string, label: string): HookResult[];
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook runner — executes user-defined commands at lifecycle points.
|
|
3
|
+
*
|
|
4
|
+
* Hooks provide deterministic control flow (not LLM-decided).
|
|
5
|
+
* They run shell commands and surface only errors — success is silent.
|
|
6
|
+
*
|
|
7
|
+
* Lifecycle points:
|
|
8
|
+
* - post_thread: After a thread commits (before compression)
|
|
9
|
+
* - post_merge: After merge_threads() completes
|
|
10
|
+
* - post_session: When the session ends
|
|
11
|
+
*/
|
|
12
|
+
import { execSync } from "node:child_process";
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
const DEFAULT_HOOKS = {
|
|
16
|
+
post_thread: [],
|
|
17
|
+
post_merge: [],
|
|
18
|
+
post_session: [],
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Load hooks from swarm_config.yaml hooks section, or from .swarm/hooks.yaml.
|
|
22
|
+
*/
|
|
23
|
+
export function loadHooks(projectDir) {
|
|
24
|
+
const hooksFile = path.join(projectDir, ".swarm", "hooks.yaml");
|
|
25
|
+
if (!fs.existsSync(hooksFile))
|
|
26
|
+
return { ...DEFAULT_HOOKS };
|
|
27
|
+
try {
|
|
28
|
+
const raw = fs.readFileSync(hooksFile, "utf-8");
|
|
29
|
+
return parseHooksYaml(raw);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { ...DEFAULT_HOOKS };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function parseHooksYaml(raw) {
|
|
36
|
+
const hooks = { post_thread: [], post_merge: [], post_session: [] };
|
|
37
|
+
let currentSection = null;
|
|
38
|
+
for (const line of raw.split("\n")) {
|
|
39
|
+
const trimmed = line.trim();
|
|
40
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
41
|
+
continue;
|
|
42
|
+
if (trimmed === "post_thread:" || trimmed === "post_merge:" || trimmed === "post_session:") {
|
|
43
|
+
currentSection = trimmed.replace(":", "");
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (currentSection && trimmed.startsWith("- command:")) {
|
|
47
|
+
const command = trimmed
|
|
48
|
+
.replace("- command:", "")
|
|
49
|
+
.trim()
|
|
50
|
+
.replace(/^["']|["']$/g, "");
|
|
51
|
+
if (command) {
|
|
52
|
+
hooks[currentSection].push({ command, on_failure: "warn" });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (currentSection && trimmed.startsWith("on_failure:")) {
|
|
56
|
+
const val = trimmed.replace("on_failure:", "").trim();
|
|
57
|
+
const last = hooks[currentSection][hooks[currentSection].length - 1];
|
|
58
|
+
if (last && (val === "warn" || val === "block")) {
|
|
59
|
+
last.on_failure = val;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return hooks;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Run hooks for a lifecycle point.
|
|
67
|
+
* Returns results for each hook. On "block" failure, throws.
|
|
68
|
+
* Success output is swallowed — only errors are surfaced.
|
|
69
|
+
*/
|
|
70
|
+
export function runHooks(hooks, cwd, label) {
|
|
71
|
+
const results = [];
|
|
72
|
+
for (const hook of hooks) {
|
|
73
|
+
try {
|
|
74
|
+
execSync(hook.command, {
|
|
75
|
+
cwd,
|
|
76
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
77
|
+
timeout: 60_000,
|
|
78
|
+
encoding: "utf-8",
|
|
79
|
+
});
|
|
80
|
+
// Success — silent (context-efficient per harness engineering best practice)
|
|
81
|
+
results.push({ success: true, output: "", command: hook.command });
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
const stderr = err.stderr || err.stdout || err.message || "unknown error";
|
|
85
|
+
// Only surface error output
|
|
86
|
+
const output = `[${label}] Hook failed: ${hook.command}\n${stderr}`.trim();
|
|
87
|
+
results.push({ success: false, output, command: hook.command });
|
|
88
|
+
if (hook.on_failure === "block") {
|
|
89
|
+
throw new Error(output);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -80,8 +80,7 @@ function parseInteractiveArgs(args) {
|
|
|
80
80
|
// Silently ignore unknown flags and positional args
|
|
81
81
|
}
|
|
82
82
|
if (!dir) {
|
|
83
|
-
|
|
84
|
-
process.exit(1);
|
|
83
|
+
dir = process.cwd();
|
|
85
84
|
}
|
|
86
85
|
return {
|
|
87
86
|
dir: path.resolve(dir),
|
package/dist/interactive.js
CHANGED
|
@@ -1582,30 +1582,73 @@ async function interactive() {
|
|
|
1582
1582
|
console.log();
|
|
1583
1583
|
}
|
|
1584
1584
|
else {
|
|
1585
|
-
// Agent works without keys (e.g. OpenCode) —
|
|
1586
|
-
console.log(` ${c.bold}${agent.name}${c.reset} ${c.dim}—
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1585
|
+
// Agent works without keys (e.g. OpenCode) — choose backend
|
|
1586
|
+
console.log(` ${c.bold}${agent.name}${c.reset} ${c.dim}— choose your backend:${c.reset}\n`);
|
|
1587
|
+
console.log(` ${c.dim}1${c.reset} Ollama ${c.dim}Run models locally (free, requires download)${c.reset}`);
|
|
1588
|
+
console.log(` ${c.dim}2${c.reset} OpenRouter ${c.dim}Cloud API for 200+ models (requires API key)${c.reset}`);
|
|
1589
|
+
console.log();
|
|
1590
|
+
const backendChoice = await questionWithEsc(setupRl, ` ${c.cyan}Backend [1-2]:${c.reset} `);
|
|
1591
|
+
const pickedOpenRouter = backendChoice !== null && backendChoice.trim() === "2";
|
|
1592
|
+
if (pickedOpenRouter) {
|
|
1593
|
+
// OpenRouter setup
|
|
1594
|
+
console.log();
|
|
1595
|
+
const orKey = await questionWithEsc(setupRl, ` ${c.cyan}OPENROUTER_API_KEY:${c.reset} `);
|
|
1596
|
+
if (orKey?.trim()) {
|
|
1597
|
+
process.env.OPENROUTER_API_KEY = orKey.trim();
|
|
1598
|
+
try {
|
|
1599
|
+
const envPath = path.join(process.cwd(), ".env");
|
|
1600
|
+
let envContent = "";
|
|
1601
|
+
try {
|
|
1602
|
+
envContent = fs.readFileSync(envPath, "utf-8");
|
|
1603
|
+
}
|
|
1604
|
+
catch { }
|
|
1605
|
+
if (!envContent.includes("OPENROUTER_API_KEY")) {
|
|
1606
|
+
fs.appendFileSync(envPath, `\nOPENROUTER_API_KEY=${orKey.trim()}\n`);
|
|
1607
|
+
}
|
|
1608
|
+
console.log(` ${c.green}✓${c.reset} OpenRouter configured`);
|
|
1609
|
+
}
|
|
1610
|
+
catch {
|
|
1611
|
+
console.log(` ${c.green}✓${c.reset} OpenRouter key set for this session`);
|
|
1612
|
+
}
|
|
1613
|
+
currentModelId = "openrouter/auto";
|
|
1614
|
+
saveModelPreference(currentModelId);
|
|
1615
|
+
}
|
|
1616
|
+
else {
|
|
1617
|
+
console.log(` ${c.dim}No key provided — you can set OPENROUTER_API_KEY in .env later${c.reset}`);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
else {
|
|
1621
|
+
// Ollama setup
|
|
1622
|
+
console.log();
|
|
1623
|
+
const ok = await ensureOllamaSetup(setupRl, "ollama/deepseek-coder-v2");
|
|
1624
|
+
if (ok)
|
|
1625
|
+
usesOllama = true;
|
|
1626
|
+
}
|
|
1590
1627
|
console.log();
|
|
1591
1628
|
}
|
|
1592
1629
|
}
|
|
1593
1630
|
// ── Step 3: Set default model ────────────────────────────────
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1631
|
+
if (currentModelId.startsWith("openrouter/")) {
|
|
1632
|
+
// Already set during OpenRouter setup
|
|
1633
|
+
console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset} ${c.dim}(OpenRouter)${c.reset}`);
|
|
1634
|
+
}
|
|
1635
|
+
else {
|
|
1636
|
+
const activeProvider = Object.keys(PROVIDER_KEYS).find((p) => process.env[providerEnvKey(p)]);
|
|
1637
|
+
if (activeProvider) {
|
|
1638
|
+
currentProviderName = activeProvider;
|
|
1639
|
+
const defaultModel = getDefaultModelForProvider(activeProvider);
|
|
1640
|
+
if (defaultModel) {
|
|
1641
|
+
currentModelId = defaultModel;
|
|
1642
|
+
saveModelPreference(currentModelId);
|
|
1643
|
+
console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset}`);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
else if (usesOllama) {
|
|
1647
|
+
currentModelId = "ollama/deepseek-coder-v2";
|
|
1600
1648
|
saveModelPreference(currentModelId);
|
|
1601
|
-
console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset}`);
|
|
1649
|
+
console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset} ${c.dim}(local)${c.reset}`);
|
|
1602
1650
|
}
|
|
1603
1651
|
}
|
|
1604
|
-
else if (usesOllama) {
|
|
1605
|
-
currentModelId = "ollama/deepseek-coder-v2";
|
|
1606
|
-
saveModelPreference(currentModelId);
|
|
1607
|
-
console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset} ${c.dim}(local)${c.reset}`);
|
|
1608
|
-
}
|
|
1609
1652
|
console.log();
|
|
1610
1653
|
setupRl.close();
|
|
1611
1654
|
}
|
|
@@ -1637,12 +1680,12 @@ async function interactive() {
|
|
|
1637
1680
|
}
|
|
1638
1681
|
}
|
|
1639
1682
|
if (!currentModel) {
|
|
1640
|
-
if (currentModelId.startsWith("ollama/")) {
|
|
1641
|
-
//
|
|
1642
|
-
|
|
1643
|
-
console.log(`\n ${c.green}✓${c.reset}
|
|
1683
|
+
if (currentModelId.startsWith("ollama/") || currentModelId.startsWith("openrouter/")) {
|
|
1684
|
+
// Non-pi-ai model — redirect to swarm mode which uses OpenCode natively.
|
|
1685
|
+
const backend = currentModelId.startsWith("ollama/") ? "Ollama" : "OpenRouter";
|
|
1686
|
+
console.log(`\n ${c.green}✓${c.reset} ${backend} model selected: ${c.bold}${currentModelId}${c.reset}`);
|
|
1644
1687
|
console.log(`\n ${c.dim}This interactive REPL uses direct LLM API calls.${c.reset}`);
|
|
1645
|
-
console.log(` ${c.dim}To use
|
|
1688
|
+
console.log(` ${c.dim}To use ${backend} with OpenCode, run:${c.reset}\n`);
|
|
1646
1689
|
console.log(` ${c.bold}swarm --dir ./your-project "your task"${c.reset}\n`);
|
|
1647
1690
|
process.exit(0);
|
|
1648
1691
|
}
|
package/dist/main.d.ts
CHANGED
|
@@ -10,6 +10,6 @@
|
|
|
10
10
|
* swarm run → single-shot RLM CLI run
|
|
11
11
|
* swarm viewer → browse trajectory files
|
|
12
12
|
* swarm benchmark → run benchmarks
|
|
13
|
-
* swarm → interactive
|
|
13
|
+
* swarm → interactive REPL (uses current directory)
|
|
14
14
|
*/
|
|
15
15
|
export declare function buildHelp(): string;
|
package/dist/main.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* swarm run → single-shot RLM CLI run
|
|
11
11
|
* swarm viewer → browse trajectory files
|
|
12
12
|
* swarm benchmark → run benchmarks
|
|
13
|
-
* swarm → interactive
|
|
13
|
+
* swarm → interactive REPL (uses current directory)
|
|
14
14
|
*/
|
|
15
15
|
import { bold, coral, cyan, dim, isTTY, symbols, termWidth, yellow } from "./ui/theme.js";
|
|
16
16
|
export function buildHelp() {
|
|
@@ -45,8 +45,10 @@ export function buildHelp() {
|
|
|
45
45
|
lines.push(` ${yellow("swarm mcp")} ${dim("Start MCP server (stdio)")}`);
|
|
46
46
|
lines.push(` ${yellow("swarm mcp")} --dir ./project ${dim("Start with default directory")}`);
|
|
47
47
|
lines.push("");
|
|
48
|
+
lines.push(` ${bold("INTERACTIVE")} ${dim("(default — uses current directory)")}`);
|
|
49
|
+
lines.push(` ${yellow("swarm")} ${dim("Interactive REPL in current dir")}`);
|
|
50
|
+
lines.push("");
|
|
48
51
|
lines.push(` ${bold("RLM MODE")} ${dim("(text processing, inherited from rlm-cli)")}`);
|
|
49
|
-
lines.push(` ${yellow("swarm")} ${dim("Interactive terminal (default)")}`);
|
|
50
52
|
lines.push(` ${yellow("swarm run")} [options] "<query>" ${dim("Run a single query")}`);
|
|
51
53
|
lines.push(` ${yellow("swarm viewer")} ${dim("Browse saved trajectory files")}`);
|
|
52
54
|
lines.push(` ${yellow("swarm benchmark")} <name> [--idx] ${dim("Run benchmark")}`);
|
|
@@ -125,13 +127,14 @@ async function main() {
|
|
|
125
127
|
}
|
|
126
128
|
return;
|
|
127
129
|
}
|
|
128
|
-
const command = args[0] || "
|
|
130
|
+
const command = args[0] || "";
|
|
131
|
+
// Default: no command → interactive swarm mode using current directory
|
|
132
|
+
if (!command || command === "interactive" || command === "i") {
|
|
133
|
+
const { runInteractiveSwarm } = await import("./interactive-swarm.js");
|
|
134
|
+
await runInteractiveSwarm(["--dir", process.cwd(), ...args.slice(command ? 1 : 0)]);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
129
137
|
switch (command) {
|
|
130
|
-
case "interactive":
|
|
131
|
-
case "i": {
|
|
132
|
-
await import("./interactive.js");
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
138
|
case "viewer":
|
|
136
139
|
case "view": {
|
|
137
140
|
process.argv = [process.argv[0], process.argv[1], ...args.slice(1)];
|
|
@@ -56,11 +56,11 @@ ${agentDescriptions}
|
|
|
56
56
|
|
|
57
57
|
1. **Analyze first**: Use \`llm_query()\` or direct Python to understand the codebase/task
|
|
58
58
|
2. **Decompose**: Break the task into independent, parallelizable units
|
|
59
|
-
3. **Extract context**: For each thread, extract ONLY the relevant code/context — don't send everything
|
|
59
|
+
3. **Extract context**: For each thread, extract ONLY the relevant code/context — don't send everything. Keep thread context under 5000 chars; agents have access to the full worktree
|
|
60
60
|
4. **Spawn threads**: Use \`async_thread()\` + \`asyncio.gather()\` for parallel work
|
|
61
61
|
5. **Inspect results**: Check each thread's result for success/failure
|
|
62
62
|
6. **Merge**: Call \`merge_threads()\` to integrate changes
|
|
63
|
-
7. **Verify**:
|
|
63
|
+
7. **Verify**: ALWAYS spawn a verification thread after merging — run the project's test/typecheck/lint commands. If verification fails, fix before calling FINAL()
|
|
64
64
|
8. **Report**: Call \`FINAL()\` with a summary
|
|
65
65
|
|
|
66
66
|
## Episode Quality & Caching
|
|
@@ -77,7 +77,8 @@ ${agentDescriptions}
|
|
|
77
77
|
4. Use \`print()\` for intermediate output visible in the next iteration
|
|
78
78
|
5. Max ${config.max_threads} concurrent threads, ${config.max_total_threads} total per session
|
|
79
79
|
6. Thread timeout: ${config.thread_timeout_ms / 1000}s per thread
|
|
80
|
-
7. Don't call FINAL prematurely — verify thread results first
|
|
80
|
+
7. Don't call FINAL prematurely — verify thread results first. Always run verification after merge.
|
|
81
|
+
8. Prefer cheap models for sub-agent threads (haiku, gpt-4o-mini) — save premium models for complex work
|
|
81
82
|
8. The REPL persists state — variables survive across iterations
|
|
82
83
|
|
|
83
84
|
## Examples
|
package/dist/swarm.js
CHANGED
|
@@ -25,6 +25,7 @@ await import("./agents/claude-code.js");
|
|
|
25
25
|
await import("./agents/codex.js");
|
|
26
26
|
await import("./agents/aider.js");
|
|
27
27
|
import { randomBytes } from "node:crypto";
|
|
28
|
+
import { loadHooks, runHooks } from "./hooks/runner.js";
|
|
28
29
|
import { EpisodicMemory } from "./memory/episodic.js";
|
|
29
30
|
import { buildSwarmSystemPrompt } from "./prompts/orchestrator.js";
|
|
30
31
|
import { classifyTaskComplexity, describeAvailableAgents, FailureTracker, routeTask } from "./routing/model-router.js";
|
|
@@ -296,6 +297,7 @@ function resolveModel(modelId) {
|
|
|
296
297
|
export async function runSwarmMode(rawArgs) {
|
|
297
298
|
const args = parseSwarmArgs(rawArgs);
|
|
298
299
|
const config = loadConfig();
|
|
300
|
+
const hooks = loadHooks(args.dir);
|
|
299
301
|
// Configure UI
|
|
300
302
|
if (args.json)
|
|
301
303
|
setJsonMode(true);
|
|
@@ -420,6 +422,12 @@ export async function runSwarmMode(rawArgs) {
|
|
|
420
422
|
}
|
|
421
423
|
// Thread handler
|
|
422
424
|
const threadHandler = async (task, threadContext, agentBackend, model, files) => {
|
|
425
|
+
// Context-size guard: warn and truncate if orchestrator sends too much context
|
|
426
|
+
const MAX_THREAD_CONTEXT = 50_000;
|
|
427
|
+
if (threadContext.length > MAX_THREAD_CONTEXT) {
|
|
428
|
+
logWarn(`Thread context too large (${(threadContext.length / 1024).toFixed(0)}KB) — truncating to ${MAX_THREAD_CONTEXT / 1000}KB. Agents have full worktree access; pass only relevant excerpts.`);
|
|
429
|
+
threadContext = `${threadContext.slice(0, MAX_THREAD_CONTEXT)}\n\n[... truncated — ${threadContext.length - MAX_THREAD_CONTEXT} chars removed ...]`;
|
|
430
|
+
}
|
|
423
431
|
let resolvedAgent = agentBackend || config.default_agent;
|
|
424
432
|
let resolvedModel = model || config.default_model;
|
|
425
433
|
let routeSlot = "";
|
|
@@ -449,6 +457,18 @@ export async function runSwarmMode(rawArgs) {
|
|
|
449
457
|
},
|
|
450
458
|
files,
|
|
451
459
|
});
|
|
460
|
+
// Run post-thread hooks (typecheck, lint, etc.) — success is silent, only errors surface
|
|
461
|
+
if (result.success && hooks.post_thread.length > 0) {
|
|
462
|
+
try {
|
|
463
|
+
const worktreePath = path.join(args.dir, config.worktree_base_dir, `wt-${threadId}`);
|
|
464
|
+
if (fs.existsSync(worktreePath)) {
|
|
465
|
+
runHooks(hooks.post_thread, worktreePath, "post_thread");
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
catch (hookErr) {
|
|
469
|
+
logWarn(`Post-thread hook failed: ${hookErr.message}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
452
472
|
// Track failure in the failure tracker for routing adjustments
|
|
453
473
|
if (!result.success) {
|
|
454
474
|
failureTracker.recordFailure(resolvedAgent, resolvedModel, task, result.summary || "unknown error");
|
|
@@ -494,6 +514,26 @@ export async function runSwarmMode(rawArgs) {
|
|
|
494
514
|
const summary = results
|
|
495
515
|
.map((r) => (r.success ? `Merged ${r.branch}: ${r.message}` : `FAILED ${r.branch}: ${r.message}`))
|
|
496
516
|
.join("\n");
|
|
517
|
+
// Run post-merge hooks (tests, etc.) — success is silent
|
|
518
|
+
if (merged > 0 && hooks.post_merge.length > 0) {
|
|
519
|
+
try {
|
|
520
|
+
const hookResults = runHooks(hooks.post_merge, args.dir, "post_merge");
|
|
521
|
+
const hookFailures = hookResults.filter((r) => !r.success);
|
|
522
|
+
if (hookFailures.length > 0) {
|
|
523
|
+
const hookOutput = hookFailures.map((r) => r.output).join("\n");
|
|
524
|
+
return {
|
|
525
|
+
result: `${summary}\n\nPost-merge verification failed:\n${hookOutput}`,
|
|
526
|
+
success: false,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
catch (hookErr) {
|
|
531
|
+
return {
|
|
532
|
+
result: `${summary}\n\nPost-merge hook blocked: ${hookErr.message}`,
|
|
533
|
+
success: false,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
}
|
|
497
537
|
return {
|
|
498
538
|
result: summary || "No threads to merge",
|
|
499
539
|
success: results.every((r) => r.success),
|
package/package.json
CHANGED