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.
Files changed (130) hide show
  1. package/README.md +12 -3
  2. package/bin/cli.mjs +13 -5
  3. package/dist/cli/convoy/engine.d.ts.map +1 -1
  4. package/dist/cli/convoy/engine.js +2 -11
  5. package/dist/cli/convoy/engine.js.map +1 -1
  6. package/dist/cli/convoy/engine.test.js +2 -1
  7. package/dist/cli/convoy/engine.test.js.map +1 -1
  8. package/dist/cli/convoy/export.d.ts +1 -3
  9. package/dist/cli/convoy/export.d.ts.map +1 -1
  10. package/dist/cli/convoy/export.js +9 -88
  11. package/dist/cli/convoy/export.js.map +1 -1
  12. package/dist/cli/convoy/export.test.js +7 -186
  13. package/dist/cli/convoy/export.test.js.map +1 -1
  14. package/dist/cli/convoy/issues.js +3 -3
  15. package/dist/cli/convoy/issues.js.map +1 -1
  16. package/dist/cli/convoy/issues.test.js +4 -3
  17. package/dist/cli/convoy/issues.test.js.map +1 -1
  18. package/dist/cli/convoy/pipeline.d.ts.map +1 -1
  19. package/dist/cli/convoy/pipeline.js +0 -21
  20. package/dist/cli/convoy/pipeline.js.map +1 -1
  21. package/dist/cli/convoy/pipeline.test.js +0 -21
  22. package/dist/cli/convoy/pipeline.test.js.map +1 -1
  23. package/dist/cli/dashboard.d.ts.map +1 -1
  24. package/dist/cli/dashboard.js +32 -8
  25. package/dist/cli/dashboard.js.map +1 -1
  26. package/dist/cli/destroy.d.ts.map +1 -1
  27. package/dist/cli/destroy.js +13 -0
  28. package/dist/cli/destroy.js.map +1 -1
  29. package/dist/cli/dispute.d.ts +3 -0
  30. package/dist/cli/dispute.d.ts.map +1 -0
  31. package/dist/cli/dispute.js +25 -0
  32. package/dist/cli/dispute.js.map +1 -0
  33. package/dist/cli/doctor.d.ts +1 -1
  34. package/dist/cli/doctor.d.ts.map +1 -1
  35. package/dist/cli/doctor.js +14 -1
  36. package/dist/cli/doctor.js.map +1 -1
  37. package/dist/cli/eject.d.ts.map +1 -1
  38. package/dist/cli/eject.js +14 -0
  39. package/dist/cli/eject.js.map +1 -1
  40. package/dist/cli/init.d.ts.map +1 -1
  41. package/dist/cli/init.js +14 -0
  42. package/dist/cli/init.js.map +1 -1
  43. package/dist/cli/log.d.ts +0 -11
  44. package/dist/cli/log.d.ts.map +1 -1
  45. package/dist/cli/log.js +2 -114
  46. package/dist/cli/log.js.map +1 -1
  47. package/dist/cli/pipeline.d.ts +3 -0
  48. package/dist/cli/pipeline.d.ts.map +1 -0
  49. package/dist/cli/pipeline.js +321 -0
  50. package/dist/cli/pipeline.js.map +1 -0
  51. package/dist/cli/plan.d.ts +37 -0
  52. package/dist/cli/plan.d.ts.map +1 -1
  53. package/dist/cli/plan.js +321 -161
  54. package/dist/cli/plan.js.map +1 -1
  55. package/dist/cli/run.js +2 -2
  56. package/dist/cli/run.js.map +1 -1
  57. package/dist/cli/update.d.ts.map +1 -1
  58. package/dist/cli/update.js +16 -0
  59. package/dist/cli/update.js.map +1 -1
  60. package/dist/cli/validate.d.ts +3 -0
  61. package/dist/cli/validate.d.ts.map +1 -0
  62. package/dist/cli/validate.js +60 -0
  63. package/dist/cli/validate.js.map +1 -0
  64. package/dist/cli/watch.d.ts.map +1 -1
  65. package/dist/cli/watch.js +1 -3
  66. package/dist/cli/watch.js.map +1 -1
  67. package/package.json +5 -4
  68. package/src/cli/convoy/engine.test.ts +2 -1
  69. package/src/cli/convoy/engine.ts +2 -5
  70. package/src/cli/convoy/export.test.ts +7 -224
  71. package/src/cli/convoy/export.ts +10 -106
  72. package/src/cli/convoy/issues.test.ts +3 -2
  73. package/src/cli/convoy/issues.ts +3 -3
  74. package/src/cli/convoy/pipeline.test.ts +0 -25
  75. package/src/cli/convoy/pipeline.ts +0 -19
  76. package/src/cli/dashboard.ts +33 -8
  77. package/src/cli/destroy.ts +15 -0
  78. package/src/cli/dispute.ts +28 -0
  79. package/src/cli/doctor.ts +16 -1
  80. package/src/cli/eject.ts +16 -0
  81. package/src/cli/init.ts +16 -0
  82. package/src/cli/log.ts +2 -120
  83. package/src/cli/pipeline.ts +362 -0
  84. package/src/cli/plan.ts +357 -153
  85. package/src/cli/run.ts +2 -2
  86. package/src/cli/update.ts +18 -0
  87. package/src/cli/validate.ts +65 -0
  88. package/src/cli/watch.ts +1 -3
  89. package/src/dashboard/dist/_astro/index.Je1YjU_y.css +1 -0
  90. package/src/dashboard/dist/data/convoy-list.json +54 -9
  91. package/src/dashboard/dist/data/convoys/demo-api-v2.json +177 -0
  92. package/src/dashboard/dist/data/convoys/demo-auth-revamp.json +239 -0
  93. package/src/dashboard/dist/data/convoys/demo-dashboard-ui.json +328 -0
  94. package/src/dashboard/dist/data/convoys/demo-data-pipeline.json +187 -0
  95. package/src/dashboard/dist/data/convoys/demo-deploy-ci.json +153 -0
  96. package/src/dashboard/dist/data/convoys/demo-docs-update.json +154 -0
  97. package/src/dashboard/dist/data/convoys/demo-perf-opt.json +227 -0
  98. package/src/dashboard/dist/data/events.ndjson +115 -0
  99. package/src/dashboard/dist/data/overall-stats.json +56 -13
  100. package/src/dashboard/dist/data/pipelines.ndjson +5285 -0
  101. package/src/dashboard/dist/index.html +165 -1392
  102. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  103. package/src/dashboard/public/data/convoy-list.json +54 -9
  104. package/src/dashboard/public/data/convoys/demo-api-v2.json +177 -0
  105. package/src/dashboard/public/data/convoys/demo-auth-revamp.json +239 -0
  106. package/src/dashboard/public/data/convoys/demo-dashboard-ui.json +328 -0
  107. package/src/dashboard/public/data/convoys/demo-data-pipeline.json +187 -0
  108. package/src/dashboard/public/data/convoys/demo-deploy-ci.json +153 -0
  109. package/src/dashboard/public/data/convoys/demo-docs-update.json +154 -0
  110. package/src/dashboard/public/data/convoys/demo-perf-opt.json +227 -0
  111. package/src/dashboard/public/data/events.ndjson +115 -0
  112. package/src/dashboard/public/data/overall-stats.json +56 -13
  113. package/src/dashboard/public/data/pipelines.ndjson +5285 -0
  114. package/src/dashboard/scripts/etl.test.ts +4 -62
  115. package/src/dashboard/scripts/etl.ts +11 -10
  116. package/src/dashboard/scripts/generate-demo-db.ts +482 -115
  117. package/src/dashboard/src/pages/index.astro +235 -1638
  118. package/src/dashboard/src/styles/dashboard.css +473 -7
  119. package/src/orchestrator/prompts/brainstorm.prompt.md +1 -0
  120. package/src/orchestrator/prompts/fix-convoy.prompt.md +79 -0
  121. package/src/orchestrator/prompts/generate-convoy.prompt.md +60 -58
  122. package/src/orchestrator/prompts/generate-prd.prompt.md +126 -0
  123. package/src/orchestrator/prompts/validate-convoy.prompt.md +89 -0
  124. package/src/orchestrator/prompts/validate-prd.prompt.md +83 -0
  125. package/dist/cli/convoy/log-merge.test.d.ts +0 -2
  126. package/dist/cli/convoy/log-merge.test.d.ts.map +0 -1
  127. package/dist/cli/convoy/log-merge.test.js +0 -147
  128. package/dist/cli/convoy/log-merge.test.js.map +0 -1
  129. package/src/cli/convoy/log-merge.test.ts +0 -179
  130. package/src/dashboard/dist/_astro/index.6L3_HsPT.css +0 -1
