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.
- package/LICENSE +21 -0
- package/README.md +215 -0
- package/bin/cli.mjs +69 -0
- package/dist/cli/adapters/claude-code.d.ts +22 -0
- package/dist/cli/adapters/claude-code.d.ts.map +1 -0
- package/dist/cli/adapters/claude-code.js +237 -0
- package/dist/cli/adapters/claude-code.js.map +1 -0
- package/dist/cli/adapters/cursor.d.ts +20 -0
- package/dist/cli/adapters/cursor.d.ts.map +1 -0
- package/dist/cli/adapters/cursor.js +231 -0
- package/dist/cli/adapters/cursor.js.map +1 -0
- package/dist/cli/adapters/vscode.d.ts +20 -0
- package/dist/cli/adapters/vscode.d.ts.map +1 -0
- package/dist/cli/adapters/vscode.js +132 -0
- package/dist/cli/adapters/vscode.js.map +1 -0
- package/dist/cli/copy.d.ts +14 -0
- package/dist/cli/copy.d.ts.map +1 -0
- package/dist/cli/copy.js +62 -0
- package/dist/cli/copy.js.map +1 -0
- package/dist/cli/dashboard.d.ts +3 -0
- package/dist/cli/dashboard.d.ts.map +1 -0
- package/dist/cli/dashboard.js +183 -0
- package/dist/cli/dashboard.js.map +1 -0
- package/dist/cli/diff.d.ts +3 -0
- package/dist/cli/diff.d.ts.map +1 -0
- package/dist/cli/diff.js +27 -0
- package/dist/cli/diff.js.map +1 -0
- package/dist/cli/eject.d.ts +3 -0
- package/dist/cli/eject.d.ts.map +1 -0
- package/dist/cli/eject.js +27 -0
- package/dist/cli/eject.js.map +1 -0
- package/dist/cli/init.d.ts +3 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +92 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/manifest.d.ts +14 -0
- package/dist/cli/manifest.d.ts.map +1 -0
- package/dist/cli/manifest.js +34 -0
- package/dist/cli/manifest.js.map +1 -0
- package/dist/cli/mcp.d.ts +14 -0
- package/dist/cli/mcp.d.ts.map +1 -0
- package/dist/cli/mcp.js +35 -0
- package/dist/cli/mcp.js.map +1 -0
- package/dist/cli/prompt.d.ts +12 -0
- package/dist/cli/prompt.d.ts.map +1 -0
- package/dist/cli/prompt.js +104 -0
- package/dist/cli/prompt.js.map +1 -0
- package/dist/cli/run/adapters/claude-code.d.ts +16 -0
- package/dist/cli/run/adapters/claude-code.d.ts.map +1 -0
- package/dist/cli/run/adapters/claude-code.js +82 -0
- package/dist/cli/run/adapters/claude-code.js.map +1 -0
- package/dist/cli/run/adapters/copilot.d.ts +16 -0
- package/dist/cli/run/adapters/copilot.d.ts.map +1 -0
- package/dist/cli/run/adapters/copilot.js +84 -0
- package/dist/cli/run/adapters/copilot.js.map +1 -0
- package/dist/cli/run/adapters/cursor.d.ts +16 -0
- package/dist/cli/run/adapters/cursor.d.ts.map +1 -0
- package/dist/cli/run/adapters/cursor.js +81 -0
- package/dist/cli/run/adapters/cursor.js.map +1 -0
- package/dist/cli/run/adapters/index.d.ts +14 -0
- package/dist/cli/run/adapters/index.d.ts.map +1 -0
- package/dist/cli/run/adapters/index.js +35 -0
- package/dist/cli/run/adapters/index.js.map +1 -0
- package/dist/cli/run/executor.d.ts +15 -0
- package/dist/cli/run/executor.d.ts.map +1 -0
- package/dist/cli/run/executor.js +249 -0
- package/dist/cli/run/executor.js.map +1 -0
- package/dist/cli/run/reporter.d.ts +10 -0
- package/dist/cli/run/reporter.d.ts.map +1 -0
- package/dist/cli/run/reporter.js +112 -0
- package/dist/cli/run/reporter.js.map +1 -0
- package/dist/cli/run/schema.d.ts +28 -0
- package/dist/cli/run/schema.d.ts.map +1 -0
- package/dist/cli/run/schema.js +511 -0
- package/dist/cli/run/schema.js.map +1 -0
- package/dist/cli/run.d.ts +6 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +123 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/stack-config.d.ts +12 -0
- package/dist/cli/stack-config.d.ts.map +1 -0
- package/dist/cli/stack-config.js +146 -0
- package/dist/cli/stack-config.js.map +1 -0
- package/dist/cli/types.d.ts +169 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +2 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/update.d.ts +3 -0
- package/dist/cli/update.d.ts.map +1 -0
- package/dist/cli/update.js +50 -0
- package/dist/cli/update.js.map +1 -0
- package/package.json +48 -0
- package/src/cli/adapters/claude-code.ts +287 -0
- package/src/cli/adapters/cursor.ts +377 -0
- package/src/cli/adapters/vscode.ts +168 -0
- package/src/cli/copy.ts +79 -0
- package/src/cli/dashboard.ts +225 -0
- package/src/cli/diff.ts +44 -0
- package/src/cli/eject.ts +39 -0
- package/src/cli/init.ts +120 -0
- package/src/cli/manifest.ts +45 -0
- package/src/cli/mcp.ts +49 -0
- package/src/cli/prompt.ts +115 -0
- package/src/cli/run/adapters/claude-code.ts +95 -0
- package/src/cli/run/adapters/copilot.ts +97 -0
- package/src/cli/run/adapters/cursor.ts +94 -0
- package/src/cli/run/adapters/index.ts +40 -0
- package/src/cli/run/executor.ts +292 -0
- package/src/cli/run/reporter.ts +129 -0
- package/src/cli/run/schema.ts +595 -0
- package/src/cli/run.ts +137 -0
- package/src/cli/stack-config.ts +180 -0
- package/src/cli/types.ts +207 -0
- package/src/cli/update.ts +75 -0
- package/src/dashboard/astro.config.mjs +6 -0
- package/src/dashboard/package-lock.json +5455 -0
- package/src/dashboard/package.json +14 -0
- package/src/dashboard/public/data/delegations.ndjson +35 -0
- package/src/dashboard/public/data/panels.ndjson +13 -0
- package/src/dashboard/public/data/sessions.ndjson +50 -0
- package/src/dashboard/public/icon-192.png +0 -0
- package/src/dashboard/scripts/generate-seed-data.ts +355 -0
- package/src/dashboard/src/layouts/Layout.astro +25 -0
- package/src/dashboard/src/pages/index.astro +1070 -0
- package/src/dashboard/src/styles/dashboard.css +1078 -0
- package/src/dashboard/tsconfig.json +6 -0
- package/src/orchestrator/agent-workflows/README.md +22 -0
- package/src/orchestrator/agent-workflows/bug-fix.md +128 -0
- package/src/orchestrator/agent-workflows/data-pipeline.md +145 -0
- package/src/orchestrator/agent-workflows/database-migration.md +159 -0
- package/src/orchestrator/agent-workflows/feature-implementation.md +223 -0
- package/src/orchestrator/agent-workflows/performance-optimization.md +125 -0
- package/src/orchestrator/agent-workflows/refactoring.md +142 -0
- package/src/orchestrator/agent-workflows/schema-changes.md +164 -0
- package/src/orchestrator/agent-workflows/security-audit.md +148 -0
- package/src/orchestrator/agent-workflows/shared-delivery-phase.md +33 -0
- package/src/orchestrator/agents/api-designer.agent.md +68 -0
- package/src/orchestrator/agents/architect.agent.md +129 -0
- package/src/orchestrator/agents/content-engineer.agent.md +57 -0
- package/src/orchestrator/agents/copywriter.agent.md +95 -0
- package/src/orchestrator/agents/data-expert.agent.md +63 -0
- package/src/orchestrator/agents/database-engineer.agent.md +62 -0
- package/src/orchestrator/agents/developer.agent.md +66 -0
- package/src/orchestrator/agents/devops-expert.agent.md +57 -0
- package/src/orchestrator/agents/documentation-writer.agent.md +60 -0
- package/src/orchestrator/agents/performance-expert.agent.md +58 -0
- package/src/orchestrator/agents/release-manager.agent.md +72 -0
- package/src/orchestrator/agents/researcher.agent.md +145 -0
- package/src/orchestrator/agents/reviewer.agent.md +62 -0
- package/src/orchestrator/agents/security-expert.agent.md +64 -0
- package/src/orchestrator/agents/seo-specialist.agent.md +67 -0
- package/src/orchestrator/agents/team-lead.agent.md +644 -0
- package/src/orchestrator/agents/testing-expert.agent.md +85 -0
- package/src/orchestrator/agents/ui-ux-expert.agent.md +63 -0
- package/src/orchestrator/copilot-instructions.md +3 -0
- package/src/orchestrator/customizations/AGENT-EXPERTISE.md +325 -0
- package/src/orchestrator/customizations/AGENT-FAILURES.md +69 -0
- package/src/orchestrator/customizations/AGENT-PERFORMANCE.md +58 -0
- package/src/orchestrator/customizations/DISPUTES.md +162 -0
- package/src/orchestrator/customizations/KNOWLEDGE-GRAPH.md +10 -0
- package/src/orchestrator/customizations/LESSONS-LEARNED.md +70 -0
- package/src/orchestrator/customizations/README.md +59 -0
- package/src/orchestrator/customizations/agents/agent-registry.md +46 -0
- package/src/orchestrator/customizations/agents/skill-matrix.md +142 -0
- package/src/orchestrator/customizations/logs/README.md +181 -0
- package/src/orchestrator/customizations/logs/delegations.ndjson +1 -0
- package/src/orchestrator/customizations/logs/panels.ndjson +1 -0
- package/src/orchestrator/customizations/logs/sessions.ndjson +1 -0
- package/src/orchestrator/customizations/project/docs-structure.md +23 -0
- package/src/orchestrator/customizations/project/tracker-config.md +45 -0
- package/src/orchestrator/customizations/project.instructions.md +64 -0
- package/src/orchestrator/customizations/stack/api-config.md +37 -0
- package/src/orchestrator/customizations/stack/cms-config.md +26 -0
- package/src/orchestrator/customizations/stack/data-pipeline-config.md +41 -0
- package/src/orchestrator/customizations/stack/database-config.md +44 -0
- package/src/orchestrator/customizations/stack/deployment-config.md +45 -0
- package/src/orchestrator/customizations/stack/testing-config.md +56 -0
- package/src/orchestrator/instructions/ai-optimization.instructions.md +143 -0
- package/src/orchestrator/instructions/general.instructions.md +194 -0
- package/src/orchestrator/mcp.json +55 -0
- package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +235 -0
- package/src/orchestrator/prompts/brainstorm.prompt.md +115 -0
- package/src/orchestrator/prompts/bug-fix.prompt.md +141 -0
- package/src/orchestrator/prompts/create-skill.prompt.md +103 -0
- package/src/orchestrator/prompts/generate-task-spec.prompt.md +154 -0
- package/src/orchestrator/prompts/implement-feature.prompt.md +124 -0
- package/src/orchestrator/prompts/metrics-report.prompt.md +142 -0
- package/src/orchestrator/prompts/quick-refinement.prompt.md +137 -0
- package/src/orchestrator/prompts/resolve-pr-comments.prompt.md +100 -0
- package/src/orchestrator/skills/accessibility-standards/SKILL.md +164 -0
- package/src/orchestrator/skills/agent-hooks/SKILL.md +147 -0
- package/src/orchestrator/skills/agent-memory/SKILL.md +144 -0
- package/src/orchestrator/skills/api-patterns/SKILL.md +106 -0
- package/src/orchestrator/skills/browser-testing/SKILL.md +203 -0
- package/src/orchestrator/skills/code-commenting/SKILL.md +133 -0
- package/src/orchestrator/skills/contentful-cms/SKILL.md +43 -0
- package/src/orchestrator/skills/context-map/SKILL.md +135 -0
- package/src/orchestrator/skills/convex-database/SKILL.md +80 -0
- package/src/orchestrator/skills/data-engineering/SKILL.md +99 -0
- package/src/orchestrator/skills/deployment-infrastructure/SKILL.md +49 -0
- package/src/orchestrator/skills/documentation-standards/SKILL.md +85 -0
- package/src/orchestrator/skills/fast-review/SKILL.md +327 -0
- package/src/orchestrator/skills/frontend-design/SKILL.md +42 -0
- package/src/orchestrator/skills/jira-management/SKILL.md +168 -0
- package/src/orchestrator/skills/memory-merger/SKILL.md +123 -0
- package/src/orchestrator/skills/nextjs-patterns/SKILL.md +75 -0
- package/src/orchestrator/skills/nx-workspace/SKILL.md +192 -0
- package/src/orchestrator/skills/panel-majority-vote/SKILL.md +184 -0
- package/src/orchestrator/skills/panel-majority-vote/panel-report.template.md +38 -0
- package/src/orchestrator/skills/performance-optimization/SKILL.md +101 -0
- package/src/orchestrator/skills/react-development/SKILL.md +117 -0
- package/src/orchestrator/skills/sanity-cms/SKILL.md +18 -0
- package/src/orchestrator/skills/security-hardening/SKILL.md +118 -0
- package/src/orchestrator/skills/self-improvement/SKILL.md +137 -0
- package/src/orchestrator/skills/seo-patterns/SKILL.md +40 -0
- package/src/orchestrator/skills/session-checkpoints/SKILL.md +205 -0
- package/src/orchestrator/skills/slack-notifications/SKILL.md +211 -0
- package/src/orchestrator/skills/strapi-cms/SKILL.md +43 -0
- package/src/orchestrator/skills/supabase-database/SKILL.md +24 -0
- package/src/orchestrator/skills/task-management/SKILL.md +143 -0
- package/src/orchestrator/skills/team-lead-reference/SKILL.md +317 -0
- package/src/orchestrator/skills/teams-notifications/SKILL.md +249 -0
- package/src/orchestrator/skills/testing-workflow/SKILL.md +134 -0
- 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
|
+
}
|