opencastle 0.27.3 → 0.29.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 +12 -3
- package/bin/cli.mjs +13 -5
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +2 -11
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +2 -1
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/convoy/export.d.ts +1 -3
- package/dist/cli/convoy/export.d.ts.map +1 -1
- package/dist/cli/convoy/export.js +9 -88
- package/dist/cli/convoy/export.js.map +1 -1
- package/dist/cli/convoy/export.test.js +7 -186
- package/dist/cli/convoy/export.test.js.map +1 -1
- package/dist/cli/convoy/issues.js +3 -3
- package/dist/cli/convoy/issues.js.map +1 -1
- package/dist/cli/convoy/issues.test.js +4 -3
- package/dist/cli/convoy/issues.test.js.map +1 -1
- package/dist/cli/convoy/pipeline.d.ts.map +1 -1
- package/dist/cli/convoy/pipeline.js +0 -21
- package/dist/cli/convoy/pipeline.js.map +1 -1
- package/dist/cli/convoy/pipeline.test.js +0 -21
- package/dist/cli/convoy/pipeline.test.js.map +1 -1
- package/dist/cli/dashboard.d.ts.map +1 -1
- package/dist/cli/dashboard.js +32 -8
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/destroy.d.ts.map +1 -1
- package/dist/cli/destroy.js +13 -0
- package/dist/cli/destroy.js.map +1 -1
- package/dist/cli/dispute.d.ts +3 -0
- package/dist/cli/dispute.d.ts.map +1 -0
- package/dist/cli/dispute.js +25 -0
- package/dist/cli/dispute.js.map +1 -0
- package/dist/cli/doctor.d.ts +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +14 -1
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/eject.d.ts.map +1 -1
- package/dist/cli/eject.js +14 -0
- package/dist/cli/eject.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +14 -0
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/log.d.ts +0 -11
- package/dist/cli/log.d.ts.map +1 -1
- package/dist/cli/log.js +2 -114
- package/dist/cli/log.js.map +1 -1
- package/dist/cli/pipeline.d.ts +3 -0
- package/dist/cli/pipeline.d.ts.map +1 -0
- package/dist/cli/pipeline.js +321 -0
- package/dist/cli/pipeline.js.map +1 -0
- package/dist/cli/plan.d.ts +37 -0
- package/dist/cli/plan.d.ts.map +1 -1
- package/dist/cli/plan.js +321 -161
- package/dist/cli/plan.js.map +1 -1
- package/dist/cli/run.js +2 -2
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +16 -0
- package/dist/cli/update.js.map +1 -1
- package/dist/cli/validate.d.ts +3 -0
- package/dist/cli/validate.d.ts.map +1 -0
- package/dist/cli/validate.js +60 -0
- package/dist/cli/validate.js.map +1 -0
- package/dist/cli/watch.d.ts.map +1 -1
- package/dist/cli/watch.js +1 -3
- package/dist/cli/watch.js.map +1 -1
- package/package.json +5 -4
- package/src/cli/convoy/engine.test.ts +2 -1
- package/src/cli/convoy/engine.ts +2 -5
- package/src/cli/convoy/export.test.ts +7 -224
- package/src/cli/convoy/export.ts +10 -106
- package/src/cli/convoy/issues.test.ts +3 -2
- package/src/cli/convoy/issues.ts +3 -3
- package/src/cli/convoy/pipeline.test.ts +0 -25
- package/src/cli/convoy/pipeline.ts +0 -19
- package/src/cli/dashboard.ts +33 -8
- package/src/cli/destroy.ts +15 -0
- package/src/cli/dispute.ts +28 -0
- package/src/cli/doctor.ts +16 -1
- package/src/cli/eject.ts +16 -0
- package/src/cli/init.ts +16 -0
- package/src/cli/log.ts +2 -120
- package/src/cli/pipeline.ts +362 -0
- package/src/cli/plan.ts +357 -153
- package/src/cli/run.ts +2 -2
- package/src/cli/update.ts +18 -0
- package/src/cli/validate.ts +65 -0
- package/src/cli/watch.ts +1 -3
- package/src/dashboard/dist/_astro/index.Je1YjU_y.css +1 -0
- package/src/dashboard/dist/data/convoy-list.json +54 -9
- package/src/dashboard/dist/data/convoys/demo-api-v2.json +177 -0
- package/src/dashboard/dist/data/convoys/demo-auth-revamp.json +239 -0
- package/src/dashboard/dist/data/convoys/demo-dashboard-ui.json +328 -0
- package/src/dashboard/dist/data/convoys/demo-data-pipeline.json +187 -0
- package/src/dashboard/dist/data/convoys/demo-deploy-ci.json +153 -0
- package/src/dashboard/dist/data/convoys/demo-docs-update.json +154 -0
- package/src/dashboard/dist/data/convoys/demo-perf-opt.json +227 -0
- package/src/dashboard/dist/data/events.ndjson +115 -0
- package/src/dashboard/dist/data/overall-stats.json +56 -13
- package/src/dashboard/dist/data/pipelines.ndjson +5285 -0
- package/src/dashboard/dist/index.html +165 -1392
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/public/data/convoy-list.json +54 -9
- package/src/dashboard/public/data/convoys/demo-api-v2.json +177 -0
- package/src/dashboard/public/data/convoys/demo-auth-revamp.json +239 -0
- package/src/dashboard/public/data/convoys/demo-dashboard-ui.json +328 -0
- package/src/dashboard/public/data/convoys/demo-data-pipeline.json +187 -0
- package/src/dashboard/public/data/convoys/demo-deploy-ci.json +153 -0
- package/src/dashboard/public/data/convoys/demo-docs-update.json +154 -0
- package/src/dashboard/public/data/convoys/demo-perf-opt.json +227 -0
- package/src/dashboard/public/data/events.ndjson +115 -0
- package/src/dashboard/public/data/overall-stats.json +56 -13
- package/src/dashboard/public/data/pipelines.ndjson +5285 -0
- package/src/dashboard/scripts/etl.test.ts +4 -62
- package/src/dashboard/scripts/etl.ts +11 -10
- package/src/dashboard/scripts/generate-demo-db.ts +482 -115
- package/src/dashboard/src/pages/index.astro +235 -1638
- package/src/dashboard/src/styles/dashboard.css +473 -7
- package/src/orchestrator/prompts/brainstorm.prompt.md +1 -0
- package/src/orchestrator/prompts/fix-convoy.prompt.md +79 -0
- package/src/orchestrator/prompts/generate-convoy.prompt.md +60 -58
- package/src/orchestrator/prompts/generate-prd.prompt.md +126 -0
- package/src/orchestrator/prompts/validate-convoy.prompt.md +89 -0
- package/src/orchestrator/prompts/validate-prd.prompt.md +83 -0
- package/dist/cli/convoy/log-merge.test.d.ts +0 -2
- package/dist/cli/convoy/log-merge.test.d.ts.map +0 -1
- package/dist/cli/convoy/log-merge.test.js +0 -147
- package/dist/cli/convoy/log-merge.test.js.map +0 -1
- package/src/cli/convoy/log-merge.test.ts +0 -179
- package/src/dashboard/dist/_astro/index.6L3_HsPT.css +0 -1
package/src/cli/log.ts
CHANGED
|
@@ -1,29 +1,18 @@
|
|
|
1
1
|
import { mkdir, appendFile, stat } from 'node:fs/promises'
|
|
2
|
-
import { readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
3
2
|
import { join, dirname } from 'node:path'
|
|
4
3
|
import type { CliContext } from './types.js'
|
|
5
4
|
|
|
6
5
|
const HELP = `
|
|
7
6
|
opencastle log [options]
|
|
8
|
-
opencastle log merge [--since <ISO-date>] [--until <ISO-date>] [--output <path>]
|
|
9
7
|
|
|
10
|
-
Append a structured event to the observability log (events.ndjson)
|
|
11
|
-
or merge per-convoy NDJSON files into a single file.
|
|
8
|
+
Append a structured event to the observability log (events.ndjson).
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
merge Merge all .opencastle/logs/convoys/*.ndjson into convoy-events.ndjson
|
|
15
|
-
|
|
16
|
-
Options (log append):
|
|
10
|
+
Options:
|
|
17
11
|
--type <type> Event type (required): session|delegation|review|panel|dispute
|
|
18
12
|
--<field> <value> Any field from the event schema (see documentation)
|
|
19
13
|
--logs-dir <path> Override the logs directory path
|
|
20
14
|
--help, -h Show this help
|
|
21
15
|
|
|
22
|
-
Options (merge):
|
|
23
|
-
--since <ISO-date> Only include records at or after this date
|
|
24
|
-
--until <ISO-date> Only include records at or before this date
|
|
25
|
-
--output <path> Output path (default: .opencastle/logs/convoy-events.ndjson)
|
|
26
|
-
|
|
27
16
|
Array fields (comma-separated): file_partition, lessons_added, discoveries, reviewing_agents
|
|
28
17
|
Boolean fields: escalated, weighted
|
|
29
18
|
Numeric fields: auto-detected from value
|
|
@@ -32,8 +21,6 @@ const HELP = `
|
|
|
32
21
|
opencastle log --type session --agent Developer --model claude-sonnet-4-6 --task "Fix bug" --outcome success
|
|
33
22
|
opencastle log --type delegation --session_id feat/prj-1 --agent Developer --tier fast --mechanism sub-agent --outcome success
|
|
34
23
|
opencastle log --type panel --panel_key auth-review --verdict pass --pass_count 3 --block_count 0
|
|
35
|
-
opencastle log merge --since 2026-01-01 --output /tmp/merged.ndjson
|
|
36
|
-
opencastle log merge
|
|
37
24
|
`
|
|
38
25
|
|
|
39
26
|
const VALID_TYPES = ['session', 'delegation', 'review', 'panel', 'dispute']
|
|
@@ -72,94 +59,6 @@ export async function resolveLogsDir(override?: string | null): Promise<string>
|
|
|
72
59
|
return join(process.cwd(), '.opencastle', 'logs')
|
|
73
60
|
}
|
|
74
61
|
|
|
75
|
-
/** Merge per-convoy NDJSON files into a single deduplicated, sorted file. */
|
|
76
|
-
export async function mergeConvoyLogs(options: {
|
|
77
|
-
since?: string
|
|
78
|
-
until?: string
|
|
79
|
-
output?: string
|
|
80
|
-
basePath?: string
|
|
81
|
-
}): Promise<{ merged: number; deduplicated: number; written: number }> {
|
|
82
|
-
const base = options.basePath ?? process.cwd()
|
|
83
|
-
const convoysDir = join(base, '.opencastle', 'logs', 'convoys')
|
|
84
|
-
|
|
85
|
-
let files: string[] = []
|
|
86
|
-
try {
|
|
87
|
-
files = readdirSync(convoysDir)
|
|
88
|
-
.filter(f => f.endsWith('.ndjson'))
|
|
89
|
-
.map(f => join(convoysDir, f))
|
|
90
|
-
} catch {
|
|
91
|
-
return { merged: 0, deduplicated: 0, written: 0 }
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (files.length === 0) {
|
|
95
|
-
return { merged: 0, deduplicated: 0, written: 0 }
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const allRecords: Array<Record<string, unknown>> = []
|
|
99
|
-
let totalRead = 0
|
|
100
|
-
|
|
101
|
-
for (const file of files) {
|
|
102
|
-
const content = readFileSync(file, 'utf8')
|
|
103
|
-
const lines = content.split('\n').filter(l => l.trim())
|
|
104
|
-
for (const line of lines) {
|
|
105
|
-
try {
|
|
106
|
-
allRecords.push(JSON.parse(line) as Record<string, unknown>)
|
|
107
|
-
totalRead++
|
|
108
|
-
} catch {
|
|
109
|
-
// skip malformed lines
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Deduplicate by _event_id — keep first occurrence
|
|
115
|
-
const seen = new Set<unknown>()
|
|
116
|
-
const unique: Array<Record<string, unknown>> = []
|
|
117
|
-
for (const record of allRecords) {
|
|
118
|
-
const id = record['_event_id']
|
|
119
|
-
if (id !== undefined) {
|
|
120
|
-
if (seen.has(id)) continue
|
|
121
|
-
seen.add(id)
|
|
122
|
-
}
|
|
123
|
-
unique.push(record)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const deduplicatedCount = totalRead - unique.length
|
|
127
|
-
|
|
128
|
-
// Filter by since/until
|
|
129
|
-
let filtered = unique
|
|
130
|
-
if (options.since) {
|
|
131
|
-
const since = options.since
|
|
132
|
-
filtered = filtered.filter(r => {
|
|
133
|
-
const ts = r['timestamp'] as string | undefined
|
|
134
|
-
return ts !== undefined && ts >= since
|
|
135
|
-
})
|
|
136
|
-
}
|
|
137
|
-
if (options.until) {
|
|
138
|
-
const until = options.until
|
|
139
|
-
filtered = filtered.filter(r => {
|
|
140
|
-
const ts = r['timestamp'] as string | undefined
|
|
141
|
-
return ts !== undefined && ts <= until
|
|
142
|
-
})
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Sort by timestamp ascending
|
|
146
|
-
filtered.sort((a, b) => {
|
|
147
|
-
const ta = (a['timestamp'] as string) ?? ''
|
|
148
|
-
const tb = (b['timestamp'] as string) ?? ''
|
|
149
|
-
return ta < tb ? -1 : ta > tb ? 1 : 0
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
if (filtered.length === 0) {
|
|
153
|
-
return { merged: totalRead, deduplicated: deduplicatedCount, written: 0 }
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const outputPath = options.output ?? join(base, '.opencastle', 'logs', 'convoy-events.ndjson')
|
|
157
|
-
mkdirSync(dirname(outputPath), { recursive: true })
|
|
158
|
-
writeFileSync(outputPath, filtered.map(r => JSON.stringify(r)).join('\n') + '\n', 'utf8')
|
|
159
|
-
|
|
160
|
-
return { merged: totalRead, deduplicated: deduplicatedCount, written: filtered.length }
|
|
161
|
-
}
|
|
162
|
-
|
|
163
62
|
/** Append a structured event record to events.ndjson. */
|
|
164
63
|
export async function appendEvent(
|
|
165
64
|
record: Record<string, unknown>,
|
|
@@ -178,23 +77,6 @@ export default async function log({ args }: CliContext): Promise<void> {
|
|
|
178
77
|
return
|
|
179
78
|
}
|
|
180
79
|
|
|
181
|
-
// merge subcommand
|
|
182
|
-
if (args[0] === 'merge') {
|
|
183
|
-
const mergeArgs = args.slice(1)
|
|
184
|
-
let since: string | undefined
|
|
185
|
-
let until: string | undefined
|
|
186
|
-
let output: string | undefined
|
|
187
|
-
for (let i = 0; i < mergeArgs.length; i++) {
|
|
188
|
-
const a = mergeArgs[i]
|
|
189
|
-
if (a === '--since' && i + 1 < mergeArgs.length) { since = mergeArgs[++i]; continue }
|
|
190
|
-
if (a === '--until' && i + 1 < mergeArgs.length) { until = mergeArgs[++i]; continue }
|
|
191
|
-
if (a === '--output' && i + 1 < mergeArgs.length) { output = mergeArgs[++i]; continue }
|
|
192
|
-
}
|
|
193
|
-
const result = await mergeConvoyLogs({ since, until, output })
|
|
194
|
-
console.log(` Merged: ${result.merged} records, Deduplicated: ${result.deduplicated}, Written: ${result.written}`)
|
|
195
|
-
return
|
|
196
|
-
}
|
|
197
|
-
|
|
198
80
|
let type: string | null = null
|
|
199
81
|
let logsDir: string | null = null
|
|
200
82
|
const fields: Record<string, unknown> = {}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import { c, confirm, closePrompts } from './prompt.js'
|
|
5
|
+
import { runPromptStep } from './plan.js'
|
|
6
|
+
import type { CliContext } from './types.js'
|
|
7
|
+
|
|
8
|
+
const HELP = `
|
|
9
|
+
opencastle pipeline [options]
|
|
10
|
+
|
|
11
|
+
Run the full convoy generation pipeline from a feature prompt:
|
|
12
|
+
|
|
13
|
+
Step 1 — Generate PRD (generate-prd)
|
|
14
|
+
Step 2 — Validate PRD (validate-prd)
|
|
15
|
+
Step 3 — Generate convoy spec (generate-convoy, using PRD as BDO)
|
|
16
|
+
Step 4 — Validate convoy spec (validate-convoy)
|
|
17
|
+
Step 5 — Fix convoy spec (fix-convoy, up to 2 retries if invalid)
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--text, -t <text> Feature prompt text (required, unless --prd is set)
|
|
21
|
+
--prd <path> Skip step 1 — use an existing PRD file
|
|
22
|
+
--output-prd <path> Override path for the generated PRD
|
|
23
|
+
--output-spec <path> Override path for the generated convoy spec
|
|
24
|
+
--adapter, -a <name> Override agent runtime adapter
|
|
25
|
+
--verbose Show full agent output for each step
|
|
26
|
+
--dry-run Generate and print the PRD prompt only, then stop
|
|
27
|
+
--skip-validation Skip steps 2 and 4 (PRD and convoy validation)
|
|
28
|
+
--help, -h Show this help
|
|
29
|
+
`
|
|
30
|
+
|
|
31
|
+
interface PipelineOptions {
|
|
32
|
+
text: string | null
|
|
33
|
+
prd: string | null
|
|
34
|
+
outputPrd: string | null
|
|
35
|
+
outputSpec: string | null
|
|
36
|
+
adapter: string | null
|
|
37
|
+
verbose: boolean
|
|
38
|
+
dryRun: boolean
|
|
39
|
+
skipValidation: boolean
|
|
40
|
+
help: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseArgs(args: string[]): PipelineOptions {
|
|
44
|
+
const opts: PipelineOptions = {
|
|
45
|
+
text: null,
|
|
46
|
+
prd: null,
|
|
47
|
+
outputPrd: null,
|
|
48
|
+
outputSpec: null,
|
|
49
|
+
adapter: null,
|
|
50
|
+
verbose: false,
|
|
51
|
+
dryRun: false,
|
|
52
|
+
skipValidation: false,
|
|
53
|
+
help: false,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < args.length; i++) {
|
|
57
|
+
const arg = args[i]
|
|
58
|
+
switch (arg) {
|
|
59
|
+
case '--help':
|
|
60
|
+
case '-h':
|
|
61
|
+
opts.help = true
|
|
62
|
+
break
|
|
63
|
+
case '--text':
|
|
64
|
+
case '-t':
|
|
65
|
+
if (i + 1 >= args.length) { console.error(' ✗ --text requires a value'); process.exit(1) }
|
|
66
|
+
opts.text = args[++i]
|
|
67
|
+
break
|
|
68
|
+
case '--prd':
|
|
69
|
+
if (i + 1 >= args.length) { console.error(' ✗ --prd requires a path'); process.exit(1) }
|
|
70
|
+
opts.prd = args[++i]
|
|
71
|
+
break
|
|
72
|
+
case '--output-prd':
|
|
73
|
+
if (i + 1 >= args.length) { console.error(' ✗ --output-prd requires a path'); process.exit(1) }
|
|
74
|
+
opts.outputPrd = args[++i]
|
|
75
|
+
break
|
|
76
|
+
case '--output-spec':
|
|
77
|
+
if (i + 1 >= args.length) { console.error(' ✗ --output-spec requires a path'); process.exit(1) }
|
|
78
|
+
opts.outputSpec = args[++i]
|
|
79
|
+
break
|
|
80
|
+
case '--adapter':
|
|
81
|
+
case '-a':
|
|
82
|
+
if (i + 1 >= args.length) { console.error(' ✗ --adapter requires a name'); process.exit(1) }
|
|
83
|
+
opts.adapter = args[++i]
|
|
84
|
+
break
|
|
85
|
+
case '--verbose':
|
|
86
|
+
opts.verbose = true
|
|
87
|
+
break
|
|
88
|
+
case '--dry-run':
|
|
89
|
+
case '--dryRun':
|
|
90
|
+
opts.dryRun = true
|
|
91
|
+
break
|
|
92
|
+
case '--skip-validation':
|
|
93
|
+
opts.skipValidation = true
|
|
94
|
+
break
|
|
95
|
+
default:
|
|
96
|
+
console.error(` ✗ Unknown option: ${arg}`)
|
|
97
|
+
console.log(HELP)
|
|
98
|
+
process.exit(1)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return opts
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function relPath(abs: string): string {
|
|
106
|
+
return abs.startsWith(process.cwd()) ? abs.slice(process.cwd().length + 1) : abs
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function stepLabel(n: number, total: number, name: string): string {
|
|
110
|
+
return c.bold(c.cyan(` [${n}/${total}] ${name}`))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default async function pipeline({ args, pkgRoot }: CliContext): Promise<void> {
|
|
114
|
+
const opts = parseArgs(args)
|
|
115
|
+
|
|
116
|
+
if (opts.help) {
|
|
117
|
+
console.log(HELP)
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!opts.text && !opts.prd) {
|
|
122
|
+
console.error(` ✗ Either --text or --prd is required.`)
|
|
123
|
+
console.log(HELP)
|
|
124
|
+
process.exit(1)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (opts.text && opts.prd) {
|
|
128
|
+
console.error(` ✗ --text and --prd are mutually exclusive.`)
|
|
129
|
+
process.exit(1)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (opts.prd) {
|
|
133
|
+
const resolvedPrd = resolve(process.cwd(), opts.prd)
|
|
134
|
+
if (!existsSync(resolvedPrd)) {
|
|
135
|
+
console.error(` ✗ PRD file not found: ${opts.prd}`)
|
|
136
|
+
process.exit(1)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const totalSteps = opts.skipValidation ? 3 : 5
|
|
141
|
+
const sharedOpts = {
|
|
142
|
+
adapterName: opts.adapter ?? undefined,
|
|
143
|
+
verbose: opts.verbose,
|
|
144
|
+
pkgRoot,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(c.bold('\n opencastle pipeline\n'))
|
|
148
|
+
|
|
149
|
+
// ── Step 1: Generate PRD ──────────────────────────────────────────────────
|
|
150
|
+
let prdPath: string
|
|
151
|
+
|
|
152
|
+
if (opts.prd) {
|
|
153
|
+
prdPath = resolve(process.cwd(), opts.prd)
|
|
154
|
+
console.log(c.dim(` [−] Skipping PRD generation — using: ${relPath(prdPath)}`))
|
|
155
|
+
} else {
|
|
156
|
+
console.log(stepLabel(1, totalSteps, 'Generating PRD…'))
|
|
157
|
+
|
|
158
|
+
let result
|
|
159
|
+
try {
|
|
160
|
+
result = await runPromptStep({
|
|
161
|
+
...sharedOpts,
|
|
162
|
+
template: 'generate-prd',
|
|
163
|
+
goalText: opts.text!,
|
|
164
|
+
outputPath: opts.outputPrd ? resolve(process.cwd(), opts.outputPrd) : undefined,
|
|
165
|
+
dryRun: opts.dryRun,
|
|
166
|
+
})
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.error(`\n ✗ Step 1 failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
169
|
+
process.exit(1)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (opts.dryRun) {
|
|
173
|
+
console.log(c.dim('\n [dry-run] Stopping after step 1. Remove --dry-run to run the full pipeline.'))
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
prdPath = result.outputPath!
|
|
178
|
+
console.log(c.green(` ✓ PRD written to ${relPath(prdPath)}\n`))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Step 2: Validate PRD ──────────────────────────────────────────────────
|
|
182
|
+
if (!opts.skipValidation) {
|
|
183
|
+
console.log(stepLabel(2, totalSteps, 'Validating PRD…'))
|
|
184
|
+
|
|
185
|
+
const prdContent = await readFile(prdPath, 'utf8')
|
|
186
|
+
let result
|
|
187
|
+
try {
|
|
188
|
+
result = await runPromptStep({
|
|
189
|
+
...sharedOpts,
|
|
190
|
+
template: 'validate-prd',
|
|
191
|
+
goalText: prdContent,
|
|
192
|
+
})
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.error(`\n ✗ Step 2 failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
195
|
+
process.exit(1)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!result.isValid) {
|
|
199
|
+
console.log(c.red(` ✗ PRD validation failed.\n`))
|
|
200
|
+
console.log(result.errors ?? result.rawOutput)
|
|
201
|
+
console.log(
|
|
202
|
+
c.dim(`\n Fix the PRD at ${relPath(prdPath)} and re-run with:\n`) +
|
|
203
|
+
` opencastle pipeline --prd ${relPath(prdPath)}${opts.adapter ? ` --adapter ${opts.adapter}` : ''}\n`
|
|
204
|
+
)
|
|
205
|
+
process.exit(1)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log(c.green(` ✓ PRD is valid\n`))
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Step 3: Generate convoy spec ──────────────────────────────────────────
|
|
212
|
+
const genStep = opts.skipValidation ? 2 : 3
|
|
213
|
+
console.log(stepLabel(genStep, totalSteps, 'Generating convoy spec…'))
|
|
214
|
+
|
|
215
|
+
let specPath: string
|
|
216
|
+
try {
|
|
217
|
+
const result = await runPromptStep({
|
|
218
|
+
...sharedOpts,
|
|
219
|
+
template: 'generate-convoy',
|
|
220
|
+
filePath: prdPath,
|
|
221
|
+
outputPath: opts.outputSpec ? resolve(process.cwd(), opts.outputSpec) : undefined,
|
|
222
|
+
})
|
|
223
|
+
specPath = result.outputPath!
|
|
224
|
+
} catch (err) {
|
|
225
|
+
console.error(`\n ✗ Step ${genStep} failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
226
|
+
process.exit(1)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log(c.green(` ✓ Convoy spec written to ${relPath(specPath)}\n`))
|
|
230
|
+
|
|
231
|
+
if (opts.skipValidation) {
|
|
232
|
+
await printFinalSummary(prdPath, specPath, opts, pkgRoot)
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Step 4: Validate convoy spec ──────────────────────────────────────────
|
|
237
|
+
console.log(stepLabel(4, totalSteps, 'Validating convoy spec…'))
|
|
238
|
+
|
|
239
|
+
const specContent = await readFile(specPath, 'utf8')
|
|
240
|
+
let validationErrors: string
|
|
241
|
+
|
|
242
|
+
{
|
|
243
|
+
let result
|
|
244
|
+
try {
|
|
245
|
+
result = await runPromptStep({
|
|
246
|
+
...sharedOpts,
|
|
247
|
+
template: 'validate-convoy',
|
|
248
|
+
goalText: specContent,
|
|
249
|
+
})
|
|
250
|
+
} catch (err) {
|
|
251
|
+
console.error(`\n ✗ Step 4 failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
252
|
+
process.exit(1)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (result.isValid) {
|
|
256
|
+
console.log(c.green(` ✓ Convoy spec is valid\n`))
|
|
257
|
+
await printFinalSummary(prdPath, specPath, opts, pkgRoot)
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
validationErrors = result.errors ?? result.rawOutput
|
|
262
|
+
console.log(c.yellow(` ⚠ Spec has validation issues — attempting auto-fix…\n`))
|
|
263
|
+
console.log(c.dim(validationErrors))
|
|
264
|
+
console.log()
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Step 5: Fix convoy spec (up to 2 retries) ─────────────────────────────
|
|
268
|
+
const MAX_FIX_RETRIES = 2
|
|
269
|
+
let fixedSpecContent = specContent
|
|
270
|
+
|
|
271
|
+
for (let attempt = 1; attempt <= MAX_FIX_RETRIES; attempt++) {
|
|
272
|
+
const label = `Fix attempt ${attempt}/${MAX_FIX_RETRIES}…`
|
|
273
|
+
console.log(stepLabel(5, totalSteps, label))
|
|
274
|
+
|
|
275
|
+
let fixResult
|
|
276
|
+
try {
|
|
277
|
+
fixResult = await runPromptStep({
|
|
278
|
+
...sharedOpts,
|
|
279
|
+
template: 'fix-convoy',
|
|
280
|
+
goalText: fixedSpecContent,
|
|
281
|
+
contextText: validationErrors,
|
|
282
|
+
outputPath: specPath, // overwrite in place
|
|
283
|
+
})
|
|
284
|
+
} catch (err) {
|
|
285
|
+
console.error(`\n ✗ Step 5 (attempt ${attempt}) failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
286
|
+
process.exit(1)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
console.log(c.dim(` Re-validating after fix…`))
|
|
290
|
+
|
|
291
|
+
// Read the newly written spec
|
|
292
|
+
fixedSpecContent = await readFile(specPath, 'utf8')
|
|
293
|
+
|
|
294
|
+
let revalidation
|
|
295
|
+
try {
|
|
296
|
+
revalidation = await runPromptStep({
|
|
297
|
+
...sharedOpts,
|
|
298
|
+
template: 'validate-convoy',
|
|
299
|
+
goalText: fixedSpecContent,
|
|
300
|
+
})
|
|
301
|
+
} catch (err) {
|
|
302
|
+
console.error(`\n ✗ Re-validation failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
303
|
+
process.exit(1)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (revalidation.isValid) {
|
|
307
|
+
console.log(c.green(` ✓ Spec fixed and validated\n`))
|
|
308
|
+
await printFinalSummary(prdPath, specPath, opts, pkgRoot)
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
validationErrors = revalidation.errors ?? revalidation.rawOutput
|
|
313
|
+
|
|
314
|
+
if (attempt < MAX_FIX_RETRIES) {
|
|
315
|
+
console.log(c.yellow(` ⚠ Still has issues after fix attempt ${attempt} — retrying…\n`))
|
|
316
|
+
console.log(c.dim(validationErrors))
|
|
317
|
+
console.log()
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// All retries exhausted
|
|
322
|
+
console.log(c.red(`\n ✗ Could not auto-fix the convoy spec after ${MAX_FIX_RETRIES} attempts.\n`))
|
|
323
|
+
console.log(` Remaining issues:\n`)
|
|
324
|
+
console.log(validationErrors)
|
|
325
|
+
console.log(
|
|
326
|
+
c.dim(`\n The spec has been saved to ${relPath(specPath)} with the best available fixes.\n`) +
|
|
327
|
+
c.dim(` Review the remaining issues above and edit the file manually, then validate with:\n`) +
|
|
328
|
+
` opencastle plan --file ${relPath(specPath)} --template validate-convoy\n`
|
|
329
|
+
)
|
|
330
|
+
process.exit(1)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function printFinalSummary(
|
|
334
|
+
prdPath: string,
|
|
335
|
+
specPath: string,
|
|
336
|
+
opts: PipelineOptions,
|
|
337
|
+
pkgRoot: string,
|
|
338
|
+
): Promise<void> {
|
|
339
|
+
const prd = relPath(prdPath)
|
|
340
|
+
const spec = relPath(specPath)
|
|
341
|
+
console.log(c.bold(c.green(' Pipeline complete!\n')))
|
|
342
|
+
console.log(` PRD: ${prd}`)
|
|
343
|
+
console.log(` Convoy spec: ${spec}\n`)
|
|
344
|
+
console.log(
|
|
345
|
+
` ${c.dim('Preview:')} npx opencastle run -f ${spec} --dry-run\n` +
|
|
346
|
+
` ${c.dim('Execute:')} npx opencastle run -f ${spec}\n`
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const shouldRun = await confirm('Run the convoy now?', true)
|
|
351
|
+
if (shouldRun) {
|
|
352
|
+
closePrompts()
|
|
353
|
+
const runModule = await import('./run.js')
|
|
354
|
+
const runArgs = ['-f', specPath]
|
|
355
|
+
if (opts.adapter) runArgs.push('-a', opts.adapter)
|
|
356
|
+
if (opts.verbose) runArgs.push('--verbose')
|
|
357
|
+
await runModule.default({ args: runArgs, pkgRoot })
|
|
358
|
+
}
|
|
359
|
+
} finally {
|
|
360
|
+
closePrompts()
|
|
361
|
+
}
|
|
362
|
+
}
|