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
@@ -0,0 +1,329 @@
1
+ import { readFileSync, existsSync, mkdirSync, readdirSync, cpSync, writeFileSync } from 'node:fs'
2
+ import { resolve, join } from 'node:path'
3
+ import { c } from './prompt.js'
4
+ import type { CliContext } from './types.js'
5
+ import { PLATFORM_CONFIGS, getSkillsForPlatform } from './package-config.js'
6
+
7
+ const HELP = `
8
+ opencastle package [options]
9
+
10
+ Package OpenCastle orchestrator as a plugin for IDE marketplaces.
11
+
12
+ Options:
13
+ --platform, -p <name> Target platform (claude-code, cursor, opencode, gemini)
14
+ --all Generate packages for all platforms
15
+ --output, -o <dir> Output directory (default: dist/plugins)
16
+ --dry-run Preview what would be built without writing files
17
+ --help, -h Show this help
18
+ `
19
+
20
+ export interface PackageArgs {
21
+ platform: string | null
22
+ all: boolean
23
+ output: string
24
+ dryRun: boolean
25
+ help: boolean
26
+ }
27
+
28
+ export function parseArgs(args: string[]): PackageArgs {
29
+ const opts: PackageArgs = { platform: null, all: false, output: 'dist/plugins', dryRun: false, help: false }
30
+ for (let i = 0; i < args.length; i++) {
31
+ const arg = args[i]
32
+ if (arg === '--help' || arg === '-h') {
33
+ opts.help = true
34
+ } else if (arg === '--all') {
35
+ opts.all = true
36
+ } else if (arg === '--dry-run' || arg === '--dryRun') {
37
+ opts.dryRun = true
38
+ } else if ((arg === '--platform' || arg === '-p') && args[i + 1]) {
39
+ opts.platform = args[++i]
40
+ } else if ((arg === '--output' || arg === '-o') && args[i + 1]) {
41
+ opts.output = args[++i]
42
+ }
43
+ }
44
+ return opts
45
+ }
46
+
47
+ export function readVersion(pkgRoot: string): string {
48
+ const pkgPath = resolve(pkgRoot, 'package.json')
49
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { version: string }
50
+ return pkg.version
51
+ }
52
+
53
+ export function listSkillDirs(pkgRoot: string): string[] {
54
+ const skillsPath = resolve(pkgRoot, 'src/orchestrator/skills')
55
+ if (!existsSync(skillsPath)) return []
56
+ return readdirSync(skillsPath, { withFileTypes: true })
57
+ .filter(d => d.isDirectory())
58
+ .map(d => d.name)
59
+ .sort()
60
+ }
61
+
62
+ export function generateManifest(
63
+ platform: string,
64
+ version: string,
65
+ skills: string[],
66
+ agents: string[],
67
+ ): object {
68
+ const base = {
69
+ name: 'opencastle',
70
+ version,
71
+ description: 'OpenCastle multi-agent orchestration framework',
72
+ skills,
73
+ agents,
74
+ }
75
+ if (platform === 'claude-code') {
76
+ return { ...base, hooks: ['SessionStart'] }
77
+ }
78
+ if (platform === 'gemini') {
79
+ return { ...base, type: 'extension' }
80
+ }
81
+ return base
82
+ }
83
+
84
+ export function generateEntryPoint(platform: string, version: string, skills: string[]): string {
85
+ const skillList = skills.map(s => '- ' + s).join('\n')
86
+ switch (platform) {
87
+ case 'claude-code':
88
+ return [
89
+ '# OpenCastle v' + version,
90
+ '',
91
+ 'Multi-agent orchestration framework for Claude Code.',
92
+ '',
93
+ '## Available Skills',
94
+ '',
95
+ skillList,
96
+ '',
97
+ '## Usage',
98
+ '',
99
+ 'Skills are available in the `skills/` directory. Reference them in your agent instructions.',
100
+ '',
101
+ ].join('\n')
102
+ case 'cursor':
103
+ return [
104
+ '# OpenCastle v' + version + ' -- Cursor Rules',
105
+ '',
106
+ 'Multi-agent orchestration framework for Cursor.',
107
+ '',
108
+ '## Skills',
109
+ '',
110
+ skillList,
111
+ '',
112
+ '## Instructions',
113
+ '',
114
+ 'Reference skill files using the `skills/` directory in your workspace.',
115
+ '',
116
+ ].join('\n')
117
+ case 'opencode':
118
+ return [
119
+ '# OpenCastle v' + version,
120
+ '',
121
+ 'Multi-agent orchestration framework for OpenCode.',
122
+ '',
123
+ '## Setup',
124
+ '',
125
+ 'Copy this package to your project and reference skill files as needed.',
126
+ '',
127
+ '## Available Skills',
128
+ '',
129
+ skillList,
130
+ '',
131
+ ].join('\n')
132
+ case 'gemini':
133
+ return [
134
+ '# OpenCastle v' + version,
135
+ '',
136
+ 'Multi-agent orchestration framework for Gemini CLI.',
137
+ '',
138
+ '## Tool Mapping',
139
+ '',
140
+ 'Each skill maps to a specialized tool capability.',
141
+ '',
142
+ '## Available Skills',
143
+ '',
144
+ skillList,
145
+ '',
146
+ ].join('\n')
147
+ default:
148
+ return '# OpenCastle v' + version + '\n'
149
+ }
150
+ }
151
+
152
+ export function generateReadme(platform: string, version: string): string {
153
+ const config = PLATFORM_CONFIGS[platform]
154
+ const displayName = config?.displayName ?? platform
155
+ return [
156
+ '# OpenCastle Plugin for ' + displayName,
157
+ '',
158
+ 'Version: ' + version,
159
+ '',
160
+ '## Installation',
161
+ '',
162
+ 'Copy the contents of this directory to your ' + displayName + ' workspace or plugin directory.',
163
+ '',
164
+ '## Usage',
165
+ '',
166
+ 'Reference the `' + (config?.entryPoint ?? 'README.md') + '` file in your ' + displayName + ' configuration.',
167
+ '',
168
+ '## Skills',
169
+ '',
170
+ 'See the `skills/` directory for available skills.',
171
+ '',
172
+ '## Agents',
173
+ '',
174
+ 'See the `agents/` directory for available agent definitions.',
175
+ '',
176
+ '## More Info',
177
+ '',
178
+ 'Visit https://www.opencastle.dev/ for documentation.',
179
+ '',
180
+ ].join('\n')
181
+ }
182
+
183
+ export interface BuildResult {
184
+ platform: string
185
+ outputDir: string
186
+ skillCount: number
187
+ agentCount: number
188
+ }
189
+
190
+ export function buildPluginPackage(
191
+ pkgRoot: string,
192
+ platform: string,
193
+ outputBase: string,
194
+ ): BuildResult {
195
+ const config = PLATFORM_CONFIGS[platform]
196
+ if (!config) {
197
+ throw new Error('Unknown platform: ' + platform + '. Valid: ' + Object.keys(PLATFORM_CONFIGS).join(', '))
198
+ }
199
+ const outputDir = resolve(outputBase, config.outputDir)
200
+ mkdirSync(outputDir, { recursive: true })
201
+
202
+ const version = readVersion(pkgRoot)
203
+ const allSkills = listSkillDirs(pkgRoot)
204
+ const filteredSkills = getSkillsForPlatform(platform, allSkills)
205
+
206
+ if (config.includedDirs.includes('skills')) {
207
+ const skillsOut = join(outputDir, 'skills')
208
+ mkdirSync(skillsOut, { recursive: true })
209
+ const skillsSrc = resolve(pkgRoot, 'src/orchestrator/skills')
210
+ for (const skill of filteredSkills) {
211
+ const src = join(skillsSrc, skill)
212
+ if (existsSync(src)) {
213
+ cpSync(src, join(skillsOut, skill), { recursive: true })
214
+ }
215
+ }
216
+ }
217
+
218
+ const agentsSrc = resolve(pkgRoot, 'src/orchestrator/agents')
219
+ const agentFiles = existsSync(agentsSrc)
220
+ ? readdirSync(agentsSrc).filter(f => f.endsWith('.agent.md'))
221
+ : []
222
+ if (config.includedDirs.includes('agents')) {
223
+ const agentsOut = join(outputDir, 'agents')
224
+ mkdirSync(agentsOut, { recursive: true })
225
+ for (const f of agentFiles) {
226
+ cpSync(join(agentsSrc, f), join(agentsOut, f))
227
+ }
228
+ }
229
+
230
+ if (config.includedDirs.includes('instructions')) {
231
+ const instrSrc = resolve(pkgRoot, 'src/orchestrator/instructions')
232
+ if (existsSync(instrSrc)) {
233
+ const instrOut = join(outputDir, 'instructions')
234
+ mkdirSync(instrOut, { recursive: true })
235
+ const files = readdirSync(instrSrc).filter(f => f.endsWith('.instructions.md'))
236
+ for (const f of files) {
237
+ cpSync(join(instrSrc, f), join(instrOut, f))
238
+ }
239
+ }
240
+ }
241
+
242
+ if (config.includedDirs.includes('prompts')) {
243
+ const promptsSrc = resolve(pkgRoot, 'src/orchestrator/prompts')
244
+ if (existsSync(promptsSrc)) {
245
+ const promptsOut = join(outputDir, 'prompts')
246
+ mkdirSync(promptsOut, { recursive: true })
247
+ const files = readdirSync(promptsSrc).filter(f => f.endsWith('.prompt.md'))
248
+ for (const f of files) {
249
+ cpSync(join(promptsSrc, f), join(promptsOut, f))
250
+ }
251
+ }
252
+ }
253
+
254
+ if (config.includedDirs.includes('agent-workflows')) {
255
+ const wfSrc = resolve(pkgRoot, 'src/orchestrator/agent-workflows')
256
+ if (existsSync(wfSrc)) {
257
+ const wfOut = join(outputDir, 'agent-workflows')
258
+ mkdirSync(wfOut, { recursive: true })
259
+ const files = readdirSync(wfSrc).filter(f => f.endsWith('.md'))
260
+ for (const f of files) {
261
+ cpSync(join(wfSrc, f), join(wfOut, f))
262
+ }
263
+ }
264
+ }
265
+
266
+ const agentNames = agentFiles.map(f => f.replace('.agent.md', ''))
267
+ const manifest = generateManifest(platform, version, filteredSkills, agentNames)
268
+ writeFileSync(join(outputDir, config.manifestFile), JSON.stringify(manifest, null, 2) + '\n', 'utf8')
269
+
270
+ const entryContent = generateEntryPoint(platform, version, filteredSkills)
271
+ writeFileSync(join(outputDir, config.entryPoint), entryContent, 'utf8')
272
+
273
+ const readmeContent = generateReadme(platform, version)
274
+ writeFileSync(join(outputDir, 'README.md'), readmeContent, 'utf8')
275
+
276
+ return { platform, outputDir, skillCount: filteredSkills.length, agentCount: agentFiles.length }
277
+ }
278
+
279
+ export default async function packageCmd({ args, pkgRoot }: CliContext): Promise<void> {
280
+ const opts = parseArgs(args)
281
+ if (opts.help) {
282
+ console.log(HELP)
283
+ return
284
+ }
285
+
286
+ const platforms = opts.all
287
+ ? Object.keys(PLATFORM_CONFIGS)
288
+ : opts.platform
289
+ ? [opts.platform]
290
+ : null
291
+ if (!platforms) {
292
+ console.error(' ' + c.red('✗') + ' Specify --platform <name> or --all')
293
+ console.log(HELP)
294
+ process.exit(1)
295
+ }
296
+
297
+ if (opts.dryRun) {
298
+ console.log(` [dry-run] Would build packages for: ${platforms.join(', ')}`)
299
+ console.log(` [dry-run] Output directory: ${resolve(process.cwd(), opts.output)}`)
300
+ return
301
+ }
302
+
303
+ const outputBase = resolve(process.cwd(), opts.output)
304
+ const results: BuildResult[] = []
305
+ const errors: string[] = []
306
+
307
+ for (const platform of platforms) {
308
+ try {
309
+ console.log(' ' + c.dim('→') + ' Building ' + platform + '...')
310
+ const result = buildPluginPackage(pkgRoot, platform, outputBase)
311
+ results.push(result)
312
+ console.log(
313
+ ' ' + c.green('✓') + ' ' + platform + ': ' +
314
+ result.skillCount + ' skills, ' + result.agentCount + ' agents → ' + result.outputDir
315
+ )
316
+ } catch (err) {
317
+ const msg = (err as Error).message
318
+ errors.push(platform + ': ' + msg)
319
+ console.error(' ' + c.red('✗') + ' ' + platform + ': ' + msg)
320
+ }
321
+ }
322
+
323
+ console.log()
324
+ if (errors.length > 0) {
325
+ console.error(' ' + c.red('✗') + ' ' + errors.length + ' platform(s) failed')
326
+ process.exit(1)
327
+ }
328
+ console.log(' ' + c.green('✔') + ' ' + results.length + ' package(s) built in ' + outputBase)
329
+ }
@@ -18,6 +18,17 @@ export interface ConvoyGroup {
18
18
  depends_on: string[]
19
19
  }
