opencastle 0.28.0 → 0.30.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 (94) hide show
  1. package/README.md +12 -3
  2. package/dist/cli/convoy/engine.d.ts.map +1 -1
  3. package/dist/cli/convoy/engine.js +1 -10
  4. package/dist/cli/convoy/engine.js.map +1 -1
  5. package/dist/cli/convoy/engine.test.js +1 -0
  6. package/dist/cli/convoy/engine.test.js.map +1 -1
  7. package/dist/cli/convoy/export.d.ts +1 -3
  8. package/dist/cli/convoy/export.d.ts.map +1 -1
  9. package/dist/cli/convoy/export.js +9 -88
  10. package/dist/cli/convoy/export.js.map +1 -1
  11. package/dist/cli/convoy/export.test.js +7 -186
  12. package/dist/cli/convoy/export.test.js.map +1 -1
  13. package/dist/cli/convoy/pipeline.d.ts.map +1 -1
  14. package/dist/cli/convoy/pipeline.js +0 -21
  15. package/dist/cli/convoy/pipeline.js.map +1 -1
  16. package/dist/cli/convoy/pipeline.test.js +0 -21
  17. package/dist/cli/convoy/pipeline.test.js.map +1 -1
  18. package/dist/cli/dashboard.d.ts.map +1 -1
  19. package/dist/cli/dashboard.js +32 -8
  20. package/dist/cli/dashboard.js.map +1 -1
  21. package/dist/cli/destroy.d.ts.map +1 -1
  22. package/dist/cli/destroy.js +13 -0
  23. package/dist/cli/destroy.js.map +1 -1
  24. package/dist/cli/dispute.d.ts +3 -0
  25. package/dist/cli/dispute.d.ts.map +1 -0
  26. package/dist/cli/dispute.js +25 -0
  27. package/dist/cli/dispute.js.map +1 -0
  28. package/dist/cli/doctor.d.ts +1 -1
  29. package/dist/cli/doctor.d.ts.map +1 -1
  30. package/dist/cli/doctor.js +14 -1
  31. package/dist/cli/doctor.js.map +1 -1
  32. package/dist/cli/eject.d.ts.map +1 -1
  33. package/dist/cli/eject.js +14 -0
  34. package/dist/cli/eject.js.map +1 -1
  35. package/dist/cli/init.d.ts.map +1 -1
  36. package/dist/cli/init.js +14 -0
  37. package/dist/cli/init.js.map +1 -1
  38. package/dist/cli/log.d.ts +0 -11
  39. package/dist/cli/log.d.ts.map +1 -1
  40. package/dist/cli/log.js +2 -114
  41. package/dist/cli/log.js.map +1 -1
  42. package/dist/cli/pipeline.d.ts +17 -0
  43. package/dist/cli/pipeline.d.ts.map +1 -1
  44. package/dist/cli/pipeline.js +259 -24
  45. package/dist/cli/pipeline.js.map +1 -1
  46. package/dist/cli/pipeline.test.d.ts +2 -0
  47. package/dist/cli/pipeline.test.d.ts.map +1 -0
  48. package/dist/cli/pipeline.test.js +178 -0
  49. package/dist/cli/pipeline.test.js.map +1 -0
  50. package/dist/cli/run.js +2 -2
  51. package/dist/cli/run.js.map +1 -1
  52. package/dist/cli/update.d.ts.map +1 -1
  53. package/dist/cli/update.js +16 -0
  54. package/dist/cli/update.js.map +1 -1
  55. package/dist/cli/watch.d.ts.map +1 -1
  56. package/dist/cli/watch.js +1 -3
  57. package/dist/cli/watch.js.map +1 -1
  58. package/package.json +1 -1
  59. package/src/cli/convoy/engine.test.ts +1 -0
  60. package/src/cli/convoy/engine.ts +1 -4
  61. package/src/cli/convoy/export.test.ts +7 -224
  62. package/src/cli/convoy/export.ts +10 -106
  63. package/src/cli/convoy/pipeline.test.ts +0 -25
  64. package/src/cli/convoy/pipeline.ts +0 -19
  65. package/src/cli/dashboard.ts +33 -8
  66. package/src/cli/destroy.ts +15 -0
  67. package/src/cli/dispute.ts +28 -0
  68. package/src/cli/doctor.ts +16 -1
  69. package/src/cli/eject.ts +16 -0
  70. package/src/cli/init.ts +16 -0
  71. package/src/cli/log.ts +2 -120
  72. package/src/cli/pipeline.test.ts +191 -0
  73. package/src/cli/pipeline.ts +326 -26
  74. package/src/cli/run.ts +2 -2
  75. package/src/cli/update.ts +18 -0
  76. package/src/cli/watch.ts +1 -3
  77. package/src/dashboard/dist/_astro/index.Je1YjU_y.css +1 -0
  78. package/src/dashboard/dist/index.html +537 -1394
  79. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  80. package/src/dashboard/scripts/etl.test.ts +4 -62
  81. package/src/dashboard/scripts/etl.ts +13 -33
  82. package/src/dashboard/src/pages/index.astro +684 -1624
  83. package/src/dashboard/src/styles/dashboard.css +473 -7
  84. package/src/orchestrator/agents/team-lead.agent.md +13 -0
  85. package/src/orchestrator/prompts/brainstorm.prompt.md +1 -0
  86. package/src/orchestrator/prompts/fix-prd.prompt.md +58 -0
  87. package/src/orchestrator/prompts/generate-convoy.prompt.md +30 -0
  88. package/src/orchestrator/prompts/generate-prd.prompt.md +38 -0
  89. package/dist/cli/convoy/log-merge.test.d.ts +0 -2
  90. package/dist/cli/convoy/log-merge.test.d.ts.map +0 -1
  91. package/dist/cli/convoy/log-merge.test.js +0 -147
  92. package/dist/cli/convoy/log-merge.test.js.map +0 -1
  93. package/src/cli/convoy/log-merge.test.ts +0 -179
  94. package/src/dashboard/dist/_astro/index.6L3_HsPT.css +0 -1
@@ -1,10 +1,56 @@
1
- import { readFile } from 'node:fs/promises'
1
+ import { readFile, writeFile, mkdir } from 'node:fs/promises'
2
2
  import { existsSync } from 'node:fs'
3
- import { resolve } from 'node:path'
4
- import { c } from './prompt.js'
3
+ import { resolve, dirname } from 'node:path'
4
+ import { stringify } from 'yaml'
5
+ import { c, confirm, closePrompts } from './prompt.js'
5
6
  import { runPromptStep } from './plan.js'
6
7
  import type { CliContext } from './types.js'
7
8
 
9
+ export interface ConvoyGroup {
10
+ name: string
11
+ description: string
12
+ phases: number[]
13
+ depends_on: string[]
14
+ }
15
+
16
+ export interface ComplexityAssessment {
17
+ total_tasks: number
18
+ total_phases: number
19
+ domains: string[]
20
+ estimated_duration_minutes?: number
21
+ complexity: 'low' | 'medium' | 'high'
22
+ recommended_strategy: 'single' | 'chain'
23
+ chain_rationale?: string
24
+ convoy_groups: ConvoyGroup[]
25
+ }
26
+
27
+ export function parseComplexityAssessment(prdContent: string): ComplexityAssessment | null {
28
+ const sectionMatch = prdContent.match(/## Complexity Assessment\s+([\s\S]*?)(?=\n## |\n# |$)/)
29
+ if (!sectionMatch) return null
30
+
31
+ const sectionContent = sectionMatch[1]
32
+ const jsonMatch = sectionContent.match(/```json\s*([\s\S]*?)```/)
33
+ if (!jsonMatch) return null
34
+
35
+ try {
36
+ const parsed = JSON.parse(jsonMatch[1].trim()) as ComplexityAssessment
37
+ // Validate required fields
38
+ if (
39
+ typeof parsed.total_tasks !== 'number' ||
40
+ typeof parsed.total_phases !== 'number' ||
41
+ !Array.isArray(parsed.domains) ||
42
+ !parsed.complexity ||
43
+ !parsed.recommended_strategy ||
44
+ !Array.isArray(parsed.convoy_groups)
45
+ ) {
46
+ return null
47
+ }
48
+ return parsed
49
+ } catch {
50
+ return null
51
+ }
52
+ }
53
+
8
54
  const HELP = `
9
55
  opencastle pipeline [options]
10
56
 
@@ -12,9 +58,10 @@ const HELP = `
12
58
 
13
59
  Step 1 — Generate PRD (generate-prd)
14
60
  Step 2 — Validate PRD (validate-prd)
15
- Step 3 — Generate convoy spec (generate-convoy, using PRD as BDO)
16
- Step 4 — Validate convoy spec (validate-convoy)
17
- Step 5 — Fix convoy spec (fix-convoy, up to 2 retries if invalid)
61
+ Step 3 — Fix PRD (fix-prd, up to 2 retries if invalid)
62
+ Step 4 — Generate convoy spec (generate-convoy, using PRD as BDO)
63
+ Step 5 — Validate convoy spec (validate-convoy)
64
+ Step 6 — Fix convoy spec (fix-convoy, up to 2 retries if invalid)
18
65
 
19
66
  Options:
20
67
  --text, -t <text> Feature prompt text (required, unless --prd is set)
@@ -137,7 +184,7 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
137
184
  }
