opencastle 0.10.7 → 0.12.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/README.md +4 -0
- package/bin/cli.mjs +4 -0
- package/dist/cli/convoy/events.d.ts +10 -0
- package/dist/cli/convoy/events.d.ts.map +1 -0
- package/dist/cli/convoy/events.js +27 -0
- package/dist/cli/convoy/events.js.map +1 -0
- package/dist/cli/convoy/events.test.d.ts +2 -0
- package/dist/cli/convoy/events.test.d.ts.map +1 -0
- package/dist/cli/convoy/events.test.js +94 -0
- package/dist/cli/convoy/events.test.js.map +1 -0
- package/dist/cli/convoy/store.d.ts +23 -0
- package/dist/cli/convoy/store.d.ts.map +1 -0
- package/dist/cli/convoy/store.js +210 -0
- package/dist/cli/convoy/store.js.map +1 -0
- package/dist/cli/convoy/store.test.d.ts +2 -0
- package/dist/cli/convoy/store.test.d.ts.map +1 -0
- package/dist/cli/convoy/store.test.js +387 -0
- package/dist/cli/convoy/store.test.js.map +1 -0
- package/dist/cli/convoy/types.d.ts +56 -0
- package/dist/cli/convoy/types.d.ts.map +1 -0
- package/dist/cli/convoy/types.js +2 -0
- package/dist/cli/convoy/types.js.map +1 -0
- package/dist/cli/dashboard.d.ts.map +1 -1
- package/dist/cli/dashboard.js +5 -1
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/init.test.js +1 -1
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/lesson.d.ts +17 -0
- package/dist/cli/lesson.d.ts.map +1 -0
- package/dist/cli/lesson.js +294 -0
- package/dist/cli/lesson.js.map +1 -0
- package/dist/cli/log.d.ts +7 -0
- package/dist/cli/log.d.ts.map +1 -0
- package/dist/cli/log.js +131 -0
- package/dist/cli/log.js.map +1 -0
- package/dist/cli/run/executor.js.map +1 -1
- package/dist/cli/run/executor.test.js +1 -0
- package/dist/cli/run/executor.test.js.map +1 -1
- package/dist/cli/run/loop-executor.d.ts +3 -0
- package/dist/cli/run/loop-executor.d.ts.map +1 -0
- package/dist/cli/run/loop-executor.js +155 -0
- package/dist/cli/run/loop-executor.js.map +1 -0
- package/dist/cli/run/loop-reporter.d.ts +6 -0
- package/dist/cli/run/loop-reporter.d.ts.map +1 -0
- package/dist/cli/run/loop-reporter.js +112 -0
- package/dist/cli/run/loop-reporter.js.map +1 -0
- package/dist/cli/run/reporter.d.ts.map +1 -1
- package/dist/cli/run/reporter.js +28 -1
- package/dist/cli/run/reporter.js.map +1 -1
- package/dist/cli/run/schema.d.ts +4 -0
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +178 -50
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run/schema.test.js +598 -1
- package/dist/cli/run/schema.test.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +84 -3
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/types.d.ts +78 -1
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +54 -1
- package/dist/cli/update.js.map +1 -1
- package/package.json +3 -2
- package/src/cli/convoy/events.test.ts +118 -0
- package/src/cli/convoy/events.ts +41 -0
- package/src/cli/convoy/store.test.ts +446 -0
- package/src/cli/convoy/store.ts +308 -0
- package/src/cli/convoy/types.ts +68 -0
- package/src/cli/dashboard.ts +5 -1
- package/src/cli/init.test.ts +1 -1
- package/src/cli/lesson.ts +312 -0
- package/src/cli/log.ts +133 -0
- package/src/cli/run/executor.test.ts +1 -0
- package/src/cli/run/executor.ts +8 -8
- package/src/cli/run/loop-executor.ts +199 -0
- package/src/cli/run/loop-reporter.ts +125 -0
- package/src/cli/run/reporter.ts +30 -1
- package/src/cli/run/schema.test.ts +704 -3
- package/src/cli/run/schema.ts +206 -56
- package/src/cli/run.ts +82 -5
- package/src/cli/types.ts +87 -1
- package/src/cli/update.ts +62 -1
- package/src/dashboard/dist/index.html +14 -15
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/scripts/generate-seed-data.ts +23 -43
- package/src/dashboard/seed-data/events.ndjson +104 -0
- package/src/dashboard/src/pages/index.astro +14 -15
- package/src/orchestrator/agents/api-designer.agent.md +1 -1
- package/src/orchestrator/agents/architect.agent.md +1 -1
- package/src/orchestrator/agents/content-engineer.agent.md +1 -1
- package/src/orchestrator/agents/copywriter.agent.md +1 -1
- package/src/orchestrator/agents/data-expert.agent.md +1 -1
- package/src/orchestrator/agents/database-engineer.agent.md +1 -1
- package/src/orchestrator/agents/developer.agent.md +1 -1
- package/src/orchestrator/agents/devops-expert.agent.md +1 -1
- package/src/orchestrator/agents/documentation-writer.agent.md +1 -1
- package/src/orchestrator/agents/performance-expert.agent.md +1 -1
- package/src/orchestrator/agents/release-manager.agent.md +1 -1
- package/src/orchestrator/agents/security-expert.agent.md +1 -1
- package/src/orchestrator/agents/seo-specialist.agent.md +1 -1
- package/src/orchestrator/agents/session-guard.agent.md +9 -21
- package/src/orchestrator/agents/team-lead.agent.md +8 -34
- package/src/orchestrator/agents/testing-expert.agent.md +1 -1
- package/src/orchestrator/agents/ui-ux-expert.agent.md +1 -1
- package/src/orchestrator/customizations/AGENT-PERFORMANCE.md +11 -12
- package/src/orchestrator/customizations/DISPUTES.md +2 -2
- package/src/orchestrator/customizations/README.md +1 -3
- package/src/orchestrator/customizations/logs/README.md +66 -14
- package/src/orchestrator/instructions/ai-optimization.instructions.md +21 -132
- package/src/orchestrator/instructions/general.instructions.md +35 -181
- package/src/orchestrator/plugins/nx/SKILL.md +1 -1
- package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +4 -8
- package/src/orchestrator/prompts/bug-fix.prompt.md +4 -4
- package/src/orchestrator/prompts/implement-feature.prompt.md +3 -3
- package/src/orchestrator/prompts/quick-refinement.prompt.md +3 -3
- package/src/orchestrator/prompts/resolve-pr-comments.prompt.md +1 -1
- package/src/orchestrator/skills/agent-hooks/SKILL.md +11 -11
- package/src/orchestrator/skills/decomposition/SKILL.md +1 -1
- package/src/orchestrator/skills/fast-review/SKILL.md +4 -19
- package/src/orchestrator/skills/git-workflow/SKILL.md +72 -0
- package/src/orchestrator/skills/memory-merger/SKILL.md +1 -1
- package/src/orchestrator/skills/observability-logging/SKILL.md +129 -0
- package/src/orchestrator/skills/orchestration-protocols/SKILL.md +2 -2
- package/src/orchestrator/skills/panel-majority-vote/SKILL.md +4 -7
- package/src/orchestrator/skills/self-improvement/SKILL.md +13 -26
- package/src/orchestrator/skills/team-lead-reference/SKILL.md +2 -2
- package/src/orchestrator/customizations/logs/delegations.ndjson +0 -1
- package/src/orchestrator/customizations/logs/panels.ndjson +0 -1
- package/src/orchestrator/customizations/logs/reviews.ndjson +0 -0
- package/src/orchestrator/customizations/logs/sessions.ndjson +0 -1
- /package/src/orchestrator/customizations/logs/{disputes.ndjson → events.ndjson} +0 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { readFile, writeFile, stat } from 'node:fs/promises'
|
|
2
|
+
import { join, dirname } from 'node:path'
|
|
3
|
+
import type { CliContext } from './types.js'
|
|
4
|
+
|
|
5
|
+
const CATEGORIES = [
|
|
6
|
+
'task-management',
|
|
7
|
+
'jira',
|
|
8
|
+
'mcp-tools',
|
|
9
|
+
'codebase-tool',
|
|
10
|
+
'terminal',
|
|
11
|
+
'framework',
|
|
12
|
+
'cms',
|
|
13
|
+
'database',
|
|
14
|
+
'git',
|
|
15
|
+
'deployment',
|
|
16
|
+
'browser-testing',
|
|
17
|
+
'general',
|
|
18
|
+
] as const
|
|
19
|
+
|
|
20
|
+
type Category = (typeof CATEGORIES)[number]
|
|
21
|
+
|
|
22
|
+
const SEVERITIES = ['high', 'medium', 'low'] as const
|
|
23
|
+
type Severity = (typeof SEVERITIES)[number]
|
|
24
|
+
|
|
25
|
+
const HELP = `
|
|
26
|
+
opencastle lesson [options]
|
|
27
|
+
|
|
28
|
+
Append a structured lesson to .github/customizations/LESSONS-LEARNED.md
|
|
29
|
+
|
|
30
|
+
Required flags:
|
|
31
|
+
--title <text> Short descriptive title
|
|
32
|
+
--category <cat> One of: ${CATEGORIES.join(', ')}
|
|
33
|
+
--severity <level> One of: high, medium, low
|
|
34
|
+
--problem <text> What went wrong
|
|
35
|
+
|
|
36
|
+
Optional flags:
|
|
37
|
+
--wrong <text> The wrong approach that was tried
|
|
38
|
+
--correct <text> The correct approach that works
|
|
39
|
+
--why <text> Root cause explanation
|
|
40
|
+
--customizations-dir <p> Override the customizations directory path
|
|
41
|
+
--help, -h Show this help
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
opencastle lesson \\
|
|
45
|
+
--title "Never call foo without bar" \\
|
|
46
|
+
--category general \\
|
|
47
|
+
--severity high \\
|
|
48
|
+
--problem "foo throws on Node 18 without bar"
|
|
49
|
+
|
|
50
|
+
opencastle lesson \\
|
|
51
|
+
--title "Always quote shell variables" \\
|
|
52
|
+
--category terminal \\
|
|
53
|
+
--severity medium \\
|
|
54
|
+
--problem "Unquoted variables break on paths with spaces" \\
|
|
55
|
+
--wrong 'rm -rf \$DIR/old' \\
|
|
56
|
+
--correct 'rm -rf "\$DIR/old"' \\
|
|
57
|
+
--why "Word splitting expands spaces into separate arguments"
|
|
58
|
+
`
|
|
59
|
+
|
|
60
|
+
function isCategory(s: string): s is Category {
|
|
61
|
+
return (CATEGORIES as ReadonlyArray<string>).includes(s)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isSeverity(s: string): s is Severity {
|
|
65
|
+
return (SEVERITIES as ReadonlyArray<string>).includes(s)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function resolveCustomizationsDir(override: string | null): Promise<string> {
|
|
69
|
+
if (override) return override
|
|
70
|
+
let dir = process.cwd()
|
|
71
|
+
for (;;) {
|
|
72
|
+
try {
|
|
73
|
+
const s = await stat(join(dir, '.github'))
|
|
74
|
+
if (s.isDirectory()) return join(dir, '.github', 'customizations')
|
|
75
|
+
} catch {
|
|
76
|
+
// .github not found here, walk up
|
|
77
|
+
}
|
|
78
|
+
const parent = dirname(dir)
|
|
79
|
+
if (parent === dir) break
|
|
80
|
+
dir = parent
|
|
81
|
+
}
|
|
82
|
+
return join(process.cwd(), '.github', 'customizations')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function nextLessonId(content: string): string {
|
|
86
|
+
const matches = [...content.matchAll(/^### LES-(\d+)/gm)]
|
|
87
|
+
if (matches.length === 0) return 'LES-001'
|
|
88
|
+
const last = Math.max(...matches.map((m) => parseInt(m[1], 10)))
|
|
89
|
+
return `LES-${String(last + 1).padStart(3, '0')}`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function escapeMarkdown(s: string): string {
|
|
93
|
+
return s.replace(/\|/g, '\\|').replace(/[\n\r]/g, ' ')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatLesson(opts: {
|
|
97
|
+
id: string
|
|
98
|
+
title: string
|
|
99
|
+
category: Category
|
|
100
|
+
severity: Severity
|
|
101
|
+
date: string
|
|
102
|
+
problem: string
|
|
103
|
+
wrong?: string
|
|
104
|
+
correct?: string
|
|
105
|
+
why?: string
|
|
106
|
+
}): string {
|
|
107
|
+
const title = opts.title.replace(/[\n\r]/g, ' ')
|
|
108
|
+
const lines: string[] = [
|
|
109
|
+
`### ${opts.id}: ${title}`,
|
|
110
|
+
'',
|
|
111
|
+
'| Field | Value |',
|
|
112
|
+
'|-------|-------|',
|
|
113
|
+
`| **Category** | \`${opts.category}\` |`,
|
|
114
|
+
`| **Added** | ${opts.date} |`,
|
|
115
|
+
`| **Severity** | \`${opts.severity}\` |`,
|
|
116
|
+
'',
|
|
117
|
+
`**Problem:** ${escapeMarkdown(opts.problem)}`,
|
|
118
|
+
]
|
|
119
|
+
if (opts.wrong !== undefined) lines.push('', `**Wrong approach:** ${escapeMarkdown(opts.wrong)}`)
|
|
120
|
+
if (opts.correct !== undefined) lines.push('', `**Correct approach:** ${escapeMarkdown(opts.correct)}`)
|
|
121
|
+
if (opts.why !== undefined) lines.push('', `**Why:** ${escapeMarkdown(opts.why)}`)
|
|
122
|
+
return lines.join('\n')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function insertLesson(content: string, block: string): string {
|
|
126
|
+
const marker = '\n## Index by Category'
|
|
127
|
+
const idx = content.indexOf(marker)
|
|
128
|
+
if (idx === -1) {
|
|
129
|
+
return content.trimEnd() + '\n\n' + block + '\n'
|
|
130
|
+
}
|
|
131
|
+
// Insert block right before the \n## Index sequence
|
|
132
|
+
return content.slice(0, idx) + '\n' + block + '\n' + content.slice(idx)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function updateIndex(content: string, category: string, lessonId: string): string {
|
|
136
|
+
const lines = content.split('\n')
|
|
137
|
+
let found = false
|
|
138
|
+
|
|
139
|
+
for (let i = 0; i < lines.length; i++) {
|
|
140
|
+
const m = lines[i].match(/^\| `([^`]+)` \| (.+) \|$/)
|
|
141
|
+
if (m && m[1] === category) {
|
|
142
|
+
const current = m[2].trim()
|
|
143
|
+
const updated = current === '\u2014' ? lessonId : `${current}, ${lessonId}`
|
|
144
|
+
lines[i] = `| \`${category}\` | ${updated} |`
|
|
145
|
+
found = true
|
|
146
|
+
break
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!found) {
|
|
151
|
+
// Row doesn't exist — append after the last table row in the Index section
|
|
152
|
+
const indexHeading = lines.findIndex((l) => l.trim() === '## Index by Category')
|
|
153
|
+
if (indexHeading !== -1) {
|
|
154
|
+
let lastTableRow = -1
|
|
155
|
+
for (let i = indexHeading; i < lines.length; i++) {
|
|
156
|
+
if (lines[i].startsWith('|')) lastTableRow = i
|
|
157
|
+
else if (lastTableRow > indexHeading && lines[i].trim() !== '') break
|
|
158
|
+
}
|
|
159
|
+
if (lastTableRow !== -1) {
|
|
160
|
+
lines.splice(lastTableRow + 1, 0, `| \`${category}\` | ${lessonId} |`)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return lines.join('\n')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface LessonInput {
|
|
169
|
+
title: string
|
|
170
|
+
category: string
|
|
171
|
+
severity: string
|
|
172
|
+
problem: string
|
|
173
|
+
wrong?: string
|
|
174
|
+
correct?: string
|
|
175
|
+
why?: string
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Append a structured lesson to LESSONS-LEARNED.md programmatically.
|
|
180
|
+
* Returns the generated lesson ID (e.g., "LES-005").
|
|
181
|
+
*/
|
|
182
|
+
export async function appendLesson(
|
|
183
|
+
input: LessonInput,
|
|
184
|
+
customizationsDir?: string | null,
|
|
185
|
+
): Promise<string> {
|
|
186
|
+
if (!isCategory(input.category)) {
|
|
187
|
+
throw new Error(`Invalid category "${input.category}". Must be one of: ${CATEGORIES.join(', ')}`)
|
|
188
|
+
}
|
|
189
|
+
if (!isSeverity(input.severity)) {
|
|
190
|
+
throw new Error(`Invalid severity "${input.severity}". Must be one of: ${SEVERITIES.join(', ')}`)
|
|
191
|
+
}
|
|
192
|
+
const category = input.category
|
|
193
|
+
const severity = input.severity
|
|
194
|
+
|
|
195
|
+
const resolvedDir = await resolveCustomizationsDir(customizationsDir ?? null)
|
|
196
|
+
const lessonsFile = join(resolvedDir, 'LESSONS-LEARNED.md')
|
|
197
|
+
|
|
198
|
+
let content: string
|
|
199
|
+
try {
|
|
200
|
+
content = await readFile(lessonsFile, 'utf8')
|
|
201
|
+
} catch {
|
|
202
|
+
throw new Error(`LESSONS-LEARNED.md not found at: ${lessonsFile}`)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const id = nextLessonId(content)
|
|
206
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
207
|
+
|
|
208
|
+
const block = formatLesson({
|
|
209
|
+
id,
|
|
210
|
+
title: input.title,
|
|
211
|
+
category,
|
|
212
|
+
severity,
|
|
213
|
+
date,
|
|
214
|
+
problem: input.problem,
|
|
215
|
+
wrong: input.wrong,
|
|
216
|
+
correct: input.correct,
|
|
217
|
+
why: input.why,
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
let updated = insertLesson(content, block)
|
|
221
|
+
updated = updateIndex(updated, category, id)
|
|
222
|
+
|
|
223
|
+
await writeFile(lessonsFile, updated, 'utf8')
|
|
224
|
+
return id
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export default async function lesson({ args }: CliContext): Promise<void> {
|
|
228
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
229
|
+
console.log(HELP)
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let title: string | null = null
|
|
234
|
+
let category: string | null = null
|
|
235
|
+
let severity: string | null = null
|
|
236
|
+
let problem: string | null = null
|
|
237
|
+
let wrong: string | undefined
|
|
238
|
+
let correct: string | undefined
|
|
239
|
+
let why: string | undefined
|
|
240
|
+
let customizationsDir: string | null = null
|
|
241
|
+
|
|
242
|
+
for (let i = 0; i < args.length; i++) {
|
|
243
|
+
const a = args[i]
|
|
244
|
+
switch (a) {
|
|
245
|
+
case '--title':
|
|
246
|
+
if (i + 1 >= args.length) { console.error(' \u2717 --title requires a value'); process.exit(1) }
|
|
247
|
+
title = args[++i]
|
|
248
|
+
break
|
|
249
|
+
case '--category':
|
|
250
|
+
if (i + 1 >= args.length) { console.error(' \u2717 --category requires a value'); process.exit(1) }
|
|
251
|
+
category = args[++i]
|
|
252
|
+
break
|
|
253
|
+
case '--severity':
|
|
254
|
+
if (i + 1 >= args.length) { console.error(' \u2717 --severity requires a value'); process.exit(1) }
|
|
255
|
+
severity = args[++i]
|
|
256
|
+
break
|
|
257
|
+
case '--problem':
|
|
258
|
+
if (i + 1 >= args.length) { console.error(' \u2717 --problem requires a value'); process.exit(1) }
|
|
259
|
+
problem = args[++i]
|
|
260
|
+
break
|
|
261
|
+
case '--wrong':
|
|
262
|
+
if (i + 1 >= args.length) { console.error(' \u2717 --wrong requires a value'); process.exit(1) }
|
|
263
|
+
wrong = args[++i]
|
|
264
|
+
break
|
|
265
|
+
case '--correct':
|
|
266
|
+
if (i + 1 >= args.length) { console.error(' \u2717 --correct requires a value'); process.exit(1) }
|
|
267
|
+
correct = args[++i]
|
|
268
|
+
break
|
|
269
|
+
case '--why':
|
|
270
|
+
if (i + 1 >= args.length) { console.error(' \u2717 --why requires a value'); process.exit(1) }
|
|
271
|
+
why = args[++i]
|
|
272
|
+
break
|
|
273
|
+
case '--customizations-dir':
|
|
274
|
+
if (i + 1 >= args.length) { console.error(' \u2717 --customizations-dir requires a path'); process.exit(1) }
|
|
275
|
+
customizationsDir = args[++i]
|
|
276
|
+
break
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const missing: string[] = []
|
|
281
|
+
if (!title) missing.push('--title')
|
|
282
|
+
if (!category) missing.push('--category')
|
|
283
|
+
if (!severity) missing.push('--severity')
|
|
284
|
+
if (!problem) missing.push('--problem')
|
|
285
|
+
|
|
286
|
+
if (missing.length > 0) {
|
|
287
|
+
console.error(` \u2717 Missing required flags: ${missing.join(', ')}`)
|
|
288
|
+
console.error(' Run "opencastle lesson --help" for usage.')
|
|
289
|
+
process.exit(1)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!isCategory(category!)) {
|
|
293
|
+
console.error(` \u2717 Invalid --category "${category}". Must be one of: ${CATEGORIES.join(', ')}`)
|
|
294
|
+
process.exit(1)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!isSeverity(severity!)) {
|
|
298
|
+
console.error(` \u2717 Invalid --severity "${severity}". Must be one of: ${SEVERITIES.join(', ')}`)
|
|
299
|
+
process.exit(1)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const id = await appendLesson(
|
|
304
|
+
{ title: title!, category: category!, severity: severity!, problem: problem!, wrong, correct, why },
|
|
305
|
+
customizationsDir,
|
|
306
|
+
)
|
|
307
|
+
console.log(`${id}: ${title}`)
|
|
308
|
+
} catch (err: unknown) {
|
|
309
|
+
console.error(` \u2717 ${(err as Error).message}`)
|
|
310
|
+
process.exit(1)
|
|
311
|
+
}
|
|
312
|
+
}
|
package/src/cli/log.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { mkdir, appendFile, stat } from 'node:fs/promises'
|
|
2
|
+
import { join, dirname } from 'node:path'
|
|
3
|
+
import type { CliContext } from './types.js'
|
|
4
|
+
|
|
5
|
+
const HELP = `
|
|
6
|
+
opencastle log [options]
|
|
7
|
+
|
|
8
|
+
Append a structured event to the observability log (events.ndjson).
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
--type <type> Event type (required): session|delegation|review|panel|dispute
|
|
12
|
+
--<field> <value> Any field from the event schema (see documentation)
|
|
13
|
+
--logs-dir <path> Override the logs directory path
|
|
14
|
+
--help, -h Show this help
|
|
15
|
+
|
|
16
|
+
Array fields (comma-separated): file_partition, lessons_added, discoveries, reviewing_agents
|
|
17
|
+
Boolean fields: escalated, weighted
|
|
18
|
+
Numeric fields: auto-detected from value
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
opencastle log --type session --agent Developer --model claude-sonnet-4-6 --task "Fix bug" --outcome success
|
|
22
|
+
opencastle log --type delegation --session_id feat/prj-1 --agent Developer --tier fast --mechanism sub-agent --outcome success
|
|
23
|
+
opencastle log --type panel --panel_key auth-review --verdict pass --pass_count 3 --block_count 0
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
const VALID_TYPES = ['session', 'delegation', 'review', 'panel', 'dispute']
|
|
27
|
+
|
|
28
|
+
const ARRAY_FIELDS = new Set([
|
|
29
|
+
'file_partition',
|
|
30
|
+
'lessons_added',
|
|
31
|
+
'discoveries',
|
|
32
|
+
'reviewing_agents',
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
const BOOLEAN_FIELDS = new Set(['escalated', 'weighted'])
|
|
36
|
+
|
|
37
|
+
function coerceValue(key: string, raw: string): unknown {
|
|
38
|
+
if (ARRAY_FIELDS.has(key)) return raw.split(',').map((s) => s.trim()).filter(Boolean)
|
|
39
|
+
if (BOOLEAN_FIELDS.has(key)) return raw === 'true'
|
|
40
|
+
if (/^-?\d+(\.\d+)?$/.test(raw)) return Number(raw)
|
|
41
|
+
return raw
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Resolve the path to the logs directory (walks up to find .github/). */
|
|
45
|
+
export async function resolveLogsDir(override?: string | null): Promise<string> {
|
|
46
|
+
if (override) return override
|
|
47
|
+
let dir = process.cwd()
|
|
48
|
+
for (;;) {
|
|
49
|
+
try {
|
|
50
|
+
const s = await stat(join(dir, '.github'))
|
|
51
|
+
if (s.isDirectory()) return join(dir, '.github', 'customizations', 'logs')
|
|
52
|
+
} catch {
|
|
53
|
+
// .github not in this directory, walk up
|
|
54
|
+
}
|
|
55
|
+
const parent = dirname(dir)
|
|
56
|
+
if (parent === dir) break
|
|
57
|
+
dir = parent
|
|
58
|
+
}
|
|
59
|
+
return join(process.cwd(), '.github', 'customizations', 'logs')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Append a structured event record to events.ndjson. */
|
|
63
|
+
export async function appendEvent(
|
|
64
|
+
record: Record<string, unknown>,
|
|
65
|
+
logsDir?: string | null,
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
const resolvedDir = await resolveLogsDir(logsDir ?? null)
|
|
68
|
+
const eventsFile = join(resolvedDir, 'events.ndjson')
|
|
69
|
+
await mkdir(resolvedDir, { recursive: true })
|
|
70
|
+
const line = JSON.stringify(record)
|
|
71
|
+
await appendFile(eventsFile, line + '\n', 'utf8')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default async function log({ args }: CliContext): Promise<void> {
|
|
75
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
76
|
+
console.log(HELP)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let type: string | null = null
|
|
81
|
+
let logsDir: string | null = null
|
|
82
|
+
const fields: Record<string, unknown> = {}
|
|
83
|
+
|
|
84
|
+
for (let i = 0; i < args.length; i++) {
|
|
85
|
+
const arg = args[i]
|
|
86
|
+
switch (arg) {
|
|
87
|
+
case '--type':
|
|
88
|
+
if (i + 1 >= args.length) { console.error(' \u2717 --type requires a value'); process.exit(1) }
|
|
89
|
+
type = args[++i]
|
|
90
|
+
break
|
|
91
|
+
case '--logs-dir':
|
|
92
|
+
if (i + 1 >= args.length) { console.error(' \u2717 --logs-dir requires a path'); process.exit(1) }
|
|
93
|
+
logsDir = args[++i]
|
|
94
|
+
break
|
|
95
|
+
default:
|
|
96
|
+
if (arg.startsWith('--')) {
|
|
97
|
+
const key = arg.slice(2)
|
|
98
|
+
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
|
|
99
|
+
if (DANGEROUS_KEYS.has(key)) break
|
|
100
|
+
const next = args[i + 1]
|
|
101
|
+
if (next === undefined || next.startsWith('--')) {
|
|
102
|
+
fields[key] = true
|
|
103
|
+
} else {
|
|
104
|
+
fields[key] = coerceValue(key, args[++i])
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!type) {
|
|
111
|
+
console.error(' \u2717 --type is required. Use one of: session, delegation, review, panel, dispute')
|
|
112
|
+
console.error(' Run "opencastle log --help" for usage.')
|
|
113
|
+
process.exit(1)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!VALID_TYPES.includes(type)) {
|
|
117
|
+
console.error(` \u2717 Invalid --type "${type}". Must be one of: ${VALID_TYPES.join(', ')}`)
|
|
118
|
+
process.exit(1)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const timestamp = (fields['timestamp'] as string | undefined) ?? new Date().toISOString()
|
|
122
|
+
delete fields['timestamp']
|
|
123
|
+
const record = { type, timestamp, ...fields }
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
await appendEvent(record, logsDir)
|
|
127
|
+
console.log(JSON.stringify(record))
|
|
128
|
+
} catch (err: unknown) {
|
|
129
|
+
console.error(` ✗ Failed to write log: ${(err as Error).message}`)
|
|
130
|
+
process.exit(1)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
package/src/cli/run/executor.ts
CHANGED
|
@@ -72,12 +72,12 @@ export function createExecutor(
|
|
|
72
72
|
adapter: AgentAdapter,
|
|
73
73
|
reporter: Reporter
|
|
74
74
|
): Executor {
|
|
75
|
-
const phases = buildPhases(spec.tasks)
|
|
75
|
+
const phases = buildPhases(spec.tasks!)
|
|
76
76
|
const statuses = new Map<string, TaskStatus>()
|
|
77
77
|
const results = new Map<string, TaskResult | null>()
|
|
78
78
|
const startTimes = new Map<string, number>()
|
|
79
79
|
|
|
80
|
-
for (const t of spec.tasks) {
|
|
80
|
+
for (const t of spec.tasks!) {
|
|
81
81
|
statuses.set(t.id, 'pending')
|
|
82
82
|
results.set(t.id, null)
|
|
83
83
|
}
|
|
@@ -154,7 +154,7 @@ export function createExecutor(
|
|
|
154
154
|
function skipTask(taskId: string, reason: string): void {
|
|
155
155
|
if (statuses.get(taskId) !== 'pending') return
|
|
156
156
|
statuses.set(taskId, 'skipped')
|
|
157
|
-
const task = spec.tasks
|
|
157
|
+
const task = spec.tasks!.find((t) => t.id === taskId)!
|
|
158
158
|
results.set(taskId, {
|
|
159
159
|
id: taskId,
|
|
160
160
|
status: 'skipped',
|
|
@@ -165,7 +165,7 @@ export function createExecutor(
|
|
|
165
165
|
reporter.onTaskSkipped(task, reason)
|
|
166
166
|
|
|
167
167
|
// Recursively skip dependents
|
|
168
|
-
for (const t of spec.tasks) {
|
|
168
|
+
for (const t of spec.tasks!) {
|
|
169
169
|
if ((t.depends_on || []).includes(taskId)) {
|
|
170
170
|
skipTask(t.id, `dependency "${taskId}" was skipped/failed`)
|
|
171
171
|
}
|
|
@@ -201,14 +201,14 @@ export function createExecutor(
|
|
|
201
201
|
if (spec.on_failure === 'stop') {
|
|
202
202
|
halted = true
|
|
203
203
|
// Skip all remaining tasks
|
|
204
|
-
for (const t of spec.tasks) {
|
|
204
|
+
for (const t of spec.tasks!) {
|
|
205
205
|
if (statuses.get(t.id) === 'pending') {
|
|
206
206
|
skipTask(t.id, 'execution halted due to on_failure: stop')
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
} else {
|
|
210
210
|
// on_failure: continue — skip dependents of this failed task
|
|
211
|
-
for (const t of spec.tasks) {
|
|
211
|
+
for (const t of spec.tasks!) {
|
|
212
212
|
if ((t.depends_on || []).includes(r.id)) {
|
|
213
213
|
skipTask(t.id, `dependency "${r.id}" failed`)
|
|
214
214
|
}
|
|
@@ -220,7 +220,7 @@ export function createExecutor(
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
const completedAt = new Date()
|
|
223
|
-
const allResults: TaskResult[] = spec.tasks
|
|
223
|
+
const allResults: TaskResult[] = spec.tasks!.map(
|
|
224
224
|
(t) =>
|
|
225
225
|
results.get(t.id) || {
|
|
226
226
|
id: t.id,
|
|
@@ -232,7 +232,7 @@ export function createExecutor(
|
|
|
232
232
|
)
|
|
233
233
|
|
|
234
234
|
const summary: RunSummary = {
|
|
235
|
-
total: spec.tasks
|
|
235
|
+
total: spec.tasks!.length,
|
|
236
236
|
done: 0,
|
|
237
237
|
failed: 0,
|
|
238
238
|
skipped: 0,
|