20
20
 
21
+ function appendTaskComplexity(base: string, taskComplexity: ComplexityAssessment['task_complexity']): string {
22
+ if (!taskComplexity?.length) return base
23
+ let result = base + '\n\n## Pre-Computed Task Complexity\n\n'
24
+ result += '| Workstream | Phase | Complexity | Rationale |\n'
25
+ result += '|-----------|-------|-----------|----------|\n'
26
+ for (const tc of taskComplexity) {
27
+ result += `| ${tc.workstream} | ${tc.phase} | ${tc.complexity} | ${tc.rationale} |\n`
28
+ }
29
+ return result
30
+ }
31
+
21
32
  export interface ComplexityAssessment {
22
33
  original_prompt: string
23
34
  total_tasks: number
@@ -28,6 +39,12 @@ export interface ComplexityAssessment {
28
39
  recommended_strategy: 'single' | 'chain'
29
40
  chain_rationale?: string
30
41
  convoy_groups: ConvoyGroup[]
42
+ task_complexity?: Array<{
43
+ workstream: string
44
+ phase: number
45
+ complexity: 1 | 2 | 3 | 5 | 8 | 13
46
+ rationale: string
47
+ }>
31
48
  }
32
49
 
33
50
  export function parseComplexityAssessment(jsonText: string): ComplexityAssessment | null {
@@ -572,12 +589,13 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
572
589
  ].filter(Boolean).join('\n')
573
590
 
574
591
  const prdContent = await readFile(prdPath, 'utf8')
592
+ const contextForSpec = appendTaskComplexity(prdContent, complexity?.task_complexity)
575
593
  const groupSpecPath = resolve(convoyDir, `${group.name}.convoy.yml`)
576
594
 
577
595
  const { specPath: resolvedGroupSpecPath } = await generateAndValidateSpec({
578
596
  sharedOpts,
579
597
  goalText: chainGoal,
580
- contextText: prdContent,
598
+ contextText: contextForSpec,
581
599
  specPath: groupSpecPath,
582
600
  skipValidation: opts.skipValidation,
583
601
  groupName: group.name,
@@ -643,12 +661,13 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
643
661
 
644
662
  // ── Generate convoy spec ──────────────────────────────────────────────────
645
663
  const singlePrdContent = await readFile(prdPath, 'utf8')
664
+ const singleContextForSpec = appendTaskComplexity(singlePrdContent, complexity?.task_complexity)
646
665
  const singleGoal = complexity?.original_prompt ?? opts.text ?? ''
647
666
 
648
667
  const specResult = await generateAndValidateSpec({
649
668
  sharedOpts,
650
669
  goalText: singleGoal,
651
- contextText: singlePrdContent,
670
+ contextText: singleContextForSpec,
652
671
  specPath: opts.outputSpec ? resolve(process.cwd(), opts.outputSpec) : undefined,
653
672
  skipValidation: opts.skipValidation,
654
673
  enrichment: complexity ? deriveSpecEnrichment(complexity) : undefined,
@@ -1830,6 +1830,64 @@ describe('validateSpec — built_in_gates config', () => {
1830
1830
  expect(result.valid).toBe(false)
1831
1831
  expect(result.errors).toContainEqual(expect.stringContaining('gate_timeout'))
1832
1832
  })
1833
+
1834
+ it('accepts tdd_check as boolean', () => {
1835
+ const result = validateSpec({
1836
+ ...validSpec,
1837
+ defaults: { built_in_gates: { tdd_check: true } },
1838
+ })
1839
+ expect(result.valid).toBe(true)
1840
+ })
1841
+
1842
+ it('accepts tdd_check as object', () => {
1843
+ const result = validateSpec({
1844
+ ...validSpec,
1845
+ defaults: { built_in_gates: { tdd_check: { enabled: true, mode: 'block' } } },
1846
+ })
1847
+ expect(result.valid).toBe(true)
1848
+ })
1849
+
1850
+ it('rejects tdd_check with invalid value', () => {
1851
+ const result = validateSpec({
1852
+ ...validSpec,
1853
+ defaults: { built_in_gates: { tdd_check: 'yes' } },
1854
+ })
1855
+ expect(result.valid).toBe(false)
1856
+ expect(result.errors).toContainEqual(expect.stringContaining('tdd_check'))
1857
+ })
1858
+ })
1859
+
1860
+ describe('validateSpec — review_stages', () => {
1861
+ const validSpec = {
1862
+ name: 'test-run',
1863
+ version: 1,
1864
+ tasks: [{ id: 'task-1', prompt: 'Do something' }],
1865
+ }
1866
+
1867
+ it('accepts review_stages as true', () => {
1868
+ const result = validateSpec({
1869
+ ...validSpec,
1870
+ defaults: { review_stages: true },
1871
+ })
1872
+ expect(result.valid).toBe(true)
1873
+ })
1874
+
1875
+ it('accepts review_stages as false', () => {
1876
+ const result = validateSpec({
1877
+ ...validSpec,
1878
+ defaults: { review_stages: false },
1879
+ })
1880
+ expect(result.valid).toBe(true)
1881
+ })
1882
+
1883
+ it('rejects review_stages with non-boolean value', () => {
1884
+ const result = validateSpec({
1885
+ ...validSpec,
1886
+ defaults: { review_stages: 'yes' },
1887
+ })
1888
+ expect(result.valid).toBe(false)
1889
+ expect(result.errors).toContainEqual(expect.stringContaining('review_stages'))
1890
+ })
1833
1891
  })
1834
1892
 
1835
1893
  describe('validateSpec — browser_test config', () => {
@@ -268,6 +268,10 @@ export function validateSpec(spec: unknown): ValidationResult {
268
268
  } else {
269
269
  const bg = d.built_in_gates as Record<string, unknown>
270
270
  const boolOrAutoFields = ['secret_scan', 'blast_radius', 'dependency_audit', 'regression_test', 'browser_test'] as const
271
+ // tdd_check can be boolean or object (TDDGateConfig)
272
+ if (bg.tdd_check !== undefined && typeof bg.tdd_check !== 'boolean' && (typeof bg.tdd_check !== 'object' || Array.isArray(bg.tdd_check) || bg.tdd_check === null)) {
273
+ errors.push('`defaults.built_in_gates.tdd_check` must be a boolean or an object')
274
+ }
271
275
  for (const field of boolOrAutoFields) {
272
276
  if (bg[field] !== undefined && typeof bg[field] !== 'boolean' && bg[field] !== 'auto') {
273
277
  errors.push(`\`defaults.built_in_gates.${field}\` must be a boolean or "auto"`)
@@ -287,6 +291,35 @@ export function validateSpec(spec: unknown): ValidationResult {
287
291
  validateBrowserTestConfig(d.browser_test, 'defaults.browser_test', errors)
288
292
  }
289
293
 
294
+ // compaction config validation (Phase 44)
295
+ if (d.compaction !== undefined) {
296
+ if (!d.compaction || typeof d.compaction !== 'object' || Array.isArray(d.compaction)) {
297
+ errors.push('`defaults.compaction` must be an object')
298
+ } else {
299
+ const comp = d.compaction as Record<string, unknown>
300
+ if (comp.enabled !== undefined && typeof comp.enabled !== 'boolean') {
301
+ errors.push('`defaults.compaction.enabled` must be a boolean')
302
+ }
303
+ if (comp.token_threshold_pct !== undefined) {
304
+ const pct = Number(comp.token_threshold_pct)
305
+ if (!Number.isFinite(pct) || pct < 1 || pct > 100) {
306
+ errors.push('`defaults.compaction.token_threshold_pct` must be a number between 1 and 100')
307
+ }
308
+ }
309
+ if (comp.summary_max_tokens !== undefined) {
310
+ const smt = Number(comp.summary_max_tokens)
311
+ if (!Number.isInteger(smt) || smt < 1) {
312
+ errors.push('`defaults.compaction.summary_max_tokens` must be a positive integer')
313
+ }
314
+ }
315
+ }
316
+ }
317
+
318
+ // review_stages validation (Phase 40 — two-stage review toggle)
319
+ if (d.review_stages !== undefined && typeof d.review_stages !== 'boolean') {
320
+ errors.push('`defaults.review_stages` must be a boolean')
321
+ }
322
+
290
323
  // review validation
291
324
  const VALID_REVIEW = ['auto', 'fast', 'panel', 'none']
292
325
  if (d.review !== undefined && !VALID_REVIEW.includes(d.review as string)) {
package/src/cli/run.ts CHANGED
@@ -532,7 +532,9 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
532
532
  const { createConvoyStore } = await import('./convoy/store.js')
533
533
  const store = createConvoyStore(dbPath)
534
534
  const latestPipeline = store.getLatestPipeline()
535
- if (latestPipeline && (latestPipeline.status === 'pending' || latestPipeline.status === 'running')) {
535
+
536
+ // ── Pipeline resume (pending / running / failed) ────────────
537
+ if (latestPipeline && latestPipeline.status !== 'done') {
536
538
  store.close()
537
539
  const resumePipelineSpec = parseTaskSpecText(latestPipeline.spec_yaml)
538
540
  if (opts.concurrency !== null) resumePipelineSpec.concurrency = opts.concurrency
@@ -572,6 +574,17 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
572
574
  process.exit(resumePipelineResult.status !== 'done' ? 1 : 0)
573
575
  }
574
576
 
577
+ // ── Pipeline done — don't fall through to convoy check ──────
578
+ if (latestPipeline && latestPipeline.status === 'done') {
579
+ store.close()
580
+ console.error(
581
+ ` ✗ Last pipeline "${latestPipeline.name}" already finished with status: done`
582
+ )
583
+ console.error(` Only interrupted (running/pending/failed) pipelines can be resumed.`)
584
+ process.exit(1)
585
+ }
586
+
587
+ // ── Standalone convoy resume ────────────────────────────────
575
588
  const convoy = store.getLatestConvoy()
576
589
  store.close()
577
590
  if (!convoy) {
@@ -0,0 +1,121 @@
1
+ import type { CliContext } from './types.js'
2
+ import { c } from './prompt.js'
3
+
4
+ const HELP = `
5
+ opencastle skills [subcommand]
6
+
7
+ Skill refinement and failure tracking.
8
+
9
+ Subcommands:
10
+ refine Scan for failure patterns and generate refinement proposals
11
+ failures Show failure stats per skill
12
+
13
+ Options:
14
+ --dry-run Preview what proposals would be generated without saving
15
+ --help, -h Show this help
16
+ `
17
+
18
+ function parseSkillsArgs(args: string[]): { subcommand: string | null; help: boolean; dryRun: boolean } {
19
+ const opts = { subcommand: null as string | null, help: false, dryRun: false }
20
+ for (const arg of args) {
21
+ if (arg === '--help' || arg === '-h') {
22
+ opts.help = true
23
+ } else if (arg === '--dry-run' || arg === '--dryRun') {
24
+ opts.dryRun = true
25
+ } else if (!arg.startsWith('--')) {
26
+ opts.subcommand ??= arg
27
+ }
28
+ }
29
+ return opts
30
+ }
31
+
32
+ export default async function skills({ args }: CliContext): Promise<void> {
33
+ const opts = parseSkillsArgs(args)
34
+
35
+ if (opts.help || !opts.subcommand) {
36
+ console.log(HELP)
37
+ return
38
+ }
39
+
40
+ const {
41
+ getFailureStats,
42
+ getSkillFailures,
43
+ detectFailurePatterns,
44
+ generateRefinementProposal,
45
+ saveProposal,
46
+ } = await import('./convoy/skill-refinement.js')
47
+
48
+ switch (opts.subcommand) {
49
+ case 'refine': {
50
+ const stats = getFailureStats()
51
+ const uniqueSkills = stats.map(s => s.skill_name)
52
+
53
+ if (uniqueSkills.length === 0) {
54
+ console.log(' No skill failures recorded yet.')
55
+ return
56
+ }
57
+
58
+ const generated: Array<{ skill: string; path: string }> = []
59
+ for (const skillName of uniqueSkills) {
60
+ const failures = getSkillFailures(skillName)
61
+ const { threshold_met } = detectFailurePatterns(failures)
62
+ if (!threshold_met) continue
63
+ const proposal = generateRefinementProposal(skillName, failures)
64
+ const proposalPath = opts.dryRun
65
+ ? `.opencastle/proposals/${skillName}-refinement.md`
66
+ : saveProposal(proposal, undefined, failures)
67
+ generated.push({ skill: skillName, path: proposalPath })
68
+ }
69
+
70
+ if (generated.length === 0) {
71
+ console.log(' No skills currently meet the refinement threshold.')
72
+ } else if (opts.dryRun) {
73
+ console.log(` [dry-run] Would generate ${generated.length} refinement proposal${generated.length === 1 ? '' : 's'}:`)
74
+ for (const g of generated) {
75
+ console.log(` ${c.dim('◆')} ${g.skill}: ${g.path}`)
76
+ }
77
+ } else {
78
+ console.log(` ${c.green('✔')} ${generated.length} skill${generated.length === 1 ? '' : 's'} have refinement proposals. Review in .opencastle/proposals/`)
79
+ for (const g of generated) {
80
+ console.log(` ${c.dim('◆')} ${g.skill}: ${g.path}`)
81
+ }
82
+ }
83
+ break
84
+ }
85
+
86
+ case 'failures': {
87
+ const stats = getFailureStats()
88
+ if (stats.length === 0) {
89
+ console.log(' No skill failures recorded yet.')
90
+ return
91
+ }
92
+
93
+ const colW = [30, 10, 30, 26]
94
+ const header = [
95
+ 'Skill'.padEnd(colW[0]),
96
+ 'Failures'.padEnd(colW[1]),
97
+ 'Agents'.padEnd(colW[2]),
98
+ 'Latest',
99
+ ].join(' ')
100
+ console.log(`\n ${c.bold(header)}`)
101
+ console.log(' ' + '-'.repeat(header.length))
102
+
103
+ for (const s of stats) {
104
+ const row = [
105
+ s.skill_name.slice(0, colW[0] - 1).padEnd(colW[0]),
106
+ String(s.count).padEnd(colW[1]),
107
+ s.agents.join(', ').slice(0, colW[2] - 1).padEnd(colW[2]),
108
+ s.latest.slice(0, 10),
109
+ ].join(' ')
110
+ console.log(` ${row}`)
111
+ }
112
+ console.log('')
113
+ break
114
+ }
115
+
116
+ default:
117
+ console.error(` ${c.red('✗')} Unknown subcommand: ${opts.subcommand}`)
118
+ console.log(HELP)
119
+ process.exit(1)
120
+ }
121
+ }
package/src/cli/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ChildProcess } from 'node:child_process';
2
- import type { BuiltInGatesConfig, BrowserTestConfig, GuardConfig, CircuitBreakerConfig, TaskStep, Hook, TaskOutput, TaskInput, WatchConfig, MCPServerConfig } from './convoy/types.js';
2
+ import type { BuiltInGatesConfig, BrowserTestConfig, GuardConfig, CircuitBreakerConfig, CompactionConfig, TaskStep, Hook, TaskOutput, TaskInput, WatchConfig, MCPServerConfig } from './convoy/types.js';
3
3
 
4
4
  // ── Stack selection types ──────────────────────────────────────
5
5
 
@@ -165,6 +165,7 @@ export interface TaskDefaults {
165
165
  escalate_to?: string;
166
166
  circuit_breaker?: CircuitBreakerConfig;
167
167
  review?: 'auto' | 'fast' | 'panel' | 'none';
168
+ review_stages?: boolean;
168
169
  reviewer_model?: string;
169
170
  review_budget?: number;
170
171
  on_review_budget_exceeded?: 'skip' | 'downgrade' | 'stop';
@@ -188,6 +189,8 @@ export interface TaskDefaults {
188
189
  mcp_server_approval_timeout?: number;
189
190
  /** Browser test gate configuration for default built-in gates. */
190
191
  browser_test?: BrowserTestConfig;
192
+ /** Auto-context compaction configuration (Phase 44). */
193
+ compaction?: CompactionConfig;
191
194
  }
192
195
 
193
196
  /** Validated task spec from YAML. */
package/src/cli/update.ts CHANGED
@@ -19,8 +19,8 @@ const UPDATE_HELP = `
19
19
 
20
20
  Options:
21
21
  --dry-run Preview what would be changed without writing files
22
- --force Overwrite customized files (default: skip)
23
- --reconfigure Re-run IDE selection and reconfigure adapters
22
+ --force Force update even if versions match
23
+ --reconfigure Re-run IDE and stack selection
24
24
  --help, -h Show this help
25
25
  `
26
26