qontinui-types 0.6.0

Canonical DTO types for Qontinui. Rust is the source of truth; TypeScript and Python are generated from JSON Schema emitted by schemars.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
//! Verification-plan DTO types.
//!
//! Wire-format types for the orchestrator's planning → execution → verification
//! pipeline: success criteria, verification plans, worker instances and their
//! signals, findings, iteration-level verification results, criterion overrides,
//! and the task-completion envelope.
//!
//! These types are a port of the shape-bearing portion of
//! `qontinui-runner/src-tauri/src/orchestrator/types.rs`. Behavior
//! (constructors that stamp timestamps, parsers that extract signals from AI
//! output text, override-application logic, feedback-string builders) stays in
//! the runner — this crate is data-only. The runner exposes behavior via
//! extension traits in `orchestrator::types`.
//!
//! Wire-format notes:
//! - Dates/times are ISO 8601 `String`s (see crate-level docs).
//! - UUIDs are `String`s (wire-format), not `uuid::Uuid`.
//! - `SuccessCriterion.criterion_type` serializes as `"type"` on the wire (the
//!   field is renamed via `#[serde(rename = "type")]`).
//! - `WorkerCoordinationMessage` uses the `{ "type": ..., "data": ... }`
//!   externally-tagged envelope.
//! - `WorkerSignal` uses `{ "signal": ..., "data": ... }` as its discriminator
//!   pair — `signal` rather than `type` to avoid collision with other tagged
//!   unions and to match the pre-extraction wire.
//! - `TaskCompletionResult` uses `{ "status": ..., ... }` with the variant
//!   fields inlined (internally tagged).

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

// ============================================================================
// Serde default helpers
// ============================================================================

fn default_true() -> bool {
    true
}

fn default_worker_count() -> u32 {
    1
}

fn default_version() -> u32 {
    1
}

// ============================================================================
// Criterion & verification plan
// ============================================================================

/// A success criterion that must be met for task completion.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct SuccessCriterion {
    /// Unique identifier for this criterion.
    #[serde(alias = "id")]
    pub id: String,

    /// Human-readable description.
    #[serde(alias = "description")]
    pub description: String,

    /// Type of verification required. Serialized as `"type"` on the wire.
    #[serde(rename = "type", alias = "criterion_type")]
    pub criterion_type: CriterionType,

    /// For deterministic criteria: the verification method to use.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "verification_method"
    )]
    pub verification_method: Option<VerificationMethod>,

    /// Configuration blob for the verification method (command args, log
    /// patterns, Playwright script path, etc.).
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "verification_config"
    )]
    pub verification_config: Option<serde_json::Value>,

    /// For AI-evaluated criteria: the evaluation prompt.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "evaluation_prompt"
    )]
    pub evaluation_prompt: Option<String>,

    /// Whether this criterion must pass for task completion.
    #[serde(default = "default_true", alias = "required")]
    pub required: bool,

    /// Whether failure of this criterion blocks task completion.
    /// - `true` (default): failure blocks completion, worker must fix.
    /// - `false`: failure is informational, doesn't block completion.
    #[serde(default = "default_true", alias = "is_critical")]
    pub is_critical: bool,

    /// Optional weight for partial success scoring.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "weight")]
    pub weight: Option<f64>,

    /// Domain this criterion belongs to (for multi-worker verification).
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "domain")]
    pub domain: Option<String>,
}

/// Type of verification for a criterion.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum CriterionType {
    /// Can be verified programmatically without AI.
    Deterministic,
    /// Requires AI evaluation (e.g., screenshot review).
    AiEvaluated,
}

/// Methods for deterministic verification.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum VerificationMethod {
    /// Build must succeed.
    BuildSuccess,
    /// Unit tests must pass.
    UnitTest,
    /// Integration tests must pass.
    IntegrationTest,
    /// Playwright script must pass.
    Playwright,
    /// Log pattern must match (or not match).
    LogPattern,
    /// GUI automation workflow must succeed.
    GuiAutomation,
    /// Type check must pass.
    TypeCheck,
    /// Lint check must pass.
    LintCheck,
    /// Custom command must succeed.
    CustomCommand,
}

