agent: lumotia — v0.1 release-completion run
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled

Closes the code-side v0.1 ship gate. All quality gates green:
cargo fmt/clippy/test (~327 tests), npm check (0/0), vitest 13/13,
scripts/dogfood-rebrand-drill.sh 8/8.

Phase F — first-run onboarding promoted to v0.1
- FirstRunPage with skip-to-main + failure recovery + event recording
- Six onboarding commands (record/list/has-completed + lumotia_events)
- Storage migration v17 (onboarding_events + lumotia_events tables)

UI hardening (in-scope items from v0.1-ui-hardening.md)
- StatusPill + PostCaptureCard components, 21st preview entry
- Sidebar recording-as-sacred-state (opacity + aria-disabled, reduced-motion)
- Settings 6-section regroup + Help section + Activation log + Privacy toggle
- Error-state copy sweep (DictationPage + SettingsPage, plain-language)
- Global :focus-visible rule, textarea outlines restored
- Ctrl+K / Ctrl+, / Escape bindings in +layout

LLM resilience
- rule_based_extract_tasks (regex-free imperative-verb extractor) +
  extract_tasks_with_fallback wrapper — task extraction never returns zero
- tokio::time::timeout(120s) wraps cleanup/tags/tasks commands

Release artefacts
- LICENSE (canonical AGPL-3.0), CHANGELOG (Keep-a-Changelog format)
- v0.1-release-notes, privacy-and-ai-use, install-warnings,
  tester-onboarding-kit, tester-acceptance-runbook, code-signing-setup,
  apple-silicon-rb08-runbook, virtual-audio-setup, v0.1-contrast-audit
- Workspace versioning + AGPL spdx; npm exact-pin (10 ranges removed)
- AppImage SHA-256 sidecar in build.yml
- README v0.1 section + Reporting-issues; canonical repo slug

Closure pass — items moved from human-required to code-complete
- KI-02 Linux idle inhibit: zbus 5 → org.freedesktop.login1.Manager.Inhibit
- KI-03 Windows sleep prevention: SetThreadExecutionState(ES_CONTINUOUS|...)
- acquire/release_idle_inhibit Tauri commands, wired in DictationPage
- Diagnostic-bundle frontend wire-up (Settings → Help button)
- WCAG-AA contrast fix via .btn-filled-text utility (no token changes)
- 8 destructive-action sites wrapped in plain-language confirm() guards
- KNOWN-ISSUES.md + v0.1-known-limitations.md updated (KI-02/03 fixed)

Scripts
- pre-tag-verify.sh, tag-day.sh, smoke-linux + driver
- parse-diagnostic-bundle.sh, parse-activation-log.py

Per-item audit trail: docs/release/v0.1-completion-status.md
Remaining: W-01…W-08 (signing certs, hardware probes, smoke matrix,
tester recruitment) — see docs/release/v0.1-known-limitations.md.
This commit is contained in:
2026-05-15 06:59:08 +01:00
parent bf1b68275a
commit 3770815fbf
77 changed files with 8697 additions and 1017 deletions

View File

