Skip to content
Merged
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
35 changes: 30 additions & 5 deletions crates/skilllite-commands/src/evolution_desktop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,18 +214,18 @@ pub fn authorize_capability_evolution(
Ok(AuthorizeCapabilitySnapshot { proposal_id })
}

fn clip_manual_trigger_summary(summary: &str) -> String {
truncate_utf8(summary, 480)
}

pub fn log_manual_evolution_trigger(
workspace: &str,
proposal_id: Option<&str>,
summary: &str,
) -> Result<()> {
let chat_root = skilllite_core::paths::chat_root();
let conn = skilllite_evolution::feedback::open_evolution_db(&chat_root)?;
let mut clipped = summary.to_string();
if clipped.len() > 480 {
clipped.truncate(480);
clipped.push('…');
}
let clipped = clip_manual_trigger_summary(summary);
let _ = skilllite_evolution::log_evolution_event(
&conn,
&chat_root,
Expand All @@ -236,3 +236,28 @@ pub fn log_manual_evolution_trigger(
);
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn manual_trigger_summary_clip_is_utf8_boundary_safe() {
let mut summary = "界".repeat(159);
summary.push('🙂');
summary.push_str("tail");

let clipped = clip_manual_trigger_summary(&summary);

assert!(clipped.ends_with('…'));
assert!(clipped.is_char_boundary(clipped.len()));
assert!(clipped.len() <= 480 + "…".len());
}

#[test]
fn manual_trigger_summary_clip_leaves_short_text_unchanged() {
let summary = "Evolution completed: 新技能已生成";

assert_eq!(clip_manual_trigger_summary(summary), summary);
}
}
27 changes: 27 additions & 0 deletions tasks/TASK-2026-066-utf8-evolution-log-truncate/CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Technical Context

## Current State

- Relevant crates/files: `crates/skilllite-commands/src/evolution_desktop.rs`, `crates/skilllite-commands/src/evolution.rs`.
- Current behavior: `log_manual_evolution_trigger()` copies the summary into a `String`, calls `truncate(480)`, and appends `…` when the byte length is above 480.

## Architecture Fit

- Layer boundaries involved: CLI command layer only; no dependency direction changes.
- Interfaces to preserve: `skilllite evolution run --log-manual-trigger`, evolution log event fields, and desktop L2 JSON contract.

## Dependency and Compatibility

- New dependencies: none.
- Backward compatibility notes: the logged reason may be a few bytes shorter when the limit lands inside a multi-byte character, but remains valid UTF-8 and keeps the same ellipsis marker.

## Design Decisions

- Decision: reuse the existing `truncate_utf8()` helper for manual trigger summaries.
- Rationale: the helper already enforces character boundaries for pending skill previews in the same module.
- Alternatives considered: count characters instead of bytes.
- Why rejected: byte-budget behavior was already established and only needs boundary safety.

## Open Questions

- [x] Is a schema or documentation update required? No; behavior remains the same except crash prevention.
36 changes: 36 additions & 0 deletions tasks/TASK-2026-066-utf8-evolution-log-truncate/PRD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# PRD

## Background

The desktop split-ready evolution bridge now records a manual trigger event after `skilllite evolution run --log-manual-trigger`. The event reason is derived from the user-visible evolution response and can contain CJK or emoji text. Rust `String::truncate(n)` requires `n` to be a valid UTF-8 character boundary; using a raw byte limit can panic.

## Objective

Manual evolution trigger logging must never crash because of non-ASCII summary text. The fix should preserve the existing audit event shape and only alter the clipping implementation.

## Functional Requirements

- FR-1: Clip manual evolution trigger summaries to the existing 480-byte budget without splitting UTF-8 characters.
- FR-2: Preserve the trailing ellipsis behavior when clipping occurs.

## Non-Functional Requirements

- Security: no security policy relaxation.
- Performance: clipping remains linear in the summary length and only runs once per manual trigger.
- Compatibility: log event type, target ID, workspace field, and JSON/CLI outputs remain unchanged.

## Constraints

- Technical: use existing local helper where possible; no new dependencies.
- Timeline: autonomous critical bug fix; no calendar estimate.

## Success Metrics

- Metric: regression test with repeated CJK text passes without panic.
- Baseline: `String::truncate(480)` panics when 480 is not a character boundary.
- Target: UTF-8-safe clipping returns valid text and appends `…`.

## Rollout

- Rollout plan: ship as a minimal Rust patch in `skilllite-commands`.
- Rollback plan: revert the single helper usage and test if unexpected behavior appears.
36 changes: 36 additions & 0 deletions tasks/TASK-2026-066-utf8-evolution-log-truncate/REVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Review Report

## Scope Reviewed

- Files/modules: `crates/skilllite-commands/src/evolution_desktop.rs`; task artifacts for `TASK-2026-066`.
- Commits/changes: Recent evolution L2 CLI bridge code paths around `skilllite evolution run --log-manual-trigger`.

## Findings

- Critical: Fixed UTF-8 byte-boundary panic in manual evolution trigger log clipping.
- Major: None remaining.
- Minor: None.

## Quality Gates

- Architecture boundary checks: `pass`
- Security invariants: `pass`
- Required tests executed: `pass`
- Docs sync (EN/ZH): `pass` (not required; no user-facing command semantics changed)

## Test Evidence

- Commands run:
- `cargo fmt --check && cargo test -p skilllite-commands evolution_desktop && python3 scripts/validate_tasks.py`
- `cargo test -p skilllite-commands --features agent manual_trigger_summary_clip`
- `cargo clippy --all-targets -- -D warnings && cargo clippy -p skilllite-commands --features agent --all-targets -- -D warnings && cargo test`
- Key outputs:
- Initial validation was blocked by Cargo 1.83 lacking edition 2024 support for `time 0.3.47`; local stable toolchain was updated to Rust/Cargo 1.96.
- Agent-feature regression run: `2 passed; 0 failed`.
- Task validation: `Task validation passed (66 task directories checked).`
- Full clippy/test command exited 0; final workspace test/doc-test output showed all executed tests passing.

## Decision

- Merge readiness: `ready`
- Follow-up actions: None.
17 changes: 17 additions & 0 deletions tasks/TASK-2026-066-utf8-evolution-log-truncate/STATUS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Status Journal

## Timeline

- 2026-05-29:
- Progress: Identified a UTF-8 byte-boundary panic in `log_manual_evolution_trigger()` introduced in recent evolution L2 CLI work. Drafted task scope, PRD, and context before code changes. Replaced raw byte truncation with the module's UTF-8-safe clipping helper and added non-ASCII regression coverage. Validation completed after updating the local Rust stable toolchain to 1.96.
- Blockers: None.
- Next step: Open PR and report the fix.

## 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
57 changes: 57 additions & 0 deletions tasks/TASK-2026-066-utf8-evolution-log-truncate/TASK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# TASK Card

## Metadata

- Task ID: `TASK-2026-066`
- Title: Fix UTF-8-safe evolution trigger logging
- Status: `done`
- Priority: `P0`
- Owner: `agent`
- Contributors: Cursor automation
- Created: `2026-05-29`
- Target milestone: Critical bug fix

## Problem

Recent desktop evolution L2 work added manual trigger logging for `skilllite evolution run --log-manual-trigger`. The log summary path truncates a UTF-8 `String` by byte length with `String::truncate(480)`, which panics if byte 480 falls inside a multi-byte CJK or emoji character. A long non-ASCII evolution summary can therefore crash the command after the evolution run has completed.

## Scope

- In scope: make manual evolution trigger log clipping UTF-8 safe; add regression coverage with non-ASCII input.
- Out of scope: broader evolution workflow refactors, log schema changes, or LLM behavior changes.

## Acceptance Criteria

- [x] Long Chinese or emoji summaries do not panic during manual trigger log clipping.
- [x] Existing preview truncation behavior remains unchanged except for UTF-8 safety.
- [x] Regression test covers the non-ASCII boundary case.

## Risks

- Risk: changing truncation semantics could alter logged summary length slightly.
- Impact: low; only audit/log preview text is affected.
- Mitigation: reuse the existing UTF-8-aware helper and keep the same byte limit.

## Validation Plan

- Required tests: targeted `skilllite-commands` regression test; repository task validation.
- Commands to run: `cargo fmt --check`; `cargo test -p skilllite-commands evolution_desktop`; `python3 scripts/validate_tasks.py`; broader cargo validation if time permits.
- Manual checks: re-read modified source and task artifacts.

## Validation Evidence

- `rustup update stable && rustup default stable`: updated local toolchain from Rust/Cargo 1.83 to 1.96 because dependency `time 0.3.47` requires edition 2024 support.
- `cargo fmt --check && cargo test -p skilllite-commands evolution_desktop && python3 scripts/validate_tasks.py`: exit 0 after toolchain update. The first targeted filter compiled successfully but selected 0 tests because `evolution_desktop` is gated behind `agent`; task validation passed with 66 task directories checked.
- `cargo test -p skilllite-commands --features agent manual_trigger_summary_clip`: exit 0; 2 tests passed, including the UTF-8 boundary regression.
- `cargo clippy --all-targets -- -D warnings && cargo clippy -p skilllite-commands --features agent --all-targets -- -D warnings && cargo test`: exit 0; full test output ended with all workspace tests/doc-tests passing.

## Regression Scope

- Areas likely affected: `skilllite evolution run --log-manual-trigger` audit/log summary text; desktop manual evolution trigger bridge.
- Explicit non-goals: changing evolution run scheduling, database schema, or pending skill handling.

## Links

- Source TODO section: N/A
- Related PRs/issues: Recent PR #79 / #81 evolution L2 CLI bridge changes.
- Related docs: `spec/verification-integrity.md`, `spec/rust-conventions.md`, `spec/testing-policy.md`
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-04-29 (TASK-2026-064 env-keys single-source done)
Last updated: 2026-05-29 (TASK-2026-066 utf8 evolution log truncate done)

## In Progress

Expand All @@ -17,6 +17,7 @@ Last updated: 2026-04-29 (TASK-2026-064 env-keys single-source done)

## Done

- `TASK-2026-066-utf8-evolution-log-truncate` - Status: `done` - Owner: `agent`
- `TASK-2026-064-env-keys-single-source` - Status: `done` - Owner: `agent`
- `TASK-2026-063-extension-tool-metadata-dispatch` - Status: `done` - Owner: `agent`
- `TASK-2026-062-wiki-lesson-optimization-template` - Status: `done` - Owner: `agent`
Expand Down
Loading