diff --git a/crates/skilllite-assistant/src-tauri/src/life_pulse.rs b/crates/skilllite-assistant/src-tauri/src/life_pulse.rs index 94f945d..f05caa1 100644 --- a/crates/skilllite-assistant/src-tauri/src/life_pulse.rs +++ b/crates/skilllite-assistant/src-tauri/src/life_pulse.rs @@ -161,24 +161,48 @@ fn check_schedule_due(workspace: &std::path::Path) -> bool { // ─── Subprocess helpers ───────────────────────────────────────────────────── +fn build_life_pulse_command( + skilllite_path: &std::path::Path, + workspace: &str, + env_pairs: &[(String, String)], + args: &[&str], +) -> Command { + let root = skilllite_bridge::find_project_root(workspace); + let mut cmd = Command::new(skilllite_path); + crate::windows_spawn::hide_child_console(&mut cmd); + cmd.args(args) + .envs(env_pairs.iter().map(|(k, v)| (k.as_str(), v.as_str()))) + .current_dir(&root) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()); + if std::path::Path::new(workspace).is_absolute() { + cmd.env( + skilllite_bridge::local::env_keys::paths::SKILLLITE_WORKSPACE, + workspace, + ); + } + cmd +} + fn spawn_growth( skilllite_path: &std::path::Path, + workspace: &str, env_pairs: &[(String, String)], running: Arc, app: tauri::AppHandle, ) { let path = skilllite_path.to_path_buf(); + let workspace = workspace.to_string(); let env: Vec<(String, String)> = env_pairs.to_vec(); std::thread::spawn(move || { emit(&app, "growth-started", None); - let mut growth_cmd = Command::new(&path); - crate::windows_spawn::hide_child_console(&mut growth_cmd); - let result = growth_cmd - .args(["evolution", "run"]) - .envs(env.iter().map(|(k, v)| (k.as_str(), v.as_str()))) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .status(); + let result = build_life_pulse_command( + &path, + &workspace, + &env, + &["evolution", "run", "--workspace", &workspace], + ) + .status(); running.store(false, Ordering::SeqCst); match result { Ok(s) if s.success() => emit(&app, "growth-done", None), @@ -194,22 +218,23 @@ fn spawn_growth( fn spawn_rhythm( skilllite_path: &std::path::Path, + workspace: &str, env_pairs: &[(String, String)], running: Arc, app: tauri::AppHandle, ) { let path = skilllite_path.to_path_buf(); + let workspace = workspace.to_string(); let env: Vec<(String, String)> = env_pairs.to_vec(); std::thread::spawn(move || { emit(&app, "rhythm-started", None); - let mut rhythm_cmd = Command::new(&path); - crate::windows_spawn::hide_child_console(&mut rhythm_cmd); - let result = rhythm_cmd - .args(["schedule", "tick"]) - .envs(env.iter().map(|(k, v)| (k.as_str(), v.as_str()))) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .status(); + let result = build_life_pulse_command( + &path, + &workspace, + &env, + &["schedule", "tick", "--workspace", &workspace], + ) + .status(); running.store(false, Ordering::SeqCst); match result { Ok(s) if s.success() => emit(&app, "rhythm-done", None), @@ -281,6 +306,7 @@ pub fn start(state: LifePulseState, skilllite_path: PathBuf, app: tauri::AppHand s.growth_running.store(true, Ordering::SeqCst); spawn_growth( &skilllite_path, + &workspace, &child_env, s.growth_running.clone(), app.clone(), @@ -293,6 +319,7 @@ pub fn start(state: LifePulseState, skilllite_path: PathBuf, app: tauri::AppHand s.rhythm_running.store(true, Ordering::SeqCst); spawn_rhythm( &skilllite_path, + &workspace, &child_env, s.rhythm_running.clone(), app.clone(), @@ -326,3 +353,92 @@ pub fn start(state: LifePulseState, skilllite_path: PathBuf, app: tauri::AppHand pub fn stop(state: &LifePulseState) { state.alive.store(false, Ordering::SeqCst); } + +#[cfg(test)] +mod tests { + use super::*; + + fn temp_workspace(name: &str) -> PathBuf { + let unique = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("duration") + .as_nanos(); + let dir = std::env::temp_dir().join(format!( + "skilllite_life_pulse_{}_{}_{}", + std::process::id(), + unique, + name + )); + std::fs::create_dir_all(&dir).expect("workspace"); + dir + } + + fn args(cmd: &Command) -> Vec { + cmd.get_args() + .map(|arg| arg.to_string_lossy().to_string()) + .collect() + } + + #[test] + fn growth_command_targets_life_pulse_workspace() { + let workspace = temp_workspace("growth"); + let workspace_str = workspace.to_string_lossy().to_string(); + let env = vec![("SKILLLITE_API_KEY".to_string(), "test-key".to_string())]; + let cmd = build_life_pulse_command( + std::path::Path::new("skilllite"), + &workspace_str, + &env, + &["evolution", "run", "--workspace", &workspace_str], + ); + + let expected_workspace = workspace.canonicalize().expect("canonical workspace"); + assert_eq!( + cmd.get_current_dir().expect("current dir"), + expected_workspace.as_path() + ); + assert_eq!( + args(&cmd), + vec![ + "evolution".to_string(), + "run".to_string(), + "--workspace".to_string(), + workspace_str.clone() + ] + ); + assert!(cmd.get_envs().any(|(key, value)| { + key == skilllite_bridge::local::env_keys::paths::SKILLLITE_WORKSPACE + && value == Some(std::ffi::OsStr::new(&workspace_str)) + })); + + let _ = std::fs::remove_dir_all(workspace); + } + + #[test] + fn rhythm_command_targets_life_pulse_workspace() { + let workspace = temp_workspace("rhythm"); + let workspace_str = workspace.to_string_lossy().to_string(); + let cmd = build_life_pulse_command( + std::path::Path::new("skilllite"), + &workspace_str, + &[], + &["schedule", "tick", "--workspace", &workspace_str], + ); + + let expected_workspace = workspace.canonicalize().expect("canonical workspace"); + assert_eq!( + cmd.get_current_dir().expect("current dir"), + expected_workspace.as_path() + ); + assert_eq!( + args(&cmd), + vec![ + "schedule".to_string(), + "tick".to_string(), + "--workspace".to_string(), + workspace_str + ] + ); + + let _ = std::fs::remove_dir_all(workspace); + } +} diff --git a/crates/skilllite-assistant/src-tauri/src/skilllite_bridge/integrations/evolution_ui/growth.rs b/crates/skilllite-assistant/src-tauri/src/skilllite_bridge/integrations/evolution_ui/growth.rs index d6d06c8..ab7a8ab 100644 --- a/crates/skilllite-assistant/src-tauri/src/skilllite_bridge/integrations/evolution_ui/growth.rs +++ b/crates/skilllite-assistant/src-tauri/src/skilllite_bridge/integrations/evolution_ui/growth.rs @@ -7,12 +7,33 @@ use crate::skilllite_bridge::chat::ChatConfigOverrides; use super::status::load_evolution_status; +fn now_unix() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64 +} + +pub(crate) fn next_periodic_anchor( + current: Option, + now: i64, + growth_tick_would_be_due: bool, + arm_periodic: bool, +) -> Option { + if current.is_none() || (growth_tick_would_be_due && arm_periodic) { + Some(now) + } else { + current + } +} + pub fn evolution_growth_due( workspace: &str, last_periodic_spawn_unix: &Mutex>, cfg: Option<&ChatConfigOverrides>, skilllite_path: &Path, ) -> bool { + let now = now_unix(); let anchor = last_periodic_spawn_unix .lock() .ok() @@ -30,6 +51,17 @@ pub fn evolution_growth_due( let Some(a9) = status.a9 else { return false; }; + let next_anchor = next_periodic_anchor( + anchor, + now, + a9.growth_tick_would_be_due, + a9.arm_periodic, + ); + if next_anchor != anchor { + if let Ok(mut guard) = last_periodic_spawn_unix.lock() { + *guard = next_anchor; + } + } if !a9.growth_tick_would_be_due { return false; } @@ -38,3 +70,29 @@ pub fn evolution_growth_due( } true } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn periodic_anchor_initializes_on_first_successful_check() { + assert_eq!(next_periodic_anchor(None, 100, false, false), Some(100)); + } + + #[test] + fn periodic_anchor_advances_when_periodic_arm_fires() { + assert_eq!( + next_periodic_anchor(Some(10), 100, true, true), + Some(100) + ); + } + + #[test] + fn periodic_anchor_does_not_advance_for_signal_only_due() { + assert_eq!( + next_periodic_anchor(Some(10), 100, true, false), + Some(10) + ); + } +} diff --git a/crates/skilllite-assistant/src-tauri/src/skilllite_bridge/mod.rs b/crates/skilllite-assistant/src-tauri/src/skilllite_bridge/mod.rs index e6ab1d9..2948e3b 100644 --- a/crates/skilllite-assistant/src-tauri/src/skilllite_bridge/mod.rs +++ b/crates/skilllite-assistant/src-tauri/src/skilllite_bridge/mod.rs @@ -25,7 +25,7 @@ pub use chat::{ pub use followup_suggestions::followup_chat_suggestions; pub use integrations::*; pub use llm_routing_error::{classify_llm_routing_error_message, LlmInvokeResult}; -pub(crate) use paths::load_dotenv_for_child; +pub(crate) use paths::{find_project_root, load_dotenv_for_child}; pub use paths::{ default_writable_workspace_dir, ensure_skilllite_version, resolve_skilllite_path_app, MIN_SKILLLITE_VERSION, diff --git a/tasks/TASK-2026-069-daily-critical-bug-sweep/CONTEXT.md b/tasks/TASK-2026-069-daily-critical-bug-sweep/CONTEXT.md new file mode 100644 index 0000000..d1e7d1e --- /dev/null +++ b/tasks/TASK-2026-069-daily-critical-bug-sweep/CONTEXT.md @@ -0,0 +1,36 @@ +# Technical Context + +## Current State + +- Relevant crates/files: `crates/skilllite-assistant/src-tauri/src/life_pulse.rs`, `crates/skilllite-assistant/src-tauri/src/skilllite_bridge/integrations/evolution_ui/growth.rs`, `crates/skilllite-assistant/src-tauri/src/skilllite_bridge/mod.rs`, `skilllite/src/cli.rs`, `crates/skilllite-commands/src/evolution.rs`, `crates/skilllite-commands/src/schedule.rs`. +- Current behavior: current branch `cursor/critical-bug-investigation-d75f` contains a fix for a recent desktop Life Pulse regression introduced by the CLI-only bridge refactor. + +## Architecture Fit + +- Layer boundaries involved: desktop assistant bridge spawning CLI subprocesses; engine CLI remains the execution boundary. +- Interfaces to preserve: Life Pulse still delegates heavy work to `skilllite` CLI; no direct engine dependency is reintroduced. + +## Dependency and Compatibility + +- New dependencies: none planned. +- Backward compatibility notes: existing Life Pulse UI and CLI arguments are preserved; background subprocesses now receive the same workspace contract as manual desktop actions. + +## Design Decisions + +- Decision: perform a review-only audit first and defer implementation until a concrete critical trigger scenario is established. + - Rationale: the automation explicitly requires a high confidence bar and no PR for doubtful findings. + - Alternatives considered: proactively patch suspicious code based on pattern matching. + - Why rejected: it risks false positives and unnecessary behavior drift. +- Decision: fix Life Pulse by adding a small command builder that sets `current_dir`, `--workspace`, and absolute `SKILLLITE_WORKSPACE` consistently for background growth/rhythm subprocesses. + - Rationale: manual desktop actions already use this workspace contract, and the bug was caused by background subprocesses omitting it. + - Alternatives considered: call the in-process engine directly from the desktop bridge. + - Why rejected: it would reverse the CLI-only bridge direction from the recent refactor. +- Decision: update the desktop periodic anchor when the first successful check initializes it and when the periodic arm fires. + - Rationale: this restores the mutation that `growth_due` performed before the read-only CLI status path was introduced. + - Alternatives considered: only update after the subprocess exits successfully. + - Why rejected: the original semantics advance the periodic anchor when the periodic arm fires, even if a periodic-only preflight later skips due to no proposals. + +## Open Questions + +- [x] Which recent commits contain high-blast-radius behavioral changes? +- [x] Does any suspicious change have a concrete trigger scenario with critical impact? diff --git a/tasks/TASK-2026-069-daily-critical-bug-sweep/PRD.md b/tasks/TASK-2026-069-daily-critical-bug-sweep/PRD.md new file mode 100644 index 0000000..3ba6c2d --- /dev/null +++ b/tasks/TASK-2026-069-daily-critical-bug-sweep/PRD.md @@ -0,0 +1,38 @@ +# PRD + +## Background + +This task is a scheduled high-severity bug sweep. The expected outcome most days is a concise verified report that no critical bugs were found. If a real critical bug is found, the task shifts to a minimal fix with regression coverage. + +## Objective + +Inspect recent commits for concrete, triggerable critical correctness bugs and either fix a confirmed issue or report that no critical bugs were found. + +## Functional Requirements + +- FR-1: Review recent commit metadata and changed files before selecting suspicious code paths. +- FR-2: Trace selected behavioral changes through callers and downstream effects. +- FR-3: Only implement a fix when the bug has a plausible concrete trigger and high-severity impact. +- FR-4: Post the outcome to Slack. + +## Non-Functional Requirements + +- Security: preserve sandbox, authorization, and execution gating invariants; do not relax protections without explicit evidence and tests. +- Performance: do not run unnecessary broad or long-running verification when no code changed. +- Compatibility: avoid behavior changes unless required for a confirmed critical fix. + +## Constraints + +- Technical: use repository specs as the execution baseline; keep any fix minimal and localized. +- Timeline: no calendar estimate; complete within the automation run if feasible. + +## Success Metrics + +- Metric: verified outcome. +- Baseline: recent commits have not been independently audited in this run. +- Target: confirmed critical bug fixed with validation, or no critical bug surfaced and no PR opened. + +## Rollout + +- Rollout plan: if a fix is committed, push the branch and open a PR through automation tooling. +- Rollback plan: if no fix is made, no rollout is required. diff --git a/tasks/TASK-2026-069-daily-critical-bug-sweep/REVIEW.md b/tasks/TASK-2026-069-daily-critical-bug-sweep/REVIEW.md new file mode 100644 index 0000000..a33cf0d --- /dev/null +++ b/tasks/TASK-2026-069-daily-critical-bug-sweep/REVIEW.md @@ -0,0 +1,44 @@ +# Review Report + +## Scope Reviewed + +- Files/modules: `crates/skilllite-assistant/src-tauri/src/life_pulse.rs`, `crates/skilllite-assistant/src-tauri/src/skilllite_bridge/integrations/evolution_ui/*`, `crates/skilllite-assistant/src-tauri/src/skilllite_bridge/evolution_cli.rs`, `skilllite/src/cli.rs`, `skilllite/src/dispatch/mod.rs`, `crates/skilllite-commands/src/evolution.rs`, `crates/skilllite-commands/src/schedule.rs`, `crates/skilllite-agent/src/llm/mod.rs`, `crates/skilllite-agent/src/prompt.rs`. +- Commits/changes: recent mainline behavioral changes including `26e6dde` desktop CLI-only bridge refactor, `897b00f` evolution workspace DB scoping fix, `42294f0` UTF-8 truncation fix, and `97bfe4e` suggest-followup feature gate fix. + +## Findings + +- Critical: Life Pulse background growth/rhythm subprocesses were launched without the selected workspace after the desktop CLI-only bridge refactor. A desktop user with Life Pulse enabled could have due checks evaluated against the configured workspace, while `skilllite evolution run` and `skilllite schedule tick` executed against the Tauri process current directory. This caused scheduled jobs/evolution to run against the wrong tree or silently do nothing. The same refactor also stopped advancing the desktop periodic growth anchor, so the periodic arm never reached its configured interval after repeated read-only status checks. +- Major: No additional major issues confirmed in the reviewed recent commits. +- Minor: Existing Tauri test builds emit unused-code warnings unrelated to this fix. + +## Quality Gates + +- Architecture boundary checks: `pass` - fix keeps desktop bridge on CLI subprocess boundary and does not reintroduce engine coupling. +- Security invariants: `pass` - no sandbox/security policy changed. +- Required tests executed: `pass` +- Docs sync (EN/ZH): `pass` - bug fix restores intended Life Pulse workspace behavior; no documented CLI/env contract changed. + +## Test Evidence + +- Commands run: + - `git status --short --branch && git fetch origin main && git log --oneline --decorate --max-count=20 && git log --oneline origin/main..HEAD && git diff --stat origin/main...HEAD` + - `git log --since='14 days ago' --oneline --no-merges --decorate && git log --since='14 days ago' --stat --no-merges --pretty=format:'%h %s'` + - `cargo fmt` + - `rustc --version && rustup update stable && rustup default stable && rustc --version` + - `cargo fmt --check && python3 scripts/validate_tasks.py` + - `cargo test --manifest-path crates/skilllite-assistant/src-tauri/Cargo.toml life_pulse --lib` + - `cargo test --manifest-path crates/skilllite-assistant/src-tauri/Cargo.toml evolution_ui::growth --lib` + - `cargo clippy --all-targets -- -D warnings` +- Key outputs: + - Current branch initially matched `origin/main` before task changes; recent non-merge code commits included `897b00f`, `42294f0`, `97bfe4e`, and larger bridge change `26e6dde`. + - Initial assistant tests were blocked by Cargo 1.83 requiring edition 2024 support; stable toolchain updated to `rustc 1.96.0 (ac68faa20 2026-05-25)`. + - Initial Tauri test build was blocked by missing `gdk-3.0`; installed Linux Tauri build dependencies. + - `Task validation passed (69 task directories checked).` + - `life_pulse` targeted tests: `2 passed; 0 failed`. + - `evolution_ui::growth` targeted tests: `3 passed; 0 failed`. + - Root `cargo clippy --all-targets -- -D warnings`: `Finished dev profile`. + +## Decision + +- Merge readiness: `ready` +- Follow-up actions: PR opened; Slack report attempted but blocked by channel membership, so final automation summary must carry bug, root cause, fix, and validation. diff --git a/tasks/TASK-2026-069-daily-critical-bug-sweep/STATUS.md b/tasks/TASK-2026-069-daily-critical-bug-sweep/STATUS.md new file mode 100644 index 0000000..47bdee2 --- /dev/null +++ b/tasks/TASK-2026-069-daily-critical-bug-sweep/STATUS.md @@ -0,0 +1,33 @@ +# Status Journal + +## Timeline + +- 2026-06-14: + - Progress: Task artifacts created and scoped for a scheduled high-severity recent-commit bug sweep. + - Blockers: None. + - Next step: Inspect recent commits and trace suspicious behavioral changes. +- 2026-06-14: + - Progress: Confirmed a recent Life Pulse regression from the CLI-only desktop bridge refactor: background growth/rhythm subprocesses did not carry the selected workspace, and periodic growth anchor state was not advanced. Implemented a localized fix with unit coverage. + - Blockers: None. + - Next step: Commit, push, and run validation. +- 2026-06-14: + - Progress: Validation completed. `cargo fmt --check`, `python3 scripts/validate_tasks.py`, root `cargo clippy --all-targets -- -D warnings`, and targeted assistant tests passed after updating Rust stable and installing Linux Tauri build dependencies. + - Blockers: None. + - Next step: Open PR and report the fix. +- 2026-06-14: + - Progress: Review evidence recorded and task marked done for PR handoff. + - Blockers: None. + - Next step: Publish PR and Slack summary. +- 2026-06-14: + - Progress: PR created. Slack summary delivery was attempted in all visible channels (`all-skilllite`, `new-channel`, `social`) but each failed because the Cursor bot is not invited. + - Blockers: Slack channel membership prevents posting the requested platform summary. + - Next step: Use automation final summary to report the outcome. + +## 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-069-daily-critical-bug-sweep/TASK.md b/tasks/TASK-2026-069-daily-critical-bug-sweep/TASK.md new file mode 100644 index 0000000..01abf57 --- /dev/null +++ b/tasks/TASK-2026-069-daily-critical-bug-sweep/TASK.md @@ -0,0 +1,54 @@ +# TASK Card + +## Metadata + +- Task ID: `TASK-2026-069` +- Title: Daily critical bug sweep +- Status: `done` +- Priority: `P0` +- Owner: `agent` +- Contributors: automation +- Created: `2026-06-14` +- Target milestone: daily critical bug investigation + +## Problem + +Recent commits can introduce high-severity correctness regressions that escaped review. This task audits recent repository changes for concrete, triggerable bugs that could cause data loss, crashes, security holes, or significant user-facing breakage. + +## Scope + +- In scope: recent commits on the current branch/base range; behavioral changes with meaningful blast radius; full caller/downstream tracing for suspicious changes. +- Out of scope: style issues, speculative concerns without a concrete trigger, minor UX degradation, broad refactors unrelated to a confirmed critical bug. + +## Acceptance Criteria + +- [x] Recent commits and their changed files are inspected. +- [x] Suspicious high-impact changes are traced through callers and downstream effects. +- [x] A critical bug is fixed only if a concrete trigger scenario is confirmed; otherwise no PR is opened. +- [x] Findings summary delivery attempted; Slack posting was blocked because the Cursor bot is not invited to any visible channel, so the automation final summary carries the result. + +## Risks + +- Risk: false positive report or unnecessary PR. + - Impact: reviewer churn and potential behavior drift. + - Mitigation: require a concrete trigger scenario before fixing or opening a PR. +- Risk: shallow diff-only review misses a downstream failure. + - Impact: critical issue remains undetected. + - Mitigation: inspect caller chains and execution paths for selected high-blast-radius changes. + +## Validation Plan + +- Required tests: targeted desktop assistant unit tests for Life Pulse command workspace propagation and growth anchor behavior; root workspace clippy; task artifact validation. +- Commands to run: `cargo fmt --check`, `python3 scripts/validate_tasks.py`, `cargo test --manifest-path crates/skilllite-assistant/src-tauri/Cargo.toml life_pulse --lib`, `cargo test --manifest-path crates/skilllite-assistant/src-tauri/Cargo.toml evolution_ui::growth --lib`, `cargo clippy --all-targets -- -D warnings`. +- Manual checks: traced Life Pulse due checks, subprocess launch arguments, CLI workspace defaults, and A9 periodic anchor semantics. + +## Regression Scope + +- Areas likely affected: desktop Life Pulse growth and rhythm background subprocesses; desktop A9 periodic growth diagnostics. +- Explicit non-goals: changing manual evolution trigger behavior, schedule file semantics, or unrelated desktop bridge integrations. + +## Links + +- Source TODO section: N/A - daily automation request. +- Related PRs/issues: N/A at task start. +- Related docs: `spec/verification-integrity.md`, `spec/README.md`. diff --git a/tasks/board.md b/tasks/board.md index df73272..4ded0d9 100644 --- a/tasks/board.md +++ b/tasks/board.md @@ -1,6 +1,6 @@ # Task Board -Last updated: 2026-06-10 (TASK-2026-068 evolution workspace db scope done) +Last updated: 2026-06-14 (TASK-2026-069 daily critical bug sweep done) ## In Progress @@ -17,6 +17,7 @@ Last updated: 2026-06-10 (TASK-2026-068 evolution workspace db scope done) ## Done +- `TASK-2026-069-daily-critical-bug-sweep` - Status: `done` - Owner: `agent` - `TASK-2026-068-evolution-workspace-db-scope` - Status: `done` - Owner: `agent` - `TASK-2026-067-utf8-llm-error-truncate` - Status: `done` - Owner: `agent` - `TASK-2026-066-utf8-evolution-log-truncate` - Status: `done` - Owner: `agent`