opencastle 0.28.0 → 0.30.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.
Files changed (94) hide show
  1. package/README.md +12 -3
  2. package/dist/cli/convoy/engine.d.ts.map +1 -1
  3. package/dist/cli/convoy/engine.js +1 -10
  4. package/dist/cli/convoy/engine.js.map +1 -1
  5. package/dist/cli/convoy/engine.test.js +1 -0
  6. package/dist/cli/convoy/engine.test.js.map +1 -1
  7. package/dist/cli/convoy/export.d.ts +1 -3
  8. package/dist/cli/convoy/export.d.ts.map +1 -1
  9. package/dist/cli/convoy/export.js +9 -88
  10. package/dist/cli/convoy/export.js.map +1 -1
  11. package/dist/cli/convoy/export.test.js +7 -186
  12. package/dist/cli/convoy/export.test.js.map +1 -1
  13. package/dist/cli/convoy/pipeline.d.ts.map +1 -1
  14. package/dist/cli/convoy/pipeline.js +0 -21
  15. package/dist/cli/convoy/pipeline.js.map +1 -1
  16. package/dist/cli/convoy/pipeline.test.js +0 -21
  17. package/dist/cli/convoy/pipeline.test.js.map +1 -1
  18. package/dist/cli/dashboard.d.ts.map +1 -1
  19. package/dist/cli/dashboard.js +32 -8
  20. package/dist/cli/dashboard.js.map +1 -1
  21. package/dist/cli/destroy.d.ts.map +1 -1
  22. package/dist/cli/destroy.js +13 -0
  23. package/dist/cli/destroy.js.map +1 -1
  24. package/dist/cli/dispute.d.ts +3 -0
  25. package/dist/cli/dispute.d.ts.map +1 -0
  26. package/dist/cli/dispute.js +25 -0
  27. package/dist/cli/dispute.js.map +1 -0
  28. package/dist/cli/doctor.d.ts +1 -1
  29. package/dist/cli/doctor.d.ts.map +1 -1
  30. package/dist/cli/doctor.js +14 -1
  31. package/dist/cli/doctor.js.map +1 -1
  32. package/dist/cli/eject.d.ts.map +1 -1
  33. package/dist/cli/eject.js +14 -0
  34. package/dist/cli/eject.js.map +1 -1
  35. package/dist/cli/init.d.ts.map +1 -1
  36. package/dist/cli/init.js +14 -0
  37. package/dist/cli/init.js.map +1 -1
  38. package/dist/cli/log.d.ts +0 -11
  39. package/dist/cli/log.d.ts.map +1 -1
  40. package/dist/cli/log.js +2 -114
  41. package/dist/cli/log.js.map +1 -1
  42. package/dist/cli/pipeline.d.ts +17 -0
  43. package/dist/cli/pipeline.d.ts.map +1 -1
  44. package/dist/cli/pipeline.js +259 -24
  45. package/dist/cli/pipeline.js.map +1 -1
  46. package/dist/cli/pipeline.test.d.ts +2 -0
  47. package/dist/cli/pipeline.test.d.ts.map +1 -0
  48. package/dist/cli/pipeline.test.js +178 -0
  49. package/dist/cli/pipeline.test.js.map +1 -0
  50. package/dist/cli/run.js +2 -2
  51. package/dist/cli/run.js.map +1 -1
  52. package/dist/cli/update.d.ts.map +1 -1
  53. package/dist/cli/update.js +16 -0
  54. package/dist/cli/update.js.map +1 -1
  55. package/dist/cli/watch.d.ts.map +1 -1
  56. package/dist/cli/watch.js +1 -3
  57. package/dist/cli/watch.js.map +1 -1
  58. package/package.json +1 -1
  59. package/src/cli/convoy/engine.test.ts +1 -0
  60. package/src/cli/convoy/engine.ts +1 -4
  61. package/src/cli/convoy/export.test.ts +7 -224
  62. package/src/cli/convoy/export.ts +10 -106
  63. package/src/cli/convoy/pipeline.test.ts +0 -25
  64. package/src/cli/convoy/pipeline.ts +0 -19
  65. package/src/cli/dashboard.ts +33 -8
  66. package/src/cli/destroy.ts +15 -0
  67. package/src/cli/dispute.ts +28 -0
  68. package/src/cli/doctor.ts +16 -1
  69. package/src/cli/eject.ts +16 -0
  70. package/src/cli/init.ts +16 -0
  71. package/src/cli/log.ts +2 -120
  72. package/src/cli/pipeline.test.ts +191 -0
  73. package/src/cli/pipeline.ts +326 -26
  74. package/src/cli/run.ts +2 -2
  75. package/src/cli/update.ts +18 -0
  76. package/src/cli/watch.ts +1 -3
  77. package/src/dashboard/dist/_astro/index.Je1YjU_y.css +1 -0
  78. package/src/dashboard/dist/index.html +537 -1394
  79. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  80. package/src/dashboard/scripts/etl.test.ts +4 -62
  81. package/src/dashboard/scripts/etl.ts +13 -33
  82. package/src/dashboard/src/pages/index.astro +684 -1624
  83. package/src/dashboard/src/styles/dashboard.css +473 -7
  84. package/src/orchestrator/agents/team-lead.agent.md +13 -0
  85. package/src/orchestrator/prompts/brainstorm.prompt.md +1 -0
  86. package/src/orchestrator/prompts/fix-prd.prompt.md +58 -0
  87. package/src/orchestrator/prompts/generate-convoy.prompt.md +30 -0
  88. package/src/orchestrator/prompts/generate-prd.prompt.md +38 -0
  89. package/dist/cli/convoy/log-merge.test.d.ts +0 -2
  90. package/dist/cli/convoy/log-merge.test.d.ts.map +0 -1
  91. package/dist/cli/convoy/log-merge.test.js +0 -147
  92. package/dist/cli/convoy/log-merge.test.js.map +0 -1
  93. package/src/cli/convoy/log-merge.test.ts +0 -179
  94. 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