138
185
  }
139
186
 
140
- const totalSteps = opts.skipValidation ? 3 : 5
187
+ const totalSteps = opts.skipValidation ? 3 : 6
141
188
  const sharedOpts = {
142
189
  adapterName: opts.adapter ?? undefined,
143
190
  verbose: opts.verbose,
@@ -196,20 +243,254 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
196
243
  }
197
244
 
198
245
  if (!result.isValid) {
199
- console.log(c.red(` ✗ PRD validation failed.\n`))
200
- console.log(result.errors ?? result.rawOutput)
246
+ let prdValidationErrors = result.errors ?? result.rawOutput
247
+ console.log(c.yellow(` ⚠ PRD has validation issues — attempting auto-fix…\n`))
248
+ console.log(c.dim(prdValidationErrors))
249
+ console.log()
250
+
251
+ // ── Step 3: Fix PRD (up to 2 retries) ──────────────────────────────────
252
+ const MAX_PRD_FIX_RETRIES = 2
253
+ let fixedPrdContent = prdContent
254
+ let prdFixed = false
255
+
256
+ for (let attempt = 1; attempt <= MAX_PRD_FIX_RETRIES; attempt++) {
257
+ const label = `Fix PRD attempt ${attempt}/${MAX_PRD_FIX_RETRIES}…`
258
+ console.log(stepLabel(3, totalSteps, label))
259
+
260
+ try {
261
+ await runPromptStep({
262
+ ...sharedOpts,
263
+ template: 'fix-prd',
264
+ goalText: fixedPrdContent,
265
+ contextText: prdValidationErrors,
266
+ outputPath: prdPath, // overwrite in place
267
+ })
268
+ } catch (err) {
269
+ console.error(`\n ✗ Step 3 (attempt ${attempt}) failed: ${err instanceof Error ? err.message : String(err)}`)
270
+ process.exit(1)
271
+ }
272
+
273
+ console.log(c.dim(` Re-validating after fix…`))
274
+
275
+ fixedPrdContent = await readFile(prdPath, 'utf8')
276
+
277
+ let revalidation
278
+ try {
279
+ revalidation = await runPromptStep({
280
+ ...sharedOpts,
281
+ template: 'validate-prd',
282
+ goalText: fixedPrdContent,
283
+ })
284
+ } catch (err) {
285
+ console.error(`\n ✗ Re-validation failed: ${err instanceof Error ? err.message : String(err)}`)
286
+ process.exit(1)
287
+ }
288
+
289
+ if (revalidation.isValid) {
290
+ console.log(c.green(` ✓ PRD fixed and validated\n`))
291
+ prdFixed = true
292
+ break
293
+ }
294
+
295
+ prdValidationErrors = revalidation.errors ?? revalidation.rawOutput
296
+
297
+ if (attempt < MAX_PRD_FIX_RETRIES) {
298
+ console.log(c.yellow(` ⚠ Still has issues after fix attempt ${attempt} — retrying…\n`))
299
+ console.log(c.dim(prdValidationErrors))
300
+ console.log()
301
+ }
302
+ }
303
+
304
+ if (!prdFixed) {
305
+ console.log(c.red(`\n ✗ Could not auto-fix the PRD after ${MAX_PRD_FIX_RETRIES} attempts.\n`))
306
+ console.log(` Remaining issues:\n`)
307
+ console.log(prdValidationErrors)
308
+ console.log(
309
+ c.dim(`\n The PRD has been saved to ${relPath(prdPath)} with the best available fixes.\n`) +
310
+ c.dim(` Review the remaining issues above and edit the file manually, then re-run with:\n`) +
311
+ ` opencastle pipeline --prd ${relPath(prdPath)}${opts.adapter ? ` --adapter ${opts.adapter}` : ''}\n`
312
+ )
313
+ process.exit(1)
314
+ }
315
+ } else {
316
+ console.log(c.green(` ✓ PRD is valid\n`))
317
+ }
318
+ }
319
+
320
+ // ── Complexity-aware strategy decision ────────────────────────────────────
321
+ const prdContentForComplexity = await readFile(prdPath, 'utf8')
322
+ const complexity = parseComplexityAssessment(prdContentForComplexity)
323
+
324
+ if (complexity) {
325
+ if (complexity.recommended_strategy === 'chain' && complexity.convoy_groups.length > 1) {
201
326
  console.log(
202
- c.dim(`\n Fix the PRD at ${relPath(prdPath)} and re-run with:\n`) +
203
- ` opencastle pipeline --prd ${relPath(prdPath)}${opts.adapter ? ` --adapter ${opts.adapter}` : ''}\n`
327
+ c.cyan(` ℹ`) +
328
+ ` Complexity: ${complexity.complexity} | Strategy: chain | ${complexity.convoy_groups.length} convoy groups\n`
329
+ )
330
+ console.log(` Chain plan:`)
331
+ for (let i = 0; i < complexity.convoy_groups.length; i++) {
332
+ const g = complexity.convoy_groups[i]
333
+ const depStr =
334
+ g.depends_on.length > 0 ? ` → depends on: ${g.depends_on.join(', ')}` : ''
335
+ console.log(
336
+ ` ${i + 1}. ${g.name.padEnd(20)} (phases: ${g.phases.join(', ')})${depStr}`
337
+ )
338
+ }
339
+ console.log()
340
+
341
+ const convoyDir = resolve(process.cwd(), '.opencastle', 'convoys')
342
+ await mkdir(convoyDir, { recursive: true })
343
+
344
+ const genBaseStep = opts.skipValidation ? 2 : 4
345
+ const groupSpecPaths: string[] = []
346
+ const totalGroupSteps =
347
+ (opts.skipValidation ? 2 : 3) + complexity.convoy_groups.length * (opts.skipValidation ? 1 : 2)
348
+
349
+ for (let i = 0; i < complexity.convoy_groups.length; i++) {
350
+ const group = complexity.convoy_groups[i]
351
+ const groupStep = genBaseStep + i * (opts.skipValidation ? 1 : 2)
352
+
353
+ console.log(
354
+ stepLabel(
355
+ groupStep,
356
+ totalGroupSteps,
357
+ `Generating convoy spec for group: ${group.name}…`
358
+ )
359
+ )
360
+
361
+ const chainContext = JSON.stringify({
362
+ mode: 'chain_subset',
363
+ group_name: group.name,
364
+ group_description: group.description,
365
+ group_phases: group.phases,
366
+ depends_on_groups: group.depends_on,
367
+ total_groups: complexity.convoy_groups.length,
368
+ group_index: i + 1,
369
+ })
370
+
371
+ const groupSpecPath = resolve(convoyDir, `${group.name}.convoy.yml`)
372
+
373
+ let groupResult
374
+ try {
375
+ groupResult = await runPromptStep({
376
+ ...sharedOpts,
377
+ template: 'generate-convoy',
378
+ filePath: prdPath,
379
+ contextText: chainContext,
380
+ outputPath: groupSpecPath,
381
+ })
382
+ } catch (err) {
383
+ console.error(
384
+ `\n ✗ Step ${groupStep} failed: ${err instanceof Error ? err.message : String(err)}`
385
+ )
386
+ process.exit(1)
387
+ }
388
+
389
+ const resolvedGroupSpecPath = groupResult.outputPath ?? groupSpecPath
390
+ groupSpecPaths.push(resolvedGroupSpecPath)
391
+
392
+ console.log(c.green(` ✓ Group spec written to ${relPath(resolvedGroupSpecPath)}\n`))
393
+
394
+ if (!opts.skipValidation) {
395
+ const valStep = groupStep + 1
396
+ console.log(stepLabel(valStep, totalGroupSteps, `Validating spec: ${group.name}…`))
397
+
398
+ const groupSpecContent = await readFile(resolvedGroupSpecPath, 'utf8')
399
+ let groupValidation
400
+ try {
401
+ groupValidation = await runPromptStep({
402
+ ...sharedOpts,
403
+ template: 'validate-convoy',
404
+ goalText: groupSpecContent,
405
+ })
406
+ } catch (err) {
407
+ console.error(
408
+ `\n ✗ Validation failed for group ${group.name}: ${err instanceof Error ? err.message : String(err)}`
409
+ )
410
+ process.exit(1)
411
+ }
412
+
413
+ if (!groupValidation.isValid) {
414
+ console.log(c.yellow(` ⚠ Spec has issues — attempting one auto-fix…\n`))
415
+ console.log(c.dim(groupValidation.errors ?? groupValidation.rawOutput))
416
+ console.log()
417
+
418
+ try {
419
+ await runPromptStep({
420
+ ...sharedOpts,
421
+ template: 'fix-convoy',
422
+ goalText: groupSpecContent,
423
+ contextText: groupValidation.errors ?? groupValidation.rawOutput,
424
+ outputPath: resolvedGroupSpecPath,
425
+ })
426
+ } catch (err) {
427
+ console.error(
428
+ `\n ✗ Fix failed for group ${group.name}: ${err instanceof Error ? err.message : String(err)}`
429
+ )
430
+ process.exit(1)
431
+ }
432
+
433
+ console.log(c.dim(` Applied fix for ${group.name}\n`))
434
+ } else {
435
+ console.log(c.green(` ✓ Spec valid\n`))
436
+ }
437
+ }
438
+ }
439
+
440
+ // Build master pipeline spec (version 2)
441
+ const featureNameMatch = prdContentForComplexity.match(/^# (.+?)\s*(?:—|-)?\s*PRD/m)
442
+ const featureName = featureNameMatch
443
+ ? featureNameMatch[1].trim().toLowerCase().replace(/[^a-z0-9]+/g, '-')
444
+ : 'feature'
445
+
446
+ const branchMatch = prdContentForComplexity.match(/`feat\/([^`]+)`/)
447
+ const branch = branchMatch ? `feat/${branchMatch[1]}` : `feat/${featureName}`
448
+
449
+ const masterSpec = {
450
+ name: featureNameMatch ? featureNameMatch[1].trim() : 'Feature Pipeline',
451
+ version: 2,
452
+ branch,
453
+ on_failure: 'stop',
454
+ depends_on_convoy: groupSpecPaths.map(p => relPath(p)),
455
+ }
456
+
457
+ const masterSpecPath = resolve(convoyDir, `${featureName}-pipeline.convoy.yml`)
458
+ await writeFile(masterSpecPath, stringify(masterSpec), 'utf8')
459
+
460
+ console.log(c.green(` ✓ Generated convoy chain:\n`))
461
+ for (const p of groupSpecPaths) {
462
+ console.log(` ${relPath(p)}`)
463
+ }
464
+ console.log(` ${relPath(masterSpecPath)} ${c.dim('(master)')}`)
465
+ console.log()
466
+ console.log(
467
+ ` ${c.dim('Preview:')} npx opencastle run -f ${relPath(masterSpecPath)} --dry-run\n` +
468
+ ` ${c.dim('Execute:')} npx opencastle run -f ${relPath(masterSpecPath)}\n`
204
469
  )
205
- process.exit(1)
206
- }
207
470
 
208
- console.log(c.green(` ✓ PRD is valid\n`))
471
+ try {
472
+ const shouldRun = await confirm('Run the convoy chain now?', true)
473
+ if (shouldRun) {
474
+ closePrompts()
475
+ const runModule = await import('./run.js')
476
+ const runArgs = ['-f', masterSpecPath]
477
+ if (opts.adapter) runArgs.push('-a', opts.adapter)
478
+ if (opts.verbose) runArgs.push('--verbose')
479
+ await runModule.default({ args: runArgs, pkgRoot })
480
+ }
481
+ } finally {
482
+ closePrompts()
483
+ }
484
+ return
485
+ } else {
486
+ console.log(
487
+ c.cyan(` ℹ`) + ` Complexity: ${complexity.complexity} | Strategy: single\n`
488
+ )
489
+ }
209
490
  }
210
491
 
211
- // ── Step 3: Generate convoy spec ──────────────────────────────────────────
212
- const genStep = opts.skipValidation ? 2 : 3
492
+ // ── Step 4: Generate convoy spec ──────────────────────────────────────────
493
+ const genStep = opts.skipValidation ? 2 : 4
213
494
  console.log(stepLabel(genStep, totalSteps, 'Generating convoy spec…'))
214
495
 
215
496
  let specPath: string
@@ -229,12 +510,12 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
229
510
  console.log(c.green(` ✓ Convoy spec written to ${relPath(specPath)}\n`))
230
511
 
231
512
  if (opts.skipValidation) {
232
- printFinalSummary(prdPath, specPath)
513
+ await printFinalSummary(prdPath, specPath, opts, pkgRoot)
233
514
  return
234
515
  }
235
516
 
236
- // ── Step 4: Validate convoy spec ──────────────────────────────────────────
237
- console.log(stepLabel(4, totalSteps, 'Validating convoy spec…'))
517
+ // ── Step 5: Validate convoy spec ──────────────────────────────────────────
518
+ console.log(stepLabel(5, totalSteps, 'Validating convoy spec…'))
238
519
 
239
520
  const specContent = await readFile(specPath, 'utf8')
240
521
  let validationErrors: string
@@ -248,13 +529,13 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
248
529
  goalText: specContent,
249
530
  })
250
531
  } catch (err) {
251
- console.error(`\n ✗ Step 4 failed: ${err instanceof Error ? err.message : String(err)}`)
532
+ console.error(`\n ✗ Step 5 failed: ${err instanceof Error ? err.message : String(err)}`)
252
533
  process.exit(1)
253
534
  }
254
535
 
255
536
  if (result.isValid) {
256
537
  console.log(c.green(` ✓ Convoy spec is valid\n`))
257
- printFinalSummary(prdPath, specPath)
538
+ await printFinalSummary(prdPath, specPath, opts, pkgRoot)
258
539
  return
259
540
  }
260
541
 
@@ -264,13 +545,13 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
264
545
  console.log()
265
546
  }
266
547
 
267
- // ── Step 5: Fix convoy spec (up to 2 retries) ─────────────────────────────
548
+ // ── Step 6: Fix convoy spec (up to 2 retries) ─────────────────────────────
268
549
  const MAX_FIX_RETRIES = 2
269
550
  let fixedSpecContent = specContent
270
551
 
271
552
  for (let attempt = 1; attempt <= MAX_FIX_RETRIES; attempt++) {
272
553
  const label = `Fix attempt ${attempt}/${MAX_FIX_RETRIES}…`
273
- console.log(stepLabel(5, totalSteps, label))
554
+ console.log(stepLabel(6, totalSteps, label))
274
555
 
275
556
  let fixResult
276
557
  try {
@@ -282,7 +563,7 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
282
563
  outputPath: specPath, // overwrite in place
283
564
  })
284
565
  } catch (err) {
285
- console.error(`\n ✗ Step 5 (attempt ${attempt}) failed: ${err instanceof Error ? err.message : String(err)}`)
566
+ console.error(`\n ✗ Step 6 (attempt ${attempt}) failed: ${err instanceof Error ? err.message : String(err)}`)
286
567
  process.exit(1)
287
568
  }
288
569
 
@@ -305,7 +586,7 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
305
586
 
306
587
  if (revalidation.isValid) {
307
588
  console.log(c.green(` ✓ Spec fixed and validated\n`))
308
- printFinalSummary(prdPath, specPath)
589
+ await printFinalSummary(prdPath, specPath, opts, pkgRoot)
309
590
  return
310
591
  }
311
592
 
@@ -330,7 +611,12 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
330
611
  process.exit(1)
331
612
  }
332
613
 
333
- function printFinalSummary(prdPath: string, specPath: string): void {
614
+ async function printFinalSummary(
615
+ prdPath: string,
616
+ specPath: string,
617
+ opts: PipelineOptions,
618
+ pkgRoot: string,
619
+ ): Promise<void> {
334
620
  const prd = relPath(prdPath)
335
621
  const spec = relPath(specPath)
336
622
  console.log(c.bold(c.green(' Pipeline complete!\n')))
@@ -340,4 +626,18 @@ function printFinalSummary(prdPath: string, specPath: string): void {
340
626
  ` ${c.dim('Preview:')} npx opencastle run -f ${spec} --dry-run\n` +
341
627
  ` ${c.dim('Execute:')} npx opencastle run -f ${spec}\n`
342
628
  )
629
+
630
+ try {
631
+ const shouldRun = await confirm('Run the convoy now?', true)
632
+ if (shouldRun) {
633
+ closePrompts()
634
+ const runModule = await import('./run.js')
635
+ const runArgs = ['-f', specPath]
636
+ if (opts.adapter) runArgs.push('-a', opts.adapter)
637
+ if (opts.verbose) runArgs.push('--verbose')
638
+ await runModule.default({ args: runArgs, pkgRoot })
639
+ }
640
+ } finally {
641
+ closePrompts()
642
+ }
343
643
  }
package/src/cli/run.ts CHANGED
@@ -790,7 +790,7 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
790
790
  }
791
791
  printPipelineResult(pipelineResult)
792
792
  if (pipelineDashboardResult) {
793
- console.log(`\n ${c.dim('Results saved to .opencastle/logs/convoys.ndjson')}`)
793
+ console.log(`\n ${c.dim('Results saved to .opencastle/convoy.db')}`)
794
794
  console.log(` ${c.dim('View again:')} opencastle dashboard`)
795
795
  pipelineDashboardResult.server.close()
796
796
  }
@@ -883,7 +883,7 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
883
883
  }
884
884
  printConvoyResult(result)
885
885
  if (dashboardResult) {
886
- console.log(`\n ${c.dim('Results saved to .opencastle/logs/convoys.ndjson')}`)
886
+ console.log(`\n ${c.dim('Results saved to .opencastle/convoy.db')}`)
887
887
  console.log(` ${c.dim('View again:')} opencastle dashboard`)
888
888
  dashboardResult.server.close()
889
889
  }
package/src/cli/update.ts CHANGED
@@ -11,10 +11,28 @@ import { rebuildMcpConfig } from './mcp.js'
11
11
  import { detectRepoInfo, mergeStackIntoRepoInfo, buildDetectedToolsSet } from './detect.js'
12
12
  import type { CliContext, IdeChoice, TechTool, TeamTool, StackConfig } from './types.js'
13
13
 
14
+ const UPDATE_HELP = `
15
+ opencastle update [options]
16
+
17
+ Update framework files to the latest version while preserving
18
+ your customizations in the .opencastle/ directory.
19
+
20
+ Options:
21
+ --dry-run Preview what would be changed without writing files
22
+ --force Overwrite customized files (default: skip)
23
+ --reconfigure Re-run IDE selection and reconfigure adapters
24
+ --help, -h Show this help
25
+ `
26
+
14
27
  export default async function update({
15
28
  pkgRoot,
16
29
  args,
17
30
  }: CliContext): Promise<void> {
31
+ if (args.includes('--help') || args.includes('-h')) {
32
+ console.log(UPDATE_HELP)
33
+ return
34
+ }
35
+
18
36
  const projectRoot = process.cwd()
19
37
 
20
38
  await migrateCustomizationsDir(projectRoot)
package/src/cli/watch.ts CHANGED
@@ -134,9 +134,7 @@ export async function watchLoop(options: WatchLoopOptions): Promise<void> {
134
134
  const dbPath = resolve(process.cwd(), '.opencastle', 'convoy.db')
135
135
  mkdirSync(dirname(dbPath), { recursive: true })
136
136
  const evtStore = createConvoyStore(dbPath)
137
- const ndjsonPath = resolve(process.cwd(), '.opencastle', 'logs', 'convoy-events.ndjson')
138
- mkdirSync(dirname(ndjsonPath), { recursive: true })
139
- const watchEvents = createEventEmitter(evtStore, { ndjsonPath })
137
+ const watchEvents = createEventEmitter(evtStore)
140
138
 
141
139
  const triggerTypes = watchConfig.triggers.map(t => t.type).join(',')
142
140
  watchEvents.emit('watch_started', { trigger_type: triggerTypes, pid: process.pid })
@@ -0,0 +1 @@
1
+ :root{--bg-primary: #0a0a0f;--bg-secondary: #111118;--bg-tertiary: #1a1a24;--bg-card: rgba(255, 255, 255, .03);--bg-card-hover: rgba(255, 255, 255, .06);--text-primary: #f0f0f5;--text-secondary: #8a8a9a;--text-tertiary: #7a7a8e;--text-accent: #a78bfa;--gradient-accent: linear-gradient(135deg, #a78bfa 0%, #6366f1 50%, #3b82f6 100%);--gradient-glow: radial-gradient(ellipse 800px 400px at 50% 0%, rgba(99, 102, 241, .12) 0%, transparent 70%);--border-color: rgba(255, 255, 255, .06);--border-accent: rgba(167, 139, 250, .3);--color-success: #22c55e;--color-partial: #f59e0b;--color-failed: #ef4444;--color-redirected: #64748b;--color-premium: #f59e0b;--color-standard: #a78bfa;--color-utility: #3b82f6;--color-economy: #64748b;--accent-blue: #3b82f6;--accent-purple: #a78bfa;--accent-indigo: #6366f1;--max-width: 1280px;--transition-fast: .15s cubic-bezier(.4, 0, .2, 1);--transition-base: .3s cubic-bezier(.4, 0, .2, 1)}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html{font-size:16px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Inter,Roboto,Helvetica,Arial,sans-serif;background-color:var(--bg-primary);color:var(--text-primary);line-height:1.6;overflow-x:hidden;min-height:100vh}.dash-header{position:sticky;top:0;z-index:50;background:#0a0a0fd9;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border-bottom:1px solid var(--border-color)}.dash-header__inner{max-width:var(--max-width);margin:0 auto;padding:0 24px;height:56px;display:flex;align-items:center;justify-content:space-between}.dash-header__brand{display:flex;align-items:center;gap:10px}.dash-header__icon{width:32px;height:32px;border-radius:8px;object-fit:contain}.dash-header__title{font-size:1rem;font-weight:600;color:var(--text-primary)}.dash-layout{display:flex;max-width:var(--max-width);margin:0 auto;position:relative}.dash-sidebar{position:sticky;top:56px;height:calc(100vh - 56px);width:180px;flex-shrink:0;padding:24px 0 24px 24px;overflow-y:auto;display:none}@media(min-width:1024px){.dash-sidebar{display:block}}.dash-sidebar__list{list-style:none;display:flex;flex-direction:column;gap:2px}.dash-sidebar__link{display:block;padding:8px 16px;font-size:.8125rem;font-weight:500;color:var(--text-tertiary);text-decoration:none;border-radius:8px;transition:color var(--transition-fast),background var(--transition-fast)}.dash-sidebar__link:hover{color:var(--text-secondary);background:#ffffff0a}.dash-sidebar__link--active{color:var(--text-accent);background:#a78bfa14;font-weight:600}.dash-main{flex:1;min-width:0;max-width:var(--max-width);margin:0 auto;padding:24px;display:flex;flex-direction:column;gap:20px;position:relative}.dash-main:before{content:"";position:fixed;top:0;left:50%;transform:translate(-50%);width:100%;height:600px;background:var(--gradient-glow);pointer-events:none;z-index:0}.dash-main>*{position:relative;z-index:1}[data-nav-section]{scroll-margin-top:72px}.kpi-row{display:grid;grid-template-columns:1fr;gap:12px}@media(min-width:480px){.kpi-row{grid-template-columns:repeat(2,1fr)}}@media(min-width:960px){.kpi-row{grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}}.kpi-card{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px;padding:20px 24px;display:flex;flex-direction:column;gap:4px;transition:border-color var(--transition-fast)}.kpi-card:hover{border-color:#ffffff1a}.kpi-card__label{font-size:.75rem;font-weight:500;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.05em}.kpi-card__value{font-size:2rem;font-weight:700;color:var(--text-primary);line-height:1.2;letter-spacing:-.02em}.kpi-card__sub{font-size:.75rem;color:var(--text-secondary);display:flex;align-items:center;gap:4px}.kpi-trend{font-weight:600}.kpi-trend--up{color:var(--color-success)}.kpi-trend--down{color:var(--color-failed)}.kpi-trend--neutral{color:var(--text-tertiary)}.chart-card{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px;transition:border-color var(--transition-fast)}.chart-card:hover{border-color:#ffffff1a}.chart-card__header{padding:20px 24px 8px}.chart-card__title{font-size:.9375rem;font-weight:600;color:var(--text-primary)}.chart-card__desc{font-size:.75rem;color:var(--text-tertiary);margin-top:2px}.chart-card__body{padding:16px 24px 24px;min-height:120px}.chart-card__body--table{padding:0}.charts-row{display:grid;grid-template-columns:1fr;gap:20px}@media(min-width:768px){.charts-row{grid-template-columns:repeat(2,1fr)}}.bar-row{display:flex;align-items:center;gap:12px;padding:6px 0}.bar-row+.bar-row{border-top:1px solid rgba(255,255,255,.03)}.bar-label{font-size:.8125rem;color:var(--text-secondary);width:130px;flex-shrink:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.bar-track{flex:1;height:24px;background:var(--bg-tertiary);border-radius:6px;display:flex;overflow:hidden}.bar-segment{height:100%;transition:width .8s cubic-bezier(.4,0,.2,1);min-width:0}.bar--success{background:var(--color-success)}.bar--partial{background:var(--color-partial)}.bar--failed{background:var(--color-failed)}.bar--premium{background:var(--color-premium)}.bar--standard{background:var(--color-standard)}.bar--utility{background:var(--color-utility)}.bar--economy{background:var(--color-economy)}.bar--accent{background:var(--accent-blue)}.bar-value{font-size:.8125rem;font-weight:600;color:var(--text-primary);width:36px;text-align:right;flex-shrink:0;font-variant-numeric:tabular-nums}.donut-container{display:flex;align-items:center;justify-content:center;gap:32px;flex-wrap:wrap}.donut-wrap{position:relative;width:180px;height:180px;flex-shrink:0}.donut-svg{width:100%;height:100%}.donut-svg circle{transition:stroke-dasharray .8s cubic-bezier(.4,0,.2,1),stroke-dashoffset .8s cubic-bezier(.4,0,.2,1)}.donut-center{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center}.donut-total{display:block;font-size:1.5rem;font-weight:700;color:var(--text-primary);line-height:1}.donut-total-label{display:block;font-size:.6875rem;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.08em;margin-top:2px}.donut-legend{display:flex;flex-direction:column;gap:10px}.legend-item{display:flex;align-items:center;gap:8px;font-size:.8125rem}.legend-dot{width:10px;height:10px;border-radius:3px;flex-shrink:0}.legend-name{color:var(--text-secondary);text-transform:capitalize}.legend-count{color:var(--text-tertiary);font-variant-numeric:tabular-nums;margin-left:auto}.timeline-svg{width:100%;height:auto;display:block}.timeline-svg text{font-family:inherit}.timeline-legend{display:flex;gap:16px;justify-content:center;margin-top:12px}.timeline-legend__item{display:flex;align-items:center;gap:6px;font-size:.75rem;color:var(--text-tertiary)}.timeline-legend__dot{width:8px;height:8px;border-radius:2px}.pipeline{display:flex;align-items:stretch;gap:0;overflow-x:auto;padding:8px 0}.pipeline-stage{flex:1;min-width:140px;display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px;position:relative}.pipeline-stage:not(:last-child):after{content:"";position:absolute;right:-1px;top:50%;transform:translateY(-50%);width:2px;height:40%;background:var(--border-color)}.pipeline-stage__icon{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1rem}.pipeline-stage__icon--pending{background:#64748b26;color:#94a3b8;border:1px solid rgba(100,116,139,.2)}.pipeline-stage__icon--active{background:#3b82f626;color:#60a5fa;border:1px solid rgba(59,130,246,.3);animation:pulse-glow 2s ease-in-out infinite}.pipeline-stage__icon--review{background:#f59e0b26;color:#fbbf24;border:1px solid rgba(245,158,11,.3)}.pipeline-stage__icon--done{background:#22c55e26;color:#4ade80;border:1px solid rgba(34,197,94,.3)}@keyframes pulse-glow{0%,to{box-shadow:0 0 #3b82f633}50%{box-shadow:0 0 12px 4px #3b82f626}}.pipeline-stage__count{font-size:1.5rem;font-weight:700;color:var(--text-primary);line-height:1}.pipeline-stage__label{font-size:.75rem;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.04em;font-weight:500}.pipeline-arrow{display:flex;align-items:center;color:var(--text-tertiary);font-size:1.25rem;padding:0 4px;flex-shrink:0}.exec-log{display:flex;flex-direction:column}.exec-step{display:flex;gap:16px;padding:14px 0;position:relative}.exec-step+.exec-step{border-top:1px solid rgba(255,255,255,.03)}.exec-step__indicator{display:flex;flex-direction:column;align-items:center;flex-shrink:0;width:32px}.exec-step__dot{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.6875rem;font-weight:700;flex-shrink:0}.exec-step__dot--success{background:#22c55e26;color:var(--color-success);border:1.5px solid rgba(34,197,94,.3)}.exec-step__dot--partial{background:#f59e0b26;color:var(--color-partial);border:1.5px solid rgba(245,158,11,.3)}.exec-step__dot--failed{background:#ef444426;color:var(--color-failed);border:1.5px solid rgba(239,68,68,.3)}.exec-step__line{flex:1;width:1.5px;background:var(--border-color);margin-top:4px}.exec-step__content{flex:1;min-width:0}.exec-step__header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.exec-step__agent{font-size:.875rem;font-weight:600;color:var(--text-primary)}.exec-step__badge{display:inline-flex;align-items:center;padding:2px 8px;font-size:.6875rem;font-weight:600;border-radius:100px;text-transform:capitalize}.exec-step__badge--success{background:#22c55e1f;color:var(--color-success);border:1px solid rgba(34,197,94,.2)}.exec-step__badge--partial{background:#f59e0b1f;color:var(--color-partial);border:1px solid rgba(245,158,11,.2)}.exec-step__badge--failed{background:#ef44441f;color:var(--color-failed);border:1px solid rgba(239,68,68,.2)}.exec-step__task{font-size:.8125rem;color:var(--text-secondary);margin-top:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.exec-step__meta{display:flex;gap:16px;margin-top:6px;font-size:.6875rem;color:var(--text-tertiary)}.exec-step__meta-item{display:flex;align-items:center;gap:4px}.panel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px}.panel-item{background:var(--bg-tertiary);border-radius:8px;padding:16px;display:flex;flex-direction:column;gap:8px;border:1px solid transparent;transition:border-color var(--transition-fast)}.panel-item:hover{border-color:var(--border-color)}.panel-item__header{display:flex;align-items:center;justify-content:space-between}.panel-item__key{font-size:.8125rem;font-weight:600;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.panel-item__verdict{font-size:.6875rem;font-weight:700;padding:2px 8px;border-radius:4px;text-transform:uppercase;letter-spacing:.04em}.panel-item__verdict--pass{background:#22c55e26;color:var(--color-success)}.panel-item__verdict--block{background:#ef444426;color:var(--color-failed)}.panel-item__votes{display:flex;gap:4px}.panel-item__vote{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.625rem;font-weight:700}.panel-item__vote--pass{background:#22c55e1f;color:var(--color-success);border:1px solid rgba(34,197,94,.2)}.panel-item__vote--block{background:#ef44441f;color:var(--color-failed);border:1px solid rgba(239,68,68,.2)}.panel-item__fixes{font-size:.6875rem;color:var(--text-tertiary)}.panel-item__meta{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px;padding-top:8px;border-top:1px solid var(--border-color)}.panel-item__meta-item{font-size:.625rem;color:var(--text-tertiary);white-space:nowrap}.sessions-table{width:100%;border-collapse:collapse;font-size:.8125rem}.sessions-table thead{position:sticky;top:0}.sessions-table th{padding:12px 16px;font-size:.6875rem;font-weight:600;color:var(--text-tertiary);text-align:left;text-transform:uppercase;letter-spacing:.06em;background:var(--bg-tertiary);border-bottom:1px solid var(--border-color)}.sessions-table th:last-child,.sessions-table td:last-child{text-align:right}.sessions-table th:nth-child(5),.sessions-table td:nth-child(5){text-align:right}.sessions-table td{padding:10px 16px;color:var(--text-secondary);border-bottom:1px solid rgba(255,255,255,.03);white-space:nowrap}.sessions-table tr:hover td{background:#ffffff05}.sessions-table .td-agent{font-weight:500;color:var(--text-primary)}.sessions-table .td-task{max-width:260px;overflow:hidden;text-overflow:ellipsis}.outcome-badge{display:inline-flex;align-items:center;padding:3px 10px;font-size:.6875rem;font-weight:600;border-radius:100px;text-transform:capitalize}.outcome-badge--success{background:#22c55e1f;color:var(--color-success);border:1px solid rgba(34,197,94,.2)}.outcome-badge--partial{background:#f59e0b1f;color:var(--color-partial);border:1px solid rgba(245,158,11,.2)}.outcome-badge--failed{background:#ef44441f;color:var(--color-failed);border:1px solid rgba(239,68,68,.2)}.td-num{font-variant-numeric:tabular-nums;text-align:right}.td-issue{font-size:.75rem;color:var(--text-accent);font-weight:500;font-variant-numeric:tabular-nums}.loading-skeleton{display:flex;align-items:center;justify-content:center;min-height:200px;color:var(--text-tertiary);font-size:.8125rem}.loading-skeleton:after{content:"Loading data…";animation:fade-pulse 1.5s ease-in-out infinite}@keyframes fade-pulse{0%,to{opacity:.4}50%{opacity:1}}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 24px;text-align:center;gap:12px}.empty-state__icon{font-size:2rem;opacity:.4}.empty-state__text{font-size:.875rem;color:var(--text-tertiary);max-width:320px}.empty-state--enhanced{padding:56px 32px;gap:16px;border:1px dashed rgba(167,139,250,.15);border-radius:12px;background:radial-gradient(ellipse 300px 200px at 50% 30%,rgba(99,102,241,.04) 0%,transparent 70%),var(--bg-tertiary);position:relative;overflow:hidden}.empty-state--enhanced:before{content:"";position:absolute;inset:0;background:repeating-linear-gradient(0deg,transparent,transparent 23px,rgba(255,255,255,.015) 23px,rgba(255,255,255,.015) 24px);pointer-events:none}.empty-state__icon-wrap{width:64px;height:64px;display:flex;align-items:center;justify-content:center;border-radius:16px;background:#a78bfa0f;border:1px solid rgba(167,139,250,.12);color:var(--text-accent);animation:empty-breathe 4s ease-in-out infinite}@keyframes empty-breathe{0%,to{box-shadow:0 0 #a78bfa14;transform:scale(1)}50%{box-shadow:0 0 20px 4px #a78bfa0f;transform:scale(1.03)}}.empty-state__title{font-size:.9375rem;font-weight:600;color:var(--text-secondary);letter-spacing:-.01em}.empty-state__desc{font-size:.8125rem;color:var(--text-tertiary);max-width:380px;line-height:1.55}.kpi-card__hint{color:var(--text-tertiary);font-style:italic;font-size:.6875rem}.kpi-row--empty .kpi-card{border-style:dashed;border-color:#ffffff0a}.kpi-row--empty .kpi-card__value{color:var(--text-tertiary);opacity:.5}.welcome-banner{position:relative;background:var(--bg-secondary);border:1px solid transparent;border-radius:16px;padding:48px 40px;overflow:hidden;z-index:1}.welcome-banner:before{content:"";position:absolute;inset:-1px;border-radius:16px;padding:1px;background:linear-gradient(135deg,#a78bfa4d,#6366f126,#3b82f61a 60%,#a78bfa33);-webkit-mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);-webkit-mask-composite:xor;mask-composite:exclude;pointer-events:none;z-index:0}.welcome-banner__glow{position:absolute;top:-60px;left:50%;transform:translate(-50%);width:500px;height:300px;background:radial-gradient(ellipse at center,rgba(167,139,250,.08) 0%,rgba(99,102,241,.04) 40%,transparent 70%);pointer-events:none;z-index:0}.welcome-banner__content{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;text-align:center;gap:20px}.welcome-banner__icon{width:72px;height:72px;display:flex;align-items:center;justify-content:center;border-radius:20px;background:#a78bfa14;border:1px solid rgba(167,139,250,.15);color:var(--text-accent);animation:welcome-float 6s ease-in-out infinite}@keyframes welcome-float{0%,to{transform:translateY(0);box-shadow:0 8px 32px #a78bfa14}50%{transform:translateY(-6px);box-shadow:0 16px 48px #a78bfa1f}}.welcome-banner__title{font-size:1.375rem;font-weight:700;color:var(--text-primary);letter-spacing:-.02em;line-height:1.3}.welcome-banner__subtitle{font-size:.9375rem;color:var(--text-secondary);max-width:480px;line-height:1.6}.welcome-banner__steps{display:flex;gap:20px;margin-top:12px;flex-wrap:wrap;justify-content:center}.welcome-step{display:flex;align-items:flex-start;gap:12px;text-align:left;padding:16px 20px;background:#ffffff05;border:1px solid rgba(255,255,255,.05);border-radius:12px;min-width:200px;max-width:220px;transition:border-color var(--transition-fast),background var(--transition-fast)}.welcome-step:hover{border-color:#a78bfa26;background:#ffffff08}.welcome-step__num{width:28px;height:28px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:700;color:var(--text-accent);background:#a78bfa1a;border:1px solid rgba(167,139,250,.2);flex-shrink:0}.welcome-step__text{display:flex;flex-direction:column;gap:3px}.welcome-step__text strong{font-size:.8125rem;font-weight:600;color:var(--text-primary)}.welcome-step__text span{font-size:.75rem;color:var(--text-tertiary);line-height:1.4}@media(max-width:640px){.welcome-banner{padding:32px 24px}.welcome-banner__steps{flex-direction:column;align-items:center}.welcome-step{max-width:100%;width:100%}}@keyframes slide-up{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.dash-main>*{animation:slide-up .5s ease-out backwards}.dash-main>*:nth-child(1){animation-delay:0ms}.dash-main>*:nth-child(2){animation-delay:60ms}.dash-main>*:nth-child(3){animation-delay:.12s}.dash-main>*:nth-child(4){animation-delay:.18s}.dash-main>*:nth-child(5){animation-delay:.24s}.dash-main>*:nth-child(6){animation-delay:.3s}.dash-main>*:nth-child(7){animation-delay:.36s}.dash-main>*:nth-child(8){animation-delay:.42s}.dash-main>*:nth-child(9){animation-delay:.48s}.dash-main>*:nth-child(10){animation-delay:.54s}.dash-main>*:nth-child(11){animation-delay:.6s}@media(max-width:640px){.bar-label{width:90px;font-size:.75rem}.donut-container{flex-direction:column;align-items:center}.donut-wrap{width:150px;height:150px}.pipeline{gap:0}.pipeline-stage{min-width:100px;padding:12px 8px}.panel-grid{grid-template-columns:1fr}.sessions-table th:nth-child(3),.sessions-table td:nth-child(3){display:none}}.filter-bar{display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end;padding:16px 20px;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px}.filter-group{display:flex;flex-direction:column;gap:4px;min-width:0}.filter-label{font-size:.6875rem;font-weight:500;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.05em}.filter-input,.filter-select{height:34px;padding:0 10px;font-size:.8125rem;color:var(--text-primary);background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;outline:none;transition:border-color var(--transition-fast);font-family:inherit}.filter-input:focus,.filter-select:focus{border-color:var(--border-accent)}.filter-input{width:140px;color-scheme:dark}.filter-select{min-width:140px;cursor:pointer;appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%235a5a6e' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;padding-right:28px}.filter-reset{height:34px;font-size:.75rem}.dash-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;font-size:.8125rem;font-weight:500;font-family:inherit;border:none;border-radius:8px;cursor:pointer;transition:background var(--transition-fast),color var(--transition-fast)}.dash-btn--ghost{color:var(--text-secondary);background:#ffffff0f}.dash-btn--ghost:hover{color:var(--text-primary);background:#ffffff1a}.dash-header__actions{display:flex;align-items:center;gap:8px}@media(max-width:480px){.dash-header__inner{padding:0 12px}.dash-main{padding:12px;gap:12px}.kpi-card,.chart-card__header{padding:14px 16px}.chart-card__body{padding:12px 16px 16px}.filter-bar{padding:12px;gap:8px}.filter-input,.filter-select{width:100%;min-width:unset}.filter-group{flex:1 1 calc(50% - 4px)}.filter-reset{width:100%}.dash-header__title{font-size:.875rem}.exec-step__meta{flex-direction:column;gap:2px}.sessions-table th:nth-child(5),.sessions-table td:nth-child(5),.sessions-table th:nth-child(6),.sessions-table td:nth-child(6),.sessions-table th:nth-child(7),.sessions-table td:nth-child(7),.sessions-table th:nth-child(8),.sessions-table td:nth-child(8){display:none}}@media(max-width:768px){.charts-row{grid-template-columns:1fr}.pipeline{flex-wrap:wrap;gap:8px}.pipeline-arrow{display:none}.pipeline-stage{flex:1 1 calc(50% - 4px);min-width:100px}.tier-chart .donut-container,.donut-container{flex-direction:column;align-items:center}.sessions-table{font-size:.75rem}.sessions-table th,.sessions-table td{padding:8px 6px}}.convoy-overview{display:flex;flex-wrap:wrap;gap:24px;margin-bottom:20px}.convoy-stat{display:flex;flex-direction:column;gap:4px}.convoy-stat__label{font-size:.75rem;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.05em}.convoy-stat__value{font-size:.95rem;color:var(--text-primary)}.convoy-stat__value--error{color:var(--color-failed)}.convoy-progress{display:flex;align-items:center;gap:12px;margin-bottom:20px}.convoy-progress__bar{flex:1;height:8px;background:var(--bg-tertiary);border-radius:4px;overflow:hidden}.convoy-progress__fill{height:100%;background:var(--gradient-accent);border-radius:4px;transition:width var(--transition-base)}.convoy-progress__label{font-size:.8rem;color:var(--text-secondary);white-space:nowrap}.convoy-tasks{margin-top:8px}.convoy-chain{display:flex;align-items:stretch;gap:0;overflow-x:auto;padding:1rem 0 1.5rem;scrollbar-width:thin;scrollbar-color:var(--border-color) transparent}.convoy-chain::-webkit-scrollbar{height:4px}.convoy-chain::-webkit-scrollbar-track{background:transparent}.convoy-chain::-webkit-scrollbar-thumb{background:var(--border-color);border-radius:2px}.convoy-chain__connector{display:flex;align-items:center;padding:0 .5rem;color:var(--text-tertiary);font-size:1.1rem;flex-shrink:0}.convoy-chain__node{display:flex;flex-direction:column;align-items:center;gap:6px;padding:12px 16px;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:10px;min-width:140px;cursor:pointer;transition:background var(--transition-fast),border-color var(--transition-fast),transform var(--transition-fast),box-shadow var(--transition-fast);flex-shrink:0}.convoy-chain__node:hover{background:var(--bg-card-hover);transform:translateY(-2px);box-shadow:0 4px 12px #0000004d}.convoy-chain__node-name{font-size:.8rem;font-weight:600;color:var(--text-primary);text-align:center;word-break:break-word;max-width:120px}.convoy-chain__node-meta{font-size:.72rem;color:var(--text-tertiary);text-align:center}.convoy-chain__node--active{border-color:var(--accent-purple);box-shadow:0 0 0 1px var(--accent-purple),0 0 12px #a78bfa33;animation:convoy-pulse 2s ease-in-out infinite}.convoy-chain__node--done{border-color:#22c55e4d}.convoy-chain__node--failed{border-color:#ef44444d}.convoy-chain__node--pending{opacity:.6}@keyframes convoy-pulse{0%,to{box-shadow:0 0 0 1px var(--accent-purple),0 0 8px #a78bfa26}50%{box-shadow:0 0 0 1px var(--accent-purple),0 0 18px #a78bfa59}}@media(max-width:768px){.convoy-chain{flex-wrap:wrap;gap:8px}.convoy-chain__connector{display:none}.convoy-chain__node{flex:1 1 calc(50% - 4px);min-width:120px}}.convoy-selector{display:flex;align-items:center;gap:.5rem}.convoy-selector__label{font-size:.75rem;text-transform:uppercase;letter-spacing:.05em;color:var(--text-secondary)}.convoy-selector__select{appearance:none;background:var(--bg-tertiary);border:1px solid rgba(255,255,255,.08);border-radius:6px;color:var(--text-primary);font-size:.8125rem;padding:.375rem 2rem .375rem .75rem;cursor:pointer;max-width:320px;background-image:url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%238a8a9a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right .75rem center;transition:border-color .15s}.convoy-selector__select:hover{border-color:#ffffff26}.convoy-selector__select:focus{outline:2px solid var(--accent-purple);outline-offset:2px}.overall-stats{margin-bottom:0;padding:1.25rem;background:var(--bg-secondary);border-radius:12px;border:1px solid rgba(255,255,255,.06)}.overall-stats__header{display:flex;align-items:center;gap:.5rem;margin-bottom:1rem}.overall-stats__title{font-size:1rem;font-weight:600;color:var(--text-primary);margin:0}.overall-stats__grid{display:grid;grid-template-columns:repeat(6,1fr);gap:.75rem}.overall-kpi{display:flex;flex-direction:column;gap:.25rem;padding:.75rem;background:var(--bg-tertiary);border-radius:8px;border:1px solid rgba(255,255,255,.04);transition:border-color .15s}.overall-kpi:hover{border-color:#ffffff1a}.overall-kpi__label{font-size:.6875rem;text-transform:uppercase;letter-spacing:.05em;color:var(--text-secondary);display:flex;align-items:center;gap:.25rem}.overall-kpi__value{font-size:1.375rem;font-weight:700;color:var(--text-primary);font-variant-numeric:tabular-nums}.convoy-detail-header{padding:1.25rem;background:var(--bg-secondary);border-radius:12px;border:1px solid rgba(255,255,255,.06)}.convoy-detail-header__top{display:flex;align-items:center;gap:.75rem;margin-bottom:.75rem}.convoy-detail-header__name{font-size:1.25rem;font-weight:700;color:var(--text-primary);margin:0}.convoy-detail-header__meta{display:flex;flex-wrap:wrap;gap:1rem}.convoy-meta__item{font-size:.8125rem;color:var(--text-secondary)}.status-badge{display:inline-flex;align-items:center;padding:.125rem .625rem;border-radius:999px;font-size:.6875rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em}.status-badge--done{background:#22c55e26;color:var(--color-success)}.status-badge--running{background:#3b82f626;color:var(--accent-blue)}.status-badge--failed{background:#ef444426;color:var(--color-failed)}.status-badge--gate-failed,.status-badge--gate_failed{background:#f59e0b26;color:var(--color-partial)}.tooltip-trigger{position:relative;cursor:help;font-size:.75rem;opacity:.5;transition:opacity .15s}.tooltip-trigger:hover{opacity:1}.tooltip-trigger:hover:after{content:attr(data-tooltip);position:absolute;bottom:100%;left:50%;transform:translate(-50%);padding:.5rem .875rem;background:var(--bg-primary);border:1px solid rgba(255,255,255,.12);border-radius:6px;font-size:.8125rem;color:var(--text-primary);max-width:420px;min-width:180px;white-space:normal;text-align:left;line-height:1.5;word-break:break-word;z-index:100;pointer-events:none;box-shadow:0 4px 12px #0006}.tooltip-trigger:focus{opacity:1;outline:2px solid var(--accent-blue);outline-offset:2px;border-radius:2px}.tooltip-trigger:focus:after,.tooltip-trigger:focus-visible:after{content:attr(data-tooltip);position:absolute;bottom:100%;left:50%;transform:translate(-50%);padding:.5rem .875rem;background:var(--bg-primary);border:1px solid rgba(255,255,255,.12);border-radius:6px;font-size:.8125rem;color:var(--text-primary);max-width:420px;min-width:180px;white-space:normal;text-align:left;line-height:1.5;word-break:break-word;z-index:100;pointer-events:none;box-shadow:0 4px 12px #0006}.status-badge--pending{background:#64748b26;color:#94a3b8}.status-badge--assigned{background:#3b82f61a;color:#60a5fa}.status-badge--timed-out{background:#ef44441f;color:#f87171}.status-badge--review-blocked{background:#f59e0b1f;color:#fbbf24}.status-badge--skipped{background:#64748b1a;color:#64748b}.status-badge--hook-failed{background:#ef44441a;color:#f87171}.status-badge--disputed{background:#a78bfa26;color:var(--accent-purple)}.status-badge--wait-for-input{background:#f59e0b1a;color:var(--color-partial)}.task-summary-cards{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:20px}.task-summary-card{flex:1 1 140px;display:flex;flex-direction:column;gap:8px;padding:14px 16px;background:var(--bg-card);border:1px solid var(--border-color);border-radius:10px;transition:border-color .15s}.task-summary-card:hover{border-color:#ffffff1f}.task-summary-card__label{font-size:.6875rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-tertiary)}.task-summary-card__value{font-size:1.75rem;font-weight:700;line-height:1;color:var(--text-primary)}.task-summary-card--done{border-left:3px solid var(--color-success)}.task-summary-card--running{border-left:3px solid var(--accent-blue)}.task-summary-card--errors{border-left:3px solid var(--color-failed)}.task-summary-card--waiting{border-left:3px solid #94a3b8}.task-summary-card--input{border-left:3px solid var(--color-partial)}.task-table-wrap{overflow-x:auto}.task-table .td-num{text-align:right}.sortable-th{cursor:pointer;user-select:none}.sortable-th:hover{color:var(--text-secondary)}.sortable-th--active{color:var(--text-primary)}.sort-indicator{margin-left:4px;font-size:.5625rem;opacity:.5}.sortable-th--active .sort-indicator{opacity:1;color:var(--accent-blue)}.phase-breakdown{display:flex;flex-direction:column;gap:8px;margin-bottom:20px}.phase-breakdown__row{display:flex;align-items:center;gap:12px}.phase-breakdown__label{font-size:.75rem;font-weight:600;color:var(--text-tertiary);min-width:60px}.phase-breakdown__bar{flex:1;height:10px;background:#ffffff0a;border-radius:5px;overflow:hidden;display:flex}.phase-breakdown__seg{height:100%;transition:width .3s ease}.phase-breakdown__seg--done{background:var(--color-success)}.phase-breakdown__seg--running{background:var(--accent-blue)}.phase-breakdown__seg--waiting{background:#475569}.phase-breakdown__seg--failed{background:var(--color-failed)}.phase-breakdown__count{font-size:.6875rem;color:var(--text-tertiary);min-width:52px;text-align:right}@media(max-width:960px){.overall-stats__grid{grid-template-columns:repeat(3,1fr)}}@media(max-width:640px){.overall-stats__grid{grid-template-columns:repeat(2,1fr)}.convoy-detail-header__name{font-size:1rem}.convoy-selector__select{max-width:200px}}@media(max-width:480px){.overall-stats__grid{grid-template-columns:1fr}}.reliability-empty{font-size:.875rem;color:var(--text-tertiary);padding:12px 0;margin:0}.secret-leak-banner{display:flex;align-items:flex-start;gap:12px;padding:14px 16px;background:#f59e0b1a;border:1px solid rgba(245,158,11,.3);border-radius:8px;margin-top:16px}.secret-leak-banner__icon{font-size:1.25rem;flex-shrink:0;line-height:1.4}.secret-leak-banner__text{display:flex;flex-direction:column;gap:4px}.secret-leak-banner__text strong{font-size:.875rem;font-weight:600;color:var(--color-partial)}.secret-leak-banner__text span{font-size:.8125rem;color:var(--text-secondary)}.artifact-type-badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:.75rem;font-weight:600;color:#fff;text-transform:uppercase;letter-spacing:.03em}.timeline-filters{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px}.timeline-filter-chip{display:inline-flex;align-items:center;padding:6px 14px;border-radius:20px;border:1px solid var(--border);background:transparent;color:var(--text-secondary);font-size:.8125rem;font-weight:500;cursor:pointer;transition:all .15s ease}.timeline-filter-chip:hover{border-color:var(--accent);color:var(--text-primary)}.timeline-filter-chip--active{background:var(--accent);border-color:var(--accent);color:#fff}.event-timeline-row{padding:12px 16px;border-bottom:1px solid var(--border);cursor:pointer;transition:background .15s ease}.event-timeline-row:hover{background:#a78bfa0d}.event-timeline-row--expanded{background:#a78bfa14}.event-timeline-row__main{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.event-timeline-ts{font-size:.8125rem;color:var(--text-secondary);min-width:140px;font-variant-numeric:tabular-nums}.event-type-badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:.6875rem;font-weight:600;color:#fff;text-transform:uppercase;letter-spacing:.03em}.event-timeline-context{font-size:.8125rem;color:var(--text-tertiary, #6b7280);font-family:var(--font-mono, "SF Mono", "Fira Code", monospace)}.event-timeline-detail{margin-top:8px;padding:12px;background:var(--bg-card, #111118);border-radius:6px;border:1px solid var(--border)}.event-timeline-json{font-size:.75rem;color:var(--text-secondary);white-space:pre-wrap;word-break:break-all;margin:0;font-family:var(--font-mono, "SF Mono", "Fira Code", monospace);max-height:300px;overflow-y:auto}@media(max-width:640px){.event-timeline-ts{min-width:auto;font-size:.75rem}.event-timeline-row__main{gap:8px}.timeline-filter-chip{padding:4px 10px;font-size:.75rem}}.dash-btn:focus-visible,.convoy-selector__select:focus-visible,.filter-select:focus-visible,.filter-input:focus-visible,.dash-sidebar__link:focus-visible,.timeline-filter-chip:focus-visible{outline:2px solid var(--accent-blue);outline-offset:2px}.convoy-status-explanation{font-size:.8125rem;color:var(--text-secondary);margin-top:.25rem;margin-bottom:.5rem;font-style:italic}.view-home,.view-convoy-detail{display:flex;flex-direction:column;gap:20px}[data-view-hidden]{display:none!important}.breadcrumbs{display:flex;align-items:center;gap:6px;font-size:.8125rem;color:var(--text-tertiary);flex-wrap:wrap;padding:0;margin-bottom:-8px}.breadcrumbs__link{color:var(--text-secondary);text-decoration:none;transition:color var(--transition-fast);border-bottom:1px solid transparent}.breadcrumbs__link:hover{color:var(--text-accent);border-bottom-color:#a78bfa66}.breadcrumbs__link:focus-visible{outline:2px solid var(--accent-blue);outline-offset:2px;border-radius:2px}.breadcrumbs__separator{color:var(--text-tertiary);opacity:.5;user-select:none;font-size:.75rem}.breadcrumbs__current{color:var(--text-accent);font-weight:500;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.convoy-list-section{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px;transition:border-color var(--transition-fast)}.convoy-list-section:hover{border-color:#ffffff1a}.convoy-list-section__header{padding:20px 24px 12px;border-bottom:1px solid var(--border-color)}.convoy-list-section__header h2,.convoy-list-section__header .convoy-list-section__title{font-size:.9375rem;font-weight:600;color:var(--text-primary);margin:0}.convoy-list-section__desc{font-size:.75rem;color:var(--text-tertiary);margin-top:2px}.convoy-list-filters{display:flex;flex-wrap:wrap;gap:10px;align-items:flex-end;padding:14px 24px;background:#ffffff03;border-bottom:1px solid var(--border-color)}.convoy-list-filters__group{display:flex;flex-direction:column;gap:4px}.convoy-list-filters__group label{font-size:.6875rem;font-weight:500;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.05em}.convoy-list-filters__input,.convoy-list-filters__select,.convoy-list-filters__date{height:34px;padding:0 10px;font-size:.8125rem;color:var(--text-primary);background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;outline:none;font-family:inherit;transition:border-color var(--transition-fast);color-scheme:dark}.convoy-list-filters__input:focus,.convoy-list-filters__select:focus,.convoy-list-filters__date:focus{border-color:var(--border-accent)}.convoy-list-filters__input{width:180px}.convoy-list-filters__date{width:150px}.convoy-list-filters__select{min-width:140px;cursor:pointer;appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%235a5a6e' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;padding-right:28px}.convoy-list-filters__reset{height:34px;padding:0 14px;font-size:.75rem;font-weight:500;font-family:inherit;color:var(--text-secondary);background:#ffffff0f;border:none;border-radius:8px;cursor:pointer;transition:background var(--transition-fast),color var(--transition-fast);align-self:flex-end;white-space:nowrap}.convoy-list-filters__reset:hover{background:#ffffff1a;color:var(--text-primary)}.convoy-list-filters__reset:focus-visible{outline:2px solid var(--accent-blue);outline-offset:2px}.convoy-list-table{width:100%;border-collapse:collapse;font-size:.8125rem}.convoy-list-table thead{position:sticky;top:0}.convoy-list-table th{padding:10px 16px;font-size:.6875rem;font-weight:600;color:var(--text-tertiary);text-align:left;text-transform:uppercase;letter-spacing:.06em;background:var(--bg-tertiary);border-bottom:1px solid var(--border-color);white-space:nowrap}.convoy-list-table td{padding:10px 16px;color:var(--text-secondary);border-bottom:1px solid rgba(255,255,255,.03);white-space:nowrap}.convoy-list-table tr{cursor:pointer;transition:background var(--transition-fast)}.convoy-list-table tbody tr:hover td{background:#a78bfa0d;color:var(--text-primary)}.convoy-list-table tbody tr:hover td:first-child{color:var(--text-accent)}.convoy-list-table .td-convoy-name{font-weight:600;color:var(--text-primary);max-width:240px;overflow:hidden;text-overflow:ellipsis}.convoy-list-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:48px 24px;text-align:center;color:var(--text-tertiary)}.convoy-list-empty__icon{font-size:2rem;opacity:.35}.convoy-list-empty__text{font-size:.875rem;color:var(--text-tertiary);max-width:300px;line-height:1.5}.convoy-detail-hero{display:flex;flex-direction:column;gap:12px;padding:24px;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px;position:relative}.convoy-detail-hero:before{content:"";position:absolute;top:0;left:0;right:0;height:2px;background:var(--gradient-accent);opacity:.6;pointer-events:none}.convoy-detail-hero__top{display:flex;align-items:center;gap:14px;flex-wrap:wrap}.convoy-detail-hero__title{font-size:1.5rem;font-weight:700;color:var(--text-primary);letter-spacing:-.02em;line-height:1.25;margin:0}.convoy-detail-hero__status{flex-shrink:0}.convoy-detail-hero__status .status-badge{padding:.25rem .875rem;font-size:.75rem;border-radius:8px}.convoy-detail-hero__meta{display:flex;flex-wrap:wrap;gap:20px;padding-top:4px;border-top:1px solid var(--border-color)}.convoy-detail-hero__meta-item{display:flex;flex-direction:column;gap:2px}.convoy-detail-hero__meta-label{font-size:.6875rem;font-weight:500;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.05em}.convoy-detail-hero__meta-value{font-size:.875rem;color:var(--text-secondary);font-variant-numeric:tabular-nums}.task-row--clickable{cursor:pointer;transition:background var(--transition-fast)}.task-row--clickable:hover td{background:#a78bfa0a}.task-row--clickable td:first-child{position:relative}.task-row--clickable td:first-child:before{content:"";position:absolute;left:0;top:0;bottom:0;width:2px;background:transparent;transition:background var(--transition-fast)}.task-row--clickable:hover td:first-child:before{background:var(--border-accent)}.task-detail-expand{background:var(--bg-tertiary);border-bottom:1px solid var(--border-color)}.task-detail-expand__inner{padding:16px 20px}.task-detail-expand__grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px 24px}.task-detail-expand__field{display:flex;flex-direction:column;gap:3px}.task-detail-expand__label{font-size:.6875rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-tertiary)}.task-detail-expand__value{font-size:.8125rem;color:var(--text-secondary);word-break:break-word;font-variant-numeric:tabular-nums}.task-detail-expand__value code{font-family:SF Mono,Fira Code,Cascadia Code,monospace;font-size:.75rem;background:#ffffff0f;padding:1px 6px;border-radius:4px;color:var(--text-accent)}@media(max-width:768px){.convoy-list-filters{padding:12px 16px;gap:8px}.convoy-list-filters__input,.convoy-list-filters__select,.convoy-list-filters__date{width:100%;min-width:unset}.convoy-list-filters__group{flex:1 1 calc(50% - 4px)}.convoy-list-filters__reset{flex:1 1 100%;width:100%}.convoy-list-section__header{padding:16px 16px 12px}}@media(max-width:480px){.convoy-list-filters__group{flex:1 1 100%}.convoy-detail-hero__title{font-size:1.25rem}.convoy-detail-hero{padding:16px}.task-detail-expand__grid{grid-template-columns:1fr}.breadcrumbs__current{max-width:180px}}