/// The verification plan created by the planning agent.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct VerificationPlan {
    /// Summary of the goal.
    #[serde(alias = "goal_summary")]
    pub goal_summary: String,

    /// All success criteria that must be verified.
    #[serde(alias = "success_criteria")]
    pub success_criteria: Vec<SuccessCriterion>,

    /// Execution steps to run before verification (GUI automation / setup).
    /// Stored as raw JSON values because the step discriminated union spans
    /// several types that are outside this module's scope.
    #[serde(
        default,
        skip_serializing_if = "Vec::is_empty",
        alias = "execution_steps"
    )]
    pub execution_steps: Vec<serde_json::Value>,

    /// Suggested number of worker agents.
    #[serde(default = "default_worker_count", alias = "suggested_worker_count")]
    pub suggested_worker_count: u32,

    /// Domain assignments for multiple workers.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "worker_domains"
    )]
    pub worker_domains: Option<Vec<WorkerDomain>>,

    /// Plan version (incremented on replan).
    #[serde(default = "default_version", alias = "version")]
    pub version: u32,
}

/// Domain assignment for a worker agent (lightweight — used inside a
/// [`VerificationPlan`]'s `worker_domains` list).
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct WorkerDomain {
    /// Worker identifier.
    #[serde(alias = "worker_id")]
    pub worker_id: String,

    /// Optional specialization label (e.g., "frontend", "tests").
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "specialization"
    )]
    pub specialization: Option<String>,

    /// Files / paths this worker owns (glob patterns).
    #[serde(
        default,
        skip_serializing_if = "Vec::is_empty",
        alias = "file_patterns"
    )]
    pub file_patterns: Vec<String>,

    /// Additional system-prompt text to inject for this worker.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "system_prompt_additions"
    )]
    pub system_prompt_additions: Option<String>,
}

// ============================================================================
// Multi-worker support
// ============================================================================

/// Full configuration for a domain that workers can be assigned to.
///
/// Domains represent logical areas of responsibility within a project
/// (e.g., "frontend", "backend", "database", "api"). Workers are assigned to
/// zero or more domains, and criteria can be scoped to a single domain for
/// multi-worker verification.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct DomainAssignment {
    /// Unique identifier for this domain.
    #[serde(alias = "domain_id")]
    pub domain_id: String,

    /// Human-readable name for the domain.
    #[serde(alias = "name")]
    pub name: String,

    /// Description of what this domain covers.
    #[serde(alias = "description")]
    pub description: String,

    /// File patterns that belong to this domain
    /// (e.g., `"src/frontend/**/*.ts"`).
    #[serde(
        default,
        skip_serializing_if = "Vec::is_empty",
        alias = "file_patterns"
    )]
    pub file_patterns: Vec<String>,

    /// Keywords that help identify this domain.
    #[serde(default, skip_serializing_if = "Vec::is_empty", alias = "keywords")]
    pub keywords: Vec<String>,

    /// Workers currently assigned to this domain.
    #[serde(
        default,
        skip_serializing_if = "Vec::is_empty",
        alias = "assigned_workers"
    )]
    pub assigned_workers: Vec<String>,

    /// Success-criterion IDs that are specific to this domain.
    #[serde(
        default,
        skip_serializing_if = "Vec::is_empty",
        alias = "domain_criteria"
    )]
    pub domain_criteria: Vec<String>,

    /// Additional system-prompt context for workers in this domain.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "system_prompt_context"
    )]
    pub system_prompt_context: Option<String>,
}

/// Current state of an individual worker.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WorkerStatus {
    /// Worker is idle, waiting for assignment.
    Idle,
    /// Worker is actively executing.
    Active,
    /// Worker signaled work complete, awaiting verification.
    AwaitingVerification,
    /// Worker is paused.
    Paused,
    /// Worker has completed its work.
    Completed,
    /// Worker encountered an error.
    Error,
}

