@stage5/lumine 0.2.53 → 0.2.54

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
@@ -229,6 +229,46 @@ claude "Read CLAUDE.md, then make the requested change."
229
229
  The login command uses a browser approval code and stores a scoped token and the
230
230
  selected project at `~/.twinkle/lumine-cli-auth.json`.
231
231
 
232
+ ## Sponsoring Zero or Ciel Build Workshop duty
233
+
234
+ Sponsorship is an approved contribution role, not an open switch and not a
235
+ website-management role. Applications are submitted only through Lumine CLI;
236
+ the Twinkle website provides instructions, but no application form.
237
+
238
+ ```bash
239
+ lumine sponsor agreement
240
+ lumine sponsor apply --providers codex --motivation "Why I want to help" \
241
+ --availability "My usual duty window" --accept-agreement
242
+ lumine sponsor status
243
+ ```
244
+
245
+ After Mikey approves an application, configure conservative limits based on
246
+ the subscription you are contributing, then start one foreground duty:
247
+
248
+ ```bash
249
+ lumine sponsor capacity --concurrency 1 --helpers 0 \
250
+ --daily-limit 3 --weekly-limit 10
251
+ lumine sponsor duty start zero --provider codex \
252
+ --model gpt-5.6-sol --effort max --service-tier priority
253
+ ```
254
+
255
+ While that foreground process has a fresh server lease, Zero or Ciel exposes a
256
+ Build Workshop in chat and users may see the named sponsor and canonical queue.
257
+ With no live duty, the website stays in its ordinary chat state and shows none
258
+ of the Workshop UI. Press Ctrl-C to stop accepting work and drain already
259
+ claimed jobs before duty ends; `pause`, `resume`, and `stop` are also available.
260
+
261
+ Zero or Ciel remains the user-facing teammate. The local coding agent receives
262
+ only the initial relay and active-job Build follow-ups covered by the user’s
263
+ explicit Workshop consent, the assigned contribution branch, and its scoped
264
+ Build Forum—not the raw assistant chat. It
265
+ cannot merge into Main, publish, or use website-management APIs. Lumine records
266
+ the requested and provider-reported model, effort, service tier, runtime/usage
267
+ evidence, coordinator/helper tree, saved artifact, and branch notice. Every
268
+ completed handoff enters the daily integrity flow; probationary, hard-flagged,
269
+ and sampled work requires review. Each unique cleared handoff earns a flat 50
270
+ Karma Points, while retries and helper agents do not multiply the award.
271
+
232
272
  ## Privileged website administration
233
273
 
234
274
  `lumine admin` reuses Mikey's saved CLI login while keeping the visible public
@@ -248,6 +288,13 @@ lumine admin economy trace lock --days 3 \
248
288
  lumine admin rescue wordle-audit --days 30 \
249
289
  --reason "Identify recorded Wordle breaks and rescue status" --json
250
290
  lumine admin daily-run start --identity auto --comment-mode off --json
291
+ lumine admin sponsor applications list --status pending --json
292
+ lumine admin sponsor applications review 12 --decision approve \
293
+ --note "Approved for probationary duty" --json
294
+ lumine admin sponsor integrity scan --json
295
+ lumine admin sponsor integrity cases --status open --json
296
+ lumine admin sponsor integrity get 34 --json
297
+ lumine admin sponsor integrity review 34 --decision clear --json
251
298
  lumine admin todo list --json
252
299
  lumine admin todo add --kind experiment --status in_progress \
253
300
  --title "Validate Zero/Ciel cost optimization" \
@@ -355,7 +402,11 @@ with `todo update`; completing a run does not complete its todos. Experiments
355
402
  must meet their stated acceptance criteria—lower AI cost with weaker user
356
403
  responses is not a successful optimization. `daily-run report` summarizes
357
404
  confirmed mutations, completed queue coverage, explicitly recorded escalations,
358
- unfinished todos, and the run brief before the run is completed.
405
+ unfinished todos, sponsor-integrity state, and the run brief before the run is
406
+ completed. A run cannot complete until its bounded sponsor-integrity snapshot
407
+ has been scanned and every selected case is cleared or disqualified. `hold` and
408
+ `flag` deliberately keep the run open for human judgment; scans never suspend,
409
+ revoke, or disqualify a sponsor automatically.
359
410
 
