opencastle 0.10.7 → 0.11.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 (103) hide show
  1. package/README.md +4 -0
  2. package/bin/cli.mjs +4 -0
  3. package/dist/cli/dashboard.d.ts.map +1 -1
  4. package/dist/cli/dashboard.js +5 -1
  5. package/dist/cli/dashboard.js.map +1 -1
  6. package/dist/cli/init.test.js +1 -1
  7. package/dist/cli/init.test.js.map +1 -1
  8. package/dist/cli/lesson.d.ts +17 -0
  9. package/dist/cli/lesson.d.ts.map +1 -0
  10. package/dist/cli/lesson.js +294 -0
  11. package/dist/cli/lesson.js.map +1 -0
  12. package/dist/cli/log.d.ts +7 -0
  13. package/dist/cli/log.d.ts.map +1 -0
  14. package/dist/cli/log.js +131 -0
  15. package/dist/cli/log.js.map +1 -0
  16. package/dist/cli/run/executor.js.map +1 -1
  17. package/dist/cli/run/loop-executor.d.ts +3 -0
  18. package/dist/cli/run/loop-executor.d.ts.map +1 -0
  19. package/dist/cli/run/loop-executor.js +154 -0
  20. package/dist/cli/run/loop-executor.js.map +1 -0
  21. package/dist/cli/run/loop-reporter.d.ts +6 -0
  22. package/dist/cli/run/loop-reporter.d.ts.map +1 -0
  23. package/dist/cli/run/loop-reporter.js +112 -0
  24. package/dist/cli/run/loop-reporter.js.map +1 -0
  25. package/dist/cli/run/reporter.d.ts.map +1 -1
  26. package/dist/cli/run/reporter.js +28 -1
  27. package/dist/cli/run/reporter.js.map +1 -1
  28. package/dist/cli/run/schema.d.ts.map +1 -1
  29. package/dist/cli/run/schema.js +104 -52
  30. package/dist/cli/run/schema.js.map +1 -1
  31. package/dist/cli/run/schema.test.js +214 -0
  32. package/dist/cli/run/schema.test.js.map +1 -1
  33. package/dist/cli/run.d.ts.map +1 -1
  34. package/dist/cli/run.js +84 -3
  35. package/dist/cli/run.js.map +1 -1
  36. package/dist/cli/types.d.ts +59 -1
  37. package/dist/cli/types.d.ts.map +1 -1
  38. package/dist/cli/update.d.ts.map +1 -1
  39. package/dist/cli/update.js +54 -1
  40. package/dist/cli/update.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/cli/dashboard.ts +5 -1
  43. package/src/cli/init.test.ts +1 -1
  44. package/src/cli/lesson.ts +312 -0
  45. package/src/cli/log.ts +133 -0
  46. package/src/cli/run/executor.ts +8 -8
  47. package/src/cli/run/loop-executor.ts +198 -0
  48. package/src/cli/run/loop-reporter.ts +125 -0
  49. package/src/cli/run/reporter.ts +30 -1
  50. package/src/cli/run/schema.test.ts +242 -2
  51. package/src/cli/run/schema.ts +115 -59
  52. package/src/cli/run.ts +82 -5
  53. package/src/cli/types.ts +67 -1
  54. package/src/cli/update.ts +62 -1
  55. package/src/dashboard/dist/index.html +14 -15
  56. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  57. package/src/dashboard/scripts/generate-seed-data.ts +23 -43
  58. package/src/dashboard/seed-data/events.ndjson +104 -0
  59. package/src/dashboard/src/pages/index.astro +14 -15
  60. package/src/orchestrator/agents/api-designer.agent.md +1 -1
  61. package/src/orchestrator/agents/architect.agent.md +1 -1
  62. package/src/orchestrator/agents/content-engineer.agent.md +1 -1
  63. package/src/orchestrator/agents/copywriter.agent.md +1 -1
  64. package/src/orchestrator/agents/data-expert.agent.md +1 -1
  65. package/src/orchestrator/agents/database-engineer.agent.md +1 -1
  66. package/src/orchestrator/agents/developer.agent.md +1 -1
  67. package/src/orchestrator/agents/devops-expert.agent.md +1 -1
  68. package/src/orchestrator/agents/documentation-writer.agent.md +1 -1
  69. package/src/orchestrator/agents/performance-expert.agent.md +1 -1
  70. package/src/orchestrator/agents/release-manager.agent.md +1 -1
  71. package/src/orchestrator/agents/security-expert.agent.md +1 -1
  72. package/src/orchestrator/agents/seo-specialist.agent.md +1 -1
  73. package/src/orchestrator/agents/session-guard.agent.md +9 -21
  74. package/src/orchestrator/agents/team-lead.agent.md +8 -34
  75. package/src/orchestrator/agents/testing-expert.agent.md +1 -1
  76. package/src/orchestrator/agents/ui-ux-expert.agent.md +1 -1
  77. package/src/orchestrator/customizations/AGENT-PERFORMANCE.md +11 -12
  78. package/src/orchestrator/customizations/DISPUTES.md +2 -2
  79. package/src/orchestrator/customizations/README.md +1 -3
  80. package/src/orchestrator/customizations/logs/README.md +66 -14
  81. package/src/orchestrator/instructions/ai-optimization.instructions.md +21 -132
  82. package/src/orchestrator/instructions/general.instructions.md +35 -181
  83. package/src/orchestrator/plugins/nx/SKILL.md +1 -1
  84. package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +4 -8
  85. package/src/orchestrator/prompts/bug-fix.prompt.md +4 -4
  86. package/src/orchestrator/prompts/implement-feature.prompt.md +3 -3
  87. package/src/orchestrator/prompts/quick-refinement.prompt.md +3 -3
  88. package/src/orchestrator/prompts/resolve-pr-comments.prompt.md +1 -1
  89. package/src/orchestrator/skills/agent-hooks/SKILL.md +11 -11
  90. package/src/orchestrator/skills/decomposition/SKILL.md +1 -1
  91. package/src/orchestrator/skills/fast-review/SKILL.md +4 -19
  92. package/src/orchestrator/skills/git-workflow/SKILL.md +72 -0
  93. package/src/orchestrator/skills/memory-merger/SKILL.md +1 -1
  94. package/src/orchestrator/skills/observability-logging/SKILL.md +129 -0
  95. package/src/orchestrator/skills/orchestration-protocols/SKILL.md +2 -2
  96. package/src/orchestrator/skills/panel-majority-vote/SKILL.md +4 -7
  97. package/src/orchestrator/skills/self-improvement/SKILL.md +13 -26
  98. package/src/orchestrator/skills/team-lead-reference/SKILL.md +2 -2
  99. package/src/orchestrator/customizations/logs/delegations.ndjson +0 -1
  100. package/src/orchestrator/customizations/logs/panels.ndjson +0 -1
  101. package/src/orchestrator/customizations/logs/reviews.ndjson +0 -0
  102. package/src/orchestrator/customizations/logs/sessions.ndjson +0 -1
  103. /package/src/orchestrator/customizations/logs/{disputes.ndjson → events.ndjson} +0 -0