/// Instance tracking for an individual worker.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct WorkerInstance {
    /// Unique identifier for this worker.
    #[serde(alias = "worker_id")]
    pub worker_id: String,

    /// Human-readable name.
    #[serde(alias = "name")]
    pub name: String,

    /// Current status.
    #[serde(alias = "status")]
    pub status: WorkerStatus,

    /// Domain this worker is assigned to (if any).
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "domain")]
    pub domain: Option<String>,

    /// Current iteration for this worker.
    #[serde(alias = "iteration")]
    pub iteration: u32,

    /// Maximum iterations allowed for this worker.
    #[serde(alias = "max_iterations")]
    pub max_iterations: u32,

    /// Last signal received from this worker.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "last_signal"
    )]
    pub last_signal: Option<WorkerSignal>,

    /// Findings recorded by this worker.
    #[serde(default, skip_serializing_if = "Vec::is_empty", alias = "findings")]
    pub findings: Vec<Finding>,

    /// Files this worker has touched.
    #[serde(
        default,
        skip_serializing_if = "Vec::is_empty",
        alias = "touched_files"
    )]
    pub touched_files: Vec<String>,

    /// ISO 8601 timestamp when the worker started.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "started_at")]
    pub started_at: Option<String>,

    /// ISO 8601 timestamp when the worker completed.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "completed_at"
    )]
    pub completed_at: Option<String>,

    /// Error message if the worker is in error state.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "error_message"
    )]
    pub error_message: Option<String>,
}

/// Coordination message between workers.
///
/// Externally tagged as `{ "type": ..., "data": { ... } }` to match the
/// pre-extraction wire.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", content = "data")]
pub enum WorkerCoordinationMessage {
    /// Worker completed work on a set of files.
    #[serde(rename = "files_modified")]
    FilesModified {
        /// ID of the worker that modified the files.
        worker_id: String,
        /// Paths of the modified files.
        files: Vec<String>,
    },

    /// Worker found an issue that may affect other workers.
    #[serde(rename = "shared_finding")]
    SharedFinding {
        /// ID of the worker that made the finding.
        worker_id: String,
        /// The finding payload.
        finding: Finding,
    },

    /// Worker is blocked waiting for another worker.
    #[serde(rename = "blocked")]
    Blocked {
        /// ID of the blocked worker.
        worker_id: String,
        /// ID of the worker being waited on.
        waiting_for: String,
        /// Human-readable reason for the block.
        reason: String,
    },

    /// Worker is ready for verification.
    #[serde(rename = "ready_for_verification")]
    ReadyForVerification {
        /// ID of the worker that is ready.
        worker_id: String,
        /// Optional domain scope for the verification.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        domain: Option<String>,
    },

    /// Coordination signal to synchronize multiple workers.
    #[serde(rename = "sync_point")]
    SyncPoint {
        /// IDs of the workers that should synchronize.
        worker_ids: Vec<String>,
        /// Human-readable reason for the sync point.
        reason: String,
    },
}

/// Result of domain-scoped verification.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct DomainVerificationResult {
    /// Domain that was verified.
    #[serde(alias = "domain_id")]
    pub domain_id: String,

    /// Workers that contributed to this domain.
    #[serde(alias = "worker_ids")]
    pub worker_ids: Vec<String>,

    /// Verification results for domain-specific criteria.
    #[serde(alias = "results")]
    pub results: Vec<VerificationResult>,

    /// Whether all domain criteria passed.
    #[serde(alias = "all_passed")]
    pub all_passed: bool,

    /// Summary of any failures.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "failure_summary"
    )]
    pub failure_summary: Option<String>,
}

// ============================================================================
// Worker signals & findings
// ============================================================================

/// Signal emitted by a worker agent.
///
/// Tagged as `{ "signal": ..., "data": ... }` rather than the more common
/// `type`/`data` pair; this matches the pre-extraction wire.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "signal", content = "data")]
pub enum WorkerSignal {
    /// Worker believes the work is complete, ready for verification.
    #[serde(rename = "work_complete")]
    WorkComplete {
        /// Optional reason provided by the worker.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        reason: Option<String>,
    },

    /// Worker needs the plan to be revised.
    #[serde(rename = "need_replan")]
    NeedReplan {
        /// Reason for the replan request.
        reason: String,
    },

    /// Worker is continuing work (default — typically emitted only when the
    /// caller wants to assert the worker is still alive).
    #[serde(rename = "continue")]
    Continue,

    /// Worker has recorded a finding.
    #[serde(rename = "finding")]
    Finding(Finding),
}

/// A finding recorded by a worker agent.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct Finding {
    /// Unique identifier.
    #[serde(alias = "id")]
    pub id: String,

    /// Type of finding.
    ///
    /// Valid types: `"bug"`, `"root_cause"`, `"observation"`, `"hypothesis"`,
    /// `"solution"`, `"environment"`. Note: `"environment"` findings (PATH
    /// issues, disk space, tools not installed) require user intervention and
    /// should NOT trigger automatic retries.
    #[serde(alias = "finding_type")]
    pub finding_type: String,

    /// Description of the finding.
    #[serde(alias = "description")]
    pub description: String,

    /// Supporting evidence (file paths, log excerpts, etc.).
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "evidence")]
    pub evidence: Option<String>,

    /// Confidence level.
    #[serde(alias = "confidence")]
    pub confidence: Confidence,

    /// Related file paths.
    #[serde(
        default,
        skip_serializing_if = "Vec::is_empty",
        alias = "related_files"
    )]
    pub related_files: Vec<String>,
}

/// Confidence level for findings and verification results.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum Confidence {
    /// High confidence — deterministic evidence or clear observation.
    High,
    /// Medium confidence — reasonable but not certain.
    Medium,
    /// Low confidence — speculative or weakly supported.
    Low,
}

// ============================================================================
// Verification results
// ============================================================================

/// Result of a single verification check.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct VerificationResult {
    /// The criterion that was checked.
    #[serde(alias = "criterion_id")]
    pub criterion_id: String,

    /// Whether the check passed.
    #[serde(alias = "passed")]
    pub passed: bool,

    /// Type of verification performed.
    #[serde(alias = "criterion_type")]
    pub criterion_type: CriterionType,

    /// Confidence level (for AI verification).
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "confidence")]
    pub confidence: Option<Confidence>,

    /// What was observed.
    #[serde(default, skip_serializing_if = "Vec::is_empty", alias = "observations")]
    pub observations: Vec<String>,

    /// Issues found (if failed).
    #[serde(default, skip_serializing_if = "Vec::is_empty", alias = "issues")]
    pub issues: Vec<String>,

    /// Suggestions for fixing (if failed).
    #[serde(default, skip_serializing_if = "Vec::is_empty", alias = "suggestions")]
    pub suggestions: Vec<String>,

    /// Raw output / details, e.g., captured command output.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "raw_output")]
    pub raw_output: Option<String>,
}

/// Aggregated verification results for a single iteration.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct IterationVerificationResults {
    /// Iteration number.
    #[serde(alias = "iteration")]
    pub iteration: u32,

    /// Deterministic verification results.
    #[serde(alias = "deterministic_results")]
    pub deterministic_results: Vec<VerificationResult>,

    /// AI verification results (empty if skipped).
    #[serde(alias = "ai_results")]
    pub ai_results: Vec<VerificationResult>,

    /// Whether all required deterministic checks passed.
    #[serde(alias = "deterministic_passed")]
    pub deterministic_passed: bool,

    /// Whether all required AI checks passed (true if no AI criteria).
    #[serde(alias = "ai_passed")]
    pub ai_passed: bool,

    /// Overall pass/fail.
    #[serde(alias = "all_passed")]
    pub all_passed: bool,

    /// Human-readable summary of failures.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "failure_summary"
    )]
    pub failure_summary: Option<String>,

    /// Criterion overrides applied in this iteration.
    #[serde(
        default,
        skip_serializing_if = "Vec::is_empty",
        alias = "applied_overrides"
    )]
    pub applied_overrides: Vec<CriterionOverride>,

    /// Criteria that failed but were accepted due to overrides.
    #[serde(
        default,
        skip_serializing_if = "Vec::is_empty",
        alias = "overridden_criteria"
    )]
    pub overridden_criteria: Vec<String>,
}

