opencastle 0.10.7 → 0.12.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 +4 -0
- package/bin/cli.mjs +4 -0
- package/dist/cli/convoy/events.d.ts +10 -0
- package/dist/cli/convoy/events.d.ts.map +1 -0
- package/dist/cli/convoy/events.js +27 -0
- package/dist/cli/convoy/events.js.map +1 -0
- package/dist/cli/convoy/events.test.d.ts +2 -0
- package/dist/cli/convoy/events.test.d.ts.map +1 -0
- package/dist/cli/convoy/events.test.js +94 -0
- package/dist/cli/convoy/events.test.js.map +1 -0
- package/dist/cli/convoy/store.d.ts +23 -0
- package/dist/cli/convoy/store.d.ts.map +1 -0
- package/dist/cli/convoy/store.js +210 -0
- package/dist/cli/convoy/store.js.map +1 -0
- package/dist/cli/convoy/store.test.d.ts +2 -0
- package/dist/cli/convoy/store.test.d.ts.map +1 -0
- package/dist/cli/convoy/store.test.js +387 -0
- package/dist/cli/convoy/store.test.js.map +1 -0
- package/dist/cli/convoy/types.d.ts +56 -0
- package/dist/cli/convoy/types.d.ts.map +1 -0
- package/dist/cli/convoy/types.js +2 -0
- package/dist/cli/convoy/types.js.map +1 -0
- package/dist/cli/dashboard.d.ts.map +1 -1
- package/dist/cli/dashboard.js +5 -1
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/init.test.js +1 -1
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/lesson.d.ts +17 -0
- package/dist/cli/lesson.d.ts.map +1 -0
- package/dist/cli/lesson.js +294 -0
- package/dist/cli/lesson.js.map +1 -0
- package/dist/cli/log.d.ts +7 -0
- package/dist/cli/log.d.ts.map +1 -0
- package/dist/cli/log.js +131 -0
- package/dist/cli/log.js.map +1 -0
- package/dist/cli/run/executor.js.map +1 -1
- package/dist/cli/run/executor.test.js +1 -0
- package/dist/cli/run/executor.test.js.map +1 -1
- package/dist/cli/run/loop-executor.d.ts +3 -0
- package/dist/cli/run/loop-executor.d.ts.map +1 -0
- package/dist/cli/run/loop-executor.js +155 -0
- package/dist/cli/run/loop-executor.js.map +1 -0
- package/dist/cli/run/loop-reporter.d.ts +6 -0
- package/dist/cli/run/loop-reporter.d.ts.map +1 -0
- package/dist/cli/run/loop-reporter.js +112 -0
- package/dist/cli/run/loop-reporter.js.map +1 -0
- package/dist/cli/run/reporter.d.ts.map +1 -1
- package/dist/cli/run/reporter.js +28 -1
- package/dist/cli/run/reporter.js.map +1 -1
- package/dist/cli/run/schema.d.ts +4 -0
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +178 -50
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run/schema.test.js +598 -1
- package/dist/cli/run/schema.test.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +84 -3
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/types.d.ts +78 -1
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +54 -1
- package/dist/cli/update.js.map +1 -1
- package/package.json +3 -2
- package/src/cli/convoy/events.test.ts +118 -0
- package/src/cli/convoy/events.ts +41 -0
- package/src/cli/convoy/store.test.ts +446 -0
- package/src/cli/convoy/store.ts +308 -0
- package/src/cli/convoy/types.ts +68 -0
- package/src/cli/dashboard.ts +5 -1
- package/src/cli/init.test.ts +1 -1
- package/src/cli/lesson.ts +312 -0
- package/src/cli/log.ts +133 -0
- package/src/cli/run/executor.test.ts +1 -0
- package/src/cli/run/executor.ts +8 -8
- package/src/cli/run/loop-executor.ts +199 -0
- package/src/cli/run/loop-reporter.ts +125 -0
- package/src/cli/run/reporter.ts +30 -1
- package/src/cli/run/schema.test.ts +704 -3
- package/src/cli/run/schema.ts +206 -56
- package/src/cli/run.ts +82 -5
- package/src/cli/types.ts +87 -1
- package/src/cli/update.ts +62 -1
- package/src/dashboard/dist/index.html +14 -15
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/scripts/generate-seed-data.ts +23 -43
- package/src/dashboard/seed-data/events.ndjson +104 -0
- package/src/dashboard/src/pages/index.astro +14 -15
- package/src/orchestrator/agents/api-designer.agent.md +1 -1
- package/src/orchestrator/agents/architect.agent.md +1 -1
- package/src/orchestrator/agents/content-engineer.agent.md +1 -1
- package/src/orchestrator/agents/copywriter.agent.md +1 -1
- package/src/orchestrator/agents/data-expert.agent.md +1 -1
- package/src/orchestrator/agents/database-engineer.agent.md +1 -1
- package/src/orchestrator/agents/developer.agent.md +1 -1
- package/src/orchestrator/agents/devops-expert.agent.md +1 -1
- package/src/orchestrator/agents/documentation-writer.agent.md +1 -1
- package/src/orchestrator/agents/performance-expert.agent.md +1 -1
- package/src/orchestrator/agents/release-manager.agent.md +1 -1
- package/src/orchestrator/agents/security-expert.agent.md +1 -1
- package/src/orchestrator/agents/seo-specialist.agent.md +1 -1
- package/src/orchestrator/agents/session-guard.agent.md +9 -21
- package/src/orchestrator/agents/team-lead.agent.md +8 -34
- package/src/orchestrator/agents/testing-expert.agent.md +1 -1
- package/src/orchestrator/agents/ui-ux-expert.agent.md +1 -1
- package/src/orchestrator/customizations/AGENT-PERFORMANCE.md +11 -12
- package/src/orchestrator/customizations/DISPUTES.md +2 -2
- package/src/orchestrator/customizations/README.md +1 -3
- package/src/orchestrator/customizations/logs/README.md +66 -14
- package/src/orchestrator/instructions/ai-optimization.instructions.md +21 -132
- package/src/orchestrator/instructions/general.instructions.md +35 -181
- package/src/orchestrator/plugins/nx/SKILL.md +1 -1
- package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +4 -8
- package/src/orchestrator/prompts/bug-fix.prompt.md +4 -4
- package/src/orchestrator/prompts/implement-feature.prompt.md +3 -3
- package/src/orchestrator/prompts/quick-refinement.prompt.md +3 -3
- package/src/orchestrator/prompts/resolve-pr-comments.prompt.md +1 -1
- package/src/orchestrator/skills/agent-hooks/SKILL.md +11 -11
- package/src/orchestrator/skills/decomposition/SKILL.md +1 -1
- package/src/orchestrator/skills/fast-review/SKILL.md +4 -19
- package/src/orchestrator/skills/git-workflow/SKILL.md +72 -0
- package/src/orchestrator/skills/memory-merger/SKILL.md +1 -1
- package/src/orchestrator/skills/observability-logging/SKILL.md +129 -0
- package/src/orchestrator/skills/orchestration-protocols/SKILL.md +2 -2
- package/src/orchestrator/skills/panel-majority-vote/SKILL.md +4 -7
- package/src/orchestrator/skills/self-improvement/SKILL.md +13 -26
- package/src/orchestrator/skills/team-lead-reference/SKILL.md +2 -2
- package/src/orchestrator/customizations/logs/delegations.ndjson +0 -1
- package/src/orchestrator/customizations/logs/panels.ndjson +0 -1
- package/src/orchestrator/customizations/logs/reviews.ndjson +0 -0
- package/src/orchestrator/customizations/logs/sessions.ndjson +0 -1
- /package/src/orchestrator/customizations/logs/{disputes.ndjson → events.ndjson} +0 -0
package/src/cli/run/schema.ts
CHANGED
|
@@ -39,6 +39,12 @@ interface RawSpec {
|
|
|
39
39
|
on_failure?: unknown
|
|
40
40
|
adapter?: unknown
|
|
41
41
|
tasks?: unknown
|
|
42
|
+
mode?: unknown
|
|
43
|
+
loop?: unknown
|
|
44
|
+
version?: unknown
|
|
45
|
+
defaults?: unknown
|
|
46
|
+
gates?: unknown
|
|
47
|
+
branch?: unknown
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
interface RawTask {
|
|
@@ -49,6 +55,8 @@ interface RawTask {
|
|
|
49
55
|
depends_on?: unknown
|
|
50
56
|
files?: unknown
|
|
51
57
|
description?: unknown
|
|
58
|
+
model?: unknown
|
|
59
|
+
max_retries?: unknown
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
/**
|
|
@@ -90,72 +98,180 @@ export function validateSpec(spec: unknown): ValidationResult {
|
|
|
90
98
|
errors.push('`adapter` must be a string')
|
|
91
99
|
}
|
|
92
100
|
|
|
93
|
-
//
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
// version
|
|
102
|
+
if (s.version !== undefined) {
|
|
103
|
+
if (typeof s.version !== 'number' || !Number.isInteger(s.version) || s.version !== 1) {
|
|
104
|
+
errors.push('`version` must be 1')
|
|
105
|
+
}
|
|
97
106
|
}
|
|
98
107
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
// defaults
|
|
109
|
+
if (s.defaults !== undefined) {
|
|
110
|
+
if (!s.defaults || typeof s.defaults !== 'object' || Array.isArray(s.defaults)) {
|
|
111
|
+
errors.push('`defaults` must be an object')
|
|
112
|
+
} else {
|
|
113
|
+
const d = s.defaults as Record<string, unknown>
|
|
114
|
+
if (d.timeout !== undefined && isNaN(parseTimeout(d.timeout as string))) {
|
|
115
|
+
errors.push(
|
|
116
|
+
'`defaults.timeout` must be in format: <number><s|m|h> (e.g. "10m")'
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
if (d.model !== undefined && typeof d.model !== 'string') {
|
|
120
|
+
errors.push('`defaults.model` must be a string')
|
|
121
|
+
}
|
|
122
|
+
if (d.max_retries !== undefined) {
|
|
123
|
+
const mr = Number(d.max_retries)
|
|
124
|
+
if (!Number.isInteger(mr) || mr < 0) {
|
|
125
|
+
errors.push('`defaults.max_retries` must be a non-negative integer')
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (d.agent !== undefined && typeof d.agent !== 'string') {
|
|
129
|
+
errors.push('`defaults.agent` must be a string')
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
105
133
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
134
|
+
// gates
|
|
135
|
+
if (s.gates !== undefined) {
|
|
136
|
+
if (
|
|
137
|
+
!Array.isArray(s.gates) ||
|
|
138
|
+
!(s.gates as unknown[]).every((g) => typeof g === 'string')
|
|
139
|
+
) {
|
|
140
|
+
errors.push('`gates` must be an array of strings')
|
|
109
141
|
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// branch
|
|
145
|
+
if (s.branch !== undefined && typeof s.branch !== 'string') {
|
|
146
|
+
errors.push('`branch` must be a string')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// mode
|
|
150
|
+
const mode = s.mode !== undefined ? s.mode : 'tasks'
|
|
151
|
+
if (mode !== 'tasks' && mode !== 'loop') {
|
|
152
|
+
errors.push('`mode` must be one of: tasks, loop')
|
|
153
|
+
}
|
|
110
154
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
errors.push(
|
|
155
|
+
if (mode === 'loop') {
|
|
156
|
+
// Loop validation — tasks array is NOT required
|
|
157
|
+
const loop = s.loop as Record<string, unknown> | undefined
|
|
158
|
+
if (!loop || typeof loop !== 'object') {
|
|
159
|
+
errors.push('`loop` is required when mode is "loop"')
|
|
116
160
|
} else {
|
|
117
|
-
|
|
161
|
+
if (!loop.prompt || typeof loop.prompt !== 'string') {
|
|
162
|
+
errors.push('`loop.prompt` is required and must be a string')
|
|
163
|
+
}
|
|
164
|
+
if (loop.max_iterations !== undefined) {
|
|
165
|
+
const mi = Number(loop.max_iterations)
|
|
166
|
+
if (!Number.isInteger(mi) || mi < 1) {
|
|
167
|
+
errors.push('`loop.max_iterations` must be an integer >= 1')
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (loop.timeout !== undefined) {
|
|
171
|
+
if (isNaN(parseTimeout(loop.timeout as string))) {
|
|
172
|
+
errors.push(
|
|
173
|
+
'`loop.timeout` must be in format: <number><s|m|h> (e.g. "10m")'
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (loop.backpressure !== undefined) {
|
|
178
|
+
if (
|
|
179
|
+
!Array.isArray(loop.backpressure) ||
|
|
180
|
+
!(loop.backpressure as unknown[]).every((b) => typeof b === 'string')
|
|
181
|
+
) {
|
|
182
|
+
errors.push('`loop.backpressure` must be an array of strings')
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (loop.plan_file !== undefined && typeof loop.plan_file !== 'string') {
|
|
186
|
+
errors.push('`loop.plan_file` must be a string')
|
|
187
|
+
}
|
|
188
|
+
if (loop.model !== undefined && typeof loop.model !== 'string') {
|
|
189
|
+
errors.push('`loop.model` must be a string')
|
|
190
|
+
}
|
|
118
191
|
}
|
|
119
|
-
|
|
120
|
-
//
|
|
121
|
-
if (!
|
|
122
|
-
errors.push(
|
|
192
|
+
} else {
|
|
193
|
+
// Tasks mode — tasks array is required
|
|
194
|
+
if (!s.tasks || !Array.isArray(s.tasks) || s.tasks.length === 0) {
|
|
195
|
+
errors.push('`tasks` is required and must be a non-empty array')
|
|
196
|
+
return { valid: false, errors }
|
|
123
197
|
}
|
|
124
198
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
199
|
+
const taskIds = new Set<string>()
|
|
200
|
+
const tasks = s.tasks as RawTask[]
|
|
201
|
+
|
|
202
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
203
|
+
const task = tasks[i]
|
|
204
|
+
const prefix = `tasks[${i}]`
|
|
205
|
+
|
|
206
|
+
if (!task || typeof task !== 'object') {
|
|
207
|
+
errors.push(`${prefix}: must be an object`)
|
|
208
|
+
continue
|
|
131
209
|
}
|
|
132
|
-
}
|
|
133
210
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
211
|
+
// id
|
|
212
|
+
if (!task.id || typeof task.id !== 'string') {
|
|
213
|
+
errors.push(`${prefix}: \`id\` is required and must be a string`)
|
|
214
|
+
} else if (taskIds.has(task.id)) {
|
|
215
|
+
errors.push(`${prefix}: duplicate task id "${task.id}"`)
|
|
138
216
|
} else {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
217
|
+
taskIds.add(task.id)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// prompt
|
|
221
|
+
if (!task.prompt || typeof task.prompt !== 'string') {
|
|
222
|
+
errors.push(`${prefix}: \`prompt\` is required and must be a string`)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// timeout
|
|
226
|
+
if (task.timeout !== undefined) {
|
|
227
|
+
if (isNaN(parseTimeout(task.timeout as string))) {
|
|
228
|
+
errors.push(
|
|
229
|
+
`${prefix}: \`timeout\` must be in format: <number><s|m|h> (e.g. "10m")`
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// depends_on
|
|
235
|
+
if (task.depends_on !== undefined) {
|
|
236
|
+
if (!Array.isArray(task.depends_on)) {
|
|
237
|
+
errors.push(`${prefix}: \`depends_on\` must be an array`)
|
|
238
|
+
} else {
|
|
239
|
+
for (const dep of task.depends_on as string[]) {
|
|
240
|
+
if (!taskIds.has(dep) && !tasks.some((t) => t && t.id === dep)) {
|
|
241
|
+
errors.push(
|
|
242
|
+
`${prefix}: \`depends_on\` references unknown task "${dep}"`
|
|
243
|
+
)
|
|
244
|
+
}
|
|
144
245
|
}
|
|
145
246
|
}
|
|
146
247
|
}
|
|
147
|
-
}
|
|
148
248
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
249
|
+
// files
|
|
250
|
+
if (task.files !== undefined && !Array.isArray(task.files)) {
|
|
251
|
+
errors.push(`${prefix}: \`files\` must be an array`)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// model
|
|
255
|
+
if (task.model !== undefined && typeof task.model !== 'string') {
|
|
256
|
+
errors.push(`${prefix}: \`model\` must be a string`)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// max_retries
|
|
260
|
+
if (task.max_retries !== undefined) {
|
|
261
|
+
const mr = Number(task.max_retries)
|
|
262
|
+
if (!Number.isInteger(mr) || mr < 0) {
|
|
263
|
+
errors.push(
|
|
264
|
+
`${prefix}: \`max_retries\` must be a non-negative integer`
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
152
268
|
}
|
|
153
|
-
}
|
|
154
269
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
270
|
+
// DAG cycle detection
|
|
271
|
+
if (errors.length === 0) {
|
|
272
|
+
const cycleErr = detectCycles(tasks as Array<{ id: string; depends_on?: string[] }>)
|
|
273
|
+
if (cycleErr) errors.push(cycleErr)
|
|
274
|
+
}
|
|
159
275
|
}
|
|
160
276
|
|
|
161
277
|
return { valid: errors.length === 0, errors }
|
|
@@ -214,19 +330,53 @@ export function applyDefaults(spec: Record<string, unknown>): TaskSpec {
|
|
|
214
330
|
s.on_failure = (s.on_failure as string) || 'continue'
|
|
215
331
|
// Leave adapter empty so run.ts can auto-detect the best available CLI
|
|
216
332
|
s.adapter = (s.adapter as string) || ''
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
333
|
+
s.mode = (s.mode as string) || 'tasks'
|
|
334
|
+
|
|
335
|
+
if (s.mode === 'loop') {
|
|
336
|
+
const loop = ((s.loop ?? {}) as Record<string, unknown>)
|
|
337
|
+
loop.max_iterations = loop.max_iterations !== undefined ? Number(loop.max_iterations) : 20
|
|
338
|
+
loop.plan_file = (loop.plan_file as string) || 'IMPLEMENTATION_PLAN.md'
|
|
339
|
+
loop.timeout = (loop.timeout as string) || '10m'
|
|
340
|
+
s.loop = loop
|
|
341
|
+
} else {
|
|
342
|
+
const tasks = s.tasks as Array<Record<string, unknown>>
|
|
343
|
+
const d =
|
|
344
|
+
s.version === 1 && s.defaults
|
|
345
|
+
? (s.defaults as Record<string, unknown>)
|
|
346
|
+
: {}
|
|
347
|
+
for (const task of tasks) {
|
|
348
|
+
task.agent =
|
|
349
|
+
(task.agent as string) || (d.agent as string | undefined) || 'developer'
|
|
350
|
+
task.timeout =
|
|
351
|
+
(task.timeout as string) ||
|
|
352
|
+
(d.timeout as string | undefined) ||
|
|
353
|
+
'30m'
|
|
354
|
+
task.depends_on = (task.depends_on as string[]) || []
|
|
355
|
+
task.files = (task.files as string[]) || []
|
|
356
|
+
task.description = (task.description as string) || (task.id as string)
|
|
357
|
+
// model: task-level overrides defaults (no hardcoded fallback)
|
|
358
|
+
if (task.model === undefined && d.model !== undefined) {
|
|
359
|
+
task.model = d.model
|
|
360
|
+
}
|
|
361
|
+
// max_retries: task-level overrides defaults, fallback to 1
|
|
362
|
+
if (task.max_retries === undefined) {
|
|
363
|
+
task.max_retries =
|
|
364
|
+
d.max_retries !== undefined ? Number(d.max_retries) : 1
|
|
365
|
+
}
|
|
366
|
+
}
|
|
225
367
|
}
|
|
226
368
|
|
|
227
369
|
return s as unknown as TaskSpec
|
|
228
370
|
}
|
|
229
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Returns true if the spec uses the Convoy Engine enhanced format (version: 1).
|
|
374
|
+
*/
|
|
375
|
+
export function isConvoySpec(spec: unknown): boolean {
|
|
376
|
+
if (!spec || typeof spec !== 'object') return false
|
|
377
|
+
return (spec as Record<string, unknown>).version === 1
|
|
378
|
+
}
|
|
379
|
+
|
|
230
380
|
/**
|
|
231
381
|
* Read, parse, validate, and return a typed task spec from a YAML file.
|
|
232
382
|
* @throws If file cannot be read, parsed, or spec is invalid
|
package/src/cli/run.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
1
2
|
import { resolve } from 'node:path'
|
|
2
3
|
import { parseTaskSpec } from './run/schema.js'
|
|
3
4
|
import { createExecutor, buildPhases } from './run/executor.js'
|
|
@@ -9,14 +10,17 @@ const HELP = `
|
|
|
9
10
|
opencastle run [options]
|
|
10
11
|
|
|
11
12
|
Process a task queue from a spec file, delegating to AI agents autonomously.
|
|
13
|
+
Supports two modes: tasks (default phase-based execution) and loop (iterative Ralph Loop).
|
|
12
14
|
|
|
13
15
|
Options:
|
|
14
16
|
--file, -f <path> Task spec file (default: opencastle.tasks.yml)
|
|
15
17
|
--dry-run Show execution plan without running
|
|
16
|
-
--concurrency, -c <n> Override max parallel tasks
|
|
18
|
+
--concurrency, -c <n> Override max parallel tasks (tasks mode)
|
|
17
19
|
--adapter, -a <name> Override agent runtime adapter
|
|
18
20
|
--report-dir <path> Where to write run reports (default: .opencastle/runs)
|
|
19
21
|
--verbose Show full agent output
|
|
22
|
+
--mode <name> Execution mode: tasks | loop
|
|
23
|
+
--max-iterations <n> Override max loop iterations (loop mode)
|
|
20
24
|
--help, -h Show this help
|
|
21
25
|
`
|
|
22
26
|
|
|
@@ -32,6 +36,8 @@ function parseArgs(args: string[]): RunOptions {
|
|
|
32
36
|
reportDir: null,
|
|
33
37
|
verbose: false,
|
|
34
38
|
help: false,
|
|
39
|
+
maxIterations: null,
|
|
40
|
+
mode: null,
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -73,6 +79,26 @@ function parseArgs(args: string[]): RunOptions {
|
|
|
73
79
|
case '--verbose':
|
|
74
80
|
opts.verbose = true
|
|
75
81
|
break
|
|
82
|
+
case '--max-iterations': {
|
|
83
|
+
if (i + 1 >= args.length) { console.error(' \u2717 --max-iterations requires a number'); process.exit(1) }
|
|
84
|
+
const val = parseInt(args[++i], 10)
|
|
85
|
+
if (!Number.isFinite(val) || val < 1) {
|
|
86
|
+
console.error(` \u2717 --max-iterations must be an integer >= 1`)
|
|
87
|
+
process.exit(1)
|
|
88
|
+
}
|
|
89
|
+
opts.maxIterations = val
|
|
90
|
+
break
|
|
91
|
+
}
|
|
92
|
+
case '--mode': {
|
|
93
|
+
if (i + 1 >= args.length) { console.error(' \u2717 --mode requires a name'); process.exit(1) }
|
|
94
|
+
const modeVal = args[++i]
|
|
95
|
+
if (modeVal !== 'tasks' && modeVal !== 'loop') {
|
|
96
|
+
console.error(` \u2717 --mode must be one of: tasks, loop`)
|
|
97
|
+
process.exit(1)
|
|
98
|
+
}
|
|
99
|
+
opts.mode = modeVal
|
|
100
|
+
break
|
|
101
|
+
}
|
|
76
102
|
default:
|
|
77
103
|
console.error(` ✗ Unknown option: ${arg}`)
|
|
78
104
|
console.log(HELP)
|
|
@@ -102,6 +128,7 @@ export default async function run({ args }: CliContext): Promise<void> {
|
|
|
102
128
|
if (opts.concurrency !== null) spec.concurrency = opts.concurrency
|
|
103
129
|
if (opts.adapter !== null) spec.adapter = opts.adapter
|
|
104
130
|
if (opts.verbose) spec._verbose = true
|
|
131
|
+
if (opts.mode !== null) spec.mode = opts.mode as 'tasks' | 'loop'
|
|
105
132
|
|
|
106
133
|
// ── Auto-detect adapter if not specified ─────────────────────
|
|
107
134
|
let detectionFailed = false
|
|
@@ -117,9 +144,25 @@ export default async function run({ args }: CliContext): Promise<void> {
|
|
|
117
144
|
}
|
|
118
145
|
|
|
119
146
|
// ── Dry run ──────────────────────────────────────────────────
|
|
120
|
-
const phases = buildPhases(spec.tasks)
|
|
121
|
-
|
|
122
147
|
if (opts.dryRun) {
|
|
148
|
+
if (spec.mode === 'loop') {
|
|
149
|
+
const loop = spec.loop!
|
|
150
|
+
console.log(`\n \uD83C\uDFF0 Loop Plan: ${spec.name}`)
|
|
151
|
+
console.log(` Mode: loop`)
|
|
152
|
+
console.log(` Prompt: ${loop.prompt}`)
|
|
153
|
+
console.log(` Max iterations: ${loop.max_iterations}`)
|
|
154
|
+
console.log(` Timeout: ${loop.timeout}`)
|
|
155
|
+
if (loop.plan_file) console.log(` Plan file: ${loop.plan_file}`)
|
|
156
|
+
if (loop.model) console.log(` Model: ${loop.model}`)
|
|
157
|
+
if (loop.backpressure?.length) {
|
|
158
|
+
console.log(` Backpressure:`)
|
|
159
|
+
for (const cmd of loop.backpressure) {
|
|
160
|
+
console.log(` - ${cmd}`)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
const phases = buildPhases(spec.tasks!)
|
|
123
166
|
printExecutionPlan(spec, phases)
|
|
124
167
|
return
|
|
125
168
|
}
|
|
@@ -164,8 +207,42 @@ export default async function run({ args }: CliContext): Promise<void> {
|
|
|
164
207
|
}
|
|
165
208
|
|
|
166
209
|
// ── Execute ──────────────────────────────────────────────────
|
|
167
|
-
|
|
168
|
-
|
|
210
|
+
if (spec.mode === 'loop') {
|
|
211
|
+
const { createLoopExecutor } = await import('./run/loop-executor.js')
|
|
212
|
+
const { createLoopReporter } = await import('./run/loop-reporter.js')
|
|
213
|
+
|
|
214
|
+
if (opts.maxIterations !== null && spec.loop) {
|
|
215
|
+
spec.loop.max_iterations = opts.maxIterations
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const promptPath = resolve(process.cwd(), spec.loop!.prompt)
|
|
219
|
+
try {
|
|
220
|
+
await readFile(promptPath)
|
|
221
|
+
} catch {
|
|
222
|
+
console.error(` \u2717 Prompt file not found: ${spec.loop!.prompt}`)
|
|
223
|
+
process.exit(1)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.log(`\n \uD83C\uDFF0 OpenCastle Loop: ${spec.name}`)
|
|
227
|
+
console.log(` Adapter: ${adapter.name} | Max iterations: ${spec.loop!.max_iterations} | Timeout: ${spec.loop!.timeout}`)
|
|
228
|
+
if (spec.loop!.backpressure?.length) {
|
|
229
|
+
console.log(` Backpressure: ${spec.loop!.backpressure.join(', ')}`)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const loopReporter = createLoopReporter(spec.name, {
|
|
233
|
+
reportDir: opts.reportDir ? resolve(process.cwd(), opts.reportDir) : undefined,
|
|
234
|
+
verbose: opts.verbose,
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
const loopExecutor = createLoopExecutor(spec, adapter, loopReporter)
|
|
238
|
+
const loopReport = await loopExecutor.run()
|
|
239
|
+
|
|
240
|
+
const failed = loopReport.stoppedReason === 'error' || loopReport.stoppedReason === 'backpressure-fail'
|
|
241
|
+
process.exit(failed ? 1 : 0)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
console.log(`\n \uD83C\uDFF0 OpenCastle Run: ${spec.name}`)
|
|
245
|
+
console.log(` Adapter: ${adapter.name} | Concurrency: ${spec.concurrency} | Tasks: ${spec.tasks!.length}`)
|
|
169
246
|
|
|
170
247
|
const reporter = createReporter(spec, {
|
|
171
248
|
reportDir: opts.reportDir
|
package/src/cli/types.ts
CHANGED
|
@@ -126,14 +126,48 @@ export const IDE_LABELS: Record<IdeChoice, string> = {
|
|
|
126
126
|
|
|
127
127
|
// ── Run command types ──────────────────────────────────────────
|
|
128
128
|
|
|
129
|
+
/** Default values merged into each task for Convoy Engine (version: 1) specs. */
|
|
130
|
+
export interface TaskDefaults {
|
|
131
|
+
timeout?: string;
|
|
132
|
+
model?: string;
|
|
133
|
+
max_retries?: number;
|
|
134
|
+
agent?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Loop execution configuration. */
|
|
138
|
+
export interface LoopConfig {
|
|
139
|
+
/** Maximum number of agent iterations (default 20). */
|
|
140
|
+
max_iterations: number;
|
|
141
|
+
/** Path to the prompt file read each iteration. */
|
|
142
|
+
prompt: string;
|
|
143
|
+
/** Path to the plan file (default 'IMPLEMENTATION_PLAN.md'). */
|
|
144
|
+
plan_file?: string;
|
|
145
|
+
/** Per-iteration timeout (default '10m'). */
|
|
146
|
+
timeout: string;
|
|
147
|
+
/** Model override for loop sessions. */
|
|
148
|
+
model?: string;
|
|
149
|
+
/** Shell commands that must exit 0 after each iteration. */
|
|
150
|
+
backpressure?: string[];
|
|
151
|
+
}
|
|
152
|
+
|
|
129
153
|
/** Validated task spec from YAML. */
|
|
130
154
|
export interface TaskSpec {
|
|
131
155
|
name: string;
|
|
132
156
|
concurrency: number;
|
|
133
157
|
on_failure: 'continue' | 'stop';
|
|
134
158
|
adapter: string;
|
|
135
|
-
tasks
|
|
159
|
+
tasks?: Task[];
|
|
160
|
+
mode?: 'tasks' | 'loop';
|
|
161
|
+
loop?: LoopConfig;
|
|
136
162
|
_verbose?: boolean;
|
|
163
|
+
/** Spec schema version (1 for Convoy Engine format). */
|
|
164
|
+
version?: number;
|
|
165
|
+
/** Worker defaults merged into each task (Convoy Engine). */
|
|
166
|
+
defaults?: TaskDefaults;
|
|
167
|
+
/** Shell commands run after all tasks complete; each must exit 0. */
|
|
168
|
+
gates?: string[];
|
|
169
|
+
/** Git feature branch name. */
|
|
170
|
+
branch?: string;
|
|
137
171
|
}
|
|
138
172
|
|
|
139
173
|
/** A single task in the spec. */
|
|
@@ -146,6 +180,10 @@ export interface Task {
|
|
|
146
180
|
files: string[];
|
|
147
181
|
description: string;
|
|
148
182
|
_process?: ChildProcess;
|
|
183
|
+
/** Model override for this task. */
|
|
184
|
+
model?: string;
|
|
185
|
+
/** Max retry attempts (default: 1). */
|
|
186
|
+
max_retries: number;
|
|
149
187
|
}
|
|
150
188
|
|
|
151
189
|
/** Task execution status. */
|
|
@@ -231,6 +269,8 @@ export interface RunOptions {
|
|
|
231
269
|
reportDir: string | null;
|
|
232
270
|
verbose: boolean;
|
|
233
271
|
help: boolean;
|
|
272
|
+
maxIterations: number | null;
|
|
273
|
+
mode: string | null;
|
|
234
274
|
}
|
|
235
275
|
|
|
236
276
|
/** Validation result. */
|
|
@@ -250,3 +290,49 @@ export interface Executor {
|
|
|
250
290
|
run(): Promise<RunReport>;
|
|
251
291
|
getPhases(): Task[][];
|
|
252
292
|
}
|
|
293
|
+
|
|
294
|
+
// ── Loop executor types ────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
/** Result of a single backpressure command run. */
|
|
297
|
+
export interface BackpressureResult {
|
|
298
|
+
command: string;
|
|
299
|
+
exitCode: number;
|
|
300
|
+
output: string;
|
|
301
|
+
passed: boolean;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Result of a single loop iteration. */
|
|
305
|
+
export interface LoopIterationResult {
|
|
306
|
+
iteration: number;
|
|
307
|
+
status: 'done' | 'failed' | 'backpressure-fail';
|
|
308
|
+
duration: number;
|
|
309
|
+
output: string;
|
|
310
|
+
backpressureResults?: BackpressureResult[];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Final report produced by the loop executor. */
|
|
314
|
+
export interface LoopRunReport {
|
|
315
|
+
name: string;
|
|
316
|
+
mode: 'loop';
|
|
317
|
+
startedAt: string;
|
|
318
|
+
completedAt: string;
|
|
319
|
+
duration: string;
|
|
320
|
+
totalIterations: number;
|
|
321
|
+
completedIterations: number;
|
|
322
|
+
stoppedReason: 'max-iterations' | 'plan-empty' | 'backpressure-fail' | 'user-abort' | 'error';
|
|
323
|
+
iterations: LoopIterationResult[];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Reporter interface for loop execution progress. */
|
|
327
|
+
export interface LoopReporter {
|
|
328
|
+
onIterationStart(iteration: number, maxIterations: number): void;
|
|
329
|
+
onIterationDone(iteration: number, result: LoopIterationResult): void;
|
|
330
|
+
onBackpressureStart(command: string): void;
|
|
331
|
+
onBackpressureResult(result: BackpressureResult): void;
|
|
332
|
+
onComplete(report: LoopRunReport): Promise<void>;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Executor for loop-mode run specs. */
|
|
336
|
+
export interface LoopExecutor {
|
|
337
|
+
run(): Promise<LoopRunReport>;
|
|
338
|
+
}
|
package/src/cli/update.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { readFile, appendFile, rename } from 'node:fs/promises'
|
|
3
4
|
import { readManifest, writeManifest } from './manifest.js'
|
|
4
5
|
import { multiselect, confirm, closePrompts, c } from './prompt.js'
|
|
5
6
|
import { isLegacyStack, migrateStackConfig, IDE_LABELS } from './types.js'
|
|
@@ -247,6 +248,9 @@ export default async function update({
|
|
|
247
248
|
}
|
|
248
249
|
}
|
|
249
250
|
|
|
251
|
+
// ── Migrate legacy log files ────────────────────────────────────
|
|
252
|
+
await migrateLegacyLogs(projectRoot)
|
|
253
|
+
|
|
250
254
|
// ── Update manifest ─────────────────────────────────────────────
|
|
251
255
|
if (hasVersionUpdate) manifest.version = pkg.version
|
|
252
256
|
manifest.ides = ides
|
|
@@ -322,6 +326,63 @@ export default async function update({
|
|
|
322
326
|
closePrompts()
|
|
323
327
|
}
|
|
324
328
|
|
|
329
|
+
async function migrateLegacyLogs(projectRoot: string): Promise<void> {
|
|
330
|
+
const logsDir = resolve(projectRoot, '.github', 'customizations', 'logs')
|
|
331
|
+
if (!existsSync(logsDir)) return
|
|
332
|
+
|
|
333
|
+
const typeMap: Record<string, string> = {
|
|
334
|
+
'sessions.ndjson': 'session',
|
|
335
|
+
'delegations.ndjson': 'delegation',
|
|
336
|
+
'reviews.ndjson': 'review',
|
|
337
|
+
'panels.ndjson': 'panel',
|
|
338
|
+
'disputes.ndjson': 'dispute',
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const eventsFile = resolve(logsDir, 'events.ndjson')
|
|
342
|
+
let totalMigrated = 0
|
|
343
|
+
|
|
344
|
+
for (const [filename, type] of Object.entries(typeMap)) {
|
|
345
|
+
const filePath = resolve(logsDir, filename)
|
|
346
|
+
if (!existsSync(filePath)) continue
|
|
347
|
+
|
|
348
|
+
let content: string
|
|
349
|
+
try {
|
|
350
|
+
content = await readFile(filePath, 'utf8')
|
|
351
|
+
} catch {
|
|
352
|
+
continue
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const lines = content.split('\n').filter((line) => line.trim() !== '')
|
|
356
|
+
if (lines.length === 0) continue
|
|
357
|
+
|
|
358
|
+
const migratedLines: string[] = []
|
|
359
|
+
for (const line of lines) {
|
|
360
|
+
try {
|
|
361
|
+
const record = JSON.parse(line) as Record<string, unknown>
|
|
362
|
+
if (!record['type']) {
|
|
363
|
+
record['type'] = type
|
|
364
|
+
}
|
|
365
|
+
migratedLines.push(JSON.stringify(record))
|
|
366
|
+
} catch {
|
|
367
|
+
console.warn(` ${c.yellow('⚠')} Skipping malformed JSON line in ${filename}`)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (migratedLines.length > 0) {
|
|
372
|
+
await appendFile(eventsFile, migratedLines.join('\n') + '\n', 'utf8')
|
|
373
|
+
totalMigrated += migratedLines.length
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
await rename(filePath, filePath + '.migrated')
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (totalMigrated > 0) {
|
|
380
|
+
console.log(
|
|
381
|
+
` ${c.green('✓')} Migrated ${c.bold(String(totalMigrated))} records from legacy log files to events.ndjson`
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
325
386
|
function sameSet(a: Set<string>, b: Set<string>): boolean {
|
|
326
387
|
if (a.size !== b.size) return false
|
|
327
388
|
for (const item of a) {
|