recess-cli 1.0.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.
- package/README.md +95 -0
- package/dist/api.js +120 -0
- package/dist/args.js +71 -0
- package/dist/auth.js +189 -0
- package/dist/cli.js +1537 -0
- package/dist/config.js +79 -0
- package/dist/errors.js +18 -0
- package/dist/index.js +41 -0
- package/dist/safety.js +7 -0
- package/dist/setup.js +43 -0
- package/package.json +48 -0
- package/skill/recess-cli/SKILL.md +290 -0
- package/skill/recess-cli/agents/openai.yaml +4 -0
- package/skill/recess-cli/reference/billing.md +146 -0
- package/skill/recess-cli/reference/cancellation-credits.md +53 -0
- package/skill/recess-cli/reference/class-ops-reschedule.md +41 -0
- package/skill/recess-cli/reference/class-ops.md +89 -0
- package/skill/recess-cli/reference/map-scores.md +28 -0
- package/skill/recess-cli/reference/onboarding.md +79 -0
- package/skill/recess-cli/reference/payout.md +74 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export const DEFAULT_OAUTH_CLIENT_ID = "c7e34138-18f9-45b1-a2fb-26a4e3a6d739";
|
|
5
|
+
export function defaultConfigPath() {
|
|
6
|
+
return (process.env.RECESS_CLI_CONFIG ??
|
|
7
|
+
path.join(os.homedir(), ".recess-cli", "config.json"));
|
|
8
|
+
}
|
|
9
|
+
export async function readStoredConfig(configPath = defaultConfigPath()) {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(await fs.readFile(configPath, "utf8"));
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error.code === "ENOENT")
|
|
15
|
+
return {};
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function resolveConfig() {
|
|
20
|
+
const configPath = defaultConfigPath();
|
|
21
|
+
const stored = await readStoredConfig(configPath);
|
|
22
|
+
const envCookie = process.env.RECESS_CLI_COOKIE;
|
|
23
|
+
const sessionCookie = envCookie ?? stored.sessionCookie;
|
|
24
|
+
return {
|
|
25
|
+
configPath,
|
|
26
|
+
apiOrigin: process.env.RECESS_CLI_API_ORIGIN ??
|
|
27
|
+
stored.apiOrigin ??
|
|
28
|
+
"https://api.recess.gg",
|
|
29
|
+
webOrigin: process.env.RECESS_CLI_WEB_ORIGIN ??
|
|
30
|
+
stored.webOrigin ??
|
|
31
|
+
"https://recess.gg",
|
|
32
|
+
oauthClientId: process.env.RECESS_CLI_OAUTH_CLIENT_ID ??
|
|
33
|
+
stored.oauthClientId ??
|
|
34
|
+
DEFAULT_OAUTH_CLIENT_ID,
|
|
35
|
+
sessionCookie,
|
|
36
|
+
sessionExpiresAt: stored.sessionExpiresAt,
|
|
37
|
+
user: stored.user,
|
|
38
|
+
authSource: envCookie ? "env" : sessionCookie ? "config" : "missing",
|
|
39
|
+
pendingDeviceCode: stored.pendingDeviceCode,
|
|
40
|
+
pendingUserCode: stored.pendingUserCode,
|
|
41
|
+
pendingApprovalUrl: stored.pendingApprovalUrl,
|
|
42
|
+
pendingExpiresAt: stored.pendingExpiresAt,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export async function writeStoredConfig(config, configPath = defaultConfigPath()) {
|
|
46
|
+
const directory = path.dirname(configPath);
|
|
47
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
48
|
+
const temporary = `${configPath}.${process.pid}.tmp`;
|
|
49
|
+
await fs.writeFile(temporary, `${JSON.stringify(config, null, 2)}\n`, {
|
|
50
|
+
mode: 0o600,
|
|
51
|
+
});
|
|
52
|
+
await fs.rename(temporary, configPath);
|
|
53
|
+
await fs.chmod(configPath, 0o600);
|
|
54
|
+
}
|
|
55
|
+
export async function updateStoredConfig(update) {
|
|
56
|
+
const configPath = defaultConfigPath();
|
|
57
|
+
const current = await readStoredConfig(configPath);
|
|
58
|
+
const next = { ...current, ...update };
|
|
59
|
+
await writeStoredConfig(next, configPath);
|
|
60
|
+
return next;
|
|
61
|
+
}
|
|
62
|
+
export async function clearStoredSession() {
|
|
63
|
+
const configPath = defaultConfigPath();
|
|
64
|
+
const current = await readStoredConfig(configPath);
|
|
65
|
+
delete current.sessionCookie;
|
|
66
|
+
delete current.sessionExpiresAt;
|
|
67
|
+
delete current.user;
|
|
68
|
+
await writeStoredConfig(current, configPath);
|
|
69
|
+
}
|
|
70
|
+
export async function clearPendingDeviceAuth() {
|
|
71
|
+
const configPath = defaultConfigPath();
|
|
72
|
+
const current = await readStoredConfig(configPath);
|
|
73
|
+
delete current.pendingDeviceCode;
|
|
74
|
+
delete current.pendingUserCode;
|
|
75
|
+
delete current.pendingApprovalUrl;
|
|
76
|
+
delete current.pendingExpiresAt;
|
|
77
|
+
await writeStoredConfig(current, configPath);
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=config.js.map
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class CliError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
exitCode;
|
|
4
|
+
details;
|
|
5
|
+
constructor(code, message, exitCode = 1, details) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.exitCode = exitCode;
|
|
9
|
+
this.details = details;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function apiError(status, body) {
|
|
13
|
+
const message = typeof body === "object" && body && "message" in body
|
|
14
|
+
? String(body.message)
|
|
15
|
+
: `Recess API request failed with status ${status}`;
|
|
16
|
+
return new CliError("api_error", message, 1, { status, body });
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=errors.js.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runCommand } from "./cli.js";
|
|
3
|
+
import { CliError } from "./errors.js";
|
|
4
|
+
const json = process.argv.slice(2).includes("--json");
|
|
5
|
+
try {
|
|
6
|
+
const data = await runCommand(process.argv.slice(2));
|
|
7
|
+
if (typeof data === "object" && data && "help" in data && !json) {
|
|
8
|
+
process.stdout.write(`${String(data.help)}\n`);
|
|
9
|
+
}
|
|
10
|
+
else if (json) {
|
|
11
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}\n`);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
const cliError = error instanceof CliError
|
|
19
|
+
? error
|
|
20
|
+
: new CliError("unexpected_error", error instanceof Error ? error.message : String(error));
|
|
21
|
+
if (json) {
|
|
22
|
+
process.stdout.write(`${JSON.stringify({
|
|
23
|
+
ok: false,
|
|
24
|
+
error: {
|
|
25
|
+
code: cliError.code,
|
|
26
|
+
message: cliError.message,
|
|
27
|
+
...(cliError.details === undefined
|
|
28
|
+
? {}
|
|
29
|
+
: { details: cliError.details }),
|
|
30
|
+
},
|
|
31
|
+
})}\n`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
process.stderr.write(`Error: ${cliError.message}\n`);
|
|
35
|
+
if (cliError.details) {
|
|
36
|
+
process.stderr.write(`${JSON.stringify(cliError.details, null, 2)}\n`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
process.exitCode = cliError.exitCode;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=index.js.map
|
package/dist/safety.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { CliError } from "./errors.js";
|
|
2
|
+
export function requireConfirmation(confirmed, preview) {
|
|
3
|
+
if (confirmed)
|
|
4
|
+
return;
|
|
5
|
+
throw new CliError("confirmation_required", "This admin action is a live write. Review the preview, obtain explicit human approval, then rerun with --confirm.", 2, { preview, requiredFlag: "--confirm" });
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=safety.js.map
|
package/dist/setup.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
/** Bundled skill source: `<package>/skill/recess-cli`, from `dist/` or `src/`. */
|
|
7
|
+
const skillSource = path.resolve(here, "..", "skill", "recess-cli");
|
|
8
|
+
/**
|
|
9
|
+
* Skill directories from earlier names of this CLI. An agent that finds both
|
|
10
|
+
* would read two overlapping skills describing a command that no longer exists,
|
|
11
|
+
* so setup removes them.
|
|
12
|
+
*/
|
|
13
|
+
const SUPERSEDED_SKILL_DIRECTORIES = ["recess-admin"];
|
|
14
|
+
export async function installSkill() {
|
|
15
|
+
const codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
|
|
16
|
+
const claudeHome = process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
|
|
17
|
+
const destinations = [
|
|
18
|
+
{ agent: "Codex", path: path.join(codexHome, "skills", "recess-cli") },
|
|
19
|
+
{ agent: "Claude", path: path.join(claudeHome, "skills", "recess-cli") },
|
|
20
|
+
];
|
|
21
|
+
for (const destination of destinations) {
|
|
22
|
+
const skillsRoot = path.dirname(destination.path);
|
|
23
|
+
await fs.mkdir(skillsRoot, { recursive: true });
|
|
24
|
+
await fs.rm(destination.path, { recursive: true, force: true });
|
|
25
|
+
await fs.cp(skillSource, destination.path, { recursive: true });
|
|
26
|
+
for (const superseded of SUPERSEDED_SKILL_DIRECTORIES) {
|
|
27
|
+
await fs.rm(path.join(skillsRoot, superseded), {
|
|
28
|
+
recursive: true,
|
|
29
|
+
force: true,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return destinations;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* True when this process is running from a throwaway `npx` cache rather than a
|
|
37
|
+
* real install. The bundled skill tells the agent to run `recess ...`, so
|
|
38
|
+
* an npx-only setup leaves the skill pointing at a command that is not on PATH.
|
|
39
|
+
*/
|
|
40
|
+
export function isEphemeralInstall() {
|
|
41
|
+
return here.includes(`${path.sep}_npx${path.sep}`);
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=setup.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "recess-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Safe Recess staff administration from the command line, for humans and coding agents.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"recess": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/**/*.js",
|
|
12
|
+
"!dist/**/*.test.js",
|
|
13
|
+
"skill",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"openapi-fetch": "^0.14.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^24.10.0",
|
|
27
|
+
"@typescript/native-preview": "7.0.0-dev.20251015.1",
|
|
28
|
+
"openapi-typescript": "^7.8.0",
|
|
29
|
+
"oxlint": "^1.28.0",
|
|
30
|
+
"oxlint-tsgolint": "^0.6.0",
|
|
31
|
+
"typescript": "^5.9.2",
|
|
32
|
+
"vitest": "^1.6.0",
|
|
33
|
+
"@tryrecess/typescript-config": "0.0.0"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc",
|
|
37
|
+
"client:generate": "node scripts/generate-api-types.mjs",
|
|
38
|
+
"typecheck": "tsgo --noEmit",
|
|
39
|
+
"format": "oxfmt --ignore-path .oxfmtignore --check",
|
|
40
|
+
"format:fix": "oxfmt --ignore-path .oxfmtignore",
|
|
41
|
+
"lint": "oxlint --type-aware src/*.ts",
|
|
42
|
+
"lint:fix": "oxlint --type-aware --fix src/*.ts",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"install-local": "pnpm run build && make install-local",
|
|
45
|
+
"install-persistent": "pnpm run build && make install-persistent",
|
|
46
|
+
"install-skill": "pnpm run build && node scripts/install-skill.mjs"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: recess-cli
|
|
3
|
+
description: Safely perform Recess staff administration through the recess CLI. Use when a Recess admin asks Codex to find a kid, parent, family, enrollment, subscription, invoice, or cohort; inspect or change a school kid's tier and capability gates; upload a kid's MAP Growth report; pause or resume billing; refund or credit an invoice item; extend a trial; cancel or restore a subscription; register or unregister a cohort against an enrollment; switch or move kids from one cohort to another; manage guide payout invoices (biweekly pay-cycle line-item changes, invoice status moves, payout recipient lookups); run class operations (take attendance, cancel or reschedule a class session, add a one-off session, end a cohort, pause cohort billing, email a cohort's families, approve or deny pending registrations); or process class-cancellation credits (the "Please credit these students accordingly" Slack message — credit every registered kid for a guide-canceled session).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Recess CLI (`recess`)
|
|
7
|
+
|
|
8
|
+
Use the installed `recess` command. Never bypass it with direct production database writes, Stripe calls, browser clicks, or hand-written API requests. Every command here operates on **live production data — real families, real kids, real money.**
|
|
9
|
+
|
|
10
|
+
> **Maintain this skill.** Every time you discover a new failure mode, a working invocation, or a surprising behavior, add it to the "Gotchas" notebook at the bottom (append-only, dated). When a whole workflow changes, update the matching playbook in `reference/`. The skill's source of truth lives in the monolith at `apps/admin-cli/skill/recess-cli/` — fix it there, then reinstall (`pnpm --dir apps/admin-cli run install-skill`) — the published npm package ships the same bundle, so a skill change reaches non-checkout installs only on the next `recess-cli` release.
|
|
11
|
+
|
|
12
|
+
## The safety model — non-negotiable
|
|
13
|
+
|
|
14
|
+
Every mutating command is two-step. Run it **without** `--confirm` first: the CLI returns `confirmation_required` (exit code 2) plus the exact `action`, `target`, and `request` body it would send, and makes **no network write**.
|
|
15
|
+
|
|
16
|
+
Some previews also carry a `details` object — server-resolved facts that cannot be known offline.
|
|
17
|
+
Today those are `enrollments create` (real price, no-charge reuse, and capacity/slot violations) and
|
|
18
|
+
`users tier set` (current/proposed capability locks and class-slot consequences); getting either
|
|
19
|
+
costs read-only calls, never a write. **When `details` is present it is part of the preview — show it
|
|
20
|
+
to the human too.** Approving from `action`/`request` alone while ignoring `details` is how an
|
|
21
|
+
override gets rubber-stamped.
|
|
22
|
+
|
|
23
|
+
1. Show that preview to the human, verbatim.
|
|
24
|
+
2. Request explicit escalated approval for that exact action using the execution tool's approval mechanism.
|
|
25
|
+
3. Only after the human approves, rerun the **unchanged** command with `--confirm`.
|
|
26
|
+
|
|
27
|
+
Never infer approval from the original task, prior approval, urgency, or a successful read. Never add `--confirm` yourself before the approval arrives. If any ID, amount, refund method, payer, pause date, or target changes, discard the approval and preview again.
|
|
28
|
+
|
|
29
|
+
**Exceptional actions** get named explicitly in the approval request, on top of the normal preview:
|
|
30
|
+
|
|
31
|
+
| Flag / command | Why it is exceptional |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `enrollments create` | **charges a real family real money.** Quote `details.billing.effectivePriceCents` (the discounted amount actually billed, NOT `listPriceCents`), the recurrence, and the first-charge date in the approval request |
|
|
34
|
+
| `enrollments create --force` | overrides a cohort-capacity or school-class-slot violation; name the specific warning being overridden |
|
|
35
|
+
| `users tier set --allow-strand` | lowers a kid's class allowance below active registrations; quote `slotsUsed`, the proposed allowance, and both capability-lock lists |
|
|
36
|
+
| `billing cancel-subscription --immediate` | no period-end grace — access ends now |
|
|
37
|
+
| `invoices refund … --who-pays recess` | Recess absorbs the cost instead of the guide |
|
|
38
|
+
| `cohorts end --cancel-subscriptions` | sets EVERY active enrollment's Stripe subscription to cancel at period end |
|
|
39
|
+
| `cohorts email` | real outward email blast to families (include the recipient count from `cohorts parent-emails`) |
|
|
40
|
+
| `events cancel` | family-facing fan-out: chat messages, parent email blast, credit-owed notes, Slack |
|
|
41
|
+
| `payout invoices set-status` to `OPEN`/`PAID`/`CANCELED` | moves real account balances; `--send-email` additionally emails the guide |
|
|
42
|
+
|
|
43
|
+
## Setup, auth, and troubleshooting
|
|
44
|
+
|
|
45
|
+
- Binary: `recess` (published on npm as `recess-cli`). The standard install needs no monolith checkout — `npm install -g recess-cli` then `recess setup` (installs this skill for Codex and Claude, then opens SSO). From a checkout, `pnpm --dir apps/admin-cli run install-persistent` builds a self-contained copy under `~/.recess-cli/cli/` linked from `~/.local/bin/recess`; `install-local` symlinks the checkout's live `dist/` instead — CLI-development only, and the link dies with its worktree.
|
|
46
|
+
- Session: a **12-hour** signed admin cookie stored at `~/.recess-cli/config.json` (mode 0600). It is **not refreshable** — when it expires, a human must approve a fresh sign-in. On a headless agent, use `auth request` + `auth poll`; on a local workstation, `auth login` remains available.
|
|
47
|
+
- Start every session with:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
command -v recess
|
|
51
|
+
recess --json doctor
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`doctor` reports the config paths/origins, the auth state (it validates the stored session against `GET /auth/admin-cli/session/`), and API reachability (`/health`). Interpret it before running anything else.
|
|
55
|
+
- If auth is missing or expired on a headless agent, start a device-authorization request:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
recess --json auth request --label "<agent or VM name>"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Show the returned `approvalUrl` and `userCode` to the human. The secret device code stays only in the mode-0600 config file and must never be copied into chat or logs. After the human approves the web page, collect the session:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
recess --json auth poll --timeout 300
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
A timed-out poll preserves the pending request, so it is safe to rerun after approval. A denied, expired, or already-used request needs a fresh `auth request`.
|
|
68
|
+
|
|
69
|
+
- On a workstation with a browser, the human can instead complete:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
recess --json auth login # uses the built-in production OAuth client
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The login opens a browser to Recess SSO and listens on `127.0.0.1:8765` (5-minute timeout). A non-default `--callback-port` only works if that exact loopback URL is registered on the OAuth client — otherwise production rejects the redirect.
|
|
76
|
+
- `auth status` shows the current session and whether it came from the env or the config file; `auth logout` clears the stored session.
|
|
77
|
+
|
|
78
|
+
Environment overrides (all optional): `RECESS_CLI_API_ORIGIN` (default `https://api.recess.gg`), `RECESS_CLI_WEB_ORIGIN` (default `https://recess.gg`), `RECESS_CLI_OAUTH_CLIENT_ID` (overrides the built-in production client for local/staging), `RECESS_CLI_COOKIE` (session override, wins over the config file), `RECESS_CLI_CONFIG` (config path). Pointing `RECESS_CLI_API_ORIGIN` at a local server is how dev/testing works; if it is set, you are NOT talking to production — say so in previews.
|
|
79
|
+
|
|
80
|
+
Auth failure modes:
|
|
81
|
+
|
|
82
|
+
| Error code | Meaning | Fix |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `auth_required` | no stored session | headless: `auth request` + human approval + `auth poll`; workstation: `auth login` |
|
|
85
|
+
| `api_error` with 401 on any command | session expired or revoked | acquire a fresh session through the same headless or workstation flow |
|
|
86
|
+
| `callback_unavailable` | port 8765 already in use | free the port or use a registered `--callback-port` |
|
|
87
|
+
| `auth_timeout` / `auth_failed` | SSO not completed in 5 min / denied | retry the login in the browser |
|
|
88
|
+
|
|
89
|
+
## The JSON contract
|
|
90
|
+
|
|
91
|
+
Always pass `--json` (it may appear anywhere in the argv). Stdout then carries exactly one JSON object:
|
|
92
|
+
|
|
93
|
+
- Success: `{"ok":true,"data":{…}}` — exit code 0.
|
|
94
|
+
- Failure: `{"ok":false,"error":{"code":"…","message":"…","details":{…}}}` — exit code 1.
|
|
95
|
+
- Write awaiting approval: code `confirmation_required`, `details.preview` = `{action,target,request}` (plus `details` on commands with server-resolved facts, e.g. `enrollments create`), `details.requiredFlag` = `--confirm` — **exit code 2**.
|
|
96
|
+
|
|
97
|
+
Without `--json`, success pretty-prints to stdout but errors go to **stderr** as text — so agents use `--json` unconditionally. Error codes you will see: `invalid_arguments`, `auth_required`, `not_found`, `api_error` (carries `details.status` and the raw API `details.body`), `confirmation_required`, `unknown_command`, `unexpected_error`, plus the auth codes above.
|
|
98
|
+
|
|
99
|
+
## Input conventions
|
|
100
|
+
|
|
101
|
+
- **Money is integer cents**, always (`--amount-cents 28800` = $288.00). Parse dollar expressions yourself; never send fractional values.
|
|
102
|
+
- **Three date shapes, not interchangeable:**
|
|
103
|
+
- `--until` / `--trial-end`: ISO date, must be in the future (`2026-09-01`).
|
|
104
|
+
- Payout item `--date`: bare `YYYY-MM-DD` (anchored to UTC noon so the calendar day survives timezone conversion) or a full ISO timestamp.
|
|
105
|
+
- Event `--starts-at`: **zoneless** cohort-local wall clock, `YYYY-MM-DDTHH:MM` — a timezone offset is rejected. "3pm" means `15:00` in the cohort's timezone.
|
|
106
|
+
- **ID and status lists are comma-separated:** `--attended id1,id2` · `--status IN_REVIEW,DRAFT`.
|
|
107
|
+
- Search queries are positional and may span words: `recess --json users search Morgan Rivera` works, quotes optional.
|
|
108
|
+
- **Flag parser traps:** `--flag value` and `--flag=value` are both accepted, but a value that itself starts with `--` MUST use the `=` form (`--content="-- headline --"`), or it is parsed as another flag. Misspelled flags are not rejected — an unknown `--flag` silently swallows the next token, and the real flag then errors as "Missing required". On any confusing `invalid_arguments`, re-check flag spelling first.
|
|
109
|
+
|
|
110
|
+
## The standard loop (reads resolve before writes)
|
|
111
|
+
|
|
112
|
+
1. **Resolve names to IDs.** `users search` returns the matching people, their family, every family member, and each member's enrollments and cohort registrations. Never act on a guessed ID; stop on ambiguous matches and ask the human which family or user they mean.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
recess --json users search "parent or kid name"
|
|
116
|
+
recess --json enrollments list --user <kid-id>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
2. **Read the exact current state** before proposing any write (playbooks list the reads per workflow).
|
|
120
|
+
3. **Collect previews** by running every planned write WITHOUT `--confirm`.
|
|
121
|
+
4. **Batch the escalation:** present the complete change plan — every preview, every side effect (emails, Stripe changes, notifications), every exceptional flag — as ONE approval request.
|
|
122
|
+
5. **Execute confirmed:** rerun each command unchanged with `--confirm`.
|
|
123
|
+
6. **Verify:** re-read the touched resources (`payout invoices get`, `events get`, `cohorts get`, `invoices list`, …) and check the new state matches the plan. Report the final API responses.
|
|
124
|
+
|
|
125
|
+
## Command quick reference
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# Health & auth
|
|
129
|
+
recess --json doctor
|
|
130
|
+
recess --json auth login|status|logout
|
|
131
|
+
|
|
132
|
+
# People & enrollment context
|
|
133
|
+
recess --json users search <name-or-id> [--limit 10]
|
|
134
|
+
recess --json users get <user-id>
|
|
135
|
+
recess --json users tier list-tiers
|
|
136
|
+
recess --json users tier get <kid-id>
|
|
137
|
+
recess --json users tier preview <kid-id> --tier <id> [--slots N]
|
|
138
|
+
recess --json users tier set <kid-id> --tier <id> [--slots N] \
|
|
139
|
+
--expected-updated-at <iso> [--allow-strand] [--confirm]
|
|
140
|
+
recess --json enrollments list --user <user-id>
|
|
141
|
+
recess --json enrollments get-for-subscription --subscription <id>
|
|
142
|
+
|
|
143
|
+
# Billing & subscriptions (reference/billing.md)
|
|
144
|
+
recess --json subscriptions list --family <family-id> [--kid <kid-id>]
|
|
145
|
+
recess --json invoices list --subscription <subscription-id>
|
|
146
|
+
recess --json billing pause --subscription <id> [--until ISO_DATE] [--confirm]
|
|
147
|
+
recess --json billing resume --subscription <id> [--confirm]
|
|
148
|
+
recess --json billing extend-trial --subscription <id> --trial-end ISO_DATE [--confirm]
|
|
149
|
+
recess --json billing cancel-subscription --subscription <id> [--immediate] [--reason TEXT] [--restore] [--confirm]
|
|
150
|
+
recess --json invoices refund --invoice <id> --line-item <id> --method refund|credit|tokens \
|
|
151
|
+
[--full | --amount-cents N] [--who-pays guide|recess] [--reason TEXT] [--confirm]
|
|
152
|
+
recess --json enrollments create --user <kid-id> --cohort <id> \
|
|
153
|
+
[--first-charge-at ISO_DATETIME] [--send-email] [--force] [--confirm]
|
|
154
|
+
recess --json enrollments register-cohort --enrollment <id> --user <id> --cohort <id> [--confirm]
|
|
155
|
+
recess --json enrollments unregister-cohort --user <id> --cohort <id> [--confirm]
|
|
156
|
+
|
|
157
|
+
# MAP Growth reports (reference/map-scores.md)
|
|
158
|
+
recess --json students upload-map-scores --student <kid-id> --file <report.pdf> [--confirm]
|
|
159
|
+
|
|
160
|
+
# Guide payouts (reference/payout.md)
|
|
161
|
+
recess --json payout payruns list [--status A,B] [--schedule <id>]
|
|
162
|
+
recess --json payout recipients list [--search <name>] [--user <id>] [--id <id>]
|
|
163
|
+
recess --json payout invoices list [--payrun <id>] [--recipient <account-id>] [--user <id>] [--status A,B]
|
|
164
|
+
recess --json payout invoices get <invoice-id>
|
|
165
|
+
recess --json payout invoices set-status <invoice-id> --status IN_REVIEW|OPEN|PAID|CANCELED [--send-email] [--confirm]
|
|
166
|
+
recess --json payout items add --invoice <id> --amount-cents N --description TEXT [--date YYYY-MM-DD] [--confirm]
|
|
167
|
+
recess --json payout items edit <item-id> [--amount-cents N] [--description TEXT] [--date YYYY-MM-DD] [--confirm]
|
|
168
|
+
recess --json payout items delete <item-id> [--confirm]
|
|
169
|
+
|
|
170
|
+
# Class ops (reference/class-ops.md)
|
|
171
|
+
recess --json cohorts search <query>
|
|
172
|
+
recess --json cohorts get <cohort-id> [--events-tab ACTIVE|ENDED|CANCELED|ARCHIVED]
|
|
173
|
+
recess --json cohorts parent-emails <cohort-id>
|
|
174
|
+
recess --json cohorts end <cohort-id> [--cancel-subscriptions] [--confirm]
|
|
175
|
+
recess --json cohorts pause-billing <cohort-id> --weeks 1..6 [--confirm]
|
|
176
|
+
recess --json cohorts resume-billing <cohort-id> [--confirm]
|
|
177
|
+
recess --json cohorts email <cohort-id> --target ALL_PARENTS|ALL_PARENTS_GUIDES --content TEXT [--confirm]
|
|
178
|
+
recess --json events get <event-id>
|
|
179
|
+
recess --json events take-attendance <event-id> --attended <ids> [--absent <ids>] [--excused <ids>] [--confirm]
|
|
180
|
+
recess --json events cancel <event-id> --reason TEXT [--confirm]
|
|
181
|
+
recess --json events set-status <event-id> --status ACTIVE|ENDED|CANCELED [--confirm]
|
|
182
|
+
recess --json events reschedule --cohort <id> --event <id> --starts-at "YYYY-MM-DDTHH:MM" [--timezone <iana>] [--length-mins N] [--confirm]
|
|
183
|
+
recess --json events add --cohort <id> --starts-at "YYYY-MM-DDTHH:MM" [--timezone <iana>] [--length-mins N] [--confirm]
|
|
184
|
+
recess --json registrations approve --registration <id> [--confirm]
|
|
185
|
+
recess --json registrations deny --cohort <id> --user <id> [--confirm]
|
|
186
|
+
|
|
187
|
+
# Onboarding (reference/onboarding.md) — all keyed on family-id
|
|
188
|
+
recess --json onboarding status <family-id>
|
|
189
|
+
recess --json onboarding kids [--time-period-days N] [--cohort <id>] [--limit N] [--stage-filter all|scheduled|oriented|course|converted|lost]
|
|
190
|
+
recess --json onboarding intake-session <family-id>
|
|
191
|
+
recess --json onboarding set-stage <family-id> --stage LEGACY|PROVISIONED|PARENT_CONFIRMED|CLEARED_FOR_COHORT|COMPLETE [--confirm]
|
|
192
|
+
recess --json onboarding set-account-state <family-id> --state ACTIVE|PENDING_PAYMENT|PAUSED|BOOTED [--note TEXT] [--confirm]
|
|
193
|
+
recess --json onboarding attest <family-id> --condition app_downloaded|tutor_met|goals_loaded|ma_diagnostic [--revoke] [--note TEXT] [--confirm]
|
|
194
|
+
recess --json onboarding set-intake <family-id> --session <id> --data <json> [--expected-updated-at <iso>] [--confirm]
|
|
195
|
+
recess --json onboarding extract <family-id> --session <id> (--transcript-file <path> | --granola <ref>) [--confirm]
|
|
196
|
+
|
|
197
|
+
# Read-only escape hatch (GET only — no raw writes exist)
|
|
198
|
+
recess --json request get /path?query=value
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### School tier contract
|
|
202
|
+
|
|
203
|
+
`users tier get` is the read-before-write source: copy its `updatedAt` into
|
|
204
|
+
`--expected-updated-at`, then run `preview` (or the unconfirmed `set`) with the intended tier and
|
|
205
|
+
slot override. Show the human `slotsUsed`, the proposed allowance,
|
|
206
|
+
`capabilitiesLockedNow`/`capabilitiesLockedAfter`, `wouldStrandRegistrations`, and `enforcementOn`.
|
|
207
|
+
Tier names are not a capability ladder: with enforcement on, `social` (and an unassigned tier)
|
|
208
|
+
locks `todo`, `browser`, and `tutor_chat`; the other four canonical tiers lock none. With
|
|
209
|
+
`enforcementOn:false`, the row still changes but the kid observes no capability change until the
|
|
210
|
+
`school-onboarding-v1` flag turns on. Never infer lock consequences from the tier name.
|
|
211
|
+
|
|
212
|
+
The CLI refuses `wouldStrandRegistrations:true` by default. `--allow-strand` is an exceptional
|
|
213
|
+
override, not a retry hint: use it only after the human explicitly approves leaving more active
|
|
214
|
+
registrations than class slots. A 409 `STALE_WRITE` means the kid changed after the read; fetch a new
|
|
215
|
+
token, preview again, and discard the old approval.
|
|
216
|
+
|
|
217
|
+
## Workflow playbooks
|
|
218
|
+
|
|
219
|
+
Each domain has a full playbook in this skill's `reference/` directory. Read the matching one BEFORE running that domain's writes — they carry the semantics that make the writes correct (which command notifies families, how amounts are computed, which statuses permit which operations).
|
|
220
|
+
|
|
221
|
+
| Task | Playbook |
|
|
222
|
+
|---|---|
|
|
223
|
+
| **Signing a kid up for a class / enrolling / "add them to this cohort" / "the parent wants to buy X"**, subscriptions, pauses, trials, cancellations, refunds/credits/token refunds, cohort register/unregister, moving kids between cohorts | [`reference/billing.md`](reference/billing.md) |
|
|
224
|
+
| MAP Growth report uploads | [`reference/map-scores.md`](reference/map-scores.md) |
|
|
225
|
+
| Guide payout invoices — the biweekly "CHANGES TO MAKE EVERY 2WKS" instruction | [`reference/payout.md`](reference/payout.md) |
|
|
226
|
+
| Attendance, class cancellations, reschedules, one-off sessions, cohort lifecycle, cohort email, registration approvals | [`reference/class-ops.md`](reference/class-ops.md) |
|
|
227
|
+
| Class-cancellation credits — the "Please credit these students accordingly" Slack workflow | [`reference/cancellation-credits.md`](reference/cancellation-credits.md) |
|
|
228
|
+
| Non-flexible course one-off time shift when `events reschedule` 400s (`allowFlexibleScheduling: false`) | [`reference/class-ops-reschedule.md`](reference/class-ops-reschedule.md) |
|
|
229
|
+
| Family onboarding — stage, account state, attestation checklist, parent intake session (fill / LLM-extract) | [`reference/onboarding.md`](reference/onboarding.md) |
|
|
230
|
+
|
|
231
|
+
## Deliberately out of scope
|
|
232
|
+
|
|
233
|
+
These are excluded from the CLI on purpose. If asked, direct the human to the web admin — do not improvise around the gap.
|
|
234
|
+
|
|
235
|
+
- **Any raw write:** there is no `request post/put/patch/delete`. `request get` exists only for read-only endpoints missing a high-level command.
|
|
236
|
+
- **Money movement:** initiating Mercury payouts, advancing whole pay runs, generating/regenerating payout invoices → `/admin/payout` in the web admin.
|
|
237
|
+
- **Enrollment cancellation** as a standalone action (`unregister-cohort` deliberately preserves the enrollment; only `registrations deny` cancels one, because that is what the web Deny button does).
|
|
238
|
+
- **Cohort creation and schedule editing:** create, start, full-edit/RRULE regeneration, generate-events, guides management → web admin cohort pages.
|
|
239
|
+
|
|
240
|
+
## Guardrails
|
|
241
|
+
|
|
242
|
+
- Use `--json` for analysis and preserve the CLI's stable JSON envelope.
|
|
243
|
+
- Treat the exceptional actions table above as mandatory callouts in every approval request.
|
|
244
|
+
- For `users tier set`, quote capability lock lists rather than describing one tier as "higher" or
|
|
245
|
+
"lower"; the mapping is binary and flag-gated. Never reuse an approval after a stale-write refresh.
|
|
246
|
+
- `unregister-cohort` leaves the enrollment active; never substitute enrollment cancellation (or vice versa — see `registrations deny`).
|
|
247
|
+
- **`enrollments create` vs `enrollments register-cohort` — pick wrong and you either double-charge a family or do nothing.** `create` sells a class: it makes a *new* Stripe subscription and charges the family. `register-cohort` only links a kid to a cohort against an enrollment they **already** hold, and moves no money. If the family is already paying for this course and you just need the kid in a cohort (a move, a swap, a slot they already bought), use `register-cohort`. Use `create` only when they are genuinely buying something new. When unsure, run `enrollments create` unconfirmed and read `details.reusedEnrollmentId`: if it is set, the kid already had a paid slot and `create` will correctly charge nothing — that is also your signal that `register-cohort` would have been the direct route.
|
|
248
|
+
- `enrollments create` is the ONLY command that spends a family's money. Its unconfirmed run performs a server-side dry run and returns the real resolved price, so never quote a price from the course catalog or from memory — quote `details.billing.effectivePriceCents` — the post-discount amount, never `listPriceCents` — and mention `creditBalanceCents` when non-zero, since credits reduce the first invoice further. If `details.reusedEnrollmentId` is set, say plainly that nothing will be charged.
|
|
249
|
+
- `enrollments create` replaces impersonating a guardian and walking their checkout. Never suggest impersonation to sign a kid up.
|
|
250
|
+
- Never retry `enrollments create` after an `ENROLLMENT_PROVISION_FAILED` error. The Stripe subscription already exists; retrying sells a second one. Escalate to engineering with the subscription ID from the error message.
|
|
251
|
+
- Stop on ambiguous search results and ask the human which family or user they mean.
|
|
252
|
+
- Report the final API response and re-read the affected resource when a read command can verify the new state.
|
|
253
|
+
|
|
254
|
+
## Gotchas (append-only lab notebook)
|
|
255
|
+
|
|
256
|
+
Dated, newest last. Add an entry every time reality surprises you.
|
|
257
|
+
|
|
258
|
+
- 2026-07-16 — Flag parser: an unknown/misspelled `--flag` is not rejected — it silently swallows the next token as its value, and the real flag then errors "Missing required --…". Check flag spelling first on weird `invalid_arguments`.
|
|
259
|
+
- 2026-07-16 — Values starting with `--` (e.g. email content beginning with a dash run) must use `--flag=value` form; the space form parses the value as a new flag.
|
|
260
|
+
- 2026-07-16 — `enrollments list --user <id>` is operations-search under the hood with the ID as the search term; an ID that search can't find returns `not_found` even if the user exists but is outside the search surface.
|
|
261
|
+
- 2026-07-16 — `payout items add` without `--date` performs one READ before the confirmation gate (fetches the invoice to compute the default item date: endDate − 1 day). Expected; not a write.
|
|
262
|
+
- 2026-07-16 — MAP uploads run the full AI extraction pipeline server-side and can take materially longer than other writes; don't treat a slow response as a hang. `kind:"learning_statements"` with zero inserted scores is a SUCCESS (narrative NWEA report → Mesa files), not a failure.
|
|
263
|
+
- 2026-07-16 — The 12h session is not refreshable and login is browser-interactive; an agent can never self-heal auth. Hand `recess --json auth login` to the human and wait.
|
|
264
|
+
- 2026-07-16 — `events reschedule` returning 409 means the target slot already has an event. Only flexible-scheduling courses and ACTIVE events can be rescheduled.
|
|
265
|
+
- 2026-07-16 — `events take-attendance` stamps `attendanceTakenAt` with the current time automatically; there is no back-dating flag.
|
|
266
|
+
- 2026-07-16 — `payout items edit --amount-cents` sets net = total (custom items carry no platform fee). Editing an auto-generated (non-custom) item with it would overwrite net semantics — only edit custom line items.
|
|
267
|
+
- 2026-07-16 — `request get` requires a path starting with exactly one `/`; `//host` forms are rejected (SSRF guard).
|
|
268
|
+
- 2026-07-16 — Payout guide emails on `set-status IN_REVIEW|OPEN` fire only with `--send-email` (server default false; suppressed on staging regardless). PAID and CANCELED invoices are terminal — the server 400s any later status change.
|
|
269
|
+
- 2026-07-17 — `billing pause` always uses Stripe `pause_collection.behavior="void"`: invoices generated during the pause are VOIDED (family never charged), never deferred for later collection; access is unaffected. Weekly subscriptions bill on Sunday-00:00 (server/UTC) anchors, so `--until` (bare date → midnight UTC = the Stripe `resumes_at`) voids every Sunday tick strictly before it and charges the first tick at/after it. Pick a mid-week `--until` between the last Sunday to skip and the next one to charge. Full rules: `reference/billing.md` § Pause timing semantics.
|
|
270
|
+
- 2026-07-17 — Per-subscription `billing pause` writes only to Stripe; `Enrollment.pausedAt`/`pauseResumesAt` stay null (only `cohorts pause-billing` stamps them). Verify via `subscriptions list`, never the enrollment row.
|
|
271
|
+
- 2026-07-17 — `billing pause`/`resume --confirm` returns `api_error` with `details.status: 200` even on SUCCESS: the server route (`post.pause-collection.ts`) does the Stripe update but never sends its declared `{success:true}` body, so the CLI rejects the empty 200. The write has landed — verify with `subscriptions list` (`pause_collection` + `pause_collection_resume_at`) instead of retrying blind. (Route patched 2026-07-17 to send the body; the false error persists until that deploys.) **RESOLVED 2026-07-20** — the patch is live in prod; `billing pause --confirm` now returns a clean `{"ok":true,"data":{"success":true}}`. Verify-after-write still applies, but an `api_error` here is now a real failure, not the known false alarm.
|
|
272
|
+
- 2026-07-17 — Before any refund/credit, read the invoice's payment composition from `invoices list` (`token_deduction_cents`, `applied_balance`, `amount` vs `subtotal`, `paymentIntent.status`) and state it in the approval request — token-paid portions go back as `--method tokens`, balance-covered portions mean a "full" cash refund over-refunds. Checklist in `reference/billing.md`.
|
|
273
|
+
- 2026-07-17 — The cancellation Slack message's "Registered students" list is a cancel-time snapshot and can under-report: a kid registered since 2025 and invoiced for the canceled week was absent from it (Honey Squad, 7/16). Build the roster live from the cohort's REGISTERED registrations when processing credits (`reference/cancellation-credits.md`).
|
|
274
|
+
- 2026-07-17 — Mixed-composition invoices (tokens + cash) need TWO refund commands, one per portion; observed a manual pass refund the $4 token portion of a $15 line and miss the $11 cash portion. Skip kids whose line already shows `credited_amount`/`token_refunded_cents` > 0 — the manual process runs days late and may race you.
|
|
275
|
+
- 2026-07-17 — Stripe invoice IDs can share their first ~18 chars within the same week (`in_1TsAobBBeAjEgjlseDHTGOT1` vs `in_1TsAobBBeAjEgjlsHynCvkUi`) — never prefix-match invoice/line IDs; compare in full.
|
|
276
|
+
- 2026-07-17 — `register-cohort` enforces cohort capacity server-side (400 `RA_REG_NOT_ALLOWED` "This cohort is at capacity") — the admin quiet-link path does NOT bypass it, and capacity editing is out of CLI scope (web admin). Before a batch cohort move, compare `cohorts get` `capacity` against incoming headcount; register-first/unregister-second per kid means a capacity failure leaves that kid safely in the old cohort.
|
|
277
|
+
- 2026-07-17 — Signup billing shape: a new class signup creates an IMMEDIATE real invoice (backdated to the last Sunday anchor — line reads "Time on <course> from <Sun> until <Sun>", often with a first-week coupon) plus a $0 "Trial period" invoice; the "trial" is an anchor-reset bridge to Sunday billing (`apps/web-server/src/libs/stripe/create-subscription.ts`), NOT a free period. A recently signed-up kid can be `status: trialing` with `trial_end` a Sunday 1–2 weeks out, so a given session week may legitimately have NO invoice (family genuinely not charged) even though the kid paid the immediate invoice for an earlier week. Same-cohort same-week signups can differ (observed: one kid trial_end 7/12 and charged $25 for the 7/12 week; another trial_end 7/19, never charged for it). Before refunding a session for a new signup, read `subscriptions list` `trial_end` to learn which week each invoice actually covers — don't map "Trial period invoice" to "never paid" or the immediate invoice to the current week.
|
|
278
|
+
- 2026-07-18 — An `install-local` symlink dies when its source worktree is deleted: every invocation exits 127 "no such file or directory". Fix by reinstalling with `pnpm --dir apps/admin-cli run install-persistent` (self-contained copy in `~/.recess-cli/cli/`), which no worktree deletion can break.
|
|
279
|
+
- 2026-07-18 — Per-kid excused-absence credits (guide asks to credit specific absent kids; class still runs) follow the `cancellation-credits.md` mechanics minus the roster sweep: same Sunday-anchor invoice lookup, same composition check, `invoices refund --method credit --full`, reason "Excused absence - <cohort> <date>". Surface the `--who-pays` call explicitly — the guide default means the requesting guide absorbs the cost, which the human may want to override for a courtesy credit.
|
|
280
|
+
- 2026-07-18 — A fully balance-paid invoice (`applied_balance` = -subtotal, `amount` 0, no tokens) takes `--method credit --full` cleanly: the credit note restores the consumed customer balance. The billing.md over-refund warning for `applied_balance < 0` is about CASH refunds (`--method refund`), not balance credits. Verified live on two CoLab invoices (cn_1Tufqt…, cn_1Tufqz…).
|
|
281
|
+
- 2026-07-18 — Never call a Slack credit/absence request "unprocessed" from the thread alone — processed requests routinely get no Slack reply. The read-before-assert discipline applies to volunteered recommendations and status summaries, not just writes you're about to execute. Completion evidence lives in billing state and takes TWO reads per kid: `invoices list` (a past week handled by credit shows `credited_amount` + memo) AND `subscriptions list` (a future week can be pre-handled by a billing pause, which leaves NO invoice or enrollment trace — only Stripe `pause_collection`). Observed live: flagged two Brannock requests as open when both credits and a pre-emptive pause through 7/29 were already in place.
|
|
282
|
+
- 2026-07-20 — "Skip next week" from a family is ambiguous mid-week and the two readings need OPPOSITE commands — resolve it with the human before previewing. The Sunday anchor means the week already in progress is ALREADY INVOICED AND PAID, so a pause does nothing for it (that week needs `invoices refund`); only the not-yet-issued Sunday tick can be voided by `billing pause`. Read `invoices list` for the latest `created_date` (a Sunday 00:00 UTC stamp) and the subscription's `period_end` to see exactly where the paid/unpaid boundary sits, then ask which session they mean. Observed live: Mon 7/20 request to skip "next week" on a Thursday 1-on-1 — Thu 7/23 was already paid, Thu 7/30 was not.
|
|
283
|
+
- 2026-07-20 — A billing pause deliberately leaves the session ACTIVE on the calendar; skipping the charge and canceling the class are separate decisions with wildly different blast radii (pause = silent, `events cancel` = family email blast + chat + Slack). Ask which one the human wants rather than assuming a skipped week implies a canceled session.
|
|
284
|
+
- 2026-07-20 — **A fully-credited MIXED invoice does NOT show `credited_amount` == the line's full amount** — the token half lands in the invoice-level `token_refunded_cents` while the line's `credited_amount` only ever reflects the CASH half. Foundations of Science 8-11, Natan Rocklin: a complete $30.00 credit verifies as `credited_amount: 2001` + `token_refunded_cents: 999` + `token_refundable_remaining_cents: 0` on a line whose `amount` is 3000. Read it as under-refunded and you will double-credit the family. The reliable "is this line fully made whole?" test is `credited_amount + token_refunded_cents == line amount` AND `token_refundable_remaining_cents == 0`. Note the two writes also return different envelopes: `--method credit` gives `{noteId: "cn_…"}`, `--method tokens` gives `{creditTransactionId, newBalance}` (no credit note exists for a token refund).
|
|
285
|
+
- 2026-07-20 — Payment composition varies PER KID inside a single cohort week — never read one kid's invoice and apply that instrument to the roster. One 3-kid cancellation sweep hit all three shapes at once: fully cash-paid, fully balance-paid (`applied_balance` −3000, `amount` 0, no paymentIntent), and mixed tokens+cash. Same course, same $30 price, same Sunday invoice batch, three different correct commands (and four total writes for three kids).
|
|
286
|
+
- 2026-07-20 — The invoice-ID prefix collision is not rare enough to ignore: within ONE kid-cohort sweep, Zayn Kacem's Foundations invoice `in_1TuiGeBBeAjEgjlsrwjle3Oz` and Andromeda Brand's Terraria invoice `in_1TuiGeBBeAjEgjlszZROm407` shared their first 18 chars (both minted in the same Sunday 00:07 UTC batch run). Scope every `invoices list` to the specific subscription and compare IDs in full — the same-second batch anchor is exactly what manufactures these near-twins.
|
|
287
|
+
- 2026-07-21 — The earlier browser-only auth limitation is resolved for headless agents: run `auth request`, give the returned approval URL/code to a human admin, then run `auth poll`. The device secret remains in the mode-0600 config and must never be surfaced. The resulting session still expires after 12 hours and cannot refresh itself.
|
|
288
|
+
- 2026-07-21 — **Never answer "was this cancellation refund handled?" from the DB alone.** `CreditTransactionLog` only shows `--method tokens` grants; Stripe cash/balance path (`--method credit` / `--method refund`) lands only as invoice `credited_amount` + credit-note memo via `invoices list`. Observed live: Foundations of Science 8-11 7/20 — Zayn (full cash credit $30) and Andromeda (full balance credit $30) looked "open" in DB/token logs while admin CLI invoices already showed `credited_amount: 3000` with memo `Guide cancellation - Foundations of Science (8-11) 2026-07-20`. Always verify completion with `recess --json invoices list --subscription <sub>` (and the mixed-invoice test `credited_amount + token_refunded_cents == line amount`).
|
|
289
|
+
- 2026-07-21 — **Future canceled session = pause, not credit.** When the cancel lands before the Sunday that starts the canceled week, no invoice exists yet — `invoices refund` has nothing to target. Use `billing pause --until` mid-week after that Sunday to void only that tick (Scratch 'n Hack 2 Aug 4 cancel → pause until 2026-08-06 on `sub_1TlVBBB…`). Documented in `reference/cancellation-credits.md` §3b. Don't route these threads to Linear; load this skill + cancellation-credits playbook immediately on `#cohort-cancellations` / "Please credit these students accordingly".
|
|
290
|
+
- 2026-07-23 — `events reschedule` on a non-flexible course 400s (`allowFlexibleScheduling: false`) even for a simple ±10 min move (Space Technology & Rocket Launches / Starship). Check `course.allowFlexibleScheduling` on `cohorts get` **before** promising a reschedule. Staff workaround already in use: `events add` at the new cohort-local time + `events set-status CANCELED` on the original (silent — not `events cancel`). Enabling flexible scheduling is web-admin-only. Details: `reference/class-ops-reschedule.md`.
|