diff --git a/crates/skilllite-evolution/src/feedback.rs b/crates/skilllite-evolution/src/feedback.rs index 05014f42..e71d6ebd 100644 --- a/crates/skilllite-evolution/src/feedback.rs +++ b/crates/skilllite-evolution/src/feedback.rs @@ -1,7 +1,7 @@ //! Evolution feedback collection and evaluation system (EVO-1). use crate::Result; -use rusqlite::{params, Connection}; +use rusqlite::{params, Connection, OptionalExtension}; use std::fs; use std::path::Path; @@ -163,7 +163,7 @@ pub fn ensure_evolution_tables(conn: &Connection) -> Result<()> { id INTEGER PRIMARY KEY AUTOINCREMENT, proposal_id TEXT NOT NULL UNIQUE, source TEXT NOT NULL, - dedupe_key TEXT NOT NULL UNIQUE, + dedupe_key TEXT NOT NULL, scope_json TEXT NOT NULL, risk_level TEXT NOT NULL, roi_score REAL NOT NULL DEFAULT 0.0, @@ -184,6 +184,7 @@ pub fn ensure_evolution_tables(conn: &Connection) -> Result<()> { CREATE INDEX IF NOT EXISTS idx_evo_log_ts ON evolution_log(ts); CREATE INDEX IF NOT EXISTS idx_evo_backlog_status_roi ON evolution_backlog(status, roi_score DESC); CREATE INDEX IF NOT EXISTS idx_evo_backlog_created_at ON evolution_backlog(created_at); + CREATE INDEX IF NOT EXISTS idx_evo_backlog_dedupe_status ON evolution_backlog(dedupe_key, status); "#, )?; // Backward-compatible migration: add column for existing DBs (ignored if column exists). @@ -204,6 +205,71 @@ pub fn ensure_evolution_tables(conn: &Connection) -> Result<()> { "CREATE INDEX IF NOT EXISTS idx_decisions_seq ON decisions(tool_sequence_key)", [], ); + migrate_evolution_backlog_drop_dedupe_unique(conn)?; + Ok(()) +} + +/// Older schemas enforced `dedupe_key TEXT NOT NULL UNIQUE`, which prevents inserting a +/// fresh active proposal after an `executed` row for the same key. Rebuild without that UNIQUE. +fn migrate_evolution_backlog_drop_dedupe_unique(conn: &Connection) -> Result<()> { + let table_sql: Option = conn + .query_row( + "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'evolution_backlog'", + [], + |row| row.get(0), + ) + .optional()?; + let Some(table_sql) = table_sql else { + return Ok(()); + }; + // Match the historical column constraint text (new installs already omit UNIQUE). + let needs_rebuild = table_sql + .to_ascii_lowercase() + .contains("dedupe_key text not null unique"); + if !needs_rebuild { + let _ = conn.execute( + "CREATE INDEX IF NOT EXISTS idx_evo_backlog_dedupe_status ON evolution_backlog(dedupe_key, status)", + [], + ); + return Ok(()); + } + + conn.execute_batch( + r#" + CREATE TABLE evolution_backlog_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + proposal_id TEXT NOT NULL UNIQUE, + source TEXT NOT NULL, + dedupe_key TEXT NOT NULL, + scope_json TEXT NOT NULL, + risk_level TEXT NOT NULL, + roi_score REAL NOT NULL DEFAULT 0.0, + expected_gain REAL NOT NULL DEFAULT 0.0, + effort REAL NOT NULL DEFAULT 1.0, + acceptance_criteria TEXT NOT NULL DEFAULT '[]', + status TEXT NOT NULL, + acceptance_status TEXT NOT NULL DEFAULT 'pending', + note TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + INSERT INTO evolution_backlog_new ( + id, proposal_id, source, dedupe_key, scope_json, risk_level, roi_score, + expected_gain, effort, acceptance_criteria, status, acceptance_status, + note, created_at, updated_at + ) + SELECT + id, proposal_id, source, dedupe_key, scope_json, risk_level, roi_score, + expected_gain, effort, acceptance_criteria, status, acceptance_status, + note, created_at, updated_at + FROM evolution_backlog; + DROP TABLE evolution_backlog; + ALTER TABLE evolution_backlog_new RENAME TO evolution_backlog; + CREATE INDEX IF NOT EXISTS idx_evo_backlog_status_roi ON evolution_backlog(status, roi_score DESC); + CREATE INDEX IF NOT EXISTS idx_evo_backlog_created_at ON evolution_backlog(created_at); + CREATE INDEX IF NOT EXISTS idx_evo_backlog_dedupe_status ON evolution_backlog(dedupe_key, status); + "#, + )?; Ok(()) } diff --git a/crates/skilllite-evolution/src/lib.rs b/crates/skilllite-evolution/src/lib.rs index d5cde7c4..737198dc 100644 --- a/crates/skilllite-evolution/src/lib.rs +++ b/crates/skilllite-evolution/src/lib.rs @@ -456,6 +456,201 @@ mod lib_tests { let _ = std::fs::remove_dir_all(&root); } + #[test] + fn enqueue_user_capability_evolution_requeues_after_executed() { + let root = + std::env::temp_dir().join(format!("skilllite-evo-test-{}", uuid::Uuid::new_v4())); + let conn = feedback::open_evolution_db(&root).expect("open db"); + let first_id = enqueue_user_capability_evolution( + &conn, + "weather", + "failure", + "first authorization", + ) + .expect("first enqueue"); + conn.execute( + "UPDATE evolution_backlog + SET status = 'executed', acceptance_status = 'met', updated_at = datetime('now') + WHERE proposal_id = ?1", + rusqlite::params![first_id], + ) + .expect("mark executed"); + + let second_id = enqueue_user_capability_evolution( + &conn, + "weather", + "failure", + "re-authorize after executed", + ) + .expect("second enqueue"); + + assert_ne!( + second_id, first_id, + "re-queue after executed must mint a new active proposal identity" + ); + let (status, active_count, total_count): (String, i64, i64) = conn + .query_row( + "SELECT + (SELECT status FROM evolution_backlog WHERE proposal_id = ?1), + (SELECT COUNT(*) FROM evolution_backlog + WHERE dedupe_key = 'user_capability:weather:failure' AND status != 'executed'), + (SELECT COUNT(*) FROM evolution_backlog + WHERE dedupe_key = 'user_capability:weather:failure')", + rusqlite::params![second_id], + |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), + ) + .expect("read re-queued state"); + assert_eq!(status, "queued"); + assert_eq!(active_count, 1); + assert_eq!(total_count, 2); + let _ = std::fs::remove_dir_all(&root); + } + + #[test] + fn migrate_evolution_backlog_allows_requeue_with_legacy_unique_dedupe() { + let root = + std::env::temp_dir().join(format!("skilllite-evo-test-{}", uuid::Uuid::new_v4())); + std::fs::create_dir_all(&root).expect("mkdir"); + let db_path = root.join("feedback.sqlite"); + { + let conn = Connection::open(&db_path).expect("open legacy db"); + conn.execute_batch( + r#" + CREATE TABLE evolution_backlog ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + proposal_id TEXT NOT NULL UNIQUE, + source TEXT NOT NULL, + dedupe_key TEXT NOT NULL UNIQUE, + scope_json TEXT NOT NULL, + risk_level TEXT NOT NULL, + roi_score REAL NOT NULL DEFAULT 0.0, + expected_gain REAL NOT NULL DEFAULT 0.0, + effort REAL NOT NULL DEFAULT 1.0, + acceptance_criteria TEXT NOT NULL DEFAULT '[]', + status TEXT NOT NULL, + acceptance_status TEXT NOT NULL DEFAULT 'pending', + note TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + INSERT INTO evolution_backlog + (proposal_id, source, dedupe_key, scope_json, risk_level, roi_score, + expected_gain, effort, acceptance_criteria, status, acceptance_status, note) + VALUES + ('proposal_legacy_executed', 'passive', 'user_capability:weather:failure', + '{}', 'medium', 0.4, 0.75, 1.8, '[]', 'executed', 'met', 'legacy'); + "#, + ) + .expect("seed legacy unique schema"); + } + + let conn = feedback::open_evolution_db(&root).expect("migrate on open"); + let table_sql: String = conn + .query_row( + "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'evolution_backlog'", + [], + |row| row.get(0), + ) + .expect("read schema"); + assert!( + !table_sql + .to_ascii_lowercase() + .contains("dedupe_key text not null unique"), + "migration must drop global UNIQUE on dedupe_key: {table_sql}" + ); + + let requeued_id = enqueue_user_capability_evolution( + &conn, + "weather", + "failure", + "after legacy migrate", + ) + .expect("enqueue after migrate"); + let active: i64 = conn + .query_row( + "SELECT COUNT(*) FROM evolution_backlog + WHERE proposal_id = ?1 AND status = 'queued'", + rusqlite::params![requeued_id], + |row| row.get(0), + ) + .expect("count active"); + assert_eq!(active, 1); + let _ = std::fs::remove_dir_all(&root); + } + + #[test] + fn coordinate_attaches_status_to_persisted_id_after_requeue() { + let _guard = EVO_LOCK.lock().expect("evo lock"); + let root = + std::env::temp_dir().join(format!("skilllite-evo-test-{}", uuid::Uuid::new_v4())); + let conn = feedback::open_evolution_db(&root).expect("open db"); + let first = build_proposal( + ProposalSource::Passive, + EvolutionScope { + skills: true, + skill_action: SkillAction::Generate, + ..Default::default() + }, + ProposalRiskLevel::Low, + 0.6, + 1.0, + vec!["criteria".into()], + ); + let dedupe_key = first.dedupe_key.clone(); + conn.execute( + "INSERT INTO evolution_backlog + (proposal_id, source, dedupe_key, scope_json, risk_level, roi_score, expected_gain, effort, acceptance_criteria, status, acceptance_status, note) + VALUES (?1, 'passive', ?2, '{}', 'low', 0.6, 0.6, 1.0, '[\"criteria\"]', 'executed', 'met', 'done')", + rusqlite::params![first.proposal_id, dedupe_key], + ) + .expect("seed executed history"); + + let next = build_proposal( + ProposalSource::Passive, + EvolutionScope { + skills: true, + skill_action: SkillAction::Generate, + ..Default::default() + }, + ProposalRiskLevel::Low, + 0.7, + 1.0, + vec!["criteria".into()], + ); + assert_eq!(next.dedupe_key, dedupe_key); + let minted_id = next.proposal_id.clone(); + let decision = coordinate_proposals_with_config( + &conn, + vec![next], + true, + EvolutionCoordinatorConfig { + policy_runtime_enabled: false, + auto_execute_low_risk: true, + deny_critical: true, + risk_budget: EvolutionRiskBudget { + low_per_day: 5, + medium_per_day: 5, + high_per_day: 5, + critical_per_day: 1, + }, + }, + ) + .expect("coordinate"); + let CoordinatorDecision::Execute(selected) = decision else { + panic!("expected Execute after re-queue"); + }; + assert_eq!(selected.proposal_id, minted_id); + let status: String = conn + .query_row( + "SELECT status FROM evolution_backlog WHERE proposal_id = ?1", + rusqlite::params![minted_id], + |row| row.get(0), + ) + .expect("persisted status"); + assert_eq!(status, "executing"); + let _ = std::fs::remove_dir_all(&root); + } + fn seed_backlog_row(conn: &Connection, proposal_id: &str, updated_at: &str) { conn.execute( "INSERT INTO evolution_backlog diff --git a/crates/skilllite-evolution/src/scope.rs b/crates/skilllite-evolution/src/scope.rs index fe796823..4280f6c2 100644 --- a/crates/skilllite-evolution/src/scope.rs +++ b/crates/skilllite-evolution/src/scope.rs @@ -478,25 +478,8 @@ fn upsert_backlog_proposal( ) -> Result<()> { let scope_json = serde_json::to_string(&proposal.scope)?; let acceptance_criteria = serde_json::to_string(&proposal.acceptance_criteria)?; - conn.execute( - "INSERT OR IGNORE INTO evolution_backlog - (proposal_id, source, dedupe_key, scope_json, risk_level, roi_score, expected_gain, effort, acceptance_criteria, status, note) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", - params![ - proposal.proposal_id, - proposal.source.as_str(), - proposal.dedupe_key, - scope_json, - proposal.risk_level.as_str(), - proposal.roi_score as f64, - proposal.expected_gain as f64, - proposal.effort as f64, - acceptance_criteria, - status, - note, - ], - )?; - conn.execute( + // Soft-dedupe among active rows only. Executed history must not block a fresh insert. + let updated = conn.execute( "UPDATE evolution_backlog SET roi_score = ?1, expected_gain = ?2, @@ -512,6 +495,26 @@ fn upsert_backlog_proposal( proposal.dedupe_key, ], )?; + if updated == 0 { + conn.execute( + "INSERT INTO evolution_backlog + (proposal_id, source, dedupe_key, scope_json, risk_level, roi_score, expected_gain, effort, acceptance_criteria, status, note) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + params![ + proposal.proposal_id, + proposal.source.as_str(), + proposal.dedupe_key, + scope_json, + proposal.risk_level.as_str(), + proposal.roi_score as f64, + proposal.expected_gain as f64, + proposal.effort as f64, + acceptance_criteria, + status, + note, + ], + )?; + } Ok(()) } @@ -969,9 +972,15 @@ pub(crate) fn coordinate_proposals_with_config( upsert_backlog_proposal(conn, proposal, "queued", "Proposal collected")?; } proposals.sort_by(|a, b| b.roi_score.total_cmp(&a.roi_score)); - let Some(selected) = proposals.into_iter().next() else { + let Some(mut selected) = proposals.into_iter().next() else { return Ok(CoordinatorDecision::NoCandidate); }; + // Soft-dedupe may keep an older proposal_id on the active row; attach status to that id. + if let Some(persisted_id) = + latest_non_executed_proposal_id_by_dedupe(conn, &selected.dedupe_key)? + { + selected.proposal_id = persisted_id; + } if force { set_backlog_status( conn, diff --git a/tasks/TASK-2026-084-evolution-backlog-requeue-after-executed/CONTEXT.md b/tasks/TASK-2026-084-evolution-backlog-requeue-after-executed/CONTEXT.md new file mode 100644 index 00000000..6fdbb37f --- /dev/null +++ b/tasks/TASK-2026-084-evolution-backlog-requeue-after-executed/CONTEXT.md @@ -0,0 +1,38 @@ +# Technical Context + +## Current State + +- Relevant crates/files: + - `crates/skilllite-evolution/src/feedback.rs` — schema `evolution_backlog.dedupe_key TEXT NOT NULL UNIQUE` + - `crates/skilllite-evolution/src/scope.rs` — `upsert_backlog_proposal`, `enqueue_user_capability_evolution`, `coordinate_proposals*` + - `crates/skilllite-evolution/src/run.rs` — forced proposal load / recovery → `NoScope` when missing +- Current behavior: + - INSERT OR IGNORE + UPDATE `WHERE status != 'executed'` + - After executed history exists, both arms no-op; enqueue returns mint-only ID + +## Architecture Fit + +- Layer boundaries involved: evolution persistence only; desktop/CLI already pass proposal_id through. +- Interfaces to preserve: `enqueue_user_capability_evolution` signature and backlog column set. + +## Dependency and Compatibility + +- New dependencies: none. +- Backward compatibility notes: migrate existing UNIQUE tables; new installs create non-unique `dedupe_key`. + +## Design Decisions + +- Decision: remove global UNIQUE on `dedupe_key`; keep application soft-dedupe for non-executed rows. + - Rationale: matches existing UPDATE filter intent and preserves executed history. + - Alternatives considered: + - Reset executed row in place (loses acceptance history / timestamps). + - Encode run generation into dedupe_key (breaks soft-dedupe and existing keys). + - Why rejected: both destroy governance history or widen blast radius. + +- Decision: after upsert, remap selected coordinator proposal_id to latest non-executed DB id. + - Rationale: soft-dedupe leaves the original proposal_id on the row; status updates by mint id no-op. + +## Open Questions + +- [x] Should multiple executed historical rows share a dedupe_key? Yes — that is the goal. +- [x] Change docs? No — internal persistence fix; no command/env surface change. diff --git a/tasks/TASK-2026-084-evolution-backlog-requeue-after-executed/PRD.md b/tasks/TASK-2026-084-evolution-backlog-requeue-after-executed/PRD.md new file mode 100644 index 00000000..97f67904 --- /dev/null +++ b/tasks/TASK-2026-084-evolution-backlog-requeue-after-executed/PRD.md @@ -0,0 +1,39 @@ +# PRD + +## Background + +Evolution governance stores proposals in SQLite `evolution_backlog` with a soft-dedupe key. Soft-dedupe is meant to collapse *active* proposals of the same scope, while preserving *executed* history for acceptance linking. The schema currently enforces a global UNIQUE on `dedupe_key`, which contradicts that intent and permanently blocks re-authorization after the first successful/forced execution. + +## Objective + +- Same-scope / same-capability proposals can re-enter backlog after prior rows are `executed`. +- Returned and forced proposal IDs always refer to a persisted backlog row. + +## Functional Requirements + +- FR-1: Soft-dedupe updates an existing non-executed row for the same `dedupe_key` instead of inserting duplicates. +- FR-2: When no non-executed row exists (including when only `executed` history remains), insert a new backlog row. +- FR-3: Existing databases that still have UNIQUE(`dedupe_key`) are migrated without data loss. +- FR-4: Coordinator status updates attach to the persisted active proposal identity after upsert. + +## Non-Functional Requirements + +- Security: N/A (no sandbox / authz boundary change). +- Performance: one-time table rebuild on migrate; steady-state upsert remains O(1) indexed updates. +- Compatibility: preserve all existing backlog columns and historical executed rows. + +## Constraints + +- Technical: SQLite cannot drop a column UNIQUE in place; rebuild table. +- Timeline: minimal fix only; no planner/policy redesign. + +## Success Metrics + +- Metric: re-authorize after executed yields a loadable proposal_id and a queued/executing active row. +- Baseline: phantom proposal_id + `NoScope` on forced run. +- Target: persisted active row + status updates succeed. + +## Rollout + +- Rollout plan: merge with evolution crate tests; migration runs on next DB open. +- Rollback plan: revert commit; old UNIQUE schema would reject multi-row history (acceptable emergency only). diff --git a/tasks/TASK-2026-084-evolution-backlog-requeue-after-executed/REVIEW.md b/tasks/TASK-2026-084-evolution-backlog-requeue-after-executed/REVIEW.md new file mode 100644 index 00000000..fa35a089 --- /dev/null +++ b/tasks/TASK-2026-084-evolution-backlog-requeue-after-executed/REVIEW.md @@ -0,0 +1,40 @@ +# Review Report + +## Scope Reviewed + +- Files/modules: + - `crates/skilllite-evolution/src/feedback.rs` + - `crates/skilllite-evolution/src/scope.rs` + - `crates/skilllite-evolution/src/lib.rs` (tests) + - `tasks/TASK-2026-084-evolution-backlog-requeue-after-executed/*` +- Commits/changes: evolution backlog re-queue after executed + +## Findings + +- Critical: none remaining in scope +- Major: none +- Minor: concurrent `sessions.json` last-writer-wins remains an open follow-up (out of scope) + +## Quality Gates + +- Architecture boundary checks: `pass` +- Security invariants: `pass` (N/A for sandbox; persistence-only) +- Required tests executed: `pass` +- Docs sync (EN/ZH): `pass` (N/A — no user-facing docs/command/env change) + +## Test Evidence + +- Commands run: + - `cargo test -p skilllite-evolution --lib` → 97 passed + - `cargo clippy -p skilllite-evolution --all-targets -- -D warnings` → clean + - `python3 scripts/validate_tasks.py` +- Key outputs: + - `enqueue_user_capability_evolution_requeues_after_executed` ok + - `migrate_evolution_backlog_allows_requeue_with_legacy_unique_dedupe` ok + - `coordinate_attaches_status_to_persisted_id_after_requeue` ok + - Existing soft-dedupe test still ok + +## Decision + +- Merge readiness: ready +- Follow-up actions: leave sessions.json atomic/merge writes and open path-escape PR backlog for later sweeps diff --git a/tasks/TASK-2026-084-evolution-backlog-requeue-after-executed/STATUS.md b/tasks/TASK-2026-084-evolution-backlog-requeue-after-executed/STATUS.md new file mode 100644 index 00000000..be9f8f37 --- /dev/null +++ b/tasks/TASK-2026-084-evolution-backlog-requeue-after-executed/STATUS.md @@ -0,0 +1,17 @@ +# Status Journal + +## Timeline + +- 2026-08-08: + - Progress: Confirmed critical authorize/re-queue bug on `main@12010e8`; implemented schema migration + upsert rewrite + coordinator persisted-id attach; added regression tests; validation green; opened PR #136. Slack notify blocked (bot not in channel). + - Blockers: none + - Next step: await review/merge. + +## Checkpoints + +- [x] PRD drafted before implementation (or `N/A` recorded) +- [x] Context drafted before implementation (or `N/A` recorded) +- [x] Implementation complete +- [x] Tests passed +- [x] Review complete +- [x] Board updated diff --git a/tasks/TASK-2026-084-evolution-backlog-requeue-after-executed/TASK.md b/tasks/TASK-2026-084-evolution-backlog-requeue-after-executed/TASK.md new file mode 100644 index 00000000..0abb70fe --- /dev/null +++ b/tasks/TASK-2026-084-evolution-backlog-requeue-after-executed/TASK.md @@ -0,0 +1,72 @@ +# TASK Card + +## Metadata + +- Task ID: TASK-2026-084-evolution-backlog-requeue-after-executed +- Title: Allow evolution backlog re-queue after executed +- Status: `in_review` +- Priority: `P0` +- Owner: automation +- Contributors: +- Created: 2026-08-08 +- Target milestone: + +## Problem + +`evolution_backlog.dedupe_key` is globally UNIQUE while `upsert_backlog_proposal` only updates rows with `status != 'executed'`. After a proposal is marked `executed`, a later same-scope / same-capability authorization cannot insert a new backlog row. `enqueue_user_capability_evolution` then returns a phantom `proposal_id`, and forced `evolution run` resolves to `NoScope` — authorized re-evolution silently fails. + +## Scope + +- In scope: + - Drop global UNIQUE on `evolution_backlog.dedupe_key` (new schema + existing DB migration). + - Make upsert update active rows or insert a new row when only executed history exists. + - Resolve persisted proposal IDs before coordinator status updates. + - Regression tests for re-queue after executed and soft-dedupe of non-executed rows. +- Out of scope: + - Broader coordinator policy changes. + - Concurrent `sessions.json` last-writer-wins (separate follow-up). + - Proposal ID UUID collision fix already tracked in open PR #133. + +## Acceptance Criteria + +- [x] After an `executed` row exists for a dedupe key, a new enqueue/upsert creates a fresh non-executed backlog row. +- [x] `enqueue_user_capability_evolution` returns a proposal_id that exists in the DB after re-auth. +- [x] Soft-dedupe still collapses multiple non-executed inserts for the same key to one active row. +- [x] Existing DBs created with UNIQUE on `dedupe_key` migrate successfully. +- [x] Focused evolution crate tests pass. + +## Risks + +- Risk: Migration rebuilds `evolution_backlog` on existing user DBs. + - Impact: Temporary lock / failure if table shape unexpected. + - Mitigation: Detect UNIQUE via `sqlite_master.sql`; copy all columns; recreate indexes; cover with migration test. +- Risk: Multiple historical rows per dedupe key change query assumptions. + - Impact: Readers that assume one row per key could mis-count. + - Mitigation: Keep `status != 'executed'` filters on active-path loaders; preserve UNIQUE on `proposal_id`. + +## Validation Plan + +- Required tests: + - Re-queue after executed for user capability enqueue. + - Soft-dedupe of consecutive non-executed enqueues. + - Schema migration from UNIQUE dedupe_key. + - Coordinator status attach after re-queue (optional focused unit). +- Commands to run: + - `cargo test -p skilllite-evolution --lib` + - `cargo clippy -p skilllite-evolution --all-targets -- -D warnings` + - `python3 scripts/validate_tasks.py` +- Manual checks: + - N/A (unit coverage of the failure path). + +## Regression Scope + +- Areas likely affected: + - `crates/skilllite-evolution` backlog upsert / authorize / coordinator. +- Explicit non-goals: + - Desktop UI changes; CLI flag surface; docs site. + +## Links + +- Source TODO section: critical bug automation sweep 2026-08-08 +- Related PRs/issues: follow-up to #133 (proposal_id collisions); distinct from #135 +- Related docs: N/A diff --git a/tasks/board.md b/tasks/board.md index 47f785f3..ca987fbd 100644 --- a/tasks/board.md +++ b/tasks/board.md @@ -8,6 +8,7 @@ Last updated: 2026-07-06 (TASK-2026-070 critical bug sweep done) ## Ready +- `TASK-2026-084-evolution-backlog-requeue-after-executed` - Status: `in_review` - Owner: `automation` - `TASK-2026-065-openclaw-claw-migrate-mvp` - Status: `done` - Owner: `maintainer` - None.