Imported from alwayslistening86-pixel/claude_plugins (
generic-tutor-1.2.0/skills/journey-planner/SKILL.md). Install upstream withnpx skills add alwayslistening86-pixel/claude_plugins --skill journey-planner. Copyright stays with the author.
Journey Planner — slot-based allocation, no calendar
Invocation
/plan runs once automatically as the final step of first-ever onboarding (after every initial course has been added), and again any time the learner explicitly re-runs it — after adding or dropping a course, or after a change to availability.sessions_per_week. It does not run silently mid-session; a re-plan is always visible and explicit, the same way every other state change in this system is.
Why there are no dates anywhere in this file
Capacity is expressed purely as a rate — sessions_per_week — used only for rough arithmetic projection ("at this rate, roughly N weeks remaining"), never for placing anything on a calendar. Everything else this skill manages is a plain sequence of session slots: slot 1, slot 2, slot 3… A missed week doesn't make anything "late"; it just means the next slot happens whenever it happens. This keeps the framing honest about a fact that's true regardless of how carefully anyone plans: a learner could always move faster or slower than the plan assumes, and the system should never imply a false precision about when something happens — only how much is left and in what order.
What determines eligibility to draw a slot at all
A course only receives slots if its subjects/<course_id>.json.roster_state is active or test_pending_convergence and its bound course.json.grounding_status is not suspended_ungrounded. dormant (level-locked), dropped, suspended, and complete courses draw nothing. Completeness (every stage pass, plus exam_status: passed if course.json.exam.enabled) is derived by cohort_status.py/roster_check.py on every call rather than stored as a roster_state value, so a finished course automatically stops occupying a roster slot and stops holding its cohort's convergence gate — there is no transition to forget to write. This means eligibility is entirely downstream of course-compiler's level-lock and course-runner's convergence gate — this skill doesn't re-decide either; it only allocates among whatever cohort_status.py (below) already reports as eligible. Don't compute this list by hand — the suspended-course exclusion specifically is the kind of check that's drifted out of a hand-derivation before (DESIGN_NOTES.md v1.0.1), so let the script be the one place it's decided.
Allocation, per planning round
- List eligible courses and group them by cohort — run the script, don't re-derive it:
Its output is already grouped bypython3 /EDU/.tutor-scripts/cohort_status.py <the learner's profile subjects/ dir> <the /EDU/courses/ dir>cohort_id, already restricted to eligible members (active/test_pending_convergence, not suspended), and already names each cohort'sbottleneck(the eligible, not-yet-test_pending_convergencemember with the most stages remaining) and whether it'sconverged. A learner can have more than one cohort active at once (e.g. a freshly-added low-level course running alongside the current lock-floor level); the script already keeps bottleneck identification within each cohort separately — never pool cohorts together when reading its output. - Estimate remaining slots per course — for finer-grained weighting than the script's plain stage-count, sum, across stages not yet
passinsyllabus_status, each stage's rough slot cost (course.json's per-stage estimate, set at compile time from lesson/practice/test length against an assumed pace — an estimate, explicitly labeled as one, not a commitment). This is genuinely a soft, approximate number feeding a soft weighting heuristic (steps 6–7 say so plainly already), not a correctness-critical gate, so summing it by hand from the small set of eligible courses'course.jsonfiles is fine. - Within each cohort, use the script's
bottleneckto weight the majority of that cohort's share of this round'ssessions_per_weektoward it. - Everything else in
test_pending_convergence(in any cohort) gets only enough slots to keep itsreview-schedulercadence alive (seecourse-runner's phase-convergence section) — not a full share, since it has no new content to advance right now. - Split
sessions_per_weekacross cohorts in rough proportion to each cohort's remaining estimated slots (step 2) before applying steps 3–4 within each — a learner with one small low-level course and one large current-floor course shouldn't have the small one starved just because it happens to share the plan with a bigger one in a different cohort. - Within a session that must carry new material for more than one course, prefer pairing a lesson (heavier, novel) against a review block (lighter, retrieval-only) over two dense lesson blocks back to back, once
review-schedulerhas material to draw on. Where two lesson blocks must share a session regardless, a contrasting pair (e.g. quantitative + humanities) is a reasonable anti-fatigue default — worth being honest that this specific pairing heuristic is a sensible design choice, not a strongly evidenced one, unlike the spacing benefit itself. - Report the plan as a sequence, not a schedule: "next N sessions: mostly Chemistry (bottleneck), a Contract Law review block folded in every other session" — never "Tuesday: Chemistry."
Feasibility, stated plainly
If a course carries a genuine external deadline the learner has mentioned, this skill can only ever report approximate readiness ("at your current rate, roughly N weeks out") — it does not, and should not, promise a date. If the estimated remaining slots for the whole eligible set clearly can't be covered at the stated sessions_per_week in any reasonable timeframe the learner cares about, say so directly rather than quietly under-planning around it.
/drop <course_id>
Two distinct cases, and they must not be conflated:
- Ordinary drop (an active or dormant course, not suspended):
roster_statebecomesdropped. Every other field insubjects/<course_id>.jsonis preserved exactly as it stood. The dropped course stops occupying roster capacity (course-compiler's Step -1 no longer counts it) and stops receiving slots here. Re-adding the same course later via/add-courseresumes from wherever it was left, and re-enters the roster cap as a fresh occupant. - After every drop, wake what the drop unblocked. Dropping a course can remove the lowest unfinished level, and the only other dormant→active step is the ledger-clearing step below — so without this,
/drop Xwith X (level 2) active and Z (level 3) dormant would leave Z locked behind a course that no longer counts. Straight after writingdropped, runpython3 /EDU/.tutor-scripts/roster_check.py <the learner's profile dir> <the /EDU/courses/ dir>(no candidate) and set every course in itswake_nowlist toroster_state: active, then tell the learner which courses woke and why ("Dropping X unlocked Z").wake_nowis exactly the dormant, unfinished, non-suspended courses at or belowhighest_level_clearedor at the lowest unfinished level (unfinished_level_floor) — never anything higher, so dropping the floor course wakes one level, not every dormant level at once. This is safe to do eagerly: if X is later resumed,roster_check.py … --resumere-locks Z (seecourse-compilerStep -0.5), so waking on drop never becomes a way round the level-lock. - Suspended-course drop (a course at
grounding_status: suspended_ungrounded): this is not the same operation. Seecourse-auditor— the learner's alternative there is to erase the enrollment file entirely, with no trace and no history, since the course was never something the learner could evaluate or control in the first place. Route a drop request on a suspended course to that flow, not to the ordinary drop above.
Updating highest_level_cleared
After any stage-test pass or exam pass changes a course's status to complete, ask cohort_status.py — don't re-derive completeness in prose, and don't re-list the profile's subjects/ directory by hand:
python3 /EDU/.tutor-scripts/cohort_status.py <the learner's profile subjects/ dir> <the /EDU/courses/ dir>
Read the entry for the finished course's cohort_id. all_complete is the one place this is decided: it is true when at least one member is complete and nothing is blocking, where "complete" means every syllabus_status entry is pass and, if course.json.exam.enabled, exam_status is passed. blocking_members names anything still unfinished that counts (active, test_pending_convergence, dormant). excluded_members names unfinished courses that deliberately do not block a level from clearing: a dropped course (the learner set it down — /drop is a pause, and roster_check.py already ignores dropped courses when computing the lock floor, so counting them here would let an abandoned course hold every higher-level course dormant forever with no way out short of erasing the profile) and a grounding-suspended course (its source vanished under the learner; that is not their fault — see course-auditor). Exclusion is never silent: when a level clears with anything in excluded_members, tell the learner plainly which courses were left out and why ("Level 4 cleared. Contract Law was dropped unfinished, so it didn't count toward this — resuming it later via /add-course will reopen this level and lock the higher courses again until it is finished"), so the ledger stays honest about what was actually studied here.
Then walk the ledger up rather than setting it to this one cohort's level. The same output carries level_ledger — stored, suggested_highest_level_cleared, cleared_cohorts (the cohorts, in order, that were clear above the stored value) and stopped_at (the first cohort that isn't, with its blocking). The walk exists because the ledger can be deliberately lowered by a resume (resume_enrollment.py --reopen-*): if levels 1–3 were cleared, a level-2 course is resumed (ledger → 1) and later finishes, this course's own level is 2, but level 3 is still fully complete and must clear again in the same step — writing only max(current, 2) would leave it stranded until some other level-3 course happened to finish. It never lowers the ledger and stops at the first cohort that isn't all_complete; a level with no cohort at all, or whose only members are excluded_members (nothing complete, nothing blocking — for example a level whose sole course was dropped), is stepped over as vacuously clear rather than stopped at. Such levels are listed in skipped_cohorts; tell the learner about them the same way as any other excluded course ("Level 2 held only Contract Law, which was dropped, so the ledger passed over it"). Stopping there would strand every finished higher level behind a course the learner has set down.
If suggested_highest_level_cleared is higher than stored, write it as highest_level_cleared to the global profile (and tell the learner every level in cleared_cohorts that just cleared), then re-run roster_check.py (or re-check each dormant course's academic_level against the new value directly) to take its wake_now list — every dormant course whose level is now at or below the new highest_level_cleared, or that now sits at the lowest unfinished level — move each to active, tell the learner which woke, and let them re-enter allocation from the next planning round. If it is not higher, do nothing and say which blocking_members remain (stopped_at's blocking).
What this skill does not do
Does not store or reason about calendar dates, days of the week, or reminders. Does not decide level-lock or convergence state itself — it only allocates among what those two gates have already made eligible. Does not promise completion by a specific date, only a rate-based estimate stated as one.