opencastle 0.23.1 → 0.24.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/dist/cli/convoy/engine.d.ts +1 -0
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +72 -22
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +205 -0
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/dashboard.d.ts.map +1 -1
- package/dist/cli/dashboard.js +5 -4
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/run/adapters/claude.d.ts +6 -0
- package/dist/cli/run/adapters/claude.d.ts.map +1 -0
- package/dist/cli/run/adapters/claude.js +211 -0
- package/dist/cli/run/adapters/claude.js.map +1 -0
- package/dist/cli/run/adapters/copilot.d.ts +0 -18
- package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
- package/dist/cli/run/adapters/copilot.js +123 -38
- package/dist/cli/run/adapters/copilot.js.map +1 -1
- package/dist/cli/run/adapters/index.js +2 -2
- package/dist/cli/run/adapters/index.js.map +1 -1
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +8 -0
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run/schema.test.js +41 -0
- package/dist/cli/run/schema.test.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +21 -9
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/types.d.ts +2 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/package.json +9 -1
- package/src/cli/convoy/engine.test.ts +240 -0
- package/src/cli/convoy/engine.ts +80 -23
- package/src/cli/dashboard.ts +6 -5
- package/src/cli/run/adapters/claude.ts +238 -0
- package/src/cli/run/adapters/copilot.ts +125 -47
- package/src/cli/run/adapters/index.ts +2 -2
- package/src/cli/run/adapters/vendor.d.ts +2 -0
- package/src/cli/run/schema.test.ts +51 -0
- package/src/cli/run/schema.ts +10 -0
- package/src/cli/run.ts +23 -11
- package/src/cli/types.ts +2 -0
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/orchestrator/agents/team-lead.agent.md +6 -6
- package/src/orchestrator/prompts/bug-fix.prompt.md +6 -2
- package/src/orchestrator/prompts/generate-convoy.prompt.md +3 -3
- package/src/orchestrator/prompts/implement-feature.prompt.md +8 -19
- package/dist/cli/run/adapters/claude-code.d.ts +0 -16
- package/dist/cli/run/adapters/claude-code.d.ts.map +0 -1
- package/dist/cli/run/adapters/claude-code.js +0 -95
- package/dist/cli/run/adapters/claude-code.js.map +0 -1
- package/src/cli/run/adapters/claude-code.ts +0 -107
|
@@ -1,29 +1,27 @@
|
|
|
1
|
+
|
|
1
2
|
import { spawn } from 'node:child_process'
|
|
2
3
|
import type { CopilotClient as CopilotClientType, CopilotSession, PermissionHandler } from '@github/copilot-sdk'
|
|
3
4
|
import { parseTimeout } from '../schema.js'
|
|
4
|
-
import type { Task, ExecuteOptions, ExecuteResult } from '../../types.js'
|
|
5
|
+
import type { Task, ExecuteOptions, ExecuteResult, TokenUsage } from '../../types.js'
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
// Adapter name
|
|
7
8
|
export const name = 'copilot'
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* The client manages a single Copilot CLI server process; all task sessions
|
|
12
|
-
* multiplex over it via JSON-RPC.
|
|
13
|
-
*/
|
|
14
|
-
let clientPromise: Promise<CopilotClientType> | null = null
|
|
15
|
-
|
|
16
|
-
/** Cached permission handler from the SDK module. */
|
|
17
|
-
let cachedApproveAll: PermissionHandler | null = null
|
|
10
|
+
// --- Unified adapter: SDK first, fallback to CLI ---
|
|
11
|
+
let mode: 'sdk' | 'cli' | null = null
|
|
18
12
|
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
// SDK check
|
|
14
|
+
async function sdkAvailable(): Promise<boolean> {
|
|
15
|
+
try {
|
|
16
|
+
await import('@github/copilot-sdk')
|
|
17
|
+
return true
|
|
18
|
+
} catch {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
}
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* The SDK communicates with the CLI in server mode, so it must be installed.
|
|
25
|
-
*/
|
|
26
|
-
export async function isAvailable(): Promise<boolean> {
|
|
23
|
+
// CLI check
|
|
24
|
+
async function cliAvailable(): Promise<boolean> {
|
|
27
25
|
return new Promise((resolve) => {
|
|
28
26
|
const proc = spawn('which', ['copilot'], { stdio: 'pipe' })
|
|
29
27
|
proc.on('close', (code) => resolve(code === 0))
|
|
@@ -31,10 +29,23 @@ export async function isAvailable(): Promise<boolean> {
|
|
|
31
29
|
})
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
export async function isAvailable(): Promise<boolean> {
|
|
33
|
+
if (await sdkAvailable()) {
|
|
34
|
+
mode = 'sdk'
|
|
35
|
+
return true
|
|
36
|
+
}
|
|
37
|
+
if (await cliAvailable()) {
|
|
38
|
+
mode = 'cli'
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- SDK implementation (existing logic) ---
|
|
45
|
+
let clientPromise: Promise<CopilotClientType> | null = null
|
|
46
|
+
let cachedApproveAll: PermissionHandler | null = null
|
|
47
|
+
const activeSessions = new Map<string, CopilotSession>()
|
|
48
|
+
|
|
38
49
|
async function getClient(): Promise<CopilotClientType> {
|
|
39
50
|
if (!clientPromise) {
|
|
40
51
|
clientPromise = (async () => {
|
|
@@ -51,29 +62,16 @@ async function getClient(): Promise<CopilotClientType> {
|
|
|
51
62
|
return clientPromise
|
|
52
63
|
}
|
|
53
64
|
|
|
54
|
-
|
|
55
|
-
* Execute a task using the Copilot SDK.
|
|
56
|
-
*
|
|
57
|
-
* Each task gets its own session with:
|
|
58
|
-
* - All tool permissions auto-approved (equivalent to `--allow-all-tools`)
|
|
59
|
-
* - No `ask_user` tool (autonomous — equivalent to `--no-ask-user`)
|
|
60
|
-
* - System message injected with the agent role
|
|
61
|
-
* - Streaming enabled in verbose mode for live output
|
|
62
|
-
*/
|
|
63
|
-
export async function execute(task: Task, options: ExecuteOptions = {}): Promise<ExecuteResult> {
|
|
65
|
+
async function executeViaSdk(task: Task, options: ExecuteOptions = {}): Promise<ExecuteResult> {
|
|
64
66
|
// NOTE: The Copilot SDK CopilotClient is a shared singleton. Per-task cwd
|
|
65
67
|
// isolation requires SDK support for per-session workingDirectory, which is
|
|
66
68
|
// not yet available. When running in convoy mode with worktrees, prefer
|
|
67
|
-
// subprocess-based adapters (
|
|
68
|
-
// natively. Copilot SDK per-session cwd support is tracked for Phase 3.
|
|
69
|
+
// subprocess-based adapters (cli mode) that support options.cwd natively.
|
|
69
70
|
let prompt = `You are a ${task.agent}. ${task.prompt}`
|
|
70
|
-
|
|
71
71
|
if (task.files && task.files.length > 0) {
|
|
72
72
|
prompt += `\n\nOnly modify files under: ${task.files.join(', ')}`
|
|
73
73
|
}
|
|
74
|
-
|
|
75
74
|
const client = await getClient()
|
|
76
|
-
|
|
77
75
|
const session = await client.createSession({
|
|
78
76
|
onPermissionRequest: cachedApproveAll!,
|
|
79
77
|
systemMessage: {
|
|
@@ -86,16 +84,12 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
|
|
|
86
84
|
infiniteSessions: { enabled: false },
|
|
87
85
|
...(options.verbose ? { streaming: true } : {}),
|
|
88
86
|
})
|
|
89
|
-
|
|
90
87
|
activeSessions.set(task.id, session)
|
|
91
|
-
|
|
92
|
-
// Stream deltas to stdout in verbose mode
|
|
93
88
|
if (options.verbose) {
|
|
94
89
|
session.on('assistant.message_delta', (event: { data: { deltaContent: string } }) => {
|
|
95
90
|
process.stdout.write(event.data.deltaContent)
|
|
96
91
|
})
|
|
97
92
|
}
|
|
98
|
-
|
|
99
93
|
try {
|
|
100
94
|
const timeoutMs = parseTimeout(task.timeout)
|
|
101
95
|
const response = await session.sendAndWait({ prompt }, timeoutMs)
|
|
@@ -107,7 +101,6 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
|
|
|
107
101
|
completion_tokens: u.completion_tokens ?? u.completionTokens,
|
|
108
102
|
total_tokens: u.total_tokens ?? u.totalTokens,
|
|
109
103
|
} : undefined
|
|
110
|
-
|
|
111
104
|
return {
|
|
112
105
|
success: true,
|
|
113
106
|
output: output.slice(0, 10_000),
|
|
@@ -126,11 +119,7 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
|
|
|
126
119
|
}
|
|
127
120
|
}
|
|
128
121
|
|
|
129
|
-
|
|
130
|
-
* Abort and destroy the session associated with a task.
|
|
131
|
-
* Called by the executor when a task exceeds its timeout.
|
|
132
|
-
*/
|
|
133
|
-
export function kill(task: Task): void {
|
|
122
|
+
function killSdk(task: Task): void {
|
|
134
123
|
const session = activeSessions.get(task.id)
|
|
135
124
|
if (session) {
|
|
136
125
|
session.abort().catch(() => {})
|
|
@@ -138,3 +127,92 @@ export function kill(task: Task): void {
|
|
|
138
127
|
activeSessions.delete(task.id)
|
|
139
128
|
}
|
|
140
129
|
}
|
|
130
|
+
|
|
131
|
+
// --- CLI implementation ---
|
|
132
|
+
async function executeViaCli(task: Task, options: ExecuteOptions = {}): Promise<ExecuteResult> {
|
|
133
|
+
// CLI supports --output-format json, --max-turns, and respects cwd
|
|
134
|
+
let prompt = `You are a ${task.agent}. ${task.prompt}`
|
|
135
|
+
if (task.files && task.files.length > 0) {
|
|
136
|
+
prompt += `\n\nOnly modify files under: ${task.files.join(', ')}`
|
|
137
|
+
}
|
|
138
|
+
const args = [
|
|
139
|
+
'-p',
|
|
140
|
+
prompt,
|
|
141
|
+
'--output-format',
|
|
142
|
+
'json',
|
|
143
|
+
'--max-turns',
|
|
144
|
+
'50',
|
|
145
|
+
]
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
const proc = spawn('copilot', args, {
|
|
148
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
149
|
+
env: { ...process.env },
|
|
150
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
151
|
+
})
|
|
152
|
+
let stdout = ''
|
|
153
|
+
let stderr = ''
|
|
154
|
+
proc.stdout.on('data', (chunk: Buffer) => {
|
|
155
|
+
stdout += chunk.toString()
|
|
156
|
+
if (options.verbose) {
|
|
157
|
+
process.stdout.write(chunk)
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
proc.stderr.on('data', (chunk: Buffer) => {
|
|
161
|
+
stderr += chunk.toString()
|
|
162
|
+
if (options.verbose) {
|
|
163
|
+
process.stderr.write(chunk)
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
proc.on('close', (code) => {
|
|
167
|
+
const output = [stdout, stderr].filter(Boolean).join('\n')
|
|
168
|
+
let usage: TokenUsage | undefined
|
|
169
|
+
try {
|
|
170
|
+
const parsedJson = JSON.parse(stdout) as Record<string, unknown>
|
|
171
|
+
const u = parsedJson?.usage as Record<string, number> | undefined
|
|
172
|
+
if (u) {
|
|
173
|
+
const promptTokens = (u.input_tokens ?? u.prompt_tokens) as number | undefined
|
|
174
|
+
const completionTokens = (u.output_tokens ?? u.completion_tokens) as number | undefined
|
|
175
|
+
const total = ((promptTokens ?? 0) + (completionTokens ?? 0)) || undefined
|
|
176
|
+
usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: total }
|
|
177
|
+
}
|
|
178
|
+
} catch { /* not JSON or no usage — graceful degradation */ }
|
|
179
|
+
resolve({
|
|
180
|
+
success: code === 0,
|
|
181
|
+
output: output.slice(0, 10000),
|
|
182
|
+
exitCode: code ?? -1,
|
|
183
|
+
usage,
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
proc.on('error', (err) => {
|
|
187
|
+
resolve({
|
|
188
|
+
success: false,
|
|
189
|
+
output: `Failed to spawn copilot: ${err.message}`,
|
|
190
|
+
exitCode: -1,
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
task._process = proc
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function killCli(task: Task): void {
|
|
198
|
+
if (task._process && !task._process.killed) {
|
|
199
|
+
task._process.kill('SIGTERM')
|
|
200
|
+
setTimeout(() => {
|
|
201
|
+
if (task._process && !task._process.killed) {
|
|
202
|
+
task._process.kill('SIGKILL')
|
|
203
|
+
}
|
|
204
|
+
}, 5000)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// --- Unified interface ---
|
|
209
|
+
export async function execute(task: Task, options: ExecuteOptions = {}): Promise<ExecuteResult> {
|
|
210
|
+
if (!mode) await isAvailable()
|
|
211
|
+
if (mode === 'sdk') return executeViaSdk(task, options)
|
|
212
|
+
return executeViaCli(task, options)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function kill(task: Task): void {
|
|
216
|
+
if (mode === 'sdk') killSdk(task)
|
|
217
|
+
else killCli(task)
|
|
218
|
+
}
|
|
@@ -4,7 +4,7 @@ import type { AgentAdapter } from '../../types.js'
|
|
|
4
4
|
* Adapter registry for agent runtimes.
|
|
5
5
|
*/
|
|
6
6
|
const ADAPTERS: Record<string, () => Promise<AgentAdapter>> = {
|
|
7
|
-
|
|
7
|
+
claude: () => import('./claude.js') as Promise<AgentAdapter>,
|
|
8
8
|
copilot: () => import('./copilot.js') as Promise<AgentAdapter>,
|
|
9
9
|
cursor: () => import('./cursor.js') as Promise<AgentAdapter>,
|
|
10
10
|
opencode: () => import('./opencode.js') as Promise<AgentAdapter>,
|
|
@@ -29,7 +29,7 @@ export async function getAdapter(name: string): Promise<AgentAdapter> {
|
|
|
29
29
|
* Detection priority order — checked first-to-last.
|
|
30
30
|
* The first available adapter wins.
|
|
31
31
|
*/
|
|
32
|
-
const DETECTION_ORDER = ['copilot', 'claude
|
|
32
|
+
const DETECTION_ORDER = ['copilot', 'claude', 'cursor', 'opencode'] as const
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Auto-detect which adapter CLI is available on the system.
|
|
@@ -1160,3 +1160,54 @@ describe('applyDefaults — pipeline spec (version:2, no tasks)', () => {
|
|
|
1160
1160
|
expect(spec.depends_on_convoy).toEqual(['phase-1', 'phase-2'])
|
|
1161
1161
|
})
|
|
1162
1162
|
})
|
|
1163
|
+
|
|
1164
|
+
// ── validateSpec — gate_retries field ─────────────────────────
|
|
1165
|
+
|
|
1166
|
+
describe('validateSpec — gate_retries field', () => {
|
|
1167
|
+
const validSpec = {
|
|
1168
|
+
name: 'test',
|
|
1169
|
+
tasks: [{ id: 'a', prompt: 'do something' }],
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
it('accepts gate_retries as 0', () => {
|
|
1173
|
+
const result = validateSpec({ ...validSpec, gate_retries: 0 })
|
|
1174
|
+
expect(result.valid).toBe(true)
|
|
1175
|
+
})
|
|
1176
|
+
|
|
1177
|
+
it('accepts gate_retries as a positive integer', () => {
|
|
1178
|
+
const result = validateSpec({ ...validSpec, gate_retries: 3 })
|
|
1179
|
+
expect(result.valid).toBe(true)
|
|
1180
|
+
})
|
|
1181
|
+
|
|
1182
|
+
it('rejects gate_retries as negative', () => {
|
|
1183
|
+
const result = validateSpec({ ...validSpec, gate_retries: -1 })
|
|
1184
|
+
expect(result.valid).toBe(false)
|
|
1185
|
+
expect(result.errors).toContainEqual(expect.stringContaining('gate_retries'))
|
|
1186
|
+
})
|
|
1187
|
+
|
|
1188
|
+
it('rejects gate_retries as a float', () => {
|
|
1189
|
+
const result = validateSpec({ ...validSpec, gate_retries: 1.5 })
|
|
1190
|
+
expect(result.valid).toBe(false)
|
|
1191
|
+
expect(result.errors).toContainEqual(expect.stringContaining('gate_retries'))
|
|
1192
|
+
})
|
|
1193
|
+
|
|
1194
|
+
it('rejects gate_retries as a string', () => {
|
|
1195
|
+
const result = validateSpec({ ...validSpec, gate_retries: 'two' })
|
|
1196
|
+
expect(result.valid).toBe(false)
|
|
1197
|
+
expect(result.errors).toContainEqual(expect.stringContaining('gate_retries'))
|
|
1198
|
+
})
|
|
1199
|
+
})
|
|
1200
|
+
|
|
1201
|
+
// ── applyDefaults — gate_retries default ───────────────────────
|
|
1202
|
+
|
|
1203
|
+
describe('applyDefaults — gate_retries default', () => {
|
|
1204
|
+
it('defaults gate_retries to 0', () => {
|
|
1205
|
+
const spec = applyDefaults({ name: 'test', tasks: [{ id: 'a', prompt: 'p' }] })
|
|
1206
|
+
expect(spec.gate_retries).toBe(0)
|
|
1207
|
+
})
|
|
1208
|
+
|
|
1209
|
+
it('preserves explicit gate_retries value', () => {
|
|
1210
|
+
const spec = applyDefaults({ name: 'test', tasks: [{ id: 'a', prompt: 'p' }], gate_retries: 2 })
|
|
1211
|
+
expect(spec.gate_retries).toBe(2)
|
|
1212
|
+
})
|
|
1213
|
+
})
|
package/src/cli/run/schema.ts
CHANGED
|
@@ -42,6 +42,7 @@ interface RawSpec {
|
|
|
42
42
|
version?: unknown
|
|
43
43
|
defaults?: unknown
|
|
44
44
|
gates?: unknown
|
|
45
|
+
gate_retries?: unknown
|
|
45
46
|
branch?: unknown
|
|
46
47
|
depends_on_convoy?: unknown
|
|
47
48
|
}
|
|
@@ -154,6 +155,14 @@ export function validateSpec(spec: unknown): ValidationResult {
|
|
|
154
155
|
}
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
// gate_retries
|
|
159
|
+
if (s.gate_retries !== undefined) {
|
|
160
|
+
const gr = Number(s.gate_retries)
|
|
161
|
+
if (!Number.isInteger(gr) || gr < 0) {
|
|
162
|
+
errors.push('`gate_retries` must be a non-negative integer')
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
157
166
|
// branch
|
|
158
167
|
if (s.branch !== undefined && typeof s.branch !== 'string') {
|
|
159
168
|
errors.push('`branch` must be a string')
|
|
@@ -319,6 +328,7 @@ export function applyDefaults(spec: Record<string, unknown>): TaskSpec {
|
|
|
319
328
|
s.on_failure = (s.on_failure as string) || 'continue'
|
|
320
329
|
// Leave adapter empty so run.ts can auto-detect the best available CLI
|
|
321
330
|
s.adapter = (s.adapter as string) || ''
|
|
331
|
+
s.gate_retries = s.gate_retries !== undefined ? Number(s.gate_retries) : 0
|
|
322
332
|
|
|
323
333
|
const tasks = (s.tasks as Array<Record<string, unknown>> | undefined) ?? []
|
|
324
334
|
const d =
|
package/src/cli/run.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { parseTaskSpecText, isConvoySpec, isPipelineSpec } from './run/schema.js
|
|
|
5
5
|
import { createExecutor, buildPhases } from './run/executor.js'
|
|
6
6
|
import { getAdapter, detectAdapter } from './run/adapters/index.js'
|
|
7
7
|
import { createReporter, printExecutionPlan } from './run/reporter.js'
|
|
8
|
+
import { c } from './prompt.js'
|
|
8
9
|
import type { CliContext, RunOptions } from './types.js'
|
|
9
10
|
import type { ConvoyResult } from './convoy/engine.js'
|
|
10
11
|
import type { PipelineResult } from './convoy/pipeline.js'
|
|
@@ -22,7 +23,7 @@ const HELP = `
|
|
|
22
23
|
Version 1 specs use the Convoy Engine; legacy specs use the standard executor.
|
|
23
24
|
|
|
24
25
|
Options:
|
|
25
|
-
--file, -f <path> Task spec file
|
|
26
|
+
--file, -f <path> Task spec file
|
|
26
27
|
--dry-run Show execution plan without running
|
|
27
28
|
--concurrency, -c <n> Override max parallel tasks
|
|
28
29
|
--adapter, -a <name> Override agent runtime adapter
|
|
@@ -38,7 +39,7 @@ const HELP = `
|
|
|
38
39
|
*/
|
|
39
40
|
function parseArgs(args: string[]): RunOptions {
|
|
40
41
|
const opts: RunOptions = {
|
|
41
|
-
file: '
|
|
42
|
+
file: 'convoy.yml',
|
|
42
43
|
dryRun: false,
|
|
43
44
|
concurrency: null,
|
|
44
45
|
adapter: null,
|
|
@@ -121,7 +122,7 @@ function printAdapterError(detectionFailed: boolean, adapterName: string): void
|
|
|
121
122
|
)
|
|
122
123
|
} else {
|
|
123
124
|
const hints: Record<string, string> = {
|
|
124
|
-
'claude
|
|
125
|
+
'claude':
|
|
125
126
|
' Install: npm install -g @anthropic-ai/claude-code\n' +
|
|
126
127
|
' Docs: https://docs.anthropic.com/en/docs/claude-code',
|
|
127
128
|
copilot:
|
|
@@ -136,7 +137,7 @@ function printAdapterError(detectionFailed: boolean, adapterName: string): void
|
|
|
136
137
|
' Install OpenCode from https://opencode.ai\n' +
|
|
137
138
|
' Ensure the "opencode" command is on your PATH.',
|
|
138
139
|
}
|
|
139
|
-
const cliName = adapterName === '
|
|
140
|
+
const cliName = adapterName === 'cursor' ? 'agent' : adapterName
|
|
140
141
|
const hint = hints[adapterName] ?? ''
|
|
141
142
|
console.error(
|
|
142
143
|
` ✗ Adapter "${adapterName}" is not available.\n` +
|
|
@@ -153,10 +154,15 @@ function printConvoyResult(result: ConvoyResult): void {
|
|
|
153
154
|
console.log(`\n ──────────────────────────────────────`)
|
|
154
155
|
console.log(` Convoy ${result.status}: ${result.duration}`)
|
|
155
156
|
console.log(
|
|
156
|
-
`
|
|
157
|
+
` Tasks: ${result.summary.done}/${result.summary.total} done` +
|
|
158
|
+
(result.summary.failed > 0 ? ` | ${result.summary.failed} failed` : '') +
|
|
159
|
+
(result.summary.skipped > 0 ? ` | ${result.summary.skipped} skipped` : '') +
|
|
160
|
+
(result.summary.timedOut > 0 ? ` | ${result.summary.timedOut} timed out` : '')
|
|
157
161
|
)
|
|
158
162
|
if (result.gateResults) {
|
|
159
|
-
|
|
163
|
+
const gatesPassed = result.gateResults.filter(g => g.passed).length
|
|
164
|
+
const gatesFailed = result.gateResults.filter(g => !g.passed).length
|
|
165
|
+
console.log(` Gates: ${gatesPassed}/${result.gateResults.length} passed${gatesFailed > 0 ? ` | ${gatesFailed} failed` : ''}`)
|
|
160
166
|
for (const g of result.gateResults) {
|
|
161
167
|
console.log(` ${g.passed ? '✓' : '✗'} ${g.command}`)
|
|
162
168
|
}
|
|
@@ -307,7 +313,7 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
307
313
|
console.log(` ℹ Auto-detected adapter: ${detected}`)
|
|
308
314
|
} else {
|
|
309
315
|
resumePipelineDetectionFailed = true
|
|
310
|
-
resumePipelineSpec.adapter = 'claude
|
|
316
|
+
resumePipelineSpec.adapter = 'claude'
|
|
311
317
|
}
|
|
312
318
|
}
|
|
313
319
|
|
|
@@ -359,7 +365,7 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
359
365
|
console.log(` ℹ Auto-detected adapter: ${detected}`)
|
|
360
366
|
} else {
|
|
361
367
|
resumeDetectionFailed = true
|
|
362
|
-
resumeSpec.adapter = 'claude
|
|
368
|
+
resumeSpec.adapter = 'claude'
|
|
363
369
|
}
|
|
364
370
|
}
|
|
365
371
|
|
|
@@ -421,7 +427,7 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
421
427
|
console.log(` ℹ Auto-detected adapter: ${detected}`)
|
|
422
428
|
} else {
|
|
423
429
|
detectionFailed = true
|
|
424
|
-
spec.adapter = 'claude
|
|
430
|
+
spec.adapter = 'claude' // fallback for availability check below
|
|
425
431
|
}
|
|
426
432
|
}
|
|
427
433
|
|
|
@@ -466,7 +472,7 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
466
472
|
if (spec.gates?.length) console.log(` Gates: ${spec.gates.length} validation commands`)
|
|
467
473
|
|
|
468
474
|
const { startDashboardServer } = await import('./dashboard.js')
|
|
469
|
-
let pipelineDashboardResult: { server: import('node:http').Server } | null = null
|
|
475
|
+
let pipelineDashboardResult: { server: import('node:http').Server; port: number; url: string } | null = null
|
|
470
476
|
try {
|
|
471
477
|
pipelineDashboardResult = await startDashboardServer({
|
|
472
478
|
pkgRoot,
|
|
@@ -476,6 +482,9 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
476
482
|
} catch {
|
|
477
483
|
// Dashboard failure must not block pipeline
|
|
478
484
|
}
|
|
485
|
+
if (pipelineDashboardResult) {
|
|
486
|
+
console.log(` ${c.dim('Dashboard:')} ${pipelineDashboardResult.url}`)
|
|
487
|
+
}
|
|
479
488
|
|
|
480
489
|
const pipelineOrchestrator = createPipelineOrchestrator({
|
|
481
490
|
spec,
|
|
@@ -503,7 +512,7 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
503
512
|
if (spec.gates?.length) console.log(` Gates: ${spec.gates.length} validation commands`)
|
|
504
513
|
|
|
505
514
|
const { startDashboardServer } = await import('./dashboard.js')
|
|
506
|
-
let dashboardResult: { server: import('node:http').Server } | null = null
|
|
515
|
+
let dashboardResult: { server: import('node:http').Server; port: number; url: string } | null = null
|
|
507
516
|
try {
|
|
508
517
|
dashboardResult = await startDashboardServer({
|
|
509
518
|
pkgRoot,
|
|
@@ -513,6 +522,9 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
513
522
|
} catch {
|
|
514
523
|
// Dashboard failure must not block convoy
|
|
515
524
|
}
|
|
525
|
+
if (dashboardResult) {
|
|
526
|
+
console.log(` ${c.dim('Dashboard:')} ${dashboardResult.url}`)
|
|
527
|
+
}
|
|
516
528
|
|
|
517
529
|
const engine = createConvoyEngine({
|
|
518
530
|
spec,
|
package/src/cli/types.ts
CHANGED
|
@@ -164,6 +164,8 @@ export interface TaskSpec {
|
|
|
164
164
|
defaults?: TaskDefaults;
|
|
165
165
|
/** Shell commands run after all tasks complete; each must exit 0. */
|
|
166
166
|
gates?: string[];
|
|
167
|
+
/** How many times to retry failing gates with an auto-fix task (default: 0). */
|
|
168
|
+
gate_retries?: number;
|
|
167
169
|
/** Git feature branch name. */
|
|
168
170
|
branch?: string;
|
|
169
171
|
/** Other convoy spec names to run before this one (version: 2 pipeline specs). */
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
|
-
"hash": "
|
|
2
|
+
"hash": "8d888497",
|
|
3
3
|
"configHash": "30f8ea04",
|
|
4
|
-
"lockfileHash": "
|
|
5
|
-
"browserHash": "
|
|
4
|
+
"lockfileHash": "433479a7",
|
|
5
|
+
"browserHash": "261fa44b",
|
|
6
6
|
"optimized": {
|
|
7
7
|
"astro > cssesc": {
|
|
8
8
|
"src": "../../../../../node_modules/cssesc/cssesc.js",
|
|
9
9
|
"file": "astro___cssesc.js",
|
|
10
|
-
"fileHash": "
|
|
10
|
+
"fileHash": "de4544e1",
|
|
11
11
|
"needsInterop": true
|
|
12
12
|
},
|
|
13
13
|
"astro > aria-query": {
|
|
14
14
|
"src": "../../../../../node_modules/aria-query/lib/index.js",
|
|
15
15
|
"file": "astro___aria-query.js",
|
|
16
|
-
"fileHash": "
|
|
16
|
+
"fileHash": "e15a50a2",
|
|
17
17
|
"needsInterop": true
|
|
18
18
|
},
|
|
19
19
|
"astro > axobject-query": {
|
|
20
20
|
"src": "../../../../../node_modules/axobject-query/lib/index.js",
|
|
21
21
|
"file": "astro___axobject-query.js",
|
|
22
|
-
"fileHash": "
|
|
22
|
+
"fileHash": "e06f0936",
|
|
23
23
|
"needsInterop": true
|
|
24
24
|
}
|
|
25
25
|
},
|
|
@@ -145,14 +145,14 @@ Before EVERY delegation verify: (1) Tracker issue exists, (2) File partition is
|
|
|
145
145
|
|
|
146
146
|
## Convoy Integration
|
|
147
147
|
|
|
148
|
-
The convoy engine is the
|
|
148
|
+
The convoy engine is the **mandatory** execution mechanism for all project-related work — features, bug fixes, and refactors. This ensures consistent observability, crash recovery, and progress visibility.
|
|
149
149
|
|
|
150
150
|
### When to use convoy vs. direct delegation
|
|
151
151
|
|
|
152
|
-
|
|
|
153
|
-
|
|
154
|
-
|
|
|
155
|
-
|
|
|
152
|
+
| Work type | Approach |
|
|
153
|
+
|-----------|----------|
|
|
154
|
+
| Features, bug fixes, refactors (any subtask count) | **Convoy execution** — always generate a `.convoy.yml` spec, even for 1-task fixes |
|
|
155
|
+
| Utility prompts (`bootstrap-customizations`, `create-skill`, `generate-convoy`, `brainstorm`, `quick-refinement`) | **Direct** — these are meta/tooling operations, not project code changes |
|
|
156
156
|
|
|
157
157
|
### How to generate a convoy spec
|
|
158
158
|
|
|
@@ -164,7 +164,7 @@ The convoy engine is the preferred execution mechanism for multi-task work. Use
|
|
|
164
164
|
|
|
165
165
|
Tell the user to run:
|
|
166
166
|
```
|
|
167
|
-
npx opencastle run -f
|
|
167
|
+
npx opencastle run -f .opencastle/convoys/<name>.convoy.yml
|
|
168
168
|
```
|
|
169
169
|
This gives the user control over when execution starts (preferred — supports overnight/unattended runs and manual review of the spec before execution).
|
|
170
170
|
|
|
@@ -83,9 +83,13 @@ Find WHY the bug happens, not just WHERE:
|
|
|
83
83
|
|
|
84
84
|
### 4. Implement the Fix
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
All bug fixes are executed via the convoy engine — even single-task fixes — to ensure observability and crash recovery.
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
1. **Generate a convoy spec** — use the `generate-convoy` prompt with the root cause analysis, fix approach, and file paths as context.
|
|
89
|
+
2. **Hand the spec to the user** — tell them to run: `npx opencastle run -f .opencastle/convoys/<name>.convoy.yml`
|
|
90
|
+
3. **After convoy completes** — proceed to Step 5 (validation).
|
|
91
|
+
|
|
92
|
+
#### Convoy Task Prompt Must Include
|
|
89
93
|
|
|
90
94
|
- **Tracker issue ID and title** — e.g., `TAS-XX — [Bug] Description`
|
|
91
95
|
- **Root cause** — What's wrong and why
|
|
@@ -7,7 +7,7 @@ agent: 'Team Lead (OpenCastle)'
|
|
|
7
7
|
|
|
8
8
|
# Generate Convoy Spec
|
|
9
9
|
|
|
10
|
-
You are the Team Lead. The user wants to run `opencastle run` to execute a batch of tasks autonomously via the convoy engine. Your job is to produce a valid `.convoy.yml` file they can feed to the CLI. Derive a short, descriptive, kebab-case filename from the user's goal (2–4 words max) and use it as the filename — for example `auth-refactor.convoy.yml` or `add-search.convoy.yml`. Always use the `.convoy.yml` extension.
|
|
10
|
+
You are the Team Lead. The user wants to run `opencastle run` to execute a batch of tasks autonomously via the convoy engine. Your job is to produce a valid `.convoy.yml` file they can feed to the CLI. Derive a short, descriptive, kebab-case filename from the user's goal (2–4 words max) and use it as the filename — for example `auth-refactor.convoy.yml` or `add-search.convoy.yml`. Always use the `.convoy.yml` extension. Store all generated convoy specs in the `.opencastle/convoys/` directory (create it if it doesn't exist).
|
|
11
11
|
|
|
12
12
|
## User Goal
|
|
13
13
|
|
|
@@ -137,7 +137,7 @@ Before presenting the YAML, mentally verify:
|
|
|
137
137
|
Return the final YAML inside a fenced code block with a filename annotation:
|
|
138
138
|
|
|
139
139
|
````yaml
|
|
140
|
-
#
|
|
140
|
+
# .opencastle/convoys/<feature-name>.convoy.yml
|
|
141
141
|
name: <run name>
|
|
142
142
|
version: 1
|
|
143
143
|
concurrency: <n>
|
|
@@ -172,6 +172,6 @@ gates:
|
|
|
172
172
|
Also provide:
|
|
173
173
|
1. A **DAG summary** showing the phase structure so the user can verify execution order.
|
|
174
174
|
2. An **estimated total duration** (sum of timeouts on the critical path).
|
|
175
|
-
3. A `--dry-run` command they can use to validate: `npx opencastle run --file
|
|
175
|
+
3. A `--dry-run` command they can use to validate: `npx opencastle run --file .opencastle/convoys/<feature-name>.convoy.yml --dry-run`
|
|
176
176
|
|
|
177
177
|
|
|
@@ -47,31 +47,20 @@ Every subtask must be tracked. **No issue = no implementation.** This step produ
|
|
|
47
47
|
5. **Link to roadmap** — Reference the roadmap section in the issue description so context is never lost
|
|
48
48
|
6. **Verify issues exist** — List all created issue IDs. If count is 0, do NOT proceed to Step 2.5
|
|
49
49
|
|
|
50
|
-
### 2.5
|
|
50
|
+
### 2.5 Generate Convoy Spec (BLOCKING — decides how Step 3 proceeds)
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
All project-related work is executed via the convoy engine — regardless of subtask count. This ensures consistent observability, crash recovery, and live progress.
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
| 3+ subtasks | **Convoy execution** — generate a `.convoy.yml` spec using the `generate-convoy` prompt, then hand it to the user |
|
|
58
|
-
|
|
59
|
-
#### Direct delegation (1–2 subtasks)
|
|
60
|
-
|
|
61
|
-
Proceed with the normal Step 3 delegation workflow. Sub-agents handle each task inline.
|
|
62
|
-
|
|
63
|
-
#### Convoy execution (3+ subtasks)
|
|
64
|
-
|
|
65
|
-
1. **Generate the spec** — use the `generate-convoy` prompt with the decomposed task list as context. The spec IS the implementation plan — no manual per-task delegation is needed.
|
|
66
|
-
2. **Hand the spec to the user** — tell them to run: `npx opencastle run -f <name>.convoy.yml`
|
|
67
|
-
3. **The convoy engine handles** isolated git worktrees, parallel execution, merge queue ordering, and crash recovery automatically.
|
|
54
|
+
1. **Generate the spec** — use the `generate-convoy` prompt with the decomposed task list as context. The spec IS the implementation plan — no manual per-task delegation is needed. Even single-task fixes go through convoy for observability.
|
|
55
|
+
2. **Hand the spec to the user** — tell them to run: `npx opencastle run -f .opencastle/convoys/<name>.convoy.yml`
|
|
56
|
+
3. **The convoy engine handles** isolated git worktrees, parallel execution, merge queue ordering, crash recovery, and structured logging automatically.
|
|
68
57
|
4. **After convoy completes** — proceed to Step 4 (validation) and Step 5 (delivery/PR). The convoy engine will have created its own commits on the configured branch.
|
|
69
58
|
|
|
70
|
-
> **Why convoy
|
|
59
|
+
> **Why always convoy?** Convoy execution is the only path that guarantees observability logging, crash recovery, gate validation, and live progress. Direct sub-agent delegation produces no structured logs and cannot be resumed if interrupted.
|
|
71
60
|
|
|
72
61
|
### 3. Implementation Rules
|
|
73
62
|
|
|
74
|
-
> **
|
|
63
|
+
> **Convoy execution:** The convoy spec file IS the implementation plan — skip the manual delegation workflow below and jump to Step 4 after the user runs the convoy. The convoy engine delegates tasks internally using the agents and prompts defined in the spec.
|
|
75
64
|
|
|
76
65
|
#### Issue Traceability
|
|
77
66
|
|
|
@@ -117,7 +106,7 @@ Every subtask must pass ALL gates before being marked Done:
|
|
|
117
106
|
|
|
118
107
|
Follow the **Delivery Outcome** defined in the **git-workflow** skill — commit, push, open PR (not merged), and link to the tracker.
|
|
119
108
|
|
|
120
|
-
>
|
|
109
|
+
> The convoy engine creates commits on the configured `branch` directly. After validation passes, open the PR from that branch. No additional commits from the Team Lead are needed unless gates failed and required manual fixes.
|
|
121
110
|
|
|
122
111
|
### 6. Documentation & Traceability
|
|
123
112
|
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { Task, ExecuteOptions, ExecuteResult } from '../../types.js';
|
|
2
|
-
/** Adapter name */
|
|
3
|
-
export declare const name = "claude-code";
|
|
4
|
-
/**
|
|
5
|
-
* Check if the `claude` CLI is available on the system PATH.
|
|
6
|
-
*/
|
|
7
|
-
export declare function isAvailable(): Promise<boolean>;
|
|
8
|
-
/**
|
|
9
|
-
* Execute a task by invoking the Claude Code CLI in print mode.
|
|
10
|
-
*/
|
|
11
|
-
export declare function execute(task: Task, options?: ExecuteOptions): Promise<ExecuteResult>;
|
|
12
|
-
/**
|
|
13
|
-
* Kill the process associated with a task (used by timeout enforcement).
|
|
14
|
-
*/
|
|
15
|
-
export declare function kill(task: Task): void;
|
|
16
|
-
//# sourceMappingURL=claude-code.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"claude-code.d.ts","sourceRoot":"","sources":["../../../../src/cli/run/adapters/claude-code.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,aAAa,EAAc,MAAM,gBAAgB,CAAA;AAErF,mBAAmB;AACnB,eAAO,MAAM,IAAI,gBAAgB,CAAA;AAEjC;;GAEG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAMpD;AAED;;GAEG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,aAAa,CAAC,CAwE9F;AAED;;GAEG;AACH,wBAAgB,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CASrC"}
|