recess-cli 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
1
  interface:
2
2
  display_name: "Recess CLI"
3
- short_description: "Safely operate Recess admin workflows"
4
- default_prompt: "Use $recess-cli to find a family and safely perform the requested Recess admin action."
3
+ short_description: "Route safe Recess CLI work by audience"
4
+ default_prompt: "Use $recess-cli to authenticate, select the correct guardian or admin skill catalog, and safely complete the requested Recess task."
@@ -0,0 +1,4 @@
1
+ {
2
+ "version": "2.0.0",
3
+ "minCliVersion": "1.6.0"
4
+ }
@@ -1,146 +0,0 @@
1
- # Billing, subscriptions, refunds, and cohort registration links
2
-
3
- Read [`../SKILL.md`](../SKILL.md) first — the safety model, escalation discipline, and input conventions there govern everything below.
4
-
5
- ## Resolve, then read
6
-
7
- Search returns the matching people, their family, every family member, and each member's enrollments and cohort registrations:
8
-
9
- ```bash
10
- recess --json users search "parent or kid name"
11
- recess --json enrollments list --user <kid-id>
12
- ```
13
-
14
- Read the exact billing state before proposing a write:
15
-
16
- ```bash
17
- recess --json subscriptions list --family <family-id> --kid <kid-id>
18
- recess --json invoices list --subscription <subscription-id>
19
- recess --json enrollments get-for-subscription --subscription <subscription-id>
20
- recess --json cohorts search "cohort name"
21
- ```
22
-
23
- ## Pausing and resuming collection
24
-
25
- ```bash
26
- recess --json billing pause --subscription sub_123 --until 2026-09-01 --confirm
27
- recess --json billing pause --subscription sub_123 --confirm # indefinite
28
- recess --json billing resume --subscription sub_123 --confirm
29
- ```
30
-
31
- - Without `--until`, the pause is **indefinite** — say so in the approval request; someone must remember to resume it.
32
- - `--until` must be a future ISO date; the CLI converts it to the Stripe resume timestamp.
33
-
34
- ### Pause timing semantics (read before choosing `--until`)
35
-
36
- Verified in source 2026-07-17: `apps/web-server/src/routes/admin/stripe/post.pause-collection.ts` (the endpoint both pause forms hit) and `apps/web-server/src/libs/enrollment/dry-run-stripe-dates.ts` (how subscriptions get their billing anchor).
37
-
38
- - **Paused invoices are voided.** Both pause forms send Stripe `pause_collection` with `behavior: "void"`: every subscription invoice generated while the pause is active is voided outright — the family is never charged for those cycles, and nothing accrues for later collection. Access is unaffected (the subscription keeps cycling; only collection stops).
39
- - **Billing ticks are Sunday-anchored.** Weekly course subscriptions are created with `billing_cycle_anchor` = Sunday 00:00 server time (UTC in prod) via `nextSundayEpochTimeS`; monthly products anchor to the 1st of the month. A weekly invoice issued on Sunday S covers the week starting S — so to skip a given session, you void the invoice of the **Sunday that starts that session's week**.
40
- - **`--until` is the Stripe `resumes_at` timestamp.** A bare ISO date parses as midnight UTC of that date. Every billing tick strictly before it is voided; the first tick at or after it charges normally.
41
- - **Choosing the date:** identify the Sunday starting the last week to skip (`S_last`) and the following Sunday (`S_next`). Pick an `--until` strictly between them, away from both edges — mid-week is safest. Example: to skip sessions on Thu 7/23 and Thu 7/30, void the Sunday 7/19 and Sunday 7/26 invoices and resume before Sunday 8/2 → `--until 2026-07-29` (anything 7/27 through 8/1 works).
42
- - **The cohort-wide sibling encodes this SOP server-side:** `cohorts pause-billing --weeks N` computes `resumes_at` = upcoming Sunday + N weeks − 2 hours (`post.cohort-pause-billing.ts`) — skip exactly N Sunday invoices, then resume with a 2-hour safety margin before the next tick so the resume never collides with the billing moment. Mirror that margin thinking when hand-picking `--until`.
43
- - **DB caveat:** the per-subscription pause writes only to Stripe — it never stamps `Enrollment.pausedAt`/`pauseResumesAt` (the cohort route stamps both). Verify a per-subscription pause via Stripe state (`subscriptions list`), never via the enrollment row.
44
- - **Known false error on success:** the confirmed write can come back as `api_error` with `details.status: 200` because the route omitted its success body (patched 2026-07-17, false error persists until deployed — see the gotcha in SKILL.md). Treat it as "probably landed" and verify via `subscriptions list` before doing anything else.
45
-
46
- ## Trials and cancellation
47
-
48
- ```bash
49
- recess --json billing extend-trial --subscription sub_123 --trial-end 2026-09-01 --confirm
50
- recess --json billing cancel-subscription --subscription sub_123 --reason "Family moved off platform" --confirm
51
- recess --json billing cancel-subscription --subscription sub_123 --restore --confirm
52
- ```
53
-
54
- - Default cancellation is **at period end** (the family keeps access until then). `--immediate` removes that grace — treat it as exceptional and call it out explicitly in the approval request, like `who-pays=recess`.
55
- - `--restore` un-cancels a previously scheduled cancellation. Read the subscription first and confirm it actually has a pending cancellation.
56
- - Always pass `--reason` on cancellations; it lands in the admin audit trail.
57
-
58
- ## Refunds and credits
59
-
60
- ```bash
61
- recess --json invoices refund --invoice in_123 --line-item il_123 \
62
- --method refund --full --who-pays guide --reason "Canceled session" --confirm
63
- ```
64
-
65
- Three methods, different rules:
66
-
67
- | `--method` | What happens | Amount rules |
68
- |---|---|---|
69
- | `refund` | money back to the payment method (Stripe credit note) | `--full` or `--amount-cents N` |
70
- | `credit` | credit against the customer's balance (Stripe credit note) | `--full` or `--amount-cents N` |
71
- | `tokens` | Recess token grant instead of money | requires explicit `--amount-cents`; `--full` is rejected |
72
-
73
- - `--who-pays` defaults to `guide`. `--who-pays recess` means Recess absorbs the cost — exceptional, name it explicitly in the approval request.
74
- - Amounts are integer cents. Cross-check any dollar figure the human states before previewing.
75
- - Target the **line item** (`--line-item`), not the whole invoice; run `invoices list` first to identify it.
76
- - **Check how the invoice was paid before choosing the method.** `invoices list` exposes the payment composition per invoice — verify it and state it in the approval request:
77
- - `token_deduction_cents` > 0 → that portion was paid with Recess tokens; give it back as `--method tokens` against `token_refundable_remaining_cents`, never as cash/credit.
78
- - `applied_balance` < 0 → that portion was covered by existing customer credit; `amount` (actually collected) is less than `subtotal`, so a "full" cash refund would over-refund.
79
- - Only when `amount` = `subtotal`, `token_deduction_cents` = 0, `applied_balance` = 0, and `paymentIntent.status` = `succeeded` was the invoice fully cash-paid and a `--full` refund/credit clean.
80
- - Also read `credited_amount`/`post_payment_credited_amount` — a line already credited (memo shows why) must not be credited twice.
81
-
82
- ## Signing a kid up for a paid class (`enrollments create`)
83
-
84
- The staff equivalent of a guardian buying a class. **This replaces impersonating the parent and walking their checkout** — same backend path (`initiateStripePayment`), so course discounts, Recess credit deduction, per-kid payment methods, and one-time-vs-recurring bundles all behave exactly as they do for the family.
85
-
86
- ```bash
87
- # Always run unconfirmed first — this performs the backend's own dry run.
88
- recess --json enrollments create --user <kid-id> --cohort <cohort-id>
89
-
90
- # Then, after explicit human approval of the quoted amount:
91
- recess --json enrollments create --user <kid-id> --cohort <cohort-id> --confirm
92
- ```
93
-
94
- **It spends real money.** The unconfirmed run is not an offline guess — it calls the backend with `dryRun:true` (writes nothing) and returns the *resolved* price, so the approval request must quote `details.billing` verbatim: `effectivePriceCents` (what Stripe bills after any course discount — **not** `listPriceCents`), `interval`, `firstChargeAt`, `chargesImmediately`, and `creditBalanceCents` when non-zero. Never quote a price from the course catalog or from memory; discounts and credits change it.
95
-
96
- Read the preview before escalating:
97
-
98
- - **`details.reusedEnrollmentId` is set** → the kid already holds an active enrollment with a free slot for this course. Nothing is charged and no subscription is created; the command just links the registration. Say that plainly — an approver told "we're charging $X" when the answer is $0 has been misled.
99
- - **`details.warnings` non-empty** → the cohort is at capacity and/or the kid's school-program class slots are exhausted. The write is **rejected** unless you also pass `--force`. Name the specific warning in the escalation; do not reach for `--force` as a reflex.
100
- - **`chargesImmediately: true`** → the family's card is charged during the confirmed call.
101
-
102
- ### Deferring the first charge (`--first-charge-at`)
103
-
104
- ```bash
105
- recess --json enrollments create --user <kid> --cohort <coh> \
106
- --first-charge-at 2026-08-02T00:00:00Z --confirm
107
- ```
108
-
109
- Recurring bundles only (a one-time bundle always invoices now — the route 400s `FIRST_CHARGE_UNSUPPORTED`). The subscription is created `trialing` until that instant, so **nothing is charged now**, and the recurring cycle re-anchors to it. Weekly Recess subscriptions all bill Sunday 00:00 UTC, so that is almost always the date you want — an arbitrary weekday anchor puts this family permanently out of step with every other weekly sub.
110
-
111
- Use it for goodwill and remediation: a kid joining mid-week who shouldn't pay for the remainder, or restoring access after a Recess-caused cancellation. Because no invoice is paid, the `invoice.paid` webhook that normally provisions everything never fires — so the route provisions the enrollment and registration **inline**, and the kid has access immediately. `result.mode` is `subscription_deferred` and `provisionedInline` is `true`.
112
-
113
- With no `--first-charge-at`, the invoice is paid during the call and the webhook does the provisioning; `enrollmentId`/`registrationId` come back `null` and `provisionedInline` is `false`. That is expected, not a failure — verify with `enrollments list --user` a few seconds later rather than treating the nulls as an error.
114
-
115
- **If it fails with `ENROLLMENT_PROVISION_FAILED`, do not retry.** The Stripe subscription was created; a retry sells a second one. Escalate with the subscription ID from the error message.
116
-
117
- ## Cohort registration against an enrollment
118
-
119
- ```bash
120
- recess --json enrollments register-cohort --enrollment <enrollment-id> --user <kid-id> --cohort <cohort-id> --confirm
121
- recess --json enrollments unregister-cohort --user <kid-id> --cohort <cohort-id> --confirm
122
- ```
123
-
124
- - `register-cohort` links the registration to the named enrollment and sends **no** email (`sendEmail:false`) — it is the quiet administrative link, unlike `registrations approve` which welcomes the family.
125
- - `unregister-cohort` removes the cohort registration but **leaves the enrollment active** (`cancelEnrollment:false`). Enrollment cancellation is a different workflow: the only CLI path that cancels an enrollment is `registrations deny` (see [`class-ops.md`](class-ops.md)). Never substitute one for the other.
126
-
127
- ### Switching a kid's cohort (the move play)
128
-
129
- Composing the two commands moves a kid (or a whole roster) to another cohort **without ever breaking the enrollment link or touching billing**. Proven 2026-07-17 moving 10 kids across two source cohorts.
130
-
131
- 1. **Read both cohorts first** (`cohorts get` each): confirm the destination belongs to the **same course** as the enrollments; compare destination `capacity` against current registered count + incoming headcount; collect each kid's `userId` + `enrollmentId` from the source roster; note schedule/timezone/guide differences for the escalation.
132
- 2. **Order is register-first, unregister-second, per kid** — never the reverse. A register that fails (capacity, state) then leaves the kid safely registered in the old cohort; the reverse order would strand them registered nowhere.
133
- ```bash
134
- recess --json enrollments register-cohort --enrollment <enr> --user <kid> --cohort <new> --confirm
135
- recess --json enrollments unregister-cohort --user <kid> --cohort <old> --confirm
136
- ```
137
- 3. **Batch shape:** preview everything → one escalation → run ALL registers → then only the unregisters whose register succeeded.
138
- 4. **Capacity is enforced server-side even on this admin path** (400 `RA_REG_NOT_ALLOWED` "This cohort is at capacity"); capacity editing is web-admin-only, so check it in step 1 or expect safe mid-batch failures.
139
- 5. **Registrations without an Enrollment** (guide's own kid, ops accounts) can't be moved this way — `register-cohort` requires `--enrollment`. List them for the human.
140
- 6. **Verify:** both rosters via `cohorts get`, and each moved kid's enrollment still ACTIVE on the same `stripeSubscriptionId` (`enrollments list --user`).
141
- 7. **Call out in the escalation:** zero family notification (both commands are quiet — offer a `cohorts email` to the destination cohort afterward for the schedule change), billing untouched, and the emptied source cohorts stay ACTIVE (`cohorts end` is a separate decision).
142
- 8. **⚠️ Never run `cohorts end --cancel-subscriptions` on the source cohort afterward.** A move reuses the *same* enrollment and Stripe subscription, and leaves a `CANCELED_REFUNDED` registration row behind in the source cohort. `cohorts end` selects its victims by *any* registration in that cohort regardless of status, so it cancels the subscription that is now paying for the **destination** cohort — and about a week later, when Stripe fires `customer.subscription.deleted`, every registration on that enrollment is swept, including the new one. **This happened on 2026-07-19** and silently unenrolled 9 of 10 just-migrated kids; the only enrollments still ACTIVE on the old cohorts were the ones that had just been moved, so the blast radius was exactly the migrated roster. End the old cohort **without** the flag. Full mechanics: [`web-server.md`](https://github.com/tryrecess/monolith/blob/staging/docs/codebase/web-server.md) § "cohorts end --cancel-subscriptions after a cohort migration".
143
-
144
- ## Verify
145
-
146
- After confirmed writes, re-read: `subscriptions list` for pause/trial/cancel state, `invoices list` for the credit note, `enrollments list` / `cohorts get` for registration changes. Report the final API responses.
@@ -1,53 +0,0 @@
1
- # Class-cancellation credits — the "Please credit these students accordingly" workflow
2
-
3
- Read [`../SKILL.md`](../SKILL.md) first — the safety model, escalation discipline, and input conventions there govern everything below. Pause/refund mechanics live in [`billing.md`](billing.md); read its "Pause timing semantics" and refund payment-composition checklist before running this.
4
-
5
- ## Trigger and why this playbook exists
6
-
7
- When a guide cancels a class session (`events cancel`, or the same route from the web), the backend (`apps/web-server/src/routes/admin/events/post.cancel-event.ts`) fires the family-facing fan-out AND posts to Slack `#cohort-cancellations`:
8
-
9
- > 🚫 Class Canceled … *Registered students:* … _Please credit these students accordingly._
10
-
11
- It also stamps a `UserNote` ("Credit owed: {course} on {date} was canceled…") on each kid registered **at cancel time**. That is where automation stops: **nothing refunds the families.** The crediting is manual, per-kid, and error-prone — this playbook is that step, done agentically. Forensics from the 2026-07-16 "Honey Squad - Steam Games" cancellation (verified via Apitally request logs + live invoice reads) showed the manual process lagging 2 days, silently under-refunding a mixed-composition invoice, and working from a stale roster. Every rule below traces to an observed failure.
12
-
13
- ## The procedure
14
-
15
- **1. Resolve the canceled event.**
16
- ```bash
17
- recess --json cohorts search "<cohort name from the Slack message>"
18
- recess --json cohorts get <cohort-id>
19
- ```
20
- Find the CANCELED event matching the stated date (cohort-local time — "1:30 PM PDT Thursday July 16" = `2026-07-16T20:30Z` for a `America/Los_Angeles` cohort). Note `course.eventsPerWeek` and the bundle's weekly price.
21
-
22
- **2. Build the roster LIVE — never from the Slack message.** The Slack roster is a cancel-time snapshot of `status: REGISTERED` registrations and has been observed to **under-report** (2026-07-16: a kid registered since 2025 was invoiced for the canceled week yet absent from the Slack list). Work from the cohort's current REGISTERED registrations, cross-checked both directions:
23
- - In Slack but no longer registered / enrollment gone → skip, note why in the escalation.
24
- - Registered + invoiced for the canceled week but absent from Slack → include.
25
- - Registration with **no Enrollment** (guide's own kid, ops/test accounts) → nothing to refund; list as "no billing".
26
-
27
- **3. For each kid, find the invoice that covers the canceled session.**
28
- ```bash
29
- recess --json enrollments list --user <kid-id> # → the enrollment registered to THIS cohort → stripeSubscriptionId
30
- recess --json invoices list --subscription <sub-id>
31
- ```
32
- Weekly subscriptions bill on Sunday-00:00-UTC anchors ([`billing.md`](billing.md)): the invoice created on the **Sunday starting the session's week** is the one owed back (session Thu 7/16 → the Sunday 7/12 invoice). No such invoice (pause, trial, joined mid-week) → skip, say so. Match invoice IDs **in full** — two distinct same-week invoices have shared their first 18 characters, differing only in the final 8.
33
-
34
- **New-signup trap:** signups create an immediate backdated invoice + a $0 "Trial period" invoice, and the sub sits `trialing` until a Sunday anchor (see the SKILL.md 2026-07-17 signup-billing gotcha). The immediate invoice pays for the kid's FIRST class week, not necessarily the session's week; if `trial_end` (from `subscriptions list`) is at/after the Sunday ending the session's week, the family was never charged for that session → skip, and don't claw back the immediate invoice (it maps to an earlier session).
35
-
36
- **4. Compute what is owed.** The canceled session's share of that week's invoice line. With `eventsPerWeek: 1` that is the full line (`--full` / the full weekly price). Multi-session weeks: the per-session share — confirm the split with the human rather than guessing.
37
-
38
- **5. Choose the instrument(s) from the invoice's payment composition** (checklist in [`billing.md`](billing.md)):
39
-
40
- | Composition | Action |
41
- |---|---|
42
- | Fully cash-paid (`token_deduction_cents` 0) | `invoices refund --method credit --full` (or `--amount-cents` for a per-session share) |
43
- | Fully token-paid | `--method tokens --amount-cents <token portion>` |
44
- | **Mixed** (tokens + cash) | **BOTH commands**: `tokens` for `token_deduction_cents`, `credit` for the cash remainder (`amount`). Observed live 2026-07-17: ops refunded a $4 token portion and missed the $11 cash portion of a $15 line |
45
- | Already credited (`credited_amount` > 0 or `token_refunded_cents` > 0) | **skip** — someone processed it first. This workflow runs days late and sometimes concurrently with a human; always re-read before previewing |
46
-
47
- **6. Flags.** `--who-pays guide` (the default) for guide-initiated cancellations. Reason: `"Guide cancellation - <cohort> <session date>"` (the observed convention is bare "Guide cancellation"; include the session date so repeat cancellations stay distinguishable).
48
-
49
- **7. Batch escalate.** One approval request: a table of kid → invoice (full ID) → line item → method → amount → reason, plus every skip and why. Then `--confirm` each unchanged command, then verify by re-reading each invoice (`credited_amount` / `token_refunded_cents`) and report final state.
50
-
51
- ## When the CLI itself cancels the class
52
-
53
- `events cancel` triggers this same Slack message and the credit-owed notes ([`class-ops.md`](class-ops.md)). After a confirmed `events cancel`, offer to run this playbook immediately — same-session processing beats the observed 2-day lag and roster drift.
@@ -1,41 +0,0 @@
1
- # Non-flexible course time shifts (`events reschedule` blocked)
2
-
3
- Companion to `reference/class-ops.md` § Reschedules and one-off sessions.
4
-
5
- ## Gate
6
-
7
- `events reschedule` requires `course.allowFlexibleScheduling === true` on `cohorts get`.
8
-
9
- When false, confirmed write returns API 400:
10
-
11
- > This course does not allow editing individual event times. Enable flexible scheduling on the course first.
12
-
13
- CLI cannot toggle the flag — web admin course settings only. Do not stop at "enable flexible scheduling" if staff needs the move now; use the workaround below.
14
-
15
- ## Worked example (2026-07-23)
16
-
17
- - Cohort: Space Technology & Rocket Launches (resolve its id with `cohorts search`)
18
- - Course: Current Events in Space Tech & Rockets — `allowFlexibleScheduling: false`
19
- - Ask: move today up 10 minutes (Starship)
20
- - Usual slot: Thu 15:45 America/Edmonton → UTC `21:45`
21
- - Target: 15:35 Edmonton
22
- - Prior week evidence: ENDED pair on 2026-07-16 at both `21:35` and `21:45` UTC — ops already used one-off add, not true reschedule
23
-
24
- ## Workaround (batch one approval)
25
-
26
- ```bash
27
- # 1) Preview both (no --confirm)
28
- recess --json events add --cohort <cohort-id> \
29
- --starts-at YYYY-MM-DDTHH:MM --timezone <cohort-tz> --length-mins 60
30
- recess --json events set-status <original-event-id> --status CANCELED
31
-
32
- # 2) After human OK, same commands with --confirm
33
- # 3) Verify
34
- recess --json cohorts get <cohort-id> --events-tab ACTIVE
35
- ```
36
-
37
- Rules:
38
-
39
- - Use `set-status CANCELED` (silent). Never `events cancel` unless staff asked to notify families.
40
- - Approval text must say: new one-off + silent cancel of original, no parent blast.
41
- - Prefer this path over asking staff to flip flexible scheduling mid-incident when history shows prior one-offs.
@@ -1,89 +0,0 @@
1
- # Class ops (attendance, cancellations, schedule changes, cohort lifecycle, registration approvals)
2
-
3
- Read [`../SKILL.md`](../SKILL.md) first — the safety model, escalation discipline, and input conventions there govern everything below.
4
-
5
- Cohorts are recurring class groups; each session is an event. Staff instructions arrive in plain language ("cancel Thursday's KSP and let families know", "take attendance for yesterday's 2pm session", "approve the pending signups"). Resolve them to IDs before writing.
6
-
7
- ## Resolve the cohort and event first
8
-
9
- ```bash
10
- recess --json cohorts search "Kids Startup Program"
11
- recess --json cohorts get <cohort-id>
12
- recess --json events get <event-id>
13
- ```
14
-
15
- `cohorts get` returns the cohort's events (IDs, dates, statuses; filter with `--events-tab ACTIVE|ENDED|CANCELED|ARCHIVED`), its registrations including PENDING_APPROVAL ones with their registrationIds, and the derived billing-pause state. `events get` returns the session's roster with each kid's current attendance mark and per-cohort attendance counts. Match a spoken time ("Thursday's class", "the 2pm session") against event dates in the cohort's timezone; if more than one event fits, ask the human which one.
16
-
17
- ## Attendance
18
-
19
- ```bash
20
- recess --json events take-attendance <event-id> \
21
- --attended <kid-id,kid-id> --absent <kid-id> --excused <kid-id> --confirm
22
- ```
23
-
24
- - Run `events get` first and account for every REGISTERED kid: each goes in exactly one of `--attended`, `--absent`, or `--excused`. The CLI rejects overlapping lists and requires at least one list.
25
- - The attendance timestamp is set to now automatically; there is no back-dating flag.
26
- - Non-excused absentees can trigger automatic "we missed you" emails to their parents (when the cohort has absence emails on), and two consecutive absences alert staff in Slack. Name this in the approval request — mis-marking a kid absent emails a real family.
27
- - "Everyone came except Matthew (excused)" → every other registered kid in `--attended`, Matthew in `--excused`.
28
-
29
- ## Cancel vs silent status change
30
-
31
- Two different operations — choose deliberately:
32
-
33
- ```bash
34
- recess --json events cancel <event-id> --reason "Guide is sick" --confirm
35
- recess --json events set-status <event-id> --status CANCELED --confirm
36
- ```
37
-
38
- - "Cancel the class and tell families" → `events cancel`. It notifies kid and parent chat channels, emails every registered parent, files a credit-owed note per student, and posts to Slack. There is no quiet variant of this command; the reason text is shown to families.
39
- - Canceling does NOT refund anyone — the Slack message ends with "Please credit these students accordingly" and that step is on us. After a confirmed `events cancel`, offer to run [`cancellation-credits.md`](cancellation-credits.md) immediately.
40
- - Administrative bookkeeping (event created by mistake, duplicate, already communicated) → `events set-status --status CANCELED`, which sends nothing.
41
- - Reactivate or publish a session → `events set-status --status ACTIVE`; mark it finished → `--status ENDED`.
42
- - If the instruction is ambiguous about notifying, ask; never guess an outward blast.
43
-
44
- ## Reschedules and one-off sessions
45
-
46
- ```bash
47
- recess --json events reschedule --cohort <cohort-id> --event <event-id> \
48
- --starts-at 2026-07-23T15:00 --timezone America/Los_Angeles --confirm
49
- recess --json events add --cohort <cohort-id> --starts-at 2026-07-23T15:00 \
50
- --timezone America/Los_Angeles --length-mins 60 --confirm
51
- ```
52
-
53
- - Always move a session with `events reschedule` — never by editing the event's status or date directly. The reschedule path leaves a tombstone so schedule regeneration doesn't re-create the old slot.
54
- - Times are zoneless cohort-local: "move to 3pm" means `15:00` in the cohort's timezone; a timezone offset in `--starts-at` is rejected. Pass `--timezone` using the value from `cohorts get` when unsure.
55
- - `--length-mins` overrides the session length; omit it to keep the cohort default.
56
- - Only flexible-scheduling courses and ACTIVE events can be rescheduled; a 409 means the target slot already has an event.
57
-
58
- ## Cohort lifecycle and email
59
-
60
- ```bash
61
- recess --json cohorts end <cohort-id> --confirm
62
- recess --json cohorts pause-billing <cohort-id> --weeks 2 --confirm
63
- recess --json cohorts resume-billing <cohort-id> --confirm
64
- recess --json cohorts email <cohort-id> --target ALL_PARENTS --content "..." --confirm
65
- ```
66
-
67
- - `cohorts end` (no flag) archives the cohort's channels and future events and leaves subscriptions untouched. `--cancel-subscriptions` sets EVERY active enrollment's Stripe subscription to cancel at period end — treat the flag as exceptional and call it out explicitly in the approval request, like `who-pays=recess`.
68
- - `pause-billing` pauses Stripe collection for every subscription in the cohort for 1–6 weeks (invoices void while paused). The server computes the window itself: `resumes_at` = upcoming Sunday 00:00 (server time, UTC in prod) + N weeks − 2 hours, because weekly subscriptions bill on Sunday-midnight anchors — so `--weeks N` skips exactly N Sunday invoices and resumes just before the next one (`post.cohort-pause-billing.ts`; full timing semantics in [`billing.md`](billing.md)). It also stamps `Enrollment.pausedAt`/`pauseResumesAt`, which is where `cohorts get`'s derived billing-pause state comes from. Enrollments already paused (`behavior: "void"`) or with `expireAtPeriodEnd` are skipped. Both directions are Stripe writes affecting real payments. `resume-billing` takes no `--weeks` and clears the pause plus the enrollment stamps.
69
- - `cohorts email` sends a real outward email blast. Before requesting approval, run `cohorts parent-emails <cohort-id>` and include the recipient count in the escalation. `ALL_PARENTS_GUIDES` adds guides, owners, and support. Content starting with a dash run needs the `--content="…"` form (see the flag parser traps in SKILL.md).
70
-
71
- ## Registration approvals
72
-
73
- `cohorts get` lists PENDING_APPROVAL registrations with their registrationIds.
74
-
75
- ```bash
76
- recess --json registrations approve --registration <registration-id> --confirm
77
- recess --json registrations deny --cohort <cohort-id> --user <kid-id> --confirm
78
- ```
79
-
80
- - Approve flips the registration to REGISTERED and sends the family a welcome email.
81
- - Deny removes the registration AND cancels the enrollment — that is what the web admin's Deny button does. Never substitute the enrollment-preserving `enrollments unregister-cohort` for a denial (or vice versa).
82
-
83
- ## Batch and verify
84
-
85
- Same discipline as payout ops: collect all previews without `--confirm`, present one batched approval request that names every side effect (family notifications, absence emails, Stripe changes, welcome emails), then rerun unchanged with `--confirm` and re-read (`events get` / `cohorts get`) to verify the new state.
86
-
87
- ## Out of scope
88
-
89
- Cohort creation, start, full schedule edit/RRULE regeneration, generate-events, and guides management stay in the web admin cohort pages. If asked, direct the human there.
@@ -1,274 +0,0 @@
1
- # Authoring learning content from the CLI
2
-
3
- The workflow behind `skills`, `goal-templates`, `goals`, and `mesa files`. Read this before the
4
- first write; the tool→command mapping and the one-way rules live in `SKILL.md` under "Learning-content
5
- contract".
6
-
7
- The point of this surface is not that an agent *can* create a template — the API always allowed
8
- that. It is that an agent can create a template **as good as one authored through `recess.gg/ai`**,
9
- because it loads the same authoring skills the in-product tutor loads. Skipping step 0 gives you a
10
- structurally valid template that is pedagogically wrong, and nothing downstream will catch it.
11
-
12
- ---
13
-
14
- ## 0. Load the authoring skills. Always. First.
15
-
16
- ```bash
17
- recess --json skills get os-v2-goal-template-builder --all-references
18
- ```
19
-
20
- `--all-references` pulls `references/deterministic-workflow-setup.md` in the same call — that
21
- document is the catalog of what the deterministic handlers can express, and you cannot author a
22
- correct spec without it. Without the flag you get SKILL.md plus a reference *index*, and each
23
- reference is a separate `--reference <name>` call (the same two-step the in-product agent uses).
24
-
25
- Responses cache for about an hour under `~/.recess-cli/skills-cache/`. Pass `--refresh` when someone
26
- has just edited a skill.
27
-
28
- These documents are proprietary and are served, never shipped. Do not copy their text anywhere.
29
-
30
- ### Which research doc, for what
31
-
32
- "Deep research" is not one method here — pick by what you are researching. Every one of these assumes
33
- OSAgent research tools you do not have; SKILL.md's "Research tools → your own harness" says what to
34
- substitute, and names the one real gap (`fetch_page_structure`).
35
-
36
- | Researching | Load |
37
- |---|---|
38
- | An external platform's assignable content, to bake a queue into a template | `os-v2-goal-template-builder --all-references` → §"The research loop" + §"The validation loop" |
39
- | Content for a module-backed goal workspace | `os-v2-goal-builder --all-references` → `research-protocol.md` (wave dispatch, lookup order, verification rules, output file shape) |
40
- | A goal from an uploaded curriculum (PDF, scope-and-sequence) | `os-v2-goal-builder --reference curriculum-source` |
41
- | A goal for one kid, from scratch, in depth | `goal-creation --reference structured-plan` (S1–S6; S4 is the parallel deep research, S2 carries the depth cascade) + `--reference subagent-tasks` for the five researcher prompts |
42
- | The same, but the ask is small | `goal-creation --reference quick-discovery` — it names its own escalation trigger into the structured flow |
43
- | One platform, deeply (pedagogy, reviews) | `pipeline-platform-research --all-references` |
44
- | The kid, before any of the above | `student-research` |
45
-
46
- Research before you draft, not after. The spec's `handler.config` must arrive **fully materialized** —
47
- the apply endpoint never fetches anything, so whatever you did not bake in does not exist at apply
48
- time, and there is no later pass that fills it in.
49
-
50
- **If the request cannot be expressed by a supported handler, report that and stop.** The skill says
51
- this explicitly and it is the single most important instruction in it. A template that "sort of"
52
- does the job becomes a global record every future apply reads.
53
-
54
- ## 1. Decide the kind before drafting
55
-
56
- `kind` is the real decision; `setupMode` is not a choice (see §5).
57
-
58
- | Kind | What a student gets | Use when |
59
- |---|---|---|
60
- | `SIMPLE` | a description-only goal plus linked todos — no modules, no Mesa workspace | ongoing habits, platform practice targets, score/streak goals, free-choice reading |
61
- | `BLUEPRINT` | a finishable learning path, built per student by the planner or copied from a snapshot | a course with a sequence and an end |
62
-
63
- The skill phrases this as a tutor-facing question with three options (the third, "Prebuilt", is a
64
- BLUEPRINT that also carries an instant-apply snapshot). You have no `ask_user_question` tool — ask
65
- the human directly, in the skill's language, and wait. Do not say "materialize",
66
- "module-backed", or "snapshot" to a tutor.
67
-
68
- A MODULE_BACKED spec **requires** `BLUEPRINT`; the server enforces it, and it additionally refuses to
69
- apply a MODULE_BACKED template that has no snapshot. Build a complete Mesa draft and capture it with
70
- the CLI workflow in §8 before applying the template.
71
-
72
- ## 2. Write one template file
73
-
74
- `validate-spec` and `create` read the same document, so you iterate on one artifact:
75
-
76
- ```jsonc
77
- {
78
- "slug": "kebab-case-unique", // required, immutable-ish: it is the human handle
79
- "title": "Daily Reading Habit", // required
80
- "description": "One line a tutor reads in the library.", // required
81
- "kind": "SIMPLE", // SIMPLE | BLUEPRINT (default SIMPLE)
82
- "setupAudience": "KID_FRIENDLY", // KID_FRIENDLY | PARENT_SETUP
83
- "emoji": "📚",
84
- "category": "reading",
85
- "tags": ["reading", "ela"], // an array, or a comma string
86
- "sortOrder": 0,
87
- "isStarter": false,
88
- "agentInstructions": "…", // required
89
- "outputTemplate": "…",
90
- "setupWorkflowSpec": { /* the fixed wizard — a real object, never a JSON string */ }
91
- }
92
- ```
93
-
94
- Unknown keys are dropped, so a file made by editing `goal-templates get <id>` output works —
95
- `version`, `createdById`, `createdAt` are ignored rather than rejected. `setupMode` must be absent
96
- (or `DETERMINISTIC_WORKFLOW`); anything else is refused locally.
97
-
98
- ## 3. Iterate on `validate-spec` — it writes nothing
99
-
100
- ```bash
101
- recess --json goal-templates validate-spec --file ./template.json
102
- ```
103
-
104
- Valid:
105
-
106
- ```json
107
- {"ok":true,"data":{"slug":"…","kind":"SIMPLE","valid":true,
108
- "setupHandler":"TOOL_GENERATED_TODO_SETUP","goalShape":"SIMPLE",
109
- "stepKeys":["students"],"sha256":"…",
110
- "totals":{"subjects":0,"recipes":0,"plans":0,"queueItems":0,"sourceUrls":0,"missingCoverage":0}}}
111
- ```
112
-
113
- Invalid returns `"valid": false` with the backend's verbatim message — read it and fix the file.
114
- `ok` stays `true` because an invalid spec is an expected step in an authoring loop, not a failure of
115
- the command. Handler configs are `.strict()`, so a guessed config fails with both missing-field and
116
- `unrecognized_keys` errors at once; that is your cue to go back to
117
- `references/deterministic-workflow-setup.md` rather than guessing again.
118
-
119
- `stepKeys` is the contract for §6: those are exactly the keys your answers file must carry.
120
-
121
- ## 4. Create — read the server-resolved preview
122
-
123
- ```bash
124
- recess --json goal-templates create --file ./template.json # preview, exit 2
125
- recess --json goal-templates create --file ./template.json --confirm
126
- ```
127
-
128
- The unconfirmed run performs one read-only validation and puts its result in `preview.details`:
129
-
130
- | Field | Why it is in the approval request |
131
- |---|---|
132
- | `resolvedSetupHandler` | which handler actually runs — derived from the spec, not from what you wrote in the file |
133
- | `resolvedGoalShape` | `SIMPLE` vs `MODULE_BACKED`; decides whether a snapshot is required to apply |
134
- | `wizardStepKeys` | the questions a tutor will be asked, and the answer keys `apply` will demand |
135
- | `specInventory` | subjects / recipes / plans / queue items / source URLs the template starts with |
136
-
137
- Show all of it. `action` alone does not tell a human what the template does.
138
-
139
- If the spec is invalid at this point the command fails with `invalid_spec` and creates nothing —
140
- even with `--confirm`. That ordering is deliberate: an approved-but-broken template must not land.
141
-
142
- ## 5. What is permanent
143
-
144
- **Every template created here is `setupMode: DETERMINISTIC_WORKFLOW` and cannot be converted back.**
145
- There is no AI_CHAT creation path and no downgrade — the server refuses the conversion outright.
146
- Legacy AI_CHAT templates remain readable and editable; you will never create one.
147
-
148
- So the confirmation gate carries more weight here than on a reversible write. What *is* recoverable:
149
- metadata (`set-metadata`) and the row itself (`delete` is a soft delete, and goals already applied
150
- from the template are unaffected).
151
-
152
- ## 6. Apply to a kid or a roster
153
-
154
- Answers are a JSON file keyed by the spec's `stepKeys`:
155
-
156
- ```jsonc
157
- { "students": ["<kid-uuid>", "<kid-uuid>"], "grade": "5" }
158
- ```
159
-
160
- ```bash
161
- recess --json goal-templates apply <id-or-slug> --answers-file ./answers.json --dry-run
162
- recess --json goal-templates apply <id-or-slug> --answers-file ./answers.json # preview
163
- recess --json goal-templates apply <id-or-slug> --answers-file ./answers.json --confirm
164
- ```
165
-
166
- `--dry-run` is a read and does not gate. The unconfirmed form runs the same backend dry run to build
167
- its preview, so `details.counts` and `details.results` are the backend's own plan, per student:
168
-
169
- - `created` — a new goal + todo
170
- - `skipped_existing` — idempotent; re-applying does not duplicate
171
- - `error` — that student failed; the others still committed (failure is per item, never per batch)
172
-
173
- `details.missingCoverage` lists content the planner could not resolve. Surface it — an apply that
174
- "succeeded" with missing coverage produced a thinner goal than intended.
175
-
176
- `apply-starter` is the one-tap starter path: one template, one student, no dry run, so its preview is
177
- offline. It is gated per acting admin behind the `school-onboarding-v1` flag and **404s when the flag
178
- is off** — which reads like a missing template but is not.
179
-
180
- ## 7. A goal directly on a kid
181
-
182
- ```bash
183
- recess --json goals list --student <kid-id>
184
- recess --json goals create --student <kid-id> --title "…" --description-file ./goal.md --confirm
185
- ```
186
-
187
- This creates a **description-only** goal: no GoalModules, no Mesa workspace. For a real course,
188
- apply a BLUEPRINT template instead. `--description-file` exists because a good goal description is
189
- long and prose-shaped; a shell mangles it.
190
-
191
- A 409 `GOAL_LIMIT_REACHED` means the kid is at capacity and nothing was created — do not retry, ask
192
- the human which goal to retire.
193
-
194
- Load `goal-creation` (and `student-research` to read the kid first) before writing the description.
195
- An unresearched goal is the failure mode this whole surface exists to prevent.
196
-
197
- ## 8. Author, capture, and read workspaces
198
-
199
- For a new Prebuilt/instant-apply course, author the complete OS-V2 tree locally and upsert it into a
200
- named draft. The directory root becomes `drafts/<slug>/workspace` in the student's Mesa repo:
201
-
202
- ```bash
203
- recess --json mesa files write --student <kid-id> --draft <draft-slug> --source-dir ./workspace
204
- # show the preview, obtain approval, then rerun unchanged:
205
- recess --json mesa files write --student <kid-id> --draft <draft-slug> --source-dir ./workspace --confirm
206
-
207
- recess --json goal-templates capture-snapshot <id-or-slug> --source-draft <draft-slug> --student <kid-id> --dry-run
208
- recess --json goal-templates capture-snapshot <id-or-slug> --source-draft <draft-slug> --student <kid-id>
209
- # show the preview, obtain approval, then rerun unchanged:
210
- recess --json goal-templates capture-snapshot <id-or-slug> --source-draft <draft-slug> --student <kid-id> --confirm
211
- ```
212
-
213
- The draft must pass the full goal-builder contract: required top-level instructions, required fresh
214
- `state/` files, directory content, and at least one parseable runtime module. Capture validates that
215
- tree, then excludes `state/`, `conversations/`, and `.recess/` from the reusable snapshot. Its
216
- preview carries the template version, Mesa change id, module/file inventory, size, and SHA-256; the
217
- confirmed request is compare-and-set against both fences. If either changed, preview again and get
218
- fresh approval.
219
-
220
- Use `--source-file <local-file> --path <workspace-relative-path>` for one-file upserts. A live goal
221
- may be targeted with `--goal <goal-id>`, but the CLI rejects direct `modules/` and `state/` writes
222
- because those paths have database projections. Structural course changes belong in a draft, then a
223
- captured/applied template.
224
-
225
- An already-built OS-V2 goal can be captured directly:
226
-
227
- ```bash
228
- recess --json goal-templates capture-snapshot <id-or-slug> --source-goal <goal-id> --dry-run
229
- ```
230
-
231
- Read the resulting workspaces and snapshot with:
232
-
233
- ```bash
234
- recess --json mesa files list --student <kid-id> --goal <goal-id>
235
- recess --json mesa files read --student <kid-id> --goal <goal-id> --path modules/01/index.md
236
- recess --json goal-templates snapshot-files <id-or-slug>
237
- recess --json goal-templates snapshot-files <id-or-slug> --path modules/01/index.md
238
- ```
239
-
240
- `mesa files list` returning `{"files":[]}` means the goal has no Mesa workspace at all — expected for
241
- a SIMPLE goal, and the signal that a BLUEPRINT goal has not been built yet.
242
-
243
- ## 9. Spec patches, metadata edits, and deletes are fenced
244
-
245
- ```bash
246
- recess --json goal-templates get <id-or-slug> # read `version`
247
- recess --json goal-templates patch-spec <id-or-slug> --expected-version 7 --patches-file ./patches.json
248
- recess --json goal-templates set-metadata <id> --expected-version 7 --title "…" --confirm
249
- recess --json goal-templates delete <id> --expected-version 7 --confirm
250
- ```
251
-
252
- `--expected-version` is mandatory. A stale one returns 409 `STALE_WRITE` (server) or `stale_write`
253
- (the CLI's own pre-check) and **nothing is written** — re-read, rebuild the edit, and get a fresh
254
- approval. Never reuse an approval across a stale-write refresh.
255
-
256
- `patches.json` is a non-empty JSON array (maximum 50 operations) using `add`, `copy`, `replace`, and
257
- `remove` with RFC 6901 JSON Pointer paths. The unconfirmed command sends `dryRun:true` to the server,
258
- which applies the operations in memory, strict-validates the complete result, and returns
259
- `details.safety` with before/after hashes, inventory totals, removals, and (when destructive) a
260
- token. Show that exact preview to the human. After approval, rerun the same command with `--confirm`;
261
- if `destructiveChanges` is true, also pass `--confirm-destructive-changes` and the preview's exact
262
- `--destructive-change-token`. The confirmed run obtains a fresh preview before writing, and a stale
263
- version or token writes nothing.
264
-
265
- `set-metadata` cannot send a `setupWorkflowSpec` at all. Never replace a whole live spec through a
266
- generic update or raw request; `patch-spec` is the only CLI path for an existing spec.
267
-
268
- ## Verify after every write
269
-
270
- ```bash
271
- recess --json goal-templates get <id-or-slug> # version bumped? metadata right?
272
- recess --json goal-templates versions <id-or-slug> # a new frozen version row exists
273
- recess --json goals list --student <kid-id> # the goal actually landed on the kid
274
- ```