opencastle 0.1.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 (224) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +215 -0
  3. package/bin/cli.mjs +69 -0
  4. package/dist/cli/adapters/claude-code.d.ts +22 -0
  5. package/dist/cli/adapters/claude-code.d.ts.map +1 -0
  6. package/dist/cli/adapters/claude-code.js +237 -0
  7. package/dist/cli/adapters/claude-code.js.map +1 -0
  8. package/dist/cli/adapters/cursor.d.ts +20 -0
  9. package/dist/cli/adapters/cursor.d.ts.map +1 -0
  10. package/dist/cli/adapters/cursor.js +231 -0
  11. package/dist/cli/adapters/cursor.js.map +1 -0
  12. package/dist/cli/adapters/vscode.d.ts +20 -0
  13. package/dist/cli/adapters/vscode.d.ts.map +1 -0
  14. package/dist/cli/adapters/vscode.js +132 -0
  15. package/dist/cli/adapters/vscode.js.map +1 -0
  16. package/dist/cli/copy.d.ts +14 -0
  17. package/dist/cli/copy.d.ts.map +1 -0
  18. package/dist/cli/copy.js +62 -0
  19. package/dist/cli/copy.js.map +1 -0
  20. package/dist/cli/dashboard.d.ts +3 -0
  21. package/dist/cli/dashboard.d.ts.map +1 -0
  22. package/dist/cli/dashboard.js +183 -0
  23. package/dist/cli/dashboard.js.map +1 -0
  24. package/dist/cli/diff.d.ts +3 -0
  25. package/dist/cli/diff.d.ts.map +1 -0
  26. package/dist/cli/diff.js +27 -0
  27. package/dist/cli/diff.js.map +1 -0
  28. package/dist/cli/eject.d.ts +3 -0
  29. package/dist/cli/eject.d.ts.map +1 -0
  30. package/dist/cli/eject.js +27 -0
  31. package/dist/cli/eject.js.map +1 -0
  32. package/dist/cli/init.d.ts +3 -0
  33. package/dist/cli/init.d.ts.map +1 -0
  34. package/dist/cli/init.js +92 -0
  35. package/dist/cli/init.js.map +1 -0
  36. package/dist/cli/manifest.d.ts +14 -0
  37. package/dist/cli/manifest.d.ts.map +1 -0
  38. package/dist/cli/manifest.js +34 -0
  39. package/dist/cli/manifest.js.map +1 -0
  40. package/dist/cli/mcp.d.ts +14 -0
  41. package/dist/cli/mcp.d.ts.map +1 -0
  42. package/dist/cli/mcp.js +35 -0
  43. package/dist/cli/mcp.js.map +1 -0
  44. package/dist/cli/prompt.d.ts +12 -0
  45. package/dist/cli/prompt.d.ts.map +1 -0
  46. package/dist/cli/prompt.js +104 -0
  47. package/dist/cli/prompt.js.map +1 -0
  48. package/dist/cli/run/adapters/claude-code.d.ts +16 -0
  49. package/dist/cli/run/adapters/claude-code.d.ts.map +1 -0
  50. package/dist/cli/run/adapters/claude-code.js +82 -0
  51. package/dist/cli/run/adapters/claude-code.js.map +1 -0
  52. package/dist/cli/run/adapters/copilot.d.ts +16 -0
  53. package/dist/cli/run/adapters/copilot.d.ts.map +1 -0
  54. package/dist/cli/run/adapters/copilot.js +84 -0
  55. package/dist/cli/run/adapters/copilot.js.map +1 -0
  56. package/dist/cli/run/adapters/cursor.d.ts +16 -0
  57. package/dist/cli/run/adapters/cursor.d.ts.map +1 -0
  58. package/dist/cli/run/adapters/cursor.js +81 -0
  59. package/dist/cli/run/adapters/cursor.js.map +1 -0
  60. package/dist/cli/run/adapters/index.d.ts +14 -0
  61. package/dist/cli/run/adapters/index.d.ts.map +1 -0
  62. package/dist/cli/run/adapters/index.js +35 -0
  63. package/dist/cli/run/adapters/index.js.map +1 -0
  64. package/dist/cli/run/executor.d.ts +15 -0
  65. package/dist/cli/run/executor.d.ts.map +1 -0
  66. package/dist/cli/run/executor.js +249 -0
  67. package/dist/cli/run/executor.js.map +1 -0
  68. package/dist/cli/run/reporter.d.ts +10 -0
  69. package/dist/cli/run/reporter.d.ts.map +1 -0
  70. package/dist/cli/run/reporter.js +112 -0
  71. package/dist/cli/run/reporter.js.map +1 -0
  72. package/dist/cli/run/schema.d.ts +28 -0
  73. package/dist/cli/run/schema.d.ts.map +1 -0
  74. package/dist/cli/run/schema.js +511 -0
  75. package/dist/cli/run/schema.js.map +1 -0
  76. package/dist/cli/run.d.ts +6 -0
  77. package/dist/cli/run.d.ts.map +1 -0
  78. package/dist/cli/run.js +123 -0
  79. package/dist/cli/run.js.map +1 -0
  80. package/dist/cli/stack-config.d.ts +12 -0
  81. package/dist/cli/stack-config.d.ts.map +1 -0
  82. package/dist/cli/stack-config.js +146 -0
  83. package/dist/cli/stack-config.js.map +1 -0
  84. package/dist/cli/types.d.ts +169 -0
  85. package/dist/cli/types.d.ts.map +1 -0
  86. package/dist/cli/types.js +2 -0
  87. package/dist/cli/types.js.map +1 -0
  88. package/dist/cli/update.d.ts +3 -0
  89. package/dist/cli/update.d.ts.map +1 -0
  90. package/dist/cli/update.js +50 -0
  91. package/dist/cli/update.js.map +1 -0
  92. package/package.json +48 -0
  93. package/src/cli/adapters/claude-code.ts +287 -0
  94. package/src/cli/adapters/cursor.ts +377 -0
  95. package/src/cli/adapters/vscode.ts +168 -0
  96. package/src/cli/copy.ts +79 -0
  97. package/src/cli/dashboard.ts +225 -0
  98. package/src/cli/diff.ts +44 -0
  99. package/src/cli/eject.ts +39 -0
  100. package/src/cli/init.ts +120 -0
  101. package/src/cli/manifest.ts +45 -0
  102. package/src/cli/mcp.ts +49 -0
  103. package/src/cli/prompt.ts +115 -0
  104. package/src/cli/run/adapters/claude-code.ts +95 -0
  105. package/src/cli/run/adapters/copilot.ts +97 -0
  106. package/src/cli/run/adapters/cursor.ts +94 -0
  107. package/src/cli/run/adapters/index.ts +40 -0
  108. package/src/cli/run/executor.ts +292 -0
  109. package/src/cli/run/reporter.ts +129 -0
  110. package/src/cli/run/schema.ts +595 -0
  111. package/src/cli/run.ts +137 -0
  112. package/src/cli/stack-config.ts +180 -0
  113. package/src/cli/types.ts +207 -0
  114. package/src/cli/update.ts +75 -0
  115. package/src/dashboard/astro.config.mjs +6 -0
  116. package/src/dashboard/package-lock.json +5455 -0
  117. package/src/dashboard/package.json +14 -0
  118. package/src/dashboard/public/data/delegations.ndjson +35 -0
  119. package/src/dashboard/public/data/panels.ndjson +13 -0
  120. package/src/dashboard/public/data/sessions.ndjson +50 -0
  121. package/src/dashboard/public/icon-192.png +0 -0
  122. package/src/dashboard/scripts/generate-seed-data.ts +355 -0
  123. package/src/dashboard/src/layouts/Layout.astro +25 -0
  124. package/src/dashboard/src/pages/index.astro +1070 -0
  125. package/src/dashboard/src/styles/dashboard.css +1078 -0
  126. package/src/dashboard/tsconfig.json +6 -0
  127. package/src/orchestrator/agent-workflows/README.md +22 -0
  128. package/src/orchestrator/agent-workflows/bug-fix.md +128 -0
  129. package/src/orchestrator/agent-workflows/data-pipeline.md +145 -0
  130. package/src/orchestrator/agent-workflows/database-migration.md +159 -0
  131. package/src/orchestrator/agent-workflows/feature-implementation.md +223 -0
  132. package/src/orchestrator/agent-workflows/performance-optimization.md +125 -0
  133. package/src/orchestrator/agent-workflows/refactoring.md +142 -0
  134. package/src/orchestrator/agent-workflows/schema-changes.md +164 -0
  135. package/src/orchestrator/agent-workflows/security-audit.md +148 -0
  136. package/src/orchestrator/agent-workflows/shared-delivery-phase.md +33 -0
  137. package/src/orchestrator/agents/api-designer.agent.md +68 -0
  138. package/src/orchestrator/agents/architect.agent.md +129 -0
  139. package/src/orchestrator/agents/content-engineer.agent.md +57 -0
  140. package/src/orchestrator/agents/copywriter.agent.md +95 -0
  141. package/src/orchestrator/agents/data-expert.agent.md +63 -0
  142. package/src/orchestrator/agents/database-engineer.agent.md +62 -0
  143. package/src/orchestrator/agents/developer.agent.md +66 -0
  144. package/src/orchestrator/agents/devops-expert.agent.md +57 -0
  145. package/src/orchestrator/agents/documentation-writer.agent.md +60 -0
  146. package/src/orchestrator/agents/performance-expert.agent.md +58 -0
  147. package/src/orchestrator/agents/release-manager.agent.md +72 -0
  148. package/src/orchestrator/agents/researcher.agent.md +145 -0
  149. package/src/orchestrator/agents/reviewer.agent.md +62 -0
  150. package/src/orchestrator/agents/security-expert.agent.md +64 -0
  151. package/src/orchestrator/agents/seo-specialist.agent.md +67 -0
  152. package/src/orchestrator/agents/team-lead.agent.md +644 -0
  153. package/src/orchestrator/agents/testing-expert.agent.md +85 -0
  154. package/src/orchestrator/agents/ui-ux-expert.agent.md +63 -0
  155. package/src/orchestrator/copilot-instructions.md +3 -0
  156. package/src/orchestrator/customizations/AGENT-EXPERTISE.md +325 -0
  157. package/src/orchestrator/customizations/AGENT-FAILURES.md +69 -0
  158. package/src/orchestrator/customizations/AGENT-PERFORMANCE.md +58 -0
  159. package/src/orchestrator/customizations/DISPUTES.md +162 -0
  160. package/src/orchestrator/customizations/KNOWLEDGE-GRAPH.md +10 -0
  161. package/src/orchestrator/customizations/LESSONS-LEARNED.md +70 -0
  162. package/src/orchestrator/customizations/README.md +59 -0
  163. package/src/orchestrator/customizations/agents/agent-registry.md +46 -0
  164. package/src/orchestrator/customizations/agents/skill-matrix.md +142 -0
  165. package/src/orchestrator/customizations/logs/README.md +181 -0
  166. package/src/orchestrator/customizations/logs/delegations.ndjson +1 -0
  167. package/src/orchestrator/customizations/logs/panels.ndjson +1 -0
  168. package/src/orchestrator/customizations/logs/sessions.ndjson +1 -0
  169. package/src/orchestrator/customizations/project/docs-structure.md +23 -0
  170. package/src/orchestrator/customizations/project/tracker-config.md +45 -0
  171. package/src/orchestrator/customizations/project.instructions.md +64 -0
  172. package/src/orchestrator/customizations/stack/api-config.md +37 -0
  173. package/src/orchestrator/customizations/stack/cms-config.md +26 -0
  174. package/src/orchestrator/customizations/stack/data-pipeline-config.md +41 -0
  175. package/src/orchestrator/customizations/stack/database-config.md +44 -0
  176. package/src/orchestrator/customizations/stack/deployment-config.md +45 -0
  177. package/src/orchestrator/customizations/stack/testing-config.md +56 -0
  178. package/src/orchestrator/instructions/ai-optimization.instructions.md +143 -0
  179. package/src/orchestrator/instructions/general.instructions.md +194 -0
  180. package/src/orchestrator/mcp.json +55 -0
  181. package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +235 -0
  182. package/src/orchestrator/prompts/brainstorm.prompt.md +115 -0
  183. package/src/orchestrator/prompts/bug-fix.prompt.md +141 -0
  184. package/src/orchestrator/prompts/create-skill.prompt.md +103 -0
  185. package/src/orchestrator/prompts/generate-task-spec.prompt.md +154 -0
  186. package/src/orchestrator/prompts/implement-feature.prompt.md +124 -0
  187. package/src/orchestrator/prompts/metrics-report.prompt.md +142 -0
  188. package/src/orchestrator/prompts/quick-refinement.prompt.md +137 -0
  189. package/src/orchestrator/prompts/resolve-pr-comments.prompt.md +100 -0
  190. package/src/orchestrator/skills/accessibility-standards/SKILL.md +164 -0
  191. package/src/orchestrator/skills/agent-hooks/SKILL.md +147 -0
  192. package/src/orchestrator/skills/agent-memory/SKILL.md +144 -0
  193. package/src/orchestrator/skills/api-patterns/SKILL.md +106 -0
  194. package/src/orchestrator/skills/browser-testing/SKILL.md +203 -0
  195. package/src/orchestrator/skills/code-commenting/SKILL.md +133 -0
  196. package/src/orchestrator/skills/contentful-cms/SKILL.md +43 -0
  197. package/src/orchestrator/skills/context-map/SKILL.md +135 -0
  198. package/src/orchestrator/skills/convex-database/SKILL.md +80 -0
  199. package/src/orchestrator/skills/data-engineering/SKILL.md +99 -0
  200. package/src/orchestrator/skills/deployment-infrastructure/SKILL.md +49 -0
  201. package/src/orchestrator/skills/documentation-standards/SKILL.md +85 -0
  202. package/src/orchestrator/skills/fast-review/SKILL.md +327 -0
  203. package/src/orchestrator/skills/frontend-design/SKILL.md +42 -0
  204. package/src/orchestrator/skills/jira-management/SKILL.md +168 -0
  205. package/src/orchestrator/skills/memory-merger/SKILL.md +123 -0
  206. package/src/orchestrator/skills/nextjs-patterns/SKILL.md +75 -0
  207. package/src/orchestrator/skills/nx-workspace/SKILL.md +192 -0
  208. package/src/orchestrator/skills/panel-majority-vote/SKILL.md +184 -0
  209. package/src/orchestrator/skills/panel-majority-vote/panel-report.template.md +38 -0
  210. package/src/orchestrator/skills/performance-optimization/SKILL.md +101 -0
  211. package/src/orchestrator/skills/react-development/SKILL.md +117 -0
  212. package/src/orchestrator/skills/sanity-cms/SKILL.md +18 -0
  213. package/src/orchestrator/skills/security-hardening/SKILL.md +118 -0
  214. package/src/orchestrator/skills/self-improvement/SKILL.md +137 -0
  215. package/src/orchestrator/skills/seo-patterns/SKILL.md +40 -0
  216. package/src/orchestrator/skills/session-checkpoints/SKILL.md +205 -0
  217. package/src/orchestrator/skills/slack-notifications/SKILL.md +211 -0
  218. package/src/orchestrator/skills/strapi-cms/SKILL.md +43 -0
  219. package/src/orchestrator/skills/supabase-database/SKILL.md +24 -0
  220. package/src/orchestrator/skills/task-management/SKILL.md +143 -0
  221. package/src/orchestrator/skills/team-lead-reference/SKILL.md +317 -0
  222. package/src/orchestrator/skills/teams-notifications/SKILL.md +249 -0
  223. package/src/orchestrator/skills/testing-workflow/SKILL.md +134 -0
  224. package/src/orchestrator/skills/validation-gates/SKILL.md +100 -0