package/src/cli/run.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { readFile } from 'node:fs/promises'
1
2
  import { resolve } from 'node:path'
2
3
  import { parseTaskSpec } from './run/schema.js'
3
4
  import { createExecutor, buildPhases } from './run/executor.js'
@@ -9,14 +10,17 @@ const HELP = `
9
10
  opencastle run [options]
10
11
 
11
12
  Process a task queue from a spec file, delegating to AI agents autonomously.
13
+ Supports two modes: tasks (default phase-based execution) and loop (iterative Ralph Loop).
12
14
 
13
15
  Options:
14
16
  --file, -f <path> Task spec file (default: opencastle.tasks.yml)
15
17
  --dry-run Show execution plan without running
16
- --concurrency, -c <n> Override max parallel tasks
18
+ --concurrency, -c <n> Override max parallel tasks (tasks mode)
17
19
  --adapter, -a <name> Override agent runtime adapter
18
20
  --report-dir <path> Where to write run reports (default: .opencastle/runs)
19
21
  --verbose Show full agent output
22
+ --mode <name> Execution mode: tasks | loop
23
+ --max-iterations <n> Override max loop iterations (loop mode)
20
24
  --help, -h Show this help
21
25
  `
22
26
 
@@ -32,6 +36,8 @@ function parseArgs(args: string[]): RunOptions {
32
36
  reportDir: null,
33
37
  verbose: false,
34
38
  help: false,
39
+ maxIterations: null,
40
+ mode: null,
35
41
  }
