jeo-code 0.8.41 → 0.8.43

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.
@@ -19,7 +19,8 @@ import { parseInThreadConfigCommand } from "../agent/notify/config-commands";
19
19
 
20
20
  import { createTodoTool, TODO_TOOL_PROTOCOL_LINE } from "../agent/todo-tool";
21
21
  import { createJobTool, JOB_TOOL_PROTOCOL_LINE } from "../agent/job-tool";
22
- import { JobRegistry } from "../agent/job-registry";
22
+ import { JobRegistry, type MonitorJobEvent } from "../agent/job-registry";
23
+ import { createMonitorTool, MONITOR_TOOL_PROTOCOL_LINE } from "../agent/monitor-tool";
23
24
  import { createIrcTool, IRC_TOOL_PROTOCOL_LINE } from "../agent/irc-tool";
24
25
  import { createGoalTool, GOAL_TOOL_PROTOCOL_LINE } from "../agent/goal-tool";
25
26
  import { createApproveTool, APPROVE_TOOL_PROTOCOL_LINE } from "../agent/approve-tool";
@@ -185,6 +186,7 @@ import {
185
186
  gatedStdout,
186
187
  formatTaskSubEvent,
187
188
  logTaskSubEvent,
189
+ formatMonitorJobEvent,
188
190
  createStreamEvents,
189
191
  shouldUseOneShotTui,
190
192
  } from "./launch/stream";
@@ -560,7 +562,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
560
562
  // pi-style: load project context (JEO.md / AGENTS.md / .jeo/context.md / CLAUDE.md) into the prompt.
561
563
  const contextFiles = await loadProjectContext(cwd);
562
564
 
563
- const KNOWN_TOOLS = new Set(["read", "write", "edit", "bash", "find", "search", "ls", "task", "eval", "todo", "subagent", "job", "irc", "goal", "approve", "ast_grep", "ast_edit", "computer", "lsp", "lsp_rename", "debug", "browser"]);
565
+ const KNOWN_TOOLS = new Set(["read", "write", "edit", "bash", "find", "search", "ls", "task", "eval", "todo", "subagent", "job", "monitor", "irc", "goal", "approve", "ast_grep", "ast_edit", "computer", "lsp", "lsp_rename", "debug", "browser"]);
564
566
  let allowedTools = new Set(KNOWN_TOOLS);
565
567
 
566
568
  if (flags.noTools) {
@@ -633,6 +635,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
633
635
  (allowedTools.has("subagent") ? "\n\nDetached subagents: " + SUBAGENT_TOOL_PROTOCOL_LINE +
634
636
  " Launch background work with task {\"detached\": true, \"role\": <role>, \"task\": <assignment>}; it returns a subagent id immediately so you can keep working and collect the result later." : "") +
635
637
  (allowedTools.has("job") ? "\n\nBackground jobs: " + JOB_TOOL_PROTOCOL_LINE : "") +
638
+ (allowedTools.has("monitor") ? "\n\nBackground monitors: " + MONITOR_TOOL_PROTOCOL_LINE : "") +
636
639
  (allowedTools.has("irc") ? "\n\nPeer messaging: " + IRC_TOOL_PROTOCOL_LINE : "") +
637
640
  (allowedTools.has("goal") ? "\n\nGoal tracking: " + GOAL_TOOL_PROTOCOL_LINE : "") +
638
641
  (allowedTools.has("approve") ? "\n\nPlan approval: " + APPROVE_TOOL_PROTOCOL_LINE : "") +
@@ -1422,7 +1425,32 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1422
1425
  // #9: per-turn registry for DETACHED subagents (task{detached:true}); the
1423
1426
  // `subagent` tool controls them and cancelAll() in finally prevents orphans.
1424
1427
  const subagentRegistry = new SubagentRegistry();
1428
+ // TUI callbacks are scoped to this registry. cancelAll() may trigger a detached
1429
+ // run's final event after the turn is complete; do not let it paint a freshly
1430
+ // reset later TUI. Non-TUI streams retain direct event logging.
1431
+ let acceptingTuiSubagentEvents = true;
1432
+ const onSubagentEvent = useTui
1433
+ ? (e: TaskSubEvent) => {
1434
+ if (acceptingTuiSubagentEvents) tui?.onSubagentEvent(e);
1435
+ }
1436
+ : (e: TaskSubEvent) => logTaskSubEvent(e);
1425
1437
  const jobRegistry = new JobRegistry();
1438
+ // Monitor output stays bounded and turn-scoped. It deliberately feeds the
1439
+ // engine's observational inbox rather than the steering inbox.
1440
+ const monitorInbox: string[] = [];
1441
+ const MONITOR_INBOX_CAP = 32;
1442
+ let acceptingMonitorEvents = true;
1443
+ const onMonitorEvent = (e: MonitorJobEvent) => {
1444
+ if (!acceptingMonitorEvents) return;
1445
+ if (useTui) tui?.onMonitorEvent(e);
1446
+ else console.log(formatMonitorJobEvent(e));
1447
+ if (e.type !== "line" || monitorInbox.length >= MONITOR_INBOX_CAP) return;
1448
+ const description = e.record.description.replace(/\s+/g, " ").trim().slice(0, 240);
1449
+ const line = e.line.slice(0, 2_000);
1450
+ monitorInbox.push(`[monitor ${e.record.id} · ${e.record.category} · ${description}]\n${line}`);
1451
+ };
1452
+ const unsubscribeMonitor = jobRegistry.subscribeMonitor(onMonitorEvent);
1453
+ const drainMonitor = () => monitorInbox.splice(0, monitorInbox.length);
1426
1454
  sessionNotifyEndpoint?.attachRegistry(subagentRegistry);
1427
1455
  currentTurnSteer = { push: line => steerInbox.push(line), flushCard: line => tui?.flushSteerCard(line) };
1428
1456
  if (sessionNotifyEndpoint && !notifyRedact) {
@@ -1495,21 +1523,18 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1495
1523
  steer: drainSteer,
1496
1524
  registry: subagentRegistry,
1497
1525
  sessionEndpoint: sessionNotifyEndpoint,
1498
- onEvent: useTui
1499
- ? (e => tui?.onSubagentEvent(e))
1500
- : (e => logTaskSubEvent(e)),
1526
+ onEvent: onSubagentEvent,
1501
1527
  }),