@@ -0,0 +1,292 @@
1
+ import { parseTimeout } from './schema.js'
2
+ import type {
3
+ Task,
4
+ TaskSpec,
5
+ TaskStatus,
6
+ TaskResult,
7
+ RunReport,
8
+ RunSummary,
9
+ AgentAdapter,
10
+ ExecuteResult,
11
+ Reporter,
12
+ Executor,
13
+ TimeoutHandle,
14
+ } from '../types.js'
15
+
16
+ /**
17
+ * Topological sort of tasks based on `depends_on` edges.
18
+ * Returns groups (phases) of tasks that can run in parallel.
19
+ */
20
+ export function buildPhases(tasks: Task[]): Task[][] {
21
+ const taskMap = new Map<string, Task>()
22
+ for (const t of tasks) taskMap.set(t.id, t)
23
+
24
+ const inDegree = new Map<string, number>()
25
+ const dependents = new Map<string, string[]>()
26
+
27
+ for (const t of tasks) {
28
+ inDegree.set(t.id, (t.depends_on || []).length)
29
+ dependents.set(t.id, [])
30
+ }
31
+
32
+ for (const t of tasks) {
33
+ for (const dep of t.depends_on || []) {
34
+ dependents.get(dep)!.push(t.id)
35
+ }
36
+ }
37
+
38
+ const phases: Task[][] = []
39
+ const remaining = new Set(tasks.map((t) => t.id))
40
+
41
+ while (remaining.size > 0) {
42
+ const phase: Task[] = []
43
+ for (const id of remaining) {
44
+ if (inDegree.get(id) === 0) {
45
+ phase.push(taskMap.get(id)!)
46
+ }
47
+ }
48
+
49
+ if (phase.length === 0) {
50
+ // Should not happen if cycle detection passed
51
+ throw new Error('Cannot resolve task order — possible circular dependency')
52
+ }
53
+
54
+ phases.push(phase)
55
+
56
+ for (const t of phase) {
57
+ remaining.delete(t.id)
58
+ for (const depId of dependents.get(t.id)!) {
59
+ inDegree.set(depId, inDegree.get(depId)! - 1)
60
+ }
61
+ }
62
+ }
63
+
64
+ return phases
65
+ }
66
+
67
+ /**
68
+ * Create a task executor.
69
+ */
70
+ export function createExecutor(
71
+ spec: TaskSpec,
72
+ adapter: AgentAdapter,
73
+ reporter: Reporter
74
+ ): Executor {
75
+ const phases = buildPhases(spec.tasks)
76
+ const statuses = new Map<string, TaskStatus>()
77
+ const results = new Map<string, TaskResult | null>()
78
+ const startTimes = new Map<string, number>()
79
+
80
+ for (const t of spec.tasks) {
81
+ statuses.set(t.id, 'pending')
82
+ results.set(t.id, null)
83
+ }
84
+
85
+ /**
86
+ * Execute a single task with timeout enforcement.
87
+ */
88
+ async function executeTask(task: Task): Promise<TaskResult> {
89
+ const timeoutMs = parseTimeout(task.timeout)
90
+ statuses.set(task.id, 'running')
91
+ startTimes.set(task.id, Date.now())
92
+ reporter.onTaskStart(task)
93
+
94
+ try {
95
+ const timeout = timeoutPromise(timeoutMs, task.id)
96
+ const result = await Promise.race([
97
+ adapter.execute(task, { verbose: spec._verbose }),
98
+ timeout.promise,
99
+ ])
100
+
101
+ const duration = Date.now() - startTimes.get(task.id)!
102
+
103
+ if (result._timedOut) {
104
+ // Kill the orphaned child process
105
+ if (typeof adapter.kill === 'function') {
106
+ adapter.kill(task)
107
+ }
108
+ statuses.set(task.id, 'timed-out')
109
+ const taskResult: TaskResult = {
110
+ id: task.id,
111
+ status: 'timed-out',
112
+ duration,
113
+ output: `Task timed out after ${task.timeout}`,
114
+ exitCode: -1,
115
+ }
116
+ results.set(task.id, taskResult)
117
+ reporter.onTaskDone(task, taskResult)
118
+ return taskResult
119
+ }
120
+
121
+ // Task completed normally — cancel the timeout timer
122
+ timeout.clear()
123
+ const status: TaskStatus = result.success ? 'done' : 'failed'
124
+ statuses.set(task.id, status)
125
+ const taskResult: TaskResult = {
126
+ id: task.id,
127
+ status,
128
+ duration,
129
+ output: result.output || '',
130
+ exitCode: result.exitCode,
131
+ }
132
+ results.set(task.id, taskResult)
133
+ reporter.onTaskDone(task, taskResult)
134
+ return taskResult
135
+ } catch (err: unknown) {
136
+ const duration = Date.now() - startTimes.get(task.id)!
137
+ statuses.set(task.id, 'failed')
138
+ const taskResult: TaskResult = {
139
+ id: task.id,
140
+ status: 'failed',
141
+ duration,
142
+ output: (err as Error).message,
143
+ exitCode: -1,
144
+ }
145
+ results.set(task.id, taskResult)
146
+ reporter.onTaskDone(task, taskResult)
147
+ return taskResult
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Skip a task and all its transitive dependents.
153
+ */
154
+ function skipTask(taskId: string, reason: string): void {
155
+ if (statuses.get(taskId) !== 'pending') return
156
+ statuses.set(taskId, 'skipped')
157
+ const task = spec.tasks.find((t) => t.id === taskId)!
158
+ results.set(taskId, {
159
+ id: taskId,
160
+ status: 'skipped',
161
+ duration: 0,
162
+ output: reason,
163
+ exitCode: -1,
164
+ })
165
+ reporter.onTaskSkipped(task, reason)
166
+
167
+ // Recursively skip dependents
168
+ for (const t of spec.tasks) {
169
+ if ((t.depends_on || []).includes(taskId)) {
170
+ skipTask(t.id, `dependency "${taskId}" was skipped/failed`)
171
+ }
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Run all tasks respecting phases and concurrency.
177
+ */
178
+ async function run(): Promise<RunReport> {
179
+ const startedAt = new Date()
180
+ let halted = false
181
+
182
+ for (let phaseIdx = 0; phaseIdx < phases.length; phaseIdx++) {
183
+ if (halted) break
184
+
185
+ const phase = phases[phaseIdx]
186
+ const eligible = phase.filter((t) => statuses.get(t.id) === 'pending')
187
+
188
+ if (eligible.length === 0) continue
189
+
190
+ reporter.onPhaseStart(phaseIdx + 1, eligible)
191
+
192
+ // Process eligible tasks in batches limited by concurrency
193
+ const concurrency = spec.concurrency
194
+ for (let i = 0; i < eligible.length; i += concurrency) {
195
+ if (halted) break
196
+ const batch = eligible.slice(i, i + concurrency)
197
+ const batchResults = await Promise.all(batch.map(executeTask))
198
+
199
+ for (const r of batchResults) {
200
+ if (r.status === 'failed' || r.status === 'timed-out') {
201
+ if (spec.on_failure === 'stop') {
202
+ halted = true
203
+ // Skip all remaining tasks
204
+ for (const t of spec.tasks) {
205
+ if (statuses.get(t.id) === 'pending') {
206
+ skipTask(t.id, 'execution halted due to on_failure: stop')
207
+ }
208
+ }
209
+ } else {
210
+ // on_failure: continue — skip dependents of this failed task
211
+ for (const t of spec.tasks) {
212
+ if ((t.depends_on || []).includes(r.id)) {
213
+ skipTask(t.id, `dependency "${r.id}" failed`)
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+
222
+ const completedAt = new Date()
223
+ const allResults: TaskResult[] = spec.tasks.map(
224
+ (t) =>
225
+ results.get(t.id) || {
226
+ id: t.id,
227
+ status: statuses.get(t.id) as TaskStatus,
228
+ duration: 0,
229
+ output: '',
230
+ exitCode: -1,
231
+ }
232
+ )
233
+
234
+ const summary: RunSummary = {
235
+ total: spec.tasks.length,
236
+ done: 0,
237
+ failed: 0,
238
+ skipped: 0,
239
+ 'timed-out': 0,
240
+ }
241
+ for (const r of allResults) {
242
+ if (r.status in summary) {
243
+ (summary as unknown as Record<string, number>)[r.status]++
244
+ }
245
+ }
246
+
247
+ const finalReport: RunReport = {
248
+ name: spec.name,
249
+ startedAt: startedAt.toISOString(),
250
+ completedAt: completedAt.toISOString(),
251
+ duration: formatDuration(completedAt.getTime() - startedAt.getTime()),
252
+ summary,
253
+ tasks: allResults,
254
+ }
255
+
256
+ await reporter.onComplete(finalReport)
257
+
258
+ return finalReport
259
+ }
260
+
261
+ return {
262
+ run,
263
+ getPhases: () => phases,
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Create a timeout promise that resolves with a sentinel.
269
+ * Returns { promise, clear } so the timer can be cancelled after normal completion.
270
+ */
271
+ function timeoutPromise(ms: number, taskId: string): TimeoutHandle {
272
+ let timerId: ReturnType<typeof setTimeout>
273
+ const promise = new Promise<ExecuteResult>((resolve) => {
274
+ timerId = setTimeout(() => resolve({ _timedOut: true, taskId, success: false, output: '', exitCode: -1 }), ms)
275
+ })
276
+ return { promise, clear: () => clearTimeout(timerId) }
277
+ }
278
+
279
+ /**
280
+ * Format a duration in ms to a human-readable string.
281
+ */
282
+ export function formatDuration(ms: number): string {
283
+ if (ms < 1000) return `${ms}ms`
284
+ const seconds = Math.floor(ms / 1000)
285
+ if (seconds < 60) return `${seconds}s`
286
+ const minutes = Math.floor(seconds / 60)
287
+ const remSec = seconds % 60
288
+ if (minutes < 60) return remSec > 0 ? `${minutes}m ${remSec}s` : `${minutes}m`
289
+ const hours = Math.floor(minutes / 60)
290
+ const remMin = minutes % 60
291
+ return remMin > 0 ? `${hours}h ${remMin}m` : `${hours}h`
292
+ }
@@ -0,0 +1,129 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises'
2
+ import { resolve } from 'node:path'
3
+ import { formatDuration } from './executor.js'
4
+ import type {
5
+ TaskSpec,
6
+ Task,
7
+ TaskResult,
8
+ RunReport,
9
+ Reporter,
10
+ ReporterOptions,
11
+ } from '../types.js'
12
+
13
+ /**
14
+ * Status icons for terminal output.
15
+ */
16
+ const ICONS: Record<string, string> = {
17
+ start: '▶',
18
+ done: '✓',
19
+ failed: '✗',
20
+ skipped: '⊘',
21
+ 'timed-out': '⏱',
22
+ }
23
+
24
+ /**
25
+ * Create a reporter that prints progress to the terminal and writes a JSON report.
26
+ */
27
+ export function createReporter(spec: TaskSpec, options: ReporterOptions = {}): Reporter {
28
+ const reportDir = options.reportDir || resolve(process.cwd(), '.opencastle', 'runs')
29
+ const verbose = options.verbose || false
30
+
31
+ return {
32
+ onTaskStart(task: Task): void {
33
+ console.log(` ${ICONS.start} [${task.id}] ${task.description}`)
34
+ },
35
+
36
+ onTaskDone(task: Task, result: TaskResult): void {
37
+ const dur = formatDuration(result.duration)
38
+ if (result.status === 'done') {
39
+ console.log(` ${ICONS.done} [${task.id}] completed (${dur})`)
40
+ } else if (result.status === 'timed-out') {
41
+ console.log(` ${ICONS['timed-out']} [${task.id}] timed out after ${task.timeout}`)
42
+ } else {
43
+ console.log(` ${ICONS.failed} [${task.id}] failed (${dur})`)
44
+ if (result.output) {
45
+ const lines = result.output.split('\n').slice(0, 5)
46
+ for (const line of lines) {
47
+ console.log(` ${line}`)
48
+ }
49
+ if (result.output.split('\n').length > 5) {
50
+ console.log(` ... (truncated)`)
51
+ }
52
+ }
53
+ }
54
+
55
+ if (verbose && result.output && result.status === 'done') {
56
+ console.log(` Output: ${result.output.slice(0, 500)}`)
57
+ }
58
+ },
59
+
60
+ onTaskSkipped(task: Task, reason: string): void {
61
+ console.log(` ${ICONS.skipped} [${task.id}] skipped — ${reason}`)
62
+ },
63
+
64
+ onPhaseStart(phase: number, tasks: Task[]): void {
65
+ const ids = tasks.map((t) => t.id).join(', ')
66
+ console.log(`\n Phase ${phase}: ${ids}`)
67
+ },
68
+
69
+ async onComplete(report: RunReport): Promise<void> {
70
+ console.log(`\n ──────────────────────────────────`)
71
+ console.log(` Run complete: ${report.name}`)
72
+ console.log(` Duration: ${report.duration}`)
73
+ console.log()
74
+
75
+ const s = report.summary
76
+ const parts: string[] = []
77
+ if (s.done > 0) parts.push(`${s.done} done`)
78
+ if (s.failed > 0) parts.push(`${s.failed} failed`)
79
+ if (s.skipped > 0) parts.push(`${s.skipped} skipped`)
80
+ if (s['timed-out'] > 0) parts.push(`${s['timed-out']} timed out`)
81
+ console.log(` Tasks: ${s.total} total — ${parts.join(', ')}`)
82
+
83
+ // Write JSON report
84
+ try {
85
+ await mkdir(reportDir, { recursive: true })
86
+ const timestamp = new Date()
87
+ .toISOString()
88
+ .replace(/[:.]/g, '-')
89
+ .slice(0, 19)
90
+ const reportPath = resolve(reportDir, `${timestamp}.json`)
91
+ await writeFile(reportPath, JSON.stringify(report, null, 2), 'utf8')
92
+ console.log(` Report: ${reportPath}`)
93
+ } catch (err: unknown) {
94
+ console.log(` ${ICONS.failed} Could not write report: ${(err as Error).message}`)
95
+ }
96
+
97
+ console.log()
98
+ },
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Print the execution plan (dry-run mode).
104
+ */
105
+ export function printExecutionPlan(spec: TaskSpec, phases: Task[][]): void {
106
+ console.log(`\n 🏰 Execution Plan: ${spec.name}`)
107
+ console.log(` Adapter: ${spec.adapter}`)
108
+ console.log(` Concurrency: ${spec.concurrency}`)
109
+ console.log(` On failure: ${spec.on_failure}`)
110
+ console.log(` Tasks: ${spec.tasks.length}`)
111
+ console.log()
112
+
113
+ for (let i = 0; i < phases.length; i++) {
114
+ const phase = phases[i]
115
+ console.log(` Phase ${i + 1}:`)
116
+ for (const task of phase) {
117
+ const deps =
118
+ task.depends_on.length > 0
119
+ ? ` (after: ${task.depends_on.join(', ')})`
120
+ : ''
121
+ const files =
122
+ task.files.length > 0 ? ` [${task.files.join(', ')}]` : ''
123
+ console.log(
124
+ ` ${task.id} — ${task.description} [${task.agent}, ${task.timeout}]${deps}${files}`
125
+ )
126
+ }
127
+ }
128
+ console.log()
129
+ }