@stage5/lumine 0.2.65 → 0.2.66

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
@@ -350,6 +350,7 @@ lumine admin economy trace lock --days 3 \
350
350
  lumine admin rescue wordle-audit --days 30 \
351
351
  --reason "Identify recorded Wordle breaks and rescue status" --json
352
352
  lumine admin daily-run start --identity auto --comment-mode off --json
353
+ lumine admin daily-run start --scope featured --identity auto --json
353
354
  lumine admin sponsor applications list --status pending --json
354
355
  lumine admin sponsor applications review 12 --decision approve \
355
356
  --note "Approved for probationary duty" --json
@@ -378,6 +379,9 @@ lumine admin subject reveal 123 --json
378
379
  lumine admin subject effort set 123 --level 3 --json
379
380
  lumine admin subject creator set-made-by-poster 123 --json
380
381
  lumine admin featured list --json
382
+ lumine admin featured history --subject-ids 50,40 --all --json
383
+ lumine admin featured add --subject-ids 50,40 \
384
+ --posted-after 2026-08-27T00:00:00+07:00 --json
381
385
  lumine admin subject feature 123 --json
382
386
  lumine admin subject unfeature 123 --json
383
387
  lumine admin featured reorder --subject-ids 30,20,10 --json
@@ -412,7 +416,19 @@ and posts use the selected bot's server-owned canonical persona. Outside Build
412
416
  threads, a later human reply is handled by Twinkle's existing autonomous
413
417
  Zero/Ciel responder without Lumine remaining active.
414
418
 
415
- Management agents also inspect recent public Build candidates during each run.
419
+ `--scope full` is the default and authorizes the complete daily-management
420
+ workflow. `--scope featured` is a deliberately narrow run for a requested
421
+ Featured-only slice: it cannot comment, does not surface carry-over todos, does
422
+ not write full-run queue coverage, does not require sponsor review, and never
423
+ advances full-run review windows. Its dedicated start endpoint also prevents an
424
+ older API from silently interpreting it as a full run. The
425
+ atomic `featured add` verb preserves the supplied relevance order and requires
426
+ the server to prove both a strict posting-date boundary and complete
427
+ never-Featured history. `featured history` returns that evidence without the
428
+ large repeated board snapshots in the general audit trail.
429
+
430
+ Management agents also inspect recent public Build candidates during each full
431
+ daily review.
416
432
  `builds review` opens one published app in an isolated temporary Chromium
417
433
  profile, captures a screenshot and console evidence, verifies that the
418
434
  published version stayed fixed, and writes a review receipt in a unique output
@@ -451,6 +467,9 @@ follows them automatically, fsyncs each confirmed page to a private NDJSON
451
467
  candidate spool, saves only bounded cursor/boundary/count metadata in the
452
468
  checkpoint, and records completed queue coverage in the run audit; `--resume`
453
469
  verifies the confirmed spool prefix and continues the exact same request. The
470
+ automatic checkpoint name includes the exact request fingerprint (including
471
+ client-side view filters and result-transform inputs), and an
472
+ exclusive adjacent lock prevents concurrent processes from sharing it. The
454
473
  final JSON contract still contains the complete collection, streamed from the
455
474
  spool instead of accumulated in memory. With `--all --json`, bounded progress
456
475
  goes to stderr so stdout remains one pipe-safe JSON value. Recommendation scans default to the previous completed
@@ -462,7 +481,7 @@ cursors are bound to the original date and effort filters.
462
481
  `news claim` can write both the canonical leased digest and an editable
463
482
  editorial scaffold. `news validate` is local and checks every citation and
464
483
  quote before submission; `news submit --claim` reads the lease identity from
465
- the claim file. Every `daily-run start` response includes writer-confirmed
484
+ the claim file. Every full `daily-run start` response includes writer-confirmed
466
485
  unfinished private todos, with once-per-run surfacing telemetry, so an agent
467
486
  can resume earlier work without relying on conversation memory. Record progress
468
487
  with `todo update`; completing a run does not complete its todos. Experiments
