recess-cli 2.2.0 → 2.4.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 +23 -2
- package/dist/api.js +17 -0
- package/dist/auth.js +7 -2
- package/dist/cli.js +253 -9
- package/dist/command-schema.js +222 -8
- package/dist/commands/onboarding.js +186 -0
- package/dist/commands/school.js +262 -0
- package/dist/help.js +54 -3
- package/dist/http.js +45 -2
- package/package.json +1 -1
- package/skill/recess-cli/SKILL.md +7 -4
- package/skill/recess-cli/agents/version.json +2 -2
package/dist/command-schema.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CliError } from "./errors.js";
|
|
2
|
-
export const AGENT_CONTEXT_SCHEMA_VERSION = "
|
|
2
|
+
export const AGENT_CONTEXT_SCHEMA_VERSION = "4";
|
|
3
3
|
const BOOLEAN_FLAGS = new Set([
|
|
4
4
|
"all-references",
|
|
5
5
|
"apply",
|
|
@@ -50,6 +50,185 @@ const GLOBAL_FLAG_TYPES = {
|
|
|
50
50
|
profile: "string",
|
|
51
51
|
reason: "string",
|
|
52
52
|
};
|
|
53
|
+
const LOCAL_COMMAND_NOUNS = new Set([
|
|
54
|
+
"--version",
|
|
55
|
+
"agent-context",
|
|
56
|
+
"auth",
|
|
57
|
+
"doctor",
|
|
58
|
+
"feedback",
|
|
59
|
+
"jobs",
|
|
60
|
+
"profile",
|
|
61
|
+
"setup",
|
|
62
|
+
]);
|
|
63
|
+
const AUTHENTICATED_COMMAND_PREFIXES = [
|
|
64
|
+
"village build",
|
|
65
|
+
"village library",
|
|
66
|
+
"village objects",
|
|
67
|
+
"village render",
|
|
68
|
+
];
|
|
69
|
+
const FAMILY_AI_COMMANDS = new Set([
|
|
70
|
+
"content-library search",
|
|
71
|
+
"goal-templates apply",
|
|
72
|
+
"goal-templates apply-starter",
|
|
73
|
+
"goal-templates capture-snapshot",
|
|
74
|
+
"goal-templates create",
|
|
75
|
+
"goal-templates get",
|
|
76
|
+
"goal-templates list",
|
|
77
|
+
"goal-templates snapshot-files",
|
|
78
|
+
"goal-templates validate-spec",
|
|
79
|
+
"goals archive",
|
|
80
|
+
"goals create",
|
|
81
|
+
"goals edit",
|
|
82
|
+
"goals files checkout",
|
|
83
|
+
"goals files init",
|
|
84
|
+
"goals files list",
|
|
85
|
+
"goals files push",
|
|
86
|
+
"goals files read",
|
|
87
|
+
"goals files write",
|
|
88
|
+
"goals list",
|
|
89
|
+
"goals pdf upload",
|
|
90
|
+
"goals queue get",
|
|
91
|
+
"goals queue set",
|
|
92
|
+
"goals unarchive",
|
|
93
|
+
"memories context",
|
|
94
|
+
"memories log",
|
|
95
|
+
"onboarding pairing-code",
|
|
96
|
+
"request get",
|
|
97
|
+
"rocky get",
|
|
98
|
+
"rocky set",
|
|
99
|
+
"skills guardian get",
|
|
100
|
+
"skills guardian list",
|
|
101
|
+
"students list",
|
|
102
|
+
"students schedule",
|
|
103
|
+
"students today",
|
|
104
|
+
"students xp-history",
|
|
105
|
+
"todos create",
|
|
106
|
+
"todos edit",
|
|
107
|
+
]);
|
|
108
|
+
const STAFF_COMMANDS = new Set([
|
|
109
|
+
"cohorts email",
|
|
110
|
+
"cohorts end",
|
|
111
|
+
"cohorts get",
|
|
112
|
+
"cohorts parent-emails",
|
|
113
|
+
"cohorts pause-billing",
|
|
114
|
+
"cohorts resume-billing",
|
|
115
|
+
"cohorts search",
|
|
116
|
+
"enrollments create",
|
|
117
|
+
"enrollments register-cohort",
|
|
118
|
+
"enrollments unregister-cohort",
|
|
119
|
+
"events add",
|
|
120
|
+
"events cancel",
|
|
121
|
+
"events get",
|
|
122
|
+
"events reschedule",
|
|
123
|
+
"events set-status",
|
|
124
|
+
"events take-attendance",
|
|
125
|
+
"goal-templates delete",
|
|
126
|
+
"goal-templates set-metadata",
|
|
127
|
+
"goal-templates versions",
|
|
128
|
+
"goals complete",
|
|
129
|
+
"goals undo-completion",
|
|
130
|
+
"onboarding active-tutors",
|
|
131
|
+
"onboarding attest",
|
|
132
|
+
"onboarding clear-for-cohort",
|
|
133
|
+
"onboarding cohort-options",
|
|
134
|
+
"onboarding comms",
|
|
135
|
+
"onboarding contracts",
|
|
136
|
+
"onboarding doctor",
|
|
137
|
+
"onboarding extract",
|
|
138
|
+
"onboarding family",
|
|
139
|
+
"onboarding generate-summaries",
|
|
140
|
+
"onboarding intake-session",
|
|
141
|
+
"onboarding intake-session-create",
|
|
142
|
+
"onboarding ixl-preview",
|
|
143
|
+
"onboarding kids",
|
|
144
|
+
"onboarding lifecycle-prompts",
|
|
145
|
+
"onboarding mark-reviewed",
|
|
146
|
+
"onboarding next",
|
|
147
|
+
"onboarding orientation-attendance",
|
|
148
|
+
"onboarding orientation-sessions",
|
|
149
|
+
"onboarding provision-ixl",
|
|
150
|
+
"onboarding provision-math-academy",
|
|
151
|
+
"onboarding queue",
|
|
152
|
+
"onboarding readiness",
|
|
153
|
+
"onboarding register-cohort",
|
|
154
|
+
"onboarding remove-ixl",
|
|
155
|
+
"onboarding review",
|
|
156
|
+
"onboarding reviews",
|
|
157
|
+
"onboarding seed-feed",
|
|
158
|
+
"onboarding send-comms",
|
|
159
|
+
"onboarding send-contract",
|
|
160
|
+
"onboarding set-intake",
|
|
161
|
+
"onboarding set-kid-grade",
|
|
162
|
+
"onboarding set-primary-tutor",
|
|
163
|
+
"onboarding starter-coverage",
|
|
164
|
+
"onboarding status",
|
|
165
|
+
"onboarding timeline",
|
|
166
|
+
"payout invoices get",
|
|
167
|
+
"payout invoices list",
|
|
168
|
+
"payout invoices set-status",
|
|
169
|
+
"payout items add",
|
|
170
|
+
"payout items delete",
|
|
171
|
+
"payout items edit",
|
|
172
|
+
"payout payruns list",
|
|
173
|
+
"payout recipients list",
|
|
174
|
+
"registrations approve",
|
|
175
|
+
"registrations deny",
|
|
176
|
+
"school codes get",
|
|
177
|
+
"school codes list",
|
|
178
|
+
"school create",
|
|
179
|
+
"school credit-transactions",
|
|
180
|
+
"school families",
|
|
181
|
+
"school family-search",
|
|
182
|
+
"school kid-slots",
|
|
183
|
+
"school list",
|
|
184
|
+
"school logo-upload",
|
|
185
|
+
"school partner-family",
|
|
186
|
+
"school representatives add",
|
|
187
|
+
"school representatives list",
|
|
188
|
+
"school representatives remove",
|
|
189
|
+
"school representatives search",
|
|
190
|
+
"school update",
|
|
191
|
+
"store-items list",
|
|
192
|
+
"students upload-map-scores",
|
|
193
|
+
"todos complete",
|
|
194
|
+
"todos delete",
|
|
195
|
+
"todos generate-applet",
|
|
196
|
+
"users get",
|
|
197
|
+
"users tier list-tiers",
|
|
198
|
+
]);
|
|
199
|
+
/**
|
|
200
|
+
* Classify the command itself, not the target passed to it. Target-level
|
|
201
|
+
* family, assignment, ownership, and feature-flag checks remain server-owned.
|
|
202
|
+
* A newly added command fails closed to admin-only discovery until its CLI
|
|
203
|
+
* audience is classified here.
|
|
204
|
+
*/
|
|
205
|
+
function commandAccess(path) {
|
|
206
|
+
const key = path.join(" ");
|
|
207
|
+
if (LOCAL_COMMAND_NOUNS.has(path[0]))
|
|
208
|
+
return "local";
|
|
209
|
+
if (AUTHENTICATED_COMMAND_PREFIXES.some((prefix) => key === prefix || key.startsWith(`${prefix} `))) {
|
|
210
|
+
return "authenticated";
|
|
211
|
+
}
|
|
212
|
+
if (FAMILY_AI_COMMANDS.has(key))
|
|
213
|
+
return "family_ai";
|
|
214
|
+
if (STAFF_COMMANDS.has(key))
|
|
215
|
+
return "staff";
|
|
216
|
+
return "admin";
|
|
217
|
+
}
|
|
218
|
+
export function commandIsAvailable(command, scope) {
|
|
219
|
+
if (command.access === "local")
|
|
220
|
+
return true;
|
|
221
|
+
if (!scope)
|
|
222
|
+
return false;
|
|
223
|
+
if (command.access === "authenticated")
|
|
224
|
+
return true;
|
|
225
|
+
if (command.access === "family_ai")
|
|
226
|
+
return scope !== "village_home";
|
|
227
|
+
if (command.access === "staff") {
|
|
228
|
+
return scope === "guide_students" || scope === "full_admin";
|
|
229
|
+
}
|
|
230
|
+
return scope === "full_admin";
|
|
231
|
+
}
|
|
53
232
|
function usageBlocks(help) {
|
|
54
233
|
const lines = help.split("\n");
|
|
55
234
|
const blocks = [];
|
|
@@ -161,6 +340,7 @@ export function buildCommandSchema(help) {
|
|
|
161
340
|
usage,
|
|
162
341
|
flags,
|
|
163
342
|
positionals,
|
|
343
|
+
access: commandAccess(path),
|
|
164
344
|
}));
|
|
165
345
|
});
|
|
166
346
|
}
|
|
@@ -172,20 +352,50 @@ export function findCommandSchema(commands, positionals) {
|
|
|
172
352
|
.filter((command) => pathStartsWith(positionals, command.path))
|
|
173
353
|
.sort((left, right) => right.path.length - left.path.length)[0];
|
|
174
354
|
}
|
|
175
|
-
export function scopedHelp(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const matches = commands.filter((command) => pathStartsWith(command.path, scope));
|
|
355
|
+
export function scopedHelp(_help, commands, scope, context) {
|
|
356
|
+
const matches = commands.filter((command) => pathStartsWith(command.path, scope) &&
|
|
357
|
+
commandIsAvailable(command, context.scope));
|
|
179
358
|
if (matches.length === 0) {
|
|
180
359
|
throw new CliError("unknown_command", `Unknown command scope: ${scope.join(" ")}. Run \`recess --help\` for available commands.`);
|
|
181
360
|
}
|
|
361
|
+
const uniqueUsages = Array.from(new Map(matches.map((command) => [command.usage, command])).values());
|
|
362
|
+
const accessLabel = context.scope
|
|
363
|
+
? context.role
|
|
364
|
+
: context.source === "unavailable"
|
|
365
|
+
? "session unavailable (local and authentication commands only)"
|
|
366
|
+
: "signed out (local and authentication commands only)";
|
|
182
367
|
return [
|
|
183
|
-
|
|
368
|
+
scope.length > 0
|
|
369
|
+
? `recess ${scope.join(" ")} — command help`
|
|
370
|
+
: "recess — safe Recess administration and family AI tools",
|
|
371
|
+
"",
|
|
372
|
+
`Access: ${accessLabel}`,
|
|
373
|
+
"Key: ◇ shared command ◆ admin-only command",
|
|
184
374
|
"",
|
|
185
375
|
"Usage:",
|
|
186
|
-
...
|
|
376
|
+
...uniqueUsages.flatMap((command) => wrapUsage(command.usage, command.access === "admin" ? "◆" : "◇")),
|
|
377
|
+
"",
|
|
378
|
+
"Run `recess help <noun> [verb]` for a focused list.",
|
|
187
379
|
].join("\n");
|
|
188
380
|
}
|
|
381
|
+
function wrapUsage(usage, icon, width = 100) {
|
|
382
|
+
const firstPrefix = ` ${icon} `;
|
|
383
|
+
const continuationPrefix = " ";
|
|
384
|
+
const output = [];
|
|
385
|
+
let current = firstPrefix;
|
|
386
|
+
for (const word of usage.split(/\s+/)) {
|
|
387
|
+
const separator = current.trim().length === 1 ? "" : " ";
|
|
388
|
+
if (current.length > firstPrefix.length &&
|
|
389
|
+
current.length + separator.length + word.length > width) {
|
|
390
|
+
output.push(current);
|
|
391
|
+
current = `${continuationPrefix}${word}`;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
current += `${separator}${word}`;
|
|
395
|
+
}
|
|
396
|
+
output.push(current);
|
|
397
|
+
return output;
|
|
398
|
+
}
|
|
189
399
|
export function validateInvocation(parsed, commands) {
|
|
190
400
|
const command = findCommandSchema(commands, parsed.positionals);
|
|
191
401
|
if (!command) {
|
|
@@ -232,14 +442,18 @@ export function agentContext(commands, options) {
|
|
|
232
442
|
return {
|
|
233
443
|
schema_version: AGENT_CONTEXT_SCHEMA_VERSION,
|
|
234
444
|
cli_version: options.cliVersion,
|
|
445
|
+
session: options.discovery,
|
|
235
446
|
commands: Object.fromEntries(commands
|
|
236
|
-
.filter((command) => command.path[0] !== "--version"
|
|
447
|
+
.filter((command) => command.path[0] !== "--version" &&
|
|
448
|
+
commandIsAvailable(command, options.discovery.scope))
|
|
237
449
|
.map((command) => [
|
|
238
450
|
command.path.join(" "),
|
|
239
451
|
{
|
|
240
452
|
usage: command.usage,
|
|
241
453
|
flags: command.flags,
|
|
242
454
|
positionals: command.positionals,
|
|
455
|
+
access: command.access,
|
|
456
|
+
admin_only: command.access === "admin",
|
|
243
457
|
},
|
|
244
458
|
])),
|
|
245
459
|
global_flags: {
|
|
@@ -48,6 +48,75 @@ function summaryPreview(text, max = 600) {
|
|
|
48
48
|
return null;
|
|
49
49
|
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* The mission-control queue rule → the CLI command(s) that perform it, with
|
|
53
|
+
* real ids substituted where the action carries them. Placeholders stay in
|
|
54
|
+
* angle brackets where a human choice remains (a template, a cohort, a
|
|
55
|
+
* transcript). Suggestions only: every write below still previews and demands
|
|
56
|
+
* its own --confirm when actually run.
|
|
57
|
+
*/
|
|
58
|
+
function suggestedCommandsForQueueAction(action, familyId) {
|
|
59
|
+
const slug = action.partnerSlug ?? "<institution-slug>";
|
|
60
|
+
const invite = action.inviteId ?? "<code-id>";
|
|
61
|
+
const kid = (action.kids.find((row) => row.activeGoalCount === 0) ?? action.kids[0])
|
|
62
|
+
?.id ?? "<kid-id>";
|
|
63
|
+
switch (action.key) {
|
|
64
|
+
case "invite_waiting":
|
|
65
|
+
return [
|
|
66
|
+
`recess school codes get ${invite} --school ${slug}`,
|
|
67
|
+
`recess school codes resend ${invite} --school ${slug}`,
|
|
68
|
+
];
|
|
69
|
+
case "invite_expired":
|
|
70
|
+
return [
|
|
71
|
+
`recess school codes replace ${invite} --school ${slug} --data-file <invite.json>`,
|
|
72
|
+
];
|
|
73
|
+
case "parked":
|
|
74
|
+
return [`recess onboarding set-account-state ${familyId} --state ACTIVE`];
|
|
75
|
+
case "start_intake":
|
|
76
|
+
return [`recess onboarding intake-session-create ${familyId}`];
|
|
77
|
+
case "fill_intake":
|
|
78
|
+
return [
|
|
79
|
+
`recess onboarding extract ${familyId} --session <session-id> --granola <ref>`,
|
|
80
|
+
`recess onboarding set-intake ${familyId} --session <session-id> --data <json>`,
|
|
81
|
+
];
|
|
82
|
+
case "send_welcome":
|
|
83
|
+
return [`recess onboarding send-welcome ${familyId}`];
|
|
84
|
+
case "nudge_confirm":
|
|
85
|
+
return [
|
|
86
|
+
`recess onboarding send-comms --subject family:${familyId} --kind confirm_nudge_1`,
|
|
87
|
+
];
|
|
88
|
+
case "load_goals":
|
|
89
|
+
return [
|
|
90
|
+
`recess goal-templates list --starter-only`,
|
|
91
|
+
`recess goal-templates apply-starter <template-id> --student ${kid}`,
|
|
92
|
+
];
|
|
93
|
+
case "assign_tutor":
|
|
94
|
+
return [
|
|
95
|
+
`recess onboarding active-tutors ${familyId}`,
|
|
96
|
+
`recess onboarding set-primary-tutor ${familyId} --tutor <user-id>`,
|
|
97
|
+
];
|
|
98
|
+
case "attest":
|
|
99
|
+
return [
|
|
100
|
+
`recess onboarding attest ${familyId} --condition <app_downloaded|tutor_met|goals_loaded|ma_diagnostic>`,
|
|
101
|
+
];
|
|
102
|
+
case "clear_for_cohort":
|
|
103
|
+
return [`recess onboarding clear-for-cohort ${familyId}`];
|
|
104
|
+
case "register_cohort":
|
|
105
|
+
return [
|
|
106
|
+
`recess onboarding cohort-options ${familyId} --kid ${kid}`,
|
|
107
|
+
`recess onboarding register-cohort ${familyId} --kid ${kid} --cohort <event-series-id>`,
|
|
108
|
+
];
|
|
109
|
+
case "at_risk":
|
|
110
|
+
return [
|
|
111
|
+
`recess onboarding readiness ${familyId}`,
|
|
112
|
+
`recess onboarding timeline ${familyId}`,
|
|
113
|
+
];
|
|
114
|
+
case "mark_complete":
|
|
115
|
+
return [`recess onboarding set-stage ${familyId} --stage COMPLETE`];
|
|
116
|
+
default:
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
51
120
|
export async function runOnboardingCommand({ parsed, api, writeCommand, }) {
|
|
52
121
|
const noun = parsed.positionals[0] ?? "";
|
|
53
122
|
const verb = parsed.positionals[1] ?? "";
|
|
@@ -97,6 +166,123 @@ export async function runOnboardingCommand({ parsed, api, writeCommand, }) {
|
|
|
97
166
|
const familyId = positional(parsed, 2, "family ID");
|
|
98
167
|
return unwrap(await api.client.GET("/admin/onboarding/families/{familyId}/active-tutors", { params: { path: { familyId } } }));
|
|
99
168
|
}
|
|
169
|
+
if (verb === "family") {
|
|
170
|
+
const familyId = positional(parsed, 2, "family ID");
|
|
171
|
+
// The whole family on one screen: status + readiness + the queue's next
|
|
172
|
+
// action + the current intake session + active tutors, in parallel.
|
|
173
|
+
// `status` is the required backbone (a missing family fails the command);
|
|
174
|
+
// the flag-gated reads degrade to a labeled `unavailable` instead of
|
|
175
|
+
// failing the view — the doctor command is the tool for "why".
|
|
176
|
+
const soft = async (read) => {
|
|
177
|
+
try {
|
|
178
|
+
return await read();
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
return {
|
|
182
|
+
unavailable: error instanceof CliError ? error.message : String(error),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const [status, readiness, nextAction, intakeSession, activeTutors] = await Promise.all([
|
|
187
|
+
api.client
|
|
188
|
+
.GET("/admin/onboarding/families/{familyId}/status", {
|
|
189
|
+
params: { path: { familyId } },
|
|
190
|
+
})
|
|
191
|
+
.then(unwrap),
|
|
192
|
+
soft(() => api.client
|
|
193
|
+
.GET("/admin/onboarding/families/{familyId}/readiness", {
|
|
194
|
+
params: { path: { familyId } },
|
|
195
|
+
})
|
|
196
|
+
.then(unwrap)),
|
|
197
|
+
soft(() => api.client
|
|
198
|
+
.GET("/admin/onboarding/families/{familyId}/next-action", {
|
|
199
|
+
params: { path: { familyId } },
|
|
200
|
+
})
|
|
201
|
+
.then(unwrap)),
|
|
202
|
+
soft(async () => {
|
|
203
|
+
try {
|
|
204
|
+
return await api.rawGet(`/admin/onboarding/families/${encodeURIComponent(familyId)}/intake-session`);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (error instanceof CliError &&
|
|
208
|
+
typeof error.details === "object" &&
|
|
209
|
+
error.details !== null &&
|
|
210
|
+
error.details.status === 404) {
|
|
211
|
+
return { intakeSession: null };
|
|
212
|
+
}
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}),
|
|
216
|
+
soft(() => api.client
|
|
217
|
+
.GET("/admin/onboarding/families/{familyId}/active-tutors", {
|
|
218
|
+
params: { path: { familyId } },
|
|
219
|
+
})
|
|
220
|
+
.then(unwrap)),
|
|
221
|
+
]);
|
|
222
|
+
return {
|
|
223
|
+
familyId,
|
|
224
|
+
status,
|
|
225
|
+
readiness,
|
|
226
|
+
nextAction,
|
|
227
|
+
intakeSession,
|
|
228
|
+
activeTutors,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (verb === "next") {
|
|
232
|
+
const familyId = positional(parsed, 2, "family ID");
|
|
233
|
+
const result = unwrap(await api.client.GET("/admin/onboarding/families/{familyId}/next-action", { params: { path: { familyId } } }));
|
|
234
|
+
const action = result.action;
|
|
235
|
+
return {
|
|
236
|
+
...result,
|
|
237
|
+
// The queue rule mapped to the command that performs it, with real ids
|
|
238
|
+
// substituted where the action carries them — so "what's next" arrives
|
|
239
|
+
// holding "and here is how", instead of making the agent re-derive the
|
|
240
|
+
// funnel from documentation. Every suggested write still previews and
|
|
241
|
+
// requires its own --confirm.
|
|
242
|
+
suggestedCommands: action === null
|
|
243
|
+
? []
|
|
244
|
+
: suggestedCommandsForQueueAction(action, familyId),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (verb === "doctor") {
|
|
248
|
+
// --family is a flag rather than an optional positional: the schema
|
|
249
|
+
// derives positional bounds from the usage line, which cannot express
|
|
250
|
+
// optionality.
|
|
251
|
+
const familyId = flagString(parsed, "family");
|
|
252
|
+
return unwrap(await api.client.GET("/admin/onboarding/doctor", {
|
|
253
|
+
params: { query: familyId ? { familyId } : {} },
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
if (verb === "starter-coverage") {
|
|
257
|
+
// The read-only pre-flip gate (same evaluation as the runbook's
|
|
258
|
+
// check-starter-coverage script). `ok: false` = do not flip the flag.
|
|
259
|
+
return unwrap(await api.client.GET("/admin/onboarding/starter-coverage"));
|
|
260
|
+
}
|
|
261
|
+
if (verb === "backfill-trackers") {
|
|
262
|
+
// WS-H over the wire. Without --apply this is the read-only census
|
|
263
|
+
// (dry-run POST, same precedent as `goal-templates apply --dry-run`).
|
|
264
|
+
// With --apply, the census is re-taken as the preview — the runbook's
|
|
265
|
+
// "fresh dry run on the identical predicate immediately before
|
|
266
|
+
// applying" — and the confirmed write re-derives every conjunct under a
|
|
267
|
+
// per-guardian lock server-side anyway.
|
|
268
|
+
if (!hasFlag(parsed, "apply")) {
|
|
269
|
+
return unwrap(await api.client.POST("/admin/onboarding/backfill-guardian-trackers", {
|
|
270
|
+
body: { apply: false },
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
273
|
+
const census = unwrap(await api.client.POST("/admin/onboarding/backfill-guardian-trackers", {
|
|
274
|
+
body: { apply: false },
|
|
275
|
+
}));
|
|
276
|
+
return writeCommand(parsed, {
|
|
277
|
+
action: "apply the WS-H guardian-tracker backfill: stamp parentOnboardingCompletedAt for the arm-1 (crash-between-commits) school guardians below",
|
|
278
|
+
target: { repairableCount: census.repairable.length },
|
|
279
|
+
request: { apply: true },
|
|
280
|
+
details: {
|
|
281
|
+
freshCensus: census,
|
|
282
|
+
authority: "The census is a photograph; the server re-derives every conjunct under a per-guardian lock at write time and skips anyone who no longer qualifies. Exact-ADMIN only.",
|
|
283
|
+
},
|
|
284
|
+
}, async () => unwrap(await api.client.POST("/admin/onboarding/backfill-guardian-trackers", { body: { apply: true } })));
|
|
285
|
+
}
|
|
100
286
|
if (verb === "meetings") {
|
|
101
287
|
const familyId = positional(parsed, 2, "family ID");
|
|
102
288
|
return unwrap(await api.client.GET("/admin/onboarding/families/{familyId}/meetings", {
|