From fc30c8a73f63084d572a763ffe2609eae141ee8a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 13:50:30 +0000 Subject: [PATCH 1/4] Add scenario 06: custom-track creation path (issue #2) Every prior scenario (01-05) and the 1.2.1 real-install smoke test picked the existing bundled python-basics track at Track Setup Round 0. This scenario instead picks "Build a custom track" and scripts the full Round 1-4 interview, targeting curriculum generation completeness, axis-level mapping from the interview answers, and confirm-before-save behavior -- the last major untested path per issue #2. Live validation run in progress; fixes to follow based on what it finds, same pattern as scenarios 01-05. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_019FNUmdkni7KChUL6s7hFvB --- tests/scenarios/06-custom-track-creation.md | 71 +++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 tests/scenarios/06-custom-track-creation.md diff --git a/tests/scenarios/06-custom-track-creation.md b/tests/scenarios/06-custom-track-creation.md new file mode 100644 index 0000000..67cf93c --- /dev/null +++ b/tests/scenarios/06-custom-track-creation.md @@ -0,0 +1,71 @@ +# Scenario 06 — Custom-track creation (Track Setup Rounds 1-4) + +## Regression target + +Every prior scenario (01-05) and the 1.2.1 real-install smoke test picked the +**existing** bundled track at Track Setup Round 0. The custom-track path — +Round 0's "Build a custom track" branch, the four-round interview, curriculum +generation from `TEMPLATE.md`, confirm-before-save, and the resulting +curriculum actually landing in `curricula/.md` — is fully specified in +`SKILL.md` (Track Setup, Rounds 1-4) but has never been exercised end-to-end. +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. + +## Script + +1. Learner triggers Sage via natural language (not a slash command): "teach + me Rust, let's build something." +2. Profile Setup — answer each round: + - Identity: "Marco, mobile developer" + - Bridge languages: Java, Kotlin + - Experience: "Junior (1-3yr)" + - 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." +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." +7. Round 4 ("What's the priority?"): "Build while learning (Balanced)." +8. Sage generates the curriculum and presents it for confirmation before + saving. +9. Learner confirms. +10. Learner runs `/sage-lesson`. + +## Assertions + +- `[mechanical]` A new file exists at `curricula/.md` (some + Rust-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 + 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 + ` exits 0. +- `[behavioral]` `mastery: low` (from Round 3's "From scratch") and + `intent: balanced` (from Round 4's "Build while learning (Balanced)") — + quote the generated YAML header and confirm both map correctly from the + interview answers, not defaulted or guessed independently of them. +- `[behavioral]` `destination` reflects Round 2's actual stated project (the + CLI grep tool), not `"general"` or a generic placeholder — Round 2 was + answered "Yes" with a specific project named. +- `[behavioral]` `bridge_from` reflects the learner's real profile languages + (Java and/or Kotlin from Profile Setup) — not invented languages the + learner never stated. +- `[behavioral]` Sage displayed the generated curriculum (at least its + 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). From ba5c1ae5bea4bdcce3d723434a957be07c9c1727 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 13:54:41 +0000 Subject: [PATCH 2/4] Release 1.3.0: scenario 06 passes clean, close out issue #2 Scenario 06 (custom-track creation, Track Setup Rounds 1-4) passed 8/8 on its first live run -- the first scenario in this harness's history not to need a fix first. The generated curriculum (curricula/rust-cli-grep.md) had a real, smoke-tested verify command, correct axis mapping from the interview answers, and confirm-before- save was verified by checking the file didn't exist until after the learner confirmed. The run's one soft finding: SKILL.md's Track Setup interview never explicitly directs Sage to derive the `consequence` axis field -- only mastery (Round 3) and intent (Round 4) have a stated mapping. Sage got it right by judgment this run, not by following an instruction. Added an explicit rule for deriving consequence from Round 2's stated project stakes. Pre-release checklist bumped from "all five" to "all six" scenarios. Version bumped to 1.3.0. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_019FNUmdkni7KChUL6s7hFvB --- .claude-plugin/plugin.json | 2 +- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 2 +- skills/sage-instructor/SKILL.md | 2 ++ tests/README.md | 2 +- 5 files changed, 37 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 5835592..b726907 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "sage-instructor", - "version": "1.2.1", + "version": "1.3.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 91fd348..2ddb1c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## [1.3.0] — 2026-07-02 + +### Added +- **`tests/scenarios/06-custom-track-creation.md`.** Every prior scenario + (01-05) and the 1.2.1 real-install smoke test picked the existing bundled + `python-basics` track at Track Setup Round 0. This scenario instead picks + "Build a custom track" and scripts the full Round 1-4 interview, closing + the last major untested path from a fresh install (see #2). + +### Fixed +- **Found by the scenario 06 live run:** the Track Setup interview (Rounds + 1-4) never explicitly directs Sage to derive the curriculum's + `consequence` axis field — only `mastery` (Round 3) and `intent` (Round 4) + have a stated mapping. The live run got `consequence` right anyway by + inferring it from the described project's stakes, but that was judgment, + not a followed instruction — a spec gap that happened not to bite this + time. Added an explicit rule: infer `consequence` from Round 2's answer + (no project/personal project → `low`; shared or stakes-bearing → `medium` + or `high`), rather than leaving it unspecified. + +### Verified +- **`06-custom-track-creation`**: 8/8 PASS on the first live run — the first + scenario in this harness's history to pass clean without needing a fix + first. Confirmed via a real generated curriculum + (`curricula/rust-cli-grep.md`) with a `verify` command + (`rustc {file} -o /tmp/sage_out && /tmp/sage_out`) that was smoke-tested + for real, not just read as plausible-looking text; confirm-before-save was + checked by verifying the file didn't exist on disk between the curriculum + being displayed and the learner confirming it. +- Pre-release checklist (`tests/README.md`/`CONTRIBUTING.md`) updated from + "all five" to "all six" scenarios. + ## [1.2.1] — 2026-07-02 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b90fbe2..ba5faa8 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 five Tier 2 scenarios) first. + checklist (all six 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 02c32ae..fe00149 100644 --- a/skills/sage-instructor/SKILL.md +++ b/skills/sage-instructor/SKILL.md @@ -94,6 +94,8 @@ If applicable, ask: "Want to start one of these, or build something custom?" Opt **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. + Generate curriculum from `TEMPLATE.md`, confirm with learner, save to `curricula/.md`. --- diff --git a/tests/README.md b/tests/README.md index 556ac72..a39b0d8 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 five** scenarios in `scenarios/` — not just the +`CHANGELOG.md`), run **all six** 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 From a92287c4eb6c9a1da43b1c4684d15e1e098ebe43 Mon Sep 17 00:00:00 2001 From: Lux Solari Date: Thu, 2 Jul 2026 11:35:04 -0300 Subject: [PATCH 3/4] Release 1.4.0: axis-recalibration accept path passes clean Scenario 04 only ever tested declining the recalibration offer; the accept branch (write axis_overrides, confirm the change) had never been live-tested. New scenario 07 triggers low_hint_streak >= 3, accepts the bump, then drives a second streak past threshold to confirm a later offer is measured against the newly-applied level. 9/9 assertions passed on the first live run. Two real spec gaps surfaced along the way and got fixed in SKILL.md: - The accept branch never said whether the triggering streak resets, unlike decline. Left alone, an unreset streak could immediately re-fire the same offer against the level Sage just applied. - The phase-transition question's 5th option (the recalibration offer) didn't say whether picking it also means "start Phase N+1" or is a standalone choice. Clarified: it means both. One soft finding (Rule 10 topic-key derivation ambiguity for coordinate-noun bullets) was left unfixed -- orthogonal to axis recalibration, recorded in CHANGELOG for a future pass instead of guessed at here. Pre-release checklist bumped from "all six" to "all seven" scenarios. Version bumped to 1.4.0. Co-Authored-By: Claude Sonnet 5 --- .claude-plugin/plugin.json | 2 +- CHANGELOG.md | 48 ++++++++++ CONTRIBUTING.md | 2 +- skills/sage-instructor/SKILL.md | 4 +- tests/README.md | 2 +- .../scenarios/07-axis-recalibration-accept.md | 95 +++++++++++++++++++ 6 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 tests/scenarios/07-axis-recalibration-accept.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index b726907..1271753 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "sage-instructor", - "version": "1.3.0", + "version": "1.4.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 2ddb1c3..d758498 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,53 @@ # Changelog +## [1.4.0] — 2026-07-02 + +### Added +- **`tests/scenarios/07-axis-recalibration-accept.md`.** Scenario 04 only + ever exercised declining the recalibration offer; the **accept** branch + (write to `axis_overrides`, confirm the change) had never been live-tested. + This scenario triggers `low_hint_streak >= 3` (the "Mastery is probably too + low" signal, complementing 04's `high_hint_streak` coverage), accepts the + bump, then drives a second streak past threshold to confirm a later offer + is measured against the newly-applied level — a follow-up named in #2's + "out of scope for this pass" list. + +### Fixed +- **Found while drafting scenario 07, before the live run:** the accept + branch of Axis Re-Calibration said to write `axis_overrides` and confirm + the change, but — unlike the decline branch — never said whether the + triggering streak resets. Left unresolved, an unreset streak could + immediately re-fire the same offer against the level Sage had just + applied. Added a rule: accepting resets the streak too, same as declining; + only a fresh streak against the new level earns a fresh offer. +- **Found by the scenario 07 live run:** the phase-transition question's 5th + option (the recalibration offer) didn't say what happens to the other 4 + options when it's picked — is it a standalone choice, or does it also mean + "start Phase N+1"? The live run inferred the latter (matching the + scenario's own script), but SKILL.md didn't say so. Added a rule: picking + the 5th option applies the recalibration and proceeds as if "Start Phase + N+1" had been picked; the standard menu isn't asked again afterward. + +### Verified +- **`07-axis-recalibration-accept`**: 9/9 PASS on the first live run. Real + Python exercise scripts written and executed for real via `python` + (not `python3` — the machine's `python3` is a Windows Store stub, exactly + the toolchain-vs-learner-bug gotcha scenario 03 guards against); progress + file diffed at every checkpoint; `curricula/python-basics.md` on disk + confirmed unchanged (`git status`/`git diff` empty) throughout, i.e. the + override never leaked into the curriculum source. + - Soft finding (not acted on this pass): Progress Rule 10's topic-key + derivation example ("Variables, dynamic typing, truthiness" → `variables`) + is a 3-term bullet reduced to its first noun; Phase 1's "Lists, dicts, + sets, tuples" bullet is a 4-term coordinate list where none of the + exercises isolate to just one of those structures, and it's unclear + whether Rule 10 wants `lists` (strict first-term) or a compound key. + Same category of ambiguity as 1.3.0's `consequence`-derivation finding, + but orthogonal to axis recalibration — left for a future scenario/issue + rather than guessed at here. +- Pre-release checklist (`tests/README.md`/`CONTRIBUTING.md`) updated from + "all six" to "all seven" scenarios. + ## [1.3.0] — 2026-07-02 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba5faa8..09d6f77 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 six Tier 2 scenarios) first. + checklist (all seven 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 fe00149..c2ea7e8 100644 --- a/skills/sage-instructor/SKILL.md +++ b/skills/sage-instructor/SKILL.md @@ -299,7 +299,7 @@ options: - "Take a challenge that combines this phase's concepts" - "Save checkpoint and stop for now" ``` -Before asking, check the recalibration signal below — if it fired, add a 5th option surfacing it. The streak is exercise-scoped, not phase-scoped, so don't frame it as "this phase" (it may span the phase boundary) — say "the last few exercises" instead: "The last few exercises went by without a hint — bump Mastery to medium?" or "The last few exercises were a grind — dial the pace back?" +Before asking, check the recalibration signal below — if it fired, add a 5th option surfacing it. The streak is exercise-scoped, not phase-scoped, so don't frame it as "this phase" (it may span the phase boundary) — say "the last few exercises" instead: "The last few exercises went by without a hint — bump Mastery to medium?" or "The last few exercises were a grind — dial the pace back?" Picking the 5th option means both things at once — apply the recalibration (see Axis Re-Calibration) and proceed as if "Start Phase N+1" had been picked. Don't ask the four standard options again afterward. ### Axis Re-Calibration @@ -307,7 +307,7 @@ Axis levels are declared once at track creation and go stale. `low_hint_streak` - **`low_hint_streak >= 3`** (three exercises in a row, zero hints) → the declared Mastery is probably too low. Offer a bump at the next phase transition. - **`high_hint_streak >= 2`** (two exercises in a row needing 3+ hints) → the declared Mastery or pace is probably too high. Offer to dial back — either Mastery down a level, or just slower pacing within the same level — at the next phase transition. -- If the learner accepts, write the new level to `axis_overrides` in the progress file (never overwrite the curriculum file itself — the override layers on top, see Progress Rules). Confirm what changed in one sentence. +- If the learner accepts, write the new level to `axis_overrides` in the progress file (never overwrite the curriculum file itself — the override layers on top, see Progress Rules). Confirm what changed in one sentence. Then reset the streak that triggered the offer to 0 — the same streak count shouldn't immediately re-trigger a second offer against the level Sage just applied; a fresh streak accumulating from here, measured against the new level, is a new signal and earns its own fresh offer. - If the learner declines, reset the streak that triggered the offer to 0. Otherwise the same offer resurfaces at every subsequent phase transition until it's acted on — a fresh streak accumulating from here is a new signal and earns a fresh offer, but a stale one shouldn't nag. - This only ever surfaces as an offer, never a silent change. The learner decides. diff --git a/tests/README.md b/tests/README.md index a39b0d8..a0fc58c 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 six** scenarios in `scenarios/` — not just the +`CHANGELOG.md`), run **all seven** 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/07-axis-recalibration-accept.md b/tests/scenarios/07-axis-recalibration-accept.md new file mode 100644 index 0000000..7c98c8a --- /dev/null +++ b/tests/scenarios/07-axis-recalibration-accept.md @@ -0,0 +1,95 @@ +# Scenario 07 — Axis re-calibration accept path + +## Regression target +- The recalibration offer's **accept** branch has never been live-tested. + Scenario 04 only exercises decline. This scenario triggers `low_hint_streak + >= 3` (the "Mastery is probably too low" signal) and has the learner + **accept** the bump, checking that: `axis_overrides` gets the new level + (never the curriculum file itself), a confirmation is given, and — per the + fix in `SKILL.md` Axis Re-Calibration (the "if the learner accepts" bullet) + — the triggering streak resets to 0 so it doesn't immediately re-fire + against the level Sage just applied. +- It also checks that a second, later streak crossing the same threshold is + measured against the *new* `axis_overrides` level (medium, not low) — a + genuinely fresh signal earning a genuinely fresh offer, worded for the new + baseline. + +## Setup +Scratch directory with `.sage-progress.json`: +```json +{ + "active_track": "python-basics", + "tracks": { + "python-basics": { + "phase": 0, + "completed_exercises": ["P0-fizzbuzz", "P0-temp-converter"], + "current_topic": "Control flow", + "current_exercise": "P0-word-counter", + "next_up": "P1-list-ops", + "observations": "", + "topic_confidence": {}, + "review_due": [], + "axis_overrides": {}, + "low_hint_streak": 2, + "high_hint_streak": 0, + "last_session": "", + "hint_count": 0 + } + } +} +``` +(`low_hint_streak: 2` simulates `P0-fizzbuzz` and `P0-temp-converter` both +already completed with zero hints. Curriculum declares `mastery: low`.) +Valid `.sage-profile.md`. + +## Script +1. Learner works `P0-word-counter` (Phase 0's last exercise) needing **zero** + hints, submits a correct solution. Verify passes. `low_hint_streak` should + become 3 — this IS the end of Phase 0, so the phase-transition + AskUserQuestion fires. +2. At the phase-transition question, the recalibration option should be + present (per the streak state) — check its exact wording ("the last few + exercises... bump Mastery to medium?" or clearly equivalent). +3. Learner picks the **recalibration option**, i.e. accepts the bump. +4. Learner starts Phase 1, completes `P1-list-ops` with zero hints. +5. Learner completes `P1-comprehension-refactor` (Phase 1's second of three + exercises) with zero hints. `low_hint_streak` reaches 2 here, but this is + **not** a phase-transition point. +6. Learner completes `P1-json-roundtrip` (Phase 1's actual last exercise) + with zero hints again, so the streak isn't broken by a hint-needed pass. + This IS the end of Phase 1 → the phase-transition AskUserQuestion fires. +7. Learner accepts this second recalibration offer too. + +## Assertions + +- `[behavioral]` The recalibration option's wording at step 2 says "the last + few exercises" (or clearly equivalent phrasing that doesn't scope to a + single phase) and names bumping Mastery upward — quote it directly. +- `[mechanical]` After step 3 (accept), `axis_overrides.mastery == "medium"` + in the progress file — diff before/after. `curricula/python-basics.md` on + disk is unchanged (`mastery: low` still declared there; the override layers + on top, it never overwrites the source file). +- `[behavioral]` At step 3, Sage confirms what changed in one sentence (per + SKILL.md: "Confirm what changed in one sentence") — quote it. +- `[mechanical]` After step 3, `low_hint_streak` is reset to `0` in the + progress file — the accept branch resets the triggering streak same as + decline does, so it doesn't immediately resurface against the level just + applied. +- `[behavioral]` After step 4 (first Phase 1 zero-hint exercise post-accept), + no recalibration offer fires yet — the streak was reset in step 3, so this + is only the first exercise of a new streak (`low_hint_streak == 1`), below + threshold. +- `[behavioral]` After step 5, `low_hint_streak == 2` (still below the `>= 3` + threshold) and, independent of that, `P1-comprehension-refactor` is not + Phase 1's last exercise, so no phase-transition question fires at all here. + Sage should show only the standard 4-option "Exercise complete. What + next?" menu, with no 5th option. +- `[behavioral]` After step 6 (Phase 1's actual last exercise, phase + transition fires), `low_hint_streak == 3` and a **fresh** recalibration + offer is presented — confirm it's worded against the *new* baseline (e.g. + "bump Mastery to high?", not a repeat of "bump Mastery to medium?" since + `axis_overrides.mastery` is already `medium` at this point). +- `[mechanical]` After step 7 (second accept), `axis_overrides.mastery == + "high"` and `low_hint_streak` is reset to `0` again. +- `[mechanical]` `python3 tests/check_progress_schema.py ` exits + 0 at every checkpoint. From 5cfc37960d032ae80f65fcfd6538f29635734e50 Mon Sep 17 00:00:00 2001 From: Lux Solari Date: Thu, 2 Jul 2026 12:03:45 -0300 Subject: [PATCH 4/4] Release 1.5.0: second bundled curriculum, /sage-switch passes clean Adds curricula/rust-cli.md -- a Rust CLI track building toward Ferrogrep, deliberately calibrated opposite to python-basics (mastery: medium, consequence: high, intent: output vs. low, low, growth) so switching between them exercises an actual posture change, not just a second copy of the same shape. New scenario 08 checks what /sage-tracks and /sage-switch have never had a chance to get wrong: progress isolation, hint-streak isolation, and axis-posture isolation between two concurrently-tracked courses, plus /tracks status accuracy across both. 8/8 PASS on the first live run (see #2's "out of scope for this pass" list). One real spec gap surfaced while drafting the scenario and got fixed in SKILL.md: Track Management gave /tracks and /switch one line each, thin enough that status vocabulary and switch semantics (resume vs. initialize, never re-onboard) were only inferable by chaining together Progress Rule 9 and Track Setup's gating language. Added an explicit paragraph. Toolchain limitation disclosed in CHANGELOG rather than hidden: the live-run machine had no Rust toolchain, so the one rust-cli exercise was verified by manual trace instead of a real rustc run. Pre-release checklist bumped from "all seven" to "all eight" scenarios. Version bumped to 1.5.0. Co-Authored-By: Claude Sonnet 5 --- .claude-plugin/plugin.json | 2 +- CHANGELOG.md | 47 ++++++++ CONTRIBUTING.md | 2 +- skills/sage-instructor/SKILL.md | 2 + skills/sage-instructor/curricula/rust-cli.md | 106 +++++++++++++++++++ tests/README.md | 2 +- tests/scenarios/08-multi-track-switching.md | 102 ++++++++++++++++++ 7 files changed, 260 insertions(+), 3 deletions(-) create mode 100644 skills/sage-instructor/curricula/rust-cli.md create mode 100644 tests/scenarios/08-multi-track-switching.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 1271753..2f7846a 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "sage-instructor", - "version": "1.4.0", + "version": "1.5.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 d758498..268984e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,52 @@ # Changelog +## [1.5.0] — 2026-07-02 + +### Added +- **`skills/sage-instructor/curricula/rust-cli.md`.** Second bundled + curriculum — Rust CLI fundamentals building toward Ferrogrep, a + production search tool. Deliberately declares axes sharply different + from `python-basics` (`mastery: medium, consequence: high, intent: + output` vs. `low, low, growth`) so `/sage-switch` has an actual + posture change to get right or wrong, not just a second copy of the + same shape. +- **`tests/scenarios/08-multi-track-switching.md`.** `/sage-tracks` and + `/sage-switch` had only ever existed alongside a single real curriculum — + never exercised with a second one actually present (see #2's "out of + scope for this pass" list). Checks progress isolation, hint-streak + isolation, and axis-posture isolation between two concurrently-tracked + courses, plus `/tracks` status accuracy across both. + +### Fixed +- **Found while drafting scenario 08, before the live run:** `SKILL.md`'s + Track Management section gave `/tracks` and `/switch` one line each — + thin enough that status vocabulary (`active` vs. `started` vs. `not + started`) and switch semantics (resume an existing entry vs. initialize a + fresh one; never re-run Track Setup onboarding on switch) were only + inferable by chaining together Progress Rule 9 and Track Setup's Round-0 + gating language, not stated directly. Added an explicit paragraph + spelling out both. + +### Verified +- **`08-multi-track-switching`**: 8/8 PASS on the first live run. Progress, + hint-streak, and axis-override isolation between `python-basics` and + `rust-cli` all held under a live switch-and-back; teaching posture during + the rust-cli exercise was confirmed to match its own declared axes + (compressed concept+bridge, explicit no-black-boxes framing on the borrow + checker, efficient Steps 1-4), not leaked from python-basics. + - Toolchain limitation, disclosed not hidden: the run's machine had no + Rust toolchain installed, so the one rust-cli exercise was verified by + manual trace instead of a real `rustc` run — same class of gap Step + 6b.3 already carves out as not counting against the exercise, but + graders of that specific transcript should know it's compiler-unverified. + - Soft finding, not acted on: rust-cli's `verify` command's `/tmp/` + temp-file path assumes a Unix-like shell. Left as-is — it matches the + existing convention in `TEMPLATE.md`'s own C++ example and the curriculum + scenario 06 generated live, and Sage's actual execution environment + (Git Bash) already provides a working `/tmp`. +- Pre-release checklist (`tests/README.md`/`CONTRIBUTING.md`) updated from + "all seven" to "all eight" scenarios. + ## [1.4.0] — 2026-07-02 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 09d6f77..e419654 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 seven Tier 2 scenarios) first. + checklist (all eight 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 c2ea7e8..d2c2535 100644 --- a/skills/sage-instructor/SKILL.md +++ b/skills/sage-instructor/SKILL.md @@ -140,6 +140,8 @@ Generate curriculum from `TEMPLATE.md`, confirm with learner, save to `curricula | `/switch TRACK` | Save current, load new. | | `/new-track` | Run Track Setup interview via AskUserQuestion. | +`/tracks` status is one of three: **Active** (this track's `active_track`), **Started** (has a `tracks.` entry but isn't active), **Not started** (no entry yet). `/switch TRACK` sets `active_track` to `TRACK`; if `tracks.` already exists, resume it exactly as-is (current phase, exercise, streaks, `axis_overrides` — untouched by whatever happened on the track just switched away from); if it doesn't exist yet, initialize a fresh entry at Phase 0 (same as a never-started track). Either way, switching is not onboarding — never re-run Track Setup just because `/switch` was called, even to a track with no entry yet. + ### Meta | Command | Behavior | |---|---| diff --git a/skills/sage-instructor/curricula/rust-cli.md b/skills/sage-instructor/curricula/rust-cli.md new file mode 100644 index 0000000..644f7c8 --- /dev/null +++ b/skills/sage-instructor/curricula/rust-cli.md @@ -0,0 +1,106 @@ +--- +track: rust-cli +title: "Rust CLI Tools" +destination: "Ferrogrep — a production Rust CLI search tool the team relies on" +description: > + Rust fluency from ownership through idiomatic error handling and + iterator-based collection processing, building toward Ferrogrep, a + real grep-style command-line search tool that ships to the rest of + the team. + +# Three Axes calibration (from DEV_PHILOSOPHY) +mastery: medium # low | medium | high — learner's current level in this domain +consequence: high # low | medium | high — what breaks if something goes wrong +intent: output # growth | balanced | output — what's the priority + +# Bridge languages — Sage will connect new concepts to these +bridge_from: + - Python + - Java + - JavaScript/TypeScript + +# Prerequisites — what should the learner know before starting +prerequisites: + - "Programming fundamentals in any language (variables, control flow, functions)" + - "Command-line comfort — running a compiled binary, reading a stack trace/panic" + - "Rust toolchain installed (rustc on PATH)" + +# Language/tools this track teaches +teaches: + - Rust + +# Every exercise is a standalone .rs file compiled and run directly — +# a clean exit (0) means the asserts passed. No cargo project scaffolding +# needed until Phase 2. +verify: "rustc --edition 2021 {file} -o /tmp/sage_out && /tmp/sage_out" +--- + +## Phase 0: Ownership & Core Syntax +**Goal:** Read and write basic Rust confidently — variables, control flow, and +enough ownership/borrowing to stop fighting the compiler on straightforward code. +**Pace:** Medium — control flow and functions transfer almost directly from +Python/Java; ownership and `Result`/`Option` instead of exceptions are the +genuinely new mental models, so don't compress those two. + +### Topics +- Variables, mutability (`let` vs `let mut`), no implicit nulls +- Ownership and borrowing — move semantics, `&`/`&mut` references, why the + borrow checker rejects code that looks fine in a GC language +- `Result` / `Option` and the `?` operator — no exceptions +- Pattern matching: `match`, `if let` +- `&str` vs `String` — borrowed vs owned text, the trap that bites first + +### Exercises +- [ ] `P0-word-count` — Given a string, return a `HashMap` of word→count, printed in sorted order. Asserts against a known sentence. +- [ ] `P0-file-reader` — Read a file's contents via `std::fs::read_to_string`, propagating errors with `?` instead of `.unwrap()`. Asserts cover both an existing file and a missing one (the missing case must return `Err`, not panic). +- [ ] `P0-arg-parser` — Parse `std::env::args()` by hand (no crate) into a struct with a pattern and optional flags. Asserts against several fixed argument-vector inputs, including a malformed one that must produce a clear error. + +### Ferrogrep Connection +This phase is Ferrogrep's skeleton: reading input safely (no panics on bad input) and parsing the flags the real tool will accept. + +--- + +## Phase 1: Collections, Iterators & Errors +**Goal:** Use Rust's iterator combinators and a real custom error type +idiomatically, instead of manual loops and `.unwrap()`. +**Pace:** Medium — `Vec`/`HashMap` map from Python's list/dict, but chained +iterator adapters (`map`/`filter`/`collect`) and structured errors are new +idiom, not new concepts; compress the "why collections exist" part. + +### Topics +- `Vec`, `HashMap`, `HashSet` — when to use which +- Iterator adapters: `map`, `filter`, `collect`, `enumerate` +- Custom error types: an `enum` implementing `std::error::Error`, `From` for `?`-based conversion +- Why "no black boxes in critical paths" means no swallowed errors here — every fallible path returns `Result`, never a silent default + +### Exercises +- [ ] `P1-line-filter` — Filter a file's lines by substring using iterator combinators (no manual `for` loop), returning `Result, Error>`. +- [ ] `P1-custom-error` — Define an `Error` enum (`Io`, `Pattern`, ...) and wire `?` to convert from `std::io::Error` via `impl From for Error`. Asserts confirm both variants are reachable and the conversion compiles without `.unwrap()`. +- [ ] `P1-pattern-match` — Implement a small literal-plus-`*`-wildcard matcher as a pure function (`fn matches(pattern: &str, line: &str) -> bool`), with asserts covering exact match, wildcard match, and non-match. This is Ferrogrep's matching engine in miniature. + +### Ferrogrep Connection +`P1-pattern-match` and `P1-custom-error` become the tool's actual matching logic and error backbone — not a simplified stand-in, the real thing. + +--- + +## Phase 2: Assembling the CLI +**Goal:** Wire Phase 0/1 into a real installable binary with correct exit +codes and no undefined failure states — the bar `consequence: high` demands +for something the team runs. +**Pace:** Fast — this phase is mostly composition of already-understood +pieces, so lean toward output mode: less new-concept teaching, more review +of the wiring and the exit-code contract. + +### Topics +- `fn main() -> Result<(), Error>` and the process exit-code contract: grep semantics are `0` = match found, `1` = no match, `2` = error — three distinct outcomes, never conflated +- Reading from a file path vs. stdin +- Splitting a single-file prototype into modules (`mod args; mod search; mod error;`) +- Why "working" at this consequence level means correctly exiting on every path, not just the happy path — an exercise here isn't complete on a green compile alone + +### Exercises +- [ ] `P2-search-core` — Wire `P0-arg-parser`, `P1-pattern-match`, and `P1-custom-error` into a `search(pattern: &str, path: &str) -> Result, Error>` core function doing real file I/O. Asserts against a fixture file with known matching and non-matching lines. +- [ ] `P2-cli-wiring` — Build `main()` that parses real CLI args, calls `search`, prints matches, and returns the correct exit code for each of the three outcomes (match / no-match / error) — verified by actually running the compiled binary with different inputs and checking `$?`/exit status, not just reading the code. +- [ ] `P2-module-split` — Split the single-file prototype into `args.rs`, `search.rs`, `error.rs` modules compiled together. Asserts confirm the split binary behaves identically to the single-file version on the same fixture inputs. + +### Ferrogrep Connection +After this phase, Ferrogrep is a real CLI the team can install and run — not a toy that only works when called exactly as demonstrated. diff --git a/tests/README.md b/tests/README.md index a0fc58c..2349dbb 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 seven** scenarios in `scenarios/` — not just the +`CHANGELOG.md`), run **all eight** 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/08-multi-track-switching.md b/tests/scenarios/08-multi-track-switching.md new file mode 100644 index 0000000..872b3ec --- /dev/null +++ b/tests/scenarios/08-multi-track-switching.md @@ -0,0 +1,102 @@ +# Scenario 08 — /sage-tracks and /sage-switch across two real tracks + +## Regression target +- `/sage-tracks` and `/sage-switch` have only ever existed alongside a single + real bundled curriculum (`python-basics`) — never exercised with a second + one actually present, per issue #2's "out of scope for this pass" list. + `SKILL.md`'s spec for `/switch` is a single line ("Save current, load + new.") — thin enough that per-track isolation is only implicit in the + progress file's `tracks: {}` shape (see Progress File Format), not spelled + out as behavior. +- This scenario adds `rust-cli` (declared `mastery: medium, consequence: + high, intent: output` — sharply different from `python-basics`'s `low, + low, growth`) and checks three things `/switch` could plausibly get wrong: + progress isolation (one track's state corrupting or leaking into the + other's), axis calibration isolation (teaching posture bleeding from one + track's declared axes into the other), and `/tracks` status accuracy + across multiple real entries. + +## Setup +Requires both `curricula/python-basics.md` and `curricula/rust-cli.md` to +exist. Scratch directory with `.sage-progress.json`: +```json +{ + "active_track": "python-basics", + "tracks": { + "python-basics": { + "phase": 0, + "completed_exercises": ["P0-fizzbuzz"], + "current_topic": "Control flow", + "current_exercise": "P0-temp-converter", + "next_up": "P0-word-counter", + "observations": "", + "topic_confidence": {"variables-dynamic-typing-truthiness": "solid"}, + "review_due": [], + "axis_overrides": {}, + "low_hint_streak": 1, + "high_hint_streak": 0, + "last_session": "", + "hint_count": 0 + } + } +} +``` +(No `rust-cli` entry yet — that track has never been started.) Valid +`.sage-profile.md`. + +## Script +1. Learner runs `/sage-tracks`. Check the listing before touching anything. +2. Learner works `P0-temp-converter` (python-basics) needing zero hints, + submits a correct solution. Verify passes. `low_hint_streak` becomes 2, + `current_exercise` advances to `P0-word-counter`. This is deliberately + *not* a phase transition (one exercise remains in Phase 0) — just + building distinguishable state to check for corruption later. +3. Learner runs `/sage-switch rust-cli`. +4. Learner works `P0-word-count` (rust-cli's first exercise — note the + different destination, `Ferrogrep`, and topics from Phase 0 of + `curricula/rust-cli.md`) needing 3+ hints, submits a correct solution. + Verify passes. +5. Learner runs `/sage-switch python-basics`. +6. Learner runs `/sage-tracks` again. + +## Assertions + +- `[behavioral]` At step 1, `/sage-tracks` lists both `python-basics` + (status: started/active — it's `active_track` and has completed + exercises) and `rust-cli` (status: not started — no entry in `tracks` + yet), each showing the title and destination from its own YAML header + (`Python Foundations`/Taskwright vs. `Rust CLI Tools`/Ferrogrep). +- `[mechanical]` After step 3 (switch), `.sage-progress.json` has + `active_track == "rust-cli"`, a new `tracks.rust-cli` entry initialized at + phase 0 with empty `completed_exercises`/`axis_overrides` and zeroed + streaks, AND `tracks.python-basics` unchanged from its step-2 state + (`current_exercise: "P0-word-counter"`, `low_hint_streak: 2`, + `completed_exercises: ["P0-fizzbuzz", "P0-temp-converter"]`) — diff + before/after the switch to confirm nothing in the other track's entry was + touched. +- `[behavioral]` During step 4's lesson delivery, Sage's teaching posture + reflects `rust-cli`'s own declared axes (`mastery: medium` → compressed + concept+bridge per `references/philosophy.md`; `consequence: high` → + explicit verification framing, no black-box treatment of the borrow + checker's rejection; `intent: output` → efficient, compressed Steps 1-4) — + not `python-basics`'s `low`/`low`/`growth` posture. Quote the specific + transcript moment and cite the philosophy.md row it matches. +- `[mechanical]` After step 4, `tracks.rust-cli.high_hint_streak == 1` and, + critically, `tracks.python-basics.high_hint_streak` is still `0` (or + whatever it was before step 3) — a hint-heavy exercise on one track must + not touch the other track's streaks. +- `[mechanical]` After step 5 (switch back), `active_track == + "python-basics"` and `tracks.python-basics` resumes exactly where step 2 + left it (`current_exercise: "P0-word-counter"`, `low_hint_streak: 2`, + `completed_exercises` unchanged) — not reset, not re-onboarded from Phase + 0. +- `[behavioral]` At step 6, `/sage-tracks` now shows both tracks as started, + with `python-basics` correctly marked active again (not `rust-cli`, which + was only switched away from, not abandoned). +- `[mechanical]` `axis_overrides` is `{}` for both tracks at every + checkpoint — this scenario never triggers a recalibration offer, so a + non-empty override anywhere would mean something wrote to the wrong place. +- `[mechanical]` `python3 tests/check_progress_schema.py ` + exits 0 at every checkpoint, confirming the schema checker validates a + multi-track file correctly (it loops over every key under `tracks`, not + just `active_track`).