cueloop 0.5.0

A Rust CLI for managing AI agent loops with a structured JSON task queue
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
//! Versioned machine-contract documents for app/CLI integration.
//!
//! Purpose:
//! - Versioned machine-contract documents for app/CLI integration.
//!
//! Responsibilities:
//! - Define the stable JSON documents consumed by the macOS app via `cueloop machine ...`.
//! - Centralize machine-only request/response and event envelope types.
//! - Provide schema-friendly wrappers around queue/config/task/run data.
//!
//! Not handled here:
//! - Command execution or clap wiring.
//! - Human CLI rendering.
//! - Queue/task/run business logic.
//!
//!
//! Usage:
//! - Used through the crate module tree or integration test harness.
//!
//! Invariants/assumptions:
//! - Every machine document includes an explicit `version`.
//! - Breaking wire changes require version bumps.
//! - Run events are emitted as NDJSON envelopes ordered by occurrence.

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;

use super::{
    BlockingState, CliSpec, Config, GitPublishMode, GitRevertMode, QueueFile, RunnerApprovalMode,
    Task,
};

pub const MACHINE_SYSTEM_INFO_VERSION: u32 = 1;
pub const MACHINE_QUEUE_READ_VERSION: u32 = 1;
pub const MACHINE_QUEUE_VALIDATE_VERSION: u32 = 1;
pub const MACHINE_QUEUE_REPAIR_VERSION: u32 = 1;
pub const MACHINE_QUEUE_UNDO_VERSION: u32 = 1;
pub const MACHINE_CONFIG_RESOLVE_VERSION: u32 = 5;
pub const MACHINE_WORKSPACE_OVERVIEW_VERSION: u32 = 1;
pub const MACHINE_TASK_CREATE_VERSION: u32 = 1;
pub const MACHINE_TASK_BUILD_VERSION: u32 = 1;
pub const MACHINE_TASK_MUTATION_VERSION: u32 = 2;
pub const MACHINE_GRAPH_READ_VERSION: u32 = 1;
pub const MACHINE_DASHBOARD_READ_VERSION: u32 = 1;
pub const MACHINE_DECOMPOSE_VERSION: u32 = 2;
pub const MACHINE_RUN_EVENT_VERSION: u32 = 3;
pub const MACHINE_RUN_SUMMARY_VERSION: u32 = 2;
pub const MACHINE_DOCTOR_REPORT_VERSION: u32 = 2;
pub const MACHINE_PARALLEL_STATUS_VERSION: u32 = 3;
pub const MACHINE_CLI_SPEC_VERSION: u32 = 2;
pub const MACHINE_ERROR_VERSION: u32 = 1;
pub const MACHINE_QUEUE_UNLOCK_INSPECT_VERSION: u32 = 1;
pub const MACHINE_RUN_STOP_VERSION: u32 = 1;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum MachineErrorCode {
    CliUnavailable,
    PermissionDenied,
    ConfigIncompatible,
    ParseError,
    NetworkError,
    QueueCorrupted,
    ResourceBusy,
    VersionMismatch,
    TaskMutationConflict,
    Unknown,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineErrorDocument {
    pub version: u32,
    pub code: MachineErrorCode,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub detail: Option<String>,
    pub retryable: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineSystemInfoDocument {
    pub version: u32,
    pub cli_version: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineQueuePaths {
    pub repo_root: String,
    pub queue_path: String,
    pub done_path: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub project_config_path: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub global_config_path: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineQueueReadDocument {
    pub version: u32,
    pub paths: MachineQueuePaths,
    pub active: QueueFile,
    pub done: QueueFile,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub next_runnable_task_id: Option<String>,
    #[schemars(schema_with = "json_value_schema")]
    pub runnability: JsonValue,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineContinuationAction {
    pub title: String,
    pub command: String,
    pub detail: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineContinuationSummary {
    pub headline: String,
    pub detail: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub blocking: Option<BlockingState>,
    #[serde(default)]
    pub next_steps: Vec<MachineContinuationAction>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineValidationWarning {
    pub task_id: String,
    pub message: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineQueueValidateDocument {
    pub version: u32,
    pub valid: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub blocking: Option<BlockingState>,
    #[serde(default)]
    pub warnings: Vec<MachineValidationWarning>,
    pub continuation: MachineContinuationSummary,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineQueueRepairDocument {
    pub version: u32,
    pub dry_run: bool,
    pub changed: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub blocking: Option<BlockingState>,
    #[schemars(schema_with = "json_value_schema")]
    pub report: JsonValue,
    pub continuation: MachineContinuationSummary,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineQueueUndoDocument {
    pub version: u32,
    pub dry_run: bool,
    pub restored: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub blocking: Option<BlockingState>,
    #[schemars(schema_with = "option_json_value_schema")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub result: Option<JsonValue>,
    pub continuation: MachineContinuationSummary,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum MachineQueueUnlockCondition {
    Clear,
    Live,
    Stale,
    OwnerMissing,
    OwnerUnreadable,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineQueueUnlockInspectDocument {
    pub version: u32,
    pub condition: MachineQueueUnlockCondition,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub blocking: Option<BlockingState>,
    pub unlock_allowed: bool,
    pub continuation: MachineContinuationSummary,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineResumeDecision {
    pub status: String,
    pub scope: String,
    pub reason: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub task_id: Option<String>,
    pub message: String,
    pub detail: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineConfigResolveDocument {
    pub version: u32,
    pub paths: MachineQueuePaths,
    pub safety: MachineConfigSafetySummary,
    pub config: Config,
    pub execution_controls: MachineExecutionControls,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub resume_preview: Option<MachineResumeDecision>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineWorkspaceOverviewDocument {
    pub version: u32,
    pub queue: MachineQueueReadDocument,
    pub config: MachineConfigResolveDocument,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineConfigSafetySummary {
    pub repo_trusted: bool,
    pub dirty_repo: bool,
    pub git_publish_mode: GitPublishMode,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub approval_mode: Option<RunnerApprovalMode>,
    pub ci_gate_enabled: bool,
    pub git_revert_mode: GitRevertMode,
    pub parallel_configured: bool,
    pub execution_interactivity: String,
    pub interactive_approval_supported: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum MachineExecutionControlDiagnosticSeverity {
    Warning,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum MachineExecutionControlDiagnosticCode {
    PluginRegistryLoadFailed,
    PluginRunnerIdConflict,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineExecutionControlDiagnostic {
    pub severity: MachineExecutionControlDiagnosticSeverity,
    pub code: MachineExecutionControlDiagnosticCode,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub detail: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub plugin_id: Option<String>,
    pub fallback: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineExecutionControls {
    pub runners: Vec<MachineRunnerOption>,
    pub reasoning_efforts: Vec<String>,
    pub parallel_workers: MachineParallelWorkersControl,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub diagnostics: Vec<MachineExecutionControlDiagnostic>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineRunnerOption {
    pub id: String,
    pub display_name: String,
    pub source: String,
    pub reasoning_effort_supported: bool,
    pub supports_arbitrary_model: bool,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub allowed_models: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub default_model: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineParallelWorkersControl {
    pub min: u8,
    pub max: u8,
    pub default_missing_value: u8,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineCliSpecDocument {
    pub version: u32,
    pub spec: CliSpec,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineTaskCreateRequest {
    pub version: u32,
    pub title: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    pub priority: String,
    #[serde(default)]
    pub tags: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub scope: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub template: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub target: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineTaskCreateDocument {
    pub version: u32,
    pub task: Task,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineTaskBuildRequest {
    pub version: u32,
    pub request: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tags: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub scope: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub template: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub target: Option<String>,
    #[serde(default)]
    pub strict_templates: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub estimated_minutes: Option<u32>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineTaskBuildDocument {
    pub version: u32,
    pub mode: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub blocking: Option<BlockingState>,
    pub result: MachineTaskBuildResult,
    #[serde(default)]
    pub warnings: Vec<String>,
    pub continuation: MachineContinuationSummary,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineTaskBuildResult {
    pub created_count: usize,
    #[serde(default)]
    pub task_ids: Vec<String>,
    #[serde(default)]
    pub tasks: Vec<Task>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineTaskMutationDocument {
    pub version: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub blocking: Option<BlockingState>,
    #[schemars(schema_with = "json_value_schema")]
    pub report: JsonValue,
    pub continuation: MachineContinuationSummary,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineGraphReadDocument {
    pub version: u32,
    #[schemars(schema_with = "json_value_schema")]
    pub graph: JsonValue,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineDashboardReadDocument {
    pub version: u32,
    #[schemars(schema_with = "json_value_schema")]
    pub dashboard: JsonValue,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineDecomposeDocument {
    pub version: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub blocking: Option<BlockingState>,
    #[schemars(schema_with = "json_value_schema")]
    pub result: JsonValue,
    pub continuation: MachineContinuationSummary,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineDoctorReportDocument {
    pub version: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub blocking: Option<BlockingState>,
    #[schemars(schema_with = "json_value_schema")]
    pub report: JsonValue,
}

/// Worker counts by lifecycle for `machine run parallel-status` (document v3+).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineParallelLifecycleCounts {
    pub running: u32,
    pub integrating: u32,
    pub completed: u32,
    pub failed: u32,
    pub blocked: u32,
    pub total: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineParallelStatusDocument {
    pub version: u32,
    pub lifecycle_counts: MachineParallelLifecycleCounts,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub blocking: Option<BlockingState>,
    pub continuation: MachineContinuationSummary,
    #[schemars(schema_with = "json_value_schema")]
    pub status: JsonValue,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum MachineRunStopAction {
    WouldCreate,
    Created,
    AlreadyPresent,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineRunStopMarker {
    pub path: String,
    pub existed_before: bool,
    pub exists_after: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineRunStopDocument {
    pub version: u32,
    pub dry_run: bool,
    pub action: MachineRunStopAction,
    pub paths: MachineQueuePaths,
    pub marker: MachineRunStopMarker,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub blocking: Option<BlockingState>,
    pub continuation: MachineContinuationSummary,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum MachineRunEventKind {
    RunStarted,
    QueueSnapshot,
    ConfigResolved,
    ResumeDecision,
    TaskSelected,
    PhaseEntered,
    PhaseCompleted,
    RunnerOutput,
    BlockedStateChanged,
    BlockedStateCleared,
    Warning,
    RunFinished,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineRunEventEnvelope {
    pub version: u32,
    pub kind: MachineRunEventKind,
    pub timestamp: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub run_mode: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub task_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub phase: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub exit_code: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stream: Option<String>,
    #[schemars(schema_with = "option_json_value_schema")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub payload: Option<JsonValue>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineRunSummaryDocument {
    pub version: u32,
    pub task_id: Option<String>,
    pub exit_code: i32,
    pub outcome: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub blocking: Option<BlockingState>,
}

fn json_value_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
    <JsonValue as JsonSchema>::json_schema(generator)
}

fn option_json_value_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
    <Option<JsonValue> as JsonSchema>::json_schema(generator)
}