opencastle 0.27.3 → 0.28.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/bin/cli.mjs +13 -5
- package/dist/cli/convoy/engine.js +2 -2
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +1 -1
- package/dist/cli/convoy/engine.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/pipeline.d.ts +3 -0
- package/dist/cli/pipeline.d.ts.map +1 -0
- package/dist/cli/pipeline.js +305 -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/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/package.json +5 -4
- package/src/cli/convoy/engine.test.ts +1 -1
- package/src/cli/convoy/engine.ts +2 -2
- package/src/cli/convoy/issues.test.ts +3 -2
- package/src/cli/convoy/issues.ts +3 -3
- package/src/cli/pipeline.ts +343 -0
- package/src/cli/plan.ts +357 -153
- package/src/cli/validate.ts +65 -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 +39 -16
- 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.ts +24 -3
- package/src/dashboard/scripts/generate-demo-db.ts +482 -115
- package/src/dashboard/src/pages/index.astro +46 -23
- package/src/orchestrator/prompts/fix-convoy.prompt.md +79 -0
- package/src/orchestrator/prompts/generate-convoy.prompt.md +53 -58
- package/src/orchestrator/prompts/generate-prd.prompt.md +120 -0
- package/src/orchestrator/prompts/validate-convoy.prompt.md +89 -0
- package/src/orchestrator/prompts/validate-prd.prompt.md +83 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { parseYaml, validateSpec } from './run/schema.js';
|
|
4
|
+
import { c } from './prompt.js';
|
|
5
|
+
const HELP = `
|
|
6
|
+
opencastle validate <file> [options]
|
|
7
|
+
|
|
8
|
+
Validate a convoy YAML spec file without executing it.
|
|
9
|
+
|
|
10
|
+
Arguments:
|
|
11
|
+
<file> Path to the convoy YAML spec file
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--help, -h Show this help
|
|
15
|
+
`;
|
|
16
|
+
export default async function validate({ args }) {
|
|
17
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
18
|
+
console.log(HELP);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const filePath = args.find(a => !a.startsWith('--'));
|
|
22
|
+
if (!filePath) {
|
|
23
|
+
console.error(' ✗ A file path is required\n Usage: opencastle validate <file>');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const absPath = resolve(process.cwd(), filePath);
|
|
27
|
+
if (!existsSync(absPath)) {
|
|
28
|
+
console.error(` ✗ File not found: ${absPath}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
let text;
|
|
32
|
+
try {
|
|
33
|
+
text = readFileSync(absPath, 'utf8');
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
console.error(` ✗ Could not read file: ${err.message}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = parseYaml(text);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
console.error(` ✗ YAML parse error: ${err.message}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const result = validateSpec(parsed);
|
|
48
|
+
if (result.valid) {
|
|
49
|
+
console.log(` ${c.green('✓')} ${filePath} is valid`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.error(` ${c.red('✗')} ${filePath} has ${result.errors.length} error${result.errors.length === 1 ? '' : 's'}:\n`);
|
|
53
|
+
for (const err of result.errors) {
|
|
54
|
+
console.error(` • ${err}`);
|
|
55
|
+
}
|
|
56
|
+
console.error();
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/cli/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACzD,OAAO,EAAE,CAAC,EAAE,MAAM,aAAa,CAAA;AAG/B,MAAM,IAAI,GAAG;;;;;;;;;;CAUZ,CAAA;AAED,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,QAAQ,CAAC,EAAE,IAAI,EAAc;IACzD,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACnD,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACjB,OAAM;IACR,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAA;IACpD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,CAAC,KAAK,CAAC,kEAAkE,CAAC,CAAA;QACjF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAED,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,CAAC,CAAA;IAChD,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,OAAO,CAAC,KAAK,CAAC,uBAAuB,OAAO,EAAE,CAAC,CAAA;QAC/C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAED,IAAI,IAAY,CAAA;IAChB,IAAI,CAAC;QACH,IAAI,GAAG,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;IACtC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,4BAA6B,GAAa,CAAC,OAAO,EAAE,CAAC,CAAA;QACnE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAED,IAAI,MAAe,CAAA;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAA;IAC1B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,yBAA0B,GAAa,CAAC,OAAO,EAAE,CAAC,CAAA;QAChE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAED,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAAA;IAEnC,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,QAAQ,WAAW,CAAC,CAAA;IACvD,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,QAAQ,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAA;QACzH,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAChC,OAAO,CAAC,KAAK,CAAC,SAAS,GAAG,EAAE,CAAC,CAAA;QAC/B,CAAC;QACD,OAAO,CAAC,KAAK,EAAE,CAAA;QACf,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencastle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Multi-agent orchestration framework for AI coding assistants",
|
|
6
6
|
"bin": {
|
|
@@ -34,10 +34,11 @@
|
|
|
34
34
|
"license": "MIT",
|
|
35
35
|
"author": "Filip Mares",
|
|
36
36
|
"scripts": {
|
|
37
|
+
"build": "npm run cli:build && npm run dashboard:build",
|
|
37
38
|
"cli:build": "tsc",
|
|
38
39
|
"test": "vitest run",
|
|
39
|
-
"dashboard:generate-demo-db": "tsx src/dashboard/scripts/generate-demo-db.ts --out .opencastle/convoy-demo.db",
|
|
40
|
-
"dashboard:etl": "tsx src/dashboard/scripts/etl.ts --db .opencastle/convoy-demo.db --out src/dashboard/public/data",
|
|
40
|
+
"dashboard:generate-demo-db": "tsx src/dashboard/scripts/generate-demo-db.ts --out .opencastle/convoy-demo.db --events-out .opencastle/convoy-demo.events.ndjson",
|
|
41
|
+
"dashboard:etl": "tsx src/dashboard/scripts/etl.ts --db .opencastle/convoy-demo.db --out src/dashboard/public/data --events .opencastle/convoy-demo.events.ndjson",
|
|
41
42
|
"dashboard:test": "tsx src/dashboard/scripts/integration-test.ts",
|
|
42
43
|
"dashboard:dev": "astro dev --root src/dashboard --port 4300",
|
|
43
44
|
"dashboard:build": "astro build --root src/dashboard",
|
|
@@ -45,7 +46,7 @@
|
|
|
45
46
|
"website:dev": "astro dev --root website",
|
|
46
47
|
"website:build": "astro build --root website",
|
|
47
48
|
"website:preview": "astro preview --root website",
|
|
48
|
-
"prepublishOnly": "npm run
|
|
49
|
+
"prepublishOnly": "npm run build"
|
|
49
50
|
},
|
|
50
51
|
"engines": {
|
|
51
52
|
"node": ">=22.5.0"
|
|
@@ -3407,7 +3407,7 @@ describe('secret scan in DLQ/dispute markdown write', () => {
|
|
|
3407
3407
|
|
|
3408
3408
|
it('emits secret_leak_prevented when dispute markdown write detects secrets', async () => {
|
|
3409
3409
|
const scanSpy = vi.spyOn(gates, 'scanForSecrets').mockImplementation((content: string, filePath = '') => {
|
|
3410
|
-
if (filePath === 'DISPUTES.md') {
|
|
3410
|
+
if (filePath === '.opencastle/DISPUTES.md') {
|
|
3411
3411
|
return {
|
|
3412
3412
|
clean: false,
|
|
3413
3413
|
findings: [{ pattern: 'Mock Secret', file: filePath, line: 1, snippet: content.slice(0, 20) }],
|
package/src/cli/convoy/engine.ts
CHANGED
|
@@ -426,7 +426,7 @@ function writeDisputeToMarkdown(
|
|
|
426
426
|
panelResults: ReviewResult[],
|
|
427
427
|
events?: ConvoyEventEmitter | null,
|
|
428
428
|
): void {
|
|
429
|
-
const mdPath = join(resolve(process.cwd()), 'DISPUTES.md')
|
|
429
|
+
const mdPath = join(resolve(process.cwd()), '.opencastle', 'DISPUTES.md')
|
|
430
430
|
const marker = `<!-- dispute:${disputeId} -->`
|
|
431
431
|
|
|
432
432
|
try {
|
|
@@ -443,7 +443,7 @@ function writeDisputeToMarkdown(
|
|
|
443
443
|
|
|
444
444
|
const entry = `\n${marker}\n## Dispute: ${task.id}\n\n| Field | Value |\n|-------|-------|\n| Convoy | ${convoyId} |\n| Task | ${task.id} |\n| Date | ${new Date().toISOString()} |\n| Panel attempts | ${task.panel_attempts + 1} |\n| Agent | ${task.agent} |\n| Status | Open |\n\n**Blocking reasons:**\n\n${blockingReasons}\n`
|
|
445
445
|
|
|
446
|
-
const scanResult = scanForSecrets(entry, 'DISPUTES.md')
|
|
446
|
+
const scanResult = scanForSecrets(entry, '.opencastle/DISPUTES.md')
|
|
447
447
|
if (!scanResult.clean) {
|
|
448
448
|
if (events) {
|
|
449
449
|
events.emit(
|
|
@@ -8,14 +8,15 @@ vi.mock('./gates.js', () => ({
|
|
|
8
8
|
scanForSecrets: vi.fn(() => ({ clean: true, findings: [] })),
|
|
9
9
|
}))
|
|
10
10
|
|
|
11
|
-
const DISCOVERED_REL = 'DISCOVERED-ISSUES.md'
|
|
12
|
-
const KNOWN_REL = 'KNOWN-ISSUES.md'
|
|
11
|
+
const DISCOVERED_REL = '.opencastle/DISCOVERED-ISSUES.md'
|
|
12
|
+
const KNOWN_REL = '.opencastle/KNOWN-ISSUES.md'
|
|
13
13
|
|
|
14
14
|
const DISCOVERED_HEADER = '# Discovered Issues\n\n'
|
|
15
15
|
const KNOWN_HEADER = '# Known Issues\n\n'
|
|
16
16
|
|
|
17
17
|
function makeBase(): string {
|
|
18
18
|
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'issues-test-')))
|
|
19
|
+
mkdirSync(join(dir, '.opencastle'), { recursive: true })
|
|
19
20
|
return dir
|
|
20
21
|
}
|
|
21
22
|
|
package/src/cli/convoy/issues.ts
CHANGED
|
@@ -3,12 +3,12 @@ import { join } from 'node:path'
|
|
|
3
3
|
import { scanForSecrets } from './gates.js'
|
|
4
4
|
import type { ConvoyEventEmitter } from './events.js'
|
|
5
5
|
|
|
6
|
-
const DISCOVERED_PATH = 'DISCOVERED-ISSUES.md'
|
|
7
|
-
const KNOWN_PATH = 'KNOWN-ISSUES.md'
|
|
6
|
+
const DISCOVERED_PATH = '.opencastle/DISCOVERED-ISSUES.md'
|
|
7
|
+
const KNOWN_PATH = '.opencastle/KNOWN-ISSUES.md'
|
|
8
8
|
|
|
9
9
|
const INJECT_INSTRUCTION =
|
|
10
10
|
'IMPORTANT: After completing your task, if you notice any pre-existing bugs or issues ' +
|
|
11
|
-
'unrelated to your task, append them to DISCOVERED-ISSUES.md in the format:\n\n' +
|
|
11
|
+
'unrelated to your task, append them to .opencastle/DISCOVERED-ISSUES.md in the format:\n\n' +
|
|
12
12
|
'### ISSUE: [title]\n' +
|
|
13
13
|
'- **File:** [filepath]\n' +
|
|
14
14
|
'- **Description:** [description]\n' +
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import { c } 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
|
+
printFinalSummary(prdPath, specPath)
|
|
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
|
+
printFinalSummary(prdPath, specPath)
|
|
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
|
+
printFinalSummary(prdPath, specPath)
|
|
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
|
+
function printFinalSummary(prdPath: string, specPath: string): void {
|
|
334
|
+
const prd = relPath(prdPath)
|
|
335
|
+
const spec = relPath(specPath)
|
|
336
|
+
console.log(c.bold(c.green(' Pipeline complete!\n')))
|
|
337
|
+
console.log(` PRD: ${prd}`)
|
|
338
|
+
console.log(` Convoy spec: ${spec}\n`)
|
|
339
|
+
console.log(
|
|
340
|
+
` ${c.dim('Preview:')} npx opencastle run -f ${spec} --dry-run\n` +
|
|
341
|
+
` ${c.dim('Execute:')} npx opencastle run -f ${spec}\n`
|
|
342
|
+
)
|
|
343
|
+
}
|