Skip to content

Commit e6576dc

Browse files
pyrex41claude
andcommitted
feat: add UUID task ID support for external tool integration
Add --id-format option to scud parse command to generate UUID task IDs instead of sequential numbers. This enables integration with external tools like Descartes that expect UUID format. Key changes: - Add uuid crate dependency - Add IdFormat enum (Sequential/Uuid) to Phase struct - Generate 32-char hex UUIDs when --id-format uuid is specified - Expand command generates UUID subtasks for UUID phases - Update natural_sort_ids() to handle UUIDs (lexicographic fallback) - Truncate long IDs in list output for readability (8 chars + "...") - Persist id_format in SCG @meta section - Add comprehensive tests for UUID functionality The sequential ID format remains the default for backwards compatibility. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8bd6909 commit e6576dc

File tree

13 files changed

+798
-88
lines changed

13 files changed

+798
-88
lines changed

scud-cli/Cargo.lock

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scud-cli/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "scud-cli"
3-
version = "1.29.1"
3+
version = "1.30.0"
44
edition = "2021"
55
authors = ["SCUD Team"]
66
description = "Fast, simple task master for AI-driven development"
@@ -32,6 +32,7 @@ dialoguer = "0.11" # Interactive CLI prompts
3232
atty = "0.2" # TTY detection for interactive mode
3333
futures = "0.3" # Async utilities for parallel execution
3434
webbrowser = "1.0" # Cross-platform browser opening
35+
uuid = { version = "1", features = ["v4"] } # UUID generation for task IDs
3536

3637
[dev-dependencies]
3738
tempfile = "3.8" # Temporary directories for tests

scud-cli/README.md

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,20 @@ This is a high-performance Rust rewrite of the SCUD task management system. It r
1717
scud (Rust Binary)
1818
├── Core Commands (No AI - Instant)
1919
│ ├── init # Initialize .scud/ and install agents
20-
│ ├── tags # List tags
21-
│ ├── use-tag # Switch active tag
20+
│ ├── tags # List tags or set active tag
2221
│ ├── list # List tasks with filters
2322
│ ├── view # Open interactive HTML viewer in browser
2423
│ ├── show # Show task details
2524
│ ├── set-status # Update task status
26-
│ ├── next # Find next available task (--claim for dynamic-wave)
25+
│ ├── next # Find next available task
2726
│ ├── stats # Show statistics
2827
│ └── doctor # [EXPERIMENTAL] Diagnose stuck states
2928
30-
├── AI Commands (Direct Anthropic API)
29+
├── AI Commands (Direct LLM API)
3130
│ ├── parse-prd # Parse PRD markdown into tasks
3231
│ ├── analyze-complexity # Analyze task complexity
3332
│ ├── expand # Break down complex tasks
34-
│ └── research # AI-powered research
33+
│ └── reanalyze-deps # Analyze cross-tag dependencies
3534
3635
└── Storage (SCG)
3736
└── .scud/tasks/tasks.scg
@@ -96,7 +95,7 @@ scud init
9695
scud tags
9796

9897
# Switch to a tag
99-
scud use-tag auth
98+
scud tags auth
10099

101100
# List tasks
102101
scud list
@@ -118,20 +117,6 @@ scud stats
118117
scud view
119118
```
120119

121-
### [EXPERIMENTAL] Dynamic-Wave Mode
122-
123-
Dynamic-wave mode allows agents to auto-claim tasks and maintain workflow health:
124-
125-
```bash
126-
# Find and auto-claim the next available task
127-
scud next --claim --name agent-1
128-
129-
# Release all tasks claimed by an agent
130-
scud next --release --name agent-1
131-
```
132-
133-
**IMPORTANT:** When using `--claim`, agents MUST run `scud set-status <id> done` when finishing a task. This ensures dependent tasks become unblocked.
134-
135120
### [EXPERIMENTAL] Doctor Command
136121

137122
Diagnose stuck workflow states:
@@ -170,10 +155,28 @@ scud analyze-complexity --task 5 # Specific task
170155
scud expand 7 # Specific task
171156
scud expand --all # All tasks >13 complexity
172157

173-
# Research a topic
174-
scud research "OAuth 2.0 best practices"
158+
# Reanalyze cross-tag dependencies
159+
scud reanalyze-deps --all-tags
175160
```
176161