1502
1528
  eval: createEvalTool({
1503
1529
  config: { ...turnConfig, defaultModel: activeModel },
1504
1530
  signal: ac.signal,
1505
1531
  steer: drainSteer,
1506
- onEvent: useTui
1507
- ? (e => tui?.onSubagentEvent(e))
1508
- : (e => logTaskSubEvent(e)),
1532
+ onEvent: onSubagentEvent,
1509
1533
  }),
1510
1534
  todo: createTodoTool({ onChange: items => { turnTodos = items; tui?.setTodos(items); } }),
1511
1535
  subagent: createSubagentTool(subagentRegistry),
1512
1536
  job: createJobTool(jobRegistry),
1537
+ monitor: createMonitorTool(jobRegistry),
1513
1538
  irc: createIrcTool(subagentRegistry),
1514
1539
  goal: createGoalTool(),
1515
1540
  approve: createApproveTool(),
@@ -1615,6 +1640,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1615
1640
  signal: ac.signal,
1616
1641
  sessionKey: cacheKey,
1617
1642
  steer: drainSteer,
1643
+ monitor: drainMonitor,
1618
1644
  rateLimitFallbackAvailable,
1619
1645
  safetyFallbackAvailable,
1620
1646
  events: wrapEvents(withStepPersistence({ ...withToolDetailCapture(tui ? tui.events() : streamEvents), onBeforeDone }, persistTurnTail), opik),
@@ -1748,6 +1774,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1748
1774
  signal: ac.signal,
1749
1775
  sessionKey: turnSessionKey,
1750
1776
  steer: drainSteer,
1777
+ monitor: drainMonitor,
1751
1778
  // Deliberately NOT wiring rateLimitFallbackAvailable here: this repair
1752
1779
  // retry has no reroute loop around it (unlike runLoopWithModel's
1753
1780
  // routeFallbackAttempt loop) — bailing the retry ladder early on a 429
@@ -1772,11 +1799,14 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1772
1799
  await opik.endTurn({ done: result.done, steps: result.steps, output: result.doneReason });
