@lumi.ai/runner 0.3.2 → 0.3.4

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 +95 -41
  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.2" : "0.0.0-dev";
536
+ var RUNNER_VERSION = true ? "0.3.4" : "0.0.0-dev";
536
537
 
537
538
  // src/auth.ts
538
539
  import { signInWithCustomToken } from "firebase/auth";
@@ -579,7 +580,39 @@ async function request(baseUrl, name, data, headers) {
579
580
 
580
581
  // src/auth.ts
581
582
  var AuthError = class extends Error {
583
+ constructor(message, gone = false) {
584
+ super(message);
585
+ this.gone = gone;
586
+ }
587
+ gone;
582
588
  };
589
+ function orphanShipKeys(config2) {
590
+ return Object.keys(config2.shipKeys ?? {}).filter((id) => !config2.ships.includes(id));
591
+ }
592
+ async function probeShipKey(config2, shipId) {
593
+ const key = config2.shipKeys?.[shipId];
594
+ if (!key) return null;
595
+ try {
596
+ await callPublicFunction(functionsBaseUrl(config2), "exchangeRunnerKey", { key });
597
+ return null;
598
+ } catch (e) {
599
+ return {
600
+ shipId,
601
+ message: `Ship ${shipId}: ${e instanceof Error ? e.message : String(e)}`,
602
+ gone: e instanceof CallableError && e.status === "PERMISSION_DENIED"
603
+ };
604
+ }
605
+ }
606
+ function pruneDeadShips(config2, failures) {
607
+ const dropped = failures.filter((f) => f.gone).map((f) => f.shipId);
608
+ if (dropped.length === 0) return { config: config2, dropped };
609
+ const shipKeys = { ...config2.shipKeys ?? {} };
610
+ for (const shipId of dropped) delete shipKeys[shipId];
611
+ return {
612
+ config: { ...config2, shipKeys, ships: config2.ships.filter((id) => !dropped.includes(id)) },
613
+ dropped
614
+ };
615
+ }
583
616
  async function signInToShip(fb, config2, shipId) {
584
617
  const key = config2.shipKeys?.[shipId];
585
618
  if (!key) {
@@ -592,7 +625,8 @@ async function signInToShip(fb, config2, shipId) {
592
625
  session = await callPublicFunction(functionsBaseUrl(config2), "exchangeRunnerKey", { key });
593
626
  } catch (e) {
594
627
  throw new AuthError(
595
- `Ship ${shipId}: ${e instanceof Error ? e.message : String(e)}`
628
+ `Ship ${shipId}: ${e instanceof Error ? e.message : String(e)}`,
629
+ e instanceof CallableError && e.status === "PERMISSION_DENIED"
596
630
  );
597
631
  }
598
632
  const cred = await signInWithCustomToken(fb.auth, session.customToken);
@@ -607,7 +641,11 @@ async function openShipSessions(config2, build) {
607
641
  const user = await signInToShip(fb, config2, shipId);
608
642
  sessions.push({ shipId, fb, user });
609
643
  } catch (e) {
610
- failures.push({ shipId, message: e instanceof Error ? e.message : String(e) });
644
+ failures.push({
645
+ shipId,
646
+ message: e instanceof Error ? e.message : String(e),
647
+ gone: e instanceof AuthError && e.gone
648
+ });
611
649
  }
612
650
  }
613
651
  return { sessions, failures };
@@ -1753,7 +1791,7 @@ var MAX_ATTEMPTS = 2;
1753
1791
  var SHUTDOWN_GRACE_MS = 1e4;
1754
1792
  async function startDaemon() {
1755
1793
  const moved = migrateLegacyDir();
1756
- const config2 = requireConfig();
1794
+ let config2 = requireConfig();
1757
1795
  if (config2.ships.length === 0) {
1758
1796
  console.error("No Ships assigned. Run: lumi-runner ship add <shipId>");
1759
1797
  process.exit(1);
@@ -1763,6 +1801,15 @@ async function startDaemon() {
1763
1801
  (shipId) => initFirebase(config2, `ship-${shipId}`)
1764
1802
  );
1765
1803
  for (const f of failures) console.error(f.message);
1804
+ const orphanFailures = (await Promise.all(orphanShipKeys(config2).map((shipId) => probeShipKey(config2, shipId)))).filter((f) => f !== null);
1805
+ const pruned = pruneDeadShips(config2, [...failures, ...orphanFailures]);
1806
+ if (pruned.dropped.length > 0) {
1807
+ saveConfig(pruned.config);
1808
+ config2 = pruned.config;
1809
+ console.log(
1810
+ `Forgot ${pruned.dropped.length} Ship(s) this machine can no longer open: ${pruned.dropped.join(", ")}. Deleted, or this machine was revoked \u2014 run \`lumi-runner login\` if that was not expected.`
1811
+ );
1812
+ }
1766
1813
  if (sessions.length === 0) {
1767
1814
  console.error("No Ship could be signed in to. Run: lumi-runner login");
1768
1815
  process.exit(1);
@@ -3146,6 +3193,9 @@ function report2(result) {
3146
3193
  `Awaiting captain approval on: ${result.pendingShips.join(", ")} \u2014 a captain approves this machine on the Ship's Daemons page.`
3147
3194
  );
3148
3195
  }
3196
+ if (serviceStatus().state === "running") {
3197
+ say.info("Restart the daemon for this to take effect: `lumi-runner service restart`.");
3198
+ }
3149
3199
  return 0;
3150
3200
  }
3151
3201
 
@@ -3376,7 +3426,8 @@ async function runSetup(options) {
3376
3426
  }
3377
3427
  say.step("Checking this machine\u2026");
3378
3428
  const doctorExit = await runDoctor();
3379
- if (serviceStatus().state === "not-installed") {
3429
+ const service2 = serviceStatus().state;
3430
+ if (service2 === "not-installed") {
3380
3431
  const install = await promptConfirm({
3381
3432
  message: "Start the daemon automatically whenever this machine is on?",
3382
3433
  initialValue: true,
@@ -3384,6 +3435,9 @@ async function runSetup(options) {
3384
3435
  });
3385
3436
  if (install) await runServiceInstall();
3386
3437
  else say.info("Skipped. Run `lumi-runner start` manually, or `lumi-runner service install` later.");
3438
+ } else if (service2 === "running" || service2 === "installed") {
3439
+ say.step("Restarting the daemon so it picks up this configuration\u2026");
3440
+ await runServiceRestart();
3387
3441
  }
3388
3442
  say.outro(
3389
3443
  doctorExit === 0 ? "Ready. This machine will claim jobs for its Ships." : "Setup finished, but some checks failed \u2014 fix those and re-run `lumi-runner doctor`."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumi.ai/runner",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
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.",