@marver-design/marver 0.9.0 → 0.10.1

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.
@@ -2,6 +2,7 @@ import { r as deviceId, t as has } from "./ledger-BgA7nQoH.mjs";
2
2
  import { n as replay } from "./events-BMtBvvgU.mjs";
3
3
  import { appendEvents, listBoards, readLog } from "./comments-BZBKhKRO.mjs";
4
4
  import { n as localProfile } from "./profile-BkiWglVE.mjs";
5
+ import { i as toFrameId } from "./manifest-DIsp3ldB.mjs";
5
6
  import { workActivity } from "./work-CLrmY-vQ.mjs";
6
7
  import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync, writeSync } from "node:fs";
7
8
  import { join } from "node:path";
@@ -176,7 +177,7 @@ function extractReanchors(text) {
176
177
  */
177
178
  /** First key of an object, or undefined. */
178
179
  const firstKey = (o) => o && typeof o === "object" ? Object.keys(o)[0] : void 0;
179
- const cleanModel = (raw) => typeof raw === "string" ? raw.replace(/\[[^\]]*\]$/, "") || void 0 : void 0;
180
+ const cleanModel$1 = (raw) => typeof raw === "string" ? raw.replace(/\[[^\]]*\]$/, "") || void 0 : void 0;
180
181
  const claudeAdapter = {
181
182
  name: "claude",
182
183
  supportsSubagents: true,
@@ -205,7 +206,7 @@ const claudeAdapter = {
205
206
  const text = (o.message?.content ?? []).find((c) => c?.type === "text" && typeof c.text === "string" && c.text.trim());
206
207
  return text ? {
207
208
  text: String(text.text).trim(),
208
- model: cleanModel(o.message?.model)
209
+ model: cleanModel$1(o.message?.model)
209
210
  } : null;
210
211
  } catch {
211
212
  return null;
@@ -225,14 +226,14 @@ const claudeAdapter = {
225
226
  if (o.type === "result") {
226
227
  if (typeof o.result === "string") text = o.result.trim();
227
228
  if (o.is_error) failed = true;
228
- model ??= cleanModel(firstKey(o.modelUsage));
229
- } else if (o.type === "assistant") model ??= cleanModel(o.message?.model);
229
+ model ??= cleanModel$1(firstKey(o.modelUsage));
230
+ } else if (o.type === "assistant") model ??= cleanModel$1(o.message?.model);
230
231
  } catch {}
231
232
  }
232
233
  if (!text) try {
233
234
  const j = JSON.parse(stdout);
234
235
  if (typeof j.result === "string") text = j.result.trim();
235
- model ??= cleanModel((typeof j.model === "string" ? j.model : void 0) ?? j.canonicalModel ?? firstKey(j.modelUsage));
236
+ model ??= cleanModel$1((typeof j.model === "string" ? j.model : void 0) ?? j.canonicalModel ?? firstKey(j.modelUsage));
236
237
  } catch {
237
238
  if (!sawEvents) text = stdout.trim();
238
239
  }
@@ -305,6 +306,425 @@ const codexAdapter = {
305
306
  }
306
307
  };
307
308
  //#endregion