/// Context passed to the verification agent (intentionally limited — does not
/// include work history, to avoid biasing AI evaluation).
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct VerificationAgentContext {
    /// The screenshot to evaluate, base64-encoded.
    #[serde(alias = "screenshot_base64")]
    pub screenshot_base64: String,

    /// The evaluation prompt from the criterion.
    #[serde(alias = "evaluation_prompt")]
    pub evaluation_prompt: String,

    /// Brief goal context (NOT work history).
    #[serde(alias = "goal_context")]
    pub goal_context: String,
}

/// Request to extend iterations after the max has been reached.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct ExtendIterationsRequest {
    /// Additional iterations to add.
    #[serde(alias = "additional_iterations")]
    pub additional_iterations: u32,

    /// Optional guidance for the worker.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "guidance")]
    pub guidance: Option<String>,
}

// ============================================================================
// Criterion overrides
// ============================================================================

/// An override for a verification criterion.
///
/// Workers can emit these to indicate that a failing criterion should be
/// accepted as-is with justification, rather than requiring a fix.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct CriterionOverride {
    /// The criterion ID being overridden.
    #[serde(alias = "criterion_id")]
    pub criterion_id: String,

    /// What specifically is being overridden (e.g., class name, file path).
    #[serde(alias = "item")]
    pub item: String,

    /// Justification for why this override is acceptable.
    #[serde(alias = "justification")]
    pub justification: String,

    /// Iteration when this override was recorded.
    #[serde(alias = "iteration")]
    pub iteration: u32,

    /// Worker ID that provided the override (if multi-worker).
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "worker_id")]
    pub worker_id: Option<String>,

    /// ISO 8601 timestamp when the override was recorded.
    #[serde(alias = "recorded_at")]
    pub recorded_at: String,
}

/// Collection of overrides for a task run.
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct OverrideCollection {
    /// All recorded overrides.
    #[serde(default, skip_serializing_if = "Vec::is_empty", alias = "overrides")]
    pub overrides: Vec<CriterionOverride>,
}

// ============================================================================
// Task completion & stage tracking
// ============================================================================

/// Task-completion result.
///
/// Internally tagged by `status`: the variant fields are inlined alongside the
/// discriminator rather than nested under a `data` key.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "status")]
pub enum TaskCompletionResult {
    /// Task completed successfully.
    #[serde(rename = "success")]
    Success {
        /// Number of iterations used.
        iterations: u32,
        /// Findings accumulated during the run.
        findings: Vec<Finding>,
        /// Final verification results.
        verification_results: IterationVerificationResults,
    },

    /// Task failed (max iterations or unrecoverable error).
    #[serde(rename = "failed")]
    Failed {
        /// Human-readable reason for the failure.
        reason: String,
        /// Number of iterations attempted.
        iterations: u32,
        /// Last verification results (if any).
        #[serde(default, skip_serializing_if = "Option::is_none")]
        last_results: Option<IterationVerificationResults>,
        /// Findings accumulated during the run.
        findings: Vec<Finding>,
    },

    /// Task stopped by user or system.
    #[serde(rename = "stopped")]
    Stopped {
        /// Iteration at which the task was stopped.
        at_iteration: u32,
        /// Findings accumulated up to the stop.
        findings: Vec<Finding>,
        /// Whether the task can be resumed from this point.
        can_resume: bool,
    },

    /// Task paused at max iterations, awaiting user decision.
    #[serde(rename = "paused")]
    Paused {
        /// Iteration at which the task paused.
        at_iteration: u32,
        /// The max-iteration limit that triggered the pause.
        max_iterations: u32,
        /// Last verification results (if any).
        #[serde(default, skip_serializing_if = "Option::is_none")]
        last_results: Option<IterationVerificationResults>,
        /// Findings accumulated up to the pause.
        findings: Vec<Finding>,
    },
}

/// A record of a stage transition during task execution.
///
/// Used for building the stage-based timeline on the recap page.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct StageTransition {
    /// Previous stage.
    #[serde(alias = "from")]
    pub from: String,
    /// New stage.
    #[serde(alias = "to")]
    pub to: String,
    /// When the transition occurred (ISO 8601).
    #[serde(alias = "timestamp")]
    pub timestamp: String,
    /// Iteration number at the time of the transition.
    #[serde(alias = "iteration")]
    pub iteration: u32,
}