From 402a954a56200a46f829a79d5fc86a57a3cc9cef Mon Sep 17 00:00:00 2001 From: Lux Solari Date: Thu, 2 Jul 2026 12:43:04 -0300 Subject: [PATCH] Release 1.6.0: grounding research and Track Setup topic shortcut Custom-track generation now checks its facts. Before writing exercises for a fast-moving library/framework with a versioned API (a game/UI library, a cloud SDK, a build tool), Sage uses WebSearch/WebFetch to pull current docs first, recording what it consulted in a new optional `sources` field. Validated live on Raylib: the research genuinely caught that Raylib is on v6.0 and shaped the generated verify command's real linker flags, not a plausible-sounding guess. Stable fundamentals (a language's own core syntax) still skip this entirely -- no need to burn a research round on what training data already gets right. Also fixes a real redundancy: Track Setup used to ask "what do you want to learn?" even when the learner's trigger message already said so. Now a stated topic matching no bundled curriculum skips Round 0 and Round 1 outright; a topic matching one compresses Round 0 into a direct named confirm instead of the generic list, without removing the custom-track choice. Two new scenarios (09, 10) validate both features live, 10/10 and 10/10 respectively. Scenario 06 gained a no-sources assertion and a staleness fix -- its original "Rust" example collided with the rust-cli curriculum added in 1.5.0, so Round 0 was silently offering three options instead of the two it assumed. Both 06 and 09 had their trigger messages reverted to topic-neutral, since the new shortcut rule would otherwise entangle their own regression targets with Round 0's behavior. All ten Tier 2 scenarios now pass. Pre-release checklist bumped from "all eight" to "all ten." Version bumped to 1.6.0. Co-Authored-By: Claude Sonnet 5 --- .claude-plugin/plugin.json | 2 +- CHANGELOG.md | 70 +++++++++++ CONTRIBUTING.md | 2 +- skills/sage-instructor/SKILL.md | 9 +- skills/sage-instructor/curricula/TEMPLATE.md | 7 ++ tests/README.md | 2 +- tests/scenarios/06-custom-track-creation.md | 42 +++++-- .../09-grounding-research-trigger.md | 98 +++++++++++++++ .../10-track-setup-topic-shortcut.md | 112 ++++++++++++++++++ 9 files changed, 327 insertions(+), 17 deletions(-) create mode 100644 tests/scenarios/09-grounding-research-trigger.md create mode 100644 tests/scenarios/10-track-setup-topic-shortcut.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 2f7846a..3939428 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "sage-instructor", - "version": "1.5.0", + "version": "1.6.0", "description": "Adaptive programming instructor — structured courses with discovery-first teaching, AskUserQuestion interactions, progress tracking, and pluggable curricula. Powered by the Three Axes Framework.", "author": { "name": "Lux Solari", diff --git a/CHANGELOG.md b/CHANGELOG.md index 268984e..67cdfed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,75 @@ # Changelog +## [1.6.0] — 2026-07-02 + +### Added +- **Grounding-research step in Track Setup.** Before generating a custom + curriculum, Sage now judges whether the stated topic is a fast-moving + library/framework with a versioned, changing API (vs. stable fundamentals + training data already gets right) and, if so, uses WebSearch/WebFetch to + pull current docs before writing a single exercise — recording what it + consulted in a new optional `sources` YAML field (see `TEMPLATE.md`). + Motivated by the original "use an LLM to teach me stuff" premise: a + generated curriculum for something like Raylib is only as good as how + current its underlying facts are. +- **Track Setup topic shortcut.** When the learner's trigger message already + names a specific topic (e.g. "teach me Go, let's build something" instead + of a bare `/sage-start`), Round 0 no longer asks blind: a topic matching no + bundled curriculum skips Round 0's question entirely (and Round 1, since + "what do you want to learn?" was already answered); a topic matching one + compresses Round 0 into a direct confirm naming that track instead of the + generic list. A genuinely ambiguous topic still asks normally. +- **`tests/scenarios/09-grounding-research-trigger.md`.** Tests the + grounding-research trigger branch (C++/Raylib) — confirms a real + WebSearch/WebFetch call happens, the `sources` field gets populated with + genuine references, and at least one generated API element traces back to + what was actually found (not a plausible-sounding invention). +- **`tests/scenarios/10-track-setup-topic-shortcut.md`.** Two-part scenario + for the new topic shortcut: Part A (Elixir, no bundled match) proves the + skip branch; Part B (Python, matches `python-basics`) proves the compress + branch still offers — and honors — the custom-track alternative rather + than railroading toward the match. + +### Changed +- **`tests/scenarios/06-custom-track-creation.md`** gained a no-`sources` + assertion (proving the grounding-research step correctly skips for stable + topics), and its trigger message was reverted to topic-neutral ("Hey, I + want to pick up a new skill — what have you got?", topic revealed at Round + 1 instead). Needed because the scenario's own regression target — Round 0 + presenting the full option list — only holds when no topic was pre-stated; + a topic-naming trigger now legitimately changes Round 0's behavior under + the new shortcut rule. Also swapped its example topic from Rust to Go: the + original topic collided with the `rust-cli` curriculum added in 1.5.0, + making Round 0 offer three options instead of the two the scenario assumed + — a real staleness bug this session's own earlier release introduced. +- **`tests/scenarios/09-grounding-research-trigger.md`**'s trigger message + was likewise reverted to topic-neutral for the same reason, keeping it + focused on one regression target (grounding-research) rather than + entangling it with the new shortcut rule. + +### Verified +- **`09-grounding-research-trigger`**: 10/10 PASS. Real tool calls confirmed + (WebSearch + two WebFetch calls against raylib.com and its GitHub wiki); + the generated `verify` command's linker flags and the exercises' API + calls (`InitWindow`, `DrawCircle`, `RAYWHITE`, ...) all traced back to the + actual fetched content, including catching that Raylib is currently on + v6.0 — a genuine grounding signal, not a coincidence. +- **`10-track-setup-topic-shortcut`**: 10/10 PASS (5/5 each part). Skip and + compress branches both confirmed; picking custom despite a compressed + match correctly avoided defaulting to the bundled track. +- **`06-custom-track-creation`** (re-run after the Go/topic-neutral fix): + 9/9 PASS. + - Soft findings not acted on this pass: the "genuinely ambiguous" branch + of the topic-shortcut rule has no worked example and remains untested — + candidate for a future scenario if the shortcut sees real use; the + grounding-research rule doesn't say how many sources are "enough" to + prove a claim was checked rather than assumed; a scratch-test-generated + curriculum's `verify` command was Linux-flag-specific with no + `prerequisites` callout — a curriculum-quality nit in generated output, + not a `SKILL.md` defect. +- Pre-release checklist (`tests/README.md`/`CONTRIBUTING.md`) updated from + "all eight" to "all ten" scenarios. + ## [1.5.0] — 2026-07-02 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e419654..bcce404 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ Open an issue describing: what you expected, what happened, and your Claude Code `python3 tests/test_check_progress_schema.py` (also run in CI) covers the deterministic Tier 1 checks and needs no agent. 5. Bumping the plugin version? Run `tests/README.md`'s full pre-release - checklist (all eight Tier 2 scenarios) first. + checklist (all ten Tier 2 scenarios) first. 6. Submit a PR with a clear description ## Code of Conduct diff --git a/skills/sage-instructor/SKILL.md b/skills/sage-instructor/SKILL.md index d2c2535..d2dbe04 100644 --- a/skills/sage-instructor/SKILL.md +++ b/skills/sage-instructor/SKILL.md @@ -89,13 +89,20 @@ Generate `.sage-profile.md` in the project root (using `references/learner-profi **Round 0 — Existing tracks** (only when Track Setup was triggered by onboarding — i.e. no `active_track` yet. Skip entirely when the learner explicitly ran `/new-track`: that command already states custom-track intent, don't second-guess it. Also skip if `curricula/` has nothing besides `TEMPLATE.md`.) If applicable, ask: "Want to start one of these, or build something custom?" Options: one per existing curriculum (using its `title`), plus "Build a custom track." If the learner picks an existing one, set it as `active_track`, confirm, and skip straight to Phase 0 — the rest of Track Setup is only for the custom path. -**Round 1** — "What do you want to learn?" (free text) +**If the trigger message already named a specific topic** (e.g. "teach me Go, let's build something" rather than a bare `/start`), don't ask Round 0 blind — use what was already said: +- Clearly matches no existing curriculum's `title`/`teaches` → skip the question entirely, there's nothing to choose between. Say so in one line ("No bundled track covers Go yet, so let's build you a custom one.") and go straight into the custom-track interview. +- Clearly matches one → still ask, but as a direct confirm naming that track, not the generic list: "Sounds like Python — want the bundled Python Foundations track, or build a custom one instead?" This is still a real choice (structured bundled track vs. one generated fresh) and shouldn't be assumed away. +- Genuinely ambiguous which it matches, or matches more than one plausibly → ask Round 0 as normal rather than guessing. + +**Round 1** — "What do you want to learn?" (free text) — skip asking if the trigger message already answered this; carry that stated topic forward as Round 1's answer instead of asking the learner to repeat it. **Round 2** — "Is there a project this feeds into?" Options: Yes (follow up), No — general skill building **Round 3** — "How much do you know already?" Options: From scratch, Basics but rusty, Intermediate, Know a related language **Round 4** — "What's the priority?" Options: Pure learning (Growth), Build while learning (Balanced), Get productive fast (Output) Rounds 3 and 4 map directly to the curriculum's `mastery` and `intent` axis fields (Round 4's options are literally labeled with the axis values). `consequence` has no dedicated round — infer it from Round 2's answer instead: no stated project, or a personal/learning-only one → `low`; a project the learner describes as shared, user-facing, or otherwise stakes-bearing → `medium` or `high`, using judgment on what was actually described, not a fixed default. +**Before generating, judge whether the topic needs grounding.** Training data goes stale fastest for fast-moving libraries/frameworks with versioned, changing APIs (a game/UI library, a cloud SDK, a build tool), and is thinnest for niche or narrow domains — Round 1's answer is the signal. When it applies, use WebSearch/WebFetch to pull the current official docs or getting-started material for what's being taught before writing a single exercise, so topics, exercise asserts, and the `verify` command reference the API as it actually exists now, not a plausible-sounding but possibly renamed/removed recollection. Skip this for stable fundamentals a language's core syntax rarely needs re-checking (e.g. "Python control flow," "SQL joins") — don't burn a research round on something training data already gets right. Record what was consulted in the generated curriculum's `sources` field (see `TEMPLATE.md`) when this step ran; omit the field entirely when it didn't. Either way, briefly tell the learner what (if anything) was checked before showing the generated curriculum, so they can flag it if a source looks wrong or outdated. + Generate curriculum from `TEMPLATE.md`, confirm with learner, save to `curricula/.md`. --- diff --git a/skills/sage-instructor/curricula/TEMPLATE.md b/skills/sage-instructor/curricula/TEMPLATE.md index 3e38784..9b5a66a 100644 --- a/skills/sage-instructor/curricula/TEMPLATE.md +++ b/skills/sage-instructor/curricula/TEMPLATE.md @@ -39,6 +39,13 @@ teaches: - Raylib - CMake +# Optional — real-world references Sage consulted while generating this +# curriculum (Track Setup's grounding-research step). Only present when that +# step ran; omit entirely for tracks generated from stable, well-known +# fundamentals that didn't need a live check. Title + URL is enough. +sources: + - "Raylib cheatsheet — https://www.raylib.com/cheatsheet/cheatsheet.html" + # How Sage verifies an exercise before marking it complete (Step 6b). # Either a shell command template — {file} is substituted with the exercise's # entry point — or the literal string "manual" for exercises with no diff --git a/tests/README.md b/tests/README.md index 2349dbb..fb4d0f9 100644 --- a/tests/README.md +++ b/tests/README.md @@ -90,7 +90,7 @@ per-commit CI check. ### Pre-release checklist Before bumping the plugin version (`.claude-plugin/plugin.json` + -`CHANGELOG.md`), run **all eight** scenarios in `scenarios/` — not just the +`CHANGELOG.md`), run **all ten** scenarios in `scenarios/` — not just the ones nearest whatever you changed. Record the result (pass/fail per assertion) in the version's CHANGELOG entry the way 1.2.0 and the harness's own rollout did, so "we ran the harness" is checkable later instead of diff --git a/tests/scenarios/06-custom-track-creation.md b/tests/scenarios/06-custom-track-creation.md index 67cf93c..f8802c5 100644 --- a/tests/scenarios/06-custom-track-creation.md +++ b/tests/scenarios/06-custom-track-creation.md @@ -13,13 +13,19 @@ This is the last major untested path from a fresh install. ## Setup Empty scratch directory. No `.sage-profile.md`, no `.sage-progress.json`. -`skills/sage-instructor/curricula/` contains `TEMPLATE.md` and -`python-basics.md` exactly as shipped — don't modify them for this scenario. +`skills/sage-instructor/curricula/` contains `TEMPLATE.md`, `python-basics.md`, +and `rust-cli.md` exactly as shipped — don't modify them for this scenario. +The learner's trigger message deliberately doesn't name a topic — this +scenario is testing Round 0's normal, un-tuned behavior (the full option +list), not the trigger-already-named-a-topic skip/compress path (see +scenario 10 for that). The topic (Go, which has no bundled equivalent among +these three files) is only revealed at Round 1. ## Script -1. Learner triggers Sage via natural language (not a slash command): "teach - me Rust, let's build something." +1. Learner triggers Sage via natural language (not a slash command), with no + topic stated yet: "Hey, I want to pick up a new skill — what have you + got?" 2. Profile Setup — answer each round: - Identity: "Marco, mobile developer" - Bridge languages: Java, Kotlin @@ -27,10 +33,12 @@ Empty scratch directory. No `.sage-profile.md`, no `.sage-progress.json`. - Learning style: "Hands-on first" - Tone: "Encouraging and patient" Confirm the generated profile looks right. -3. Track Setup Round 0 fires (curricula/ has `python-basics.md` beyond just - `TEMPLATE.md`, no `active_track` yet). Learner explicitly picks **"Build a - custom track"** — not "Python Foundations." -4. Round 1 ("What do you want to learn?"): "Rust." +3. Track Setup Round 0 fires (curricula/ has `python-basics.md` and + `rust-cli.md` beyond just `TEMPLATE.md`, no `active_track` yet, and no + topic was named yet — nothing to compress or skip). Learner explicitly + picks **"Build a custom track"** — not "Python Foundations" or "Rust CLI + Tools." +4. Round 1 ("What do you want to learn?"): "Go." 5. Round 2 ("Is there a project this feeds into?"): Yes — "A small CLI tool that recursively greps through a directory, like a mini ripgrep clone." 6. Round 3 ("How much do you know already?"): "From scratch." @@ -43,11 +51,11 @@ Empty scratch directory. No `.sage-profile.md`, no `.sage-progress.json`. ## Assertions - `[mechanical]` A new file exists at `curricula/.md` (some - Rust-related track slug) with a YAML header containing every field + Go-related track slug) with a YAML header containing every field `TEMPLATE.md` marks required: `track`, `title`, `destination`, `mastery`, `consequence`, `intent`, `bridge_from`, `teaches`, `verify`. - `[mechanical]` `verify` is a real, non-placeholder command appropriate for - Rust (e.g. involving `rustc` or `cargo`) — not `TEMPLATE.md`'s literal + Go (e.g. involving `go run` or `go build`) — not `TEMPLATE.md`'s literal C++/g++ example copied verbatim, and not empty/missing. - `[mechanical]` `.sage-progress.json`'s `active_track` matches the new curriculum's `track` slug, and `python3 tests/check_progress_schema.py @@ -66,6 +74,14 @@ Empty scratch directory. No `.sage-profile.md`, no `.sage-progress.json`. structure/header) and explicitly asked the learner to confirm **before** saving it — quote the confirmation prompt. The file must not exist on disk before that confirmation turn (check between steps 8 and 9). -- `[mechanical]` Round 0's options included "Build a custom track." alongside - "Python Foundations" (same mechanism scenario 01 already proved works for - the other branch — this exercises picking custom instead). +- `[mechanical]` Round 0 offered all three real options — "Python + Foundations", "Rust CLI Tools", and "Build a custom track." (per `SKILL.md` + line 89, one per existing curriculum plus custom) — and the learner picked + custom (same mechanism scenario 01 already proved works for an existing- + track pick — this exercises the custom branch instead). +- `[mechanical]` The generated YAML header has **no** `sources` field. "Go" + (the language, its core syntax/tooling) is stable-fundamentals territory + per `SKILL.md`'s grounding-research rule — training data doesn't need a + live check for this, so the field should be entirely absent, not + present-but-empty. (Contrast: scenario 09 exercises a topic where the field + *should* appear.) diff --git a/tests/scenarios/09-grounding-research-trigger.md b/tests/scenarios/09-grounding-research-trigger.md new file mode 100644 index 0000000..b700368 --- /dev/null +++ b/tests/scenarios/09-grounding-research-trigger.md @@ -0,0 +1,98 @@ +# Scenario 09 — Grounding-research trigger branch (custom-track creation) + +## Regression target + +`SKILL.md`'s Track Setup gained a grounding-research step: before generating +a custom curriculum, Sage judges whether the topic is a fast-moving +library/framework with a versioned, changing API (vs. stable fundamentals +training data already gets right), and if so, uses WebSearch/WebFetch to +pull current docs before writing exercises — recording what it consulted in +the curriculum's `sources` field. Scenario 06 exercises this same custom- +track path with "Rust" (a *stable* topic — its 06 assertions confirm the +step correctly does **not** fire). This scenario exercises the *other* +branch: a topic that should trigger it — bridging into a real, versioned +game library — and checks that the research actually happened (a real tool +call, not a narrated one) and actually shaped the output, not just that a +`sources` field got stapled on for show. + +## Setup + +Empty scratch directory. No `.sage-profile.md`, no `.sage-progress.json`. +`skills/sage-instructor/curricula/` contains `TEMPLATE.md`, `python-basics.md`, +and `rust-cli.md` exactly as shipped — don't modify them for this scenario. +This run needs real WebSearch/WebFetch access; if unavailable in the +execution environment, stop and report that as an environment limitation +rather than narrating a plausible-looking search result. The trigger message +deliberately doesn't name a topic — this scenario's regression target is the +grounding-research step at generation time, not Round 0's trigger-already- +named-a-topic skip/compress behavior (see scenario 10 for that); the topic +is revealed at Round 1 instead, so Round 0 fires normally here. + +## Script + +1. Learner triggers Sage via natural language, with no topic stated yet: "I + want to pick up something new for making small games on the side." +2. Profile Setup — answer each round exactly: + - Identity: "Sam, web developer" + - Bridge languages: JavaScript, TypeScript + - Experience: "Junior (1-3yr)" + - Learning style: "Hands-on first" + - Tone: "Sardonic humor welcome" + Confirm the generated profile looks right. +3. Track Setup Round 0 fires (curricula/ has more than TEMPLATE.md, and no + topic was named yet). Learner picks **"Build a custom track"**. +4. Round 1 ("What do you want to learn?"): "C++, and Raylib to build small + games." +5. Round 2 ("Is there a project this feeds into?"): Yes — "A couple small + personal games, just for fun, nothing shipped or shared with anyone." +6. Round 3 ("How much do you know already?"): "Know a related language." +7. Round 4 ("What's the priority?"): "Build while learning (Balanced)." +8. Before generating, Sage judges Raylib warrants a grounding check (a + versioned game library, not a language's stable core syntax) and actually + issues a WebSearch/WebFetch call for current Raylib docs — execute this + for real, don't narrate a plausible result. +9. Sage briefly tells the learner what it checked, then presents the + generated curriculum for confirmation. +10. Learner confirms. +11. Learner runs `/sage-lesson`. + +## Assertions + +- `[mechanical]` A new file exists at `curricula/.md` with a YAML + header containing every field `TEMPLATE.md` marks required: `track`, + `title`, `destination`, `mastery`, `consequence`, `intent`, `bridge_from`, + `teaches`, `verify`. +- `[mechanical]` The header's `sources` field **is present** and non-empty, + containing at least one real, plausible Raylib-related URL (e.g. an actual + raylib.com page) — the direct contrast to scenario 06's confirmed absence + for a stable topic. +- `[mechanical]` `verify` is a real, non-placeholder command appropriate for + compiling a C++/Raylib program (references an actual Raylib linking + flag/library, e.g. `-lraylib` or an equivalent build invocation) — not + `TEMPLATE.md`'s literal g++ example copied verbatim without adapting it + for Raylib linkage. +- `[behavioral]` The transcript shows a real WebSearch/WebFetch tool call + before the curriculum was generated, not a narrated/assumed result — quote + the query and confirm the returned content is genuine Raylib + documentation, not fabricated. +- `[behavioral]` Sage told the learner what it checked (a sentence + referencing the research, per `SKILL.md`'s grounding-research step) before + presenting the curriculum for confirmation — quote it. +- `[behavioral]` At least one specific Raylib API element named in the + generated exercises/topics (a function, constant, or concept) matches real + Raylib documentation rather than a plausible-sounding invention — name the + element and confirm it against what the research step actually found. +- `[behavioral]` `consequence: low` in the generated header — Round 2 + described unshipped, unshared personal games, matching `SKILL.md`'s + inference rule for a personal/learning-only project. Quote the header and + the reasoning. +- `[behavioral]` `mastery` reflects Round 3's "Know a related language" + answer and `intent: balanced` reflects Round 4 — quote the header and + confirm both map from the interview, not defaulted. +- `[behavioral]` Sage displayed the generated curriculum and explicitly + asked the learner to confirm **before** saving — quote the confirmation + prompt. The file must not exist on disk before that confirmation turn + (check between steps 9 and 10). +- `[mechanical]` `.sage-progress.json`'s `active_track` matches the new + curriculum's `track` slug, and `python3 tests/check_progress_schema.py + ` exits 0. diff --git a/tests/scenarios/10-track-setup-topic-shortcut.md b/tests/scenarios/10-track-setup-topic-shortcut.md new file mode 100644 index 0000000..1bc2e39 --- /dev/null +++ b/tests/scenarios/10-track-setup-topic-shortcut.md @@ -0,0 +1,112 @@ +# Scenario 10 — Track Setup's topic-already-stated shortcut + +## Regression target + +`SKILL.md`'s Track Setup gained a rule: when the learner's trigger message +already names a specific topic (instead of a bare `/sage-start`), don't ask +Round 0 blind. If the topic clearly matches no existing curriculum, skip +Round 0's question entirely and go straight into the custom-track interview +— and skip Round 1 too, since "what do you want to learn?" was already +answered. If it clearly matches one, compress Round 0 into a direct confirm +naming that track, instead of the generic "want to start one of these" +list. Neither branch has ever been exercised — scenarios 06/09 deliberately +use a topic-neutral trigger so their own regression targets (custom-track +generation, grounding-research) aren't entangled with this. This scenario +covers both branches directly, in two independent parts. + +## Setup + +Two independent empty scratch directories (Part A, Part B), each with no +`.sage-profile.md`/`.sage-progress.json`. Both use +`skills/sage-instructor/curricula/` containing `TEMPLATE.md`, +`python-basics.md`, and `rust-cli.md` exactly as shipped — don't modify them. + +## Part A — No match (skip Round 0 and Round 1 entirely) + +### Script +1. Learner triggers Sage via natural language: "Teach me Elixir, I want to + build something small." (No bundled curriculum teaches Elixir.) +2. Profile Setup — answer each round: + - Identity: "Priya, backend engineer" + - Bridge languages: Erlang, Python + - Experience: "Intermediate" + - Learning style: "Hands-on first" + - Tone: "Direct and concise" + Confirm the generated profile looks right. +3. Track Setup: per the new rule, Round 0's "want to start one of these, or + build something custom?" question should NOT be asked — Elixir matches + neither `python-basics` nor `rust-cli`. Sage should say so in one line + and go straight into the custom-track interview. +4. Round 1 should also be skipped — the topic was already stated in step 1. + Sage proceeds directly to Round 2 ("Is there a project this feeds + into?"): Yes — "A small message queue consumer for a personal project." +5. Round 3 ("How much do you know already?"): "Basics but rusty." +6. Round 4 ("What's the priority?"): "Pure learning (Growth)." +7. Sage generates the curriculum and presents it for confirmation. +8. Learner confirms. + +### Assertions +- `[behavioral]` Round 0's "want to start one of these, or build something + custom?" question (or the generic per-curriculum option list) was never + presented — quote the transcript showing the trigger message led directly + to a one-line acknowledgment plus the custom-track interview, not a + separate Round 0 turn. +- `[behavioral]` That acknowledgment explicitly said no bundled track covers + the stated topic (Elixir) — quote it. +- `[behavioral]` Round 1's "What do you want to learn?" was never separately + asked — the transcript goes from the acknowledgment straight to Round 2's + question, carrying "Elixir" forward as the already-given answer. +- `[mechanical]` The generated curriculum's `teaches` field names Elixir + (the topic actually stated in step 1) — not a mismatched or re-derived + topic, proving the carried-forward answer was used, not re-solicited and + potentially drifted. +- `[mechanical]` `.sage-progress.json`'s `active_track` matches the new + curriculum's `track` slug, and `python3 tests/check_progress_schema.py + ` exits 0. + +## Part B — Clear match (compress Round 0, don't skip it) + +### Script +1. Learner triggers Sage via natural language: "Teach me Python, I want to + build something with it." (Matches `python-basics`'s `teaches`/`title`.) +2. Profile Setup — answer each round: + - Identity: "Devon, QA engineer" + - Bridge languages: JavaScript + - Experience: "Junior (1-3yr)" + - Learning style: "Read first, then try" + - Tone: "Encouraging and patient" + Confirm the generated profile looks right. +3. Track Setup: per the new rule, Round 0 should still be asked (this is a + real choice — bundled structured track vs. one generated fresh) but + compressed into a direct confirm naming the match, e.g. "Sounds like + Python — want the bundled Python Foundations track, or build a custom + one instead?" — not the full undifferentiated list including Rust CLI + Tools. Learner picks **"build a custom one instead"** — explicitly + declining the matched bundled track, to prove the compression didn't + remove the choice. +4. Round 1 should be skipped (Python was already stated) — Sage proceeds + directly to Round 2 ("Is there a project this feeds into?"): No — + general skill building. +5. Round 3 ("How much do you know already?"): "Intermediate." +6. Round 4 ("What's the priority?"): "Get productive fast (Output)." +7. Sage generates the curriculum and presents it for confirmation. +8. Learner confirms. + +### Assertions +- `[behavioral]` Round 0's question referenced the matched track by name + (Python / "Python Foundations") rather than presenting the generic + "want to start one of these" list with every bundled curriculum — + quote it. +- `[behavioral]` The compressed question still offered the custom-track + alternative explicitly (didn't railroad toward the bundled match) — + quote the option. +- `[behavioral]` Picking custom despite the matched offer correctly + proceeded into the custom-track interview (not silently defaulted to + `active_track: python-basics`) — confirm `active_track` in the final + progress file is a new custom slug, not `python-basics`. +- `[behavioral]` Round 1's "What do you want to learn?" was never separately + asked — the transcript goes from Round 0's confirm straight to Round 2, + carrying "Python" forward. +- `[mechanical]` `.sage-progress.json`'s `active_track` matches the new + custom curriculum's `track` slug (not `python-basics`), and `python3 + tests/check_progress_schema.py ` exits 0.