309
+ //#region src/server/jam/adapter/cursor.ts
310
+ /**
311
+ * The Cursor adapter. Spawns `cursor-agent -p` headless with stream-json output - a
312
+ * Claude-Code-shaped stream (system/init carries the model, assistant events carry complete
313
+ * messages, a terminal `result` event carries the reply).
314
+ *
315
+ * The long binary name is deliberate: cursor's installer ships BOTH `cursor-agent` and a bare
316
+ * `agent`, and grok's installer symlinks `agent` too - on a machine with both, the short name
317
+ * is whichever installed last. `cursor-agent` is unambiguous.
318
+ *
319
+ * Jail: codex-parity, not claude-parity - cursor's print mode carries write AND shell
320
+ * tools, and the CLI has no per-run flag that removes the shell outright. So the OS
321
+ * sandbox is switched ON (`--sandbox enabled`, overriding any config that turned it off)
322
+ * to bound what commands can touch, and `--force` is never passed, so nothing gets blanket
323
+ * command approval. The packet carries untrusted text (synced collaborators' comments);
324
+ * the sandbox is what stands between it and the machine.
325
+ */
326
+ const assistantText$2 = (o) => {
327
+ if (o.type !== "assistant") return void 0;
328
+ const t = (o.message?.content ?? []).find((c) => c?.type === "text" && typeof c.text === "string" && c.text.trim());
329
+ return t ? String(t.text).trim() : void 0;
330
+ };
331
+ const cursorAdapter = {
332
+ name: "cursor",
333
+ supportsSubagents: false,
334
+ spawnArgs(goal) {
335
+ return {
336
+ cmd: "cursor-agent",
337
+ args: [
338
+ "-p",
339
+ goal,
340
+ "--output-format",
341
+ "stream-json",
342
+ "--sandbox",
343
+ "enabled",
344
+ "--trust"
345
+ ]
346
+ };
347
+ },
348
+ earlyText(line) {
349
+ try {
350
+ const o = JSON.parse(line);
351
+ const text = assistantText$2(o);
352
+ return text ? { text } : null;
353
+ } catch {
354
+ return null;
355
+ }
356
+ },
357
+ parse(stdout, code) {
358
+ let text = "";
359
+ let lastAssistant = "";
360
+ let model;
361
+ let failed = false;
362
+ for (const line of stdout.split("\n")) {
363
+ const t = line.trim();
364
+ if (!t) continue;
365
+ try {
366
+ const o = JSON.parse(t);
367
+ if (o.type === "result") {
368
+ if (typeof o.result === "string" && o.result.trim()) text = o.result.trim();
369
+ if (o.is_error) failed = true;
370
+ } else if (o.type === "system" && o.subtype === "init" && typeof o.model === "string" && o.model !== "unknown") model = o.model;
371
+ else {
372
+ const at = assistantText$2(o);
373
+ if (at) lastAssistant = at;
374
+ }
375
+ } catch {}
376
+ }
377
+ if (!text) text = lastAssistant;
378
+ const { reply: visible, reanchors } = extractReanchors(text);
379
+ const reply = extractReplyBlock(visible);
380
+ return {
381
+ reply,
382
+ model,
383
+ reanchors,
384
+ ok: code === 0 && !failed && !!reply
385
+ };
386
+ }
387
+ };
388
+ //#endregion
389
+ //#region src/server/jam/adapter/droid.ts
390
+ /**
391
+ * The Factory droid adapter. Spawns `droid exec -o stream-json` headless - a
392
+ * Claude-Code-shaped stream (system/init carries the model, a terminal `result` event
393
+ * carries the reply and `is_error`). Factory marks stream-json deprecated but it works
394
+ * (v0.200.0); parse() also reads the `-o json` single-envelope shape, so if a future droid
395
+ * drops the stream the switch is one argv word, not a parser rewrite.
396
+ *
397
+ * Jail: `--auto low` approves file creation/edit, and `--disabled-tools` then REMOVES the
398
+ * shell (`Execute`) and the outbound connectors (Slack posting, connector search) outright -
399
+ * the packet carries untrusted text, and both are exfiltration channels. Tool names come
400
+ * from droid's own `--list-tools` (v0.200.0). Claude-parity: edits yes, shell no.
401
+ */
402
+ const droidAdapter = {
403
+ name: "droid",
404
+ supportsSubagents: false,
405
+ spawnArgs(goal) {
406
+ return {
407
+ cmd: "droid",
408
+ args: [
409
+ "exec",
410
+ "-o",
411
+ "stream-json",
412
+ "--auto",
413
+ "low",
414
+ "--disabled-tools",
415
+ "Execute,Task,ProposeMission,StartMissionRun,GenerateDroid,slack_post_message,slack_post_file,ConnectorSearch",
416
+ "--disable-builtin-skills",
417
+ goal
418
+ ]
419
+ };
420
+ },
421
+ earlyText(line) {
422
+ try {
423
+ const o = JSON.parse(line);
424
+ if (o.type !== "assistant") return null;
425
+ const t = (o.message?.content ?? []).find((c) => c?.type === "text" && typeof c.text === "string" && c.text.trim());
426
+ return t ? { text: String(t.text).trim() } : null;
427
+ } catch {
428
+ return null;
429
+ }
430
+ },
431
+ parse(stdout, code) {
432
+ let text = "";
433
+ let model;
434
+ let failed = false;
435
+ let sawEvents = false;
436
+ for (const line of stdout.split("\n")) {
437
+ const t = line.trim();
438
+ if (!t) continue;
439
+ try {
440
+ const o = JSON.parse(t);
441
+ sawEvents = true;
442
+ if (o.type === "result") {
443
+ if (typeof o.result === "string") text = o.result.trim();
444
+ if (o.is_error) failed = true;
445
+ } else if (o.type === "system" && o.subtype === "init" && typeof o.model === "string") model = o.model;
446
+ else if (o.type === "error") failed = true;
447
+ } catch {}
448
+ }
449
+ if (!sawEvents) try {
450
+ const j = JSON.parse(stdout);
451
+ if (typeof j.result === "string") text = j.result.trim();
452
+ if (j.is_error) failed = true;
453
+ } catch {}
454
+ const { reply: visible, reanchors } = extractReanchors(text);
455
+ const reply = extractReplyBlock(visible);
456
+ return {
457
+ reply,
458
+ model,
459
+ reanchors,
460
+ ok: code === 0 && !failed && !!reply
461
+ };
462
+ }
463
+ };
464
+ //#endregion
465
+ //#region src/server/jam/adapter/grok.ts
466
+ /**
467
+ * The grok adapter (xAI's Grok Build CLI). Spawns `grok -p` headless with
468
+ * streaming-messages-json - an Anthropic-Messages-shaped stream: system/init and assistant
469
+ * frames name the model, a terminal `result` event carries the reply and `is_error`.
470
+ *
471
+ * Jail: an explicit `--tools` ALLOWLIST of read/edit primitives only - so the shell, web,
472
+ * X-search, subagents, scheduler, MCP, and image tools are all simply absent, not denied by
473
+ * name. (A deny-list is a footgun: grok's real shell tool is `run_terminal_command`, and an
474
+ * earlier `--disallowed-tools run_terminal_cmd` left the shell fully live - verified. An
475
+ * allowlist can't miss a tool it doesn't name.) `--permission-mode acceptEdits` auto-applies
476
+ * those edits so the headless run never stalls; `--no-subagents` is belt-and-braces (the
477
+ * allowlist already omits spawn_subagent). The packet carries untrusted text, so this is the
478
+ * no-shell, no-network posture.
479
+ *
480
+ * grok injects NO env marker into processes it spawns (verified in the grok-build source),
481
+ * so detection finds it by PATH order only - naming `jam: "grok"` in config always works.
482
+ */
483
+ const cleanModel = (raw) => typeof raw === "string" && raw && raw !== "unknown" ? raw : void 0;
484
+ const assistantText$1 = (o) => {
485
+ if (o.type !== "assistant") return void 0;
486
+ const t = (o.message?.content ?? []).find((c) => c?.type === "text" && typeof c.text === "string" && c.text.trim());
487
+ return t ? String(t.text).trim() : void 0;
488
+ };
489
+ const grokAdapter = {
490
+ name: "grok",
491
+ supportsSubagents: false,
492
+ spawnArgs(goal) {
493
+ return {
494
+ cmd: "grok",
495
+ args: [
496
+ "-p",
497
+ goal,
498
+ "--output-format",
499
+ "streaming-messages-json",
500
+ "--permission-mode",
501
+ "acceptEdits",
502
+ "--no-subagents",
503
+ "--tools",
504
+ "read_file,search_replace,list_dir,grep,todo_write"
505
+ ]
506
+ };
507
+ },
508
+ earlyText(line) {
509
+ try {
510
+ const o = JSON.parse(line);
511
+ const text = assistantText$1(o);
512
+ return text ? {
513
+ text,
514
+ model: cleanModel(o.message?.model)
515
+ } : null;
516
+ } catch {
517
+ return null;
518
+ }
519
+ },
520
+ parse(stdout, code) {
521
+ let text = "";
522
+ let lastAssistant = "";
523
+ let model;
524
+ let failed = false;
525
+ for (const line of stdout.split("\n")) {
526
+ const t = line.trim();
527
+ if (!t) continue;
528
+ try {
529
+ const o = JSON.parse(t);
530
+ if (o.type === "result") {
531
+ if (typeof o.result === "string" && o.result.trim()) text = o.result.trim();
532
+ if (o.is_error) failed = true;
533
+ } else if (o.type === "system" && o.subtype === "init") model ??= cleanModel(o.model);
534
+ else {
535
+ const at = assistantText$1(o);
536
+ if (at) {
537
+ lastAssistant = at;
538
+ model ??= cleanModel(o.message?.model);
539
+ }
540
+ }
541
+ } catch {}
542
+ }
543
+ if (!text) text = lastAssistant;
544
+ const { reply: visible, reanchors } = extractReanchors(text);
545
+ const reply = extractReplyBlock(visible);
546
+ return {
547
+ reply,
548
+ model,
549
+ reanchors,
550
+ ok: code === 0 && !failed && !!reply
551
+ };
552
+ }
553
+ };
554
+ //#endregion
555
+ //#region src/server/jam/adapter/opencode.ts
556
+ /**
557
+ * The opencode adapter. Spawns `opencode run --format json` headless - NDJSON events where
558
+ * finalized text parts are `{"type":"text","part":{type:"text",text}}` and errors are
559
+ * `{"type":"error","error":{name,data}}`. The LAST text part is the reply.
560
+ *
561
+ * Jail: headless opencode auto-REJECTS permission asks, and its `--auto` flag approves
562
+ * everything including arbitrary shell - so neither default works. The OPENCODE_PERMISSION
563
+ * env var (verified: it merges over the user's config for this one process) is DEFAULT-DENY
564
+ * with an explicit allowlist - so an unnamed or future tool is denied, not defaulted open.
565
+ * `--pure` runs without external plugins, which execute code OUTSIDE the tool-permission
566
+ * layer (a repo could otherwise ship a plugin the daemon's spawn would load and run).
567
+ * Claude-parity: edits yes, shell no. Subagents inherit it (verified live).
568
+ *
569
+ * opencode's stream names NO model in any event (verified against its schema) - so replies
570
+ * carry the harness without a model, same honest gap as codex. Never faked.
571
+ */
572
+ const textPart = (o) => o.type === "text" && typeof o.part?.text === "string" && o.part.text.trim() ? String(o.part.text).trim() : void 0;
573
+ const opencodeAdapter = {
574
+ name: "opencode",
575
+ supportsSubagents: true,
576
+ spawnArgs(goal) {
577
+ return {
578
+ cmd: "opencode",
579
+ args: [
580
+ "run",
581
+ "--pure",
582
+ "--format",
583
+ "json",
584
+ goal
585
+ ],
586
+ env: { OPENCODE_PERMISSION: "{\"*\":\"deny\",\"read\":\"allow\",\"edit\":\"allow\",\"glob\":\"allow\",\"grep\":\"allow\",\"list\":\"allow\",\"ls\":\"allow\",\"webfetch\":\"allow\",\"websearch\":\"allow\",\"task\":\"allow\",\"todowrite\":\"allow\",\"todoread\":\"allow\"}" }
587
+ };
588
+ },
589
+ earlyText(line) {
590
+ try {
591
+ const text = textPart(JSON.parse(line));
592
+ return text ? { text } : null;
593
+ } catch {
594
+ return null;
595
+ }
596
+ },
597
+ parse(stdout, code) {
598
+ let msgId;
599
+ let parts = [];
600
+ let failed = false;
601
+ for (const line of stdout.split("\n")) {
602
+ const t = line.trim();
603
+ if (!t) continue;
604
+ try {
605
+ const o = JSON.parse(t);
606
+ const part = textPart(o);
607
+ if (part) {
608
+ const id = typeof o.part?.messageID === "string" ? o.part.messageID : void 0;
609
+ if (id !== msgId) {
610
+ msgId = id;
611
+ parts = [];
612
+ }
613
+ parts.push(part);
614
+ } else if (o.type === "error") failed = true;
615
+ } catch {}
616
+ }
617
+ const { reply: visible, reanchors } = extractReanchors(parts.join("\n\n"));
618
+ const reply = extractReplyBlock(visible);
619
+ return {
620
+ reply,
621
+ reanchors,
622
+ ok: code === 0 && !failed && !!reply
623
+ };
624
+ }
625
+ };
626
+ //#endregion
627
+ //#region src/server/jam/adapter/pi.ts
628
+ /**
629
+ * The pi adapter (the pi coding agent). Spawns `pi -p --mode json` headless - NDJSON events
630
+ * where `message_end` carries each finished message and the terminal `agent_end` carries the
631
+ * authoritative full transcript. The reply is the last assistant text in `agent_end`
632
+ * (falling back to the last `message_end` when the stream was cut before it).
633
+ *
634
+ * Jail: pi has NO runtime permission system - the tool allowlist IS the jail, so bash is
635
+ * simply not on the list: read/edit/write/grep/find/ls. Claude-parity - edits yes, shell no
636
+ * (the packet carries untrusted text; a shell is an exfiltration channel).
637
+ * `--no-extensions --no-skills` keeps the headless run to those built-ins: extensions load
638
+ * arbitrary code outside the allowlist, which has no place in an unattended spawn.
639
+ */
640
+ /** Assistant text + model of one message object (message_end payload or agent_end member). */
641
+ const assistantText = (m) => {
642
+ const msg = m;
643
+ if (!msg || msg.role !== "assistant") return null;
644
+ const text = (Array.isArray(msg.content) ? msg.content : []).filter((c) => c?.type === "text" && typeof c.text === "string").map((c) => String(c.text).trim()).filter(Boolean).join("\n\n");
645
+ if (!text && !msg.errorMessage) return null;
646
+ return {
647
+ text,
648
+ model: typeof msg.model === "string" && msg.model ? msg.model : void 0,
649
+ error: Boolean(msg.errorMessage) || msg.stopReason === "error"
650
+ };
651
+ };
652
+ //#endregion
653
+ //#region src/server/jam/adapter/index.ts
654
+ const adapters = {
655
+ claude: claudeAdapter,
656
+ codex: codexAdapter,
657
+ cursor: cursorAdapter,
658
+ droid: droidAdapter,
659
+ opencode: opencodeAdapter,
660
+ grok: grokAdapter,
661
+ pi: {
662
+ name: "pi",
663
+ supportsSubagents: false,
664
+ spawnArgs(goal) {
665
+ return {
666
+ cmd: "pi",
667
+ args: [
668
+ "-p",
669
+ "--mode",
670
+ "json",
671
+ "--no-extensions",
672
+ "--no-skills",
673
+ "--tools",
674
+ "read,edit,write,grep,find,ls",
675
+ goal
676
+ ]
677
+ };
678
+ },
679
+ earlyText(line) {
680
+ try {
681
+ const o = JSON.parse(line);
682
+ if (o.type !== "message_end") return null;
683
+ const a = assistantText(o.message);
684
+ return a?.text && !a.error ? {
685
+ text: a.text,
686
+ model: a.model
687
+ } : null;
688
+ } catch {
689
+ return null;
690
+ }
691
+ },
692
+ parse(stdout, code) {
693
+ let last = null;
694
+ let fromEnd = false;
695
+ let failed = false;
696
+ for (const line of stdout.split("\n")) {
697
+ const t = line.trim();
698
+ if (!t) continue;
699
+ try {
700
+ const o = JSON.parse(t);
701
+ if (o.type === "agent_end" && Array.isArray(o.messages)) for (let i = o.messages.length - 1; i >= 0; i--) {
702
+ const a = assistantText(o.messages[i]);
703
+ if (a) {
704
+ last = a;
705
+ fromEnd = true;
706
+ break;
707
+ }
708
+ }
709
+ else if (o.type === "message_end" && !fromEnd) {
710
+ const a = assistantText(o.message);
711
+ if (a) last = a;
712
+ } else if (o.type === "error") failed = true;
713
+ } catch {}
714
+ }
715
+ if (last?.error) failed = true;
716
+ const { reply: visible, reanchors } = extractReanchors(last?.text ?? "");
717
+ const reply = extractReplyBlock(visible);
718
+ return {
719
+ reply,
720
+ model: last?.model,
721
+ reanchors,
722
+ ok: code === 0 && !failed && !!reply
723
+ };
724
+ }
725
+ }
726
+ };
727
+ //#endregion
308
728
  //#region src/server/jam/journal.ts
