feat(intentions): Phase 7 — if-then rules for task / time / triage triggers
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

Small if-then automation layer. Rules persist in SQLite; the runner
lives on the frontend and binds to the Phase 6 event bus so the rule
pipeline reuses the same delivery primitives (timer events, TTS,
Tasks navigation).

Storage:
- Migration v12 adds implementation_rules (id, enabled, trigger_kind,
  trigger_value, actions_json, last_fired_key, created_at,
  updated_at) with enabled+trigger_kind index for the runner's hot
  path.
- CRUD helpers: insert / list / get / set-enabled / mark-fired /
  delete, plus a round-trip test.

Commands (all main-window-guarded via ensure_main_window):
- list_implementation_rules
- create_implementation_rule — validates HH:MM, checks the target
  task exists at save time for surface-task actions, caps
  speak-line at 240 chars, pins v1 timers to 5 minutes.
- set_implementation_rule_enabled
- mark_implementation_rule_fired — main-thread idempotency shim so
  the runner can atomically claim a fire.
- delete_implementation_rule

Runner (implementationIntentions.svelte.ts):
- Subscribes to kon:task-completed and kon:morning-triage-finished
  (MorningTriageModal now emits on all three exit paths — empty,
  skipped, picked — so skip counts as finishing).
- 30 s poll for time-of-day rules, plus an immediate check on startup
  so a rule whose time has already passed today catches up once.
- Idempotency via last_fired_key composed as YYYY-MM-DD@HH:MM for
  time rules; new time rules whose HH:MM has already passed today
  are pre-seeded so they don't fire retroactively on save.
- Rules are paused when Nudges "Mute for now" is on — a hard mute
  stops all rule delivery in addition to OS notifications.
- Stale-task safety: if a surface-task action's target has been
  deleted, the runner opens Tasks and warns clearly rather than
  pretending to surface something that's gone.

Editor (ImplementationRulesEditor.svelte):
- Lives in Settings under a new "If-then rules" accordion section.
- `If` picker: time of day (with time input), a task completes,
  morning triage finishes.
- `Then` composer: optional surface (inbox / today / all tasks /
  specific task), optional 5-min timer, optional speak-aloud line.
- Saved rules list with enable toggle + delete.

Rules table integration for Phase 10b rename sweep: add
implementation_rules to the kon.db → corbie.db migration shim when
that phase lands.

Gates: fmt, clippy -D warnings, cargo test 265/0, svelte-check
0/0, npm build green. Pre-existing Vite chunk warning on sounds.ts
is unrelated to Phase 7.
This commit is contained in:
2026-04-24 19:27:06 +01:00
parent eebea8cb9a
commit 6cd1c22c0f
12 changed files with 1180 additions and 10 deletions

View File

@@ -16,6 +16,11 @@ export type LlmPromptPreset = "default" | "email" | "notes" | "code";
export type AiGpuConcurrency = "parallel" | "sequential";
export type TaskBucket = "inbox" | "today" | "soon" | "later";
export type ToastSeverity = "info" | "success" | "warn" | "error";
export type ImplementationRuleTriggerKind =
| "time_of_day"
| "task_completed"
| "morning_triage_finished";
export type ImplementationRuleSurfaceTarget = "inbox" | "today" | "tasks" | "task";
export interface PageState {
current: string;
@@ -249,6 +254,43 @@ export interface TaskEntry {
energy: EnergyLevel | null;
}
export type ImplementationRuleAction =
| {
kind: "surface";
target: ImplementationRuleSurfaceTarget;
taskId?: string | null;
label?: string | null;
}
| {
kind: "start_timer";
seconds: number;
taskId?: string | null;
label?: string | null;
}
| {
kind: "speak_line";
text: string;
};
export interface ImplementationRule {
id: string;
enabled: boolean;
triggerKind: ImplementationRuleTriggerKind;
triggerValue: string;
actions: ImplementationRuleAction[];
lastFiredKey: string | null;
createdAt: string;
updatedAt: string;
}
export interface ImplementationRuleDraft {
enabled?: boolean;
triggerKind: ImplementationRuleTriggerKind;
triggerValue: string;
actions: ImplementationRuleAction[];
lastFiredKey?: string | null;
}
export interface TaskDraft {
text: string;
bucket?: TaskBucket;