Development
Coach engine
How the desktop Coach page grades practice: the 27-rule catalog, scoring, and the flow, pace, timeline, and output analyzers.
Coach engine
The desktop Coach page turns the archive’s per-call rows into practice scores, anti-pattern findings, flow/pace analysis, a per-day session timeline, and AI code-output stats. Everything is computed locally and deterministically in src/coach/ — no network calls, no LLM calls.
Attribution
The scoring model, rule catalog, and flow/pace/timeline/output algorithms are ported from Microsoft’s AI-Engineering-Coach VS Code extension (MIT, Copyright (c) Microsoft Corporation) — see NOTICE at the repository root. The port is Rust-native: rules are code, not the reference’s user-editable markdown DSL. Thresholds keep the reference defaults unless a deviation is listed below.
Data model: calls → turns → sessions
tokens’ parsers emit one ParsedCall per assistant API round; the reference evaluates rules over one row per user turn. src/coach/sessions.rs therefore collapses consecutive calls of a session that share the same prompt state (user_message, prompt_chars) into a Turn before rules run:
- token fields, tool calls, code blocks, and file lists are summed/concatenated across the turn’s rounds;
elapsed_msis the max round latency (the last round’s user→assistant gap ≈ full turn latency);- two identical prompts sent back-to-back merge into one turn — an accepted simplification.
User think-time between turns is think_gap_ms (next turn start − next turn latency − current turn end), used by flow scoring and speed-accept.
src/coach/signals.rs gates rule denominators per tool so “no signal” never reads as “clean” (or as a finding): cancellation, file references, cache reads, and conversational-session shape are currently claude-code + codex only. The per-tool signal matrices live in docs/development/tools/<name>.md.
Practice scores (src/coach/score.rs)
- Severity penalties: high = 12, medium = 7, low = 3, applied once per triggered rule — occurrence counts do not move the score.
score = max(0, round(100 · (1 − penalty / (rules_in_group × 12))))per group.- Weekly series: calls bucketed by local ISO week, the full rule set re-run per bucket. WoW compares the last two weekly scores; MoM compares the mean of the last 4 weeks against weeks 5–8 back. Fewer buckets → “–”. The trailing 12 weekly scores per group ship in the payload as
trendfor the report-card sparklines. - Composite grade:
composite_scoreis the rules-weighted mean of the group scores —round(Σ(score·rules_in_group) / 27)— so a group carries the share of the overall grade its rules hold in the catalog.grade_idmaps any 0–100 score to a letter id: A+ ≥ 97, A ≥ 90, B+ ≥ 85, B ≥ 80, C ≥ 70, D ≥ 60, else F (copy maps ids to letters viacoach.report.grade_labels). The composite plus its grade ship asCoachData.overall; each group also carries its owngrade_id.
Rule catalog (27 rules, src/coach/rules/)
Groups: prompt-quality 8, session-hygiene 9, code-review 5, tool-mastery 5. User-facing wording lives in src/copy/copy.json under coach.rules.<id> (a test asserts the ids match). Signals: P prompt text/length, T timestamps, K tokens, M model, L tool calls, C code blocks, F file lists, X cancellation.
| id | group | severity | trigger (defaults) | signals |
|---|---|---|---|---|
| lazy-prompting | prompt-quality | medium | >30% of prompts <30 chars, count >10 | P |
| caps-lock | prompt-quality | medium | ≥1 message ≥90% caps | P |
| frustration-signals | prompt-quality | medium | ≥2 messages with !!!/???/hostile phrases or ≥40% caps words | P |
| repeated-prompts | prompt-quality | medium | ≥3 near-duplicates (first-100-chars key); high >20 | P |
| low-constraint-usage | prompt-quality | medium | <8% of ≥40-char prompts contain constraints, ≥30 samples | P |
| verbose-output | prompt-quality | medium | >10% of turns: >5K output tokens from <200-char prompts, count >10 | P K |
| verbose-prompt-no-compression | prompt-quality | low | >20% of prompts ≥800 chars with 2+ filler words, count >15 | P |
| excessive-file-context | prompt-quality | medium | ≥10 turns referencing ≥30 files | F |
| session-drift | session-hygiene | medium | >3 sessions covering ≥4 work types in ≥5 turns | P |
| late-night-coding | session-hygiene | low | >10 turns between 00:00–05:00 local | T |
| weekend-overwork | session-hygiene | low | >25% weekend turns, >20 weekend turns | T |
| mega-sessions | session-hygiene | high | any session ≥50 turns | — |
| abandon-sessions | session-hygiene | low | >40% single-turn sessions, >10 abandoned | — |
| high-cancellation | session-hygiene | medium | >15% interrupted turns; high >30% | X |
| broken-flow-state | session-hygiene | medium | >60% of ≥5 scored days fragmented; high >80% | T |
| slow-responses | session-hygiene | low | >5 turns over 5 min (elapsed) | T |
| runaway-agent-loops | session-hygiene | high | ≥3 turns with 40+ tool calls | L |
| copy-paste-blindness | code-review | high | ≥3 sessions: ≥50 AI LoC, no refinement afterwards | C P F |
| speed-accept | code-review | high | ≥5 times: next message <15s after ≥20 AI LoC | C T |
| vibe-coding | code-review | high | ≥3 sessions: ≥100 AI LoC from ≤5 unstructured prompts | C P |
| tunnel-vision | code-review | low | >95% of turns in one project (≥3 projects, ≥50 turns) | — |
| no-language-exploration | code-review | low | no new language in 8+ ISO weeks (≥4 active weeks); high >12 | C T |
| mcp-tool-bloat | tool-mastery | medium | ≥3 sessions with >40 distinct tools | L |
| model-overreliance | tool-mastery | medium | >80% of turns on one model, <3 models, >10 turns | M |
| reasoning-effort-overuse | tool-mastery | medium | >50% of effort-tagged turns at high/max, >20 tagged | M |
| premium-waste | tool-mastery | medium | >10 trivial/lookup turns on premium models | P M C L |
| cache-hit-starvation | tool-mastery | medium | >20 turns >5K input tokens with <10% cache-read share | K |
Deviations from the reference
- runaway-agent-loops raises the per-turn tool threshold from 15 to 40: tokens’ turns aggregate full agentic CLI loops where 15+ tool calls is routine.
- slow-responses raises the latency threshold from 30s to 5 minutes for the same reason — a turn spans the whole agentic run, and 300s matches the flow score’s slowest latency band.
- mcp-tool-bloat implements the documented semantics (distinct tools per session); the reference’s DSL referenced a nonexistent field and could never fire.
- premium-waste merges the reference’s
premium-wasteandpremium-for-lookup-questions, and derives “premium” from tokens’ own pricing book (output rate ≥ $10/M tokens) instead of a hardcoded model-tier table. - verbose-prompt-no-compression drops the “compression skill installed” exemption (no installed-skills signal) and matches filler words on the stored 500-char prompt prefix.
- Regex patterns are reimplemented as word-boundary phrase matching in
src/coach/text.rs(no regex dependency); semantics match, including quirks such as\btest\bnot matching “tests”. - Content rules run on the 500-char
user_messageprefix; length rules use the fullprompt_chars.
Skipped rules
Reference rules whose inputs tokens does not ingest (most flagged requiresIdeContext upstream): yolo-mode, no-plan-mode, no-skills, no-slash-commands, no-custom-instructions, auto-approve-terminal, agent-mode-for-asks, no-devcontainer, auto-avoidance, no-file-context, instruction-bloat, context-engineering-gaps, no-spec-driven-development, no-spec-structure, low-markdown-ratio, agentic-no-tools — those signals only exist in VS Code chat logs (tool confirmations, agent modes, instruction files) or presume IDE-style sessions. profanity is skipped as a judgment call: scanning prompts for swearing reads as surveillance in a usage dashboard. All LLM-dependent reference features (Skill Finder, Learning Center, Context Health AI review) are permanently out of scope under the no-network rule.
Flow, pace, timeline, output
- Flow (
flow.rs): per session (≥3 timestamped turns)score = 0.40·rapid-follow-up rate (≤30s) + 0.30·median-gap band + 0.15·duration band + 0.15·density band; labels deep ≥70 / moderate ≥45 / shallow ≥25 / else fragmented. Days merge session spans into work blocks split at >15 min gaps. The summary counts deep (≥70) and fragmented (<25) days; both counts and the daily scores ship in the payload for the KPI panel and recent-flow sparkline. - Pace (
pace.rs): late-night = local hour ≥22 or <5; weekend = Sat/Sun; streak = consecutive active days; alerts on streak ≥14, rising 3-week trends (+20% band), late-night rate >0.15, weekend rate >0.25; risk high at 3+ alerts or (streak ≥14 ∧ late-night rising). - Timeline (
timeline.rs): per local day, one row per session, blocks split at >15 min gaps, max concurrency via a start/end event sweep. - Output (
output.rs): folds per-callcode_blocks(fences + Write/Edit payloads, merged by language) into LoC by language/day/project/model, and names tools contributing no code-output signal. - Projects (
projects.rs): per-project detail for the Activity → Projects cards. Estimated active time reuses the timeline block model (call timestamps split at >15 min gaps, each block spanning at least one minute, summed per session). Languages fold turncode_blocksby LoC; hot files count per-calledited_files(paths shown project-relative, else the trailing two segments) and ship the top 3. The work-pattern chip is a display heuristic of this port, not an upstream rule: weekend share of timestamped turns ≥60% reads “mostly weekends”, ≤20% “mostly weekdays”, else “weekends + weekdays”; a daypart (mornings 05–11, afternoons 12–16, evenings 17–21, late nights 22–04, matching the pace module’s late-night window) is appended when it carries ≥40% of timestamped turns. All labels resolve throughcoach.timeline.pattern_*copy ids.
Wiring
timeline::daily_turn_counts buckets turns by local day (a turn lands on its first timestamped call’s day), windowed by timeline::grid_window_days anchored at today — a trailing 9 weeks for scoped periods, 53 weeks for All Time; the counts ship as timeline_grid with a per-day in_period flag and drive the daily-bars day picker above the session table. The day picker is built from tool/project-filtered calls that ignore the period filter — same scope as coach_timeline — because context days outside the period stay visible (dimmed bars) on the strip. coach_timeline and the AI-Output project ranking resolve project names through the dashboard’s project_label short labels, so the same project reads identically across pages.
Ingested::coach / Ingested::coach_timeline (src/ingest/pipeline.rs) filter by period/tool/project and delegate to coach::coach_data / coach::coach_timeline, which build the CoachData / CoachTimelineDay payloads in src/data/mod.rs. App::coach_for / App::coach_timeline_for (src/app.rs) memoize per filter key in the generation-keyed QueryCache (bounding the data::leak growth exactly like Analytics). The desktop exposes get_coach(period) and get_coach_timeline(day) Tauri commands; after a timeline row is selected, the existing get_session_detail(key) command supplies its call-level inspector. The Work Hours view deliberately reuses the memoized get_analytics(period) hour×weekday matrix and the dashboard activity timeline rather than introducing a second aggregation with different semantics. The Coach page fetches on the period|tool|project|data_generation|currency key. Sample mode (Shift+D) serves data::coach_sample().
The archive v4 enrichment columns that feed all of this are documented in docs/development/architecture.md and per-parser in docs/development/tools/<name>.md.