@loompad/cli 1.2.0 → 1.2.2
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/CHANGELOG.md +72 -0
- package/README.md +13 -7
- package/dist/adapters/antigravity-cli.js +7 -3
- package/dist/adapters/antigravity-cli.js.map +1 -1
- package/dist/adapters/opencode.d.ts +6 -0
- package/dist/adapters/opencode.js +27 -2
- package/dist/adapters/opencode.js.map +1 -1
- package/dist/cli/doctor.d.ts +9 -1
- package/dist/cli/doctor.js +48 -5
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.js +144 -20
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/ui.js +3 -2
- package/dist/cli/ui.js.map +1 -1
- package/dist/core/brain-index.d.ts +0 -18
- package/dist/core/brain-index.js +10 -2
- package/dist/core/brain-index.js.map +1 -1
- package/dist/core/council.d.ts +3 -3
- package/dist/core/council.js +6 -4
- package/dist/core/council.js.map +1 -1
- package/dist/core/crew-protocol.js +4 -1
- package/dist/core/crew-protocol.js.map +1 -1
- package/dist/core/crew.d.ts +23 -1
- package/dist/core/crew.js +80 -9
- package/dist/core/crew.js.map +1 -1
- package/dist/core/digest.d.ts +2 -0
- package/dist/core/digest.js +9 -0
- package/dist/core/digest.js.map +1 -1
- package/dist/core/loops.js +22 -2
- package/dist/core/loops.js.map +1 -1
- package/dist/core/orchestra.d.ts +16 -0
- package/dist/core/orchestra.js +45 -2
- package/dist/core/orchestra.js.map +1 -1
- package/dist/core/prices.d.ts +27 -0
- package/dist/core/prices.js +38 -0
- package/dist/core/prices.js.map +1 -1
- package/dist/core/proof.d.ts +11 -1
- package/dist/core/proof.js +56 -7
- package/dist/core/proof.js.map +1 -1
- package/dist/core/registry.d.ts +7 -0
- package/dist/core/registry.js.map +1 -1
- package/dist/core/replay.js +3 -1
- package/dist/core/replay.js.map +1 -1
- package/dist/core/routes.js +14 -0
- package/dist/core/routes.js.map +1 -1
- package/dist/core/setup.d.ts +14 -0
- package/dist/core/setup.js +22 -6
- package/dist/core/setup.js.map +1 -1
- package/dist/core/shield.js +7 -3
- package/dist/core/shield.js.map +1 -1
- package/dist/core/shift.d.ts +2 -0
- package/dist/core/shift.js +14 -2
- package/dist/core/shift.js.map +1 -1
- package/dist/daemon/auth.d.ts +7 -1
- package/dist/daemon/auth.js +19 -3
- package/dist/daemon/auth.js.map +1 -1
- package/dist/daemon/client.d.ts +29 -1
- package/dist/daemon/client.js +37 -1
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/council.d.ts +8 -0
- package/dist/daemon/council.js +121 -36
- package/dist/daemon/council.js.map +1 -1
- package/dist/daemon/proof.js +11 -7
- package/dist/daemon/proof.js.map +1 -1
- package/dist/daemon/review.d.ts +10 -0
- package/dist/daemon/review.js +41 -3
- package/dist/daemon/review.js.map +1 -1
- package/dist/daemon/routes/brain.js +4 -0
- package/dist/daemon/routes/brain.js.map +1 -1
- package/dist/daemon/routes/council.js +9 -0
- package/dist/daemon/routes/council.js.map +1 -1
- package/dist/daemon/routes/crews.js +4 -0
- package/dist/daemon/routes/crews.js.map +1 -1
- package/dist/daemon/routes/messages.js +8 -2
- package/dist/daemon/routes/messages.js.map +1 -1
- package/dist/daemon/routes/pairing.js +6 -1
- package/dist/daemon/routes/pairing.js.map +1 -1
- package/dist/daemon/routes/setup.js +9 -1
- package/dist/daemon/routes/setup.js.map +1 -1
- package/dist/daemon/routes/team.js +2 -0
- package/dist/daemon/routes/team.js.map +1 -1
- package/dist/daemon/runtime/accounting.js +7 -3
- package/dist/daemon/runtime/accounting.js.map +1 -1
- package/dist/daemon/runtime/queue.d.ts +9 -0
- package/dist/daemon/runtime/queue.js +27 -1
- package/dist/daemon/runtime/queue.js.map +1 -1
- package/dist/daemon/runtime/relay.d.ts +2 -0
- package/dist/daemon/runtime/relay.js +7 -0
- package/dist/daemon/runtime/relay.js.map +1 -1
- package/dist/daemon/runtime/turns.d.ts +4 -0
- package/dist/daemon/runtime/turns.js +29 -0
- package/dist/daemon/runtime/turns.js.map +1 -1
- package/dist/daemon/runtime-support.d.ts +5 -1
- package/dist/daemon/runtime-support.js +9 -3
- package/dist/daemon/runtime-support.js.map +1 -1
- package/dist/daemon/runtime.d.ts +46 -2
- package/dist/daemon/runtime.js +158 -6
- package/dist/daemon/runtime.js.map +1 -1
- package/dist/daemon/server.js +4 -0
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/shift.d.ts +6 -0
- package/dist/daemon/shift.js +11 -2
- package/dist/daemon/shift.js.map +1 -1
- package/dist/daemon/team.d.ts +6 -0
- package/dist/daemon/team.js +15 -0
- package/dist/daemon/team.js.map +1 -1
- package/dist/providers/agent.js +3 -0
- package/dist/providers/agent.js.map +1 -1
- package/dist/types.d.ts +12 -0
- package/dist/types.js.map +1 -1
- package/dist/web/app.css +72 -0
- package/dist/web/app.js +9834 -9635
- package/dist/web/app.js.map +4 -4
- package/docs/release-notes/v1.2.1.md +40 -0
- package/docs/release-notes/v1.2.2.md +39 -0
- package/package.json +1 -1
package/dist/daemon/runtime.js
CHANGED
|
@@ -55,9 +55,11 @@ import { ContinuityEngine } from "../core/continuity/engine.js";
|
|
|
55
55
|
import { ContinuityError } from "../core/continuity/contracts.js";
|
|
56
56
|
import { HarnessMonitor, isNativeKind } from "../core/continuity/capabilities.js";
|
|
57
57
|
import { ProviderAgent } from "../providers/agent.js";
|
|
58
|
-
import { READ_ONLY_RULE } from "../core/review.js";
|
|
58
|
+
import { READ_ONLY_RULE, decisionText } from "../core/review.js";
|
|
59
|
+
import { parseVerdicts } from "../core/council.js";
|
|
59
60
|
import { ReviewRunner } from "./review.js";
|
|
60
61
|
import { CouncilRunner } from "./council.js";
|
|
62
|
+
import { coldResume, observedInputRate, priceBook } from "../core/prices.js";
|
|
61
63
|
import { LiveDeltaThrottle } from "../providers/live.js";
|
|
62
64
|
import { AdapterBase } from "../adapters/base.js";
|
|
63
65
|
export { BudgetExceededError, CLOCK_TICK_MS, LOOM_ASK_TIMEOUT_MESSAGE, LOOM_ASK_TIMEOUT_MS, LoomAskTimeoutError, QuarantinedError, activityLine, planModeBriefing, relativeToProject, withLoomAskTimeout } from './runtime-support.js';
|
|
@@ -304,6 +306,7 @@ export class ProjectRuntime {
|
|
|
304
306
|
projectId: info.id,
|
|
305
307
|
projectName: info.name,
|
|
306
308
|
projectDir: info.dir,
|
|
309
|
+
worktreeDir: () => this.config.orchestra?.worktreeDir,
|
|
307
310
|
roster: () => this.config.agents.filter((a) => a.enabled !== false && tierForKind(a.kind) === "adapter" && !isWithdrawnKind(a.kind)),
|
|
308
311
|
installedKinds: () => this.installedKinds,
|
|
309
312
|
makeAgent: (cfg, dir) => {
|
|
@@ -355,6 +358,7 @@ export class ProjectRuntime {
|
|
|
355
358
|
config: () => this.config,
|
|
356
359
|
busy: (id) => { const a = this.agents.get(id); return Boolean(a && isAdapter(a) && a.busy()) || this.turns.busySince.has(id); },
|
|
357
360
|
turnChat: (id) => this.turns.turnChat.get(id),
|
|
361
|
+
inSubtask: (id) => [...this.subtasks.values()].some((t) => t.agentId === id),
|
|
358
362
|
available: async (id) => { const a = this.agents.get(id); return Boolean(a && isAdapter(a) && (await a.available())); },
|
|
359
363
|
interrupt: (chat) => this.interrupt({ chat, source: "route" }),
|
|
360
364
|
handoff: (to) => this.handoff(to, { source: "route" }),
|
|
@@ -396,6 +400,7 @@ export class ProjectRuntime {
|
|
|
396
400
|
list: (opts) => this.log.list(opts),
|
|
397
401
|
append: (e) => (this.closed ? { ...e, id: -1, ts: Date.now() } : this.log.append(e)),
|
|
398
402
|
warn: (message, detail) => logbook.warn("shift", message, String(detail), this.info.id),
|
|
403
|
+
noteDirtyStart: (chat, what) => this.noteDirtyStart(chat, what),
|
|
399
404
|
});
|
|
400
405
|
this.reviews = new ReviewRunner({
|
|
401
406
|
hasAgent: (id) => this.config.agents.some((a) => a.id === id),
|
|
@@ -416,6 +421,18 @@ export class ProjectRuntime {
|
|
|
416
421
|
this.brain.add({ kind: "decision", text, provenance: { agentId: "user", eventId: event.id, ts: event.ts } });
|
|
417
422
|
},
|
|
418
423
|
maxReviewers: ProjectRuntime.MAX_CONCURRENT_SUBTASKS,
|
|
424
|
+
reviewerTimeoutMs: () => Math.max(1, Number(this.config.review?.timeoutMin) || 15) * 60_000,
|
|
425
|
+
// a reviewer left out at its time limit is stopped, and its process with it — an opencode serve
|
|
426
|
+
// outlived a left-out review by 42 minutes (#312); its next turn starts it again
|
|
427
|
+
stopReviewer: async (agentId) => {
|
|
428
|
+
const a = this.agents.get(agentId);
|
|
429
|
+
if (!a || !isAdapter(a))
|
|
430
|
+
return;
|
|
431
|
+
if (a.busy())
|
|
432
|
+
await a.interrupt();
|
|
433
|
+
setTimeout(() => { if (!this.closed && !a.busy())
|
|
434
|
+
void a.stop().catch(() => { }); }, 30_000).unref?.();
|
|
435
|
+
},
|
|
419
436
|
});
|
|
420
437
|
this.councils = new CouncilRunner({
|
|
421
438
|
hasAgent: (id) => this.config.agents.some((a) => a.id === id),
|
|
@@ -451,6 +468,7 @@ export class ProjectRuntime {
|
|
|
451
468
|
append: (e) => (this.closed ? { ...e, id: -1, ts: Date.now() } : this.log.append(e)),
|
|
452
469
|
lastId: () => this.log.list({ limit: 1 }).at(-1)?.id ?? 0,
|
|
453
470
|
warn: (message, detail) => logbook.warn("council", message, String(detail), this.info.id),
|
|
471
|
+
noteDirtyStart: (chat, what) => this.noteDirtyStart(chat, what),
|
|
454
472
|
});
|
|
455
473
|
this.crews = new CrewEngine({
|
|
456
474
|
projectId: info.id,
|
|
@@ -552,6 +570,10 @@ export class ProjectRuntime {
|
|
|
552
570
|
}
|
|
553
571
|
rt.configMtime = configMtimeOf(info.dir);
|
|
554
572
|
rt.rehydrateCosts();
|
|
573
|
+
rt.rehydrateSignIns();
|
|
574
|
+
// finished orchestra runs older than a week give their disk back (#295)
|
|
575
|
+
setTimeout(() => void (rt.closed ? Promise.resolve([]) : rt.orchestra.sweep()).then((ids) => { if (ids.length)
|
|
576
|
+
logbook.info("orchestra", `removed the worktrees of ${ids.length} finished run(s)`, ids.join(", "), rt.info.id); }).catch(() => { }), 30_000).unref?.();
|
|
555
577
|
// Pull each connected ADE's native memory into the shared brain on open.
|
|
556
578
|
try {
|
|
557
579
|
rt.importMemories();
|
|
@@ -2378,8 +2400,8 @@ export class ProjectRuntime {
|
|
|
2378
2400
|
}
|
|
2379
2401
|
/**
|
|
2380
2402
|
* A plan turn on a provider agent runs in the agent's own plan mode, which
|
|
2381
|
-
* changes nothing; Loom keeps the plan it proposes under plans
|
|
2382
|
-
*
|
|
2403
|
+
* changes nothing; Loom keeps the plan it proposes under .loom/plans/ — its
|
|
2404
|
+
* own folder, not a new untracked one in your repo (#279).
|
|
2383
2405
|
*/
|
|
2384
2406
|
saveProposedPlan(agentId, chat, markdown) {
|
|
2385
2407
|
if (!markdown.trim())
|
|
@@ -2388,7 +2410,7 @@ export class ProjectRuntime {
|
|
|
2388
2410
|
const heading = /^#+\s*(.+)$/m.exec(markdown)?.[1] ?? markdown.split("\n")[0] ?? "plan";
|
|
2389
2411
|
const slug = heading.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || "plan";
|
|
2390
2412
|
const day = new Date().toISOString().slice(0, 10);
|
|
2391
|
-
const dir = path.join(this.agentDir(agentId), "plans");
|
|
2413
|
+
const dir = path.join(projectLoomDir(this.agentDir(agentId)), "plans");
|
|
2392
2414
|
fs.mkdirSync(dir, { recursive: true });
|
|
2393
2415
|
let file = path.join(dir, `${day}-${slug}.md`);
|
|
2394
2416
|
for (let n = 2; fs.existsSync(file); n++)
|
|
@@ -2502,6 +2524,107 @@ export class ProjectRuntime {
|
|
|
2502
2524
|
if (message && SIGNED_OUT.test(message))
|
|
2503
2525
|
this.signedOutAgents.set(event.agentId, message.split("\n")[0].slice(0, 200));
|
|
2504
2526
|
}
|
|
2527
|
+
/**
|
|
2528
|
+
* The size of an agent's native session in a chat — the context its last
|
|
2529
|
+
* turn sent — and how long ago that was; a resumed session past the cache's
|
|
2530
|
+
* life is re-read in full (#280).
|
|
2531
|
+
*/
|
|
2532
|
+
sessionWeight(agentId, chat = MAIN_CHAT) {
|
|
2533
|
+
const last = this.log.list({ chat, agentId, kinds: ["run_complete"], limit: 50 }).at(-1);
|
|
2534
|
+
if (!last)
|
|
2535
|
+
return null;
|
|
2536
|
+
const tokens = Number(last.payload.inputTokens ?? 0) || 0;
|
|
2537
|
+
const model = typeof last.payload.model === "string" ? last.payload.model : undefined;
|
|
2538
|
+
const ageMs = Date.now() - last.ts;
|
|
2539
|
+
// what this agent is really billed per token, from its own recent priced turns (any chat)
|
|
2540
|
+
const rate = observedInputRate(this.log.list({ agentId, kinds: ["run_complete"], limit: 40 }).map((e) => ({
|
|
2541
|
+
costUsd: Number(e.payload.costUsd ?? 0) || 0, tokensIn: Number(e.payload.inputTokens ?? 0) || 0,
|
|
2542
|
+
cachedIn: Number(e.payload.cachedInputTokens ?? 0) || 0, tokensOut: Number(e.payload.outputTokens ?? 0) || 0
|
|
2543
|
+
})));
|
|
2544
|
+
return { tokens, ageMs, ...(model ? { model } : {}), ...coldResume({ tokens, ageMs, rate, ...(model ? { model } : {}) }, priceBook()) };
|
|
2545
|
+
}
|
|
2546
|
+
/**
|
|
2547
|
+
* Uncommitted changes in the tree right now (Loom's own .loom/ aside) —
|
|
2548
|
+
* a shift or council starting on top of an interrupted turn's edits says so,
|
|
2549
|
+
* rather than building on them silently (#313).
|
|
2550
|
+
*/
|
|
2551
|
+
dirtyFiles() {
|
|
2552
|
+
try {
|
|
2553
|
+
const out = execFileSync("git", ["status", "--porcelain", "--", ".", ":(exclude).loom"], { cwd: this.info.dir, encoding: "utf8", timeout: 10_000, stdio: ["ignore", "pipe", "ignore"] });
|
|
2554
|
+
return out.split("\n").map((l) => l.slice(3).trim()).filter(Boolean);
|
|
2555
|
+
}
|
|
2556
|
+
catch {
|
|
2557
|
+
return [];
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
/** Say, in the chat that's starting, that the tree isn't clean — and how to put it back. */
|
|
2561
|
+
noteDirtyStart(chat, what) {
|
|
2562
|
+
const files = this.dirtyFiles();
|
|
2563
|
+
if (!files.length)
|
|
2564
|
+
return;
|
|
2565
|
+
this.log.append({ kind: "status", ...(chat !== MAIN_CHAT ? { chat } : {}), payload: { state: "dirty_start", files: files.slice(0, 40),
|
|
2566
|
+
text: `${what} starts on ${files.length} uncommitted change${files.length === 1 ? "" : "s"} already in the tree (${files.slice(0, 4).join(", ")}${files.length > 4 ? ", …" : ""}) — commit them first, or Rewind to a turn's checkpoint to put them back` } });
|
|
2567
|
+
}
|
|
2568
|
+
/**
|
|
2569
|
+
* Decisions recorded from a council's verdicts before #275 lost every `_`
|
|
2570
|
+
* ("test_reach.py" became "testreach.py") and briefings carried them on.
|
|
2571
|
+
* Rebuild each from the review finding (kept intact) and the planner's own
|
|
2572
|
+
* reply, still in the chat — the note exactly as written (#318). Returns
|
|
2573
|
+
* how many memories were mended.
|
|
2574
|
+
*/
|
|
2575
|
+
repairDecisions() {
|
|
2576
|
+
const findings = [];
|
|
2577
|
+
const chats = new Map(); // finding id → the review's chat
|
|
2578
|
+
for (const e of this.log.list({ kinds: ["review"] })) {
|
|
2579
|
+
if (e.payload.phase !== "done")
|
|
2580
|
+
continue;
|
|
2581
|
+
for (const f of e.payload.findings ?? []) {
|
|
2582
|
+
findings.push(f);
|
|
2583
|
+
chats.set(f.id, String(e.chat ?? e.payload.chat ?? MAIN_CHAT));
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
let repaired = 0, checked = 0;
|
|
2587
|
+
for (const m of this.brain.all()) {
|
|
2588
|
+
if (m.kind !== "decision" || !m.text.startsWith("Review finding accepted ("))
|
|
2589
|
+
continue;
|
|
2590
|
+
checked++;
|
|
2591
|
+
const f = findings.find((x) => m.text.startsWith(decisionText(x)));
|
|
2592
|
+
if (!f)
|
|
2593
|
+
continue;
|
|
2594
|
+
// the planner's raw verdict line for this finding, anywhere in that chat
|
|
2595
|
+
let note;
|
|
2596
|
+
for (const msg of this.log.list({ chat: chats.get(f.id) ?? MAIN_CHAT, kinds: ["message"], limit: 2000 }).reverse()) {
|
|
2597
|
+
if (!msg.agentId || typeof msg.payload.text !== "string")
|
|
2598
|
+
continue;
|
|
2599
|
+
const v = parseVerdicts(msg.payload.text, [f.id]).get(f.id);
|
|
2600
|
+
if (v) {
|
|
2601
|
+
note = v.note;
|
|
2602
|
+
break;
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
const fixed = decisionText(f, note);
|
|
2606
|
+
if (fixed === m.text)
|
|
2607
|
+
continue;
|
|
2608
|
+
this.brain.update(m.id, { text: fixed }, "loom");
|
|
2609
|
+
repaired++;
|
|
2610
|
+
}
|
|
2611
|
+
return { repaired, checked };
|
|
2612
|
+
}
|
|
2613
|
+
/** Resolves once the agent isn't mid-turn (or after `maxMs`, when the send gets the adapter's own answer). */
|
|
2614
|
+
async whenIdle(agentId, maxMs = 20 * 60_000) {
|
|
2615
|
+
const busy = () => { const a = this.agents.get(agentId); return Boolean(a && isAdapter(a) && a.busy()) || this.turns.busySince.has(agentId); };
|
|
2616
|
+
for (const end = Date.now() + maxMs; busy() && Date.now() < end && !this.closed;)
|
|
2617
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
2618
|
+
}
|
|
2619
|
+
/** The agents a real turn found signed out, with what the CLI said (for `loom doctor`). */
|
|
2620
|
+
learnedSignOuts() {
|
|
2621
|
+
return [...this.signedOutAgents.entries()].map(([agentId, message]) => ({ agentId, kind: this.config.agents.find((a) => a.id === agentId)?.kind ?? "", message }));
|
|
2622
|
+
}
|
|
2623
|
+
/** What a restart would forget: each agent's latest sign-in failure that no successful turn has cleared since. */
|
|
2624
|
+
rehydrateSignIns() {
|
|
2625
|
+
for (const e of this.log.list({ kinds: ["error", "run_complete"], limit: 3000 }))
|
|
2626
|
+
this.noteSignIn(e);
|
|
2627
|
+
}
|
|
2505
2628
|
/** What status says about an agent's sign-in: a real failure beats the CLI's own (cached) check. */
|
|
2506
2629
|
signInState(agentId, kind) {
|
|
2507
2630
|
const learned = this.signedOutAgents.get(agentId);
|
|
@@ -2547,6 +2670,18 @@ export class ProjectRuntime {
|
|
|
2547
2670
|
}
|
|
2548
2671
|
/** The loop card's buttons: stop the turn, nudge the agent, or hand the task to another agent. */
|
|
2549
2672
|
async actOnLoop(chat, agentId, action, to, loop) {
|
|
2673
|
+
if (action !== "stop") {
|
|
2674
|
+
const release = this.queueCoordinator.hold();
|
|
2675
|
+
try {
|
|
2676
|
+
return await this.actOnLoopNow(chat, agentId, action, to, loop);
|
|
2677
|
+
}
|
|
2678
|
+
finally {
|
|
2679
|
+
release();
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
return this.actOnLoopNow(chat, agentId, action, to, loop);
|
|
2683
|
+
}
|
|
2684
|
+
async actOnLoopNow(chat, agentId, action, to, loop) {
|
|
2550
2685
|
const card = loop ?? this.lastLoop(chat, agentId);
|
|
2551
2686
|
await this.interrupt({ chat, source: "route" });
|
|
2552
2687
|
if (action === "stop")
|
|
@@ -2800,8 +2935,9 @@ export class ProjectRuntime {
|
|
|
2800
2935
|
const startedAt = Date.now();
|
|
2801
2936
|
// the child's reply belongs to the chat it was asked in, not wherever it last talked
|
|
2802
2937
|
this.turns.turnChat.set(opts.agentId, chat);
|
|
2803
|
-
|
|
2804
|
-
|
|
2938
|
+
// an agent that's mid-turn finishes it first: a review sent to a busy reviewer used to fail at once (#310)
|
|
2939
|
+
void this.whenIdle(opts.agentId)
|
|
2940
|
+
.then(() => { this.turns.turnChat.set(opts.agentId, chat); return child.send(input); })
|
|
2805
2941
|
.then(() => {
|
|
2806
2942
|
this.subtasks.delete(id);
|
|
2807
2943
|
// an adapter can report a failed turn as an error event and still resolve: that subtask
|
|
@@ -3183,8 +3319,24 @@ export class ProjectRuntime {
|
|
|
3183
3319
|
// "the self-heal did nothing".
|
|
3184
3320
|
quarantine: this.quarantined(),
|
|
3185
3321
|
orchestra: this.orchestraSummary(),
|
|
3322
|
+
activity: this.inFlight(),
|
|
3186
3323
|
};
|
|
3187
3324
|
}
|
|
3325
|
+
/** What's in flight besides an agent's own turn — an orchestra planning, a crew, a council — so "idle" isn't said of a busy project (#294). */
|
|
3326
|
+
inFlight() {
|
|
3327
|
+
const out = [];
|
|
3328
|
+
if (this.orchestra.active())
|
|
3329
|
+
out.push("orchestra");
|
|
3330
|
+
for (const c of this.crews.workingCrews)
|
|
3331
|
+
out.push(`crew ${c}`);
|
|
3332
|
+
if (this.councils.active)
|
|
3333
|
+
out.push("council");
|
|
3334
|
+
if (this.shifts.active)
|
|
3335
|
+
out.push("night shift");
|
|
3336
|
+
if (this.reviews.running)
|
|
3337
|
+
out.push("review");
|
|
3338
|
+
return out;
|
|
3339
|
+
}
|
|
3188
3340
|
/**
|
|
3189
3341
|
* What every agent in this project is doing right now — for the fleet view.
|
|
3190
3342
|
*
|