- Subcommands:
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,191 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { parseComplexityAssessment } from './pipeline.js'
3
+
4
+ const SINGLE_PRD = `# My Feature — PRD
5
+
6
+ ## Overview
7
+ Some overview.
8
+
9
+ ## Risks & Open Questions
10
+ None identified.
11
+
12
+ ## Complexity Assessment
13
+
14
+ \`\`\`json
15
+ {
16
+ "total_tasks": 4,
17
+ "total_phases": 2,
18
+ "domains": ["api", "frontend"],
19
+ "estimated_duration_minutes": 60,
20
+ "complexity": "medium",
21
+ "recommended_strategy": "single",
22
+ "chain_rationale": "",
23
+ "convoy_groups": [
24
+ {
25
+ "name": "full-implementation",
26
+ "description": "All phases in a single convoy",
27
+ "phases": [1, 2],
28
+ "depends_on": []
29
+ }
30
+ ]
31
+ }
32
+ \`\`\`
33
+ `
34
+
35
+ const CHAIN_PRD = `# Big Feature — PRD
36
+
37
+ ## Overview
38
+ Big feature.
39
+
40
+ ## Risks & Open Questions
41
+ None identified.
42
+
43
+ ## Complexity Assessment
44
+
45
+ \`\`\`json
46
+ {
47
+ "total_tasks": 12,
48
+ "total_phases": 4,
49
+ "domains": ["database", "api", "frontend", "testing"],
50
+ "estimated_duration_minutes": 240,
51
+ "complexity": "high",
52
+ "recommended_strategy": "chain",
53
+ "chain_rationale": "Database schema changes have no frontend dependencies and can be validated independently.",
54
+ "convoy_groups": [
55
+ {
56
+ "name": "database-setup",
57
+ "description": "Schema changes and migrations",
58
+ "phases": [1],
59
+ "depends_on": []
60
+ },
61
+ {
62
+ "name": "api-integration",
63
+ "description": "API routes and server logic",
64
+ "phases": [2],
65
+ "depends_on": ["database-setup"]
66
+ },
67
+ {
68
+ "name": "frontend-testing",
69
+ "description": "UI components and test suite",
70
+ "phases": [3, 4],
71
+ "depends_on": ["api-integration"]
72
+ }
73
+ ]
74
+ }
75
+ \`\`\`
76
+ `
77
+
78
+ const NO_SECTION_PRD = `# Feature — PRD
79
+
80
+ ## Overview
81
+ Plain prd with no complexity section.
82
+
83
+ ## Risks & Open Questions
84
+ None.
85
+ `
86
+
87
+ const MALFORMED_JSON_PRD = `# Feature — PRD
88
+
89
+ ## Complexity Assessment
90
+
91
+ \`\`\`json
92
+ { "total_tasks": 3, "broken":
93
+ \`\`\`
94
+ `
95
+
96
+ const UNFENCED_JSON_PRD = `# Feature — PRD
97
+
98
+ ## Complexity Assessment
99
+
100
+ The complexity is medium.
101
+
102
+ total_tasks: 3, total_phases: 2
103
+ `
104
+
105
+ const OTHER_JSON_PRD = `# Feature — PRD
106
+
107
+ ## Overview
108
+ Some feature with json-like content: \`{"key": "value"}\`
109
+
110
+ Another block:
111
+ \`\`\`json
112
+ {"not": "complexity"}
113
+ \`\`\`
114
+
115
+ ## Complexity Assessment
116
+
117
+ \`\`\`json
118
+ {
119
+ "total_tasks": 6,
120
+ "total_phases": 3,
121
+ "domains": ["api", "frontend"],
122
+ "complexity": "medium",
123
+ "recommended_strategy": "single",
124
+ "convoy_groups": [
125
+ {
126
+ "name": "impl",
127
+ "description": "All phases",
128
+ "phases": [1, 2, 3],
129
+ "depends_on": []
130
+ }
131
+ ]
132
+ }
133
+ \`\`\`
134
+ `
135
+
136
+ describe('parseComplexityAssessment', () => {
137
+ it('returns null when PRD has no Complexity Assessment section', () => {
138
+ expect(parseComplexityAssessment(NO_SECTION_PRD)).toBeNull()
139
+ })
140
+
141
+ it('returns null when JSON is malformed', () => {
142
+ expect(parseComplexityAssessment(MALFORMED_JSON_PRD)).toBeNull()
143
+ })
144
+
145
+ it('parses a valid single strategy assessment', () => {
146
+ const result = parseComplexityAssessment(SINGLE_PRD)
147
+ expect(result).not.toBeNull()
148
+ expect(result?.recommended_strategy).toBe('single')
149
+ expect(result?.complexity).toBe('medium')
150
+ expect(result?.total_tasks).toBe(4)
151
+ expect(result?.total_phases).toBe(2)
152
+ expect(result?.domains).toEqual(['api', 'frontend'])
153
+ expect(result?.convoy_groups).toHaveLength(1)
154
+ expect(result?.convoy_groups[0].name).toBe('full-implementation')
155
+ })
156
+
157
+ it('parses a valid chain strategy assessment with multiple groups', () => {
158
+ const result = parseComplexityAssessment(CHAIN_PRD)
159
+ expect(result).not.toBeNull()
160
+ expect(result?.recommended_strategy).toBe('chain')
161
+ expect(result?.complexity).toBe('high')
162
+ expect(result?.convoy_groups).toHaveLength(3)
163
+ expect(result?.convoy_groups[0].name).toBe('database-setup')
164
+ expect(result?.convoy_groups[1].depends_on).toEqual(['database-setup'])
165
+ expect(result?.convoy_groups[2].phases).toEqual([3, 4])
166
+ })
167
+
168
+ it('handles missing optional fields gracefully', () => {
169
+ const prd = `# Feature — PRD\n\n## Complexity Assessment\n\n\`\`\`json\n{\n "total_tasks": 3,\n "total_phases": 1,\n "domains": ["api"],\n "complexity": "low",\n "recommended_strategy": "single",\n "convoy_groups": [{"name": "all", "description": "All", "phases": [1], "depends_on": []}]\n}\n\`\`\`\n`
170
+ const result = parseComplexityAssessment(prd)
171
+ expect(result).not.toBeNull()
172
+ expect(result?.estimated_duration_minutes).toBeUndefined()
173
+ expect(result?.chain_rationale).toBeUndefined()
174
+ })
175
+
176
+ it('returns null when JSON block is not fenced properly', () => {
177
+ expect(parseComplexityAssessment(UNFENCED_JSON_PRD)).toBeNull()
178
+ })
179
+
180
+ it('correctly extracts JSON even when PRD has other JSON-like content elsewhere', () => {
181
+ const result = parseComplexityAssessment(OTHER_JSON_PRD)
182
+ expect(result).not.toBeNull()
183
+ expect(result?.total_tasks).toBe(6)
184
+ expect(result?.recommended_strategy).toBe('single')
185
+ })
186
+
187
+ it('returns null when required fields are missing from JSON', () => {
188
+ const prd = `# Feature — PRD\n\n## Complexity Assessment\n\n\`\`\`json\n{"total_tasks": 3}\n\`\`\`\n`
189
+ expect(parseComplexityAssessment(prd)).toBeNull()
190
+ })
191
+ })