@stage5/lumine 0.2.61 → 0.2.63

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 CHANGED
@@ -284,7 +284,7 @@ starts unless they accept it there. This notice is invitation metadata only;
284
284
  it does not expose the unapproved Workshop plan or private chat.
285
285
 
286
286
  When a user approves a plan, `duty watch` returns the scoped assignment file and
287
- contribution workspace to that same session. It does not launch a fresh Codex
287
+ exact approved Build workspace to that same session. It does not launch a fresh Codex
288
288
  or Claude process. The on-duty agent uses this explicit lifecycle while it
289
289
  works:
290
290
 
@@ -298,7 +298,7 @@ lumine sponsor job complete 42 --summary "Implemented and tested the approved ch
298
298
 
299
299
  `pulse` renews the duty/job leases and returns any newly approved follow-up.
300
300
  `update` sends the exact file text back to Zero or Ciel as a user-visible
301
- message from Lumine, their added brain. Use it for concise, deliberate
301
+ message from Lumine, the on-duty project collaborator they talk with. Use it for concise, deliberate
302
302
  milestones only; never send hidden reasoning, raw terminal output, credentials,
303
303
  tokens, private paths, or unrelated data. Run `pulse` between substantial work
304
304
  steps. `relay-applied` is an explicit receipt;
@@ -318,11 +318,14 @@ shows none of the Workshop UI.
318
318
 
319
319
  Zero or Ciel remains the user-facing teammate. The on-duty agent session
320
320
  receives only the initial relay and active-job Build follow-ups covered by the
321
- user’s explicit Workshop consent, the assigned contribution branch, and its
322
- scoped Build Forum—not the raw assistant chat. It cannot merge into Main,
323
- publish, or use website-management APIs. Lumine records
321
+ user’s explicit Workshop consent and the exact approved Main or requester-owned
322
+ branch—not the raw assistant chat. Temporary job access never includes Forum
323
+ comments. A Forum snapshot is available only when the sponsor's normal account
324
+ already owns the project or is an accepted teammate. Editing jobs save directly
325
+ to the approved workspace after Twinkle creates a restore point; they cannot
326
+ publish or use website-management APIs. Lumine records
324
327
  the requested and provider-reported model, effort, service tier, runtime/usage
325
- evidence, coordinator/helper tree, saved artifact, and branch notice. Every
328
+ evidence, coordinator/helper tree, restore point, and saved artifact. Every
326
329
  completed handoff enters the daily integrity flow; probationary, hard-flagged,
327
330
  and sampled work requires review. Each unique cleared contribution to another
328
331
  user earns a flat 50 Karma Points, while self-sponsored testing, retries, and