309
729
  /**
310
730
  * The durable batch journal - design/.local/jam-jobs.json.
@@ -518,6 +938,28 @@ const fenceGroup = (pid) => {
518
938
  if (pid) process.kill(-pid, "SIGKILL");
519
939
  } catch {}
520
940
  };
941
+ const FRAME_FILE = /\.(tsx|jsx|html)$/;
942
+ /** Every frame FILE under design/scenes, as `frame id -> mtimeMs`. Used to see which frames the
943
+ * agent creates or edits mid-job (so the working glow can follow the work, not sit on the frame
944
+ * the comment happened to be pinned to). Infra files (`_layout`, `_fixtures`) are skipped. */
945
+ const sceneFrameMtimes = (root) => {
946
+ const scenesDir = join(root, "design", "scenes");
947
+ const out = /* @__PURE__ */ new Map();
948
+ let entries;
949
+ try {
950
+ entries = readdirSync(scenesDir, { recursive: true });
951
+ } catch {
952
+ return out;
953
+ }
954
+ for (const rel of entries) {
955
+ const base = rel.split("/").pop() ?? rel;
956
+ if (base.startsWith("_") || !FRAME_FILE.test(base)) continue;
957
+ try {
958
+ out.set(toFrameId(`scenes/${rel}`), statSync(join(scenesDir, rel)).mtimeMs);
959
+ } catch {}
960
+ }
961
+ return out;
962
+ };
521
963
  /** The early ack posts VERBATIM, so a first line that narrates the agent's plan instead of
522
964
  * addressing the owner must not ship. Deliberately NARROW - "acknowledg" only ever appears
523
965
  * in meta-talk (a real ack never names itself), and the openers are pure plan phrasing.
@@ -533,7 +975,7 @@ function createJam(root, cfg, adapter, log = () => {}, hooks = {}) {
533
975
  let stopped = false;
534
976
  const activeChildren = /* @__PURE__ */ new Set();
