@stage5/lumine 0.3.23 → 0.3.25
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 +11 -0
- package/lib/admin-daily.js +2 -0
- package/lib/admin-identity-inspection.js +66 -0
- package/lib/admin.js +69 -0
- package/lib/commands.js +67 -6
- package/lib/math-lab-build-sheet.js +133 -0
- package/lib/math-lab-preservation.js +105 -0
- package/lib/workspace.js +2 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +4 -4
- package/sdk/LUMINE_ADMIN.md +101 -1
package/README.md
CHANGED
|
@@ -81,6 +81,17 @@ pulling the owner's main project creates or reuses your contribution branch and
|
|
|
81
81
|
checks out that branch locally. Saves go to your branch, so the project owner
|
|
82
82
|
can merge or replace main from Twinkle.
|
|
83
83
|
|
|
84
|
+
For an exact source review of the live app, use `lumine pull <build-id>
|
|
85
|
+
--published` in a persistent review folder. This checks out the requested
|
|
86
|
+
build's published artifact read-only; `--main` explicitly selects its root
|
|
87
|
+
project. It never substitutes the latest draft or creates a contribution
|
|
88
|
+
branch. The CLI checks the publication pointer again before writing and
|
|
89
|
+
refuses a concurrent publication; modified local files are stashed before a
|
|
90
|
+
successful checkout. It requires the API's published-version selector to be
|
|
91
|
+
deployed first. `--published` and `--version <n>` are mutually exclusive.
|
|
92
|
+
`lumine admin builds review` already reviews the published app; unsupported
|
|
93
|
+
selectors such as `--mode` or `--review-mode` fail instead of being ignored.
|
|
94
|
+
|
|
84
95
|
Use `lumine branches <build-url-or-id>` to list the contribution branches you
|
|
85
96
|
can review, including each contributor, branch number, status, and URL. Then use
|
|
86
97
|
`lumine diff <branch-url>` to inspect one branch.
|
package/lib/admin-daily.js
CHANGED
|
@@ -73,6 +73,7 @@ export function runAdmin(
|
|
|
73
73
|
"reward-review": ["list"],
|
|
74
74
|
approvals: ["list"],
|
|
75
75
|
todo: ["list"],
|
|
76
|
+
followup: ["list"],
|
|
76
77
|
escalation: ["list"],
|
|
77
78
|
featured: ["list", "history"],
|
|
78
79
|
sponsor: ["applications"],
|
|
@@ -192,6 +193,7 @@ export async function dailyRunGather(options) {
|
|
|
192
193
|
// flags: a required report section (agent-guides/lumine-admin.md)
|
|
193
194
|
step("approvals-pending", ["approvals", "list", "--status", "pending"], "approvals-pending.json");
|
|
194
195
|
step("todos", ["todo", "list"], "todos.json");
|
|
196
|
+
step("personal-followups", ["followup", "list"], "personal-followups.json");
|
|
195
197
|
step("escalations", ["escalation", "list"], "escalations.json");
|
|
196
198
|
step("featured-list", ["featured", "list"], "featured-start.json");
|
|
197
199
|
step(
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
function seenAt(value) {
|
|
2
|
+
if (value == null || !Number.isFinite(Number(value)) || Number(value) <= 0)
|
|
3
|
+
return "unknown";
|
|
4
|
+
const date = new Date(Number(value) * 1000);
|
|
5
|
+
return Number.isNaN(date.getTime()) ? "unknown" : date.toISOString();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function eventTypes(counts) {
|
|
9
|
+
const entries = Object.entries(counts || {}).sort(([a], [b]) =>
|
|
10
|
+
a.localeCompare(b),
|
|
11
|
+
);
|
|
12
|
+
return entries.length
|
|
13
|
+
? `; ${entries.map(([type, count]) => `${type}: ${count}`).join(", ")}`
|
|
14
|
+
: "";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Display observed device events separately from accounts and physical visits. */
|
|
18
|
+
export function formatIdentityDeviceUsage(inspection) {
|
|
19
|
+
const devices = inspection.sharedDevices || [];
|
|
20
|
+
const coverage = inspection.evidenceCoverage || {};
|
|
21
|
+
const lines = [];
|
|
22
|
+
if (devices.length) {
|
|
23
|
+
lines.push(
|
|
24
|
+
`Device history (${coverage.deviceLookbackDays || "unknown"} days): recorded account events, not visits or unique sessions.`,
|
|
25
|
+
);
|
|
26
|
+
for (const device of devices) {
|
|
27
|
+
const usage = device.usage;
|
|
28
|
+
if (!usage) {
|
|
29
|
+
lines.push(
|
|
30
|
+
` Device ${device.key}: usage counts unavailable from this API; ${device.accountCount ?? "unknown"} observed account(s).`,
|
|
31
|
+
);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (usage.status === "not_read" || usage.eventCount == null) {
|
|
35
|
+
lines.push(
|
|
36
|
+
` Device ${device.key}: usage and sharing not read (${usage.truncatedBy?.join(", ") || "incomplete evidence"}).`,
|
|
37
|
+
);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const partial = usage.status !== "complete";
|
|
41
|
+
const prefix = partial ? "at least " : "";
|
|
42
|
+
lines.push(
|
|
43
|
+
` Device ${device.key}: ${prefix}${usage.eventCount} recorded event(s), ${prefix}${device.accountCount} account(s)${eventTypes(usage.eventsByType)}.`,
|
|
44
|
+
` First observed ${seenAt(usage.firstSeenAt)}; last observed ${seenAt(usage.lastSeenAt)}.`,
|
|
45
|
+
);
|
|
46
|
+
if (partial) {
|
|
47
|
+
lines.push(
|
|
48
|
+
` Partial history (${usage.truncatedBy?.join(", ") || "incomplete evidence"}); older events and other accounts may be missing.`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
for (const account of usage.accounts || []) {
|
|
52
|
+
lines.push(
|
|
53
|
+
` #${account.userId} ${account.username || "(unknown account)"}: ${prefix}${account.eventCount} recorded event(s)${eventTypes(account.eventsByType)}; first ${seenAt(account.firstSeenAt)}, last ${seenAt(account.lastSeenAt)}.`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (usage.accountsTruncated)
|
|
57
|
+
lines.push(" Account detail list capped; totals include omitted accounts.");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (coverage.truncated) {
|
|
61
|
+
lines.push(
|
|
62
|
+
`Device discovery/evidence is incomplete (${coverage.truncatedBy?.join(", ") || "history limit"}); additional devices or accounts may exist.`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return lines;
|
|
66
|
+
}
|
package/lib/admin.js
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
printOwnerTrace,
|
|
40
40
|
} from "./admin-owner-trace.js";
|
|
41
41
|
import { formatChatReportResult } from "./admin-chat-reports.js";
|
|
42
|
+
import { formatIdentityDeviceUsage } from "./admin-identity-inspection.js";
|
|
42
43
|
import {
|
|
43
44
|
buildTeacherBranchOperation,
|
|
44
45
|
printTeacherBranchResult,
|
|
@@ -2478,6 +2479,21 @@ export function parseAdminOperation(options) {
|
|
|
2478
2479
|
},
|
|
2479
2480
|
);
|
|
2480
2481
|
}
|
|
2482
|
+
if (action === "device" && target === "block-signup") {
|
|
2483
|
+
if (!/^[a-f0-9]{16}$/.test(options.adminDeviceKey || "")) {
|
|
2484
|
+
throw cliValidationError("Pass --device-key with the exact key from identity inspect.");
|
|
2485
|
+
}
|
|
2486
|
+
return writeOperation(
|
|
2487
|
+
"ai-bucket.device.signup.block", "POST",
|
|
2488
|
+
`/cli/admin/ai-buckets/${bucketId}/signup-devices`,
|
|
2489
|
+
{
|
|
2490
|
+
sourceUserId: parseRequiredInteger(options.adminUser, "--user", 1),
|
|
2491
|
+
deviceKey: options.adminDeviceKey,
|
|
2492
|
+
expectedUserIds: parseAiBucketUserIds(options.adminUserIds),
|
|
2493
|
+
note: parseAiBucketNote(options.note),
|
|
2494
|
+
},
|
|
2495
|
+
);
|
|
2496
|
+
}
|
|
2481
2497
|
if (action === "note" && target === "set") {
|
|
2482
2498
|
return writeOperation(
|
|
2483
2499
|
"ai-bucket.note.set",
|
|
@@ -3174,6 +3190,25 @@ export function parseAdminOperation(options) {
|
|
|
3174
3190
|
);
|
|
3175
3191
|
}
|
|
3176
3192
|
|
|
3193
|
+
if (namespace === "followup" || namespace === "followups") {
|
|
3194
|
+
if (!action || action === "list") {
|
|
3195
|
+
return readOperation("followup.list", withQuery("/cli/admin/personal-followups", {
|
|
3196
|
+
cursor: options.cursor || undefined,
|
|
3197
|
+
status: options.adminStatus === "all" ? "all" : "pending",
|
|
3198
|
+
}));
|
|
3199
|
+
}
|
|
3200
|
+
if (action === "update") {
|
|
3201
|
+
const id = parseRequiredInteger(target, "Follow-up ID", 1);
|
|
3202
|
+
const revision = parseRequiredInteger(options.followupRevision, "--revision", 1);
|
|
3203
|
+
const note = String(options.note || "").trim();
|
|
3204
|
+
if (!note || note.length > MAX_TODO_NOTE_LENGTH) throw cliValidationError("Record the actual work, evidence and reply outcome (or blocker) in --note, up to 4000 characters.");
|
|
3205
|
+
return writeOperation("followup.update", "PUT", `/cli/admin/personal-followups/${id}`, {
|
|
3206
|
+
revision, status: parseTodoStatus(options.adminStatus), note,
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3209
|
+
throw cliValidationError("Usage: lumine admin followup list [--cursor <id>] [--status all] | followup update <id> --revision <n> --status <status> --note <evidence and outcome>.");
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3177
3212
|
if (namespace === "todo" || namespace === "todos") {
|
|
3178
3213
|
if (!action || action === "list") {
|
|
3179
3214
|
return readOperation(
|
|
@@ -3229,6 +3264,16 @@ export function parseAdminOperation(options) {
|
|
|
3229
3264
|
{
|
|
3230
3265
|
status: parseTodoStatus(options.adminStatus),
|
|
3231
3266
|
note,
|
|
3267
|
+
...(options.todoWorkflowFile
|
|
3268
|
+
? {
|
|
3269
|
+
workflow: readAdminJsonFile(options.todoWorkflowFile),
|
|
3270
|
+
expectedRevision: parseRequiredInteger(
|
|
3271
|
+
options.followupRevision,
|
|
3272
|
+
"--revision",
|
|
3273
|
+
1,
|
|
3274
|
+
),
|
|
3275
|
+
}
|
|
3276
|
+
: {}),
|
|
3232
3277
|
},
|
|
3233
3278
|
);
|
|
3234
3279
|
}
|
|
@@ -6814,6 +6859,7 @@ function printAdminResult({ operation, result }) {
|
|
|
6814
6859
|
);
|
|
6815
6860
|
} else {
|
|
6816
6861
|
printTodoItems(report.carryoverTodos?.items || [], "Unfinished work");
|
|
6862
|
+
printPersonalFollowups(report.personalFollowups);
|
|
6817
6863
|
}
|
|
6818
6864
|
if (report.sponsorIntegrity) {
|
|
6819
6865
|
const pendingApplications = Number.isSafeInteger(
|
|
@@ -6870,6 +6916,7 @@ function printAdminResult({ operation, result }) {
|
|
|
6870
6916
|
);
|
|
6871
6917
|
}
|
|
6872
6918
|
}
|
|
6919
|
+
for (const line of formatIdentityDeviceUsage(inspection)) console.log(line);
|
|
6873
6920
|
return;
|
|
6874
6921
|
}
|
|
6875
6922
|
if (Array.isArray(data.escalations)) {
|
|
@@ -6912,6 +6959,8 @@ function printAdminResult({ operation, result }) {
|
|
|
6912
6959
|
}
|
|
6913
6960
|
return;
|
|
6914
6961
|
}
|
|
6962
|
+
if (data.personalFollowups) printPersonalFollowups(data.personalFollowups);
|
|
6963
|
+
if (data.personalFollowup) printPersonalFollowups({ items: [data.personalFollowup], nextCursor: null });
|
|
6915
6964
|
if (Array.isArray(data.todos)) {
|
|
6916
6965
|
printTodoItems(data.todos, "Private carry-over work");
|
|
6917
6966
|
if (data.truncated) {
|
|
@@ -7191,6 +7240,18 @@ function printFeaturedHistoryEvent(event) {
|
|
|
7191
7240
|
);
|
|
7192
7241
|
}
|
|
7193
7242
|
|
|
7243
|
+
function printPersonalFollowups(handoff) {
|
|
7244
|
+
if (!handoff) return;
|
|
7245
|
+
console.log(`Personal follow-ups: ${handoff.items.length}${handoff.nextCursor ? "+" : ""}`);
|
|
7246
|
+
for (const item of handoff.items) {
|
|
7247
|
+
console.log(` #${item.id} r${item.revision} ${item.status} — ${item.title}`);
|
|
7248
|
+
console.log(` ${item.details}`);
|
|
7249
|
+
console.log(` Source: ${item.source.type} ${item.source.id}, ${item.source.rootType} ${item.source.rootId}; member ${item.requesterUserId}`);
|
|
7250
|
+
if (item.lastProgressNote) console.log(` Progress: ${item.lastProgressNote}`);
|
|
7251
|
+
}
|
|
7252
|
+
if (handoff.nextCursor) console.log(` Continue: lumine admin followup list --cursor ${handoff.nextCursor}`);
|
|
7253
|
+
}
|
|
7254
|
+
|
|
7194
7255
|
function printTodoItems(items, heading) {
|
|
7195
7256
|
console.log(`${heading}: ${items.length} item(s).`);
|
|
7196
7257
|
for (const todo of items) {
|
|
@@ -7198,6 +7259,14 @@ function printTodoItems(items, heading) {
|
|
|
7198
7259
|
` #${todo.id} ${String(todo.status || "open").toUpperCase()} ${todo.kind || "task"} — ${todo.title || "(untitled)"}`,
|
|
7199
7260
|
);
|
|
7200
7261
|
if (todo.details) console.log(` ${todo.details}`);
|
|
7262
|
+
if (todo.workflow) {
|
|
7263
|
+
const w = todo.workflow;
|
|
7264
|
+
console.log(` Implementation: ${w.implementation}; release: ${w.release}; verification: ${w.verification}`);
|
|
7265
|
+
if (w.fixRef) console.log(` Fix: ${w.fixRef}`);
|
|
7266
|
+
if (w.releaseRef) console.log(` Release: ${w.releaseRef}`);
|
|
7267
|
+
if (w.evidence) console.log(` Evidence: ${w.evidence}`);
|
|
7268
|
+
if (w.nextAction) console.log(` Next: ${w.nextAction}`);
|
|
7269
|
+
}
|
|
7201
7270
|
if (todo.lastProgressNote) {
|
|
7202
7271
|
console.log(` Latest progress: ${todo.lastProgressNote}`);
|
|
7203
7272
|
}
|
package/lib/commands.js
CHANGED
|
@@ -943,6 +943,8 @@ export async function selectProject(options) {
|
|
|
943
943
|
}
|
|
944
944
|
|
|
945
945
|
export async function pull(options) {
|
|
946
|
+
if (options.pullPublished && options.versionProvided)
|
|
947
|
+
throw new Error("Choose --published or --version, not both.");
|
|
946
948
|
if (options.versionProvided && !(options.pullVersion > 0)) {
|
|
947
949
|
throw new Error(
|
|
948
950
|
"Pass a save number: `lumine pull --version <n>` (run `lumine versions` to see save numbers).",
|
|
@@ -962,7 +964,7 @@ export async function pull(options) {
|
|
|
962
964
|
auth,
|
|
963
965
|
buildId: requestedBuildId,
|
|
964
966
|
});
|
|
965
|
-
if (options.pullVersion > 0) {
|
|
967
|
+
if (options.pullPublished || options.pullVersion > 0) {
|
|
966
968
|
await pullVersionBuildFiles({ options, auth, build: selectedBuild });
|
|
967
969
|
return;
|
|
968
970
|
}
|
|
@@ -1830,10 +1832,10 @@ export async function pullMainBuildFiles({ options, auth, build }) {
|
|
|
1830
1832
|
}
|
|
1831
1833
|
|
|
1832
1834
|
// Read-only checkout of ONE previous save (`lumine pull --version <n>`).
|
|
1833
|
-
//
|
|
1834
|
-
//
|
|
1835
|
+
// Numeric saves target the editable workspace branch unless --main is given.
|
|
1836
|
+
// --published reads the named Build directly; --main selects the project root.
|
|
1835
1837
|
export async function pullVersionBuildFiles({ options, auth, build }) {
|
|
1836
|
-
const versionNumber = options.pullVersion;
|
|
1838
|
+
const versionNumber = options.pullPublished ? "published" : options.pullVersion;
|
|
1837
1839
|
let targetBuild = build;
|
|
1838
1840
|
if (options.pullMain) {
|
|
1839
1841
|
const rootBuildId =
|
|
@@ -1842,7 +1844,7 @@ export async function pullVersionBuildFiles({ options, auth, build }) {
|
|
|
1842
1844
|
rootBuildId === Number(build?.id || 0)
|
|
1843
1845
|
? build
|
|
1844
1846
|
: await loadBuildMetadata({ options, auth, buildId: rootBuildId });
|
|
1845
|
-
} else {
|
|
1847
|
+
} else if (!options.pullPublished) {
|
|
1846
1848
|
targetBuild = await resolveEditableWorkspaceBuild({
|
|
1847
1849
|
options,
|
|
1848
1850
|
auth,
|
|
@@ -1856,6 +1858,27 @@ export async function pullVersionBuildFiles({ options, auth, build }) {
|
|
|
1856
1858
|
buildId,
|
|
1857
1859
|
versionNumber,
|
|
1858
1860
|
});
|
|
1861
|
+
if (options.pullPublished) {
|
|
1862
|
+
const expected = Number(result.build?.publishedArtifactVersionId || 0);
|
|
1863
|
+
if (
|
|
1864
|
+
!expected ||
|
|
1865
|
+
Number(result.version?.versionId) !== expected ||
|
|
1866
|
+
!result.build?.isPublic
|
|
1867
|
+
) {
|
|
1868
|
+
throw new Error(
|
|
1869
|
+
"The API did not return the exact published artifact; update the API before using --published.",
|
|
1870
|
+
);
|
|
1871
|
+
}
|
|
1872
|
+
const current = await loadBuildMetadata({ options, auth, buildId });
|
|
1873
|
+
if (
|
|
1874
|
+
!current.isPublic ||
|
|
1875
|
+
Number(current.publishedArtifactVersionId) !== expected
|
|
1876
|
+
) {
|
|
1877
|
+
throw new Error(
|
|
1878
|
+
"The published version changed during checkout. Retry; no local files were changed.",
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1859
1882
|
const resolvedBuild = result.build || targetBuild;
|
|
1860
1883
|
const version = result.version || { version: versionNumber };
|
|
1861
1884
|
const files = Array.isArray(result.projectFiles) ? result.projectFiles : [];
|
|
@@ -1879,6 +1902,7 @@ export async function pullVersionBuildFiles({ options, auth, build }) {
|
|
|
1879
1902
|
? path.join(path.dirname(enclosing.rootDir), dirName)
|
|
1880
1903
|
: dirName),
|
|
1881
1904
|
);
|
|
1905
|
+
await stashLocalProjectFilesBeforePull({ dir, files });
|
|
1882
1906
|
await writeProjectFiles({ dir, files });
|
|
1883
1907
|
if (files.some((file) => isIndexHtmlPath(String(file.path)))) {
|
|
1884
1908
|
await removeLocalProjectFilesNotIn({ dir, files });
|
|
@@ -2422,6 +2446,7 @@ export function parseArgs(args) {
|
|
|
2422
2446
|
"open",
|
|
2423
2447
|
"noDescription",
|
|
2424
2448
|
"publish",
|
|
2449
|
+
"published",
|
|
2425
2450
|
"save",
|
|
2426
2451
|
"force",
|
|
2427
2452
|
"noUpdateCheck",
|
|
@@ -2491,6 +2516,35 @@ export function parseArgs(args) {
|
|
|
2491
2516
|
}
|
|
2492
2517
|
}
|
|
2493
2518
|
|
|
2519
|
+
if (
|
|
2520
|
+
command === "admin" &&
|
|
2521
|
+
positional[0] === "builds" &&
|
|
2522
|
+
positional[1] === "review"
|
|
2523
|
+
) {
|
|
2524
|
+
const allowed = new Set([
|
|
2525
|
+
"help",
|
|
2526
|
+
"output",
|
|
2527
|
+
"outputDir",
|
|
2528
|
+
"dir",
|
|
2529
|
+
"waitMs",
|
|
2530
|
+
"interact",
|
|
2531
|
+
"browserPath",
|
|
2532
|
+
"json",
|
|
2533
|
+
"apiUrl",
|
|
2534
|
+
"siteUrl",
|
|
2535
|
+
"authFile",
|
|
2536
|
+
"authToken",
|
|
2537
|
+
"clientName",
|
|
2538
|
+
"timeoutMs",
|
|
2539
|
+
"noUpdateCheck",
|
|
2540
|
+
]);
|
|
2541
|
+
for (const key of Object.keys(raw)) {
|
|
2542
|
+
if (!allowed.has(key))
|
|
2543
|
+
throw new Error(
|
|
2544
|
+
`Unsupported Build review option --${key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}. Runtime review already uses the published app; use pull --published for its source.`,
|
|
2545
|
+
);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2494
2548
|
const suggestionInboxActions = new Set([
|
|
2495
2549
|
"list",
|
|
2496
2550
|
"merge",
|
|
@@ -2592,6 +2646,8 @@ export function parseArgs(args) {
|
|
|
2592
2646
|
adminReviewed: Boolean(raw.reviewed),
|
|
2593
2647
|
adminSeverity: raw.severity ? String(raw.severity) : "",
|
|
2594
2648
|
adminStatus: raw.status ? String(raw.status) : "",
|
|
2649
|
+
followupRevision: raw.revision ? String(raw.revision) : "",
|
|
2650
|
+
todoWorkflowFile: raw.workflow ? String(raw.workflow) : "",
|
|
2595
2651
|
adminDecision: raw.decision ? String(raw.decision) : "",
|
|
2596
2652
|
adminWaitMs: raw.waitMs ? String(raw.waitMs) : "",
|
|
2597
2653
|
adminBrowserPath: raw.browserPath ? String(raw.browserPath) : "",
|
|
@@ -2673,6 +2729,7 @@ export function parseArgs(args) {
|
|
|
2673
2729
|
? String(raw.addIds)
|
|
2674
2730
|
: "",
|
|
2675
2731
|
adminBucketId: raw.bucketId ? String(raw.bucketId) : "",
|
|
2732
|
+
adminDeviceKey: raw.deviceKey ? String(raw.deviceKey) : "",
|
|
2676
2733
|
adminLabel: raw.label ? String(raw.label) : "",
|
|
2677
2734
|
adminUserIds: raw.userIds ? String(raw.userIds) : "",
|
|
2678
2735
|
adminUser: raw.user ? String(raw.user) : "",
|
|
@@ -2806,6 +2863,7 @@ export function parseArgs(args) {
|
|
|
2806
2863
|
clientName: String(raw.clientName || "Lumine CLI").slice(0, 120),
|
|
2807
2864
|
dir: raw.dir ? String(raw.dir) : "",
|
|
2808
2865
|
pullMain: parseBoolean(raw.main, false),
|
|
2866
|
+
pullPublished: parseBoolean(raw.published, false),
|
|
2809
2867
|
// Strict: only a plain positive integer counts. Anything else (missing
|
|
2810
2868
|
// value, "foo", a swallowed flag like "--main", "2.5") stays 0 and the
|
|
2811
2869
|
// consuming command must reject it via versionProvided instead of
|
|
@@ -3065,6 +3123,7 @@ export function printHelp() {
|
|
|
3065
3123
|
lumine pull [twinkle-build-url]
|
|
3066
3124
|
lumine pull [twinkle-build-url] --main
|
|
3067
3125
|
lumine pull [twinkle-build-url] --version <n> [--main]
|
|
3126
|
+
lumine pull [twinkle-build-url] --published [--main]
|
|
3068
3127
|
lumine versions [twinkle-build-url] [--main] [--limit <n>]
|
|
3069
3128
|
lumine restore <n> [--main]
|
|
3070
3129
|
lumine reference <twinkle-build-url>
|
|
@@ -3104,6 +3163,7 @@ export function printHelp() {
|
|
|
3104
3163
|
lumine admin ai-bucket create --label <name> --note <text> [--json]
|
|
3105
3164
|
lumine admin ai-bucket get --bucket-id <id> [--json]
|
|
3106
3165
|
lumine admin ai-bucket accounts add --bucket-id <id> --user-ids <id,id,...> [--note <text>] [--json]
|
|
3166
|
+
lumine admin ai-bucket device block-signup --bucket-id <id> --user <source-user-id> --device-key <inspection-key> --user-ids <reviewed-device-users> --note <reason> [--json]
|
|
3107
3167
|
lumine admin ai-bucket note set --bucket-id <id> --note <text> [--json]
|
|
3108
3168
|
lumine admin ai-email-policy get --email <address> [--json]
|
|
3109
3169
|
lumine admin ai-email-policy set --email <address> --mode <automatic|separate_accounts> --note <text> [--json]
|
|
@@ -3114,7 +3174,7 @@ export function printHelp() {
|
|
|
3114
3174
|
lumine admin escalation set <audit-id> --status open|acknowledged|resolved --note <decision> [--json]
|
|
3115
3175
|
lumine admin todo list [--status pending|open|in_progress|blocked|completed|cancelled|all] [--limit <number>] [--json]
|
|
3116
3176
|
lumine admin todo add --title <title> --note <handoff-and-acceptance-criteria> [--kind task|experiment] [--status open|in_progress|blocked] [--json]
|
|
3117
|
-
lumine admin todo update <todo-id> --status open|in_progress|blocked|completed|cancelled --note <progress-or-evidence> [--json]
|
|
3177
|
+
lumine admin todo update <todo-id> --status open|in_progress|blocked|completed|cancelled --note <progress-or-evidence> [--workflow <file.json> --revision <n>] [--json]
|
|
3118
3178
|
lumine admin sponsor applications list [--status pending|approved|rejected|withdrawn] [--json]
|
|
3119
3179
|
lumine admin sponsor applications review <application-id> --decision approve|reject [--note <text>] [--json]
|
|
3120
3180
|
lumine admin sponsor status set <user-id> --status probationary|trusted|suspended|revoked [--note <text>] [--json]
|
|
@@ -3277,6 +3337,7 @@ Options:
|
|
|
3277
3337
|
--no-review-loop Skip the evidence-based post-run loop retrospective
|
|
3278
3338
|
--target <build> Explicit Build URL or ID for rename/describe/upgrade
|
|
3279
3339
|
--main With pull/versions/restore: target the team project's main
|
|
3340
|
+
--published With pull: exact currently published artifact, read-only
|
|
3280
3341
|
--version <n> With pull: read-only checkout of previous save v<n>
|
|
3281
3342
|
--title <text> Build title for new/rename, or private todo title
|
|
3282
3343
|
--description <text> Build description for new/describe
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Merges parts/<level>.json into the private question sheet the Math Lab
|
|
2
|
+
// uploads with `lumine rewards sheet`, then validates the sheet against the
|
|
3
|
+
// workspace's rewards.json exactly the way the server will (same code).
|
|
4
|
+
// Usage: node --import tsx build-sheet.mjs --approved-review <live review id>
|
|
5
|
+
// Reads that review afresh and preserves its questions before writing output.
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
|
+
import {
|
|
12
|
+
LEVELS,
|
|
13
|
+
verifyApprovedPrefix,
|
|
14
|
+
verifyRewardEconomy,
|
|
15
|
+
} from "./math-lab-preservation.js";
|
|
16
|
+
|
|
17
|
+
export async function buildMathLabSheet({
|
|
18
|
+
contentDir,
|
|
19
|
+
args = process.argv.slice(2),
|
|
20
|
+
}) {
|
|
21
|
+
const here = path.resolve(contentDir);
|
|
22
|
+
const parts = path.join(here, "parts");
|
|
23
|
+
if (
|
|
24
|
+
args.length !== 2 ||
|
|
25
|
+
args[0] !== "--approved-review" ||
|
|
26
|
+
!/^[1-9]\d*$/.test(args[1])
|
|
27
|
+
) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"Usage: node --import tsx build-sheet.mjs --approved-review <current live review id>. No sheet was written.",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const cli = fileURLToPath(new URL("../bin/lumine.js", import.meta.url));
|
|
33
|
+
const approved = JSON.parse(
|
|
34
|
+
execFileSync(
|
|
35
|
+
process.execPath,
|
|
36
|
+
[
|
|
37
|
+
cli,
|
|
38
|
+
"admin",
|
|
39
|
+
"reward-review",
|
|
40
|
+
"show",
|
|
41
|
+
args[1],
|
|
42
|
+
"--json",
|
|
43
|
+
"--no-update-check",
|
|
44
|
+
],
|
|
45
|
+
{
|
|
46
|
+
encoding: "utf8",
|
|
47
|
+
timeout: 60000,
|
|
48
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
49
|
+
},
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
const sheet = { rules: {} };
|
|
53
|
+
const missing = [];
|
|
54
|
+
for (const level of LEVELS) {
|
|
55
|
+
const file = path.join(parts, `${level}.json`);
|
|
56
|
+
if (!fs.existsSync(file)) {
|
|
57
|
+
missing.push(level);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const data = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
61
|
+
sheet.rules[`${level}-daily`] = {
|
|
62
|
+
sets: data.sets.map((set) => ({
|
|
63
|
+
key: set.key,
|
|
64
|
+
questions: set.questions.map((q) => ({
|
|
65
|
+
prompt: q.prompt,
|
|
66
|
+
answer: q.answer,
|
|
67
|
+
...(q.hint ? { hint: q.hint } : {}),
|
|
68
|
+
...(q.guide ? { guide: q.guide } : {}),
|
|
69
|
+
})),
|
|
70
|
+
})),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const out = path.resolve(here, "..", "math-lab-sheet.json");
|
|
74
|
+
if (missing.length)
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Missing grade parts: ${missing.join(", ")}. No sheet was written.`,
|
|
77
|
+
);
|
|
78
|
+
const preservation = verifyApprovedPrefix(sheet, approved);
|
|
79
|
+
|
|
80
|
+
// Server-identical validation: rewards.json + sheet → approved-shape config.
|
|
81
|
+
const policy = await import(
|
|
82
|
+
pathToFileURL(
|
|
83
|
+
path.resolve(
|
|
84
|
+
here,
|
|
85
|
+
"..",
|
|
86
|
+
"..",
|
|
87
|
+
"twinkle-api",
|
|
88
|
+
"helpers",
|
|
89
|
+
"build",
|
|
90
|
+
"rewardsPolicy.ts",
|
|
91
|
+
),
|
|
92
|
+
).href
|
|
93
|
+
);
|
|
94
|
+
const declared = JSON.parse(
|
|
95
|
+
fs.readFileSync(
|
|
96
|
+
path.resolve(here, "..", "math-lab", "rewards.json"),
|
|
97
|
+
"utf8",
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
const composed = policy.composeRewardConfig({ declared, sheet });
|
|
101
|
+
if (composed.errors.length) {
|
|
102
|
+
for (const error of composed.errors) console.log(`- ${error}`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
verifyRewardEconomy(composed.config, approved);
|
|
106
|
+
const total = composed.config.rules.reduce(
|
|
107
|
+
(sum, rule) => sum + (rule.sets || []).length,
|
|
108
|
+
0,
|
|
109
|
+
);
|
|
110
|
+
// Nothing replaces the prior usable sheet until every check succeeds.
|
|
111
|
+
const output = JSON.stringify(sheet, null, 1) + "\n";
|
|
112
|
+
fs.writeFileSync(`${out}.next`, output, { mode: 0o600 });
|
|
113
|
+
fs.renameSync(`${out}.next`, out);
|
|
114
|
+
fs.writeFileSync(
|
|
115
|
+
`${out}.preservation.json`,
|
|
116
|
+
JSON.stringify(
|
|
117
|
+
{
|
|
118
|
+
...preservation,
|
|
119
|
+
checkedAt: new Date().toISOString(),
|
|
120
|
+
sha256: createHash("sha256").update(output).digest("hex"),
|
|
121
|
+
},
|
|
122
|
+
null,
|
|
123
|
+
2,
|
|
124
|
+
) + "\n",
|
|
125
|
+
{ mode: 0o600 },
|
|
126
|
+
);
|
|
127
|
+
console.log(
|
|
128
|
+
`wrote ${out}; preserved ${preservation.rules.reduce((n, r) => n + r.preserved, 0)} published questions from review ${preservation.liveReview}`,
|
|
129
|
+
);
|
|
130
|
+
console.log(
|
|
131
|
+
`composed OK: ${composed.config.rules.length} rules, ${total} sets, ${Buffer.byteLength(JSON.stringify(sheet))} bytes`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
|
|
3
|
+
export const LEVELS = [
|
|
4
|
+
"e1",
|
|
5
|
+
"e2",
|
|
6
|
+
"e3",
|
|
7
|
+
"e4",
|
|
8
|
+
"e5",
|
|
9
|
+
"e6",
|
|
10
|
+
"m1",
|
|
11
|
+
"m2",
|
|
12
|
+
"m3",
|
|
13
|
+
"h1",
|
|
14
|
+
"h2",
|
|
15
|
+
"h3",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function verifyRewardEconomy(config, response) {
|
|
19
|
+
const withoutRefillSets = (value) => ({
|
|
20
|
+
...value,
|
|
21
|
+
rules: value.rules.map((rule) => {
|
|
22
|
+
if (!LEVELS.some((level) => rule.id === `${level}-daily`)) return rule;
|
|
23
|
+
const { sets: _sets, ...economy } = rule;
|
|
24
|
+
return economy;
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
if (
|
|
28
|
+
!isDeepStrictEqual(
|
|
29
|
+
withoutRefillSets(config),
|
|
30
|
+
withoutRefillSets(response.data.review.config),
|
|
31
|
+
)
|
|
32
|
+
) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"A refill must preserve the approved reward rules and budgets. Review any economy change separately.",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Refill-only: edits/removals to existing questions need a separate content review.
|
|
40
|
+
export function verifyApprovedPrefix(sheet, response) {
|
|
41
|
+
const review = response?.data?.review;
|
|
42
|
+
const artifact = Number(review?.publishedArtifactVersionId);
|
|
43
|
+
if (
|
|
44
|
+
response?.ok !== true ||
|
|
45
|
+
Number(review?.buildId) !== 2460 ||
|
|
46
|
+
review?.status !== "approved" ||
|
|
47
|
+
review?.isLive !== true ||
|
|
48
|
+
!Number.isSafeInteger(artifact) ||
|
|
49
|
+
artifact <= 0 ||
|
|
50
|
+
Number(review?.publishedVersionId) !== artifact
|
|
51
|
+
) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"A fresh, live, approved Math Lab review is required. Re-read the current reward review.",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const rules = review.config?.rules;
|
|
57
|
+
if (!Array.isArray(rules))
|
|
58
|
+
throw new Error("The approved review has no question rules.");
|
|
59
|
+
const result = [];
|
|
60
|
+
for (const level of LEVELS) {
|
|
61
|
+
const id = `${level}-daily`;
|
|
62
|
+
const matches = rules.filter((rule) => rule.id === id);
|
|
63
|
+
const before = matches[0]?.sets;
|
|
64
|
+
const after = sheet?.rules?.[id]?.sets;
|
|
65
|
+
if (
|
|
66
|
+
matches.length !== 1 ||
|
|
67
|
+
!Array.isArray(before) ||
|
|
68
|
+
!before.length ||
|
|
69
|
+
!Array.isArray(after)
|
|
70
|
+
) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`${id}: missing or ambiguous approved questions or candidate grade.`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (after.length < before.length)
|
|
76
|
+
throw new Error(
|
|
77
|
+
`${id}: candidate loses ${before.length - after.length} approved questions.`,
|
|
78
|
+
);
|
|
79
|
+
for (let i = 0; i < before.length; i++) {
|
|
80
|
+
if (!isDeepStrictEqual(after[i], before[i])) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`${id}: approved question ${before[i].key || i + 1} changed or moved. Sync the canonical sheet before refilling.`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const keys = after.map((set) => set.key);
|
|
87
|
+
if (
|
|
88
|
+
keys.some((key) => typeof key !== "string" || !key.trim()) ||
|
|
89
|
+
new Set(keys).size !== keys.length
|
|
90
|
+
) {
|
|
91
|
+
throw new Error(`${id}: question keys must be present and unique.`);
|
|
92
|
+
}
|
|
93
|
+
result.push({
|
|
94
|
+
rule: id,
|
|
95
|
+
preserved: before.length,
|
|
96
|
+
added: after.length - before.length,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
liveReview: Number(review.id),
|
|
101
|
+
artifactVersionId: artifact,
|
|
102
|
+
preservedExact: true,
|
|
103
|
+
rules: result,
|
|
104
|
+
};
|
|
105
|
+
}
|
package/lib/workspace.js
CHANGED
|
@@ -738,6 +738,8 @@ export async function writeVersionCheckoutMetadata({
|
|
|
738
738
|
readOnly: true,
|
|
739
739
|
versionCheckout: true,
|
|
740
740
|
checkoutVersion: Number(version?.version || 0) || null,
|
|
741
|
+
checkoutArtifactVersionId: Number(version?.versionId || 0) || null,
|
|
742
|
+
publishedCheckout: options.pullPublished === true,
|
|
741
743
|
checkoutVersionSummary: version?.summary || null,
|
|
742
744
|
checkoutVersionCreatedAt: Number(version?.createdAt || 0) || null,
|
|
743
745
|
build: {
|
package/package.json
CHANGED
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -502,9 +502,9 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
|
|
|
502
502
|
- Template values describe types, not answers to copy. All object keys are required and extra keys are rejected, including inside nested objects; {} means an exactly empty object, not a free-form dictionary. A non-empty array uses only its first item as the template for every returned item, with no fixed length; mixed examples do not declare a union. [] allows arbitrary JSON items. A null template accepts any JSON primitive (string, number, boolean or null), but the key is still required. Use 'number' for fractional numbers; 0 and other integer examples require integers. State content requirements in prompt or instructions. GPT routes retain descriptive string examples as field guidance, not enforced enum values.
|
|
503
503
|
- GPT models prefer native JSON Schema output for compatible shapes. Shapes outside the strict provider subset, such as [] with arbitrary items or deeply nested structures, use prompt-based JSON generation with the same final validation. A provider rejection specifically of the native schema triggers one prompt-based fallback; auth, quota, safety and unrelated request failures do not trigger that fallback. Validation retries preserve the original task and instructions. The returned object/result shape and streaming callbacks are unchanged.
|
|
504
504
|
- mode is accepted as an alias for thinkingMode, and mid is accepted as an alias for medium.
|
|
505
|
-
- Omit model to use the normal Lite/Medium/High routing. model
|
|
506
|
-
- thinkingMode low uses
|
|
507
|
-
- thinkingMode medium uses
|
|
505
|
+
- Omit model to use the normal Lite/Medium/High routing. Lumine Light defaults to claude-haiku-5-5; gpt-6-luna remains its second choice. Both model IDs accept Low, Medium or High thinking. Explicit claude-sonnet-5-5, gpt-6.1-sol or claude-opus-5-5 still require thinkingMode: 'high'; unknown model IDs reject instead of silently falling back. Retired IDs still work and run on their replacement: claude-sonnet-5 runs claude-sonnet-5-5; gpt-6-sol and gpt-5.6-sol run gpt-6.1-sol; gpt-6-astra, claude-opus-5 and claude-fable-5-1 run claude-opus-5-5.
|
|
506
|
+
- thinkingMode low uses Claude Haiku 5.5 with low adaptive thinking and consumes the viewer's AI Energy from confirmed provider usage. Set model: 'gpt-6-luna' to choose Luna instead.
|
|
507
|
+
- thinkingMode medium is Lumine Light: it uses Claude Haiku 5.5 with medium adaptive thinking and consumes normal AI Energy. Set model: 'gpt-6-luna' to keep Luna as the second choice.
|
|
508
508
|
- thinkingMode high without model uses Claude Sonnet 5.5 with high adaptive thinking and consumes high AI Energy. Explicit model: 'gpt-6.1-sol' selects GPT-6.1 Sol with xhigh reasoning at the same High AI Energy tier; model: 'claude-sonnet-5-5' names Sonnet 5.5 directly.
|
|
509
509
|
- claude-sonnet-5-5 and claude-opus-5-5 use Anthropic adaptive High thinking and debit confirmed provider usage at the High tier.
|
|
510
510
|
- Pass onStatus, onReasoning, and/or onText to stream progress from the same structured generation. onStatus receives high-level phases such as thinking, searching_web, responding, validating, and completed.
|
|
@@ -513,7 +513,7 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
|
|
|
513
513
|
- Put a user-facing field such as producerNotes in expectedStructure when commentary must be part of the validated final object rather than transient reasoning progress.
|
|
514
514
|
- When AI Energy is empty, every automatic or named model choice rejects before new provider work; there is no free fallback mode.
|
|
515
515
|
- Live web search is enabled by default in Medium and High modes. Pass webSearch: false to disable it for the app. Low/Lite Mode remains tool-free; explicitly forcing webSearch: true in Low Mode returns an error.
|
|
516
|
-
- The server validates the final shape;
|
|
516
|
+
- The server validates the final shape; OpenAI/xAI routes can retry malformed output, while Anthropic routes (claude-haiku-5-5, claude-sonnet-5-5 and claude-opus-5-5) use native JSON Schema output and retry one malformed or shape-invalid result. App code should still validate business-specific enum values.
|
|
517
517
|
- Example: const { object } = await Twinkle.ai.generateObject({ thinkingMode: 'high', model: 'claude-opus-5-5', prompt: 'Plan the next section from: ' + currentState, expectedStructure: { producerNotes: 'string', action: 'string', confidence: 'number' }, onStatus: (phase) => showPhase(phase), onReasoning: (summary, meta) => showReasoningProgress(summary, meta), onText: (partialJson, meta) => showStructuredProgress(partialJson, meta) });
|
|
518
518
|
- onChatStatus(listener) | scopes: none
|
|
519
519
|
- Returns: unsubscribe function
|
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -130,13 +130,35 @@ private Lumine audit log; no public bot identity is involved.
|
|
|
130
130
|
canonical bucket. Use it to distinguish quota bookkeeping from moderation;
|
|
131
131
|
the note itself changes no access, ban, or identity rules.
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
The commands above are quota bookkeeping only. They cannot ban accounts, block signup,
|
|
134
134
|
add IP/device/risk-key rules, or infer an account family. Identification
|
|
135
135
|
remains a human/LLM evidence judgment and must be explicitly requested by
|
|
136
136
|
Mikey; routine administrator runs still escalate suspected alternate accounts
|
|
137
137
|
and never auto-enforce. `accounts add` and `note set` still accept only an
|
|
138
138
|
existing **unbanned** bucket.
|
|
139
139
|
|
|
140
|
+
For an exact device Mikey explicitly authorizes blocking, use the separate
|
|
141
|
+
signup-only action after reviewing `identity inspect`:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
lumine admin ai-bucket device block-signup --bucket-id 10 --user 3127 \
|
|
145
|
+
--device-key 0123456789abcdef --user-ids 3127 \
|
|
146
|
+
--note "Mikey approved this reviewed device for the household signup block" --json
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`--user` is the source account ID; `--user-ids` is the complete reviewed list
|
|
150
|
+
of accounts observed on that device, not every member of the household.
|
|
151
|
+
The API resolves the inspection handle internally, rechecks bounded 365-day
|
|
152
|
+
device evidence, requires the exact expected account set, and rejects devices
|
|
153
|
+
used by anyone outside the existing bucket. Incomplete history, unresolved
|
|
154
|
+
handles and a rule belonging to another bucket stop the action. The bucket
|
|
155
|
+
must already have an active **signup-only** restriction. This cannot turn on
|
|
156
|
+
a bucket ban, change full-site bans, add network rules or infer membership.
|
|
157
|
+
The response confirms the canonical rule and restriction without returning the
|
|
158
|
+
raw device ID. The private audit records the request before evidence is read
|
|
159
|
+
and records successful completion separately. Routine reviews still never
|
|
160
|
+
auto-enforce; this action requires Mikey's explicit instruction for the device.
|
|
161
|
+
|
|
140
162
|
### Shared verified-email policies
|
|
141
163
|
|
|
142
164
|
A verified address proves that an account can use that mailbox; it does not
|
|
@@ -198,6 +220,22 @@ IP evidence, private messages, or unrelated activity. `--include-private-evidenc
|
|
|
198
220
|
adds DOB values and verified email addresses only; exact device IDs are never
|
|
199
221
|
returned.
|
|
200
222
|
|
|
223
|
+
`sharedDevices[].usage` reports recorded event counts, first/last observed Unix
|
|
224
|
+
timestamps, an `eventsByType` breakdown, and the same details per account. These
|
|
225
|
+
come from `ai_usage_session_evidence`, which records the exact device identifier;
|
|
226
|
+
`users_actions` has account/IP activity but no device identifier. Counts include
|
|
227
|
+
events such as login, logout, socket binding and AI use, **not unique visits or
|
|
228
|
+
physical-device sessions**. Only events attached to an account are counted.
|
|
229
|
+
|
|
230
|
+
Usage `status: complete` means the queried device's records fit inside the
|
|
231
|
+
365-day lookback and read limit. `partial` counts are lower bounds, with observed
|
|
232
|
+
dates and `truncatedBy` explaining the gap. `not_read` has a null event count,
|
|
233
|
+
never zero. `accountsTruncated` separately marks a capped account-detail list;
|
|
234
|
+
its totals still include omitted accounts. Device discovery can be truncated
|
|
235
|
+
even when an individual device's usage is complete: always check the overall
|
|
236
|
+
`evidenceCoverage` too. Human-readable CLI output includes these distinctions;
|
|
237
|
+
older APIs explicitly report that usage counts are unavailable.
|
|
238
|
+
|
|
201
239
|
The CLI sends `identity inspect` and `identity network` as reads, so it keeps
|
|
202
240
|
no copy of the result on disk; the server still records who looked, when and
|
|
203
241
|
why, one audit row per call. When a case needs a kept copy (a bullying or police
|
|
@@ -1647,6 +1685,39 @@ to a child):
|
|
|
1647
1685
|
|
|
1648
1686
|
## Private carry-over todos
|
|
1649
1687
|
|
|
1688
|
+
### Zero and Ciel's personal follow-ups
|
|
1689
|
+
|
|
1690
|
+
Each bot has a separate persistent list of commitments made to members. The
|
|
1691
|
+
API reply tools save a concrete task before saying it is on the bot's list;
|
|
1692
|
+
they link the real source comment/chat and keep the requesting member and bot
|
|
1693
|
+
server-owned. Retries of the same message return the same task. Members can
|
|
1694
|
+
ask that bot to read or cancel their own tasks. This does not approve any
|
|
1695
|
+
administrative action, deployment, reward, or publication.
|
|
1696
|
+
|
|
1697
|
+
Full `daily-run start` returns `personalFollowups` for the assigned bot;
|
|
1698
|
+
`daily-run gather` saves its first page in `personal-followups.json`. Follow
|
|
1699
|
+
`nextCursor` to exhaustion. Other bots' tasks wait for their own turns, and
|
|
1700
|
+
scoped sessions do not surface or modify the list.
|
|
1701
|
+
|
|
1702
|
+
```bash
|
|
1703
|
+
lumine admin followup list --json
|
|
1704
|
+
lumine admin followup list --cursor 100 --json
|
|
1705
|
+
lumine admin followup update 17 --revision 2 --status completed --note "Played the requested path on published version 400; observed ...; replied in original comment 91 (reply 103)." --json
|
|
1706
|
+
```
|
|
1707
|
+
|
|
1708
|
+
Read the original context, do the promised work, keep evidence, and report the
|
|
1709
|
+
real result to the member in the original context under the existing reply
|
|
1710
|
+
rules. Do not move a private request into a public post. Build replies still
|
|
1711
|
+
require exact-version review evidence. Record `blocked` with a concrete next
|
|
1712
|
+
step when access, the creator, or Mikey's decision is needed. A completed
|
|
1713
|
+
daily run never completes promises automatically. Update requires the current
|
|
1714
|
+
revision and the assigned bot's active full run; stale or sibling updates are
|
|
1715
|
+
refused. Daily reports include pending follow-ups and their latest progress.
|
|
1716
|
+
|
|
1717
|
+
Apply `add-ai-daily-followups.sql` before deploying this API/CLI capability.
|
|
1718
|
+
|
|
1719
|
+
### Operator carryovers
|
|
1720
|
+
|
|
1650
1721
|
```bash
|
|
1651
1722
|
lumine admin todo list --json
|
|
1652
1723
|
lumine admin todo list --status all --json
|
|
@@ -1666,6 +1737,35 @@ audit path: canonical todo state and its private `todo.create` / `todo.update`
|
|
|
1666
1737
|
audit response commit together, and no public bot, public mutation count, or
|
|
1667
1738
|
rotation signal is involved.
|
|
1668
1739
|
|
|
1740
|
+
Track implementation, release and verification separately when a carryover
|
|
1741
|
+
involves a fix. After `add-admin-todo-workflow.sql` and the matching API are
|
|
1742
|
+
deployed, save a local JSON file such as:
|
|
1743
|
+
|
|
1744
|
+
```json
|
|
1745
|
+
{
|
|
1746
|
+
"implementation": "ready",
|
|
1747
|
+
"release": "pending",
|
|
1748
|
+
"verification": "pending",
|
|
1749
|
+
"fixRef": "work/topic/release-review.md",
|
|
1750
|
+
"releaseRef": "",
|
|
1751
|
+
"evidence": "Focused regression passed; production is unchanged.",
|
|
1752
|
+
"nextAction": "Deployer releases the reviewed fix, then check the live result."
|
|
1753
|
+
}
|
|
1754
|
+
```
|
|
1755
|
+
|
|
1756
|
+
Use `lumine admin todo update 12 --revision <current-revision> --workflow
|
|
1757
|
+
work/topic/workflow.json --status in_progress --note "Ready for the deployer."
|
|
1758
|
+
--json`. The revision comes from the canonical list/update response; stale
|
|
1759
|
+
updates are rejected. Implementation values are `needed`, `in_progress`,
|
|
1760
|
+
`ready`, `not_needed`; release is `pending`, `live`, `not_needed`; verification
|
|
1761
|
+
is `pending` or `passed`. Any unfinished phase needs a concrete `nextAction`;
|
|
1762
|
+
passed verification needs `evidence`. Completion requires implementation ready
|
|
1763
|
+
(or unnecessary), release live (or unnecessary), and verification passed.
|
|
1764
|
+
Keep physical-device checks and observation windows pending until they happen.
|
|
1765
|
+
Legacy todos have `workflow: null`, which means unknown, never verified. An
|
|
1766
|
+
update without `--workflow` preserves an existing workflow and cannot bypass
|
|
1767
|
+
its completion requirements.
|
|
1768
|
+
|
|
1669
1769
|
Every successful full `daily-run start` response automatically includes all
|
|
1670
1770
|
unfinished items under `data.carryoverTodos`. The same run ID increments an
|
|
1671
1771
|
item's surfacing telemetry at most once, even when start is retried. This is the
|