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.
Files changed (132) hide show
  1. package/README.md +4 -0
  2. package/bin/cli.mjs +4 -0
  3. package/dist/cli/convoy/events.d.ts +10 -0
  4. package/dist/cli/convoy/events.d.ts.map +1 -0
  5. package/dist/cli/convoy/events.js +27 -0
  6. package/dist/cli/convoy/events.js.map +1 -0
  7. package/dist/cli/convoy/events.test.d.ts +2 -0
  8. package/dist/cli/convoy/events.test.d.ts.map +1 -0
  9. package/dist/cli/convoy/events.test.js +94 -0
  10. package/dist/cli/convoy/events.test.js.map +1 -0
  11. package/dist/cli/convoy/store.d.ts +23 -0
  12. package/dist/cli/convoy/store.d.ts.map +1 -0
  13. package/dist/cli/convoy/store.js +210 -0
  14. package/dist/cli/convoy/store.js.map +1 -0
  15. package/dist/cli/convoy/store.test.d.ts +2 -0
  16. package/dist/cli/convoy/store.test.d.ts.map +1 -0
  17. package/dist/cli/convoy/store.test.js +387 -0
  18. package/dist/cli/convoy/store.test.js.map +1 -0
  19. package/dist/cli/convoy/types.d.ts +56 -0
  20. package/dist/cli/convoy/types.d.ts.map +1 -0
  21. package/dist/cli/convoy/types.js +2 -0
  22. package/dist/cli/convoy/types.js.map +1 -0
  23. package/dist/cli/dashboard.d.ts.map +1 -1
  24. package/dist/cli/dashboard.js +5 -1
  25. package/dist/cli/dashboard.js.map +1 -1
  26. package/dist/cli/init.test.js +1 -1
  27. package/dist/cli/init.test.js.map +1 -1
  28. package/dist/cli/lesson.d.ts +17 -0
  29. package/dist/cli/lesson.d.ts.map +1 -0
  30. package/dist/cli/lesson.js +294 -0
  31. package/dist/cli/lesson.js.map +1 -0
  32. package/dist/cli/log.d.ts +7 -0
  33. package/dist/cli/log.d.ts.map +1 -0
  34. package/dist/cli/log.js +131 -0
  35. package/dist/cli/log.js.map +1 -0
  36. package/dist/cli/run/executor.js.map +1 -1
  37. package/dist/cli/run/executor.test.js +1 -0
  38. package/dist/cli/run/executor.test.js.map +1 -1
  39. package/dist/cli/run/loop-executor.d.ts +3 -0
  40. package/dist/cli/run/loop-executor.d.ts.map +1 -0
  41. package/dist/cli/run/loop-executor.js +155 -0
  42. package/dist/cli/run/loop-executor.js.map +1 -0
  43. package/dist/cli/run/loop-reporter.d.ts +6 -0
  44. package/dist/cli/run/loop-reporter.d.ts.map +1 -0
  45. package/dist/cli/run/loop-reporter.js +112 -0
  46. package/dist/cli/run/loop-reporter.js.map +1 -0
  47. package/dist/cli/run/reporter.d.ts.map +1 -1
  48. package/dist/cli/run/reporter.js +28 -1
  49. package/dist/cli/run/reporter.js.map +1 -1
  50. package/dist/cli/run/schema.d.ts +4 -0
  51. package/dist/cli/run/schema.d.ts.map +1 -1
  52. package/dist/cli/run/schema.js +178 -50
  53. package/dist/cli/run/schema.js.map +1 -1
  54. package/dist/cli/run/schema.test.js +598 -1
  55. package/dist/cli/run/schema.test.js.map +1 -1
  56. package/dist/cli/run.d.ts.map +1 -1
  57. package/dist/cli/run.js +84 -3
  58. package/dist/cli/run.js.map +1 -1
  59. package/dist/cli/types.d.ts +78 -1
  60. package/dist/cli/types.d.ts.map +1 -1
  61. package/dist/cli/update.d.ts.map +1 -1
  62. package/dist/cli/update.js +54 -1
  63. package/dist/cli/update.js.map +1 -1
  64. package/package.json +3 -2
  65. package/src/cli/convoy/events.test.ts +118 -0
  66. package/src/cli/convoy/events.ts +41 -0
  67. package/src/cli/convoy/store.test.ts +446 -0
  68. package/src/cli/convoy/store.ts +308 -0
  69. package/src/cli/convoy/types.ts +68 -0
  70. package/src/cli/dashboard.ts +5 -1
  71. package/src/cli/init.test.ts +1 -1
  72. package/src/cli/lesson.ts +312 -0
  73. package/src/cli/log.ts +133 -0
  74. package/src/cli/run/executor.test.ts +1 -0
  75. package/src/cli/run/executor.ts +8 -8
  76. package/src/cli/run/loop-executor.ts +199 -0
  77. package/src/cli/run/loop-reporter.ts +125 -0
  78. package/src/cli/run/reporter.ts +30 -1
  79. package/src/cli/run/schema.test.ts +704 -3
  80. package/src/cli/run/schema.ts +206 -56
  81. package/src/cli/run.ts +82 -5
  82. package/src/cli/types.ts +87 -1
  83. package/src/cli/update.ts +62 -1
  84. package/src/dashboard/dist/index.html +14 -15
  85. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  86. package/src/dashboard/scripts/generate-seed-data.ts +23 -43
  87. package/src/dashboard/seed-data/events.ndjson +104 -0
  88. package/src/dashboard/src/pages/index.astro +14 -15
  89. package/src/orchestrator/agents/api-designer.agent.md +1 -1
  90. package/src/orchestrator/agents/architect.agent.md +1 -1
  91. package/src/orchestrator/agents/content-engineer.agent.md +1 -1
  92. package/src/orchestrator/agents/copywriter.agent.md +1 -1
  93. package/src/orchestrator/agents/data-expert.agent.md +1 -1
  94. package/src/orchestrator/agents/database-engineer.agent.md +1 -1
  95. package/src/orchestrator/agents/developer.agent.md +1 -1
  96. package/src/orchestrator/agents/devops-expert.agent.md +1 -1
  97. package/src/orchestrator/agents/documentation-writer.agent.md +1 -1
  98. package/src/orchestrator/agents/performance-expert.agent.md +1 -1
  99. package/src/orchestrator/agents/release-manager.agent.md +1 -1
  100. package/src/orchestrator/agents/security-expert.agent.md +1 -1
  101. package/src/orchestrator/agents/seo-specialist.agent.md +1 -1
  102. package/src/orchestrator/agents/session-guard.agent.md +9 -21
  103. package/src/orchestrator/agents/team-lead.agent.md +8 -34
  104. package/src/orchestrator/agents/testing-expert.agent.md +1 -1
  105. package/src/orchestrator/agents/ui-ux-expert.agent.md +1 -1
  106. package/src/orchestrator/customizations/AGENT-PERFORMANCE.md +11 -12
  107. package/src/orchestrator/customizations/DISPUTES.md +2 -2
  108. package/src/orchestrator/customizations/README.md +1 -3
  109. package/src/orchestrator/customizations/logs/README.md +66 -14
  110. package/src/orchestrator/instructions/ai-optimization.instructions.md +21 -132
  111. package/src/orchestrator/instructions/general.instructions.md +35 -181
  112. package/src/orchestrator/plugins/nx/SKILL.md +1 -1
  113. package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +4 -8
  114. package/src/orchestrator/prompts/bug-fix.prompt.md +4 -4
  115. package/src/orchestrator/prompts/implement-feature.prompt.md +3 -3
  116. package/src/orchestrator/prompts/quick-refinement.prompt.md +3 -3
  117. package/src/orchestrator/prompts/resolve-pr-comments.prompt.md +1 -1
  118. package/src/orchestrator/skills/agent-hooks/SKILL.md +11 -11
  119. package/src/orchestrator/skills/decomposition/SKILL.md +1 -1
  120. package/src/orchestrator/skills/fast-review/SKILL.md +4 -19
  121. package/src/orchestrator/skills/git-workflow/SKILL.md +72 -0
  122. package/src/orchestrator/skills/memory-merger/SKILL.md +1 -1
  123. package/src/orchestrator/skills/observability-logging/SKILL.md +129 -0
  124. package/src/orchestrator/skills/orchestration-protocols/SKILL.md +2 -2
  125. package/src/orchestrator/skills/panel-majority-vote/SKILL.md +4 -7
  126. package/src/orchestrator/skills/self-improvement/SKILL.md +13 -26
  127. package/src/orchestrator/skills/team-lead-reference/SKILL.md +2 -2
  128. package/src/orchestrator/customizations/logs/delegations.ndjson +0 -1
  129. package/src/orchestrator/customizations/logs/panels.ndjson +0 -1
  130. package/src/orchestrator/customizations/logs/reviews.ndjson +0 -0
  131. package/src/orchestrator/customizations/logs/sessions.ndjson +0 -1
  132. /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
+
@@ -11,6 +11,7 @@ function makeTask(overrides: Partial<Task> & { id: string; prompt: string }): Ta
11
11
  depends_on: [],
12
12
  files: [],
13
13
  description: overrides.id,
14
+ max_retries: 1,
14
15
  ...overrides,
15
16
  }
16
17
  }
@@ -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.find((t) => t.id === taskId)!
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.map(
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.length,
235
+ total: spec.tasks!.length,
236
236
  done: 0,
237
237
  failed: 0,
238
238
  skipped: 0,