@@ -1,228 +1,11 @@
1
- import { mkdtempSync, rmSync, readFileSync, existsSync, realpathSync } from 'node:fs'
2
- import { tmpdir } from 'node:os'
3
- import { join } from 'node:path'
4
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
5
- import { exportConvoyToNdjson } from './export.js'
6
- import { createConvoyStore } from './store.js'
7
- import type { ConvoyStore } from './store.js'
8
- import type { ConvoyTaskStatus } from './types.js'
1
+ // export.test.ts exportConvoyToNdjson and exportPipelineToNdjson have been
2
+ // removed from export.ts (see that file for rationale). No tests needed.
9
3
 
10
- vi.mock('../log.js', () => ({
11
- appendEvent: vi.fn().mockResolvedValue(undefined),
12
- }))
4
+ import { describe, it } from 'vitest'
13
5
 
14
- const NOW = '2026-03-08T10:00:00.000Z'
15
-
16
- let tmpDir: string
17
- let store: ConvoyStore
18
-
19
- function insertConvoy(id: string, name = 'Test Convoy') {
20
- store.insertConvoy({
21
- id,
22
- name,
23
- spec_hash: 'abc123',
24
- status: 'done',
25
- branch: 'main',
26
- created_at: NOW,
27
- spec_yaml: 'name: test',
28
- })
29
- store.updateConvoyStatus(id, 'done', { started_at: NOW, finished_at: NOW })
30
- }
31
-
32
- function insertTask(
33
- taskId: string,
34
- convoyId: string,
35
- phase = 1,
36
- status: ConvoyTaskStatus = 'done',
37
- ) {
38
- store.insertTask({
39
- id: taskId,
40
- convoy_id: convoyId,
41
- phase,
42
- prompt: 'Do something',
43
- agent: 'developer',
44
- adapter: 'claude-code',
45
- model: null,
46
- timeout_ms: 1_800_000,
47
- status,
48
- retries: 0,
49
- max_retries: 1,
50
- files: null,
51
- depends_on: null,
52
- gates: null,
53
- })
54
- if (status !== 'pending') {
55
- store.updateTaskStatus(taskId, convoyId, status, {
56
- started_at: NOW,
57
- finished_at: NOW,
58
- retries: 0,
59
- })
60
- }
61
- }
62
-
63
- function insertEvent(convoyId: string) {
64
- store.insertEvent({
65
- convoy_id: convoyId,
66
- task_id: null,
67
- worker_id: null,
68
- type: 'convoy.started',
69
- data: null,
70
- created_at: NOW,
71
- })
72
- }
73
-
74
- beforeEach(() => {
75
- tmpDir = realpathSync(mkdtempSync(join(tmpdir(), 'convoy-export-test-')))
76
- store = createConvoyStore(join(tmpDir, 'convoy.db'))
77
- })
78
-
79
- afterEach(() => {
80
- store.close()
81
- rmSync(tmpDir, { recursive: true, force: true })
82
- })
83
-
84
- describe('exportConvoyToNdjson', () => {
85
- it('creates convoys.ndjson with valid NDJSON', async () => {
86
- insertConvoy('c1')
87
- insertTask('t1', 'c1')
88
- const logsDir = join(tmpDir, 'logs')
89
-
90
- await exportConvoyToNdjson(store, 'c1', logsDir)
91
-
92
- const outFile = join(logsDir, 'convoys.ndjson')
93
- expect(existsSync(outFile)).toBe(true)
94
- const line = readFileSync(outFile, 'utf8').trimEnd()
95
- expect(() => JSON.parse(line)).not.toThrow()
96
- })
97
-
98
- it('appends on multiple exports (2 convoys -> 2 lines)', async () => {
99
- insertConvoy('c1')
100
- insertConvoy('c2', 'Second Convoy')
101
- const logsDir = join(tmpDir, 'logs')
102
-
103
- await exportConvoyToNdjson(store, 'c1', logsDir)
104
- await exportConvoyToNdjson(store, 'c2', logsDir)
105
-
106
- const content = readFileSync(join(logsDir, 'convoys.ndjson'), 'utf8')
107
- const lines = content.trim().split('\n').filter(Boolean)
108
- expect(lines).toHaveLength(2)
109
- expect(JSON.parse(lines[0]).id).toBe('c1')
110
- expect(JSON.parse(lines[1]).id).toBe('c2')
111
- })
112
-
113
- it('required fields present', async () => {
114
- insertConvoy('c1')
115
- insertTask('t1', 'c1', 1, 'done')
116
- insertTask('t2', 'c1', 1, 'failed')
117
- insertEvent('c1')
118
- const logsDir = join(tmpDir, 'logs')
119
-
120
- await exportConvoyToNdjson(store, 'c1', logsDir)
121
-
122
- const record = JSON.parse(readFileSync(join(logsDir, 'convoys.ndjson'), 'utf8').trim())
123
- expect(record.id).toBe('c1')
124
- expect(record.name).toBe('Test Convoy')
125
- expect(record.status).toBe('done')
126
- expect(Array.isArray(record.tasks)).toBe(true)
127
- expect(record.tasks).toHaveLength(2)
128
- expect(record.tasks[0]).toMatchObject({
129
- id: 't1',
130
- phase: 1,
131
- agent: 'developer',
132
- adapter: 'claude-code',
133
- status: 'done',
134
- retries: 0,
135
- })
136
- expect(record.summary).toMatchObject({ total: 2, done: 1, failed: 1, skipped: 0, timedOut: 0 })
137
- expect(record.events_count).toBe(1)
138
- })
139
-
140
- it('missing convoy -> no error, no file', async () => {
141
- const logsDir = join(tmpDir, 'logs')
142
- await expect(exportConvoyToNdjson(store, 'nonexistent', logsDir)).resolves.toBeUndefined()
143
- expect(existsSync(join(logsDir, 'convoys.ndjson'))).toBe(false)
144
- })
145
-
146
- it('creates directory if missing', async () => {
147
- insertConvoy('c1')
148
- const logsDir = join(tmpDir, 'deep', 'nested', 'logs')
149
-
150
- await exportConvoyToNdjson(store, 'c1', logsDir)
151
-
152
- expect(existsSync(join(logsDir, 'convoys.ndjson'))).toBe(true)
153
- })
154
-
155
- it('respects custom logsDir', async () => {
156
- insertConvoy('c1')
157
- const customDir = join(tmpDir, 'custom-output')
158
-
159
- await exportConvoyToNdjson(store, 'c1', customDir)
160
-
161
- const outFile = join(customDir, 'convoys.ndjson')
162
- expect(existsSync(outFile)).toBe(true)
163
- const record = JSON.parse(readFileSync(outFile, 'utf8').trim())
164
- expect(record.id).toBe('c1')
165
- })
166
-
167
- it('never throws on store error — writes warning to stderr', async () => {
168
- const broken = {
169
- getConvoy: () => { throw new Error('db exploded') },
170
- } as unknown as ConvoyStore
171
- const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true)
172
-
173
- await expect(exportConvoyToNdjson(broken, 'c1', join(tmpDir, 'logs'))).resolves.toBeUndefined()
174
- expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('exportConvoyToNdjson warning'))
175
-
176
- stderrSpy.mockRestore()
177
- })
178
-
179
- it('defaults to .opencastle/logs when logsDir is omitted', async () => {
180
- insertConvoy('c1')
181
- const originalCwd = process.cwd()
182
- process.chdir(tmpDir)
183
- try {
184
- await exportConvoyToNdjson(store, 'c1')
185
- const outFile = join(tmpDir, '.opencastle', 'logs', 'convoys.ndjson')
186
- expect(existsSync(outFile)).toBe(true)
187
- } finally {
188
- process.chdir(originalCwd)
189
- }
190
- })
191
-
192
- it('includes cost fields (prompt_tokens, completion_tokens, total_tokens) per task when present', async () => {
193
- insertConvoy('c1')
194
- insertTask('t1', 'c1')
195
- // Manually set token cost on the task record
196
- store.updateTaskStatus('t1', 'c1', 'done', {
197
- prompt_tokens: 200,
198
- completion_tokens: 100,
199
- total_tokens: 300,
200
- })
201
- // Set convoy-level totals
202
- store.updateConvoyStatus('c1', 'done', { total_tokens: 300 })
203
- const logsDir = join(tmpDir, 'logs')
204
-
205
- await exportConvoyToNdjson(store, 'c1', logsDir)
206
-
207
- const record = JSON.parse(readFileSync(join(logsDir, 'convoys.ndjson'), 'utf8').trim())
208
- expect(record.tasks[0].prompt_tokens).toBe(200)
209
- expect(record.tasks[0].completion_tokens).toBe(100)
210
- expect(record.tasks[0].total_tokens).toBe(300)
211
- expect(record.total_tokens).toBe(300)
212
- })
213
-
214
- it('cost fields are null when no usage data recorded', async () => {
215
- insertConvoy('c1')
216
- insertTask('t1', 'c1')
217
- const logsDir = join(tmpDir, 'logs')
218
-
219
- await exportConvoyToNdjson(store, 'c1', logsDir)
220
-
221
- const record = JSON.parse(readFileSync(join(logsDir, 'convoys.ndjson'), 'utf8').trim())
222
- expect(record.tasks[0].prompt_tokens).toBeNull()
223
- expect(record.tasks[0].completion_tokens).toBeNull()
224
- expect(record.tasks[0].total_tokens).toBeNull()
225
- expect(record.total_tokens).toBeNull()
226
- expect(record.total_cost_usd).toBeNull()
6
+ describe('export', () => {
7
+ it('export functions removed — data access goes through SQLite store', () => {
8
+ // The monolithic NDJSON export functions were removed to prevent unbounded
9
+ // file growth. All convoy/pipeline data is in convoy.db (SQLite).
227
10
  })
228
11
  })
@@ -1,106 +1,10 @@
1
- import { appendFileSync, mkdirSync } from 'node:fs'
2
- import { resolve } from 'node:path'
3
- import type { ConvoyStore } from './store.js'
4
-
5
- export async function exportPipelineToNdjson(
6
- store: ConvoyStore,
7
- pipelineId: string,
8
- logsDir?: string,
9
- ): Promise<void> {
10
- try {
11
- const pipeline = store.getPipeline(pipelineId)
12
- if (!pipeline) return
13
-
14
- const convoys = store.getConvoysByPipeline(pipelineId)
15
-
16
- const record = {
17
- id: pipeline.id,
18
- name: pipeline.name,
19
- status: pipeline.status,
20
- branch: pipeline.branch,
21
- created_at: pipeline.created_at,
22
- started_at: pipeline.started_at,
23
- finished_at: pipeline.finished_at,
24
- total_tokens: pipeline.total_tokens,
25
- total_cost_usd: pipeline.total_cost_usd,
26
- convoy_count: convoys.length,
27
- convoys: convoys.map(c => ({
28
- id: c.id,
29
- name: c.name,
30
- status: c.status,
31
- started_at: c.started_at,
32
- finished_at: c.finished_at,
33
- total_tokens: c.total_tokens,
34
- })),
35
- }
36
-
37
- const dir = logsDir ?? resolve(process.cwd(), '.opencastle', 'logs')
38
- mkdirSync(dir, { recursive: true })
39
- appendFileSync(resolve(dir, 'pipelines.ndjson'), JSON.stringify(record) + '\n', 'utf8')
40
- } catch (err) {
41
- process.stderr.write(`[opencastle] exportPipelineToNdjson warning: ${String(err)}\n`)
42
- }
43
- }
44
-
45
- export async function exportConvoyToNdjson(
46
- store: ConvoyStore,
47
- convoyId: string,
48
- logsDir?: string,
49
- ): Promise<void> {
50
- try {
51
- const convoy = store.getConvoy(convoyId)
52
- if (!convoy) return
53
-
54
- const tasks = store.getTasksByConvoy(convoyId)
55
- const eventsCount = store.getEvents(convoyId).length
56
-
57
- const summary = {
58
- total: tasks.length,
59
- done: tasks.filter((t) => t.status === 'done').length,
60
- failed: tasks.filter((t) => t.status === 'failed').length,
61
- skipped: tasks.filter((t) => t.status === 'skipped').length,
62
- timedOut: tasks.filter((t) => t.status === 'timed-out').length,
63
- }
64
-
65
- const durationSec =
66
- convoy.started_at && convoy.finished_at
67
- ? Math.round(
68
- (new Date(convoy.finished_at).getTime() - new Date(convoy.started_at).getTime()) / 1_000,
69
- )
70
- : undefined
71
-
72
- const record = {
73
- id: convoy.id,
74
- name: convoy.name,
75
- status: convoy.status,
76
- branch: convoy.branch,
77
- created_at: convoy.created_at,
78
- started_at: convoy.started_at,
79
- finished_at: convoy.finished_at,
80
- duration_sec: durationSec,
81
- summary,
82
- tasks: tasks.map((t) => ({
83
- id: t.id,
84
- phase: t.phase,
85
- agent: t.agent,
86
- adapter: t.adapter,
87
- status: t.status,
88
- started_at: t.started_at,
89
- finished_at: t.finished_at,
90
- retries: t.retries,
91
- prompt_tokens: t.prompt_tokens,
92
- completion_tokens: t.completion_tokens,
93
- total_tokens: t.total_tokens,
94
- })),
95
- events_count: eventsCount,
96
- total_tokens: convoy.total_tokens,
97
- total_cost_usd: convoy.total_cost_usd,
98
- }
99
-
100
- const dir = logsDir ?? resolve(process.cwd(), '.opencastle', 'logs')
101
- mkdirSync(dir, { recursive: true })
102
- appendFileSync(resolve(dir, 'convoys.ndjson'), JSON.stringify(record) + '\n', 'utf8')
103
- } catch (err) {
104
- process.stderr.write(`[opencastle] exportConvoyToNdjson warning: ${String(err)}\n`)
105
- }
106
- }
1
+ // Convoy data exports are handled by the dashboard ETL (src/dashboard/scripts/etl.ts)
2
+ // which reads directly from the SQLite convoy store.
3
+ //
4
+ // Per-convoy event logs are in .opencastle/logs/convoys/{convoy-id}.ndjson
5
+ // (written by the event emitter in events.ts, one file per convoy run).
6
+ //
7
+ // export functions removed to prevent unbounded NDJSON growth.
8
+ // All convoy and pipeline data is queryable from the SQLite store at .opencastle/convoy.db.
9
+
10
+ export {}
@@ -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
 
@@ -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' +
@@ -747,31 +747,6 @@ describe('getCurrentBranch fallback', () => {
747
747
  })
748
748
 
749
749
 
750
- describe('NDJSON export', () => {
751
- it('writes pipelines.ndjson to logsDir after run', async () => {
752
- const factory = makeEngineFactory([makeConvoyResult()])
753
- const logsDir = join(tmpDir, 'logs')
754
-
755
- await createPipelineOrchestrator({
756
- spec: makePipelineSpec({ depends_on_convoy: ['./a.yaml'] }),
757
- specYaml: 'name: pipeline',
758
- adapter: makeAdapter(),
759
- dbPath,
760
- logsDir,
761
- _createConvoyEngine: factory,
762
- }).run()
763
-
764
- const { existsSync, readFileSync } = await import('node:fs')
765
- const ndjsonPath = join(logsDir, 'pipelines.ndjson')
766
- expect(existsSync(ndjsonPath)).toBe(true)
767
-
768
- const parsed = JSON.parse(readFileSync(ndjsonPath, 'utf8').trim())
769
- expect(parsed.status).toBe('done')
770
- expect(typeof parsed.id).toBe('string')
771
- expect(Array.isArray(parsed.convoys)).toBe(true)
772
- })
773
- })
774
-
775
750
  // ── 13. Path traversal protection ────────────────────────────────────────────
776
751
 
777
752
  describe('path traversal protection', () => {
@@ -12,7 +12,6 @@ import {
12
12
  type ConvoyResult,
13
13
  type ConvoyEngineOptions,
14
14
  } from './engine.js'
15
- import { exportPipelineToNdjson } from './export.js'
16
15
  import type { PipelineStatus } from './types.js'
17
16
  import { formatDuration } from '../run/executor.js'
18
17
 
@@ -240,15 +239,6 @@ export function createPipelineOrchestrator(
240
239
  updateStore.close()
241
240
  }
242
241
 
243
- try {
244
- const exportStore = createConvoyStore(dbPath)
245
- try {
246
- await exportPipelineToNdjson(exportStore, pipelineId, options.logsDir)
247
- } finally {
248
- exportStore.close()
249
- }
250
- } catch { /* silent */ }
251
-
252
242
  return {
253
243
  pipelineId,
254
244
  status: finalStatus,
@@ -407,15 +397,6 @@ export function createPipelineOrchestrator(
407
397
  updateStore.close()
408
398
  }
409
399
 
410
- try {
411
- const exportStore = createConvoyStore(dbPath)
412
- try {
413
- await exportPipelineToNdjson(exportStore, pipelineId, options.logsDir)
414
- } finally {
415
- exportStore.close()
416
- }
417
- } catch { /* silent */ }
418
-
419
400
  return {
420
401
  pipelineId,
421
402
  status: finalStatus,
@@ -20,7 +20,6 @@ const MIME_TYPES: Record<string, string> = {
20
20
 
21
21
  const DATA_FILES = [
22
22
  'events.ndjson',
23
- 'convoys.ndjson',
24
23
  'pipelines.ndjson',
25
24
  // Legacy individual files — kept for backwards compatibility
26
25
  'sessions.ndjson',
@@ -35,6 +34,7 @@ interface DashboardArgs {
35
34
  openBrowser: boolean
36
35
  seed: boolean
37
36
  convoyId?: string
37
+ help: boolean
38
38
  }
39
39
 
40
40
  export interface DashboardServerOptions {
@@ -51,14 +51,30 @@ export interface DashboardServerResult {
51
51
  url: string
52
52
  }
53
53
 
54
+ const DASHBOARD_HELP = `
55
+ opencastle dashboard [options]
56
+
57
+ Start the observability dashboard server.
58
+
59
+ Options:
60
+ --port <number> Port to listen on (default: 4300, auto-increments if busy)
61
+ --no-open Don't auto-open the browser
62
+ --seed Show demo data instead of project logs
63
+ --convoy <id> Filter dashboard to a specific convoy
64
+ --help, -h Show this help
65
+ `
66
+
54
67
  function parseArgs(args: string[]): DashboardArgs {
55
68
  let port = 4300
56
69
  let openBrowser = true
57
70
  let seed = false
58
71
  let convoyId: string | undefined
72
+ let help = false
59
73
 
60
74
  for (let i = 0; i < args.length; i++) {
61
- if (args[i] === '--port' && args[i + 1]) {
75
+ if (args[i] === '--help' || args[i] === '-h') {
76
+ help = true
77
+ } else if (args[i] === '--port' && args[i + 1]) {
62
78
  port = parseInt(args[i + 1], 10)
63
79
  i++
64
80
  } else if (args[i] === '--no-open') {
@@ -71,7 +87,7 @@ function parseArgs(args: string[]): DashboardArgs {
71
87
  }
72
88
  }
73
89
 
74
- return { port, openBrowser, seed, convoyId }
90
+ return { port, openBrowser, seed, convoyId, help }
75
91
  }
76
92
 
77
93
  function openUrl(url: string): void {
@@ -234,19 +250,28 @@ export default async function dashboard({
234
250
  pkgRoot,
235
251
  args,
236
252
  }: CliContext): Promise<void> {
237
- const { port, openBrowser, seed, convoyId } = parseArgs(args)
253
+ const { port, openBrowser, seed, convoyId, help } = parseArgs(args)
254
+
255
+ if (help) {
256
+ console.log(DASHBOARD_HELP)
257
+ return
258
+ }
238
259
 
239
260
  // Check if any log files exist (for messaging)
240
261
  let hasLogs = false
241
262
  if (!seed) {
242
263
  const projectRoot = process.cwd()
264
+ const convoyLogsDir2 = resolve(projectRoot, '.opencastle', 'logs')
243
265
  const logsDir = resolve(projectRoot, '.github', 'customizations', 'logs')
244
266
  const checkFiles = ['events.ndjson', ...DATA_FILES]
245
- for (const f of checkFiles) {
246
- if (await fileExists(join(logsDir, f))) {
247
- hasLogs = true
248
- break
267
+ for (const dir of [convoyLogsDir2, logsDir]) {
268
+ for (const f of checkFiles) {
269
+ if (await fileExists(join(dir, f))) {
270
+ hasLogs = true
271
+ break
272
+ }
249
273
  }
274
+ if (hasLogs) break
250
275
  }
251
276
  }
252
277
 
@@ -7,10 +7,25 @@ import { removeGitignoreBlock } from './gitignore.js'
7
7
  import { confirm, closePrompts, c } from './prompt.js'
8
8
  import type { CliContext } from './types.js'
9
9
 
10
+ const DESTROY_HELP = `
11
+ opencastle destroy [options]
12
+
13
+ Remove ALL OpenCastle files from your project (reverse of init).
14
+
15
+ Options:
16
+ --dry-run Preview what would be removed without deleting files
17
+ --help, -h Show this help
18
+ `
19
+
10
20
  export default async function destroy({
11
21
  pkgRoot: _pkgRoot,
12
22
  args,
13
23
  }: CliContext): Promise<void> {
24
+ if (args.includes('--help') || args.includes('-h')) {
25
+ console.log(DESTROY_HELP)
26
+ return
27
+ }
28
+
14
29
  const projectRoot = process.cwd()
15
30
  const dryRun = args.includes('--dry-run') || args.includes('--dryRun')
16
31
 
@@ -0,0 +1,28 @@
1
+ import type { CliContext } from './types.js'
2
+
3
+ const HELP = `
4
+ opencastle dispute [options]
5
+
6
+ Manage convoy dispute resolution — view, create, and resolve disputes
7
+ that arise when panel reviews repeatedly block a task.
8
+
9
+ Subcommands:
10
+ list List all disputes
11
+ show <id> Show dispute details
12
+ resolve <id> Mark a dispute as resolved
13
+
14
+ Options:
15
+ --convoy <id> Filter by convoy ID
16
+ --help, -h Show this help
17
+ `
18
+
19
+ export default async function dispute({ args }: CliContext): Promise<void> {
20
+ if (args.includes('--help') || args.includes('-h') || args.length === 0) {
21
+ console.log(HELP)
22
+ return
23
+ }
24
+
25
+ console.error(' ✗ Dispute management is not yet implemented.')
26
+ console.log(HELP)
27
+ process.exit(1)
28
+ }
package/src/cli/doctor.ts CHANGED
@@ -159,7 +159,22 @@ export function checkMcpFromPaths(projectRoot: string, mcpPaths: string[]): Chec
159
159
 
160
160
  // ── Main doctor command ───────────────────────────────────────
161
161
 
162
- export default async function doctor({ args: _args }: CliContext): Promise<void> {
162
+ const DOCTOR_HELP = `
163
+ opencastle doctor [options]
164
+
165
+ Validate your OpenCastle setup — checks manifest, customizations, skills,
166
+ logs, MCP configuration, and IDE-specific rules.
167
+
168
+ Options:
169
+ --help, -h Show this help
170
+ `
171
+
172
+ export default async function doctor({ args }: CliContext): Promise<void> {
173
+ if (args.includes('--help') || args.includes('-h')) {
174
+ console.log(DOCTOR_HELP)
175
+ return
176
+ }
177
+
163
178
  const projectRoot = process.cwd();
164
179
 
165
180
  console.log(`\n 🏰 ${BOLD('OpenCastle Doctor')}\n`);
package/src/cli/eject.ts CHANGED
@@ -4,10 +4,26 @@ import { readManifest } from './manifest.js'
4
4
  import { confirm, closePrompts } from './prompt.js'
5
5
  import type { CliContext } from './types.js'
6
6
 
7
+ const EJECT_HELP = `
8
+ opencastle eject [options]
9
+
10
+ Remove the OpenCastle dependency while keeping all framework files
11
+ standalone in your project.
12
+
13
+ Options:
14
+ --dry-run Preview what would be changed without writing files
15
+ --help, -h Show this help
16
+ `
17
+
7
18
  export default async function eject({
8
19
  pkgRoot: _pkgRoot,
9
20
  args,
10
21
  }: CliContext): Promise<void> {
22
+ if (args.includes('--help') || args.includes('-h')) {
23
+ console.log(EJECT_HELP)
24
+ return
25
+ }
26
+
11
27
  const projectRoot = process.cwd()
12
28
  const dryRun = args.includes('--dry-run') || args.includes('--dryRun')
13
29
 
package/src/cli/init.ts CHANGED
@@ -13,7 +13,23 @@ import { IDE_LABELS } from './types.js'
13
13
  import type { CliContext, IdeChoice, TechTool, TeamTool, StackConfig } from './types.js'
14
14
  import { bootstrapCustomizations } from './bootstrap.js'
15
15
 
16
+ const INIT_HELP = `
17
+ opencastle init [options]
18
+
19
+ Set up OpenCastle in your project — copies framework files, configures
20
+ IDE adapters, and creates the .opencastle/ directory.
21
+
22
+ Options:
23
+ --dry-run Preview what would be changed without writing files
24
+ --help, -h Show this help
25
+ `
26
+
16
27
  export default async function init({ pkgRoot, args }: CliContext): Promise<void> {
28
+ if (args.includes('--help') || args.includes('-h')) {
29
+ console.log(INIT_HELP)
30
+ return
31
+ }
32
+
17
33
  const projectRoot = process.cwd()
18
34
  const dryRun = args.includes('--dry-run') || args.includes('--dryRun')
19
35