Implementer synops-tasks CLI-verktøy (oppgave 21.12)
Parser tasks.md og viser formatert oppgavestatus med filtrering på fase (--phase N) og status (--status todo|done|blocked|inprogress|question). Viser sammendrag med antall per status og fremdriftsbar. Rent filbasert verktøy — ingen database nødvendig. Finner tasks.md automatisk fra arbeidsdir eller absolutt sti. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
590afabc23
commit
da2155e34e
4 changed files with 282 additions and 3 deletions
3
tasks.md
3
tasks.md
|
|
@ -255,8 +255,7 @@ kaller dem direkte. Samme verktøy, to brukere.
|
||||||
|
|
||||||
- [x] 21.10 `synops-context`: Hent kontekst for en samtale. Input: `--communication-id <uuid>`. Output: markdown med spec, deltakere, historikk, relaterte noder.
|
- [x] 21.10 `synops-context`: Hent kontekst for en samtale. Input: `--communication-id <uuid>`. Output: markdown med spec, deltakere, historikk, relaterte noder.
|
||||||
- [x] 21.11 `synops-search`: Fulltekstsøk i grafen. Input: `<query> [--kind <node_kind>] [--limit N]`. Output: matchende noder med utdrag.
|
- [x] 21.11 `synops-search`: Fulltekstsøk i grafen. Input: `<query> [--kind <node_kind>] [--limit N]`. Output: matchende noder med utdrag.
|
||||||
- [~] 21.12 `synops-tasks`: Parse tasks.md og vis status. Input: `[--phase N] [--status todo|done|blocked]`. Output: formatert oppgaveliste.
|
- [x] 21.12 `synops-tasks`: Parse tasks.md og vis status. Input: `[--phase N] [--status todo|done|blocked]`. Output: formatert oppgaveliste.
|
||||||
> Påbegynt: 2026-03-18T10:10
|
|
||||||
- [ ] 21.13 `synops-feature-status`: Sjekk feature-status. Input: `<feature_key>`. Output: spec-sammendrag, oppgavestatus, nylige commits, ubesvart feedback.
|
- [ ] 21.13 `synops-feature-status`: Sjekk feature-status. Input: `<feature_key>`. Output: spec-sammendrag, oppgavestatus, nylige commits, ubesvart feedback.
|
||||||
- [ ] 21.14 `synops-node`: Hent/vis en node med edges. Input: `<uuid> [--depth N] [--format json|md]`. Output: node-data med edges.
|
- [ ] 21.14 `synops-node`: Hent/vis en node med edges. Input: `<uuid> [--depth N] [--format json|md]`. Output: node-data med edges.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ eller maskinrommet-API. Ligger i PATH via symlink eller direkte kall.
|
||||||
| `synops-prune` | Opprydding av gamle CAS-filer (TTL + disk-nødventil) | Ferdig |
|
| `synops-prune` | Opprydding av gamle CAS-filer (TTL + disk-nødventil) | Ferdig |
|
||||||
| `synops-context` | Hent kontekst for en samtale (deltakere, historikk, spec, relaterte noder) | Ferdig |
|
| `synops-context` | Hent kontekst for en samtale (deltakere, historikk, spec, relaterte noder) | Ferdig |
|
||||||
| `synops-search` | Fulltekstsøk i noder (title + content, norsk tsvector) | Ferdig |
|
| `synops-search` | Fulltekstsøk i noder (title + content, norsk tsvector) | Ferdig |
|
||||||
|
| `synops-tasks` | Parse tasks.md og vis oppgavestatus (filtrering på fase/status) | Ferdig |
|
||||||
|
|
||||||
## Konvensjoner
|
## Konvensjoner
|
||||||
- Navnekonvensjon: `synops-<verb>` (f.eks. `synops-context`)
|
- Navnekonvensjon: `synops-<verb>` (f.eks. `synops-context`)
|
||||||
|
|
@ -31,7 +32,7 @@ Ref: `docs/infra/agent_api.md`
|
||||||
|
|
||||||
- ~~`synops-context`~~ — implementert (se tabell over)
|
- ~~`synops-context`~~ — implementert (se tabell over)
|
||||||
- ~~`synops-search`~~ — implementert (se tabell over)
|
- ~~`synops-search`~~ — implementert (se tabell over)
|
||||||
- `synops-tasks [--phase N] [--status S]` — oppgavestatus fra tasks.md
|
- ~~`synops-tasks [--phase N] [--status S]`~~ — implementert (se tabell over)
|
||||||
- `synops-feature-status <key>` — implementeringsstatus for en feature
|
- `synops-feature-status <key>` — implementeringsstatus for en feature
|
||||||
- ~~`synops-respond`~~ — implementert (se tabell over)
|
- ~~`synops-respond`~~ — implementert (se tabell over)
|
||||||
- `synops-update-spec <node_id>` — oppdater spec-node (stdin)
|
- `synops-update-spec <node_id>` — oppdater spec-node (stdin)
|
||||||
|
|
|
||||||
12
tools/synops-tasks/Cargo.toml
Normal file
12
tools/synops-tasks/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "synops-tasks"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "synops-tasks"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
regex = "1"
|
||||||
267
tools/synops-tasks/src/main.rs
Normal file
267
tools/synops-tasks/src/main.rs
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
// synops-tasks — Parse tasks.md og vis oppgavestatus.
|
||||||
|
//
|
||||||
|
// Leser tasks.md fra repo-roten og parser oppgaver med status,
|
||||||
|
// fase og detaljer. Støtter filtrering på fase og status.
|
||||||
|
//
|
||||||
|
// Output: formatert oppgaveliste til stdout.
|
||||||
|
//
|
||||||
|
// Ref: docs/retninger/unix_filosofi.md
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use regex::Regex;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process;
|
||||||
|
|
||||||
|
/// Parse tasks.md og vis oppgavestatus.
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "synops-tasks", about = "Vis oppgavestatus fra tasks.md")]
|
||||||
|
struct Cli {
|
||||||
|
/// Filtrer på fase (f.eks. 21 for "Fase 21")
|
||||||
|
#[arg(long)]
|
||||||
|
phase: Option<u32>,
|
||||||
|
|
||||||
|
/// Filtrer på status: todo, done, blocked, inprogress, question, all
|
||||||
|
#[arg(long, default_value = "all")]
|
||||||
|
status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Task {
|
||||||
|
phase: u32,
|
||||||
|
phase_title: String,
|
||||||
|
id: String,
|
||||||
|
title: String,
|
||||||
|
status: TaskStatus,
|
||||||
|
details: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
enum TaskStatus {
|
||||||
|
Todo,
|
||||||
|
InProgress,
|
||||||
|
Done,
|
||||||
|
Question,
|
||||||
|
Blocked,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TaskStatus {
|
||||||
|
fn symbol(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
TaskStatus::Todo => "[ ]",
|
||||||
|
TaskStatus::InProgress => "[~]",
|
||||||
|
TaskStatus::Done => "[x]",
|
||||||
|
TaskStatus::Question => "[?]",
|
||||||
|
TaskStatus::Blocked => "[!]",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
TaskStatus::Todo => "Klar",
|
||||||
|
TaskStatus::InProgress => "Pågår",
|
||||||
|
TaskStatus::Done => "Ferdig",
|
||||||
|
TaskStatus::Question => "Spørsmål",
|
||||||
|
TaskStatus::Blocked => "Blokkert",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches_filter(&self, filter: &str) -> bool {
|
||||||
|
match filter {
|
||||||
|
"all" => true,
|
||||||
|
"todo" => *self == TaskStatus::Todo,
|
||||||
|
"done" => *self == TaskStatus::Done,
|
||||||
|
"blocked" => *self == TaskStatus::Blocked || *self == TaskStatus::Question,
|
||||||
|
"inprogress" => *self == TaskStatus::InProgress,
|
||||||
|
"question" => *self == TaskStatus::Question,
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_tasks_md() -> Result<PathBuf, String> {
|
||||||
|
// Prøv relativ sti først (repo-rot), deretter absolutt
|
||||||
|
let candidates = [
|
||||||
|
PathBuf::from("tasks.md"),
|
||||||
|
PathBuf::from("/home/vegard/synops/tasks.md"),
|
||||||
|
];
|
||||||
|
for path in &candidates {
|
||||||
|
if path.exists() {
|
||||||
|
return Ok(path.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err("Fant ikke tasks.md (prøvde ./ og /home/vegard/synops/)".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_tasks(content: &str) -> Vec<Task> {
|
||||||
|
let phase_re = Regex::new(r"^## Fase (\d+):\s*(.+)$").unwrap();
|
||||||
|
let task_re = Regex::new(r"^- \[(.)\] (\d+\.\d+)\s+(.+)$").unwrap();
|
||||||
|
let detail_re = Regex::new(r"^ > (.+)$").unwrap();
|
||||||
|
|
||||||
|
let mut tasks = Vec::new();
|
||||||
|
let mut current_phase: Option<(u32, String)> = None;
|
||||||
|
|
||||||
|
for line in content.lines() {
|
||||||
|
if let Some(caps) = phase_re.captures(line) {
|
||||||
|
let num: u32 = caps[1].parse().unwrap_or(0);
|
||||||
|
let title = caps[2].trim().to_string();
|
||||||
|
current_phase = Some((num, title));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(caps) = task_re.captures(line) {
|
||||||
|
let status_char = &caps[1];
|
||||||
|
let id = caps[2].to_string();
|
||||||
|
let title = caps[3].to_string();
|
||||||
|
|
||||||
|
let status = match status_char {
|
||||||
|
" " => TaskStatus::Todo,
|
||||||
|
"~" => TaskStatus::InProgress,
|
||||||
|
"x" => TaskStatus::Done,
|
||||||
|
"?" => TaskStatus::Question,
|
||||||
|
"!" => TaskStatus::Blocked,
|
||||||
|
_ => TaskStatus::Todo,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (phase, phase_title) = current_phase
|
||||||
|
.clone()
|
||||||
|
.unwrap_or((0, "Ukjent".to_string()));
|
||||||
|
|
||||||
|
tasks.push(Task {
|
||||||
|
phase,
|
||||||
|
phase_title,
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
status,
|
||||||
|
details: Vec::new(),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(caps) = detail_re.captures(line) {
|
||||||
|
if let Some(task) = tasks.last_mut() {
|
||||||
|
task.details.push(caps[1].to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
let valid_statuses = ["all", "todo", "done", "blocked", "inprogress", "question"];
|
||||||
|
if !valid_statuses.contains(&cli.status.as_str()) {
|
||||||
|
eprintln!(
|
||||||
|
"Ugyldig status «{}». Gyldige verdier: {}",
|
||||||
|
cli.status,
|
||||||
|
valid_statuses.join(", ")
|
||||||
|
);
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = match find_tasks_md() {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Feil: {e}");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let content = match std::fs::read_to_string(&path) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Kunne ikke lese {}: {e}", path.display());
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let tasks = parse_tasks(&content);
|
||||||
|
|
||||||
|
// Filtrer
|
||||||
|
let filtered: Vec<&Task> = tasks
|
||||||
|
.iter()
|
||||||
|
.filter(|t| {
|
||||||
|
if let Some(phase) = cli.phase {
|
||||||
|
if t.phase != phase {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.status.matches_filter(&cli.status)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if filtered.is_empty() {
|
||||||
|
let mut msg = String::from("Ingen oppgaver funnet");
|
||||||
|
if let Some(phase) = cli.phase {
|
||||||
|
msg.push_str(&format!(" i fase {phase}"));
|
||||||
|
}
|
||||||
|
if cli.status != "all" {
|
||||||
|
msg.push_str(&format!(" med status «{}»", cli.status));
|
||||||
|
}
|
||||||
|
println!("{msg}.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grupper etter fase
|
||||||
|
let mut current_phase: Option<u32> = None;
|
||||||
|
|
||||||
|
// Sammendrag
|
||||||
|
let total = filtered.len();
|
||||||
|
let done = filtered.iter().filter(|t| t.status == TaskStatus::Done).count();
|
||||||
|
let todo = filtered.iter().filter(|t| t.status == TaskStatus::Todo).count();
|
||||||
|
let progress = filtered
|
||||||
|
.iter()
|
||||||
|
.filter(|t| t.status == TaskStatus::InProgress)
|
||||||
|
.count();
|
||||||
|
let blocked = filtered
|
||||||
|
.iter()
|
||||||
|
.filter(|t| t.status == TaskStatus::Blocked || t.status == TaskStatus::Question)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
if let Some(phase) = cli.phase {
|
||||||
|
println!("# Fase {phase}\n");
|
||||||
|
} else {
|
||||||
|
println!("# Oppgavestatus\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Totalt: {total} | Ferdig: {done} | Klar: {todo} | Pågår: {progress} | Blokkert: {blocked}\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
for task in &filtered {
|
||||||
|
if current_phase != Some(task.phase) {
|
||||||
|
current_phase = Some(task.phase);
|
||||||
|
println!("## Fase {}: {}\n", task.phase, task.phase_title);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
" {} {} {}",
|
||||||
|
task.status.symbol(),
|
||||||
|
task.id,
|
||||||
|
task.title
|
||||||
|
);
|
||||||
|
|
||||||
|
// Vis statuslabel for ikke-ferdige oppgaver
|
||||||
|
if task.status != TaskStatus::Done {
|
||||||
|
println!(" Status: {}", task.status.label());
|
||||||
|
}
|
||||||
|
|
||||||
|
for detail in &task.details {
|
||||||
|
println!(" > {detail}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fremdriftsbar for filtrert sett
|
||||||
|
if total > 0 {
|
||||||
|
let pct = (done as f64 / total as f64 * 100.0) as u32;
|
||||||
|
let bar_width = 30;
|
||||||
|
let filled = (pct as usize * bar_width) / 100;
|
||||||
|
let empty = bar_width - filled;
|
||||||
|
println!(
|
||||||
|
"\nFremdrift: [{}{}] {pct}% ({done}/{total})",
|
||||||
|
"█".repeat(filled),
|
||||||
|
"░".repeat(empty)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue