@lumi.ai/runner 0.3.1 → 0.3.3

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.
Files changed (2) hide show
  1. package/dist/cli.js +83 -72
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -324,6 +324,40 @@ function memoryBlock(memory) {
324
324
  ` + lines;
325
325
  }
326
326
 
327
+ // ../shared/dist/task.js
328
+ var TASK_STATUS_CATEGORIES = [
329
+ "backlog",
330
+ "todo",
331
+ "in_progress",
332
+ "waiting_approval",
333
+ "review",
334
+ "done",
335
+ "failed"
336
+ ];
337
+ var TASK_STATUS_CATEGORY_META = {
338
+ backlog: { label: "Backlog", meaning: "Parked \u2014 not ready to be worked; agents ignore it." },
339
+ todo: { label: "To do", meaning: "Ready to start \u2014 assigning an agent here starts a run." },
340
+ in_progress: { label: "In progress", meaning: "Actively being worked on." },
341
+ waiting_approval: {
342
+ label: "Waiting approval",
343
+ meaning: "Blocked on a captain decision before work can continue."
344
+ },
345
+ review: { label: "Review", meaning: "Work finished but a human must check it." },
346
+ done: { label: "Done", meaning: "Complete \u2014 nothing pending." },
347
+ failed: { label: "Failed", meaning: "Ended unsuccessfully; needs human attention." }
348
+ };
349
+ var DEFAULT_TASK_STATUSES = TASK_STATUS_CATEGORIES.map((category) => ({ id: category, label: TASK_STATUS_CATEGORY_META[category].label, category }));
350
+ function shipTaskStatuses(ship2) {
351
+ const list = ship2?.settings?.taskStatuses;
352
+ return Array.isArray(list) && list.length > 0 ? list : DEFAULT_TASK_STATUSES;
353
+ }
354
+ function statusById(statuses, statusId) {
355
+ return statuses.find((s) => s.id === statusId);
356
+ }
357
+ function firstStatusIn(statuses, category) {
358
+ return statuses.find((s) => s.category === category);
359
+ }
360
+
327
361
  // ../shared/dist/riskyAction.js
328
362
  var RISKY_ACTIONS = [
329
363
  {
@@ -373,10 +407,11 @@ function autonomyRule(agent) {
373
407
  return "You have full autonomy (level 3) within your tool list: no action needs approval. Use it carefully \u2014 nothing will stop you.";
374
408
  }
375
409
  if (agent.autonomy <= 1) {
376
- return "You are at autonomy level 1 \u2014 DRAFTS ONLY. Produce comments and drafts; do not mark work as done or ready for review, do not create or reassign tasks, and do not push to GitHub. When you need any of that, call approval_request with a short summary and STOP \u2014 a captain decides, and you will be started again with their answer.";
410
+ return "You are at autonomy level 1 \u2014 DRAFTS ONLY. Produce comments and drafts; do not mark work as done or ready for review, do not create or reassign tasks, and do not push to GitHub. When you need any of that, call approval_request with a short summary and STOP \u2014 somebody decides, and you will be started again with their answer. " + ASK_GUIDANCE;
377
411
  }
378
- return `You are at autonomy level 2. Work freely, except for these actions, which need a captain: ${named.length ? named.join(", ") : "(none configured)"}. Before any of them, call approval_request with a short summary and STOP \u2014 you will be started again with the captain's answer.`;
412
+ return `You are at autonomy level 2. Work freely, except for these actions, which need approval: ${named.length ? named.join(", ") : "(none configured)"}. Before any of them, call approval_request with a short summary and STOP \u2014 you will be started again with the answer. ` + ASK_GUIDANCE;
379
413
  }
414
+ var ASK_GUIDANCE = "If the task says who should approve \u2014 in its description, or because a particular person owns that area \u2014 name them in the request's `ask` so it reaches them rather than everyone.";
380
415
 
381
416
  // ../shared/dist/runner.js
382
417
  var RUNNER_OFFLINE_AFTER_MS = 2 * 60 * 1e3;
@@ -398,40 +433,6 @@ var SECRET_DOCS = {
398
433
  runnerKey: "runnerKey"
399
434
  };
400
435
 
401
- // ../shared/dist/task.js
402
- var TASK_STATUS_CATEGORIES = [
403
- "backlog",
404
- "todo",
405
- "in_progress",
406
- "waiting_approval",
407
- "review",
408
- "done",
409
- "failed"
410
- ];
411
- var TASK_STATUS_CATEGORY_META = {
412
- backlog: { label: "Backlog", meaning: "Parked \u2014 not ready to be worked; agents ignore it." },
413
- todo: { label: "To do", meaning: "Ready to start \u2014 assigning an agent here starts a run." },
414
- in_progress: { label: "In progress", meaning: "Actively being worked on." },
415
- waiting_approval: {
416
- label: "Waiting approval",
417
- meaning: "Blocked on a captain decision before work can continue."
418
- },
419
- review: { label: "Review", meaning: "Work finished but a human must check it." },
420
- done: { label: "Done", meaning: "Complete \u2014 nothing pending." },
421
- failed: { label: "Failed", meaning: "Ended unsuccessfully; needs human attention." }
422
- };
423
- var DEFAULT_TASK_STATUSES = TASK_STATUS_CATEGORIES.map((category) => ({ id: category, label: TASK_STATUS_CATEGORY_META[category].label, category }));
424
- function shipTaskStatuses(ship2) {
425
- const list = ship2?.settings?.taskStatuses;
426
- return Array.isArray(list) && list.length > 0 ? list : DEFAULT_TASK_STATUSES;
427
- }
428
- function statusById(statuses, statusId) {
429
- return statuses.find((s) => s.id === statusId);
430
- }
431
- function firstStatusIn(statuses, category) {
432
- return statuses.find((s) => s.category === category);
433
- }
434
-
435
436
  // ../shared/dist/ship.js