1773
1800
  } finally {
1774
1801
  harness.dispose();
1802
+ acceptingTuiSubagentEvents = false;
1803
+ acceptingMonitorEvents = false;
1775
1804
  subagentRegistry.cancelAll(); // #9: no detached run leaks past the turn
1776
1805
  sessionNotifyEndpoint?.detachRegistry(); // this turn's registry is gone — subagent frames stop applying to it
1777
1806
  currentTurnSteer = undefined; // a remote message after this point queues instead of steering a dead turn
1778
1807
  await stopSessionNotifyEndpoint(subagentRegistry); // tear down the FALLBACK lazy endpoint, if one was started (no-op when sessionNotifyEndpoint owns the registry)
1779
1808
  jobRegistry.cancelAll(); // background jobs are turn-scoped too — no orphaned processes
1809
+ unsubscribeMonitor();
1780
1810
 
1781
1811
  // Steering typed but never drained (e.g. entered just after the final step)
1782
1812
  // must not be lost — fold it into the next prompt draft so it runs next.
package/src/tui/app.ts CHANGED
@@ -20,6 +20,7 @@ import { renderFooter, type FooterData } from "./components/footer";
20
20
  import { renderForgeMark, forgeMarkHeight, forgeMarkFrameCount, forgeBeat } from "./components/ascii-art";
21
21
  import { evolutionTrack, createStageProgress, type StageProgress, transitionMessage } from "./components/evolution";
22
22
  import type { TaskSubEvent } from "../agent/task-tool";
23
+ import type { MonitorJobEvent } from "../agent/job-registry";
23
24
  import { supportsUnicode } from "./components/capability";
24
25
  import { centerBlock, padLineTo, boxBlock, BOX_ASCII, BOX_UNICODE } from "./components/layout";
25
26
  import { SECTION_GAP, sectionLabel, stackSections } from "./components/section";
@@ -274,19 +275,15 @@ export class LaunchTui {
274
275
  // Cumulative token usage for the live turn (engine onUsage event).
275
276
  private turnUsage: { inputTokens: number; outputTokens: number } | null = null;
276
277
  // True while at least one delegated subagent SLOT is in flight — drives the `(sub)`
277
- // status marker. Derived from `subagentLiveSlots.size` (see below) rather than a
278
- // plain boolean so a fan-out batch where ONE worker finishes early no longer
279
- // clears the marker while its siblings are still visibly running.
278
+ // status marker. Derived from `subagentLiveSlots.size` rather than a plain boolean
279
+ // so one completed concurrent run cannot clear still-running siblings.
280
280
  private subagentActive = false;
281
- // Latest nested subagent activity, PER CONCURRENT SLOT (fan-out `task` batches — both
282
- // the executor and read-only roles — now run several workers at once; a single
283
- // shared string clobbered on every event made concurrent subagents look sequential
284
- // and, worse, cleared the whole `(sub)` marker the instant ANY one worker finished).
285
- // Keyed by the event's fan-out `index` (1-based); a single/detached run with no
286
- // index shares the fixed key 0. `subagentLiveOrder` tracks touch order so the
281
+ // Latest nested subagent activity, PER CONCURRENT SLOT. Detached runs use their
282
+ // stable registry id, fan-out workers use their 1-based index, and legacy
283
+ // synchronous runs share one slot. `subagentLiveOrder` tracks touch order so the
287
284
  // status row can show the MOST RECENTLY active slot plus a "+N more" count.
288
- private readonly subagentLiveSlots = new Map<number, string>();
289
- private readonly subagentLiveOrder: number[] = [];
285
+ private readonly subagentLiveSlots = new Map<string, string>();
286
+ private readonly subagentLiveOrder: string[] = [];
290
287
  // Bounded activity-history ring: one plain-text entry per ledger append, with a
291
288
  // turn-relative timestamp. Powers Ctrl+O's "recent activity" tail so the detail
292
289
  // view ALWAYS answers "what has been happening", even before the first reply.
@@ -1004,10 +1001,9 @@ export class LaunchTui {
1004
1001
  }
1005
1002
  // A delegated subagent's LATEST nested event beats the parent's static
1006
1003
  // "Task: <role> …" card title — a long task otherwise reads as a stall even
1007
- // though the subagent is actively reading/editing/running underneath. When a
1008
- // fan-out batch has MULTIPLE concurrent slots live, append a "+N more running"
1009
- // count so a parallel batch visibly reads as parallel instead of looking like
1010
- // one subagent whose label keeps randomly changing.
1004
+ // though the subagent is actively reading/editing/running underneath. When
1005
+ // multiple concurrent slots live, append a "+N more running" count so parallel
1006
+ // work does not look like one subagent whose label keeps randomly changing.
1011
1007
  if (this.subagentActive && this.subagentLiveSlots.size > 0) {
1012
1008
  const mostRecent = this.subagentLiveOrder[this.subagentLiveOrder.length - 1];
1013
1009
  const line = (mostRecent !== undefined ? this.subagentLiveSlots.get(mostRecent) : undefined)
@@ -1063,6 +1059,7 @@ export class LaunchTui {
1063
1059
  const color = this.theme.color;
1064
1060
  const role = e.role || "subagent";
1065
1061
  const roleLabel = e.index && e.total ? `${role.toUpperCase()}[${e.index}/${e.total}]` : role.toUpperCase();
1062
+ const detachedLabel = e.detached && e.id ? ` [${e.id}]` : "";
1066
1063
  const badge = categoryBadge("subagent", { color });
1067
1064
  const ok = this.unicode ? "✓" : "v";
1068
1065
  const bad = this.unicode ? "✗" : "x";
@@ -1070,10 +1067,14 @@ export class LaunchTui {
1070
1067
  const last = this.unicode ? "└─" : "`-";
1071
1068
  const detail = (e.detail ?? "").split("\n").find(l => l.trim().length > 0)?.trim().slice(0, 140) ?? "";
1072
1069
  const summary = e.summary ? ` — ${e.summary}` : "";
1073
- // Fan-out slot key: the event's 1-based `index` when present (concurrent batch
1074
- // worker), else the fixed key 0 (single-task/detached run — matches the prior
1075
- // single-string behavior for the non-batch case).
1076
- const slot = e.index ?? 0;
1070
+ // Detached runs must retain their registry identity so concurrent detached events
1071
+ // cannot collide. Fan-out indices remain separate, while legacy synchronous/eval
1072
+ // events preserve their single shared slot.
1073
+ const slot = e.detached && e.id
1074
+ ? `detached:${e.id}`
1075
+ : e.index !== undefined
1076
+ ? `fanout:${e.index}`
1077
+ : "single";
1077
1078
  const touchSlot = (line: string) => {
1078
1079
  this.subagentLiveSlots.set(slot, line);
1079
1080
  const at = this.subagentLiveOrder.indexOf(slot);
@@ -1086,41 +1087,58 @@ export class LaunchTui {
1086
1087
  // activity readable in scrollback and visually separate from parent tools.
1087
1088
  switch (e.kind) {
1088
1089
  case "start":
1089
- touchSlot(`${roleLabel} ${this.unicode ? "▸" : ">"} ${detail || "starting"}`);
1090
- this.appendLedger(`${badge} ${this.unicode ? "▸" : ">"} ${roleLabel} · ${detail}\n`, "subagent");
1090
+ touchSlot(`${roleLabel}${detachedLabel} ${this.unicode ? "▸" : ">"} ${detail || "starting"}`);
1091
+ this.appendLedger(`${badge} ${this.unicode ? "▸" : ">"} ${roleLabel}${detachedLabel} · ${detail}\n`, "subagent");
1091
1092
  break;
1092
1093
  case "step":
1093
- touchSlot(`${roleLabel} ${this.unicode ? "·" : "-"} ${detail || "working"}`);
1094
- this.appendLedger(` ${badge} ${branch} ${roleLabel} · ${detail || "working"}\n`, "subagent");
1094
+ touchSlot(`${roleLabel}${detachedLabel} ${this.unicode ? "·" : "-"} ${detail || "working"}`);
1095
+ this.appendLedger(` ${badge} ${branch} ${roleLabel}${detachedLabel} · ${detail || "working"}\n`, "subagent");
1095
1096
  break;
1096
1097
  case "tool":
1097
- touchSlot(`${roleLabel} ${e.success === false ? bad : ok} ${detail || "tool"}`);
1098
- this.appendLedger(` ${badge} ${branch} ${roleLabel} ${e.success === false ? bad : ok} ${detail || "tool"}${summary}\n`, "subagent");
1098
+ touchSlot(`${roleLabel}${detachedLabel} ${e.success === false ? bad : ok} ${detail || "tool"}`);
1099
+ this.appendLedger(` ${badge} ${branch} ${roleLabel}${detachedLabel} ${e.success === false ? bad : ok} ${detail || "tool"}${summary}\n`, "subagent");
1099
1100
  break;
1100
1101
  case "error":
1101
- touchSlot(`${roleLabel} ${bad} ${detail || "error"}`);
1102
- this.appendLedger(` ${badge} ${branch} ${roleLabel} ${bad} ${detail || "error"}\n`, "subagent");
1102
+ touchSlot(`${roleLabel}${detachedLabel} ${bad} ${detail || "error"}`);
1103
+ this.appendLedger(` ${badge} ${branch} ${roleLabel}${detachedLabel} ${bad} ${detail || "error"}\n`, "subagent");
1103
1104
  break;
1104
1105
  case "thinking":
1105
1106
  // Live-only preview (mirrors the main turn's dimmed "Thinking" block) — never
1106
1107
  // persisted to the ledger; see TaskSubEvent.kind's doc comment for why.
1107
- touchSlot(`${roleLabel} ${this.unicode ? "…" : "..."} ${detail || "thinking"}`);
1108
+ touchSlot(`${roleLabel}${detachedLabel} ${this.unicode ? "…" : "..."} ${detail || "thinking"}`);
1108
1109
  break;
1109
1110
  case "done":
1110
- // Clear ONLY this slot — a fan-out batch where one worker finishes early
1111
- // must keep showing its still-running siblings, not drop the `(sub)`
1112
- // marker for the whole batch (the bug this per-slot map fixes).
1111
+ // Clear ONLY this slot — completing one concurrent run must leave its
1112
+ // still-running siblings visible and keep the `(sub)` marker active.
1113
1113
  this.subagentLiveSlots.delete(slot);
1114
1114
  {
1115
1115
  const at = this.subagentLiveOrder.indexOf(slot);
1116
1116
  if (at !== -1) this.subagentLiveOrder.splice(at, 1);
1117
1117
  }
1118
1118
  this.subagentActive = this.subagentLiveSlots.size > 0;
1119
- this.appendLedger(`${badge} ${last} ${roleLabel} done${e.tokens ? ` (${e.tokens.input + e.tokens.output} tok)` : ""}${e.success === false ? " (incomplete)" : ""}: ${detail}\n`, "subagent");
1119
+ this.appendLedger(`${badge} ${last} ${roleLabel}${detachedLabel} done${e.tokens ? ` (${e.tokens.input + e.tokens.output} tok)` : ""}${e.success === false ? " (incomplete)" : ""}: ${detail}\n`, "subagent");
1120
1120
  break;
1121
1121
  }
1122
1122
  this.draw();
1123
1123
  }
1124
+ /** Render turn-scoped monitor lifecycle/output separately from subagent activity. */
1125
+ onMonitorEvent(e: MonitorJobEvent): void {
1126
+ if (this.finished) return;
1127
+ const badge = categoryBadge("progress", { color: this.theme.color });
1128
+ const label = sanitizeForFrame(
1129
+ `${e.record.id} · ${e.record.category} · ${e.record.description}`,
1130
+ ).replace(/\s+/g, " ").trim().slice(0, 180);
1131
+ const branch = this.unicode ? "│" : "|";
1132
+ if (e.type === "start") {
1133
+ this.appendLedger(`${badge} ${this.unicode ? "◉" : "o"} Monitor ${label} started\n`, "monitor");
1134
+ } else if (e.type === "line") {
1135
+ const line = sanitizeForFrame(e.line).split("\n").find(row => row.trim())?.trim().slice(0, 180) ?? "";
1136
+ this.appendLedger(`${badge} ${branch} Monitor ${label}${line ? ` — ${line}` : ""}\n`, "monitor");
1137
+ } else {
1138
+ this.appendLedger(`${badge} ${this.unicode ? "└" : "`"} Monitor ${label} done\n`, "monitor");
1139
+ }
1140
+ this.draw();
1141
+ }
1124
1142
 
1125
1143
  start(): void {
1126
1144
  this.startedAt = Date.now();
@@ -1484,13 +1502,12 @@ export class LaunchTui {
1484
1502
  * block shows only the most-recent lines, capped at ~30% of the screen height (a
1485
1503
  * ceiling guards a tall terminal), so it grows with the stream and shrinks with the
1486
1504
  * viewport. Returns [] when there is nothing to show. */
1487
- /** gjc-parity multi-line parallel-subagent panel: when a fan-out `task` batch has
1488
- * MORE THAN ONE concurrent slot live, render one status line PER active slot
1505
+ /** gjc-parity multi-line parallel-subagent panel: when MORE THAN ONE concurrent
1506
+ * fan-out or detached slot is live, render one status line PER active slot
1489
1507
  * (most-recently-touched first) instead of collapsing everything into a single
1490
- * "+N more running" summary line (see `currentActivity()`). A single/detached
1491
- * subagent still uses the plain status-line path — this panel exists specifically
1492
- * to make a truly PARALLEL batch visibly read as parallel: every worker's latest
1493
- * activity is on screen at once, capped so a large fan-out can't blow the frame. */
1508
+ * "+N more running" summary line (see `currentActivity()`). A single subagent
1509
+ * still uses the plain status-line path; this panel makes concurrent work visibly
1510
+ * parallel while capping rows so a large batch cannot blow the frame. */
1494
1511
  private static readonly SUBAGENT_PANEL_MAX_ROWS = 6;
1495
1512
  private renderSubagentPanel(cols: number): string[] {
1496
1513
  if (this.subagentLiveSlots.size <= 1) return [];
@@ -1689,9 +1706,8 @@ export class LaunchTui {
1689
1706
  if (this.runningTool && this.liveToolOutput.trim()) {
1690
1707
  tail.push(...this.renderLiveBlock("Output", this.liveToolOutput, cols, rows, 8));
1691
1708
  }
1692
- // Parallel subagent panel (gjc parity): a fan-out `task` batch with more than one
1693
- // concurrent slot live gets its OWN multi-line block — one row per active worker —
1694
- // so a genuinely parallel batch reads as parallel instead of a single rotating line.
1709
+ // Parallel subagent panel: more than one fan-out or detached slot gets its own
1710
+ // multi-line block so genuinely parallel work does not read as one rotating line.
1695
1711
  if (this.subagentActive) {
1696
1712
  tail.push(...this.renderSubagentPanel(cols));
1697
1713
  }
@@ -293,6 +293,37 @@ export function summarizeForgeInvocation(tool: string, rawArgs: unknown, opts: {
293
293
 
294
294
  if (normalized === "task") {
295
295
  const role = stringArg(args, "role") ?? "executor";
296
+ // Fan-out/batch form (`tasks: [...]`, task-tool.ts's concurrent MAX_FANOUT path) previously
297
+ // fell through to the single-task branch below, which only reads `task`/`prompt`/`assignment`
298
+ // — every batch call rendered as a bare "<missing task>" placeholder with zero visibility into
299
+ // what was actually about to run concurrently, even on a well-formed call (live-reproduced: a
300
+ // real 3-item fan-out showed nothing but "<missing task>" in the tool-call preview box).
301
+ // A JSON-STRINGIFIED `tasks` (live-reproduced model mistake task-tool.ts's handler itself now
302
+ // tolerates — see createTaskTool's `tasksArg` coercion) is accepted here too, for the same
303
+ // reason: the preview must reflect what will actually run, not just the well-formed case.
304
+ const tasksValue = typeof args.tasks === "string"
305
+ ? (() => { try { const p = JSON.parse(args.tasks as string); return Array.isArray(p) ? p : args.tasks; } catch { return args.tasks; } })()
306
+ : args.tasks;
307
+ if (Array.isArray(tasksValue)) {
308
+ const items = (tasksValue as unknown[])
309
+ .map(entry => {
310
+ if (typeof entry === "string") return entry.trim();
311
+ if (entry && typeof entry === "object") {
312
+ const e = entry as Record<string, unknown>;
313
+ return String(e.task ?? e.assignment ?? e.prompt ?? "").trim();
314
+ }
315
+ return "";
316
+ })
317
+ .filter(Boolean);
318
+ const lines = items.length > 0
319
+ ? items.map((t, i) => {
320
+ const firstLine = t.split("\n")[0]!;
321
+ const truncated = firstLine.length > 120 || t.includes("\n");
322
+ return `${i + 1}. ${firstLine.slice(0, 120)}${truncated ? "…" : ""}`;
323
+ })
324
+ : ["<missing tasks>"];
325
+ return { title: `Task: ${role} ×${items.length}`, language: "text", lines };
326
+ }
296
327
  const task = stringArg(args, "task", "prompt", "assignment") ?? "<missing task>";
297
328
  const context = stringArg(args, "context");
298
329
  return {
@@ -0,0 +1,15 @@
1
+ // src/types/playwriter.d.ts
2
+ // Declaration file to allow TypeScript to recognize the optional Playwriter CLI module.
3
+ // This is only needed for the example automation script in scripts/playwriter-apply.ts.
4
+ // The actual Playwriter package is expected to be installed globally or run via npx.
5
+
6
+ declare module "playwriter" {
7
+ export class Playwriter {
8
+ /** Connect to the running Chrome extension bridge */
9
+ connect(): Promise<void>;
10
+ /** Open a new page (tab) in the connected browser */
11
+ newPage(): Promise<any>;
12
+ /** Disconnect from the bridge */
13
+ disconnect(): Promise<void>;
14
+ }
15
+ }