36
42
 
37
43
  for (let i = 0; i < args.length; i++) {
@@ -73,6 +79,26 @@ function parseArgs(args: string[]): RunOptions {
73
79
  case '--verbose':
74
80
  opts.verbose = true
75
81
  break
82
+ case '--max-iterations': {
83
+ if (i + 1 >= args.length) { console.error(' \u2717 --max-iterations requires a number'); process.exit(1) }
84
+ const val = parseInt(args[++i], 10)
85
+ if (!Number.isFinite(val) || val < 1) {
86
+ console.error(` \u2717 --max-iterations must be an integer >= 1`)
87
+ process.exit(1)
88
+ }
89
+ opts.maxIterations = val
90
+ break
91
+ }
92
+ case '--mode': {
93
+ if (i + 1 >= args.length) { console.error(' \u2717 --mode requires a name'); process.exit(1) }
94
+ const modeVal = args[++i]
95
+ if (modeVal !== 'tasks' && modeVal !== 'loop') {
96
+ console.error(` \u2717 --mode must be one of: tasks, loop`)
97
+ process.exit(1)
98
+ }
99
+ opts.mode = modeVal
100
+ break
101
+ }
76
102
  default:
77
103
  console.error(` ✗ Unknown option: ${arg}`)
78
104
  console.log(HELP)
@@ -102,6 +128,7 @@ export default async function run({ args }: CliContext): Promise<void> {
102
128
  if (opts.concurrency !== null) spec.concurrency = opts.concurrency
103
129
  if (opts.adapter !== null) spec.adapter = opts.adapter
104
130
  if (opts.verbose) spec._verbose = true
131
+ if (opts.mode !== null) spec.mode = opts.mode as 'tasks' | 'loop'
105
132
 
106
133
  // ── Auto-detect adapter if not specified ─────────────────────
107
134
  let detectionFailed = false
@@ -117,9 +144,25 @@ export default async function run({ args }: CliContext): Promise<void> {
117
144
  }
118
145
 
119
146
  // ── Dry run ──────────────────────────────────────────────────
120
- const phases = buildPhases(spec.tasks)
121
-
122
147
  if (opts.dryRun) {
148
+ if (spec.mode === 'loop') {
149
+ const loop = spec.loop!
150
+ console.log(`\n \uD83C\uDFF0 Loop Plan: ${spec.name}`)
151
+ console.log(` Mode: loop`)
152
+ console.log(` Prompt: ${loop.prompt}`)
153
+ console.log(` Max iterations: ${loop.max_iterations}`)
154
+ console.log(` Timeout: ${loop.timeout}`)
155
+ if (loop.plan_file) console.log(` Plan file: ${loop.plan_file}`)
156
+ if (loop.model) console.log(` Model: ${loop.model}`)
157
+ if (loop.backpressure?.length) {
158
+ console.log(` Backpressure:`)
159
+ for (const cmd of loop.backpressure) {
160
+ console.log(` - ${cmd}`)
161
+ }
162
+ }
163
+ return
164
+ }
165
+ const phases = buildPhases(spec.tasks!)
123
166
  printExecutionPlan(spec, phases)
124
167
  return
125
168
  }
@@ -164,8 +207,42 @@ export default async function run({ args }: CliContext): Promise<void> {
164
207
  }
165
208
 
166
209
  // ── Execute ──────────────────────────────────────────────────
167
- console.log(`\n 🏰 OpenCastle Run: ${spec.name}`)
168
- console.log(` Adapter: ${adapter.name} | Concurrency: ${spec.concurrency} | Tasks: ${spec.tasks.length}`)
210
+ if (spec.mode === 'loop') {
211
+ const { createLoopExecutor } = await import('./run/loop-executor.js')
212
+ const { createLoopReporter } = await import('./run/loop-reporter.js')
213
+
214
+ if (opts.maxIterations !== null && spec.loop) {
215
+ spec.loop.max_iterations = opts.maxIterations
216
+ }
217
+
218
+ const promptPath = resolve(process.cwd(), spec.loop!.prompt)
219
+ try {
220
+ await readFile(promptPath)
221
+ } catch {
222
+ console.error(` \u2717 Prompt file not found: ${spec.loop!.prompt}`)
223
+ process.exit(1)
224
+ }
225
+
226
+ console.log(`\n \uD83C\uDFF0 OpenCastle Loop: ${spec.name}`)
227
+ console.log(` Adapter: ${adapter.name} | Max iterations: ${spec.loop!.max_iterations} | Timeout: ${spec.loop!.timeout}`)
228
+ if (spec.loop!.backpressure?.length) {
229
+ console.log(` Backpressure: ${spec.loop!.backpressure.join(', ')}`)
230
+ }
231
+
232
+ const loopReporter = createLoopReporter(spec.name, {
233
+ reportDir: opts.reportDir ? resolve(process.cwd(), opts.reportDir) : undefined,
234
+ verbose: opts.verbose,
235
+ })
236
+
237
+ const loopExecutor = createLoopExecutor(spec, adapter, loopReporter)
238
+ const loopReport = await loopExecutor.run()
239
+
240
+ const failed = loopReport.stoppedReason === 'error' || loopReport.stoppedReason === 'backpressure-fail'
241
+ process.exit(failed ? 1 : 0)
242
+ }
243
+
244
+ console.log(`\n \uD83C\uDFF0 OpenCastle Run: ${spec.name}`)
245
+ console.log(` Adapter: ${adapter.name} | Concurrency: ${spec.concurrency} | Tasks: ${spec.tasks!.length}`)
169
246
 
170
247
  const reporter = createReporter(spec, {
171
248
  reportDir: opts.reportDir
package/src/cli/types.ts CHANGED
@@ -126,13 +126,31 @@ export const IDE_LABELS: Record<IdeChoice, string> = {
126
126
 
127
127
  // ── Run command types ──────────────────────────────────────────
128
128
 
129
+ /** Loop execution configuration. */
130
+ export interface LoopConfig {
131
+ /** Maximum number of agent iterations (default 20). */
132
+ max_iterations: number;
133
+ /** Path to the prompt file read each iteration. */
134
+ prompt: string;
135
+ /** Path to the plan file (default 'IMPLEMENTATION_PLAN.md'). */
136
+ plan_file?: string;
137
+ /** Per-iteration timeout (default '10m'). */
138
+ timeout: string;
139
+ /** Model override for loop sessions. */
140
+ model?: string;
141
+ /** Shell commands that must exit 0 after each iteration. */
142
+ backpressure?: string[];
143
+ }
144
+
129
145
  /** Validated task spec from YAML. */
130
146
  export interface TaskSpec {
131
147
  name: string;
132
148
  concurrency: number;
133
149
  on_failure: 'continue' | 'stop';
134
150
  adapter: string;
135
- tasks: Task[];
151
+ tasks?: Task[];
152
+ mode?: 'tasks' | 'loop';
153
+ loop?: LoopConfig;
136
154
  _verbose?: boolean;
137
155
  }
138
156
 
@@ -231,6 +249,8 @@ export interface RunOptions {
231
249
  reportDir: string | null;
232
250
  verbose: boolean;
233
251
  help: boolean;
252
+ maxIterations: number | null;
253
+ mode: string | null;
234
254
  }
235
255
 
236
256
  /** Validation result. */
@@ -250,3 +270,49 @@ export interface Executor {
250
270
  run(): Promise<RunReport>;
251
271
  getPhases(): Task[][];
252
272
  }
273
+
274
+ // ── Loop executor types ────────────────────────────────────────
275
+
276
+ /** Result of a single backpressure command run. */
277
+ export interface BackpressureResult {
278
+ command: string;
279
+ exitCode: number;
280
+ output: string;
281
+ passed: boolean;
282
+ }
283
+
284
+ /** Result of a single loop iteration. */
285
+ export interface LoopIterationResult {
286
+ iteration: number;
287
+ status: 'done' | 'failed' | 'backpressure-fail';
288
+ duration: number;
289
+ output: string;
290
+ backpressureResults?: BackpressureResult[];
291
+ }
292
+
293
+ /** Final report produced by the loop executor. */
294
+ export interface LoopRunReport {
295
+ name: string;
296
+ mode: 'loop';
297
+ startedAt: string;
298
+ completedAt: string;
299
+ duration: string;
300
+ totalIterations: number;
301
+ completedIterations: number;
302
+ stoppedReason: 'max-iterations' | 'plan-empty' | 'backpressure-fail' | 'user-abort' | 'error';
303
+ iterations: LoopIterationResult[];
304
+ }
305
+
306
+ /** Reporter interface for loop execution progress. */
307
+ export interface LoopReporter {
308
+ onIterationStart(iteration: number, maxIterations: number): void;
309
+ onIterationDone(iteration: number, result: LoopIterationResult): void;
310
+ onBackpressureStart(command: string): void;
311
+ onBackpressureResult(result: BackpressureResult): void;
312
+ onComplete(report: LoopRunReport): Promise<void>;
313
+ }
314
+
315
+ /** Executor for loop-mode run specs. */
316
+ export interface LoopExecutor {
317
+ run(): Promise<LoopRunReport>;
318
+ }
package/src/cli/update.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { resolve } from 'node:path'
2
- import { readFile } from 'node:fs/promises'
2
+ import { existsSync } from 'node:fs'
3
+ import { readFile, appendFile, rename } from 'node:fs/promises'
3
4
  import { readManifest, writeManifest } from './manifest.js'
4
5
  import { multiselect, confirm, closePrompts, c } from './prompt.js'
5
6
  import { isLegacyStack, migrateStackConfig, IDE_LABELS } from './types.js'
@@ -247,6 +248,9 @@ export default async function update({
247
248
  }
248
249
  }
249
250
 
251
+ // ── Migrate legacy log files ────────────────────────────────────
252
+ await migrateLegacyLogs(projectRoot)
253
+
250
254
  // ── Update manifest ─────────────────────────────────────────────
251
255
  if (hasVersionUpdate) manifest.version = pkg.version
252
256
  manifest.ides = ides
@@ -322,6 +326,63 @@ export default async function update({
322
326
  closePrompts()
323
327
  }
324
328
 
329
+ async function migrateLegacyLogs(projectRoot: string): Promise<void> {
330
+ const logsDir = resolve(projectRoot, '.github', 'customizations', 'logs')
331
+ if (!existsSync(logsDir)) return
332
+
333
+ const typeMap: Record<string, string> = {
334
+ 'sessions.ndjson': 'session',
335
+ 'delegations.ndjson': 'delegation',
336
+ 'reviews.ndjson': 'review',
337
+ 'panels.ndjson': 'panel',
338
+ 'disputes.ndjson': 'dispute',
339
+ }
340
+
341
+ const eventsFile = resolve(logsDir, 'events.ndjson')
342
+ let totalMigrated = 0
343
+
344
+ for (const [filename, type] of Object.entries(typeMap)) {
345
+ const filePath = resolve(logsDir, filename)
346
+ if (!existsSync(filePath)) continue
347
+
348
+ let content: string
349
+ try {
350
+ content = await readFile(filePath, 'utf8')
351
+ } catch {
352
+ continue
353
+ }
354
+
355
+ const lines = content.split('\n').filter((line) => line.trim() !== '')
356
+ if (lines.length === 0) continue
357
+
358
+ const migratedLines: string[] = []
359
+ for (const line of lines) {
360
+ try {
361
+ const record = JSON.parse(line) as Record<string, unknown>
362
+ if (!record['type']) {
363
+ record['type'] = type
364
+ }
365
+ migratedLines.push(JSON.stringify(record))
366
+ } catch {
367
+ console.warn(` ${c.yellow('⚠')} Skipping malformed JSON line in ${filename}`)
368
+ }
369
+ }
370
+
371
+ if (migratedLines.length > 0) {
372
+ await appendFile(eventsFile, migratedLines.join('\n') + '\n', 'utf8')
373
+ totalMigrated += migratedLines.length
374
+ }
375
+
376
+ await rename(filePath, filePath + '.migrated')
377
+ }
378
+
379
+ if (totalMigrated > 0) {
380
+ console.log(
381
+ ` ${c.green('✓')} Migrated ${c.bold(String(totalMigrated))} records from legacy log files to events.ndjson`
382
+ )
383
+ }
384
+ }
385
+
325
386
  function sameSet(a: Set<string>, b: Set<string>): boolean {
326
387
  if (a.size !== b.size) return false
327
388
  for (const item of a) {
@@ -1059,29 +1059,28 @@ Export
1059
1059
  // ── Export ─────────────────────────────────────────────────
1060
1060
 
1061
1061
  function exportData() {
1062
- const data = {
1063
- exported_at: new Date().toISOString(),
1064
- sessions: rawSessions,
1065
- delegations: rawDelegations,
1066
- panels: rawPanels,
1067
- reviews: rawReviews,
1068
- };
1069
- const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
1062
+ const events = [
1063
+ ...rawSessions,
1064
+ ...rawDelegations,
1065
+ ...rawPanels,
1066
+ ...rawReviews,
1067
+ ].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
1068
+ const blob = new Blob([events.map((e) => JSON.stringify(e)).join('\n') + '\n'], { type: 'application/x-ndjson' });
1070
1069
  const url = URL.createObjectURL(blob);
1071
1070
  const a = document.createElement('a');
1072
1071
  a.href = url;
1073
- a.download = 'opencastle-dashboard-' + new Date().toISOString().slice(0, 10) + '.json';
1072
+ a.download = 'opencastle-events-' + new Date().toISOString().slice(0, 10) + '.ndjson';
1074
1073
  a.click();
1075
1074
  URL.revokeObjectURL(url);
1076
1075
  }
1077
1076
 
1078
1077
  async function main() {
1079
- const [sessions, delegations, panels, reviews] = await Promise.all([
1080
- loadNdjson(base + 'data/sessions.ndjson'),
1081
- loadNdjson(base + 'data/delegations.ndjson'),
1082
- loadNdjson(base + 'data/panels.ndjson'),
1083
- loadNdjson(base + 'data/reviews.ndjson'),
1084
- ]);
1078
+ const events = await loadNdjson(base + 'data/events.ndjson');
1079
+
1080
+ const sessions = events.filter((e) => e.type === 'session');
1081
+ const delegations = events.filter((e) => e.type === 'delegation');
1082
+ const panels = events.filter((e) => e.type === 'panel');
1083
+ const reviews = events.filter((e) => e.type === 'review');
1085
1084
 
1086
1085
  rawSessions = sessions;
1087
1086
  rawDelegations = delegations;
@@ -1,25 +1,25 @@
1
1
  {
2
- "hash": "314c051f",
2
+ "hash": "87401706",
3
3
  "configHash": "30f8ea04",
4
- "lockfileHash": "153a5e79",
5
- "browserHash": "9424a639",
4
+ "lockfileHash": "56be8082",
5
+ "browserHash": "8ccc6e37",
6
6
  "optimized": {
7
7
  "astro > cssesc": {
8
8
  "src": "../../../../../node_modules/cssesc/cssesc.js",
9
9
  "file": "astro___cssesc.js",
10
- "fileHash": "de6d329b",
10
+ "fileHash": "7e157996",
11
11
  "needsInterop": true
12
12
  },
13
13
  "astro > aria-query": {
14
14
  "src": "../../../../../node_modules/aria-query/lib/index.js",
15
15
  "file": "astro___aria-query.js",
16
- "fileHash": "280e2b7f",
16
+ "fileHash": "021f979d",
17
17
  "needsInterop": true
18
18
  },
19
19
  "astro > axobject-query": {
20
20
  "src": "../../../../../node_modules/axobject-query/lib/index.js",
21
21
  "file": "astro___axobject-query.js",
22
- "fileHash": "3aee3dc0",
22
+ "fileHash": "f1a04609",
23
23
  "needsInterop": true
24
24
  }
25
25
  },
@@ -2,15 +2,13 @@
2
2
  /**
3
3
  * Generate realistic seed data for the Agent Dashboard.
4
4
  *
5
- * Writes NDJSON files to docs/ai-agents/logs/:
6
- * - sessions.ndjson (50 records)
7
- * - delegations.ndjson (35 records)
8
- * - panels.ndjson (12 records + preserves existing)
5
+ * Writes a single NDJSON file to the configured logs directory:
6
+ * - events.ndjson (50 session + 35 delegation + 12 panel records, sorted by timestamp)
9
7
  *
10
8
  * Usage: npx tsx opencastle/src/dashboard/scripts/generate-seed-data.ts
11
9
  */
12
10
 
13
- import { readFileSync, writeFileSync, mkdirSync } from 'fs';
11
+ import { writeFileSync, mkdirSync } from 'fs';
14
12
  import { join } from 'path';
15
13
 
16
14
  const REPO_ROOT = join(__dirname, '..', '..', '..', '..');
@@ -156,6 +154,7 @@ const END_DATE = new Date('2026-02-25T18:00:00Z');
156
154
  // --- Generate Sessions ---
157
155
 
158
156
  interface SessionRecord {
157
+ type: 'session';
159
158
  timestamp: string;
160
159
  agent: string;
161
160
  model: string;
@@ -180,6 +179,7 @@ function generateSessions(count: number): SessionRecord[] {
180
179
  const discoveries = rng.next() > 0.7 ? [rng.pick(TASK_DESCRIPTIONS)] : [];
181
180
 
182
181
  records.push({
182
+ type: 'session',
183
183
  timestamp: generateTimestamp(i, count, START_DATE, END_DATE),
184
184
  agent: rng.pick(AGENTS),
185
185
  model: rng.pick(MODELS),
@@ -199,6 +199,7 @@ function generateSessions(count: number): SessionRecord[] {
199
199
  // --- Generate Delegations ---
200
200
 
201
201
  interface DelegationRecord {
202
+ type: 'delegation';
202
203
  timestamp: string;
203
204
  session_id: string;
204
205
  agent: string;
@@ -220,9 +221,9 @@ function generateDelegations(count: number): DelegationRecord[] {
220
221
  const outcome = outcomeRoll < 0.75 ? 'success' : outcomeRoll < 0.9 ? 'partial' : 'failed';
221
222
 
222
223
  records.push({
224
+ type: 'delegation',
223
225
  timestamp: generateTimestamp(i, count, START_DATE, END_DATE),
224
- session_id: `feat/${issue.toLowerCase()}`,
225
- agent: rng.pick(AGENTS),
226
+ session_id: `feat/${issue.toLowerCase()}`, agent: rng.pick(AGENTS),
226
227
  model: rng.pick(MODELS),
227
228
  tier: rng.weighted(TIERS),
228
229
  mechanism: rng.next() < 0.6 ? 'sub-agent' : 'background',
@@ -239,6 +240,7 @@ function generateDelegations(count: number): DelegationRecord[] {
239
240
  // --- Generate Panels ---
240
241
 
241
242
  interface PanelRecord {
243
+ type: 'panel';
242
244
  timestamp: string;
243
245
  panel_key: string;
244
246
  verdict: string;
@@ -264,6 +266,7 @@ function generatePanels(count: number): PanelRecord[] {
264
266
  const issue = rng.pick(TRACKER_ISSUES);
265
267
 
266
268
  records.push({
269
+ type: 'panel',
267
270
  timestamp: generateTimestamp(i, count, START_DATE, END_DATE),
268
271
  panel_key: panelKey,
269
272
  verdict: isPass ? 'pass' : 'block',
@@ -287,50 +290,27 @@ function generatePanels(count: number): PanelRecord[] {
287
290
  function main() {
288
291
  mkdirSync(LOGS_DIR, { recursive: true });
289
292
 
290
- // Read existing panel records to preserve them
291
- const panelsPath = join(LOGS_DIR, 'panels.ndjson');
292
- let existingPanels: string[] = [];
293
- try {
294
- const content = readFileSync(panelsPath, 'utf-8').trim();
295
- if (content) {
296
- existingPanels = content.split('\n').filter(Boolean);
297
- }
298
- } catch {
299
- // File doesn't exist yet
300
- }
301
-
302
293
  // Generate data
303
294
  const sessions = generateSessions(50);
304
295
  const delegations = generateDelegations(35);
305
296
  const panels = generatePanels(12);
306
297
 
307
- // Write sessions
308
- const sessionsPath = join(LOGS_DIR, 'sessions.ndjson');
309
- writeFileSync(sessionsPath, sessions.map((r) => JSON.stringify(r)).join('\n') + '\n');
310
- console.log(`Wrote ${sessions.length} session records to ${sessionsPath}`);
311
-
312
- // Write delegations
313
- const delegationsPath = join(LOGS_DIR, 'delegations.ndjson');
314
- writeFileSync(delegationsPath, delegations.map((r) => JSON.stringify(r)).join('\n') + '\n');
315
- console.log(`Wrote ${delegations.length} delegation records to ${delegationsPath}`);
316
-
317
- // Write panels (preserve existing + add new, sorted by timestamp)
318
- const allPanelLines = [
319
- ...existingPanels,
320
- ...panels.map((r) => JSON.stringify(r)),
321
- ];
322
- // Parse and sort all panel records by timestamp
323
- const allPanelRecords = allPanelLines
324
- .map((line) => JSON.parse(line))
325
- .sort((a: { timestamp: string }, b: { timestamp: string }) => a.timestamp.localeCompare(b.timestamp));
326
- writeFileSync(panelsPath, allPanelRecords.map((r: unknown) => JSON.stringify(r)).join('\n') + '\n');
327
- console.log(`Wrote ${allPanelRecords.length} panel records to ${panelsPath} (${existingPanels.length} existing + ${panels.length} new)`);
298
+ // Merge and sort all events by timestamp
299
+ type AnyRecord = SessionRecord | DelegationRecord | PanelRecord;
300
+ const allEvents: AnyRecord[] = [...sessions, ...delegations, ...panels]
301
+ .sort((a, b) => a.timestamp.localeCompare(b.timestamp));
302
+
303
+ // Write single events.ndjson
304
+ const eventsPath = join(LOGS_DIR, 'events.ndjson');
305
+ writeFileSync(eventsPath, allEvents.map((r) => JSON.stringify(r)).join('\n') + '\n');
306
+ console.log(`Wrote ${allEvents.length} event records to ${eventsPath}`);
328
307
 
329
308
  // Summary
330
309
  console.log('\n--- Seed Data Summary ---');
331
310
  console.log(`Sessions: ${sessions.length}`);
332
311
  console.log(`Delegations: ${delegations.length}`);
333
- console.log(`Panels: ${allPanelRecords.length} (${existingPanels.length} existing + ${panels.length} generated)`);
312
+ console.log(`Panels: ${panels.length}`);
313
+ console.log(`Total: ${allEvents.length}`);
334
314
 
335
315
  // Outcome distribution
336
316
  const sessionOutcomes = sessions.reduce<Record<string, number>>((acc, s) => {
@@ -345,10 +325,10 @@ function main() {
345
325
  }, {});
346
326
  console.log(`Delegation tiers: ${JSON.stringify(tierDist)}`);
347
327
 
348
- const panelVerdicts = allPanelRecords.reduce((acc: Record<string, number>, p: { verdict: string }) => {
328
+ const panelVerdicts = panels.reduce<Record<string, number>>((acc, p) => {
349
329
  acc[p.verdict] = (acc[p.verdict] || 0) + 1;
350
330
  return acc;
351
- }, {} as Record<string, number>);
331
+ }, {});
352
332
  console.log(`Panel verdicts: ${JSON.stringify(panelVerdicts)}`);
353
333
  }
354
334