@@ -470,7 +489,7 @@ must meet their stated acceptance criteria—lower AI cost with weaker user
470
489
  responses is not a successful optimization. `daily-run report` summarizes
471
490
  confirmed mutations, completed queue coverage, explicitly recorded escalations,
472
491
  unfinished todos, sponsor-integrity state, and the run brief before the run is
473
- completed. A run cannot complete until its bounded sponsor-integrity snapshot
492
+ completed. A full run cannot complete until its bounded sponsor-integrity snapshot
474
493
  has been scanned and every selected case is cleared or disqualified. `hold` and
475
494
  `flag` deliberately keep the run open for human judgment; scans never suspend,
476
495
  revoke, or disqualify a sponsor automatically.
@@ -3,7 +3,9 @@ import {
3
3
  chmodSync,
4
4
  closeSync,
5
5
  createReadStream,
6
+ fchmodSync,
6
7
  fsyncSync,
8
+ linkSync,
7
9
  lstatSync,
8
10
  mkdirSync,
9
11
  openSync,
@@ -74,9 +76,198 @@ function pathWithCursor(requestPath, cursor) {
74
76
  return `${url.pathname}${url.search}`;
75
77
  }
76
78
 
77
- function defaultCheckpointPath({ runId, operationName }) {
79
+ function defaultCheckpointPath({
80
+ runId,
81
+ operationName,
82
+ operationFingerprint = "",
83
+ }) {
78
84
  const safeName = String(operationName).replace(/[^a-z0-9_.-]+/gi, "-");
79
- return path.join(os.tmpdir(), `lumine-admin-run-${runId}-${safeName}.json`);
85
+ const fingerprintSuffix = operationFingerprint
86
+ ? `-${operationFingerprint}`
87
+ : "";
88
+ return path.join(
89
+ os.tmpdir(),
90
+ `lumine-admin-run-${runId}-${safeName}${fingerprintSuffix}.json`,
91
+ );
92
+ }
93
+
94
+ function resolveAutomaticCheckpointPath({
95
+ options,
96
+ operation,
97
+ runId,
98
+ operationFingerprint,
99
+ }) {
100
+ if (String(options.adminCheckpoint || "").trim()) {
101
+ return path.resolve(options.adminCheckpoint);
102
+ }
103
+ const keyedPath = defaultCheckpointPath({
104
+ runId,
105
+ operationName: operation.name,
106
+ operationFingerprint,
107
+ });
108
+ if (
109
+ options.adminResume &&
110
+ !pathEntryExists(keyedPath) &&
111
+ !pathEntryExists(`${keyedPath}.lock`) &&
112
+ !pathEntryExists(`${keyedPath}.lock.reclaim`)
113
+ ) {
114
+ const legacyPath = defaultCheckpointPath({
115
+ runId,
116
+ operationName: operation.name,
117
+ });
118
+ if (pathEntryExists(legacyPath)) return legacyPath;
119
+ }
120
+ return keyedPath;
121
+ }
122
+
123
+ function pathEntryExists(filePath) {
124
+ try {
125
+ lstatSync(filePath);
126
+ return true;
127
+ } catch (error) {
128
+ if (error?.code === "ENOENT") return false;
129
+ throw error;
130
+ }
131
+ }
132
+
133
+ function acquireCheckpointLock(checkpointPath, operationFingerprint) {
134
+ const lockPath = `${checkpointPath}.lock`;
135
+ const reclaimPath = `${lockPath}.reclaim`;
136
+ mkdirSync(path.dirname(lockPath), { recursive: true });
137
+ for (let attempt = 0; attempt < 2; attempt += 1) {
138
+ if (pathEntryExists(reclaimPath)) {
139
+ throw validationError(
140
+ `Another Lumine admin scan is recovering checkpoint ${checkpointPath}. Wait for it to finish or choose a different --checkpoint.`,
141
+ );
142
+ }
143
+ const token = randomUUID();
144
+ let descriptor;
145
+ let ownsLock = false;
146
+ try {
147
+ descriptor = openSync(lockPath, "wx", 0o600);
148
+ ownsLock = true;
149
+ fchmodSync(descriptor, 0o600);
150
+ writeFileSync(
151
+ descriptor,
152
+ `${JSON.stringify({
153
+ kind: "admin-pagination-lock",
154
+ pid: process.pid,
155
+ token,
156
+ operationFingerprint,
157
+ createdAt: new Date().toISOString(),
158
+ })}\n`,
159
+ );
160
+ fsyncSync(descriptor);
161
+ closeSync(descriptor);
162
+ descriptor = undefined;
163
+ // A stale-lock reclaimer can begin after the check above but before this
164
+ // O_EXCL create. It hard-links the exact old inode before removal. If
165
+ // that claim still exists, relinquish our new lock rather than allowing
166
+ // two owners to cross during the handoff.
167
+ if (pathEntryExists(reclaimPath)) {
168
+ releaseCheckpointLock({ lockPath, token });
169
+ ownsLock = false;
170
+ throw validationError(
171
+ `Another Lumine admin scan is recovering checkpoint ${checkpointPath}. Retry after it finishes.`,
172
+ );
173
+ }
174
+ return { lockPath, token };
175
+ } catch (error) {
176
+ if (descriptor !== undefined) {
177
+ try {
178
+ closeSync(descriptor);
179
+ } catch {
180
+ // The failed lock initialization may already have closed it.
181
+ }
182
+ }
183
+ if (ownsLock) {
184
+ try {
185
+ unlinkSync(lockPath);
186
+ } catch {
187
+ // A failed initialization may leave no owned lock to remove.
188
+ }
189
+ }
190
+ if (error?.code !== "EEXIST") throw error;
191
+ if (attempt > 0 || !removeDeadCheckpointLock(lockPath)) {
192
+ throw validationError(
193
+ `Another Lumine admin scan owns checkpoint ${checkpointPath}. Wait for it to finish or choose a different --checkpoint.`,
194
+ );
195
+ }
196
+ }
197
+ }
198
+ throw validationError(
199
+ `Could not acquire the pagination checkpoint lock for ${checkpointPath}.`,
200
+ );
201
+ }
202
+
203
+ function removeDeadCheckpointLock(lockPath) {
204
+ const reclaimPath = `${lockPath}.reclaim`;
205
+ let ownsReclaim = false;
206
+ try {
207
+ // Creating a fixed-name hard link is an atomic claim on the exact lock
208
+ // inode. It serializes reclaimers and lets us prove we are not unlinking a
209
+ // replacement lock created after another process removed the stale one.
210
+ linkSync(lockPath, reclaimPath);
211
+ ownsReclaim = true;
212
+ const fileState = lstatSync(reclaimPath);
213
+ if (!fileState.isFile() || fileState.isSymbolicLink()) return false;
214
+ if (fileState.size > 16 * 1024) return false;
215
+ const state = JSON.parse(readFileSync(reclaimPath, "utf8"));
216
+ if (
217
+ state?.kind !== "admin-pagination-lock" ||
218
+ !/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i.test(
219
+ String(state?.token || ""),
220
+ ) ||
221
+ !/^[a-f0-9]{64}$/i.test(String(state?.operationFingerprint || ""))
222
+ ) {
223
+ return false;
224
+ }
225
+ const pid = Number(state?.pid);
226
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false;
227
+ try {
228
+ process.kill(pid, 0);
229
+ return false;
230
+ } catch (error) {
231
+ if (error?.code !== "ESRCH") return false;
232
+ }
233
+ const claimedState = lstatSync(reclaimPath);
234
+ const currentState = lstatSync(lockPath);
235
+ if (
236
+ claimedState.dev !== currentState.dev ||
237
+ claimedState.ino !== currentState.ino
238
+ ) {
239
+ return false;
240
+ }
241
+ unlinkSync(lockPath);
242
+ return true;
243
+ } catch {
244
+ return false;
245
+ } finally {
246
+ if (ownsReclaim) {
247
+ try {
248
+ unlinkSync(reclaimPath);
249
+ } catch {
250
+ // A failed or completed reclaim leaves the current lock untouched.
251
+ }
252
+ }
253
+ }
254
+ }
255
+
256
+ function releaseCheckpointLock(lock) {
257
+ try {
258
+ const fileState = lstatSync(lock.lockPath);
259
+ if (!fileState.isFile() || fileState.isSymbolicLink()) return;
260
+ const saved = JSON.parse(readFileSync(lock.lockPath, "utf8"));
261
+ if (saved?.token === lock.token && Number(saved?.pid) === process.pid) {
262
+ unlinkSync(lock.lockPath);
263
+ }
264
+ } catch (error) {
265
+ if (error?.code !== "ENOENT") {
266
+ process.stderr.write(
267
+ `Lumine admin: could not release pagination lock ${lock.lockPath} (${error?.message || error}).\n`,
268
+ );
269
+ }
270
+ }
80
271
  }
81
272
 
82
273
  function operationFingerprintValue({
@@ -91,6 +282,13 @@ function operationFingerprintValue({
91
282
  apiUrl: String(options.apiUrl || "").replace(/\/$/, ""),
92
283
  name: operation.name,
93
284
  path: pathWithCursor(operation.path, ""),
285
+ ...(operation.name === "builds.candidates"
286
+ ? {
287
+ resultTransform: {
288
+ siteUrl: String(options.siteUrl || "").replace(/\/$/, ""),
289
+ },
290
+ }
291
+ : {}),
94
292
  pagination: {
95
293
  collectionKey: operation.pagination.collectionKey,
96
294
  coverageQueue: operation.pagination.coverageQueue || null,
@@ -604,22 +802,46 @@ function aggregatePageResult({ operation, lastPage, checkpointPath, state }) {
604
802
  });
605
803
  }
606
804
 
607
- export async function runAutomaticPagination({
805
+ export async function runAutomaticPagination(args) {
806
+ const { options, operation, runId } = args;
807
+ if (!operation.pagination) {
808
+ throw validationError("--all is supported only by paginated admin listings.");
809
+ }
810
+ const operationFingerprint = operationFingerprintValue({
811
+ schemaVersion: ADMIN_PAGINATION_SCHEMA_VERSION,
812
+ options,
813
+ operation,
814
+ runId,
815
+ });
816
+ const checkpointPath = resolveAutomaticCheckpointPath({
817
+ options,
818
+ operation,
819
+ runId,
820
+ operationFingerprint,
821
+ });
822
+ const lock = acquireCheckpointLock(checkpointPath, operationFingerprint);
823
+ try {
824
+ return await runAutomaticPaginationWithLock({
825
+ ...args,
826
+ checkpointPath,
827
+ operationFingerprint,
828
+ });
829
+ } finally {
830
+ releaseCheckpointLock(lock);
831
+ }
832
+ }
833
+
834
+ async function runAutomaticPaginationWithLock({
608
835
  options,
609
836
  operation,
610
837
  runId,
838
+ checkpointPath,
839
+ operationFingerprint,
611
840
  fetchPage,
612
841
  transformPage,
613
842
  recordCoverage,
614
843
  reportProgress = (message) => process.stderr.write(`${message}\n`),
615
844
  }) {
616
- if (!operation.pagination) {
617
- throw validationError("--all is supported only by paginated admin listings.");
618
- }
619
- const checkpointPath = path.resolve(
620
- options.adminCheckpoint ||
621
- defaultCheckpointPath({ runId, operationName: operation.name }),
622
- );
623
845
  const requestedOutputPath = String(options.adminOutput || "").trim();
624
846
  if (options.adminOutput && !requestedOutputPath) {
625
847
  throw validationError("An output file path is required.");
@@ -632,12 +854,6 @@ export async function runAutomaticPagination({
632
854
  "The pagination checkpoint and result output must use different files.",
633
855
  );
634
856
  }
635
- const operationFingerprint = operationFingerprintValue({
636
- schemaVersion: ADMIN_PAGINATION_SCHEMA_VERSION,
637
- options,
638
- operation,
639
- runId,
640
- });
641
857
  const legacyOperationFingerprint = operationFingerprintValue({
642
858
  schemaVersion: LEGACY_ADMIN_PAGINATION_SCHEMA_VERSION,
643
859
  options,