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.
- package/README.md +12 -3
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +1 -10
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +1 -0
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/convoy/export.d.ts +1 -3
- package/dist/cli/convoy/export.d.ts.map +1 -1
- package/dist/cli/convoy/export.js +9 -88
- package/dist/cli/convoy/export.js.map +1 -1
- package/dist/cli/convoy/export.test.js +7 -186
- package/dist/cli/convoy/export.test.js.map +1 -1
- package/dist/cli/convoy/pipeline.d.ts.map +1 -1
- package/dist/cli/convoy/pipeline.js +0 -21
- package/dist/cli/convoy/pipeline.js.map +1 -1
- package/dist/cli/convoy/pipeline.test.js +0 -21
- package/dist/cli/convoy/pipeline.test.js.map +1 -1
- package/dist/cli/dashboard.d.ts.map +1 -1
- package/dist/cli/dashboard.js +32 -8
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/destroy.d.ts.map +1 -1
- package/dist/cli/destroy.js +13 -0
- package/dist/cli/destroy.js.map +1 -1
- package/dist/cli/dispute.d.ts +3 -0
- package/dist/cli/dispute.d.ts.map +1 -0
- package/dist/cli/dispute.js +25 -0
- package/dist/cli/dispute.js.map +1 -0
- package/dist/cli/doctor.d.ts +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +14 -1
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/eject.d.ts.map +1 -1
- package/dist/cli/eject.js +14 -0
- package/dist/cli/eject.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +14 -0
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/log.d.ts +0 -11
- package/dist/cli/log.d.ts.map +1 -1
- package/dist/cli/log.js +2 -114
- package/dist/cli/log.js.map +1 -1
- package/dist/cli/pipeline.d.ts +17 -0
- package/dist/cli/pipeline.d.ts.map +1 -1
- package/dist/cli/pipeline.js +259 -24
- package/dist/cli/pipeline.js.map +1 -1
- package/dist/cli/pipeline.test.d.ts +2 -0
- package/dist/cli/pipeline.test.d.ts.map +1 -0
- package/dist/cli/pipeline.test.js +178 -0
- package/dist/cli/pipeline.test.js.map +1 -0
- package/dist/cli/run.js +2 -2
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +16 -0
- package/dist/cli/update.js.map +1 -1
- package/dist/cli/watch.d.ts.map +1 -1
- package/dist/cli/watch.js +1 -3
- package/dist/cli/watch.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/convoy/engine.test.ts +1 -0
- package/src/cli/convoy/engine.ts +1 -4
- package/src/cli/convoy/export.test.ts +7 -224
- package/src/cli/convoy/export.ts +10 -106
- package/src/cli/convoy/pipeline.test.ts +0 -25
- package/src/cli/convoy/pipeline.ts +0 -19
- package/src/cli/dashboard.ts +33 -8
- package/src/cli/destroy.ts +15 -0
- package/src/cli/dispute.ts +28 -0
- package/src/cli/doctor.ts +16 -1
- package/src/cli/eject.ts +16 -0
- package/src/cli/init.ts +16 -0
- package/src/cli/log.ts +2 -120
- package/src/cli/pipeline.test.ts +191 -0
- package/src/cli/pipeline.ts +326 -26
- package/src/cli/run.ts +2 -2
- package/src/cli/update.ts +18 -0
- package/src/cli/watch.ts +1 -3
- package/src/dashboard/dist/_astro/index.Je1YjU_y.css +1 -0
- package/src/dashboard/dist/index.html +537 -1394
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/scripts/etl.test.ts +4 -62
- package/src/dashboard/scripts/etl.ts +13 -33
- package/src/dashboard/src/pages/index.astro +684 -1624
- package/src/dashboard/src/styles/dashboard.css +473 -7
- package/src/orchestrator/agents/team-lead.agent.md +13 -0
- package/src/orchestrator/prompts/brainstorm.prompt.md +1 -0
- package/src/orchestrator/prompts/fix-prd.prompt.md +58 -0
- package/src/orchestrator/prompts/generate-convoy.prompt.md +30 -0
- package/src/orchestrator/prompts/generate-prd.prompt.md +38 -0
- package/dist/cli/convoy/log-merge.test.d.ts +0 -2
- package/dist/cli/convoy/log-merge.test.d.ts.map +0 -1
- package/dist/cli/convoy/log-merge.test.js +0 -147
- package/dist/cli/convoy/log-merge.test.js.map +0 -1
- package/src/cli/convoy/log-merge.test.ts +0 -179
- package/src/dashboard/dist/_astro/index.6L3_HsPT.css +0 -1
package/src/cli/pipeline.ts
CHANGED
|
@@ -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 {
|
|
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 —
|
|
16
|
-
Step 4 —
|
|
17
|
-
Step 5 —
|
|
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 :
|
|
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
|
-
|
|
200
|
-
console.log(
|
|
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.
|
|
203
|
-
`
|
|
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
|
-
|
|
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
|
|
212
|
-
const genStep = opts.skipValidation ? 2 :
|
|
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
|
|
237
|
-
console.log(stepLabel(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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/
|
|
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/
|
|
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
|
|
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}}
|