@swarmclawai/swarmclaw 1.6.0 → 1.7.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.
- package/README.md +20 -0
- package/package.json +3 -3
- package/src/app/.well-known/agent-card.json/route.ts +15 -0
- package/src/app/api/.well-known/agent-card/route.ts +6 -37
- package/src/app/home/page.tsx +10 -19
- package/src/components/auth/setup-wizard/index.tsx +2 -6
- package/src/components/auth/setup-wizard/step-next.tsx +39 -46
- package/src/components/auth/setup-wizard/step-providers.tsx +142 -76
- package/src/components/auth/setup-wizard/types.ts +2 -5
- package/src/components/auth/setup-wizard/utils.test.ts +19 -0
- package/src/components/auth/setup-wizard/utils.ts +69 -0
- package/src/components/home/home-launchpad.tsx +100 -80
- package/src/lib/a2a/agent-card.test.ts +94 -0
- package/src/lib/a2a/agent-card.ts +41 -1
- package/src/lib/home-launchpad.test.ts +31 -1
- package/src/lib/home-launchpad.ts +58 -0
- package/src/lib/providers/cli-utils.test.ts +10 -0
- package/src/lib/providers/cli-utils.ts +31 -0
- package/src/lib/providers/generic-cli.test.ts +71 -0
- package/src/lib/providers/generic-cli.ts +138 -0
- package/src/lib/providers/index.ts +56 -1
- package/src/lib/providers/opencode-cli.test.ts +9 -0
- package/src/lib/providers/opencode-cli.ts +5 -1
- package/src/lib/server/missions/mission-templates.test.ts +17 -0
- package/src/lib/server/missions/mission-templates.ts +69 -0
- package/src/lib/server/protocols/protocol-service.test.ts +25 -0
- package/src/lib/server/protocols/protocol-templates.ts +48 -0
- package/src/lib/strip-internal-metadata.test.ts +23 -0
- package/src/lib/strip-internal-metadata.ts +136 -7
- package/src/types/provider.ts +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { GENERIC_CLI_BINARIES, streamGenericCliChat } from './generic-cli'
|
|
5
|
+
|
|
6
|
+
describe('GENERIC_CLI_BINARIES', () => {
|
|
7
|
+
it('maps every generic CLI provider id to a non-empty binary name', () => {
|
|
8
|
+
for (const [providerId, binaryName] of Object.entries(GENERIC_CLI_BINARIES)) {
|
|
9
|
+
assert.equal(typeof binaryName, 'string', `${providerId} binary must be a string`)
|
|
10
|
+
assert.ok(binaryName.length > 0, `${providerId} binary must not be empty`)
|
|
11
|
+
assert.ok(providerId.endsWith('-cli'), `${providerId} should end with -cli for the registry pattern`)
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('does not collide with bespoke CLI provider ids', () => {
|
|
16
|
+
const bespoke = new Set([
|
|
17
|
+
'claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli',
|
|
18
|
+
'droid-cli', 'cursor-cli', 'qwen-code-cli', 'goose',
|
|
19
|
+
])
|
|
20
|
+
for (const id of Object.keys(GENERIC_CLI_BINARIES)) {
|
|
21
|
+
assert.equal(bespoke.has(id), false, `${id} must not collide with a bespoke CLI provider`)
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('streamGenericCliChat', () => {
|
|
27
|
+
it('streams stdout lines as deltas via the SSE write callback when the binary exists', async () => {
|
|
28
|
+
// Use `echo` as a stand-in binary. Every POSIX system has it on PATH.
|
|
29
|
+
// Note: `echo "<prompt>"` will print the prompt itself, which exercises
|
|
30
|
+
// the line-buffered pipe.
|
|
31
|
+
const writes: string[] = []
|
|
32
|
+
const active = new Map<string, unknown>()
|
|
33
|
+
|
|
34
|
+
const result = await streamGenericCliChat({
|
|
35
|
+
session: { id: 'test-session', cwd: process.cwd() },
|
|
36
|
+
message: 'hello world',
|
|
37
|
+
write: (data) => writes.push(data),
|
|
38
|
+
active,
|
|
39
|
+
loadHistory: () => [],
|
|
40
|
+
binaryName: 'echo',
|
|
41
|
+
displayName: 'Echo',
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
assert.ok(result.includes('hello world'), `expected response to include the prompt, got: ${JSON.stringify(result)}`)
|
|
45
|
+
assert.ok(writes.length > 0, 'should have emitted at least one SSE event')
|
|
46
|
+
const allText = writes.join('')
|
|
47
|
+
assert.ok(allText.includes('hello world'), 'SSE stream should include the echoed prompt')
|
|
48
|
+
assert.ok(!allText.includes('"t":"err"'), 'should not emit an error event on a successful run')
|
|
49
|
+
assert.equal(active.size, 0, 'session should be removed from active map after close')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('emits an error event when the binary cannot be found on PATH', async () => {
|
|
53
|
+
const writes: string[] = []
|
|
54
|
+
const active = new Map<string, unknown>()
|
|
55
|
+
|
|
56
|
+
const result = await streamGenericCliChat({
|
|
57
|
+
session: { id: 'missing-bin-session', cwd: process.cwd() },
|
|
58
|
+
message: 'test',
|
|
59
|
+
write: (data) => writes.push(data),
|
|
60
|
+
active,
|
|
61
|
+
loadHistory: () => [],
|
|
62
|
+
binaryName: 'definitely-not-a-real-binary-zzz-' + Date.now(),
|
|
63
|
+
displayName: 'Nonexistent CLI',
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
assert.equal(result, '', 'missing binary should produce empty response')
|
|
67
|
+
assert.equal(writes.length, 1, 'should emit exactly one error event')
|
|
68
|
+
assert.ok(writes[0].includes('"t":"err"'), 'event should carry the err type')
|
|
69
|
+
assert.ok(writes[0].includes('Nonexistent CLI not found'), 'error message should mention the display name')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { spawn } from 'child_process'
|
|
2
|
+
import type { StreamChatOptions } from './index'
|
|
3
|
+
import { log } from '../server/logger'
|
|
4
|
+
import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
|
|
5
|
+
import { resolveCliBinary, buildCliEnv, attachAbortHandler, isStderrNoise } from './cli-utils'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Map of swarmclaw provider id to the binary name we look up on PATH.
|
|
9
|
+
* Used by the generic CLI streamer for tools without a bespoke handler.
|
|
10
|
+
*/
|
|
11
|
+
export const GENERIC_CLI_BINARIES: Record<string, string> = {
|
|
12
|
+
'aider-cli': 'aider',
|
|
13
|
+
'amp-cli': 'amp',
|
|
14
|
+
'augment-cli': 'augment',
|
|
15
|
+
'adal-cli': 'adal',
|
|
16
|
+
'bob-cli': 'bob',
|
|
17
|
+
'cline-cli': 'cline',
|
|
18
|
+
'codebuddy-cli': 'codebuddy',
|
|
19
|
+
'command-code-cli': 'commandcode',
|
|
20
|
+
'continue-cli': 'continue',
|
|
21
|
+
'cortex-cli': 'cortex',
|
|
22
|
+
'crush-cli': 'crush',
|
|
23
|
+
'deepagents-cli': 'deepagents',
|
|
24
|
+
'firebender-cli': 'firebender',
|
|
25
|
+
'iflow-cli': 'iflow',
|
|
26
|
+
'junie-cli': 'junie',
|
|
27
|
+
'kilo-code-cli': 'kilocode',
|
|
28
|
+
'kimi-cli': 'kimi',
|
|
29
|
+
'kode-cli': 'kode',
|
|
30
|
+
'mcpjam-cli': 'mcpjam',
|
|
31
|
+
'mistral-vibe-cli': 'vibe',
|
|
32
|
+
'mux-cli': 'mux',
|
|
33
|
+
'neovate-cli': 'neovate',
|
|
34
|
+
'openhands-cli': 'openhands',
|
|
35
|
+
'pochi-cli': 'pochi',
|
|
36
|
+
'qoder-cli': 'qoder',
|
|
37
|
+
'replit-cli': 'replit',
|
|
38
|
+
'roo-code-cli': 'roo',
|
|
39
|
+
'trae-cn-cli': 'trae-cn',
|
|
40
|
+
'warp-cli': 'warp',
|
|
41
|
+
'windsurf-cli': 'windsurf',
|
|
42
|
+
'zencoder-cli': 'zencoder',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface GenericCliOptions extends StreamChatOptions {
|
|
46
|
+
binaryName: string
|
|
47
|
+
displayName: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generic streamer for CLI providers without a bespoke stream parser.
|
|
52
|
+
*
|
|
53
|
+
* Spawns the configured binary with the prompt as the final argv, captures
|
|
54
|
+
* stdout/stderr line-by-line, and emits each line as a delta. No JSON event
|
|
55
|
+
* parsing — callers that need structured event streams should use a tool-
|
|
56
|
+
* specific provider instead.
|
|
57
|
+
*/
|
|
58
|
+
export function streamGenericCliChat(opts: GenericCliOptions): Promise<string> {
|
|
59
|
+
const { session, message, systemPrompt, write, active, signal, binaryName, displayName } = opts
|
|
60
|
+
const processTimeoutMs = loadRuntimeSettings().cliProcessTimeoutMs
|
|
61
|
+
const binary = resolveCliBinary(binaryName)
|
|
62
|
+
if (!binary) {
|
|
63
|
+
const msg = `${displayName} not found. Install \`${binaryName}\` and ensure it is on your PATH, or remove this provider.`
|
|
64
|
+
write(`data: ${JSON.stringify({ t: 'err', text: msg })}\n\n`)
|
|
65
|
+
return Promise.resolve('')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const env = buildCliEnv()
|
|
69
|
+
const prompt = systemPrompt ? `[System instructions]\n${systemPrompt}\n\n${message}` : message
|
|
70
|
+
|
|
71
|
+
log.info('generic-cli', `Spawning: ${binary}`, {
|
|
72
|
+
binaryName,
|
|
73
|
+
cwd: session.cwd,
|
|
74
|
+
hasSystemPrompt: Boolean(systemPrompt),
|
|
75
|
+
promptLength: prompt.length,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const proc = spawn(binary, [prompt], {
|
|
79
|
+
cwd: session.cwd,
|
|
80
|
+
env,
|
|
81
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
82
|
+
timeout: processTimeoutMs,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
active.set(session.id, proc)
|
|
86
|
+
attachAbortHandler(proc, signal)
|
|
87
|
+
|
|
88
|
+
let fullResponse = ''
|
|
89
|
+
let buf = ''
|
|
90
|
+
let stderrText = ''
|
|
91
|
+
|
|
92
|
+
proc.stdout?.on('data', (chunk: Buffer) => {
|
|
93
|
+
buf += chunk.toString()
|
|
94
|
+
const lines = buf.split('\n')
|
|
95
|
+
buf = lines.pop() || ''
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
if (!line) continue
|
|
98
|
+
fullResponse += `${line}\n`
|
|
99
|
+
write(`data: ${JSON.stringify({ t: 'd', text: `${line}\n` })}\n\n`)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
proc.stderr?.on('data', (chunk: Buffer) => {
|
|
104
|
+
const text = chunk.toString()
|
|
105
|
+
stderrText += text
|
|
106
|
+
if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
|
|
107
|
+
if (isStderrNoise(text)) {
|
|
108
|
+
log.debug('generic-cli', `stderr noise [${binaryName}/${session.id}]`, text.slice(0, 400))
|
|
109
|
+
} else {
|
|
110
|
+
log.warn('generic-cli', `stderr [${binaryName}/${session.id}]`, text.slice(0, 400))
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
proc.on('close', (code, sig) => {
|
|
116
|
+
active.delete(session.id)
|
|
117
|
+
if (buf) {
|
|
118
|
+
fullResponse += buf
|
|
119
|
+
write(`data: ${JSON.stringify({ t: 'd', text: buf })}\n\n`)
|
|
120
|
+
buf = ''
|
|
121
|
+
}
|
|
122
|
+
if ((code ?? 0) !== 0 && !fullResponse.trim()) {
|
|
123
|
+
const msg = stderrText.trim()
|
|
124
|
+
? `${displayName} exited with code ${code ?? 'unknown'}${sig ? ` (${sig})` : ''}: ${stderrText.trim().slice(0, 1200)}`
|
|
125
|
+
: `${displayName} exited with code ${code ?? 'unknown'}${sig ? ` (${sig})` : ''} and returned no output.`
|
|
126
|
+
write(`data: ${JSON.stringify({ t: 'err', text: msg })}\n\n`)
|
|
127
|
+
}
|
|
128
|
+
log.info('generic-cli', `Process closed [${binaryName}]: code=${code} signal=${sig} response=${fullResponse.length}chars`)
|
|
129
|
+
resolve(fullResponse.trim())
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
proc.on('error', (err) => {
|
|
133
|
+
active.delete(session.id)
|
|
134
|
+
write(`data: ${JSON.stringify({ t: 'err', text: err.message })}\n\n`)
|
|
135
|
+
resolve(fullResponse.trim())
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
}
|
|
@@ -8,6 +8,7 @@ import { streamDroidCliChat } from './droid-cli'
|
|
|
8
8
|
import { streamCursorCliChat } from './cursor-cli'
|
|
9
9
|
import { streamQwenCodeCliChat } from './qwen-code-cli'
|
|
10
10
|
import { streamGooseChat } from './goose'
|
|
11
|
+
import { streamGenericCliChat, GENERIC_CLI_BINARIES } from './generic-cli'
|
|
11
12
|
import { streamOpenAiChat } from './openai'
|
|
12
13
|
import { streamOllamaChat } from './ollama'
|
|
13
14
|
import { streamAnthropicChat } from './anthropic'
|
|
@@ -51,6 +52,59 @@ interface BuiltinProviderConfig extends ProviderInfo {
|
|
|
51
52
|
handler: ProviderHandler
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
const GENERIC_CLI_DISPLAY_NAMES: Record<string, string> = {
|
|
56
|
+
'aider-cli': 'Aider CLI',
|
|
57
|
+
'amp-cli': 'Amp CLI',
|
|
58
|
+
'augment-cli': 'Augment CLI',
|
|
59
|
+
'adal-cli': 'AdaL CLI',
|
|
60
|
+
'bob-cli': 'IBM Bob CLI',
|
|
61
|
+
'cline-cli': 'Cline CLI',
|
|
62
|
+
'codebuddy-cli': 'CodeBuddy CLI',
|
|
63
|
+
'command-code-cli': 'Command Code CLI',
|
|
64
|
+
'continue-cli': 'Continue CLI',
|
|
65
|
+
'cortex-cli': 'Cortex Code CLI',
|
|
66
|
+
'crush-cli': 'Crush CLI',
|
|
67
|
+
'deepagents-cli': 'Deep Agents CLI',
|
|
68
|
+
'firebender-cli': 'Firebender CLI',
|
|
69
|
+
'iflow-cli': 'iFlow CLI',
|
|
70
|
+
'junie-cli': 'Junie CLI',
|
|
71
|
+
'kilo-code-cli': 'Kilo Code CLI',
|
|
72
|
+
'kimi-cli': 'Kimi Code CLI',
|
|
73
|
+
'kode-cli': 'Kode CLI',
|
|
74
|
+
'mcpjam-cli': 'MCPJam CLI',
|
|
75
|
+
'mistral-vibe-cli': 'Mistral Vibe CLI',
|
|
76
|
+
'mux-cli': 'Mux CLI',
|
|
77
|
+
'neovate-cli': 'Neovate CLI',
|
|
78
|
+
'openhands-cli': 'OpenHands CLI',
|
|
79
|
+
'pochi-cli': 'Pochi CLI',
|
|
80
|
+
'qoder-cli': 'Qoder CLI',
|
|
81
|
+
'replit-cli': 'Replit Agent CLI',
|
|
82
|
+
'roo-code-cli': 'Roo Code CLI',
|
|
83
|
+
'trae-cn-cli': 'TRAE CN CLI',
|
|
84
|
+
'warp-cli': 'Warp Agent CLI',
|
|
85
|
+
'windsurf-cli': 'Windsurf CLI',
|
|
86
|
+
'zencoder-cli': 'Zencoder CLI',
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildGenericCliEntries(): Record<string, BuiltinProviderConfig> {
|
|
90
|
+
const entries: Record<string, BuiltinProviderConfig> = {}
|
|
91
|
+
for (const [providerId, binaryName] of Object.entries(GENERIC_CLI_BINARIES)) {
|
|
92
|
+
const displayName = GENERIC_CLI_DISPLAY_NAMES[providerId] ?? providerId
|
|
93
|
+
entries[providerId] = {
|
|
94
|
+
id: providerId,
|
|
95
|
+
name: displayName,
|
|
96
|
+
models: ['default'],
|
|
97
|
+
requiresApiKey: false,
|
|
98
|
+
optionalApiKey: true,
|
|
99
|
+
requiresEndpoint: false,
|
|
100
|
+
handler: {
|
|
101
|
+
streamChat: (opts) => streamGenericCliChat({ ...opts, binaryName, displayName }),
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return entries
|
|
106
|
+
}
|
|
107
|
+
|
|
54
108
|
export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
55
109
|
'claude-cli': {
|
|
56
110
|
id: 'claude-cli',
|
|
@@ -223,6 +277,7 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
223
277
|
requiresEndpoint: false,
|
|
224
278
|
handler: { streamChat: streamGooseChat },
|
|
225
279
|
},
|
|
280
|
+
...buildGenericCliEntries(),
|
|
226
281
|
google: {
|
|
227
282
|
id: 'google',
|
|
228
283
|
name: 'Google Gemini',
|
|
@@ -475,7 +530,7 @@ export function getProviderList(): ProviderInfo[] {
|
|
|
475
530
|
...info,
|
|
476
531
|
models: overrides[info.id] || info.models,
|
|
477
532
|
defaultModels: info.models,
|
|
478
|
-
supportsModelDiscovery: !['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'droid-cli', 'cursor-cli', 'qwen-code-cli', 'goose', 'fireworks'].includes(info.id),
|
|
533
|
+
supportsModelDiscovery: !['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'droid-cli', 'cursor-cli', 'qwen-code-cli', 'goose', 'fireworks', ...Object.keys(GENERIC_CLI_BINARIES)].includes(info.id),
|
|
479
534
|
}
|
|
480
535
|
})
|
|
481
536
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import { OPENCODE_CLI_STDIO } from './opencode-cli'
|
|
4
|
+
|
|
5
|
+
describe('opencode-cli provider', () => {
|
|
6
|
+
it('closes child stdin so argv-prompt runs do not hang waiting for input', () => {
|
|
7
|
+
assert.deepEqual(OPENCODE_CLI_STDIO, ['ignore', 'pipe', 'pipe'])
|
|
8
|
+
})
|
|
9
|
+
})
|
|
@@ -4,6 +4,8 @@ import { log } from '../server/logger'
|
|
|
4
4
|
import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
|
|
5
5
|
import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, isStderrNoise } from './cli-utils'
|
|
6
6
|
|
|
7
|
+
export const OPENCODE_CLI_STDIO: ['ignore', 'pipe', 'pipe'] = ['ignore', 'pipe', 'pipe']
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* OpenCode CLI provider — spawns `opencode run <message> --format json` for non-interactive usage.
|
|
9
11
|
* Tracks `session.opencodeSessionId` from streamed JSON events to support multi-turn continuity.
|
|
@@ -60,7 +62,9 @@ export function streamOpenCodeCliChat({ session, message, imagePath, systemPromp
|
|
|
60
62
|
const proc = spawn(binary, args, {
|
|
61
63
|
cwd,
|
|
62
64
|
env,
|
|
63
|
-
|
|
65
|
+
// stdin must be closed: OpenCode CLI can wait forever on a connected pipe
|
|
66
|
+
// even when the prompt is passed via argv.
|
|
67
|
+
stdio: OPENCODE_CLI_STDIO,
|
|
64
68
|
timeout: processTimeoutMs,
|
|
65
69
|
})
|
|
66
70
|
|
|
@@ -96,6 +96,23 @@ describe('mission-templates: registry', () => {
|
|
|
96
96
|
assert.equal(templates.getMissionTemplate('weekly-agent-quality-report')?.category, 'monitoring')
|
|
97
97
|
})
|
|
98
98
|
|
|
99
|
+
it('includes v1.6 love-path templates for review, research, and content', () => {
|
|
100
|
+
const expected = [
|
|
101
|
+
['codebase-review-sprint', 'productivity'],
|
|
102
|
+
['research-bureau-scan', 'research'],
|
|
103
|
+
['content-studio-cycle', 'communication'],
|
|
104
|
+
] as const
|
|
105
|
+
|
|
106
|
+
for (const [id, category] of expected) {
|
|
107
|
+
const template = templates.getMissionTemplate(id)
|
|
108
|
+
assert.ok(template, `expected ${id} template`)
|
|
109
|
+
assert.equal(template.category, category)
|
|
110
|
+
assert.ok(template.defaults.goal.length > 120, `${id} should have a concrete goal`)
|
|
111
|
+
assert.ok(template.defaults.successCriteria.length >= 3, `${id} should have acceptance criteria`)
|
|
112
|
+
assert.ok(template.defaults.budget.maxTurns, `${id} should have a turn budget`)
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
99
116
|
it('getMissionTemplate resolves known ids', () => {
|
|
100
117
|
const list = templates.listMissionTemplates()
|
|
101
118
|
const first = list[0]
|
|
@@ -303,6 +303,75 @@ export const BUILT_IN_MISSION_TEMPLATES: MissionTemplate[] = [
|
|
|
303
303
|
reportSchedule: report(DAY),
|
|
304
304
|
},
|
|
305
305
|
},
|
|
306
|
+
{
|
|
307
|
+
id: 'codebase-review-sprint',
|
|
308
|
+
name: 'Codebase Review Sprint',
|
|
309
|
+
description:
|
|
310
|
+
'Inspect a repository for user-facing bugs, fragile flows, missing tests, and release-readiness risks.',
|
|
311
|
+
icon: '🧪',
|
|
312
|
+
category: 'productivity',
|
|
313
|
+
tags: ['codebase', 'review', 'release', 'quality'],
|
|
314
|
+
setupNote:
|
|
315
|
+
'Set the repository path and risk areas in the goal. Keep code edits disabled unless the mission is explicitly converted into implementation work.',
|
|
316
|
+
defaults: {
|
|
317
|
+
title: 'Codebase Review Sprint',
|
|
318
|
+
goal:
|
|
319
|
+
'Review the current codebase for release-readiness. Inspect tests, build scripts, recent failure-prone flows, user-facing onboarding, desktop/package notes, and high-risk runtime paths. Produce a prioritized markdown report with bugs, missing tests, quick wins, and deferred risks. Do not edit files unless explicitly approved.',
|
|
320
|
+
successCriteria: [
|
|
321
|
+
'At least 5 concrete risks or no-finding checks are documented with file or workflow evidence',
|
|
322
|
+
'Recommended fixes are prioritized by user impact and implementation effort',
|
|
323
|
+
'The report separates release blockers from follow-up improvements',
|
|
324
|
+
],
|
|
325
|
+
budget: budget({ maxUsd: 2, maxTokens: 140_000, maxTurns: 140, maxWallclockSec: DAY }),
|
|
326
|
+
reportSchedule: report(6 * HOUR),
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
id: 'research-bureau-scan',
|
|
331
|
+
name: 'Research Bureau Scan',
|
|
332
|
+
description:
|
|
333
|
+
'Fan out a topic across multiple research angles, then synthesize evidence into a concise decision brief.',
|
|
334
|
+
icon: '🔎',
|
|
335
|
+
category: 'research',
|
|
336
|
+
tags: ['research', 'synthesis', 'competitive', 'decision'],
|
|
337
|
+
setupNote:
|
|
338
|
+
'Name the topic, sources, and decision the research should support before starting.',
|
|
339
|
+
defaults: {
|
|
340
|
+
title: 'Research Bureau Scan',
|
|
341
|
+
goal:
|
|
342
|
+
'Research the target topic from at least three angles: current market signals, technical feasibility, and user impact. Gather source-backed notes, compare conflicting evidence, and produce a concise decision brief with recommendation, confidence, and open questions.',
|
|
343
|
+
successCriteria: [
|
|
344
|
+
'At least 6 source-backed findings are captured',
|
|
345
|
+
'The final brief compares evidence across at least 3 research angles',
|
|
346
|
+
'Recommendation includes confidence level and open questions',
|
|
347
|
+
],
|
|
348
|
+
budget: budget({ maxUsd: 3, maxTokens: 180_000, maxTurns: 180, maxWallclockSec: 2 * DAY }),
|
|
349
|
+
reportSchedule: report(12 * HOUR),
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
id: 'content-studio-cycle',
|
|
354
|
+
name: 'Content Studio Cycle',
|
|
355
|
+
description:
|
|
356
|
+
'Turn a brief into draft, edit pass, publish checklist, and repurposed snippets for multiple channels.',
|
|
357
|
+
icon: '✍️',
|
|
358
|
+
category: 'communication',
|
|
359
|
+
tags: ['content', 'writing', 'editorial', 'launch'],
|
|
360
|
+
setupNote:
|
|
361
|
+
'Provide the audience, voice, channels, and any approval boundary. Public posting stays manual by default.',
|
|
362
|
+
defaults: {
|
|
363
|
+
title: 'Content Studio Cycle',
|
|
364
|
+
goal:
|
|
365
|
+
'Convert the supplied brief into a polished content package. Produce an outline, long-form draft, editor notes, publish checklist, and short repurposed snippets for the requested channels. Do not publish or post externally without approval.',
|
|
366
|
+
successCriteria: [
|
|
367
|
+
'Package includes outline, draft, editor notes, checklist, and channel snippets',
|
|
368
|
+
'Copy follows the requested audience and voice constraints',
|
|
369
|
+
'Any claims that need evidence are marked before publication',
|
|
370
|
+
],
|
|
371
|
+
budget: budget({ maxUsd: 2, maxTokens: 120_000, maxTurns: 120, maxWallclockSec: DAY }),
|
|
372
|
+
reportSchedule: report(6 * HOUR),
|
|
373
|
+
},
|
|
374
|
+
},
|
|
306
375
|
{
|
|
307
376
|
id: 'hello-world-demo',
|
|
308
377
|
name: 'Hello World Demo',
|
|
@@ -2,6 +2,31 @@ import assert from 'node:assert/strict'
|
|
|
2
2
|
import test from 'node:test'
|
|
3
3
|
import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
|
|
4
4
|
|
|
5
|
+
test('protocol-service exposes v1.6 built-in templates for release, research, and builder flows', () => {
|
|
6
|
+
const output = runWithTempDataDir<{
|
|
7
|
+
ids: string[]
|
|
8
|
+
releaseOutputs: string[]
|
|
9
|
+
builderKinds: string[]
|
|
10
|
+
}>(`
|
|
11
|
+
const protocolsMod = await import('./src/lib/server/protocols/protocol-templates')
|
|
12
|
+
const protocols = protocolsMod.default || protocolsMod
|
|
13
|
+
const templates = protocols.listAllTemplates()
|
|
14
|
+
const release = templates.find((template) => template.id === 'release_readiness_panel')
|
|
15
|
+
const builder = templates.find((template) => template.id === 'builder_review_loop')
|
|
16
|
+
console.log(JSON.stringify({
|
|
17
|
+
ids: templates.map((template) => template.id),
|
|
18
|
+
releaseOutputs: release?.recommendedOutputs || [],
|
|
19
|
+
builderKinds: builder?.defaultPhases?.map((phase) => phase.kind) || [],
|
|
20
|
+
}))
|
|
21
|
+
`, { prefix: 'swarmclaw-protocol-templates-' })
|
|
22
|
+
|
|
23
|
+
assert.ok(output.ids.includes('release_readiness_panel'))
|
|
24
|
+
assert.ok(output.ids.includes('research_bureau_synthesis'))
|
|
25
|
+
assert.ok(output.ids.includes('builder_review_loop'))
|
|
26
|
+
assert.ok(output.releaseOutputs.includes('go/no-go decision'))
|
|
27
|
+
assert.deepEqual(output.builderKinds, ['present', 'collect_independent_inputs', 'compare', 'emit_tasks', 'summarize'])
|
|
28
|
+
})
|
|
29
|
+
|
|
5
30
|
test('protocol-service creates a hidden transcript run and completes a structured session', () => {
|
|
6
31
|
const output = runWithTempDataDir<{
|
|
7
32
|
status: string | null
|
|
@@ -77,6 +77,54 @@ export const BUILT_IN_PROTOCOL_TEMPLATES: ProtocolTemplate[] = [
|
|
|
77
77
|
{ id: 'summarize', kind: 'summarize', label: 'Summarize the outcome' },
|
|
78
78
|
],
|
|
79
79
|
},
|
|
80
|
+
{
|
|
81
|
+
id: 'release_readiness_panel',
|
|
82
|
+
name: 'Release Readiness Panel',
|
|
83
|
+
description: 'Review a candidate release, compare risks, and produce a go/no-go summary.',
|
|
84
|
+
builtIn: true,
|
|
85
|
+
singleAgentAllowed: true,
|
|
86
|
+
tags: ['release', 'quality', 'review'],
|
|
87
|
+
recommendedOutputs: ['blockers', 'risk summary', 'go/no-go decision'],
|
|
88
|
+
defaultPhases: [
|
|
89
|
+
{ id: 'present', kind: 'present', label: 'Set the release context' },
|
|
90
|
+
{ id: 'collect', kind: 'collect_independent_inputs', label: 'Collect release risks' },
|
|
91
|
+
{ id: 'compare', kind: 'compare', label: 'Compare blockers and evidence' },
|
|
92
|
+
{ id: 'decide', kind: 'decide', label: 'Make a go/no-go recommendation' },
|
|
93
|
+
{ id: 'summarize', kind: 'summarize', label: 'Summarize release readiness' },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'research_bureau_synthesis',
|
|
98
|
+
name: 'Research Bureau Synthesis',
|
|
99
|
+
description: 'Collect multiple research angles and turn them into one source-backed brief.',
|
|
100
|
+
builtIn: true,
|
|
101
|
+
singleAgentAllowed: true,
|
|
102
|
+
tags: ['research', 'synthesis', 'decision'],
|
|
103
|
+
recommendedOutputs: ['findings', 'recommendation', 'open questions'],
|
|
104
|
+
defaultPhases: [
|
|
105
|
+
{ id: 'present', kind: 'present', label: 'Frame the research question' },
|
|
106
|
+
{ id: 'collect', kind: 'collect_independent_inputs', label: 'Collect research angles' },
|
|
107
|
+
{ id: 'compare', kind: 'compare', label: 'Compare source-backed findings' },
|
|
108
|
+
{ id: 'decide', kind: 'decide', label: 'Recommend the current direction' },
|
|
109
|
+
{ id: 'summarize', kind: 'summarize', label: 'Write the decision brief' },
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 'builder_review_loop',
|
|
114
|
+
name: 'Builder Review Loop',
|
|
115
|
+
description: 'Move an implementation plan through builder, reviewer, decision, and follow-up summary.',
|
|
116
|
+
builtIn: true,
|
|
117
|
+
singleAgentAllowed: true,
|
|
118
|
+
tags: ['builder', 'review', 'implementation'],
|
|
119
|
+
recommendedOutputs: ['implementation notes', 'review risks', 'follow-up tasks'],
|
|
120
|
+
defaultPhases: [
|
|
121
|
+
{ id: 'present', kind: 'present', label: 'Present the work item' },
|
|
122
|
+
{ id: 'collect', kind: 'collect_independent_inputs', label: 'Collect builder and reviewer notes' },
|
|
123
|
+
{ id: 'compare', kind: 'compare', label: 'Compare implementation and review signals' },
|
|
124
|
+
{ id: 'emit_tasks', kind: 'emit_tasks', label: 'Emit follow-up tasks' },
|
|
125
|
+
{ id: 'summarize', kind: 'summarize', label: 'Summarize the delivery path' },
|
|
126
|
+
],
|
|
127
|
+
},
|
|
80
128
|
{
|
|
81
129
|
id: 'decision_round',
|
|
82
130
|
name: 'Decision Round',
|
|
@@ -35,6 +35,29 @@ describe('stripInternalJson', () => {
|
|
|
35
35
|
assert.equal(stripInternalJson(input).trim(), '')
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
+
it('removes multi-line working-state JSON with nested strings and arrays', () => {
|
|
39
|
+
const input = [
|
|
40
|
+
'Answer first.',
|
|
41
|
+
'{',
|
|
42
|
+
' "factsUpsert": [{ "title": "Nested", "value": "brace } inside a string" }],',
|
|
43
|
+
' "questionsUpsert": []',
|
|
44
|
+
'}',
|
|
45
|
+
'Answer second.',
|
|
46
|
+
].join('\n')
|
|
47
|
+
const result = stripInternalJson(input)
|
|
48
|
+
assert.equal(result, 'Answer first. Answer second.')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('preserves objects with internal-looking keys when schema validation fails', () => {
|
|
52
|
+
const input = 'The score object is { "quality_score": "high", "quality_reasoning": 42 }'
|
|
53
|
+
assert.equal(stripInternalJson(input), input)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('preserves taskIntent-only user JSON without classifier fields', () => {
|
|
57
|
+
const input = 'Example payload: { "taskIntent": "book a flight" }'
|
|
58
|
+
assert.equal(stripInternalJson(input), input)
|
|
59
|
+
})
|
|
60
|
+
|
|
38
61
|
it('handles multiple JSON blocks, only removing internal ones', () => {
|
|
39
62
|
const input = '{ "isDeliverableTask": true } some text { "foo": "bar" }'
|
|
40
63
|
const result = stripInternalJson(input)
|