opencastle 0.31.6 → 0.32.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 (210) hide show
  1. package/LICENSE +93 -21
  2. package/README.md +9 -3
  3. package/bin/cli.mjs +15 -0
  4. package/dist/cli/agents.d.ts.map +1 -1
  5. package/dist/cli/agents.js +19 -5
  6. package/dist/cli/agents.js.map +1 -1
  7. package/dist/cli/artifacts-cli.d.ts +3 -0
  8. package/dist/cli/artifacts-cli.d.ts.map +1 -0
  9. package/dist/cli/artifacts-cli.js +36 -0
  10. package/dist/cli/artifacts-cli.js.map +1 -0
  11. package/dist/cli/baselines.d.ts.map +1 -1
  12. package/dist/cli/baselines.js +11 -0
  13. package/dist/cli/baselines.js.map +1 -1
  14. package/dist/cli/convoy/artifacts.d.ts +25 -0
  15. package/dist/cli/convoy/artifacts.d.ts.map +1 -0
  16. package/dist/cli/convoy/artifacts.js +129 -0
  17. package/dist/cli/convoy/artifacts.js.map +1 -0
  18. package/dist/cli/convoy/artifacts.test.d.ts +2 -0
  19. package/dist/cli/convoy/artifacts.test.d.ts.map +1 -0
  20. package/dist/cli/convoy/artifacts.test.js +169 -0
  21. package/dist/cli/convoy/artifacts.test.js.map +1 -0
  22. package/dist/cli/convoy/compaction.d.ts +23 -0
  23. package/dist/cli/convoy/compaction.d.ts.map +1 -0
  24. package/dist/cli/convoy/compaction.js +117 -0
  25. package/dist/cli/convoy/compaction.js.map +1 -0
  26. package/dist/cli/convoy/compaction.test.d.ts +2 -0
  27. package/dist/cli/convoy/compaction.test.d.ts.map +1 -0
  28. package/dist/cli/convoy/compaction.test.js +205 -0
  29. package/dist/cli/convoy/compaction.test.js.map +1 -0
  30. package/dist/cli/convoy/contracts.d.ts +22 -0
  31. package/dist/cli/convoy/contracts.d.ts.map +1 -0
  32. package/dist/cli/convoy/contracts.js +254 -0
  33. package/dist/cli/convoy/contracts.js.map +1 -0
  34. package/dist/cli/convoy/contracts.test.d.ts +2 -0
  35. package/dist/cli/convoy/contracts.test.d.ts.map +1 -0
  36. package/dist/cli/convoy/contracts.test.js +239 -0
  37. package/dist/cli/convoy/contracts.test.js.map +1 -0
  38. package/dist/cli/convoy/dag-analysis.d.ts +40 -0
  39. package/dist/cli/convoy/dag-analysis.d.ts.map +1 -0
  40. package/dist/cli/convoy/dag-analysis.js +282 -0
  41. package/dist/cli/convoy/dag-analysis.js.map +1 -0
  42. package/dist/cli/convoy/dag-analysis.test.d.ts +2 -0
  43. package/dist/cli/convoy/dag-analysis.test.d.ts.map +1 -0
  44. package/dist/cli/convoy/dag-analysis.test.js +289 -0
  45. package/dist/cli/convoy/dag-analysis.test.js.map +1 -0
  46. package/dist/cli/convoy/effort-scaling.d.ts +20 -0
  47. package/dist/cli/convoy/effort-scaling.d.ts.map +1 -0
  48. package/dist/cli/convoy/effort-scaling.js +82 -0
  49. package/dist/cli/convoy/effort-scaling.js.map +1 -0
  50. package/dist/cli/convoy/effort-scaling.test.d.ts +2 -0
  51. package/dist/cli/convoy/effort-scaling.test.d.ts.map +1 -0
  52. package/dist/cli/convoy/effort-scaling.test.js +120 -0
  53. package/dist/cli/convoy/effort-scaling.test.js.map +1 -0
  54. package/dist/cli/convoy/engine.d.ts.map +1 -1
  55. package/dist/cli/convoy/engine.js +298 -11
  56. package/dist/cli/convoy/engine.js.map +1 -1
  57. package/dist/cli/convoy/engine.test.js +155 -18
  58. package/dist/cli/convoy/engine.test.js.map +1 -1
  59. package/dist/cli/convoy/event-schemas.d.ts.map +1 -1
  60. package/dist/cli/convoy/event-schemas.js +55 -0
  61. package/dist/cli/convoy/event-schemas.js.map +1 -1
  62. package/dist/cli/convoy/isolation.d.ts +27 -0
  63. package/dist/cli/convoy/isolation.d.ts.map +1 -0
  64. package/dist/cli/convoy/isolation.js +120 -0
  65. package/dist/cli/convoy/isolation.js.map +1 -0
  66. package/dist/cli/convoy/isolation.test.d.ts +2 -0
  67. package/dist/cli/convoy/isolation.test.d.ts.map +1 -0
  68. package/dist/cli/convoy/isolation.test.js +105 -0
  69. package/dist/cli/convoy/isolation.test.js.map +1 -0
  70. package/dist/cli/convoy/review-stages.d.ts +9 -0
  71. package/dist/cli/convoy/review-stages.d.ts.map +1 -0
  72. package/dist/cli/convoy/review-stages.js +134 -0
  73. package/dist/cli/convoy/review-stages.js.map +1 -0
  74. package/dist/cli/convoy/review-stages.test.d.ts +2 -0
  75. package/dist/cli/convoy/review-stages.test.d.ts.map +1 -0
  76. package/dist/cli/convoy/review-stages.test.js +197 -0
  77. package/dist/cli/convoy/review-stages.test.js.map +1 -0
  78. package/dist/cli/convoy/skill-refinement.d.ts +39 -0
  79. package/dist/cli/convoy/skill-refinement.d.ts.map +1 -0
  80. package/dist/cli/convoy/skill-refinement.js +239 -0
  81. package/dist/cli/convoy/skill-refinement.js.map +1 -0
  82. package/dist/cli/convoy/skill-refinement.test.d.ts +2 -0
  83. package/dist/cli/convoy/skill-refinement.test.d.ts.map +1 -0
  84. package/dist/cli/convoy/skill-refinement.test.js +230 -0
  85. package/dist/cli/convoy/skill-refinement.test.js.map +1 -0
  86. package/dist/cli/convoy/spec-builder.d.ts +1 -0
  87. package/dist/cli/convoy/spec-builder.d.ts.map +1 -1
  88. package/dist/cli/convoy/spec-builder.js +11 -0
  89. package/dist/cli/convoy/spec-builder.js.map +1 -1
  90. package/dist/cli/convoy/spec-builder.test.js +54 -0
  91. package/dist/cli/convoy/spec-builder.test.js.map +1 -1
  92. package/dist/cli/convoy/store.d.ts +3 -2
  93. package/dist/cli/convoy/store.d.ts.map +1 -1
  94. package/dist/cli/convoy/store.js +20 -2
  95. package/dist/cli/convoy/store.js.map +1 -1
  96. package/dist/cli/convoy/store.test.js +15 -15
  97. package/dist/cli/convoy/store.test.js.map +1 -1
  98. package/dist/cli/convoy/tdd-gate.d.ts +15 -0
  99. package/dist/cli/convoy/tdd-gate.d.ts.map +1 -0
  100. package/dist/cli/convoy/tdd-gate.js +119 -0
  101. package/dist/cli/convoy/tdd-gate.js.map +1 -0
  102. package/dist/cli/convoy/tdd-gate.test.d.ts +2 -0
  103. package/dist/cli/convoy/tdd-gate.test.d.ts.map +1 -0
  104. package/dist/cli/convoy/tdd-gate.test.js +227 -0
  105. package/dist/cli/convoy/tdd-gate.test.js.map +1 -0
  106. package/dist/cli/convoy/types.d.ts +91 -0
  107. package/dist/cli/convoy/types.d.ts.map +1 -1
  108. package/dist/cli/convoy/types.js +8 -0
  109. package/dist/cli/convoy/types.js.map +1 -1
  110. package/dist/cli/dashboard.d.ts.map +1 -1
  111. package/dist/cli/dashboard.js +54 -0
  112. package/dist/cli/dashboard.js.map +1 -1
  113. package/dist/cli/insights.d.ts +3 -0
  114. package/dist/cli/insights.d.ts.map +1 -0
  115. package/dist/cli/insights.js +94 -0
  116. package/dist/cli/insights.js.map +1 -0
  117. package/dist/cli/lesson.d.ts.map +1 -1
  118. package/dist/cli/lesson.js +7 -0
  119. package/dist/cli/lesson.js.map +1 -1
  120. package/dist/cli/log.d.ts.map +1 -1
  121. package/dist/cli/log.js +7 -0
  122. package/dist/cli/log.js.map +1 -1
  123. package/dist/cli/package-config.d.ts +12 -0
  124. package/dist/cli/package-config.d.ts.map +1 -0
  125. package/dist/cli/package-config.js +37 -0
  126. package/dist/cli/package-config.js.map +1 -0
  127. package/dist/cli/package.d.ts +23 -0
  128. package/dist/cli/package.d.ts.map +1 -0
  129. package/dist/cli/package.js +285 -0
  130. package/dist/cli/package.js.map +1 -0
  131. package/dist/cli/package.test.d.ts +2 -0
  132. package/dist/cli/package.test.d.ts.map +1 -0
  133. package/dist/cli/package.test.js +236 -0
  134. package/dist/cli/package.test.js.map +1 -0
  135. package/dist/cli/pipeline.d.ts +6 -0
  136. package/dist/cli/pipeline.d.ts.map +1 -1
  137. package/dist/cli/pipeline.js +15 -2
  138. package/dist/cli/pipeline.js.map +1 -1
  139. package/dist/cli/run/schema.d.ts.map +1 -1
  140. package/dist/cli/run/schema.js +32 -0
  141. package/dist/cli/run/schema.js.map +1 -1
  142. package/dist/cli/run/schema.test.js +51 -0
  143. package/dist/cli/run/schema.test.js.map +1 -1
  144. package/dist/cli/run.d.ts.map +1 -1
  145. package/dist/cli/run.js +10 -1
  146. package/dist/cli/run.js.map +1 -1
  147. package/dist/cli/skills.d.ts +3 -0
  148. package/dist/cli/skills.d.ts.map +1 -0
  149. package/dist/cli/skills.js +107 -0
  150. package/dist/cli/skills.js.map +1 -0
  151. package/dist/cli/types.d.ts +4 -1
  152. package/dist/cli/types.d.ts.map +1 -1
  153. package/dist/cli/update.js +2 -2
  154. package/package.json +3 -2
  155. package/src/cli/agents.ts +20 -5
  156. package/src/cli/artifacts-cli.ts +41 -0
  157. package/src/cli/baselines.ts +12 -0
  158. package/src/cli/convoy/artifacts.test.ts +201 -0
  159. package/src/cli/convoy/artifacts.ts +186 -0
  160. package/src/cli/convoy/compaction.test.ts +245 -0
  161. package/src/cli/convoy/compaction.ts +164 -0
  162. package/src/cli/convoy/contracts.test.ts +279 -0
  163. package/src/cli/convoy/contracts.ts +280 -0
  164. package/src/cli/convoy/dag-analysis.test.ts +349 -0
  165. package/src/cli/convoy/dag-analysis.ts +371 -0
  166. package/src/cli/convoy/effort-scaling.test.ts +140 -0
  167. package/src/cli/convoy/effort-scaling.ts +90 -0
  168. package/src/cli/convoy/engine.test.ts +175 -18
  169. package/src/cli/convoy/engine.ts +315 -12
  170. package/src/cli/convoy/event-schemas.ts +55 -0
  171. package/src/cli/convoy/isolation.test.ts +137 -0
  172. package/src/cli/convoy/isolation.ts +165 -0
  173. package/src/cli/convoy/review-stages.test.ts +235 -0
  174. package/src/cli/convoy/review-stages.ts +166 -0
  175. package/src/cli/convoy/skill-refinement.test.ts +277 -0
  176. package/src/cli/convoy/skill-refinement.ts +306 -0
  177. package/src/cli/convoy/spec-builder.test.ts +61 -0
  178. package/src/cli/convoy/spec-builder.ts +9 -0
  179. package/src/cli/convoy/store.test.ts +15 -15
  180. package/src/cli/convoy/store.ts +26 -4
  181. package/src/cli/convoy/tdd-gate.test.ts +281 -0
  182. package/src/cli/convoy/tdd-gate.ts +154 -0
  183. package/src/cli/convoy/types.ts +51 -0
  184. package/src/cli/dashboard.ts +55 -0
  185. package/src/cli/insights.ts +99 -0
  186. package/src/cli/lesson.ts +8 -0
  187. package/src/cli/log.ts +8 -0
  188. package/src/cli/package-config.ts +48 -0
  189. package/src/cli/package.test.ts +276 -0
  190. package/src/cli/package.ts +329 -0
  191. package/src/cli/pipeline.ts +21 -2
  192. package/src/cli/run/schema.test.ts +58 -0
  193. package/src/cli/run/schema.ts +33 -0
  194. package/src/cli/run.ts +14 -1
  195. package/src/cli/skills.ts +121 -0
  196. package/src/cli/types.ts +4 -1
  197. package/src/cli/update.ts +2 -2
  198. package/src/dashboard/dist/_astro/{index.Je1YjU_y.css → index.BRDFmNzR.css} +1 -1
  199. package/src/dashboard/dist/index.html +163 -2
  200. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  201. package/src/dashboard/src/pages/index.astro +162 -1
  202. package/src/dashboard/src/styles/dashboard.css +85 -0
  203. package/src/orchestrator/agents/developer.agent.md +8 -0
  204. package/src/orchestrator/agents/ui-ux-expert.agent.md +7 -0
  205. package/src/orchestrator/prompts/assess-complexity.prompt.md +13 -0
  206. package/src/orchestrator/prompts/brainstorm.prompt.md +18 -0
  207. package/src/orchestrator/prompts/generate-convoy.prompt.md +61 -0
  208. package/src/orchestrator/skills/decomposition/SKILL.md +35 -0
  209. package/src/orchestrator/skills/frontend-design/SKILL.md +27 -1
  210. package/src/orchestrator/skills/project-consistency/SKILL.md +350 -0