@@ -542,6 +542,12 @@ function aggregatePageResult({ operation, lastPage, checkpointPath, state }) {
542
542
  const last = lastPage || { ok: true, status: "success", data: {} };
543
543
  const lastData = { ...(last.data || {}) };
544
544
  delete lastData[operation.pagination.collectionKey];
545
+ const lastPagination = { ...(last.data?.pagination || {}) };
546
+ const pageScannedCount = normalizeScannedCount(
547
+ lastPagination.scannedCount,
548
+ 0,
549
+ );
550
+ delete lastPagination.scannedCount;
545
551
  if (state.filterSummariesComplete) {
546
552
  if (state.clientFilter) lastData.clientFilter = state.clientFilter;
547
553
  else delete lastData.clientFilter;
@@ -568,7 +574,8 @@ function aggregatePageResult({ operation, lastPage, checkpointPath, state }) {
568
574
  data: {
569
575
  ...lastData,
570
576
  pagination: {
571
- ...(last.data?.pagination || {}),
577
+ ...lastPagination,
578
+ pageScannedCount,
572
579
  nextCursor: state.nextCursor,
573
580
  hasMore: !state.exhausted,
574
581
  exhausted: state.exhausted,
package/lib/admin.js CHANGED
@@ -171,12 +171,32 @@ export async function adminCommand(options) {
171
171
  scope: operation.mutates ? "build:write" : "build:read",
172
172
  });
173
173
  let runId = 0;
174
+ let correctionSessionId = 0;
175
+ let correctionSession = null;
174
176
  if (adminOperationRequiresRun(operation)) {
175
- const runStatus = await requestJson({
176
- url: `${options.apiUrl}/cli/admin/daily-runs/status`,
177
- authToken: auth.token,
178
- timeoutMs: options.timeoutMs,
179
- });
177
+ if (operation.correctionEligible) {
178
+ const correctionStatus = await requestJson({
179
+ url: `${options.apiUrl}/cli/admin/corrections/status`,
180
+ authToken: auth.token,
181
+ timeoutMs: options.timeoutMs,
182
+ });
183
+ const correction = correctionStatus?.data?.correction || null;
184
+ if (
185
+ correction?.status === "active" &&
186
+ Number(correction.correctionCommentId || 0) ===
187
+ Number(operation.correctionCommentId || 0)
188
+ ) {
189
+ correctionSessionId = Number(correction.id || 0);
190
+ correctionSession = correction;
191
+ }
192
+ }
193
+ const runStatus = correctionSessionId
194
+ ? null
195
+ : await requestJson({
196
+ url: `${options.apiUrl}/cli/admin/daily-runs/status`,
197
+ authToken: auth.token,
198
+ timeoutMs: options.timeoutMs,
199
+ });
180
200
  const activeRun = runStatus?.data?.run || null;
181
201
  const retryableFinishedRun = [
182
202
  "daily-run.complete",
@@ -186,15 +206,29 @@ export async function adminCommand(options) {
186
206
  : null;
187
207
  const selectedRun = activeRun || retryableFinishedRun;
188
208
  runId = Number(selectedRun?.id || 0);
189
- if (!runId) throw noActiveRunError();
190
- if (options.adminIdentity) {
209
+ if (!runId && !correctionSessionId) {
210
+ if (operation.correctionEligible) {
211
+ const error = new Error(
212
+ `Start a correction session first: lumine admin correction start ${operation.correctionCommentId}.`,
213
+ );
214
+ error.code = "CLI_ADMIN_CORRECTION_NOT_ACTIVE";
215
+ throw error;
216
+ }
217
+ throw noActiveRunError();
218
+ }
219
+ if (options.adminIdentity && (selectedRun || correctionSession)) {
191
220
  const requestedIdentity = parseIdentity(options.adminIdentity);
221
+ const canonicalIdentity = correctionSession
222
+ ? correctionSession.identity?.key
223
+ : selectedRun?.identity?.key;
192
224
  if (
193
225
  requestedIdentity !== "auto" &&
194
- requestedIdentity !== selectedRun?.identity?.key
226
+ requestedIdentity !== canonicalIdentity
195
227
  ) {
196
228
  throw cliValidationError(
197
- `--identity ${requestedIdentity} does not match active run #${runId} (${selectedRun?.identity?.key || "unknown"}).`,
229
+ correctionSession
230
+ ? `--identity ${requestedIdentity} does not match correction session #${correctionSessionId} (${canonicalIdentity || "unknown"}).`
231
+ : `--identity ${requestedIdentity} does not match active run #${runId} (${canonicalIdentity || "unknown"}).`,
198
232
  );
199
233
  }
200
234
  }
@@ -211,6 +245,13 @@ export async function adminCommand(options) {
211
245
  body: operation.body,
212
246
  headers: {
213
247
  ...(runId ? { "x-lumine-admin-run-id": String(runId) } : {}),
248
+ ...(correctionSessionId
249
+ ? {
250
+ "x-lumine-admin-correction-session-id": String(
251
+ correctionSessionId,
252
+ ),
253
+ }
254
+ : {}),
214
255
  ...(requestId ? { "x-lumine-idempotency-key": requestId } : {}),
215
256
  },
216
257
  timeoutMs: options.timeoutMs,
@@ -663,6 +704,9 @@ function adminOperationRequiresRun(operation) {
663
704
  "rescue.wordle.audit",
664
705
  "daily-run.start",
665
706
  "daily-run.status",
707
+ "correction.start",
708
+ "correction.status",
709
+ "correction.complete",
666
710
  "escalation.list",
667
711
  "escalation.set",
668
712
  "notable.add",
@@ -694,6 +738,39 @@ export function parseAdminOperation(options) {
694
738
  const [namespace = "", action = "", target = "", extra = ""] =
695
739
  options.positional;
696
740
 
741
+ if (namespace === "correction" || namespace === "corrections") {
742
+ if (!action || action === "status") {
743
+ return readOperation("correction.status", "/cli/admin/corrections/status");
744
+ }
745
+ if (action === "start") {
746
+ const commentId = parseRequiredInteger(target, "comment ID", 1);
747
+ return writeOperation(
748
+ "correction.start",
749
+ "POST",
750
+ "/cli/admin/corrections",
751
+ {
752
+ commentId,
753
+ },
754
+ );
755
+ }
756
+ if (action === "complete" || action === "stop") {
757
+ const correctionId = parseRequiredInteger(
758
+ target,
759
+ "correction session ID",
760
+ 1,
761
+ );
762
+ return writeOperation(
763
+ "correction.complete",
764
+ "POST",
765
+ `/cli/admin/corrections/${correctionId}/complete`,
766
+ {},
767
+ );
768
+ }
769
+ throw cliValidationError(
770
+ "Usage: lumine admin correction start <commentId> | correction status | correction complete <sessionId>.",
771
+ );
772
+ }
773
+
697
774
  if (namespace === "identity") {
698
775
  if (action === "list") {
699
776
  return readOperation("identity.list", "/cli/admin/identities");
@@ -1260,7 +1337,10 @@ export function parseAdminOperation(options) {
1260
1337
 
1261
1338
  if (namespace === "comments" && action === "get") {
1262
1339
  const commentId = parseRequiredInteger(target, "comment ID", 1);
1263
- return readOperation("comments.get", `/cli/admin/comments/${commentId}`);
1340
+ return readOperation("comments.get", `/cli/admin/comments/${commentId}`, {
1341
+ correctionEligible: true,
1342
+ correctionCommentId: commentId,
1343
+ });
1264
1344
  }
1265
1345
 
1266
1346
  if (namespace === "post") {
@@ -1674,6 +1754,10 @@ export function parseAdminOperation(options) {
1674
1754
  "PUT",
1675
1755
  `/cli/admin/comments/${commentId}`,
1676
1756
  { content: readComposedTextFile(options.adminFile) },
1757
+ {
1758
+ correctionEligible: true,
1759
+ correctionCommentId: commentId,
1760
+ },
1677
1761
  );
1678
1762
  }
1679
1763
  if (action === "post") {
@@ -1738,7 +1822,10 @@ function postGetOperation(target, options) {
1738
1822
  return subjectGetOperation(parsedTarget.id, options);
1739
1823
  }
1740
1824
  if (parsedTarget.type === "comment") {
1741
- return readOperation("post.get", `/cli/admin/comments/${parsedTarget.id}`);
1825
+ return readOperation("post.get", `/cli/admin/comments/${parsedTarget.id}`, {
1826
+ correctionEligible: true,
1827
+ correctionCommentId: parsedTarget.id,
1828
+ });
1742
1829
  }
1743
1830
  return readOperation(
1744
1831
  "post.get",
@@ -2066,8 +2153,8 @@ function bodyReadOperation(name, method, path, body) {
2066
2153
  return { name, method, path, body, mutates: false };
2067
2154
  }
2068
2155
 
2069
- function writeOperation(name, method, path, body) {
2070
- return { name, method, path, body, mutates: true };
2156
+ function writeOperation(name, method, path, body, extra = {}) {
2157
+ return { name, method, path, body, mutates: true, ...extra };
2071
2158
  }
2072
2159
 
2073
2160
  function withQuery(path, values) {
@@ -2810,6 +2897,11 @@ function printAdminResult({ operation, result }) {
2810
2897
  console.log(
2811
2898
  `Run #${data.run.id}: ${data.run.status}; identity ${data.run.identity.key}; comments ${data.run.commentMode}.`,
2812
2899
  );
2900
+ if (data.scheduledDay && data.scheduledIdentity) {
2901
+ console.log(
2902
+ `Bangkok schedule for ${data.scheduledDay}: ${data.scheduledIdentity.key}.`,
2903
+ );
2904
+ }
2813
2905
  if (data.carryoverTodos) {
2814
2906
  printTodoItems(data.carryoverTodos.items || [], "Carry-over work");
2815
2907
  }
@@ -2824,6 +2916,11 @@ function printAdminResult({ operation, result }) {
2824
2916
  }
2825
2917
  if (Object.hasOwn(data, "activeRun")) {
2826
2918
  console.log(`Preferred identity: ${data.preferredIdentity}.`);
2919
+ if (data.scheduledDay && data.scheduledIdentity) {
2920
+ console.log(
2921
+ `Bangkok schedule for ${data.scheduledDay}: ${data.scheduledIdentity.key}.`,
2922
+ );
2923
+ }
2827
2924
  console.log(
2828
2925
  `Last completed identity: ${data.lastCompletedIdentity || "none"}.`,
2829
2926
  );
package/lib/api.js CHANGED
@@ -80,6 +80,7 @@ export async function loadBuildFiles({ options, auth, buildId, includeContent })
80
80
  url: url.toString(),
81
81
  authToken: auth.token,
82
82
  timeoutMs: options.timeoutMs,
83
+ signal: options.signal,
83
84
  });
84
85
  }
85
86
 
package/lib/auth.js CHANGED
@@ -23,6 +23,7 @@ export async function login(options) {
23
23
  ],
24
24
  },
25
25
  timeoutMs: options.timeoutMs,
26
+ signal: options.signal,
26
27
  });
27
28
 
28
29
  const approvalUrl = start.verificationUriComplete || start.verificationUri;
@@ -41,7 +42,7 @@ export async function login(options) {
41
42
  const expiresInMs = Math.max(Number(start.expiresIn || 600), 1) * 1000;
42
43
 
43
44
  while (Date.now() - startedAt < expiresInMs) {
44
- await sleep(intervalMs);
45
+ await sleep(intervalMs, options.signal);
45
46
  const tokenResponse = await pollToken({
46
47
  options,
47
48
  deviceCode: start.deviceCode,
@@ -72,6 +73,7 @@ export async function pollToken({ options, deviceCode }) {
72
73
  url: `${options.apiUrl}/cli/device/token`,
73
74
  body: { deviceCode },
74
75
  timeoutMs: options.timeoutMs,
76
+ signal: options.signal,
75
77
  });
76
78
  } catch (error) {
77
79
  if (error.status === 428 || error.data?.error === "authorization_pending") {
@@ -102,6 +104,7 @@ export async function whoami(options) {
102
104
  url: `${options.apiUrl}/cli/session`,
103
105
  authToken: auth.token,
104
106
  timeoutMs: options.timeoutMs,
107
+ signal: options.signal,
105
108
  });
106
109
  console.log(
107
110
  `Logged in as ${session.username || auth.username || "unknown"} ` +
@@ -187,6 +190,7 @@ export async function assertAuthScope({ options, auth, scope }) {
187
190
  url: `${options.apiUrl}/cli/session`,
188
191
  authToken: auth.token,
189
192
  timeoutMs: options.timeoutMs,
193
+ signal: options.signal,
190
194
  });
191
195
  const scopes = Array.isArray(session.scopes) ? session.scopes : [];
192
196
  if (!scopes.includes(scope)) {
package/lib/commands.js CHANGED
@@ -1547,6 +1547,7 @@ export async function pullBuildFiles({ options, auth, buildId }) {
1547
1547
  build,
1548
1548
  dir,
1549
1549
  fileCount: files.length,
1550
+ filesHash: typeof result.filesHash === "string" ? result.filesHash : null,
1550
1551
  manifest: result.projectManifest || null,
1551
1552
  assetCount,
1552
1553
  };
package/lib/forum.js CHANGED
@@ -76,6 +76,7 @@ export async function loadBuildForumPage({
76
76
  url: url.toString(),
77
77
  authToken: auth.token,
78
78
  timeoutMs: options.timeoutMs,
79
+ signal: options.signal,
79
80
  });
80
81
  }
81
82
 
package/lib/http.js CHANGED
@@ -7,16 +7,17 @@ export async function requestJson({
7
7
  body,
8
8
  timeoutMs,
9
9
  headers = {},
10
+ signal,
10
11
  }) {
11
- const response = await request({
12
+ const { response, text } = await requestText({
12
13
  method,
13
14
  url,
14
15
  authToken,
15
16
  body,
16
17
  timeoutMs,
17
18
  headers,
19
+ signal,
18
20
  });
19
- const text = await response.text();
20
21
  const data = parseJson(text);
21
22
  if (!response.ok) {
22
23
  const error = new Error(
@@ -32,9 +33,13 @@ export async function requestJson({
32
33
  return data || {};
33
34
  }
34
35
 
35
- export async function probeUrl({ url, authToken, timeoutMs }) {
36
- const response = await request({ url, authToken, timeoutMs });
37
- const text = await response.text();
36
+ export async function probeUrl({ url, authToken, timeoutMs, signal }) {
37
+ const { response, text } = await requestText({
38
+ url,
39
+ authToken,
40
+ timeoutMs,
41
+ signal,
42
+ });
38
43
  return {
39
44
  ok: response.ok && text.trim().length > 0,
40
45
  status: response.status,
@@ -42,18 +47,30 @@ export async function probeUrl({ url, authToken, timeoutMs }) {
42
47
  };
43
48
  }
44
49
 
45
- export async function request({
50
+ export async function requestText({
46
51
  method = "GET",
47
52
  url,
48
53
  authToken,
49
54
  body,
50
55
  timeoutMs,
51
56
  headers = {},
57
+ signal,
52
58
  }) {
53
59
  const controller = new AbortController();
54
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
60
+ const parentAbort = () => controller.abort(signal?.reason);
61
+ if (signal?.aborted) parentAbort();
62
+ else signal?.addEventListener("abort", parentAbort, { once: true });
63
+ const effectiveTimeoutMs = Math.max(Number(timeoutMs) || 0, 1);
64
+ const timeoutError = new Error(
65
+ `HTTP request timed out after ${effectiveTimeoutMs}ms.`,
66
+ );
67
+ timeoutError.code = "lumine_http_timeout";
68
+ const timeout = setTimeout(
69
+ () => controller.abort(timeoutError),
70
+ effectiveTimeoutMs,
71
+ );
55
72
  try {
56
- return await fetch(url, {
73
+ const response = await fetch(url, {
57
74
  method,
58
75
  headers: {
59
76
  ...(authToken ? { authorization: authorizationHeader(authToken) } : {}),
@@ -63,8 +80,11 @@ export async function request({
63
80
  body: body ? JSON.stringify(body) : undefined,
64
81
  signal: controller.signal,
65
82
  });
83
+ const text = await response.text();
84
+ return { response, text };
66
85
  } finally {
67
86
  clearTimeout(timeout);
87
+ signal?.removeEventListener("abort", parentAbort);
68
88
  }
69
89
  }
70
90
 
package/lib/sdk.js CHANGED
@@ -2,7 +2,7 @@ import path from "path";
2
2
 
3
3
  import { ensureAuth, assertAuthScope } from "./auth.js";
4
4
  import { mintBuildApiToken } from "./api.js";
5
- import { request } from "./http.js";
5
+ import { requestText } from "./http.js";
6
6
  import { parseJson, resolveBuildId } from "./util.js";
7
7
  import { findLocalProjectMetadata } from "./workspace.js";
8
8
 
@@ -495,7 +495,7 @@ export async function executeSdkHttpCall({
495
495
  buildApiToken,
496
496
  }) {
497
497
  const startedAt = Date.now();
498
- const response = await request({
498
+ const { response, text } = await requestText({
499
499
  method,
500
500
  url,
501
501
  authToken,
@@ -503,7 +503,6 @@ export async function executeSdkHttpCall({
503
503
  timeoutMs: options.timeoutMs,
504
504
  headers: buildApiToken ? { "x-build-api-token": buildApiToken } : {},
505
505
  });
506
- const text = await response.text();
507
506
  return {
508
507
  ok: response.ok,
509
508
  status: response.status,