periplon 0.2.0

Rust SDK for building multi-agent AI workflows and automation
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
# Predefined Tasks Lock File System

## Overview

The lock file system ensures reproducible dependency resolution for predefined tasks across different environments and time periods. It captures exact versions, checksums, and source information for all resolved dependencies.

## Features

- **Reproducibility**: Ensures the same dependency versions are used across different machines
- **Integrity**: Verifies task content hasn't changed via SHA-256 checksums
- **Transparency**: Tracks where each task came from (local, git, registry)
- **Performance**: Skips resolution when lock file is valid
- **Version Compatibility**: Lock file format versioning with compatibility checks

## Lock File Format

Lock files are stored in YAML format at `.claude/tasks.lock.yaml`:

```yaml
version: "1.0.0"
generated_at: "2025-01-15T10:30:00Z"
generated_by: "periplon@0.1.0"

tasks:
  google-drive-upload:
    version: "1.2.0"
    checksum: "sha256:abc123..."
    source:
      type: "local"
      path: "./.claude/tasks/google-drive-upload.task.yaml"
    resolved_at: "2025-01-15T10:30:00Z"
    dependencies:
      auth-helper: "2.1.0"
    metadata:
      description: "Upload files to Google Drive"
      author: "Example Author"
      license: "MIT"

  auth-helper:
    version: "2.1.0"
    checksum: "sha256:def456..."
    source:
      type: "git"
      url: "https://github.com/org/tasks.git"
      ref: "v2.1.0"
      subpath: "auth-helper"
    resolved_at: "2025-01-15T10:30:00Z"
    dependencies: {}
    metadata:
      description: "Authentication helper"
      author: "Example Author"
      license: "MIT"

metadata:
  task_count: 2
```

## Usage

### Generating a Lock File

```rust
use periplon_sdk::dsl::predefined_tasks::{
    DependencyResolver, LocalSourceResolver, generate_lock_file,
    TaskReference, LOCK_FILE_NAME,
};

// Resolve dependencies
let mut resolver = DependencyResolver::new();
// ... add tasks to resolver ...

let task_ref = TaskReference::parse("my-task@1.0.0")?;
let resolved = resolver.resolve(&task_ref)?;

// Generate lock file
let source_resolver = LocalSourceResolver::new("./.claude/tasks".into());
let lock_file = generate_lock_file(&resolved, &source_resolver)?;

// Save to disk
lock_file.save(LOCK_FILE_NAME)?;
```

### Loading and Validating a Lock File

```rust
use periplon_sdk::dsl::predefined_tasks::{
    LockFile, validate_lock_file,
};

// Load existing lock file
let lock_file = LockFile::load(".claude/tasks.lock.yaml")?;

// Validate against current dependencies
let result = validate_lock_file(&lock_file, &resolved)?;

if result.is_valid() {
    println!("Lock file is valid!");
} else {
    println!("Issues found:");
    println!("{}", result.summary());
}
```

### Checking for Staleness

```rust
// Quick check if lock file needs regeneration
if lock_file.is_stale(&resolved) {
    println!("Lock file is stale - regenerating...");
    let new_lock_file = generate_lock_file(&resolved, &source_resolver)?;
    new_lock_file.save(LOCK_FILE_NAME)?;
}
```

### Verifying Task Integrity

```rust
// Verify a specific task's checksum
match lock_file.verify_task("my-task", &task) {
    Ok(_) => println!("Checksum verified!"),
    Err(e) => println!("Verification failed: {}", e),
}

// Verify all tasks
let tasks = /* HashMap of task name -> PredefinedTask */;
lock_file.verify_all(&tasks)?;
```

## Lock File Components

### LockFile

Main lock file structure containing:
- `version`: Lock file format version
- `generated_at`: Timestamp when generated
- `generated_by`: Tool and version that created it
- `tasks`: Map of task name to LockedTask
- `metadata`: Optional metadata

### LockedTask

Information about a locked task:
- `version`: Exact resolved version
- `checksum`: SHA-256 content checksum
- `source`: Source information (local/git/registry)
- `resolved_at`: When this task was resolved
- `dependencies`: Resolved dependency versions
- `metadata`: Optional task metadata (description, author, license)

### TaskSource