436
437
  var DEFAULT_SHIP_SETTINGS = {
437
438
  maxChainDepth: 3,
@@ -532,7 +533,7 @@ function mcpUrl(config2) {
532
533
  }
533
534
 
534
535
  // src/version.ts
535
- var RUNNER_VERSION = true ? "0.3.1" : "0.0.0-dev";
536
+ var RUNNER_VERSION = true ? "0.3.3" : "0.0.0-dev";
536
537
 
537
538
  // src/auth.ts
538
539
  import { signInWithCustomToken } from "firebase/auth";
@@ -2639,10 +2640,10 @@ function serviceStatus() {
2639
2640
  };
2640
2641
  }
2641
2642
  if (process.platform === "win32") {
2642
- const query6 = run("schtasks", ["/Query", "/TN", WINDOWS_TASK]);
2643
- if (!query6.ok) return { state: "not-installed", detail: "No scheduled task installed." };
2643
+ const query5 = run("schtasks", ["/Query", "/TN", WINDOWS_TASK]);
2644
+ if (!query5.ok) return { state: "not-installed", detail: "No scheduled task installed." };
2644
2645
  return {
2645
- state: /\bRunning\b/i.test(query6.out) ? "running" : "installed",
2646
+ state: /\bRunning\b/i.test(query5.out) ? "running" : "installed",
2646
2647
  detail: "Scheduled task installed (runs at logon).",
2647
2648
  unitPath: WINDOWS_TASK
2648
2649
  };
@@ -2751,21 +2752,6 @@ async function openShipSession(shipId) {
2751
2752
  const user = await signInToShip(fb, config2, shipId);
2752
2753
  return { shipId, config: config2, fb, user };
2753
2754
  }
2754
- async function openSession() {
2755
- const config2 = requireConfig();
2756
- if (config2.ships.length === 0) {
2757
- throw new AuthError("No Ships assigned. Run: lumi-runner ship add <shipId>");
2758
- }
2759
- let last;
2760
- for (const shipId of config2.ships) {
2761
- try {
2762
- return await openShipSession(shipId);
2763
- } catch (e) {
2764
- last = e;
2765
- }
2766
- }
2767
- throw last instanceof Error ? last : new AuthError("Could not sign in to any Ship.");
2768
- }
2769
2755
 
2770
2756
  // src/cli/commands/doctor.ts
2771
2757
  var ok = (id, label, detail) => ({ id, label, level: "ok", detail });
@@ -3280,17 +3266,19 @@ async function runUninstall(options) {
3280
3266
  }
3281
3267
 
3282
3268
  // src/cli/commands/ship.ts
3283
- import { collectionGroup, doc as doc9, getDoc as getDoc7, getDocs as getDocs5, query as query4, where as where4 } from "firebase/firestore";
3269
+ import { doc as doc9, getDoc as getDoc7 } from "firebase/firestore";
3284
3270
  async function listMyShips() {
3285
- const { fb, user } = await openSession();
3286
- const memberships = await getDocs5(
3287
- query4(collectionGroup(fb.db, COLLECTIONS.members), where4("uid", "==", user.uid))
3288
- );
3289
- const shipIds = memberships.docs.map((d) => d.ref.parent.parent?.id).filter((id) => !!id);
3271
+ const config2 = requireConfig();
3272
+ const ids = Object.keys(config2.shipKeys ?? {});
3290
3273
  const ships = await Promise.all(
3291
- shipIds.map(async (id) => {
3292
- const snap = await getDoc7(doc9(fb.db, COLLECTIONS.ships, id));
3293
- return snap.exists() ? { id: snap.id, ...snap.data() } : null;
3274
+ ids.map(async (id) => {
3275
+ try {
3276
+ const { fb } = await openShipSession(id);
3277
+ const snap = await getDoc7(doc9(fb.db, COLLECTIONS.ships, id));
3278
+ return { id, ...snap.data() };
3279
+ } catch {
3280
+ return null;
3281
+ }
3294
3282
  })
3295
3283
  );
3296
3284
  return ships.filter((s) => s !== null).sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
@@ -3298,7 +3286,9 @@ async function listMyShips() {
3298
3286
  async function pickShips(current) {
3299
3287
  const ships = await listMyShips();
3300
3288
  if (ships.length === 0) {
3301
- throw new CliError("You are not a member of any Ship yet \u2014 create one in the web app first.");
3289
+ throw new CliError(
3290
+ "This machine holds no Ship keys. Run `lumi-runner login` and have a captain approve it \u2014 a daemon can only serve Ships it was approved for."
3291
+ );
3302
3292
  }
3303
3293
  return promptMultiSelect({
3304
3294
  message: "Which Ships should this machine serve?",
@@ -3408,9 +3398,9 @@ import {
3408
3398
  doc as doc10,
3409
3399
  getCountFromServer as getCountFromServer2,
3410
3400
  getDoc as getDoc8,
3411
- getDocs as getDocs6,
3412
- query as query5,
3413
- where as where5
3401
+ getDocs as getDocs5,
3402
+ query as query4,
3403
+ where as where4
3414
3404
  } from "firebase/firestore";
3415
3405
  async function runStatus() {
3416
3406
  const config2 = loadConfig();
@@ -3424,16 +3414,33 @@ async function runStatus() {
3424
3414
  }
3425
3415
  const progress = spinner2();
3426
3416
  progress.start("Reading Ship state\u2026");
3427
- const { fb } = await openSession();
3428
3417
  const ships = [];
3429
3418
  for (const shipId of config2.ships) {
3419
+ let fb;
3420
+ try {
3421
+ ({ fb } = await openShipSession(shipId));
3422
+ } catch {
3423
+ ships.push({
3424
+ shipId,
3425
+ enrolled: false,
3426
+ approved: false,
3427
+ online: false,
3428
+ lastSeenAt: null,
3429
+ currentJob: null,
3430
+ queued: 0,
3431
+ today: { ...EMPTY_USAGE_TOTALS },
3432
+ engineLimits: [],
3433
+ unreachable: true
3434
+ });
3435
+ continue;
3436
+ }
3430
3437
  const shipRef = doc10(fb.db, COLLECTIONS.ships, shipId);
3431
3438
  const mirrorSnap = await getDoc8(doc10(shipRef, COLLECTIONS.runners, config2.runnerId));
3432
3439
  const mirror = mirrorSnap.data();
3433
3440
  let queued = 0;
3434
3441
  try {
3435
3442
  const counted = await getCountFromServer2(
3436
- query5(collection7(shipRef, COLLECTIONS.jobs), where5("status", "==", "queued"))
3443
+ query4(collection7(shipRef, COLLECTIONS.jobs), where4("status", "==", "queued"))
3437
3444
  );
3438
3445
  queued = counted.data().count;
3439
3446
  } catch {
@@ -3442,7 +3449,7 @@ async function runStatus() {
3442
3449
  const usageSnap = await getDoc8(doc10(shipRef, COLLECTIONS.usageDaily, utcDay(Date.now())));
3443
3450
  const today = { ...EMPTY_USAGE_TOTALS, ...usageSnap.data()?.totals ?? {} };
3444
3451
  const now = Date.now();
3445
- const limitsSnap = await getDocs6(collection7(shipRef, COLLECTIONS.engineLimits));
3452
+ const limitsSnap = await getDocs5(collection7(shipRef, COLLECTIONS.engineLimits));
3446
3453
  const engineLimits = limitsSnap.docs.map((d) => ({ id: d.id, ...d.data() })).filter((l) => isEngineLimited(l, now));
3447
3454
  ships.push({
3448
3455
  shipId,
@@ -3479,8 +3486,12 @@ async function runStatus() {
3479
3486
  return 0;
3480
3487
  }
3481
3488
  for (const ship2 of ships) {
3482
- const state = !ship2.enrolled ? pc.dim("not enrolled") : !ship2.approved ? pc.yellow("awaiting approval") : ship2.online ? pc.green("online") : pc.red("offline");
3489
+ const state = ship2.unreachable ? pc.red("no access") : !ship2.enrolled ? pc.dim("not enrolled") : !ship2.approved ? pc.yellow("awaiting approval") : ship2.online ? pc.green("online") : pc.red("offline");
3483
3490
  say.line(` ${pc.bold(ship2.shipId)} ${state}`);
3491
+ if (ship2.unreachable) {
3492
+ say.line(` ${pc.dim("this machine\u2019s key for that Ship was revoked \u2014 re-approve it on the Daemons page")}`);
3493
+ continue;
3494
+ }
3484
3495
  if (ship2.lastSeenAt) say.line(` last seen ${new Date(ship2.lastSeenAt).toLocaleString()}`);
3485
3496
  say.line(
3486
3497
  ship2.currentJob ? ` running job ${ship2.currentJob.jobId} on task ${ship2.currentJob.taskId} (agent ${ship2.currentJob.agentId})` : " running nothing"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumi.ai/runner",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "type": "module",
5
5
  "description": "Lumi Crew runner daemon — claims jobs from your Ships and executes them as headless Claude sessions on your own machine.",
6
6
  "//name": "The ONLY package in this monorepo published to the public registry, so it is the one that does not follow the internal @lumi/crew-* convention: `@lumi` is not a scope we own, `@lumi.ai` is (the npm org). The workspace DIRECTORY stays packages/crew/runner — renaming the package is not renaming the folder.",