162+
### UUID Task IDs
163+
164+
For integration with external tools that expect UUID task IDs (like Descartes):
165+
166+
```bash
167+
# Generate tasks with UUID IDs instead of sequential numbers
168+
scud parse requirements.md --tag myproject --id-format uuid
169+
```
170+
171+
This generates tasks with 32-character UUID identifiers (e.g., `a1b2c3d4e5f6789012345678901234ab`) instead of sequential numbers (`1`, `2`, `3`).
172+
173+
**Key behaviors:**
174+
- Sequential IDs remain the default for backwards compatibility
175+
- The ID format is stored in the phase metadata and inherited during expansion
176+
- Subtasks also get UUID IDs when the parent phase uses UUID format
177+
- Long UUIDs are truncated in CLI output (`a1b2c3d4...`) for readability
178+
- The `scud show` command displays the full UUID
179+
177180
## Performance Comparison
178181

179182
| Operation | Old (task-master) | New (Rust) | Improvement |
@@ -249,6 +252,7 @@ struct Task {
249252
struct Phase {
250253
name: String,
251254
tasks: Vec<Task>,
255+
id_format: IdFormat, // sequential (default) or uuid
252256
}
253257
```
254258

@@ -274,7 +278,7 @@ Located in `src/llm/prompts.rs`:
274278
- `parse_prd()` - Converts markdown to structured tasks
275279
- `analyze_complexity()` - Scores task difficulty
276280
- `expand_task()` - Breaks down complex tasks
277-
- `research_topic()` - AI research assistant
281+
- `reanalyze_dependencies()` - Cross-tag dependency analysis
278282

279283
## Integration with SCUD
280284

@@ -303,7 +307,7 @@ scud-cli/
303307
│ │ ├── parse_prd.rs
304308
│ │ ├── analyze_complexity.rs
305309
│ │ ├── expand.rs
306-
│ │ └── research.rs
310+
│ │ └── reanalyze_deps.rs
307311
│ ├── models/
308312
│ │ ├── task.rs
309313
│ │ └── phase.rs

scud-cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "scud-task",
3-
"version": "1.28.1",
3+
"version": "1.30.0",
44
"description": "Fast, simple task master for AI-driven development - BMAD-TM workflow automation",
55
"main": "index.js",
66
"bin": {

scud-cli/src/commands/ai/expand.rs

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
77
use serde::Deserialize;
88
use std::path::PathBuf;
99
use std::sync::Arc;
10+
use uuid::Uuid;
1011

1112
use crate::llm::{LLMClient, Prompts};
12-
use crate::models::{Priority, Task, TaskStatus};
13+
use crate::models::{IdFormat, Priority, Task, TaskStatus};
1314
use crate::storage::Storage;
1415

1516
#[derive(Debug, Deserialize)]
@@ -289,10 +290,29 @@ pub async fn run(
289290
.get_mut(tag)
290291
.expect("Tag should exist since task came from it");
291292

293+
// Check if this phase uses UUID format
294+
let use_uuid = epic.id_format == IdFormat::Uuid;
295+
292296
// Create subtasks
297+
// For UUID format, pre-generate all IDs so we can map dependencies
298+
let subtask_ids: Vec<String> = if use_uuid {
299+
expansion
300+
.expanded_tasks
301+
.iter()
302+
.map(|_| Uuid::new_v4().to_string().replace("-", ""))
303+
.collect()
304+
} else {
305+
expansion
306+
.expanded_tasks
307+
.iter()
308+
.enumerate()
309+
.map(|(idx, _)| format!("{}.{}", parent_id, idx + 1))
310+
.collect()
311+
};
312+
293313
let mut new_subtask_ids = Vec::new();
294314
for (idx, expanded) in expansion.expanded_tasks.iter().enumerate() {
295-
let new_id = format!("{}.{}", parent_id, idx + 1);
315+
let new_id = subtask_ids[idx].clone();
296316

297317
let priority = if !expanded.priority.is_empty() {
298318
match expanded.priority.to_lowercase().as_str() {
@@ -313,18 +333,21 @@ pub async fn run(
313333
new_task.complexity = 0;
314334
new_task.parent_id = Some(parent_id.clone());
315335

316-
// Map dependency references to nested IDs
336+
// Map dependency references to actual subtask IDs
337+
// LLM returns dependencies as 1-indexed references to other subtasks
317338
new_task.dependencies = expanded
318339
.dependencies
319340
.iter()
320341
.filter_map(|dep| {
321342
if let Ok(dep_idx) = dep.parse::<usize>() {
343+
// Map 1-indexed reference to actual subtask ID
322344
if dep_idx > 0 && dep_idx <= idx + 1 {
323-
Some(format!("{}.{}", parent_id, dep_idx))
345+
Some(subtask_ids[dep_idx - 1].clone())
324346
} else {
325347
None
326348
}
327349
} else {
350+
// Already a full ID reference
328351
Some(dep.clone())
329352
}
330353
})

scud-cli/src/commands/ai/parse_prd.rs

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ use colored::Colorize;
33
use indicatif::{ProgressBar, ProgressStyle};
44
use serde::Deserialize;
55
use std::path::{Path, PathBuf};
6+
use uuid::Uuid;
67

78
use crate::llm::{LLMClient, Prompts};
8-
use crate::models::{Phase, Priority, Task};
9+
use crate::models::{IdFormat, Phase, Priority, Task};
910
use crate::storage::Storage;
1011

1112
#[derive(Debug, Deserialize)]
@@ -25,6 +26,7 @@ pub async fn run(
2526
num_tasks: u32,
2627
append: bool,
2728
no_guidance: bool,
29+
id_format: &str,
2830
) -> Result<()> {
2931
let storage = Storage::new(project_root.clone());
3032

@@ -85,7 +87,15 @@ pub async fn run(
8587
// Check if other phases exist for cross-tag dependency hint
8688
let other_phases: Vec<_> = all_tasks.keys().filter(|k| *k != tag).cloned().collect();
8789

88-
// Determine starting ID based on append mode
90+
// Parse ID format from CLI argument
91+
let use_uuid = id_format == "uuid";
92+
let parsed_id_format = if use_uuid {
93+
IdFormat::Uuid
94+
} else {
95+
IdFormat::Sequential
96+
};
97+
98+
// Determine starting ID based on append mode (only used for sequential format)
8999
let start_id = if append && all_tasks.contains_key(tag) {
90100
let existing = all_tasks.get(tag).unwrap();
91101
existing.tasks.len() + 1
@@ -99,19 +109,41 @@ pub async fn run(
99109
"{}",
100110
format!("📎 Appending to existing task group '{}'...", tag).cyan()
101111
);
102-
all_tasks.get(tag).unwrap().clone()
112+
let existing = all_tasks.get(tag).unwrap().clone();
113+
// When appending, warn if id_format doesn't match existing
114+
if existing.id_format != parsed_id_format {
115+
println!(
116+
"{}",
117+
format!(
118+
"⚠ Ignoring --id-format: existing phase uses {} format",
119+
existing.id_format.as_str()
120+
)
121+
.yellow()
122+
);
123+
}
124+
existing
103125
} else {
104126
if all_tasks.contains_key(tag) {
105127
println!(
106128
"{}",
107129
format!("⚠ Task group '{}' already exists. Replacing...", tag).yellow()
108130
);
109131
}
110-
Phase::new(tag.to_string())
132+
let mut new_phase = Phase::new(tag.to_string());
133+
new_phase.id_format = parsed_id_format;
134+
new_phase
111135
};
112136

137+
// Determine actual id_format to use (from phase, which may be inherited when appending)
138+
let use_uuid = group.id_format == IdFormat::Uuid;
139+
113140
for (idx, parsed) in parsed_tasks.iter().enumerate() {
114-
let task_id = (start_id + idx).to_string();
141+
let task_id = if use_uuid {
142+
// Generate UUID v4 as 32-character hex string (no dashes)
143+
Uuid::new_v4().to_string().replace("-", "")
144+
} else {
145+
(start_id + idx).to_string()
146+
};
115147

116148
let priority = match parsed.priority.to_lowercase().as_str() {
117149
"high" => Priority::High,

0 commit comments

Comments
 (0)