Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions crates/skilllite-commands/src/evolution_status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,14 @@ fn existing_workspace_skills_root(workspace_root: &Path) -> Option<PathBuf> {
skills_root.is_dir().then_some(skills_root)
}

fn shorten_status_reason(reason: &str) -> String {
if reason.chars().count() > 50 {
format!("{}...", reason.chars().take(47).collect::<String>())
} else {
reason.to_string()
}
}

/// Build evolution status snapshot (workspace `.env` + process env; no desktop UI overrides).
pub fn build_evolution_status_snapshot(params: &EvolutionStatusParams) -> EvolutionStatusSnapshot {
let workspace_root = resolve_workspace_root(&params.workspace);
Expand Down Expand Up @@ -405,11 +413,7 @@ fn cmd_status_human(workspace: &str) -> Result<()> {
t if t.contains("rolled_back") => "🔙",
_ => " ",
};
let reason_short = if reason.len() > 50 {
format!("{}...", &reason[..47])
} else {
reason
};
let reason_short = shorten_status_reason(&reason);
println!(" {} {} {} {}", icon, date, etype, reason_short);
if !target.is_empty() {
println!(" └─ target: {}", target);
Expand Down Expand Up @@ -497,6 +501,29 @@ mod workspace_scope_tests {
let _ = std::fs::remove_dir_all(env_workspace);
let _ = std::fs::remove_dir_all(target_workspace);
}

#[test]
fn status_human_handles_non_ascii_event_reasons() {
let workspace = temp_workspace("utf8-reason");
let chat_root = workspace.join("chat");
let conn = skilllite_evolution::feedback::open_evolution_db(&chat_root).expect("open db");
let reason =
"用户手动触发进化,原因包含中文和emoji🙂,需要展示最近事件摘要而不能因为截断崩溃。"
.repeat(2);
skilllite_evolution::log_evolution_event(
&conn,
&chat_root,
"manual_evolution_run_triggered",
"all",
&reason,
"txn-utf8",
)
.expect("insert log event");

cmd_status(false, workspace.to_string_lossy().as_ref(), None).expect("status succeeds");

let _ = std::fs::remove_dir_all(workspace);
}
}

#[cfg(test)]
Expand Down
28 changes: 28 additions & 0 deletions tasks/TASK-2026-069-evolution-status-utf8-reason/CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Technical Context

## Current State

- Relevant crates/files: `crates/skilllite-commands/src/evolution_status.rs`.
- Current behavior: The JSON status path serializes full event reasons safely, but the human status path computes `reason_short` with `&reason[..47]` when `reason.len() > 50`.

## Architecture Fit

- Layer boundaries involved: CLI entry calls `skilllite-commands`; persistence remains in `skilllite-evolution`.
- Interfaces to preserve: `cmd_status(json, workspace, periodic_anchor_unix)` and `EvolutionStatusSnapshot` shape.

## Dependency and Compatibility

- New dependencies: None.
- Backward compatibility notes: No schema, command flag, or JSON contract changes.

## Design Decisions

- Decision: Add a local `shorten_status_reason` helper that checks character count and builds the preview with `.chars().take(...)`.
- Rationale: This directly removes the panic source while preserving the existing output intent.
- Alternatives considered: Reuse `evolution.rs` private `truncate_chars` or make a shared utility.
- Why rejected: Sharing would broaden the edit beyond the critical path and require extra module surface changes.

## Open Questions

- [x] Does the fix need docs updates? No; behavior is a crash fix with unchanged user-facing command semantics.
- [x] Does the regression need LLM/API credentials? No; it seeds the local SQLite event log directly.
37 changes: 37 additions & 0 deletions tasks/TASK-2026-069-evolution-status-utf8-reason/PRD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# PRD

## Background

Recent critical-bug work fixed UTF-8 truncation crashes in evolution logging and LLM error summarization. During review of the latest evolution workspace DB scoping changes, the human `evolution status` path still contained byte slicing for event reasons read from SQLite. Those reasons can include Chinese or emoji text from manual evolution runs or desktop-triggered summaries.

## Objective

Prevent `skilllite evolution status` from crashing when rendering long non-ASCII event reasons, without changing persisted data or desktop JSON contracts.

## Functional Requirements

- FR-1: Human status output must shorten long event reasons using UTF-8-safe character iteration.
- FR-2: The existing recent event table should continue to display an ellipsis for long reasons.
- FR-3: A regression test must seed a real evolution log event with non-ASCII text and call the status command path.

## Non-Functional Requirements

- Security: No change to authorization, sandbox, or policy behavior.
- Performance: Reason truncation remains bounded and negligible relative to DB reads.
- Compatibility: JSON output and DB schema remain unchanged; human-only formatting may only change by avoiding invalid byte slicing.

## Constraints

- Technical: Follow Rust UTF-8 safety conventions and avoid new dependencies.
- Timeline: N/A for automation; complete in the current investigation loop.

## Success Metrics

- Metric: `cmd_status(false, workspace)` with a long Chinese/emoji reason.
- Baseline: Panics at a non-character byte boundary.
- Target: Returns `Ok(())` and prints a shortened reason.

## Rollout

- Rollout plan: Ship as a small bug-fix PR with regression coverage.
- Rollback plan: Revert the single code/test commit if unexpected output regressions appear.
29 changes: 29 additions & 0 deletions tasks/TASK-2026-069-evolution-status-utf8-reason/REVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Review Report

## Scope Reviewed

- Files/modules: `crates/skilllite-commands/src/evolution_status.rs`; recent evolution workspace and UTF-8 truncation fixes.
- Commits/changes: Investigation started from `897b00f` and adjacent UTF-8 commits; fixed unsafe status reason truncation.

## Findings

- Critical: Human `evolution status` can panic on long non-ASCII event reasons because it byte-slices `reason[..47]`.
- Major: None.
- Minor: None.

## Quality Gates

- Architecture boundary checks: `pass`
- Security invariants: `pass`
- Required tests executed: `pass`
- Docs sync (EN/ZH): `pass`

## Test Evidence

- Commands run: `cargo fmt --check`; `cargo clippy --all-targets -- -D warnings`; `cargo test -p skilllite-commands`; `cargo test -p skilllite-commands --features agent status_human_handles_non_ascii_event_reasons`; `cargo clippy --all-targets --all-features -- -D warnings`; `cargo test`; `python3 scripts/validate_tasks.py`.
- Key outputs: `cargo fmt --check` passed after rustfmt; default clippy passed after Rust toolchain update to 1.96.0; focused agent regression test passed with `1 passed; 0 failed`; all-features clippy passed; full `cargo test` passed; task validation passed for 69 task directories.

## Decision

- Merge readiness: `ready`
- Follow-up actions: None for this bug.
37 changes: 37 additions & 0 deletions tasks/TASK-2026-069-evolution-status-utf8-reason/STATUS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Status Journal

## Timeline

- 2026-06-11:
- Progress: Found unsafe byte slicing in `evolution_status.rs` human recent-event reason preview while reviewing recent evolution commits. PRD and context drafted before implementation. Implemented UTF-8-safe reason preview and added a regression test that seeds a real non-ASCII evolution log event before calling human status output.
- Blockers: None.
- Next step: Done; open PR and report findings.

## 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

## Validation Evidence

- `cargo fmt --check`:
- Initial run failed on one rustfmt line wrap; `cargo fmt` applied formatting.
- Re-run passed with exit code 0.
- `cargo clippy --all-targets -- -D warnings`:
- Initial run was blocked before code analysis by Cargo 1.83.0 lacking edition 2024 support.
- After `rustup update stable && rustup default stable`, re-run passed: `Finished dev profile ... target(s) in 29.21s`.
- `cargo test -p skilllite-commands`:
- Passed: `test result: ok. 23 passed; 0 failed`.
- Note: default features do not compile the agent-gated evolution status test.
- `cargo test -p skilllite-commands --features agent status_human_handles_non_ascii_event_reasons`:
- Passed: `1 passed; 0 failed`.
- `cargo clippy --all-targets --all-features -- -D warnings`:
- Passed: `Finished dev profile ... target(s) in 7.95s`.
- `cargo test`:
- Passed; final doctest outputs all reported `test result: ok`.
- `python3 scripts/validate_tasks.py`:
- Passed before and after final task updates: `Task validation passed (69 task directories checked).`
50 changes: 50 additions & 0 deletions tasks/TASK-2026-069-evolution-status-utf8-reason/TASK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# TASK Card

## Metadata

- Task ID: `TASK-2026-069`
- Title: Fix UTF-8 crash in evolution status reasons
- Status: `done`
- Priority: `P0`
- Owner: `agent`
- Contributors: Cursor automation
- Created: `2026-06-11`
- Target milestone: critical bug investigation

## Problem

`skilllite evolution status` human output shortens recent event reasons with byte slicing. A long non-ASCII reason stored in `evolution_log` can panic when byte index 47 lands inside a UTF-8 character, making the status command unusable for affected workspaces.

## Scope

- In scope: replace the unsafe human status reason preview with UTF-8-safe truncation and add a regression test that exercises the status command with non-ASCII event text.
- Out of scope: broader evolution workspace scoping, JSON status schema changes, and unrelated rendering cleanup.

## Acceptance Criteria

- [x] Human `evolution status` does not panic on long CJK/emoji event reasons.
- [x] Regression test covers the actual status rendering path, not only a helper.
- [x] Required Rust and task validation commands pass.

## Risks

- Risk: Changing preview length semantics could alter human-only output formatting.
- Impact: Low; JSON output and persisted data are unchanged.
- Mitigation: Keep the same ASCII preview threshold shape and only change the truncation mechanism.

## Validation Plan

- Required tests: focused `skilllite-commands` test, full workspace tests per policy.
- Commands to run: `cargo fmt --check`; `cargo clippy --all-targets -- -D warnings`; `cargo test -p skilllite-commands`; `cargo test`; `python3 scripts/validate_tasks.py`.
- Manual checks: Re-read modified files and task board after updates.

## Regression Scope

- Areas likely affected: `skilllite evolution status` human recent-event output.
- Explicit non-goals: no evolution DB schema changes, no desktop JSON payload changes, no behavior changes for backlog/proposal commands.

## Links

- Source TODO section: N/A; found during critical bug investigation automation.
- Related PRs/issues: Recent UTF-8 truncation fixes `TASK-2026-066`, `TASK-2026-067`; workspace DB scoping fix `TASK-2026-068`.
- Related docs: N/A; user-facing semantics are unchanged.
3 changes: 2 additions & 1 deletion tasks/board.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Task Board

Last updated: 2026-06-10 (TASK-2026-068 evolution workspace db scope done)
Last updated: 2026-06-11 (TASK-2026-069 evolution status UTF-8 reason fix done)

## In Progress

Expand All @@ -17,6 +17,7 @@ Last updated: 2026-06-10 (TASK-2026-068 evolution workspace db scope done)

## Done

- `TASK-2026-069-evolution-status-utf8-reason` - 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`
Expand Down
Loading