535
977
  const runAgent = (goal, onSpawn, onEarly) => new Promise((resolve) => {
536
- const { cmd, args } = adapter.spawnArgs(goal);
978
+ const { cmd, args, env } = adapter.spawnArgs(goal);
537
979
  let child;
538
980
  try {
539
981
  child = spawn(cmd, args, {
@@ -543,7 +985,12 @@ function createJam(root, cfg, adapter, log = () => {}, hooks = {}) {
543
985
  "ignore",
544
986
  "pipe",
545
987
  "ignore"
546
- ]
988
+ ],
989
+ env: {
990
+ ...process.env,
991
+ ...env,
992
+ PWD: root
993
+ }
547
994
  });
548
995
  } catch {
549
996
  return resolve({
@@ -587,7 +1034,9 @@ function createJam(root, cfg, adapter, log = () => {}, hooks = {}) {
587
1034
  for (const line of lines) {
588
1035
  const hit = adapter.earlyText(line);
589
1036
  if (!hit) continue;
590
- const text = extractReplyBlock(extractReanchors(hit.text).reply);
1037
+ const visible = extractReanchors(hit.text).reply;
1038
+ const block = extractReplyBlock(visible);
1039
+ const text = block !== visible.trim() ? block : block.split(/\n\s*\n/)[0].trim();
591
1040
  if (!text || metaNarration(text)) continue;
592
1041
  earlyFired = true;
593
1042
  try {
@@ -688,8 +1137,30 @@ function createJam(root, cfg, adapter, log = () => {}, hooks = {}) {
688
1137
  persist();
689
1138
  const member = buildMember(p, replay(readLog(commentsDir, b.board)));
690
1139
  const packet = buildPacket(b.batchId, [member]);
691
- hooks.work?.(member.frame, true);
692
- const beat = setInterval(() => hooks.work?.(member.frame, true), 3e4);
1140
+ const lit = /* @__PURE__ */ new Set();
1141
+ const light = (f) => {
1142
+ if (f && !lit.has(f)) {
1143
+ hooks.work?.(f, true);
1144
+ lit.add(f);
1145
+ }
1146
+ };
1147
+ const clearGlow = () => {
1148
+ for (const f of lit) hooks.work?.(f, false);
1149
+ lit.clear();
1150
+ };
1151
+ const startMtimes = sceneFrameMtimes(root);
1152
+ const touched = /* @__PURE__ */ new Set();
1153
+ light(member.frame);
1154
+ const followWork = () => {
1155
+ for (const [id, mt] of sceneFrameMtimes(root)) if (startMtimes.get(id) !== mt) touched.add(id);
1156
+ for (const id of touched) light(id);
1157
+ if (member.frame && lit.has(member.frame) && !touched.has(member.frame) && touched.size > 0) {
1158
+ hooks.work?.(member.frame, false);
1159
+ lit.delete(member.frame);
1160
+ }
1161
+ for (const f of lit) hooks.work?.(f, true);
1162
+ };
1163
+ const beat = setInterval(followWork, 2e3);
693
1164
  beat.unref?.();
694
1165
  let earlyBody;
695
1166
  let run;
@@ -707,20 +1178,20 @@ function createJam(root, cfg, adapter, log = () => {}, hooks = {}) {
707
1178
  }
708
1179
  logRun(b.batchId, run.raw);
709
1180
  if (stopped) {
710
- hooks.work?.(member.frame, false);
1181
+ clearGlow();
711
1182
  return;
712
1183
  }
713
1184
  if (run.ok) {
714
1185
  if (run.reply !== earlyBody) writeReply(b, p, run.reply, run.model);
715
1186
  emitReanchors(b, run.reanchors);
716
1187
  hooks.changed?.(b.board);
717
- hooks.work?.(member.frame, false);
1188
+ clearGlow();
718
1189
  finish(b);
719
1190
  log(` jam: replied on ${b.board}${run.model ? ` (${run.model})` : ""}${run.reanchors.length ? ` · re-pinned ${run.reanchors.length}` : ""}`);
720
1191
  } else if (b.attempts >= MAX_ATTEMPTS) {
721
- writeReply(b, p, "I couldn't finish that one. Try rephrasing, or check the dev logs.", run.model);
1192
+ writeReply(b, p, "I couldn't finish that one. The raw run log is in design/.local/jam-logs - the troubleshooting drill in design/instructions/jam.md reads it. Or just rephrase and try again.", run.model);
722
1193
  hooks.changed?.(b.board);
723
- hooks.work?.(member.frame, false);
1194
+ clearGlow();
724
1195
  finish(b);
725
1196
  log(` jam: gave up on ${b.board} after ${b.attempts} attempts`);
726
1197
  } else {
@@ -848,7 +1319,7 @@ function createJam(root, cfg, adapter, log = () => {}, hooks = {}) {
848
1319
  * one merged glow. Returns null when the adapter is unavailable or another dev server
849
1320
  * already holds the repo lock (that one runs the loop; this one watches without it). */
850
1321
  function startJam(root, cfg, log = () => {}, onChanged = () => {}) {
851
- const adapter = cfg.agent === "claude" ? claudeAdapter : cfg.agent === "codex" ? codexAdapter : null;
1322
+ const adapter = adapters[cfg.agent] ?? null;
852
1323
  if (!adapter) {
853
1324
  log(` jam: the "${cfg.agent}" adapter is not available yet; Live Jam is off`);
854
1325
  return null;
@@ -1,6 +1,6 @@
1
1
  import { n as NAME, r as PKG } from "./cli.mjs";
2
- import { c as detectHost, o as loadConfig } from "./manifest-DJHU7qfu.mjs";
3
- import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-BrBWk2Qn.mjs";
2
+ import { l as detectHost, s as loadConfig } from "./manifest-DIsp3ldB.mjs";
3
+ import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-DhkR3NdW.mjs";
4
4
  import { basename, dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { createLogger, createServer, searchForWorkspaceRoot } from "vite";
@@ -171,7 +171,7 @@ async function dev(root, portFlag) {
171
171
  });
172
172
  }
173
173
  if (config.jam) {
174
- const { startJam } = await import("./daemon-2Lft_lVV.mjs");
174
+ const { startJam } = await import("./daemon-B_8BmmSJ.mjs");
175
175
  const jam = startJam(root, config.jam, (m) => console.log(m), (board) => server.ws.send("sh:jam-comment", { board }));
176
176
  if (jam) {
177
177
  const close = server.close.bind(server);
@@ -180,7 +180,7 @@ async function dev(root, portFlag) {
180
180
  return close();
181
181
  });
182
182
  }
183
- } else if (config.jamOff === "no-agent") console.log(` jam: no agent CLI on PATH (claude or codex) - install one and tag @${NAME} in a comment\n`);
183
+ } else if (config.jamOff === "no-agent") console.log(` jam: no agent CLI on PATH (claude, codex, cursor, droid, opencode, grok, or pi) - install one and tag @${NAME} in a comment\n`);
184
184
  return server;
185
185
  }
186
186
  //#endregion
@@ -1,5 +1,5 @@
1
1
  import { n as NAME } from "./cli.mjs";
2
- import { a as DEFAULTS, c as detectHost, i as writeManifest, l as readJson, r as scanFrames, s as detectAgent } from "./manifest-DJHU7qfu.mjs";
2
+ import { a as writeManifest, c as detectAgent, l as detectHost, o as DEFAULTS, r as scanFrames, u as readJson } from "./manifest-DIsp3ldB.mjs";
3
3
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { dirname, join, relative } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -188,7 +188,7 @@ function init(root, opts) {
188
188
  console.log(`\n commit design/ - only .local/ is ignored`);
189
189
  console.log(` uninstall = delete design/, remove the ${NAME} dependency${host.tsconfigSweepsDesign ? ", revert the \"design\" line in tsconfig exclude" : ""}`);
190
190
  if (!noApp(host) && !existsSync(join(design, "DESIGN.md"))) console.log(`\n note: design/DESIGN.md (the brand doc) does not exist yet - have your agent create it from the app's tokens (instructions/brand.md, Path A) to reach the idle state.`);
191
- if (!jamAgent) console.log(`\n note: Live Jam found no agent CLI on PATH (claude or codex) - install one and it arms itself on the next \`${NAME} dev\`.`);
191
+ if (!jamAgent) console.log(`\n note: Live Jam found no agent CLI on PATH (claude, codex, cursor, droid, opencode, grok, or pi) - install one and it arms itself on the next \`${NAME} dev\`.`);
192
192
  else if (created.includes("design/config.ts")) console.log(`\n Live Jam is on (${jamAgent}): tag @${NAME} in a canvas comment and your agent does the work, then replies in the thread.`);
193
193
  console.log(`\n next: npx ${NAME} dev (or: npx ${NAME} canvas - same thing; canvas on http://localhost:${DEFAULTS.port} by default)\n`);
194
194
  if (!noApp(host)) console.log(` then, to your agent: "Read design/AGENTS.md. This is our first session - follow design/instructions/welcome.md."\n`);
@@ -488,7 +488,8 @@ const jamBlock = (agent) => agent ? ` // Live Jam - tag @${NAME} in a canvas co
488
488
  // the wrong tool, raise concurrency for more frames at once, \`jam: false\` to turn it off.
489
489
  jam: { agent: ${JSON.stringify(agent)}, concurrency: 6 },` : ` // Live Jam - tag @${NAME} in a canvas comment and your coding agent picks the job up,
490
490
  // edits the real frame, and replies in the thread. No agent CLI was on PATH when init ran;
491
- // install claude or codex and it arms itself, or name one here. \`jam: false\` turns it off.
491
+ // install an agent CLI (claude, codex, cursor, droid, opencode, grok, or pi) and it arms
492
+ // itself, or name one here. \`jam: false\` turns it off.
492
493
  // jam: { agent: "claude", concurrency: 6 },`;
493
494
  const configTemplate = (mode, jamAgent) => `// ${NAME} config - OPTIONAL. Delete this file and everything still works on defaults.
494
495
  // Theme lives in design/theme.css (it imports your app's real stylesheet) - not here.