@@ -26,6 +26,20 @@ const MAX_CONTEXT_TOKENS: u32 = 8192;
const CONTEXT_RESERVE_TOKENS: u32 = 64;
const GENERATION_SEED: u32 = 0;
/// Maximum number of tasks returned by the rule-based fallback extractor.
/// Caps output to avoid wall-of-text dumps when the transcript is dense.
const MAX_RULE_BASED_TASKS: usize = 10;
/// Indicates which extraction path produced the task list.
/// Propagated to callers so the UI can label rule-based results accordingly.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TaskExtractionSource {
/// Tasks extracted by the local LLM.
Llm,
/// LLM path failed; tasks extracted by the rule-based regex fallback.
RuleBased,
}
#[derive(Debug, thiserror::Error)]
pub enum EngineError {
#[error("LLM not loaded. Download an AI model in Settings.")]
@@ -553,6 +567,31 @@ impl LlmEngine {
parse_string_array(&raw)
}
/// Wrapper around [`extract_tasks_with_feedback`] that NEVER returns
/// an error: if the LLM path fails for any reason the rule-based
/// extractor fires as a safety net, satisfying the data-loss contract
/// documented in `docs/release/v0.1-known-limitations.md`.
///
/// Returns `(tasks, source)` where `source` tells the caller which
/// path produced the results so the UI can label them.
pub fn extract_tasks_with_fallback(
&self,
transcript: &str,
examples: &[prompts::FeedbackExample],
) -> (Vec<String>, TaskExtractionSource) {
match self.extract_tasks_with_feedback(transcript, examples) {
Ok(tasks) => (tasks, TaskExtractionSource::Llm),
Err(err) => {
tracing::warn!(
"LLM task extraction failed; using rule-based fallback: {}",
err
);
let tasks = rule_based_extract_tasks(transcript);
(tasks, TaskExtractionSource::RuleBased)
}
}
}
fn loaded_handles(&self) -> Result<(Arc<LlamaBackend>, Arc<LlamaModel>), EngineError> {
let guard = self.inner.lock().unwrap();
let backend = guard.backend.clone().ok_or(EngineError::NotLoaded)?;
@@ -741,6 +780,129 @@ fn parse_string_array(raw: &str) -> Result<Vec<String>, EngineError> {
Ok(normalized)
}
/// Rule-based task extractor used as the safety net when the LLM extraction
/// path fails. Per `docs/release/v0.1-known-limitations.md`, task extraction
/// must NEVER return zero tasks just because the LLM failed.
///
/// Heuristic: split on sentence boundaries (`. ? ! \n`), keep sentences that
/// begin with (or contain near the start) an imperative-style cue. Trim,
/// dedupe, cap at [`MAX_RULE_BASED_TASKS`] to avoid wall-of-text dumps.
pub fn rule_based_extract_tasks(transcript: &str) -> Vec<String> {
// Filler words that may precede the real imperative start.
const FILLER: &[&str] = &["and ", "so ", "then ", "also ", "well ", "okay ", "ok "];
// Phrase-level cues (checked against the lowercased sentence start).
const PHRASE_CUES: &[&str] = &[
"i need to ",
"i should ",
"i have to ",
"i must ",
"need to ",
"got to ",
"have to ",
"must ",
"let me ",
"let's ",
"lets ",
"remember to ",
"don't forget to ",
"dont forget to ",
"don't forget ",
"dont forget ",
"make sure to ",
"make sure i ",
"todo:",
"to-do:",
"task:",
];
// Bare imperative verbs expected at the start of a sentence.
const IMPERATIVE_VERBS: &[&str] = &[
"send",
"write",
"call",
"email",
"fix",
"update",
"review",
"check",
"finish",
"schedule",
"book",
"order",
"buy",
"ask",
"follow up",
"followup",
"create",
"add",
"remove",
"delete",
"submit",
"upload",
"download",
"install",
"configure",
"test",
"deploy",
"merge",
"close",
"open",
"share",
"contact",
"reach out",
"prepare",
"draft",
"complete",
"reply",
"respond",
];
// Split on sentence-terminating punctuation and newlines.
let sentences: Vec<&str> = transcript.split(['.', '?', '!', '\n']).collect();
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut results: Vec<String> = Vec::new();
for raw in sentences {
let trimmed = raw.trim();
if trimmed.is_empty() {
continue;
}
// Build a lowercase version for matching, stripping leading filler.
let mut lc = trimmed.to_lowercase();
for filler in FILLER {
if lc.starts_with(filler) {
lc = lc[filler.len()..].trim_start().to_string();
break;
}
}
let is_task = PHRASE_CUES.iter().any(|cue| lc.starts_with(cue))
|| IMPERATIVE_VERBS.iter().any(|verb| {
lc.starts_with(verb)
&& lc
.as_bytes()
.get(verb.len())
.map(|&b| b == b' ' || b == b',')
.unwrap_or(true)
});
if is_task {
let key = lc.clone();
if seen.insert(key) {
results.push(trimmed.to_string());
if results.len() >= MAX_RULE_BASED_TASKS {
break;
}
}
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1061,4 +1223,50 @@ mod tests {
.unload()
.expect("unload after load completes must succeed");
}
// ── rule_based_extract_tasks ──────────────────────────────────────────
#[test]
fn rule_based_extract_finds_explicit_imperatives() {
let t = "I need to send Sarah the report tomorrow. Don't forget the slide deck.";
let tasks = rule_based_extract_tasks(t);
assert_eq!(tasks.len(), 2, "expected 2 tasks, got: {tasks:?}");
assert!(
tasks[0].to_lowercase().contains("send sarah"),
"first task should mention 'send sarah': {tasks:?}"
);
assert!(
tasks[1].to_lowercase().contains("slide deck"),
"second task should mention 'slide deck': {tasks:?}"
);
}
#[test]
fn rule_based_extract_caps_at_max() {
let t = "Send email. Write doc. Call client. Fix bug. Update spec. Review PR. Check tests. Finish report. Schedule meeting. Book hotel. Order parts. Buy supplies.";
let tasks = rule_based_extract_tasks(t);
assert!(
tasks.len() <= MAX_RULE_BASED_TASKS,
"expected at most {MAX_RULE_BASED_TASKS} tasks, got {}",
tasks.len()
);
}
#[test]
fn rule_based_extract_returns_empty_for_no_imperatives() {
let t = "The weather is lovely today. The garden looks nice.";
let tasks = rule_based_extract_tasks(t);
assert_eq!(tasks.len(), 0, "expected 0 tasks, got: {tasks:?}");
}
#[test]
fn rule_based_extract_dedupes_repeated_sentences() {
let t = "I need to send the report. I need to send the report.";
let tasks = rule_based_extract_tasks(t);
assert_eq!(
tasks.len(),
1,
"expected 1 deduplicated task, got: {tasks:?}"
);
}
}