@@ -75,9 +75,11 @@ export interface TaskRecord {
75
75
  dispute_id: string | null
76
76
  drift_score: number | null
77
77
  drift_retried: number
78
+ compaction_count: number
78
79
  outputs?: string | null // JSON array of TaskOutput
79
80
  inputs?: string | null // JSON array of TaskInput
80
81
  discovered_issues?: string | null // JSON array
82
+ contract_result?: string | null // JSON ContractResult
81
83
  }
82
84
 
83
85
  export interface WorkerRecord {
@@ -117,6 +119,15 @@ export interface PipelineRecord {
117
119
  total_cost_usd: number | null
118
120
  }
119
121
 
122
+ export interface TDDGateConfig {
123
+ enabled: boolean
124
+ source_patterns: string[]
125
+ test_patterns: string[]
126
+ exclude_patterns: string[]
127
+ mode: 'warn' | 'block'
128
+ exempt_agents: string[]
129
+ }
130
+
120
131
  export interface BuiltInGatesConfig {
121
132
  secret_scan?: boolean
122
133
  blast_radius?: boolean
@@ -124,6 +135,7 @@ export interface BuiltInGatesConfig {
124
135
  regression_test?: 'auto' | boolean
125
136
  browser_test?: 'auto' | boolean
126
137
  gate_timeout?: number
138
+ tdd_check?: boolean | TDDGateConfig
127
139
  }
128
140
 
129
141
 
@@ -163,6 +175,12 @@ export interface CircuitBreakerConfig {
163
175
  fallback_agent?: string // reassign pending tasks when circuit opens
164
176
  }
165
177
 
178
+ export interface CompactionConfig {
179
+ enabled: boolean
180
+ token_threshold_pct: number // e.g., 70 = compact at 70% of model context window
181
+ summary_max_tokens: number // max tokens for the compaction summary
182
+ }
183
+
166
184
  export interface TaskOutput {
167
185
  name: string
168
186
  type: 'file' | 'summary' | 'json'
@@ -260,6 +278,23 @@ export interface MCPServerConfig {
260
278
  config?: Record<string, unknown>
261
279
  }
262
280
 
281
+ // ── Two-stage review ─────────────────────────────────────────────────────────
282
+
283
+ export type ReviewStage = 'spec-compliance' | 'code-quality'
284
+
285
+ export interface StageVerdict {
286
+ stage: ReviewStage
287
+ verdict: 'pass' | 'block'
288
+ issues: string[]
289
+ tokens_used: number
290
+ }
291
+
292
+ export interface TwoStageReviewResult {
293
+ stages: StageVerdict[]
294
+ overall_verdict: 'pass' | 'block'
295
+ total_tokens: number
296
+ }
297
+
263
298
  // ---------------------------------------------------------------------------
264
299
  // Discriminated union covering every canonical convoy event type.
265
300
  // Each variant constrains the `data` shape that callers may pass to emit().
@@ -321,6 +356,14 @@ export type ConvoyEventType =
321
356
  | { type: 'watch_stopped'; data?: { reason?: string } }
322
357
  | { type: 'worker_killed'; data?: { reason?: string; worker_id?: string; task_id?: string } }
323
358
  | { type: 'discovered_issue'; data?: { task_id?: string; title?: string; file?: string; description?: string; severity?: string } }
359
+ | { type: 'contract_violation'; data?: { task_id?: string; agent?: string; missing?: string[]; warnings?: string[] } }
360
+ | { type: 'review_stage_completed'; data?: { stage: string; verdict: string; tokens: number; task_id?: string; model?: string } }
361
+ | { type: 'partition_violation'; data?: { task_id?: string; allowed?: string[]; actual?: string[]; violations?: string[] } }
362
+ | { type: 'context_compacted'; data?: { task_id?: string; compaction_count?: number; summary_path?: string; model?: string; tokens_used?: number } }
363
+ | { type: 'skill_refinement_proposed'; data?: { skill_name?: string; proposal_path?: string; failure_count?: number; confidence?: string } }
364
+ | { type: 'tdd_check_passed'; data?: { task_id?: string; new_source_files?: number; existing_test_files?: number } }
365
+ | { type: 'tdd_check_failed'; data?: { task_id?: string; missing_test_files?: string[]; new_source_files?: number } }
366
+ | { type: 'tdd_check_skipped'; data?: { task_id?: string; reason?: string; agent?: string } }
324
367
 
325
368
  /** All canonical convoy event type strings. Used for runtime validation. */
326
369
  export const KNOWN_EVENT_TYPES: Set<string> = new Set<ConvoyEventType['type']>([
@@ -363,4 +406,12 @@ export const KNOWN_EVENT_TYPES: Set<string> = new Set<ConvoyEventType['type']>([
363
406
  'watch_stopped',
364
407
  'worker_killed',
365
408
  'discovered_issue',
409
+ 'contract_violation',
410
+ 'review_stage_completed',
411
+ 'partition_violation',
412
+ 'context_compacted',
413
+ 'skill_refinement_proposed',
414
+ 'tdd_check_passed',
415
+ 'tdd_check_failed',
416
+ 'tdd_check_skipped',
366
417
  ])
@@ -185,6 +185,61 @@ export async function startDashboardServer(
185
185
  pathname = '/index.html'
186
186
  }
187
187
 
188
+ // Handle refresh requests — re-run ETL on demand
189
+ if (pathname === '/data/refresh' && runtimeDataDir) {
190
+ try {
191
+ const { runEtl } = await import('../dashboard/scripts/etl.js')
192
+ const dbPath = resolve(projectRoot, '.opencastle', 'convoy.db')
193
+ const result = await runEtl({ dbPath, outputDir: runtimeDataDir })
194
+ res.writeHead(200, { 'Content-Type': 'application/json' })
195
+ res.end(JSON.stringify({ ok: true, convoyCount: result.convoyCount }))
196
+ } catch (err) {
197
+ res.writeHead(200, { 'Content-Type': 'application/json' })
198
+ res.end(JSON.stringify({ ok: true, convoyCount: 0 }))
199
+ }
200
+ return
201
+ }
202
+
203
+ // Handle active convoy resolution — queries DB directly (no ETL needed)
204
+ if (pathname === '/data/active-convoy.json' && !seed) {
205
+ try {
206
+ const { createConvoyStore } = await import('./convoy/store.js')
207
+ const dbPath = resolve(projectRoot, '.opencastle', 'convoy.db')
208
+ const { existsSync } = await import('node:fs')
209
+ if (!existsSync(dbPath)) {
210
+ res.writeHead(200, { 'Content-Type': 'application/json' })
211
+ res.end(JSON.stringify({ convoy: null, pipeline: null }))
212
+ return
213
+ }
214
+ const store = createConvoyStore(dbPath)
215
+ try {
216
+ // Find the running convoy, or fall back to the latest
217
+ const allConvoys = store.getConvoyList(10, 0)
218
+ const running = allConvoys.find(c => c.status === 'running')
219
+ const pending = allConvoys.find(c => c.status === 'pending')
220
+ const target = running ?? pending ?? (allConvoys.length > 0 ? allConvoys[0] : null)
221
+
222
+ let pipelineConvoys: Array<{ id: string; name: string; status: string }> | null = null
223
+ if (target?.pipeline_id) {
224
+ const pipeConvoys = store.getConvoysByPipeline(target.pipeline_id)
225
+ pipelineConvoys = pipeConvoys.map(c => ({ id: c.id, name: c.name, status: c.status }))
226
+ }
227
+
228
+ res.writeHead(200, { 'Content-Type': 'application/json' })
229
+ res.end(JSON.stringify({
230
+ convoy: target ? { id: target.id, name: target.name, status: target.status, pipeline_id: target.pipeline_id ?? null } : null,
231
+ pipeline: pipelineConvoys,
232
+ }))
233
+ } finally {
234
+ store.close()
235
+ }
236
+ } catch {
237
+ res.writeHead(200, { 'Content-Type': 'application/json' })
238
+ res.end(JSON.stringify({ convoy: null, pipeline: null }))
239
+ }
240
+ return
241
+ }
242
+
188
243
  // Handle data file requests — proxy to project logs or dist
189
244
  const dataMatch = pathname.match(/^\/data\/(.+\.ndjson)$/)
190
245
  if (dataMatch && DATA_FILES.includes(dataMatch[1])) {
@@ -0,0 +1,99 @@
1
+ import { stat } from 'node:fs/promises'
2
+ import { join, dirname } from 'node:path'
3
+ import type { CliContext } from './types.js'
4
+ import { createConvoyStore } from './convoy/store.js'
5
+ import { analyzeDAG, formatInsightsMarkdown, formatInsightsJSON } from './convoy/dag-analysis.js'
6
+
7
+ const HELP = `
8
+ opencastle insights [options]
9
+
10
+ Analyze convoy execution history and generate recommendations.
11
+
12
+ Options:
13
+ --json Output machine-readable JSON instead of markdown
14
+ --since <days> Limit analysis window (default: 90 days)
15
+ --db <path> Path to convoy.db (default: auto-detect)
16
+ --help, -h Show this help
17
+ `
18
+
19
+ /** Walk up the directory tree to find .opencastle/convoy.db. */
20
+ async function findConvoyDb(override?: string | null): Promise<string | null> {
21
+ if (override) return override
22
+
23
+ let dir = process.cwd()
24
+ for (;;) {
25
+ const candidate = join(dir, '.opencastle', 'convoy.db')
26
+ try {
27
+ const s = await stat(candidate)
28
+ if (s.isFile()) return candidate
29
+ } catch {
30
+ // not found here — walk up
31
+ }
32
+ const parent = dirname(dir)
33
+ if (parent === dir) break
34
+ dir = parent
35
+ }
36
+ return null
37
+ }
38
+
39
+ export default async function insights({ args }: CliContext): Promise<void> {
40
+ if (args.includes('--help') || args.includes('-h')) {
41
+ console.log(HELP)
42
+ return
43
+ }
44
+
45
+ let jsonMode = false
46
+ let sinceDays = 90
47
+ let dbOverride: string | null = null
48
+
49
+ for (let i = 0; i < args.length; i++) {
50
+ const arg = args[i]
51
+ if (arg === '--json') {
52
+ jsonMode = true
53
+ } else if (arg === '--since') {
54
+ const raw = args[++i]
55
+ const parsed = parseInt(raw, 10)
56
+ if (isNaN(parsed) || parsed <= 0) {
57
+ console.error(' \u2717 --since requires a positive integer (number of days)')
58
+ process.exit(1)
59
+ }
60
+ sinceDays = parsed
61
+ } else if (arg === '--db') {
62
+ dbOverride = args[++i] ?? null
63
+ }
64
+ }
65
+
66
+ const dbPath = await findConvoyDb(dbOverride)
67
+ if (!dbPath) {
68
+ if (jsonMode) {
69
+ console.log(
70
+ JSON.stringify(
71
+ {
72
+ patterns: [],
73
+ agent_stats: [],
74
+ insights: ['No execution history available yet. Run some convoys first.'],
75
+ generated_at: new Date().toISOString(),
76
+ },
77
+ null,
78
+ 2,
79
+ ),
80
+ )
81
+ } else {
82
+ console.log(' No convoy.db found. Run some convoys first.\n')
83
+ console.log(' Tip: convoy.db is created automatically when you run `opencastle run`.')
84
+ }
85
+ return
86
+ }
87
+
88
+ const store = createConvoyStore(dbPath)
89
+ try {
90
+ const rec = analyzeDAG(store, sinceDays)
91
+ if (jsonMode) {
92
+ console.log(formatInsightsJSON(rec))
93
+ } else {
94
+ console.log(formatInsightsMarkdown(rec))
95
+ }
96
+ } finally {
97
+ store.close()
98
+ }
99
+ }
package/src/cli/lesson.ts CHANGED
@@ -38,6 +38,7 @@ const HELP = `
38
38
  --correct <text> The correct approach that works
39
39
  --why <text> Root cause explanation
40
40
  --customizations-dir <p> Override the customizations directory path
41
+ --dry-run Preview what would be appended without writing
41
42
  --help, -h Show this help
42
43
 
43
44
  Examples:
@@ -230,6 +231,7 @@ export default async function lesson({ args }: CliContext): Promise<void> {
230
231
  return
231
232
  }
232
233
 
234
+ const dryRun = args.includes('--dry-run') || args.includes('--dryRun')
233
235
  let title: string | null = null
234
236
  let category: string | null = null
235
237
  let severity: string | null = null
@@ -299,6 +301,12 @@ export default async function lesson({ args }: CliContext): Promise<void> {
299
301
  process.exit(1)
300
302
  }
301
303
 
304
+ if (dryRun) {
305
+ console.log(` [dry-run] Would append lesson to LESSONS-LEARNED.md:`)
306
+ console.log(` Title: ${title}`)
307
+ return
308
+ }
309
+
302
310
  try {
303
311
  const id = await appendLesson(
304
312
  { title: title!, category: category!, severity: severity!, problem: problem!, wrong, correct, why },
package/src/cli/log.ts CHANGED
@@ -11,6 +11,7 @@ const HELP = `
11
11
  --type <type> Event type (required): session|delegation|review|panel|dispute
12
12
  --<field> <value> Any field from the event schema (see documentation)
13
13
  --logs-dir <path> Override the logs directory path
14
+ --dry-run Preview what would be logged without writing
14
15
  --help, -h Show this help
15
16
 
16
17
  Array fields (comma-separated): file_partition, lessons_added, discoveries, reviewing_agents
@@ -77,6 +78,7 @@ export default async function log({ args }: CliContext): Promise<void> {
77
78
  return
78
79
  }
79
80
 
81
+ const dryRun = args.includes('--dry-run') || args.includes('--dryRun')
80
82
  let type: string | null = null
81
83
  let logsDir: string | null = null
82
84
  const fields: Record<string, unknown> = {}
@@ -122,6 +124,12 @@ export default async function log({ args }: CliContext): Promise<void> {
122
124
  delete fields['timestamp']
123
125
  const record = { type, timestamp, ...fields }
124
126
 
127
+ if (dryRun) {
128
+ console.log(` [dry-run] Would append to events.ndjson:`)
129
+ console.log(JSON.stringify(record))
130
+ return
131
+ }
132
+
125
133
  try {
126
134
  await appendEvent(record, logsDir)
127
135
  console.log(JSON.stringify(record))
@@ -0,0 +1,48 @@
1
+ // Platform skill compatibility -- not all skills are relevant on all platforms
2
+
3
+ export interface PlatformSkillConfig {
4
+ platform: 'claude-code' | 'cursor' | 'opencode' | 'gemini'
5
+ excludedSkills: string[]
6
+ displayName: string
7
+ manifestFile: string
8
+ entryPoint: string
9
+ outputDir: string
10
+ includedDirs: ('skills' | 'agents' | 'instructions' | 'prompts' | 'agent-workflows')[]
11
+ }
12
+
13
+ const SESSION_ONLY = ['session-checkpoints']
14
+ const CONCURRENT_ONLY = ['panel-majority-vote']
15
+ const TEAM_LEAD_ONLY = [
16
+ 'orchestration-protocols', 'team-lead-reference', 'decomposition',
17
+ 'agent-memory', 'context-map', 'fast-review', 'memory-merger',
18
+ ]
19
+ const SIMPLE_PLATFORM_EXCLUSIONS = [...SESSION_ONLY, ...CONCURRENT_ONLY, ...TEAM_LEAD_ONLY]
20
+
21
+ export const PLATFORM_CONFIGS: Record<string, PlatformSkillConfig> = {
22
+ 'claude-code': {
23
+ platform: 'claude-code', excludedSkills: [], displayName: 'Claude Code',
24
+ manifestFile: 'manifest.json', entryPoint: 'CLAUDE.md', outputDir: 'claude-code',
25
+ includedDirs: ['skills', 'agents', 'instructions', 'prompts', 'agent-workflows'],
26
+ },
27
+ cursor: {
28
+ platform: 'cursor', excludedSkills: [...SESSION_ONLY], displayName: 'Cursor',
29
+ manifestFile: 'manifest.json', entryPoint: '.cursorrules', outputDir: 'cursor',
30
+ includedDirs: ['skills', 'agents', 'instructions'],
31
+ },
32
+ opencode: {
33
+ platform: 'opencode', excludedSkills: [...SIMPLE_PLATFORM_EXCLUSIONS], displayName: 'OpenCode',
34
+ manifestFile: 'manifest.json', entryPoint: 'OPENCODE.md', outputDir: 'opencode',
35
+ includedDirs: ['skills', 'agents', 'instructions'],
36
+ },
37
+ gemini: {
38
+ platform: 'gemini', excludedSkills: [...SIMPLE_PLATFORM_EXCLUSIONS], displayName: 'Gemini CLI',
39
+ manifestFile: 'gemini-extension.json', entryPoint: 'GEMINI.md', outputDir: 'gemini',
40
+ includedDirs: ['skills', 'agents', 'instructions'],
41
+ },
42
+ }
43
+
44
+ export function getSkillsForPlatform(platform: string, allSkills: string[]): string[] {
45
+ const config = PLATFORM_CONFIGS[platform]
46
+ if (!config) return allSkills
47
+ return allSkills.filter(skill => !config.excludedSkills.includes(skill))
48
+ }
@@ -0,0 +1,276 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mkdtempSync, rmSync, existsSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { join, resolve } from 'node:path'
5
+ import {
6
+ readVersion,
7
+ listSkillDirs,
8
+ generateManifest,
9
+ generateEntryPoint,
10
+ generateReadme,
11
+ buildPluginPackage,
12
+ parseArgs,
13
+ } from './package.js'
14
+ import { PLATFORM_CONFIGS, getSkillsForPlatform } from './package-config.js'
15
+
16
+ const pkgRoot = resolve(process.cwd())
17
+
18
+ describe('readVersion', () => {
19
+ it('reads the correct version from package.json', () => {
20
+ const version = readVersion(pkgRoot)
21
+ expect(version).toMatch(/^\d+\.\d+\.\d+$/)
22
+ })
23
+ })
24
+
25
+ describe('listSkillDirs', () => {
26
+ it('returns skill directory names', () => {
27
+ const skills = listSkillDirs(pkgRoot)
28
+ expect(skills.length).toBeGreaterThan(0)
29
+ expect(skills).toContain('react-development')
30
+ expect(skills).toContain('testing-workflow')
31
+ })
32
+ it('returns only directory names (not paths)', () => {
33
+ const skills = listSkillDirs(pkgRoot)
34
+ expect(skills.every(s => !s.includes('/'))).toBe(true)
35
+ })
36
+ })
37
+
38
+ describe('getSkillsForPlatform', () => {
39
+ const allSkills = [
40
+ 'react-development', 'session-checkpoints', 'panel-majority-vote',
41
+ 'orchestration-protocols', 'team-lead-reference', 'testing-workflow',
42
+ 'decomposition', 'agent-memory', 'context-map', 'fast-review', 'memory-merger',
43
+ ]
44
+
45
+ it('returns all skills for claude-code (fewest exclusions)', () => {
46
+ const skills = getSkillsForPlatform('claude-code', allSkills)
47
+ expect(skills).toEqual(allSkills)
48
+ })
49
+
50
+ it('excludes session-checkpoints for cursor', () => {
51
+ const skills = getSkillsForPlatform('cursor', allSkills)
52
+ expect(skills).not.toContain('session-checkpoints')
53
+ expect(skills).toContain('react-development')
54
+ })
55
+
56
+ it('excludes team-lead skills from opencode', () => {
57
+ const skills = getSkillsForPlatform('opencode', allSkills)
58
+ expect(skills).not.toContain('session-checkpoints')
59
+ expect(skills).not.toContain('panel-majority-vote')
60
+ expect(skills).not.toContain('orchestration-protocols')
61
+ expect(skills).not.toContain('team-lead-reference')
62
+ expect(skills).not.toContain('decomposition')
63
+ expect(skills).not.toContain('agent-memory')
64
+ expect(skills).not.toContain('context-map')
65
+ expect(skills).not.toContain('fast-review')
66
+ expect(skills).not.toContain('memory-merger')
67
+ expect(skills).toContain('react-development')
68
+ expect(skills).toContain('testing-workflow')
69
+ })
70
+
71
+ it('excludes team-lead skills from gemini', () => {
72
+ const skills = getSkillsForPlatform('gemini', allSkills)
73
+ expect(skills).not.toContain('session-checkpoints')
74
+ expect(skills).not.toContain('orchestration-protocols')
75
+ })
76
+
77
+ it('returns all skills for unknown platform', () => {
78
+ const skills = getSkillsForPlatform('unknown-platform', allSkills)
79
+ expect(skills).toEqual(allSkills)
80
+ })
81
+ })
82
+
83
+ describe('generateManifest', () => {
84
+ const skills = ['react-development', 'testing-workflow']
85
+ const agents = ['developer', 'reviewer']
86
+ const version = '0.31.6'
87
+
88
+ it('generates correct structure for claude-code', () => {
89
+ const m = generateManifest('claude-code', version, skills, agents) as Record<string, unknown>
90
+ expect(m.name).toBe('opencastle')
91
+ expect(m.version).toBe(version)
92
+ expect(m.skills).toEqual(skills)
93
+ expect(m.agents).toEqual(agents)
94
+ expect(m.hooks).toEqual(['SessionStart'])
95
+ })
96
+
97
+ it('generates correct structure for cursor', () => {
98
+ const m = generateManifest('cursor', version, skills, agents) as Record<string, unknown>
99
+ expect(m.name).toBe('opencastle')
100
+ expect(m.version).toBe(version)
101
+ expect(m.hooks).toBeUndefined()
102
+ expect(m.type).toBeUndefined()
103
+ })
104
+
105
+ it('generates correct structure for opencode', () => {
106
+ const m = generateManifest('opencode', version, skills, agents) as Record<string, unknown>
107
+ expect(m.name).toBe('opencastle')
108
+ expect(m.version).toBe(version)
109
+ expect(m.type).toBeUndefined()
110
+ })
111
+
112
+ it('generates correct structure for gemini (includes type: extension)', () => {
113
+ const m = generateManifest('gemini', version, skills, agents) as Record<string, unknown>
114
+ expect(m.name).toBe('opencastle')
115
+ expect(m.version).toBe(version)
116
+ expect(m.type).toBe('extension')
117
+ })
118
+
119
+ it('includes version from package.json', () => {
120
+ const ver = readVersion(pkgRoot)
121
+ const m = generateManifest('cursor', ver, skills, agents) as Record<string, unknown>
122
+ expect(m.version).toBe(ver)
123
+ })
124
+ })
125
+
126
+ describe('generateEntryPoint', () => {
127
+ const skills = ['react-development', 'testing-workflow']
128
+ const version = '1.0.0'
129
+
130
+ it('generates CLAUDE.md content for claude-code', () => {
131
+ const ep = generateEntryPoint('claude-code', version, skills)
132
+ expect(ep).toContain('OpenCastle v' + version)
133
+ expect(ep).toContain('react-development')
134
+ expect(ep).toContain('Claude Code')
135
+ })
136
+
137
+ it('generates .cursorrules content for cursor', () => {
138
+ const ep = generateEntryPoint('cursor', version, skills)
139
+ expect(ep).toContain('OpenCastle v' + version)
140
+ expect(ep).toContain('Cursor')
141
+ expect(ep).toContain('react-development')
142
+ })
143
+
144
+ it('generates OPENCODE.md content for opencode', () => {
145
+ const ep = generateEntryPoint('opencode', version, skills)
146
+ expect(ep).toContain('OpenCastle v' + version)
147
+ expect(ep).toContain('react-development')
148
+ })
149
+
150
+ it('generates GEMINI.md content for gemini', () => {
151
+ const ep = generateEntryPoint('gemini', version, skills)
152
+ expect(ep).toContain('OpenCastle v' + version)
153
+ expect(ep).toContain('react-development')
154
+ })
155
+ })
156
+
157
+ describe('generateReadme', () => {
158
+ it('contains installation instructions', () => {
159
+ const readme = generateReadme('claude-code', '1.0.0')
160
+ expect(readme).toContain('Installation')
161
+ expect(readme).toContain('Claude Code')
162
+ expect(readme).toContain('1.0.0')
163
+ })
164
+
165
+ it('references the correct entry point', () => {
166
+ const readme = generateReadme('cursor', '1.0.0')
167
+ expect(readme).toContain('.cursorrules')
168
+ })
169
+ })
170
+
171
+ describe('PLATFORM_CONFIGS', () => {
172
+ it('has configs for all 4 platforms', () => {
173
+ expect(PLATFORM_CONFIGS).toHaveProperty('claude-code')
174
+ expect(PLATFORM_CONFIGS).toHaveProperty('cursor')
175
+ expect(PLATFORM_CONFIGS).toHaveProperty('opencode')
176
+ expect(PLATFORM_CONFIGS).toHaveProperty('gemini')
177
+ })
178
+
179
+ it('each platform config has required fields', () => {
180
+ for (const [, config] of Object.entries(PLATFORM_CONFIGS)) {
181
+ expect(config.outputDir).toBeTruthy()
182
+ expect(config.manifestFile).toBeTruthy()
183
+ expect(config.entryPoint).toBeTruthy()
184
+ expect(Array.isArray(config.includedDirs)).toBe(true)
185
+ expect(config.includedDirs.length).toBeGreaterThan(0)
186
+ }
187
+ })
188
+
189
+ it('gemini uses gemini-extension.json manifest', () => {
190
+ expect(PLATFORM_CONFIGS.gemini.manifestFile).toBe('gemini-extension.json')
191
+ })
192
+
193
+ it('cursor uses .cursorrules entry point', () => {
194
+ expect(PLATFORM_CONFIGS.cursor.entryPoint).toBe('.cursorrules')
195
+ })
196
+ })
197
+
198
+ describe('buildPluginPackage', () => {
199
+ it('creates correct directory structure for claude-code', () => {
200
+ const tmpDir = mkdtempSync(join(tmpdir(), 'oc-test-'))
201
+ try {
202
+ const result = buildPluginPackage(pkgRoot, 'claude-code', tmpDir)
203
+ expect(result.platform).toBe('claude-code')
204
+ expect(result.skillCount).toBeGreaterThan(0)
205
+ expect(result.agentCount).toBeGreaterThan(0)
206
+ expect(existsSync(join(result.outputDir, 'manifest.json'))).toBe(true)
207
+ expect(existsSync(join(result.outputDir, 'CLAUDE.md'))).toBe(true)
208
+ expect(existsSync(join(result.outputDir, 'README.md'))).toBe(true)
209
+ expect(existsSync(join(result.outputDir, 'skills'))).toBe(true)
210
+ expect(existsSync(join(result.outputDir, 'agents'))).toBe(true)
211
+ } finally {
212
+ rmSync(tmpDir, { recursive: true, force: true })
213
+ }
214
+ })
215
+
216
+ it('respects platform skill filtering for gemini', () => {
217
+ const tmpDir = mkdtempSync(join(tmpdir(), 'oc-test-'))
218
+ try {
219
+ const result = buildPluginPackage(pkgRoot, 'gemini', tmpDir)
220
+ expect(existsSync(join(result.outputDir, 'skills', 'session-checkpoints'))).toBe(false)
221
+ expect(existsSync(join(result.outputDir, 'skills', 'react-development'))).toBe(true)
222
+ } finally {
223
+ rmSync(tmpDir, { recursive: true, force: true })
224
+ }
225
+ })
226
+
227
+ it('throws for unknown platform', () => {
228
+ const tmpDir = mkdtempSync(join(tmpdir(), 'oc-test-'))
229
+ try {
230
+ expect(() => buildPluginPackage(pkgRoot, 'unknown-platform', tmpDir)).toThrow()
231
+ } finally {
232
+ rmSync(tmpDir, { recursive: true, force: true })
233
+ }
234
+ })
235
+ })
236
+
237
+ describe('parseArgs', () => {
238
+ it('parses --platform flag', () => {
239
+ const opts = parseArgs(['--platform', 'cursor'])
240
+ expect(opts.platform).toBe('cursor')
241
+ expect(opts.all).toBe(false)
242
+ })
243
+
244
+ it('parses -p shorthand', () => {
245
+ const opts = parseArgs(['-p', 'gemini'])
246
+ expect(opts.platform).toBe('gemini')
247
+ })
248
+
249
+ it('parses --all flag', () => {
250
+ const opts = parseArgs(['--all'])
251
+ expect(opts.all).toBe(true)
252
+ })
253
+
254
+ it('parses --output flag', () => {
255
+ const opts = parseArgs(['--output', '/tmp/out'])
256
+ expect(opts.output).toBe('/tmp/out')
257
+ })
258
+
259
+ it('parses -o shorthand', () => {
260
+ const opts = parseArgs(['-o', '/tmp/out'])
261
+ expect(opts.output).toBe('/tmp/out')
262
+ })
263
+
264
+ it('parses --help flag', () => {
265
+ const opts = parseArgs(['--help'])
266
+ expect(opts.help).toBe(true)
267
+ })
268
+
269
+ it('returns defaults for empty args', () => {
270
+ const opts = parseArgs([])
271
+ expect(opts.platform).toBeNull()
272
+ expect(opts.all).toBe(false)
273
+ expect(opts.output).toBe('dist/plugins')
274
+ expect(opts.help).toBe(false)
275
+ })
276
+ })