opencode-swarm-plugin 0.36.0 → 0.36.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/.hive/issues.jsonl +4 -4
- package/.hive/memories.jsonl +274 -1
- package/.turbo/turbo-build.log +4 -4
- package/.turbo/turbo-test.log +307 -307
- package/CHANGELOG.md +71 -0
- package/bin/swarm.ts +234 -179
- package/dist/compaction-hook.d.ts +54 -4
- package/dist/compaction-hook.d.ts.map +1 -1
- package/dist/eval-capture.d.ts +122 -17
- package/dist/eval-capture.d.ts.map +1 -1
- package/dist/index.d.ts +1 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1278 -619
- package/dist/planning-guardrails.d.ts +121 -0
- package/dist/planning-guardrails.d.ts.map +1 -1
- package/dist/plugin.d.ts +9 -9
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +1283 -329
- package/dist/schemas/task.d.ts +0 -1
- package/dist/schemas/task.d.ts.map +1 -1
- package/dist/swarm-decompose.d.ts +0 -8
- package/dist/swarm-decompose.d.ts.map +1 -1
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts +0 -4
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm-review.d.ts.map +1 -1
- package/dist/swarm.d.ts +0 -6
- package/dist/swarm.d.ts.map +1 -1
- package/evals/README.md +38 -0
- package/evals/coordinator-session.eval.ts +154 -0
- package/evals/fixtures/coordinator-sessions.ts +328 -0
- package/evals/lib/data-loader.ts +69 -0
- package/evals/scorers/coordinator-discipline.evalite-test.ts +536 -0
- package/evals/scorers/coordinator-discipline.ts +315 -0
- package/evals/scorers/index.ts +12 -0
- package/examples/plugin-wrapper-template.ts +303 -4
- package/package.json +2 -2
- package/src/compaction-hook.test.ts +8 -1
- package/src/compaction-hook.ts +31 -21
- package/src/eval-capture.test.ts +390 -0
- package/src/eval-capture.ts +163 -4
- package/src/index.ts +68 -1
- package/src/planning-guardrails.test.ts +387 -2
- package/src/planning-guardrails.ts +289 -0
- package/src/plugin.ts +10 -10
- package/src/swarm-decompose.ts +20 -0
- package/src/swarm-orchestrate.ts +44 -0
- package/src/swarm-prompts.ts +20 -0
- package/src/swarm-review.ts +41 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# opencode-swarm-plugin
|
|
2
2
|
|
|
3
|
+
## 0.36.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`9c1f3f3`](https://github.com/joelhooks/swarm-tools/commit/9c1f3f3e7204f02c133c4a036fa34e83d8376a8c) Thanks [@joelhooks](https://github.com/joelhooks)! - ## 🐝 Coordinator Discipline: Prohibition-First Enforcement
|
|
8
|
+
|
|
9
|
+
Coordinators kept "just doing it themselves" after compaction. Now they can't.
|
|
10
|
+
|
|
11
|
+
**The Problem:**
|
|
12
|
+
After context compaction, coordinators would ignore their own instructions to "spawn workers for remaining subtasks" and edit files directly. The compaction context was narrative ("do this") rather than prescriptive ("NEVER do that").
|
|
13
|
+
|
|
14
|
+
**The Fix:**
|
|
15
|
+
|
|
16
|
+
### 1. Prohibition-First Compaction Context
|
|
17
|
+
|
|
18
|
+
The `SWARM_COMPACTION_CONTEXT` now leads with explicit anti-patterns:
|
|
19
|
+
|
|
20
|
+
```markdown
|
|
21
|
+
### ⛔ NEVER DO THESE (Coordinator Anti-Patterns)
|
|
22
|
+
|
|
23
|
+
- ❌ **NEVER** use `edit` or `write` tools - SPAWN A WORKER
|
|
24
|
+
- ❌ **NEVER** run tests with `bash` - SPAWN A WORKER
|
|
25
|
+
- ❌ **NEVER** implement features yourself - SPAWN A WORKER
|
|
26
|
+
- ❌ **NEVER** "just do it myself to save time" - NO. SPAWN A WORKER.
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. Runtime Violation Detection
|
|
30
|
+
|
|
31
|
+
`detectCoordinatorViolation()` is now wired up in `tool.execute.before`:
|
|
32
|
+
|
|
33
|
+
- Detects when coordinators call `edit`, `write`, or test commands
|
|
34
|
+
- Emits warnings to help coordinators self-correct
|
|
35
|
+
- Captures VIOLATION events for post-hoc analysis
|
|
36
|
+
|
|
37
|
+
### 3. Coordinator Context Tracking
|
|
38
|
+
|
|
39
|
+
New functions track when we're in coordinator mode:
|
|
40
|
+
|
|
41
|
+
- `setCoordinatorContext()` - Activated when `hive_create_epic` or `swarm_decompose` is called
|
|
42
|
+
- `isInCoordinatorContext()` - Checks if we're currently coordinating
|
|
43
|
+
- `clearCoordinatorContext()` - Cleared when epic is closed
|
|
44
|
+
|
|
45
|
+
**Why This Matters:**
|
|
46
|
+
|
|
47
|
+
Coordinators that do implementation work burn context, create conflicts, and defeat the purpose of swarm coordination. This fix makes the anti-pattern visible and provides guardrails to prevent it.
|
|
48
|
+
|
|
49
|
+
**Validation:**
|
|
50
|
+
|
|
51
|
+
- Check `~/.config/swarm-tools/sessions/` for VIOLATION events
|
|
52
|
+
- Run `coordinator-behavior.eval.ts` to score coordinator discipline
|
|
53
|
+
|
|
54
|
+
- [`4c23c7a`](https://github.com/joelhooks/swarm-tools/commit/4c23c7a31013bc6537d83a9294b51540056cde93) Thanks [@joelhooks](https://github.com/joelhooks)! - ## Fix Double Hook Registration
|
|
55
|
+
|
|
56
|
+
The compaction hook was firing twice per compaction event because OpenCode's plugin loader
|
|
57
|
+
calls ALL exports as plugin functions. We were exporting `SwarmPlugin` as both:
|
|
58
|
+
|
|
59
|
+
1. Named export: `export const SwarmPlugin`
|
|
60
|
+
2. Default export: `export default SwarmPlugin`
|
|
61
|
+
|
|
62
|
+
This caused the plugin to register twice, doubling all hook invocations.
|
|
63
|
+
|
|
64
|
+
**Fix:** Changed to default-only export pattern:
|
|
65
|
+
|
|
66
|
+
- `src/index.ts`: `const SwarmPlugin` (no export keyword)
|
|
67
|
+
- `src/plugin.ts`: `export default SwarmPlugin` (no named re-export)
|
|
68
|
+
|
|
69
|
+
**Impact:** Compaction hooks now fire once. LLM calls during compaction reduced by 50%.
|
|
70
|
+
|
|
71
|
+
- Updated dependencies [[`e0c422d`](https://github.com/joelhooks/swarm-tools/commit/e0c422de3f5e15c117cc0cc655c0b03242245be4), [`43c8c93`](https://github.com/joelhooks/swarm-tools/commit/43c8c93ef90b2f04ce59317192334f69d7c4204e)]:
|
|
72
|
+
- swarm-mail@1.5.1
|
|
73
|
+
|
|
3
74
|
## 0.36.0
|
|
4
75
|
|
|
5
76
|
### Minor Changes
|
package/bin/swarm.ts
CHANGED
|
@@ -983,7 +983,7 @@ function getPluginWrapper(): string {
|
|
|
983
983
|
`[swarm] Could not read plugin template from ${templatePath}, using minimal wrapper`,
|
|
984
984
|
);
|
|
985
985
|
return `// Minimal fallback - install opencode-swarm-plugin globally for full functionality
|
|
986
|
-
import
|
|
986
|
+
import SwarmPlugin from "opencode-swarm-plugin"
|
|
987
987
|
export default SwarmPlugin
|
|
988
988
|
`;
|
|
989
989
|
}
|
|
@@ -1709,7 +1709,7 @@ async function doctor() {
|
|
|
1709
1709
|
if (updateInfo) showUpdateNotification(updateInfo);
|
|
1710
1710
|
}
|
|
1711
1711
|
|
|
1712
|
-
async function setup() {
|
|
1712
|
+
async function setup(forceReinstall = false, nonInteractive = false) {
|
|
1713
1713
|
console.clear();
|
|
1714
1714
|
console.log(yellow(BANNER));
|
|
1715
1715
|
console.log(getDecoratedBee());
|
|
@@ -1783,7 +1783,7 @@ async function setup() {
|
|
|
1783
1783
|
legacyWorkerPath,
|
|
1784
1784
|
].filter((f) => existsSync(f));
|
|
1785
1785
|
|
|
1786
|
-
if (existingFiles.length > 0) {
|
|
1786
|
+
if (existingFiles.length > 0 && !forceReinstall) {
|
|
1787
1787
|
p.log.success("Swarm is already configured!");
|
|
1788
1788
|
p.log.message(dim(" Found " + existingFiles.length + "/5 config files"));
|
|
1789
1789
|
|
|
@@ -1903,7 +1903,8 @@ async function setup() {
|
|
|
1903
1903
|
p.log.step("Missing " + requiredMissing.length + " required dependencies");
|
|
1904
1904
|
|
|
1905
1905
|
for (const { dep } of requiredMissing) {
|
|
1906
|
-
|
|
1906
|
+
// In non-interactive mode, auto-install required deps
|
|
1907
|
+
const shouldInstall = nonInteractive ? true : await p.confirm({
|
|
1907
1908
|
message: "Install " + dep.name + "? (" + dep.description + ")",
|
|
1908
1909
|
initialValue: true,
|
|
1909
1910
|
});
|
|
@@ -1931,8 +1932,8 @@ async function setup() {
|
|
|
1931
1932
|
}
|
|
1932
1933
|
}
|
|
1933
1934
|
|
|
1934
|
-
// Only prompt for optional deps if there are missing ones
|
|
1935
|
-
if (optionalMissing.length > 0) {
|
|
1935
|
+
// Only prompt for optional deps if there are missing ones (skip in non-interactive mode)
|
|
1936
|
+
if (optionalMissing.length > 0 && !nonInteractive) {
|
|
1936
1937
|
const installable = optionalMissing.filter(
|
|
1937
1938
|
(r) => r.dep.installType !== "manual",
|
|
1938
1939
|
);
|
|
@@ -2099,135 +2100,157 @@ async function setup() {
|
|
|
2099
2100
|
p.log.message(dim(' No OpenCode config found (skipping MCP check)'));
|
|
2100
2101
|
}
|
|
2101
2102
|
|
|
2102
|
-
// Model
|
|
2103
|
-
|
|
2104
|
-
|
|
2103
|
+
// Model defaults: opus for coordinator, sonnet for worker, haiku for lite
|
|
2104
|
+
const DEFAULT_COORDINATOR = "anthropic/claude-opus-4-5";
|
|
2105
|
+
const DEFAULT_WORKER = "anthropic/claude-sonnet-4-5";
|
|
2106
|
+
const DEFAULT_LITE = "anthropic/claude-haiku-4-5";
|
|
2107
|
+
|
|
2108
|
+
// Model selection (skip if non-interactive)
|
|
2109
|
+
let coordinatorModel: string;
|
|
2110
|
+
let workerModel: string;
|
|
2111
|
+
let liteModel: string;
|
|
2112
|
+
|
|
2113
|
+
if (nonInteractive) {
|
|
2114
|
+
coordinatorModel = DEFAULT_COORDINATOR;
|
|
2115
|
+
workerModel = DEFAULT_WORKER;
|
|
2116
|
+
liteModel = DEFAULT_LITE;
|
|
2117
|
+
p.log.step("Using default models:");
|
|
2118
|
+
p.log.message(dim(` Coordinator: ${coordinatorModel}`));
|
|
2119
|
+
p.log.message(dim(` Worker: ${workerModel}`));
|
|
2120
|
+
p.log.message(dim(` Lite: ${liteModel}`));
|
|
2121
|
+
} else {
|
|
2122
|
+
p.log.step("Configuring swarm agents...");
|
|
2123
|
+
p.log.message(dim(" Coordinator handles orchestration, worker executes tasks"));
|
|
2105
2124
|
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2125
|
+
const selectedCoordinator = await p.select({
|
|
2126
|
+
message: "Select coordinator model (for orchestration/planning):",
|
|
2127
|
+
options: [
|
|
2128
|
+
{
|
|
2129
|
+
value: "anthropic/claude-opus-4-5",
|
|
2130
|
+
label: "Claude Opus 4.5",
|
|
2131
|
+
hint: "Most capable, best for complex orchestration (recommended)",
|
|
2132
|
+
},
|
|
2133
|
+
{
|
|
2134
|
+
value: "anthropic/claude-sonnet-4-5",
|
|
2135
|
+
label: "Claude Sonnet 4.5",
|
|
2136
|
+
hint: "Good balance of speed and capability",
|
|
2137
|
+
},
|
|
2138
|
+
{
|
|
2139
|
+
value: "anthropic/claude-haiku-4-5",
|
|
2140
|
+
label: "Claude Haiku 4.5",
|
|
2141
|
+
hint: "Fast and cost-effective",
|
|
2142
|
+
},
|
|
2143
|
+
{
|
|
2144
|
+
value: "openai/gpt-4o",
|
|
2145
|
+
label: "GPT-4o",
|
|
2146
|
+
hint: "Fast, good for most tasks",
|
|
2147
|
+
},
|
|
2148
|
+
{
|
|
2149
|
+
value: "openai/gpt-4-turbo",
|
|
2150
|
+
label: "GPT-4 Turbo",
|
|
2151
|
+
hint: "Powerful, more expensive",
|
|
2152
|
+
},
|
|
2153
|
+
{
|
|
2154
|
+
value: "google/gemini-2.0-flash",
|
|
2155
|
+
label: "Gemini 2.0 Flash",
|
|
2156
|
+
hint: "Fast and capable",
|
|
2157
|
+
},
|
|
2158
|
+
{
|
|
2159
|
+
value: "google/gemini-1.5-pro",
|
|
2160
|
+
label: "Gemini 1.5 Pro",
|
|
2161
|
+
hint: "More capable",
|
|
2162
|
+
},
|
|
2163
|
+
],
|
|
2164
|
+
initialValue: DEFAULT_COORDINATOR,
|
|
2165
|
+
});
|
|
2147
2166
|
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2167
|
+
if (p.isCancel(selectedCoordinator)) {
|
|
2168
|
+
p.cancel("Setup cancelled");
|
|
2169
|
+
process.exit(0);
|
|
2170
|
+
}
|
|
2171
|
+
coordinatorModel = selectedCoordinator;
|
|
2152
2172
|
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2173
|
+
const selectedWorker = await p.select({
|
|
2174
|
+
message: "Select worker model (for task execution):",
|
|
2175
|
+
options: [
|
|
2176
|
+
{
|
|
2177
|
+
value: "anthropic/claude-sonnet-4-5",
|
|
2178
|
+
label: "Claude Sonnet 4.5",
|
|
2179
|
+
hint: "Best balance of speed and capability (recommended)",
|
|
2180
|
+
},
|
|
2181
|
+
{
|
|
2182
|
+
value: "anthropic/claude-haiku-4-5",
|
|
2183
|
+
label: "Claude Haiku 4.5",
|
|
2184
|
+
hint: "Fast and cost-effective",
|
|
2185
|
+
},
|
|
2186
|
+
{
|
|
2187
|
+
value: "anthropic/claude-opus-4-5",
|
|
2188
|
+
label: "Claude Opus 4.5",
|
|
2189
|
+
hint: "Most capable, slower",
|
|
2190
|
+
},
|
|
2191
|
+
{
|
|
2192
|
+
value: "openai/gpt-4o",
|
|
2193
|
+
label: "GPT-4o",
|
|
2194
|
+
hint: "Fast, good for most tasks",
|
|
2195
|
+
},
|
|
2196
|
+
{
|
|
2197
|
+
value: "openai/gpt-4-turbo",
|
|
2198
|
+
label: "GPT-4 Turbo",
|
|
2199
|
+
hint: "Powerful, more expensive",
|
|
2200
|
+
},
|
|
2201
|
+
{
|
|
2202
|
+
value: "google/gemini-2.0-flash",
|
|
2203
|
+
label: "Gemini 2.0 Flash",
|
|
2204
|
+
hint: "Fast and capable",
|
|
2205
|
+
},
|
|
2206
|
+
{
|
|
2207
|
+
value: "google/gemini-1.5-pro",
|
|
2208
|
+
label: "Gemini 1.5 Pro",
|
|
2209
|
+
hint: "More capable",
|
|
2210
|
+
},
|
|
2211
|
+
],
|
|
2212
|
+
initialValue: DEFAULT_WORKER,
|
|
2213
|
+
});
|
|
2194
2214
|
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2215
|
+
if (p.isCancel(selectedWorker)) {
|
|
2216
|
+
p.cancel("Setup cancelled");
|
|
2217
|
+
process.exit(0);
|
|
2218
|
+
}
|
|
2219
|
+
workerModel = selectedWorker;
|
|
2199
2220
|
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2221
|
+
// Lite model selection for simple tasks (docs, tests)
|
|
2222
|
+
const selectedLite = await p.select({
|
|
2223
|
+
message: "Select lite model (for docs, tests, simple edits):",
|
|
2224
|
+
options: [
|
|
2225
|
+
{
|
|
2226
|
+
value: "anthropic/claude-haiku-4-5",
|
|
2227
|
+
label: "Claude Haiku 4.5",
|
|
2228
|
+
hint: "Fast and cost-effective (recommended)",
|
|
2229
|
+
},
|
|
2230
|
+
{
|
|
2231
|
+
value: "anthropic/claude-sonnet-4-5",
|
|
2232
|
+
label: "Claude Sonnet 4.5",
|
|
2233
|
+
hint: "More capable, slower",
|
|
2234
|
+
},
|
|
2235
|
+
{
|
|
2236
|
+
value: "openai/gpt-4o-mini",
|
|
2237
|
+
label: "GPT-4o Mini",
|
|
2238
|
+
hint: "Fast and cheap",
|
|
2239
|
+
},
|
|
2240
|
+
{
|
|
2241
|
+
value: "google/gemini-2.0-flash",
|
|
2242
|
+
label: "Gemini 2.0 Flash",
|
|
2243
|
+
hint: "Fast and capable",
|
|
2244
|
+
},
|
|
2245
|
+
],
|
|
2246
|
+
initialValue: DEFAULT_LITE,
|
|
2247
|
+
});
|
|
2227
2248
|
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2249
|
+
if (p.isCancel(selectedLite)) {
|
|
2250
|
+
p.cancel("Setup cancelled");
|
|
2251
|
+
process.exit(0);
|
|
2252
|
+
}
|
|
2253
|
+
liteModel = selectedLite;
|
|
2231
2254
|
}
|
|
2232
2255
|
|
|
2233
2256
|
p.log.success("Selected models:");
|
|
@@ -2249,7 +2272,8 @@ async function setup() {
|
|
|
2249
2272
|
|
|
2250
2273
|
// Write plugin and command files
|
|
2251
2274
|
p.log.step("Writing configuration files...");
|
|
2252
|
-
|
|
2275
|
+
const pluginContent = getPluginWrapper().replace(/__SWARM_LITE_MODEL__/g, liteModel);
|
|
2276
|
+
stats[writeFileWithStatus(pluginPath, pluginContent, "Plugin")]++;
|
|
2253
2277
|
stats[writeFileWithStatus(commandPath, SWARM_COMMAND, "Command")]++;
|
|
2254
2278
|
|
|
2255
2279
|
// Write nested agent files (swarm/planner.md, swarm/worker.md, swarm/researcher.md)
|
|
@@ -2288,21 +2312,8 @@ async function setup() {
|
|
|
2288
2312
|
);
|
|
2289
2313
|
|
|
2290
2314
|
if (missingBundled.length > 0 || managedBundled.length > 0) {
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
"Sync bundled skills into your global skills directory? " +
|
|
2294
|
-
(missingBundled.length > 0
|
|
2295
|
-
? `(${missingBundled.length} missing)`
|
|
2296
|
-
: "(update managed skills)"),
|
|
2297
|
-
initialValue: isReinstall || missingBundled.length > 0,
|
|
2298
|
-
});
|
|
2299
|
-
|
|
2300
|
-
if (p.isCancel(shouldSync)) {
|
|
2301
|
-
p.cancel("Setup cancelled");
|
|
2302
|
-
process.exit(0);
|
|
2303
|
-
}
|
|
2304
|
-
|
|
2305
|
-
if (shouldSync) {
|
|
2315
|
+
// Always sync bundled skills - no prompt needed
|
|
2316
|
+
{
|
|
2306
2317
|
const syncSpinner = p.spinner();
|
|
2307
2318
|
syncSpinner.start("Syncing bundled skills...");
|
|
2308
2319
|
try {
|
|
@@ -2339,15 +2350,10 @@ async function setup() {
|
|
|
2339
2350
|
}
|
|
2340
2351
|
}
|
|
2341
2352
|
|
|
2342
|
-
//
|
|
2353
|
+
// Always update AGENTS.md with skill awareness - no prompt needed
|
|
2343
2354
|
const agentsPath = join(configDir, "AGENTS.md");
|
|
2344
2355
|
if (existsSync(agentsPath)) {
|
|
2345
|
-
|
|
2346
|
-
message: "Update AGENTS.md with skill awareness?",
|
|
2347
|
-
initialValue: true,
|
|
2348
|
-
});
|
|
2349
|
-
|
|
2350
|
-
if (!p.isCancel(updateAgents) && updateAgents) {
|
|
2356
|
+
{
|
|
2351
2357
|
const s = p.spinner();
|
|
2352
2358
|
s.start("Updating AGENTS.md...");
|
|
2353
2359
|
|
|
@@ -2708,8 +2714,10 @@ async function help() {
|
|
|
2708
2714
|
console.log(magenta(" " + getRandomMessage()));
|
|
2709
2715
|
console.log(`
|
|
2710
2716
|
${cyan("Commands:")}
|
|
2711
|
-
swarm setup
|
|
2712
|
-
|
|
2717
|
+
swarm setup Interactive installer - checks and installs dependencies
|
|
2718
|
+
--reinstall, -r Skip prompt, go straight to reinstall
|
|
2719
|
+
--yes, -y Non-interactive with defaults (opus/sonnet/haiku)
|
|
2720
|
+
swarm doctor Health check - shows status of all dependencies
|
|
2713
2721
|
swarm init Initialize beads in current project
|
|
2714
2722
|
swarm config Show paths to generated config files
|
|
2715
2723
|
swarm agents Update AGENTS.md with skill awareness
|
|
@@ -3101,17 +3109,43 @@ interface LogLine {
|
|
|
3101
3109
|
time: string;
|
|
3102
3110
|
module: string;
|
|
3103
3111
|
msg: string;
|
|
3112
|
+
data?: Record<string, unknown>; // Extra structured data
|
|
3104
3113
|
}
|
|
3105
3114
|
|
|
3106
|
-
function parseLogLine(line: string): LogLine | null {
|
|
3115
|
+
function parseLogLine(line: string, sourceFile?: string): LogLine | null {
|
|
3107
3116
|
try {
|
|
3108
3117
|
const parsed = JSON.parse(line);
|
|
3109
|
-
if (
|
|
3118
|
+
if (parsed.time && parsed.msg) {
|
|
3119
|
+
// Handle both pino format (level: number) and plugin wrapper format (level: string)
|
|
3120
|
+
let level: number;
|
|
3121
|
+
if (typeof parsed.level === "number") {
|
|
3122
|
+
level = parsed.level;
|
|
3123
|
+
} else if (typeof parsed.level === "string") {
|
|
3124
|
+
level = levelNameToNumber(parsed.level);
|
|
3125
|
+
} else {
|
|
3126
|
+
level = 30; // default to info
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
// Derive module from: explicit field, or source filename (e.g., "compaction.log" -> "compaction")
|
|
3130
|
+
let module = parsed.module;
|
|
3131
|
+
if (!module && sourceFile) {
|
|
3132
|
+
// Extract module from filename: "compaction.log" -> "compaction", "swarm.1log" -> "swarm"
|
|
3133
|
+
const match = sourceFile.match(/([^/]+?)(?:\.\d+)?\.?log$/);
|
|
3134
|
+
if (match) {
|
|
3135
|
+
module = match[1];
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
// Extract extra data (everything except core fields)
|
|
3140
|
+
const { level: _l, time: _t, module: _m, msg: _msg, ...extraData } = parsed;
|
|
3141
|
+
const hasExtraData = Object.keys(extraData).length > 0;
|
|
3142
|
+
|
|
3110
3143
|
return {
|
|
3111
|
-
level
|
|
3144
|
+
level,
|
|
3112
3145
|
time: parsed.time,
|
|
3113
|
-
module:
|
|
3146
|
+
module: module || "unknown",
|
|
3114
3147
|
msg: parsed.msg,
|
|
3148
|
+
data: hasExtraData ? extraData : undefined,
|
|
3115
3149
|
};
|
|
3116
3150
|
}
|
|
3117
3151
|
} catch {
|
|
@@ -3164,36 +3198,51 @@ function parseDuration(duration: string): number | null {
|
|
|
3164
3198
|
return value * multipliers[unit];
|
|
3165
3199
|
}
|
|
3166
3200
|
|
|
3167
|
-
function formatLogLine(log: LogLine, useColor = true): string {
|
|
3201
|
+
function formatLogLine(log: LogLine, useColor = true, verbose = false): string {
|
|
3168
3202
|
const timestamp = new Date(log.time).toLocaleTimeString();
|
|
3169
3203
|
const levelName = levelToName(log.level);
|
|
3170
3204
|
const module = log.module.padEnd(12);
|
|
3171
3205
|
const levelStr = useColor ? levelToColor(log.level)(levelName) : levelName;
|
|
3172
3206
|
|
|
3173
|
-
|
|
3207
|
+
let output = `${timestamp} ${levelStr} ${module} ${log.msg}`;
|
|
3208
|
+
|
|
3209
|
+
// In verbose mode, pretty print the structured data
|
|
3210
|
+
if (verbose && log.data) {
|
|
3211
|
+
output += `\n${dim(JSON.stringify(log.data, null, 2))}`;
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
return output;
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
interface LogEntry {
|
|
3218
|
+
line: string;
|
|
3219
|
+
file: string;
|
|
3174
3220
|
}
|
|
3175
3221
|
|
|
3176
|
-
function readLogFiles(dir: string):
|
|
3222
|
+
function readLogFiles(dir: string): LogEntry[] {
|
|
3177
3223
|
if (!existsSync(dir)) return [];
|
|
3178
3224
|
|
|
3179
3225
|
const allFiles = readdirSync(dir);
|
|
3226
|
+
// Match both pino-roll format (*.1log, *.2log) AND plain *.log files
|
|
3180
3227
|
const logFiles = allFiles
|
|
3181
|
-
.filter((f: string) => /\.\d+log$/.test(f))
|
|
3228
|
+
.filter((f: string) => /\.\d+log$/.test(f) || /\.log$/.test(f))
|
|
3182
3229
|
.sort()
|
|
3183
3230
|
.map((f: string) => join(dir, f));
|
|
3184
3231
|
|
|
3185
|
-
const
|
|
3232
|
+
const entries: LogEntry[] = [];
|
|
3186
3233
|
for (const file of logFiles) {
|
|
3187
3234
|
try {
|
|
3188
3235
|
const content = readFileSync(file, "utf-8");
|
|
3189
3236
|
const fileLines = content.split("\n").filter((line: string) => line.trim());
|
|
3190
|
-
|
|
3237
|
+
for (const line of fileLines) {
|
|
3238
|
+
entries.push({ line, file });
|
|
3239
|
+
}
|
|
3191
3240
|
} catch {
|
|
3192
3241
|
// Skip unreadable files
|
|
3193
3242
|
}
|
|
3194
3243
|
}
|
|
3195
3244
|
|
|
3196
|
-
return
|
|
3245
|
+
return entries;
|
|
3197
3246
|
}
|
|
3198
3247
|
|
|
3199
3248
|
async function logs() {
|
|
@@ -3207,6 +3256,7 @@ async function logs() {
|
|
|
3207
3256
|
let limit = 50;
|
|
3208
3257
|
let watchMode = false;
|
|
3209
3258
|
let pollInterval = 1000; // 1 second default
|
|
3259
|
+
let verbose = false;
|
|
3210
3260
|
|
|
3211
3261
|
for (let i = 0; i < args.length; i++) {
|
|
3212
3262
|
const arg = args[i];
|
|
@@ -3231,6 +3281,8 @@ async function logs() {
|
|
|
3231
3281
|
}
|
|
3232
3282
|
} else if (arg === "--watch" || arg === "-w") {
|
|
3233
3283
|
watchMode = true;
|
|
3284
|
+
} else if (arg === "--verbose" || arg === "-v") {
|
|
3285
|
+
verbose = true;
|
|
3234
3286
|
} else if (arg === "--interval" && i + 1 < args.length) {
|
|
3235
3287
|
pollInterval = parseInt(args[++i], 10);
|
|
3236
3288
|
if (isNaN(pollInterval) || pollInterval < 100) {
|
|
@@ -3290,7 +3342,7 @@ async function logs() {
|
|
|
3290
3342
|
// Initialize positions from current file sizes
|
|
3291
3343
|
const initializePositions = () => {
|
|
3292
3344
|
if (!existsSync(logsDir)) return;
|
|
3293
|
-
const files = readdirSync(logsDir).filter((f: string) => /\.\d+log$/.test(f));
|
|
3345
|
+
const files = readdirSync(logsDir).filter((f: string) => /\.\d+log$/.test(f) || /\.log$/.test(f));
|
|
3294
3346
|
for (const file of files) {
|
|
3295
3347
|
const filePath = join(logsDir, file);
|
|
3296
3348
|
try {
|
|
@@ -3327,14 +3379,14 @@ async function logs() {
|
|
|
3327
3379
|
};
|
|
3328
3380
|
|
|
3329
3381
|
// Print initial logs (last N lines)
|
|
3330
|
-
const
|
|
3331
|
-
let logs: LogLine[] =
|
|
3332
|
-
.map(parseLogLine)
|
|
3382
|
+
const rawEntries = readLogFiles(logsDir);
|
|
3383
|
+
let logs: LogLine[] = rawEntries
|
|
3384
|
+
.map(entry => parseLogLine(entry.line, entry.file))
|
|
3333
3385
|
.filter((log): log is LogLine => log !== null);
|
|
3334
3386
|
logs = filterLogs(logs).slice(-limit);
|
|
3335
3387
|
|
|
3336
3388
|
for (const log of logs) {
|
|
3337
|
-
console.log(formatLogLine(log));
|
|
3389
|
+
console.log(formatLogLine(log, true, verbose));
|
|
3338
3390
|
}
|
|
3339
3391
|
|
|
3340
3392
|
// Initialize positions after printing initial logs
|
|
@@ -3344,18 +3396,18 @@ async function logs() {
|
|
|
3344
3396
|
const pollForNewLogs = () => {
|
|
3345
3397
|
if (!existsSync(logsDir)) return;
|
|
3346
3398
|
|
|
3347
|
-
const files = readdirSync(logsDir).filter((f: string) => /\.\d+log$/.test(f));
|
|
3399
|
+
const files = readdirSync(logsDir).filter((f: string) => /\.\d+log$/.test(f) || /\.log$/.test(f));
|
|
3348
3400
|
|
|
3349
3401
|
for (const file of files) {
|
|
3350
3402
|
const filePath = join(logsDir, file);
|
|
3351
3403
|
const newLines = readNewLines(filePath);
|
|
3352
3404
|
|
|
3353
3405
|
for (const line of newLines) {
|
|
3354
|
-
const parsed = parseLogLine(line);
|
|
3406
|
+
const parsed = parseLogLine(line, filePath);
|
|
3355
3407
|
if (parsed) {
|
|
3356
3408
|
const filtered = filterLogs([parsed]);
|
|
3357
3409
|
if (filtered.length > 0) {
|
|
3358
|
-
console.log(formatLogLine(filtered[0]));
|
|
3410
|
+
console.log(formatLogLine(filtered[0], true, verbose));
|
|
3359
3411
|
}
|
|
3360
3412
|
}
|
|
3361
3413
|
}
|
|
@@ -3381,11 +3433,11 @@ async function logs() {
|
|
|
3381
3433
|
}
|
|
3382
3434
|
|
|
3383
3435
|
// Non-watch mode - one-shot output
|
|
3384
|
-
const
|
|
3436
|
+
const rawEntries = readLogFiles(logsDir);
|
|
3385
3437
|
|
|
3386
3438
|
// Parse and filter
|
|
3387
|
-
let logs: LogLine[] =
|
|
3388
|
-
.map(parseLogLine)
|
|
3439
|
+
let logs: LogLine[] = rawEntries
|
|
3440
|
+
.map(entry => parseLogLine(entry.line, entry.file))
|
|
3389
3441
|
.filter((log): log is LogLine => log !== null);
|
|
3390
3442
|
|
|
3391
3443
|
logs = filterLogs(logs);
|
|
@@ -3410,7 +3462,7 @@ async function logs() {
|
|
|
3410
3462
|
console.log();
|
|
3411
3463
|
|
|
3412
3464
|
for (const log of logs) {
|
|
3413
|
-
console.log(formatLogLine(log));
|
|
3465
|
+
console.log(formatLogLine(log, true, verbose));
|
|
3414
3466
|
}
|
|
3415
3467
|
console.log();
|
|
3416
3468
|
}
|
|
@@ -3522,9 +3574,12 @@ async function db() {
|
|
|
3522
3574
|
const command = process.argv[2];
|
|
3523
3575
|
|
|
3524
3576
|
switch (command) {
|
|
3525
|
-
case "setup":
|
|
3526
|
-
|
|
3577
|
+
case "setup": {
|
|
3578
|
+
const reinstallFlag = process.argv.includes("--reinstall") || process.argv.includes("-r");
|
|
3579
|
+
const yesFlag = process.argv.includes("--yes") || process.argv.includes("-y");
|
|
3580
|
+
await setup(reinstallFlag || yesFlag, yesFlag);
|
|
3527
3581
|
break;
|
|
3582
|
+
}
|
|
3528
3583
|
case "doctor":
|
|
3529
3584
|
await doctor();
|
|
3530
3585
|
break;
|