360
411
  Identity inspection, escalation dispositions, AI-bucket maintenance, and
361
412
  approved Notable User additions and todos are private operator bookkeeping and
package/lib/admin.js CHANGED
@@ -814,6 +814,108 @@ export function parseAdminOperation(options) {
814
814
  );
815
815
  }
816
816
 
817
+ if (namespace === "sponsor" || namespace === "sponsors") {
818
+ if (action === "applications") {
819
+ if (!target || target === "list") {
820
+ return readOperation(
821
+ "sponsor.applications.list",
822
+ withQuery("/cli/admin/sponsors/applications", {
823
+ status: options.adminStatus || "pending",
824
+ limit: options.limit,
825
+ }),
826
+ );
827
+ }
828
+ if (target === "review") {
829
+ const applicationId = parseRequiredInteger(
830
+ extra,
831
+ "Sponsor application ID",
832
+ 1,
833
+ );
834
+ const decision = parseChoice(
835
+ options.adminDecision || options.adminStatus,
836
+ "--decision",
837
+ ["approve", "reject"],
838
+ );
839
+ return writeOperation(
840
+ "sponsor.application.review",
841
+ "POST",
842
+ `/cli/admin/sponsors/applications/${applicationId}/review`,
843
+ { decision, note: options.note || undefined },
844
+ );
845
+ }
846
+ }
847
+ if (action === "status" && target === "set") {
848
+ const sponsorUserId = parseRequiredInteger(extra, "Sponsor user ID", 1);
849
+ const status = parseChoice(options.adminStatus, "--status", [
850
+ "probationary",
851
+ "trusted",
852
+ "suspended",
853
+ "revoked",
854
+ ]);
855
+ return writeOperation(
856
+ "sponsor.status.set",
857
+ "PUT",
858
+ `/cli/admin/sponsors/${sponsorUserId}/status`,
859
+ { status, note: options.note || undefined },
860
+ );
861
+ }
862
+ if (action === "integrity") {
863
+ if (target === "status") {
864
+ return readOperation(
865
+ "sponsor.integrity.status",
866
+ "/cli/admin/daily-runs/sponsor-integrity/status",
867
+ );
868
+ }
869
+ if (target === "scan") {
870
+ return writeOperation(
871
+ "sponsor.integrity.scan",
872
+ "POST",
873
+ "/cli/admin/daily-runs/sponsor-integrity/scan",
874
+ {},
875
+ );
876
+ }
877
+ if (target === "cases") {
878
+ return readOperation(
879
+ "sponsor.integrity.cases",
880
+ withQuery("/cli/admin/daily-runs/sponsor-integrity/cases", {
881
+ status: options.adminStatus || "open",
882
+ limit: options.limit,
883
+ }),
884
+ );
885
+ }
886
+ if (target === "get") {
887
+ const caseId = parseRequiredInteger(extra, "Integrity case ID", 1);
888
+ return readOperation(
889
+ "sponsor.integrity.get",
890
+ `/cli/admin/daily-runs/sponsor-integrity/cases/${caseId}`,
891
+ );
892
+ }
893
+ if (target === "review") {
894
+ const caseId = parseRequiredInteger(extra, "Integrity case ID", 1);
895
+ const decision = parseChoice(options.adminDecision, "--decision", [
896
+ "clear",
897
+ "hold",
898
+ "flag",
899
+ "disqualify",
900
+ ]);
901
+ if (decision !== "clear" && !String(options.note || "").trim()) {
902
+ throw cliValidationError(
903
+ "Sponsor-integrity hold, flag, and disqualify decisions require --note <evidence>.",
904
+ );
905
+ }
906
+ return writeOperation(
907
+ "sponsor.integrity.review",
908
+ "POST",
909
+ `/cli/admin/daily-runs/sponsor-integrity/cases/${caseId}/review`,
910
+ { decision, note: options.note || undefined },
911
+ );
912
+ }
913
+ }
914
+ throw cliValidationError(
915
+ "Usage: lumine admin sponsor applications list|review | sponsor status set | sponsor integrity status|scan|cases|get|review.",
916
+ );
917
+ }
918
+
817
919
  if (namespace === "daily-run") {
818
920
  if (action === "start") {
819
921
  return writeOperation(
@@ -1490,7 +1592,7 @@ export function parseAdminOperation(options) {
1490
1592
  }
1491
1593
 
1492
1594
  throw cliValidationError(
1493
- "Usage: lumine admin identity|economy|rescue|daily-run|escalation|todo|recommendations|builds|post|subjects|subject|featured|comment|announcement|chat|news|audit|brief|ai-costs|media-costs|bot-output|notable ...",
1595
+ "Usage: lumine admin identity|economy|rescue|sponsor|daily-run|escalation|todo|recommendations|builds|post|subjects|subject|featured|comment|announcement|chat|news|audit|brief|ai-costs|media-costs|bot-output|notable ...",
1494
1596
  );
1495
1597
  }
1496
1598
 
@@ -2049,6 +2151,16 @@ function parseRequiredInteger(
2049
2151
  return number;
2050
2152
  }
2051
2153
 
2154
+ function parseChoice(value, label, choices) {
2155
+ const normalized = String(value || "")
2156
+ .trim()
2157
+ .toLowerCase();
2158
+ if (!choices.includes(normalized)) {
2159
+ throw cliValidationError(`${label} must be ${choices.join(", ")}.`);
2160
+ }
2161
+ return normalized;
2162
+ }
2163
+
2052
2164
  function defaultDailyRunKey() {
2053
2165
  const parts = new Intl.DateTimeFormat("en-CA", {
2054
2166
  timeZone: "Asia/Bangkok",
@@ -2265,6 +2377,79 @@ function printAdminMonthlyMediaCosts(monthlyMediaCosts) {
2265
2377
 
2266
2378
  function printAdminResult({ operation, result }) {
2267
2379
  const data = result?.data || {};
2380
+ if (Array.isArray(data.applications)) {
2381
+ console.log(`${data.applications.length} sponsor application(s):`);
2382
+ for (const application of data.applications) {
2383
+ console.log(
2384
+ ` #${application.id} ${String(application.status).toUpperCase()} · ${application.username || `user ${application.userId}`} · agreement ${application.agreementVersion || "unknown"}`,
2385
+ );
2386
+ }
2387
+ return;
2388
+ }
2389
+ if (data.application) {
2390
+ console.log(
2391
+ `Sponsor application #${data.application.id}: ${String(data.application.status).toUpperCase()} · ${data.application.username || `user ${data.application.userId}`}`,
2392
+ );
2393
+ return;
2394
+ }
2395
+ if (data.sponsor) {
2396
+ console.log(
2397
+ `Sponsor ${data.sponsor.username || `user ${data.sponsor.userId}`}: ${String(data.sponsor.status).toUpperCase()}.`,
2398
+ );
2399
+ return;
2400
+ }
2401
+ if (data.scan) {
2402
+ const scan = data.scan;
2403
+ console.log(
2404
+ `Sponsor-integrity scan #${scan.id || "not started"}: ${scan.status || "not started"} · ${scan.scannedCount || 0} scanned · ${scan.selectedReviewCount || 0} selected · ${data.openCaseCount || 0} open.`,
2405
+ );
2406
+ if (Array.isArray(data.casesCreated) && data.casesCreated.length > 0) {
2407
+ console.log(
2408
+ `Created ${data.casesCreated.length} review case(s) on this page.`,
2409
+ );
2410
+ }
2411
+ return;
2412
+ }
2413
+ if (Array.isArray(data.cases)) {
2414
+ console.log(`${data.cases.length} sponsor-integrity case(s):`);
2415
+ for (const item of data.cases) {
2416
+ const flags =
2417
+ Array.isArray(item.hardFlags) && item.hardFlags.length
2418
+ ? ` · flags=${item.hardFlags.join(",")}`
2419
+ : "";
2420
+ console.log(
2421
+ ` #${item.id} ${String(item.status).toUpperCase()} · job #${item.jobId} · ${item.sponsorUsername || `sponsor ${item.sponsorUserId}`}${flags}`,
2422
+ );
2423
+ }
2424
+ return;
2425
+ }
2426
+ if (data.case) {
2427
+ const item = data.case;
2428
+ console.log(
2429
+ `Sponsor-integrity case #${item.id}: ${String(item.status).toUpperCase()} · job #${item.job?.id} · ${item.sponsorUsername || `sponsor ${item.sponsorUserId}`}.`,
2430
+ );
2431
+ const relays = Array.isArray(item.relays) && item.relays.length > 0
2432
+ ? item.relays
2433
+ : item.relay
2434
+ ? [item.relay]
2435
+ : [];
2436
+ console.log(
2437
+ `Relays: ${relays.map((relay) => `${relay.kind || "initial_request"}: ${relay.summary || "(missing)"}`).join(" | ") || "(missing)"}`,
2438
+ );
2439
+ console.log(
2440
+ `Provenance: ${(item.agents || []).map((agent) => `${agent.role}:${agent.provider}:${agent.resolvedModel || "unresolved"}:${agent.resolvedEffort || "unresolved"}`).join(" · ") || "missing"}`,
2441
+ );
2442
+ console.log(
2443
+ `Artifact files: ${(item.artifactFiles || []).map((file) => file.path).join(", ") || "none"}`,
2444
+ );
2445
+ return;
2446
+ }
2447
+ if (data.caseId) {
2448
+ console.log(
2449
+ `Sponsor-integrity case #${data.caseId}: ${data.decision}${data.scanCompleted ? "; scan complete" : ""}.`,
2450
+ );
2451
+ return;
2452
+ }
2268
2453
  if (data.review) {
2269
2454
  console.log(
2270
2455
  `Confirmed Build #${data.review.buildId} runtime at published artifact #${data.review.publishedArtifactVersionId}.`,
@@ -2313,6 +2498,11 @@ function printAdminResult({ operation, result }) {
2313
2498
  );
2314
2499
  }
2315
2500
  printTodoItems(report.carryoverTodos?.items || [], "Unfinished work");
2501
+ if (report.sponsorIntegrity) {
2502
+ console.log(
2503
+ `Sponsor integrity: ${report.sponsorIntegrity.scan?.status || "not started"}; ${report.sponsorIntegrity.cases?.open || 0} open case(s); ${report.sponsorIntegrity.pendingApplications || 0} pending application(s).`,
2504
+ );
2505
+ }
2316
2506
  const surfaces = report.brief?.engagementPulse?.surfaces;
2317
2507
  if (surfaces && typeof surfaces === "object") {
2318
2508
  const deltas = Object.entries(surfaces)
@@ -79,8 +79,12 @@ export async function runClaudeCodeAgentPass({
79
79
  args,
80
80
  cwd: isolationDir,
81
81
  streamJson: true,
82
+ signal: options.agentAbortSignal,
82
83
  });
83
- return { finalText: result.finalText };
84
+ return {
85
+ finalText: result.finalText,
86
+ provenance: result.provenance,
87
+ };
84
88
  }
85
89
 
86
90
  export async function reviewClaudeCodeAgentLoop({
@@ -96,6 +100,7 @@ export async function reviewClaudeCodeAgentLoop({
96
100
  binary,
97
101
  cwd: isolationDir,
98
102
  streamJson: false,
103
+ signal: options.agentAbortSignal,
99
104
  args: [
100
105
  "-p",
101
106
  "--output-format",
@@ -129,16 +134,22 @@ export async function reviewClaudeCodeAgentLoop({
129
134
  }
130
135
  }
131
136
 
132
- async function runClaudeProcess({ binary, args, cwd, streamJson }) {
137
+ async function runClaudeProcess({ binary, args, cwd, streamJson, signal }) {
133
138
  const child = spawn(binary, args, {
134
139
  cwd,
135
140
  stdio: ["ignore", "pipe", "pipe"],
136
141
  env: createSubscriptionAgentEnvironment(),
142
+ ...(signal ? { signal } : {}),
137
143
  });
138
144
  child.stderr.on("data", (chunk) => process.stderr.write(chunk));
139
145
  let finalText = "";
140
146
  let structuredOutput = null;
141
147
  let rawOutput = "";
148
+ let providerModel = null;
149
+ let providerEffort = null;
150
+ let providerUsage = {};
151
+ let providerServiceTier = null;
152
+ let providerRuntimeVersion = null;
142
153
 
143
154
  if (streamJson) {
144
155
  const lines = readline.createInterface({
@@ -155,13 +166,53 @@ async function runClaudeProcess({ binary, args, cwd, streamJson }) {
155
166
  return;
156
167
  }
157
168
  if (event?.type === "assistant") {
169
+ providerModel =
170
+ firstNonEmptyString(event.message?.model, providerModel) || null;
171
+ providerEffort =
172
+ firstNonEmptyString(
173
+ event.message?.effort,
174
+ event.message?.reasoning_effort,
175
+ providerEffort,
176
+ ) || null;
177
+ providerUsage = mergeUsageEvidence(providerUsage, event.message?.usage);
158
178
  const texts = (event.message?.content || [])
159
179
  .filter((block) => block?.type === "text")
160
180
  .map((block) => String(block.text || "").trim())
161
181
  .filter(Boolean);
162
182
  if (texts.length > 0) finalText = texts.join("\n\n");
163
183
  }
184
+ if (event?.type === "system") {
185
+ providerModel = firstNonEmptyString(event.model, providerModel) || null;
186
+ providerRuntimeVersion =
187
+ firstNonEmptyString(
188
+ event.claude_code_version,
189
+ event.claudeCodeVersion,
190
+ event.version,
191
+ providerRuntimeVersion,
192
+ ) || null;
193
+ }
164
194
  if (event?.type === "result") {
195
+ providerModel = firstNonEmptyString(event.model, providerModel) || null;
196
+ providerEffort =
197
+ firstNonEmptyString(
198
+ event.effort,
199
+ event.reasoning_effort,
200
+ providerEffort,
201
+ ) || null;
202
+ providerServiceTier =
203
+ firstNonEmptyString(
204
+ event.service_tier,
205
+ event.serviceTier,
206
+ providerServiceTier,
207
+ ) || null;
208
+ providerUsage = mergeUsageEvidence(providerUsage, event.usage);
209
+ providerRuntimeVersion =
210
+ firstNonEmptyString(
211
+ event.claude_code_version,
212
+ event.claudeCodeVersion,
213
+ event.version,
214
+ providerRuntimeVersion,
215
+ ) || null;
165
216
  if (typeof event.result === "string" && event.result.trim()) {
166
217
  finalText = event.result.trim();
167
218
  }
@@ -200,8 +251,58 @@ async function runClaudeProcess({ binary, args, cwd, streamJson }) {
200
251
  }
201
252
  finalText = String(parsed?.result || "").trim();
202
253
  structuredOutput = parsed?.structured_output || null;
254
+ providerModel = firstNonEmptyString(parsed?.model) || null;
255
+ providerEffort =
256
+ firstNonEmptyString(parsed?.effort, parsed?.reasoning_effort) || null;
257
+ providerServiceTier =
258
+ firstNonEmptyString(parsed?.service_tier, parsed?.serviceTier) || null;
259
+ providerRuntimeVersion =
260
+ firstNonEmptyString(
261
+ parsed?.claude_code_version,
262
+ parsed?.claudeCodeVersion,
263
+ parsed?.version,
264
+ ) || null;
265
+ providerUsage = mergeUsageEvidence(providerUsage, parsed?.usage);
203
266
  }
204
- return { finalText, structuredOutput };
267
+ return {
268
+ finalText,
269
+ structuredOutput,
270
+ provenance: {
271
+ resolvedModel: providerModel,
272
+ resolvedEffort: providerEffort,
273
+ resolvedServiceTier: providerServiceTier,
274
+ runtimeVersion: providerRuntimeVersion,
275
+ evidenceTier:
276
+ providerModel || providerEffort || providerServiceTier
277
+ ? "provider_reported"
278
+ : "requested_only",
279
+ usage: providerUsage,
280
+ },
281
+ };
282
+ }
283
+
284
+ function firstNonEmptyString(...values) {
285
+ for (const value of values) {
286
+ const normalized = String(value || "").trim();
287
+ if (normalized) return normalized.slice(0, 160);
288
+ }
289
+ return null;
290
+ }
291
+
292
+ function mergeUsageEvidence(current, value) {
293
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
294
+ return current;
295
+ }
296
+ return {
297
+ ...current,
298
+ ...Object.fromEntries(
299
+ Object.entries(value)
300
+ .filter(([, amount]) =>
301
+ ["number", "string", "boolean"].includes(typeof amount),
302
+ )
303
+ .slice(0, 30),
304
+ ),
305
+ };
205
306
  }
206
307
 
207
308
  function normalizeProviderLaunchError(error, binary) {