Source tracking for tasks:

**Local Source:**
```yaml
source:
  type: local
  path: "./.claude/tasks/my-task.task.yaml"
```

**Git Source:**
```yaml
source:
  type: git
  url: "https://github.com/org/repo.git"
  ref: "v1.0.0"
  subpath: "tasks/my-task"
```

**Registry Source:**
```yaml
source:
  type: registry
  url: "https://registry.example.com"
  package: "org/my-task"
```

## Validation

The validation system checks for:

1. **Version Mismatches**: Locked version differs from resolved version
2. **Dependency Mismatches**: Dependency graph changed
3. **Checksum Failures**: Task content modified
4. **Missing Tasks**: Task in dependencies but not in lock file
5. **Extra Tasks**: Task in lock file but not in dependencies

### ValidationResult

```rust
pub struct ValidationResult {
    pub issues: Vec<ValidationIssue>,
}

impl ValidationResult {
    pub fn is_valid(&self) -> bool { /* ... */ }
    pub fn summary(&self) -> String { /* ... */ }
}
```

### ValidationIssue Types

```rust
pub enum ValidationIssue {
    MissingTask { name: String },
    ExtraTask { name: String },
    VersionMismatch { name: String, locked: String, resolved: String },
    DependencyMismatch { name: String },
    ChecksumFailed { name: String, error: String },
}
```

## Source Resolution

Implement the `SourceResolver` trait to customize source tracking:

```rust
use periplon_sdk::dsl::predefined_tasks::{
    SourceResolver, LockFileError,
    lockfile::TaskSource,
};

struct CustomSourceResolver {
    // ... custom fields ...
}

impl SourceResolver for CustomSourceResolver {
    fn resolve_source(
        &self,
        name: &str,
        version: &str,
    ) -> Result<TaskSource, LockFileError> {
        // Custom logic to determine source
        // e.g., check git, local, or registry
        Ok(TaskSource::Local {
            path: format!("./tasks/{}.task.yaml", name),
        })
    }
}
```

### Built-in Resolvers

**LocalSourceResolver**: Assumes all tasks are local files

```rust
use periplon_sdk::dsl::predefined_tasks::LocalSourceResolver;

let resolver = LocalSourceResolver::new("./.claude/tasks".into());
```

## Checksum Computation

Checksums are computed from the canonical YAML serialization of tasks:

```rust
use periplon_sdk::dsl::predefined_tasks::compute_task_checksum;

let checksum = compute_task_checksum(&task)?;
// Returns: "sha256:abc123..."
```

The checksum includes:
- All task metadata (name, version, description, etc.)
- Agent template configuration
- Input/output specifications
- Dependencies
- Examples

Any change to the task definition changes the checksum.

## Best Practices

### 1. Commit Lock Files

Always commit `tasks.lock.yaml` to version control:
```bash
git add .claude/tasks.lock.yaml
git commit -m "Update task dependencies"
```

### 2. Regenerate After Updates

Regenerate lock files when dependencies change:
```bash
# After updating a task version
periplon-executor lock --regenerate
```

### 3. Validate in CI/CD

Add lock file validation to your CI pipeline:
```rust
let lock_file = LockFile::load("tasks.lock.yaml")?;
let result = validate_lock_file(&lock_file, &resolved)?;

if !result.is_valid() {
    eprintln!("Lock file validation failed!");
    eprintln!("{}", result.summary());
    std::process::exit(1);
}
```

### 4. Review Lock File Changes

When reviewing PRs, check lock file changes for:
- Expected version updates
- New/removed dependencies
- Source changes

### 5. Handle Conflicts

Resolve lock file merge conflicts by regenerating:
```bash
# In case of conflict
git checkout --theirs .claude/tasks.lock.yaml
periplon-executor lock --regenerate
```

## Integration with Dependency Resolution

Lock files integrate seamlessly with the dependency resolver:

```rust
// 1. Load lock file if it exists
let lock_file = match LockFile::load(LOCK_FILE_NAME) {
    Ok(lf) => Some(lf),
    Err(_) => None,
};

// 2. Resolve dependencies
let resolved = resolver.resolve(&task_ref)?;

// 3. Check if lock file is valid
if let Some(ref lf) = lock_file {
    if !lf.is_stale(&resolved) {
        // Use locked versions
        println!("Using locked versions");
    } else {
        // Regenerate lock file
        let new_lf = generate_lock_file(&resolved, &source_resolver)?;
        new_lf.save(LOCK_FILE_NAME)?;
    }
} else {
    // Generate new lock file
    let new_lf = generate_lock_file(&resolved, &source_resolver)?;
    new_lf.save(LOCK_FILE_NAME)?;
}
```

## Example: Complete Workflow

```rust
use periplon_sdk::dsl::predefined_tasks::{
    DependencyResolver, LocalSourceResolver, LockFile,
    generate_lock_file, validate_lock_file, TaskReference,
    LOCK_FILE_NAME,
};

fn resolve_with_lockfile(
    task_ref: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    // Setup
    let mut resolver = DependencyResolver::new();
    // ... populate resolver ...

    let task_ref = TaskReference::parse(task_ref)?;
    let resolved = resolver.resolve(&task_ref)?;

    // Try to use existing lock file
    match LockFile::load(LOCK_FILE_NAME) {
        Ok(lock_file) => {
            println!("Found existing lock file");

            // Validate
            let result = validate_lock_file(&lock_file, &resolved)?;

            if result.is_valid() && !lock_file.is_stale(&resolved) {
                println!("✓ Lock file is valid and up-to-date");
                // Use locked versions
                for (name, locked) in &lock_file.tasks {
                    println!("  {} @ {}", name, locked.version);
                }
            } else {
                println!("⚠ Lock file needs regeneration");
                println!("{}", result.summary());

                // Regenerate
                let source_resolver = LocalSourceResolver::new(
                    "./.claude/tasks".into()
                );
                let new_lock = generate_lock_file(&resolved, &source_resolver)?;
                new_lock.save(LOCK_FILE_NAME)?;
                println!("✓ Lock file regenerated");
            }
        }
        Err(_) => {
            println!("No lock file found, generating...");

            // Generate new lock file
            let source_resolver = LocalSourceResolver::new(
                "./.claude/tasks".into()
            );
            let lock_file = generate_lock_file(&resolved, &source_resolver)?;
            lock_file.save(LOCK_FILE_NAME)?;
            println!("✓ Lock file created");
        }
    }

    Ok(())
}
```

## Error Handling

The lock file system provides detailed errors:

```rust
use periplon_sdk::dsl::predefined_tasks::LockFileError;

match lock_file.verify_task("my-task", &task) {
    Ok(_) => println!("Verified"),
    Err(LockFileError::ChecksumMismatch { task, expected, actual }) => {
        eprintln!("Checksum mismatch for {}:", task);
        eprintln!("  Expected: {}", expected);
        eprintln!("  Actual:   {}", actual);
    }
    Err(LockFileError::TaskNotFound(name)) => {
        eprintln!("Task '{}' not in lock file", name);
    }
    Err(e) => eprintln!("Error: {}", e),
}
```

## Running the Demo

See the complete working example:

```bash
cargo run --example lockfile_demo
```

This demonstrates:
1. Generating lock files from dependencies
2. Validating lock files
3. Checksum verification
4. Lock file persistence and loading

## API Reference

### Core Functions

- `generate_lock_file(resolved, source_resolver)` - Generate lock file from resolved tasks
- `validate_lock_file(lock_file, resolved)` - Validate lock file against dependencies
- `compute_task_checksum(task)` - Compute SHA-256 checksum of a task

### Types

- `LockFile` - Main lock file structure
- `LockedTask` - Individual locked task entry
- `TaskSource` - Source tracking (Local/Git/Registry)
- `ValidationResult` - Validation results with issues
- `ValidationIssue` - Individual validation issue
- `SourceResolver` - Trait for resolving task sources
- `LocalSourceResolver` - Built-in local file resolver

### Constants

- `LOCK_FILE_VERSION` - Current lock file format version ("1.0.0")
- `LOCK_FILE_NAME` - Default lock file name ("tasks.lock.yaml")

## See Also

- [Dependency Resolution]./predefined-tasks-deps.md
- [Predefined Tasks Overview]./predefined-tasks.md
- [Task Sources]./predefined-tasks-sources.md