@plurnk/plurnk 0.71.4 → 0.72.0

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 (48) hide show
  1. package/README.md +4 -4
  2. package/dist/agui.d.ts +13 -0
  3. package/dist/agui.d.ts.map +1 -1
  4. package/dist/agui.js +54 -9
  5. package/dist/agui.js.map +1 -1
  6. package/dist/agui_cli.d.ts +12 -15
  7. package/dist/agui_cli.d.ts.map +1 -1
  8. package/dist/agui_cli.js +67 -34
  9. package/dist/agui_cli.js.map +1 -1
  10. package/dist/build-info.json +1 -1
  11. package/dist/cli.d.ts +6 -5
  12. package/dist/cli.d.ts.map +1 -1
  13. package/dist/cli.js +18 -7
  14. package/dist/cli.js.map +1 -1
  15. package/dist/completion.js +1 -1
  16. package/dist/completion.js.map +1 -1
  17. package/dist/diagnostics.d.ts +65 -0
  18. package/dist/diagnostics.d.ts.map +1 -0
  19. package/dist/diagnostics.js +228 -0
  20. package/dist/diagnostics.js.map +1 -0
  21. package/dist/dispatcher.d.ts +2 -5
  22. package/dist/dispatcher.d.ts.map +1 -1
  23. package/dist/dispatcher.js +85 -84
  24. package/dist/dispatcher.js.map +1 -1
  25. package/dist/render.d.ts +22 -5
  26. package/dist/render.d.ts.map +1 -1
  27. package/dist/render.js +37 -34
  28. package/dist/render.js.map +1 -1
  29. package/dist/stream.d.ts +4 -1
  30. package/dist/stream.d.ts.map +1 -1
  31. package/dist/stream.js +5 -4
  32. package/dist/stream.js.map +1 -1
  33. package/dist/subcommands.d.ts.map +1 -1
  34. package/dist/subcommands.js +16 -22
  35. package/dist/subcommands.js.map +1 -1
  36. package/dist/transport.d.ts +20 -7
  37. package/dist/transport.d.ts.map +1 -1
  38. package/dist/transport.js +141 -40
  39. package/dist/transport.js.map +1 -1
  40. package/dist/tui.d.ts +7 -4
  41. package/dist/tui.d.ts.map +1 -1
  42. package/dist/tui.js +129 -97
  43. package/dist/tui.js.map +1 -1
  44. package/package.json +6 -4
  45. package/dist/telemetry.d.ts +0 -48
  46. package/dist/telemetry.d.ts.map +0 -1
  47. package/dist/telemetry.js +0 -246
  48. package/dist/telemetry.js.map +0 -1
package/dist/tui.js CHANGED
@@ -21,11 +21,16 @@ import { extractOpenPaths } from "./openpaths.js";
21
21
  import { pathPartial, completePath, dslOpPartial, completeOps } from "./completion.js";
22
22
  import { renderLogEntry, renderSummary, isPromptEntry, coordLabel, progressLabel, entryTarget, isEntryMaterialization } from "./render.js";
23
23
  import { renderProposalMenu, keyToResolution, isServerResolved, questionFromProposal, renderQuestionMenu, answerForLine } from "./proposal.js";
24
- import { RunAckError } from "./transport.js";
25
- import { renderTelemetryEvent, report, clientSubcommandUnknownVerb, NO_MODEL_HINT } from "./telemetry.js";
24
+ import { ProblemError, renderDiagnostic, report, clientSubcommandUnknownVerb, NO_MODEL_HINT } from "./diagnostics.js";
26
25
  import StreamTrace, { inlineable, renderInline } from "./stream.js";
27
- import { runOAuth } from "./auth.js";
28
26
  import { runModels, runWorkspaceList, runWorkspaceWorkers, runLogRead } from "./subcommands.js";
27
+ export const renderTuiFailure = (cause) => {
28
+ if (cause instanceof ProblemError) {
29
+ return renderDiagnostic(cause.problem)
30
+ + (cause.problem.status === 501 ? NO_MODEL_HINT : "");
31
+ }
32
+ return ` \x1b[31merror: ${cause instanceof Error ? cause.message : String(cause)}\x1b[0m`;
33
+ };
29
34
  // One verb vocabulary across nvim's :AI/, the TUI, and (where they exist)
30
35
  // the argv subcommands. Convergence is policy: divergence needs a reason.
31
36
  // Singular = CREATE, plural = LIST: /workspace makes a new workspace, /workspaces
@@ -33,14 +38,14 @@ import { runModels, runWorkspaceList, runWorkspaceWorkers, runLogRead } from "./
33
38
  // ambiguous (workspace or worker?) and is gone. /rename retargets the current
34
39
  // workspace's mutable handle (a worker's name is immutable — no /rename for workers).
35
40
  export const VERBS = [
36
- "help", "models", "workspaces", "workers", "log", "model",
41
+ "help", "models", "workspaces", "workers", "log", "model", "child",
37
42
  "yolo", "workspace", "rename", "worker", "stop", "quit",
38
- "pick", "hide", "view", "repo", "drop", "members", "import", "script",
39
- "auth", "accept", "reject", "cancel", "edit",
43
+ "pick", "hide", "view", "drop", "members", "import", "script",
44
+ "accept", "reject", "cancel", "edit",
40
45
  ];
41
46
  export const TUI_HELP = [
42
- " /models /workspaces /runs /log [n] inspect",
43
- " /model <alias> model for subsequent loops",
47
+ " /models /workspaces /workers /log [n] inspect",
48
+ " /model <alias> · /child <alias|inherit> parent and child models",
44
49
  " /yolo toggle local auto-accept",
45
50
  " /workspace [name] new workspace",
46
51
  " /rename <name> rename this workspace (a mutable handle)",
@@ -48,12 +53,10 @@ export const TUI_HELP = [
48
53
  " /pick <glob> membership: track file(s) in manifest",
49
54
  " /hide <glob> membership: block file(s) from manifest",
50
55
  " /view <glob> membership: track file(s) in manifest (read-only)",
51
- " /repo <glob> membership: track a git repo folder",
52
- " /drop <glob> membership: remove tracking rules for file(s), remove git tracking for folder",
56
+ " /drop <glob> membership: remove a matching rule",
53
57
  " /members the model's resolved file universe (+ rules)",
54
58
  " /import <path> dump a local file's content into the prompt",
55
59
  " /script <path> run a .plk file (its DSL → op.parse)",
56
- " /auth <target> OAuth an auth-protected exec (e.g. notion) via device code",
57
60
  " /accept /reject /cancel /edit resolve a pending proposal (or keys a/e/r/c)",
58
61
  " /stop cancel the running loop",
59
62
  " /quit exit",
@@ -86,7 +89,7 @@ export const expandNewlines = (line) => line.replaceAll(NL_MARK, "\n");
86
89
  // doesn't change a control code), Ctrl-s is XOFF, Ctrl-i is Tab, and readline
87
90
  // owns a/e/n/p/u/w/k/l. Alt-b/f/d are skipped (readline word-ops).
88
91
  export const ALT_SHORTCUTS = Object.freeze({
89
- m: "/models", s: "/workspaces", R: "/runs", L: "/log",
92
+ m: "/models", s: "/workspaces", R: "/workers", L: "/log",
90
93
  Y: "/yolo", N: "/workspace", M: "/members", x: "/stop", h: "/help",
91
94
  });
92
95
  // An Alt-<letter> keypress (ESC then a single letter, no `[`/`O` → not an arrow
@@ -96,14 +99,9 @@ export const altShortcut = (forward) => {
96
99
  const m = forward.match(/^\x1b([a-zA-Z])$/);
97
100
  return m ? (ALT_SHORTCUTS[m[1]] ?? null) : null;
98
101
  };
99
- // `<<LOOK…::LOOK` is a CLIENT pseudo-op "READ, but for me instead of the model".
100
- // The grammar (plurnk-grammar plurnk.md §Operations) terminates a statement with its
101
- // own op name (`<<OP…:body:OP`), so a faithful rewrite swaps BOTH ends LOOK→READ and passes
102
- // [signal](target)<sel>:body through untouched — the daemon only ever sees a real
103
- // READ. `\b` keeps `<<LOOKUP` from matching. null when the line isn't a LOOK.
104
- export const lookRewrite = (line) => /^<<LOOK\b/i.test(line)
105
- ? line.replace(/^<<LOOK\b/i, "<<READ").replace(/:LOOK(\s*)$/i, ":READ$1")
106
- : null;
102
+ // Recognize the client-only LOOK surface so it can be routed to `op.look`.
103
+ // The AG-UI observation action owns validation and the single LOOK→READ rewrite.
104
+ export const lookStatement = (line) => /^<<LOOK\b/i.test(line) ? line : null;
107
105
  // §2.0 prompt prefixes (converged with nvim's :AI ? / :AI :): `? ` puts mode:"ask"
108
106
  // on the wire flags, `: ` forces act, both overriding a --flags base mode; `...`
109
107
  // (injection) strips without minting flags. Mode is a per-line habit, never --ask.
@@ -135,7 +133,7 @@ export const parseSlash = (line) => {
135
133
  const m = line.match(/^\/(\S*)\s*(.*)$/);
136
134
  return { verb: m?.[1] ?? "", rest: (m?.[2] ?? "").trim() };
137
135
  };
138
- // readline completer — verbs after `/`, model aliases after `/model `.
136
+ // readline completer — verbs after `/`, model aliases after `/model ` or `/child `.
139
137
  // Plain readline machinery only; no screen takeover, no terminal hell.
140
138
  // readline's async completer form (arity 2 → async). Verb/alias completion
141
139
  // answers synchronously; path positions read the local fs (co-location law)
@@ -146,9 +144,12 @@ export const makeCompleter = (getAliases, cwd) => (line, callback) => {
146
144
  callback(null, [VERBS.map((v) => `/${v}`).filter((v) => v.startsWith(line)), line]);
147
145
  return;
148
146
  }
149
- const aliasFrag = line.match(/^\/model\s+(\S*)$/);
147
+ const aliasFrag = line.match(/^\/(model|child)\s+(\S*)$/);
150
148
  if (aliasFrag) {
151
- callback(null, [getAliases().filter((a) => a.startsWith(aliasFrag[1])), aliasFrag[1]]);
149
+ const candidates = aliasFrag[1] === "child"
150
+ ? ["inherit", ...getAliases().filter((alias) => alias !== "inherit")]
151
+ : getAliases();
152
+ callback(null, [candidates.filter((a) => a.startsWith(aliasFrag[2])), aliasFrag[2]]);
152
153
  return;
153
154
  }
154
155
  const op = dslOpPartial(line);
@@ -223,6 +224,21 @@ export const handleVerb = async (line, ctx) => {
223
224
  opts.model = ctx.resolveModel?.(rest);
224
225
  write(` model: ${rest}\n`);
225
226
  return;
227
+ case "child":
228
+ if (rest.length === 0) {
229
+ write(` child: ${opts.childAlias ?? "inherit"}\n`);
230
+ return;
231
+ }
232
+ if (rest === "inherit") {
233
+ opts.childAlias = null;
234
+ opts.childModel = undefined;
235
+ }
236
+ else {
237
+ opts.childAlias = rest;
238
+ opts.childModel = ctx.resolveModel?.(rest);
239
+ }
240
+ write(` child: ${rest}\n`);
241
+ return;
226
242
  case "yolo":
227
243
  opts.yolo = !opts.yolo;
228
244
  write(` yolo: ${opts.yolo ? "ON" : "OFF"}\n`);
@@ -260,9 +276,8 @@ export const handleVerb = async (line, ctx) => {
260
276
  }
261
277
  case "pick":
262
278
  case "hide":
263
- case "view":
264
- case "repo": {
265
- // Membership overlay (svc#200/#242) — service vocabulary, live via
279
+ case "view": {
280
+ // Membership overlay — service vocabulary, live via
266
281
  // workspace.constrain (workspace-scoped, re-resolved immediately).
267
282
  if (rest.length === 0) {
268
283
  write(` usage: /${verb} <glob>\n`);
@@ -298,7 +313,7 @@ export const handleVerb = async (line, ctx) => {
298
313
  const editable = members.filter((m) => m.effect === "member");
299
314
  const view = members.filter((m) => m.effect === "view");
300
315
  if (members.length === 0 && hidden.length === 0) {
301
- write(" the model's universe is empty — no members (/pick a file or /repo a folder)\n");
316
+ write(" the model's universe is empty — no Git members or /pick rules\n");
302
317
  }
303
318
  else {
304
319
  write(` the model's universe: ${members.length} file${members.length === 1 ? "" : "s"}`
@@ -351,19 +366,6 @@ export const handleVerb = async (line, ctx) => {
351
366
  // Typed no-modifier fallback for the a/e/r/c proposal review keys.
352
367
  await ctx.resolveProposal(verb);
353
368
  return;
354
- case "auth": {
355
- // #116 — the OAuth device-grant leg: authorize → print URL + code →
356
- // poll to authorized. Blocks the REPL while polling (Ctrl-C exits); the
357
- // service half is stateless, we own the poll loop + expiry bound.
358
- const target = rest.trim();
359
- if (target.length === 0) {
360
- write(" \x1b[2musage: /auth <target> (the exec needing auth, e.g. notion)\x1b[0m");
361
- return;
362
- }
363
- const r = await runOAuth(rpc, target, { print: write });
364
- write(` ${r.ok ? "✅" : "❌"} ${r.message}`);
365
- return;
366
- }
367
369
  case "stop":
368
370
  await rpc.call("loop.cancel", { reason: "user_stop" });
369
371
  return;
@@ -390,6 +392,7 @@ export const runTui = async (transport, workspace, opts) => {
390
392
  let embeddingPercent = null;
391
393
  let searchFetching = false;
392
394
  let searchPercent = null;
395
+ let branchBatch = null;
393
396
  let hibernating = false; // the loop parked on a SEND[202] (awaiting streams/workers)
394
397
  // <<LOOK off-run inspection: the REAL target URIs of prior operations the
395
398
  // waterfall has shown (oldest→newest, e.g. worker:///plan.md) feed the Alt-p/
@@ -448,13 +451,12 @@ export const runTui = async (transport, workspace, opts) => {
448
451
  // the row identity. 🐹 (U+1F439, the brand head — was the generic 👤
449
452
  // U+1F464) and ✅ (U+2705) are plain East-Asian-Wide single code points —
450
453
  // width 2 in node AND every major terminal, no VS16 — so they stay; the
451
- // 201 is the contract constant (the prompt row is always a 201 EDIT);
454
+ // 201 is the input-affordance constant;
452
455
  // 💬 (U+1F4AC, stable-wide) fills the op slot the toxic ✉️ vacated.
453
456
  // Policy: VS16/ambiguous glyphs are banned from the ENTIRE palette
454
457
  // (render.ts) — stable widths are also what make columns align.
455
458
  // The prompt is the user's row, coordinate-prefixed like every other
456
- // waterfall line (§5.1). The coordinate is the one the typed line will
457
- // get: the prompt becomes the next loop's foist EDIT at <next>/01/01.
459
+ // waterfall line (§5.1). The coordinate is the next actionless prompt row.
458
460
  // lastLoopSeq tracks the highest loop the waterfall has shown; the next
459
461
  // prompt is one beyond it. Plain-ASCII coordinate — width-safe.
460
462
  // The prompt is the SAME whether or not a loop is running — kept alive above
@@ -474,11 +476,23 @@ export const runTui = async (transport, workspace, opts) => {
474
476
  // TWO lanes exactly, same as every waterfall row (identity · status), so the
475
477
  // prompt sits flush in the ladder. All glyphs are same-width palette members;
476
478
  // swapping in place keeps the columns and the readline cursor stable.
477
- const busy = inFlight || embedding || searchFetching;
478
- const origin = embedding ? "🧮" : searchFetching ? "🔎" : "🐹";
479
- const status = busy ? (inFlight && hibernating ? "💤" : "⏳") : " ";
479
+ const busy = inFlight || embedding || searchFetching || branchBatch !== null;
480
+ const origin = embedding ? "🧮" : searchFetching ? "🔎" : branchBatch !== null ? "🌿" : "🐹";
481
+ const status = branchBatch?.state === "recovery_required"
482
+ ? "❌"
483
+ : busy ? (inFlight && hibernating && branchBatch === null ? "💤" : "⏳") : " ";
480
484
  const yolo = opts.yolo ? "🔥" : " ";
481
- const activePercent = embedding ? embeddingPercent : searchFetching ? searchPercent : null;
485
+ const branchCompleted = Number(branchBatch?.completed);
486
+ const branchTotal = Number(branchBatch?.total);
487
+ const branchPercent = branchBatch !== null
488
+ && Number.isFinite(branchCompleted)
489
+ && Number.isFinite(branchTotal)
490
+ && branchTotal > 0
491
+ ? Math.floor((branchCompleted / branchTotal) * 100)
492
+ : null;
493
+ const activePercent = embedding ? embeddingPercent
494
+ : searchFetching ? searchPercent
495
+ : branchPercent;
482
496
  const position = activePercent !== null
483
497
  ? progressLabel(activePercent)
484
498
  : coordLabel(lastLoopSeq + 1, 1, 1);
@@ -539,17 +553,14 @@ export const runTui = async (transport, workspace, opts) => {
539
553
  return;
540
554
  setLine(`<<LOOK(${priorTargets[lookCursor]})::LOOK`);
541
555
  };
542
- // `<<LOOK(uri)` already rewritten (op token LOOK→READ) to a READ statement.
543
- // LOOK is a PURE QUERY: op.look (plurnk-service#283, shipped v0.57.0) runs READ's
544
- // full resolver but writes NO log entry — so the model never sees it and there's no
545
- // run to manage. Run on the main connection (the conversation run, so log:/// resolves
546
- // run-relative against the right run). Render the content; no op, no harvest. Trust
547
- // the contract: status is required, content is `string | null` (svc ReadResult).
548
- const runLook = async (readText) => {
549
- const r = await transport.rpc("op.look", { text: readText });
556
+ // LOOK is a pure query: AG-UI validates and rewrites the original statement, then
557
+ // resolves READ without writing a log entry. Run it on the conversation connection
558
+ // so run-relative targets resolve against the right run.
559
+ const runLook = async (lookText) => {
560
+ const r = await transport.rpc("op.look", { text: lookText });
550
561
  const content = r.content ?? "";
551
562
  printAbove(content.length > 0 ? content : ` \x1b[2m(look ${r.status}: no content)\x1b[0m`);
552
- return r.status;
563
+ return r;
553
564
  };
554
565
  // Multi-line composition. The non-submitting newline keys insert a single ↵
555
566
  // marker into readline's ONE line; Enter submits, expanding ↵→\n. Keeping it
@@ -625,7 +636,7 @@ export const runTui = async (transport, workspace, opts) => {
625
636
  await transport.resolve({ logEntryId: p.logEntryId, ...resolution });
626
637
  }
627
638
  catch (cause) {
628
- process.stdout.write(` \x1b[31mresolve failed: ${cause instanceof Error ? cause.message : String(cause)}\x1b[0m\n`);
639
+ process.stdout.write(`${renderTuiFailure(cause)}\n`);
629
640
  }
630
641
  showNextProposal();
631
642
  reprompt();
@@ -644,7 +655,7 @@ export const runTui = async (transport, workspace, opts) => {
644
655
  resolution = (await keyToResolution("e", p)) ?? { decision: "cancel", outcome: "edit_failed" };
645
656
  }
646
657
  catch (cause) {
647
- process.stdout.write(` \x1b[31medit failed: ${cause instanceof Error ? cause.message : String(cause)}\x1b[0m\n`);
658
+ process.stdout.write(`${renderTuiFailure(cause)}\n`);
648
659
  resolution = { decision: "cancel", outcome: "edit_error" };
649
660
  }
650
661
  finally {
@@ -668,19 +679,19 @@ export const runTui = async (transport, workspace, opts) => {
668
679
  // transport is live. Same bodies as the old inline rpc.onNotification handlers;
669
680
  // they render the shared workspace's activity whether this REPL started the loop
670
681
  // or a worker/second client did (multi-client observability).
671
- const handleTelemetry = (event) => {
682
+ const handleNotice = (notice) => {
672
683
  // engine:turn liveness is the ⏳ gutter (inFlight), not a waterfall line.
673
- if (event.source === "engine:turn")
684
+ if (notice.source === "engine:turn")
674
685
  return;
675
686
  // Indexing progress is ephemeral prompt state, not an append-only waterfall
676
687
  // record. The coordinate slot becomes a fixed-width percentage while warm,
677
688
  // then returns to the next real L/T/S coordinate on completion.
678
- if (event.source === "engine:derivation" && event.kind === "embed_progress") {
679
- const phase = typeof event.phase === "string" ? event.phase : null;
689
+ if (notice.source === "engine:derivation" && notice.kind === "embed_progress") {
690
+ const phase = typeof notice.phase === "string" ? notice.phase : null;
680
691
  embedding = phase === "preparing"
681
- || (phase !== "complete" && phase !== "failed" && Number(event.completed) < Number(event.total));
682
- const completed = Number(event.completed);
683
- const total = Number(event.total);
692
+ || (phase !== "complete" && phase !== "failed" && Number(notice.completed) < Number(notice.total));
693
+ const completed = Number(notice.completed);
694
+ const total = Number(notice.total);
684
695
  embeddingPercent = embedding && Number.isFinite(completed) && Number.isFinite(total) && total > 0
685
696
  ? Math.floor((completed / total) * 100)
686
697
  : null;
@@ -688,15 +699,37 @@ export const runTui = async (transport, workspace, opts) => {
688
699
  return;
689
700
  }
690
701
  // Search acquisition is the same compact lifecycle shape: update the
691
- // fixed-width prompt gauge, never append one telemetry line per tick.
692
- if (event.kind === "search_progress" && typeof event.source === "string" && event.source.startsWith("exec:")) {
693
- searchFetching = event.phase !== "complete" && event.phase !== "failed";
694
- const percent = Number(event.percent);
702
+ // fixed-width prompt gauge, never append one notice line per tick.
703
+ if (notice.kind === "search_progress" && notice.source.startsWith("exec:")) {
704
+ searchFetching = notice.phase !== "complete" && notice.phase !== "failed";
705
+ const percent = Number(notice.percent);
695
706
  searchPercent = searchFetching && Number.isFinite(percent) ? percent : null;
696
707
  repromptPreserving();
697
708
  return;
698
709
  }
699
- printAbove(renderTelemetryEvent(event));
710
+ printAbove(renderDiagnostic(notice));
711
+ };
712
+ const handleBranchBatch = (event) => {
713
+ let rendered = false;
714
+ if (event.state === "completed") {
715
+ branchBatch = null;
716
+ printAbove(` 🌿 branch batch ${event.batchId} complete (${event.completed ?? event.total ?? 0}/${event.total ?? event.completed ?? 0})`);
717
+ rendered = true;
718
+ }
719
+ else if (event.state === "failed") {
720
+ branchBatch = null;
721
+ printAbove(` \x1b[31m❌ branch batch ${event.batchId} failed: ${event.problem?.detail ?? "branch preflight failed"}\x1b[0m`);
722
+ rendered = true;
723
+ }
724
+ else {
725
+ branchBatch = event;
726
+ if (event.state === "recovery_required") {
727
+ printAbove(` \x1b[31m❌ branch batch ${event.batchId} requires recovery: ${event.problem?.detail ?? "inspect the workspace Git state"}\x1b[0m`);
728
+ rendered = true;
729
+ }
730
+ }
731
+ if (!rendered)
732
+ repromptPreserving();
700
733
  };
701
734
  transport.subscribe({
702
735
  onEntry: (entry) => {
@@ -716,17 +749,16 @@ export const runTui = async (transport, workspace, opts) => {
716
749
  hibernating = entry.op === "SEND" && entry.signal === 202;
717
750
  printAbove(renderLogEntry(entry));
718
751
  },
719
- onTelemetry: handleTelemetry,
752
+ onNotice: handleNotice,
753
+ onProblem: (problem) => printAbove(renderDiagnostic(problem)),
754
+ onBranchBatch: handleBranchBatch,
720
755
  onStream: (payload) => {
721
- // One channel for the lifecycle: concluded carries closeStatus, a start
756
+ // One channel for the lifecycle: concluded carries its exact result, a start
722
757
  // event carries state. One start line, one conclusion line, tiny outputs inlined.
723
- if (typeof payload.closeStatus === "number") {
758
+ if (typeof payload.result?.status === "number") {
724
759
  const p = payload;
725
760
  printAbove(streams.concluded(p));
726
- // #116 a 401 close offers the device-grant flow (run /auth <scheme>).
727
- if (p.closeStatus === 401)
728
- printAbove(` 🔒 ${p.scheme} needs authorization — run \x1b[1m/auth ${p.scheme}\x1b[0m`);
729
- void transport.rpc("entry.read", { target: p.target }).then((r) => {
761
+ void transport.rpc("entry.read", { target: p.target, workerId: p.workerId }).then((r) => {
730
762
  const channels = r.entry?.channels ?? {};
731
763
  for (const name of ["stdout", "stderr"]) {
732
764
  const content = channels[name]?.content;
@@ -767,7 +799,7 @@ export const runTui = async (transport, workspace, opts) => {
767
799
  });
768
800
  // Startup warming begins while AG-UI is still establishing the workspace, before a
769
801
  // request-scoped SSE exists. Poll the engine's latest structured state until terminal;
770
- // later /repo actions stay on their own SSE until warming completes and stream live events.
802
+ // Later membership actions stay on their own SSE while warming streams live events.
771
803
  let lastDerivationState = "";
772
804
  let derivationPoll = null;
773
805
  const pollDerivation = async () => {
@@ -782,7 +814,7 @@ export const runTui = async (transport, workspace, opts) => {
782
814
  const signature = JSON.stringify(status);
783
815
  if (signature !== lastDerivationState) {
784
816
  lastDerivationState = signature;
785
- handleTelemetry({ ...status, source: "engine:derivation", kind: "embed_progress" });
817
+ handleNotice({ ...status, source: "engine:derivation", kind: "embed_progress" });
786
818
  }
787
819
  if (status.phase === "complete" || status.phase === "failed") {
788
820
  if (derivationPoll !== null)
@@ -807,7 +839,7 @@ export const runTui = async (transport, workspace, opts) => {
807
839
  rpc: verbRpc, opts, resolveModel: opts.resolveModel,
808
840
  getWorkspace: () => current,
809
841
  setWorkspace: (s) => { current = s; },
810
- switchWorkspace: (name) => transport.useSession(name, { projectRoot: opts.projectRoot, client: opts.client, autoReadAgents: opts.autoReadAgents }),
842
+ switchWorkspace: (name) => transport.useSession(name, { projectRoot: opts.projectRoot, client: opts.client }),
811
843
  write: (text) => { process.stdout.write(text); },
812
844
  importFile: async (rest) => {
813
845
  const abs = isAbsolute(rest) ? rest : resolve(process.cwd(), rest);
@@ -846,7 +878,7 @@ export const runTui = async (transport, workspace, opts) => {
846
878
  }
847
879
  }
848
880
  catch (cause) {
849
- process.stdout.write(` \x1b[31merror: ${cause instanceof Error ? cause.message : String(cause)}\x1b[0m\n`);
881
+ process.stdout.write(`${renderTuiFailure(cause)}\n`);
850
882
  }
851
883
  reprompt();
852
884
  })();
@@ -899,8 +931,7 @@ export const runTui = async (transport, workspace, opts) => {
899
931
  }
900
932
  }
901
933
  catch (cause) {
902
- const msg = cause instanceof Error ? cause.message : String(cause);
903
- process.stdout.write(` \x1b[31merror: ${msg}\x1b[0m\n`);
934
+ process.stdout.write(`${renderTuiFailure(cause)}\n`);
904
935
  }
905
936
  reprompt();
906
937
  return;
@@ -929,26 +960,25 @@ export const runTui = async (transport, workspace, opts) => {
929
960
  reprompt();
930
961
  const start = Date.now();
931
962
  let turnCount = 0;
932
- let finalStatus = 0;
963
+ let terminalResult = { status: 0 };
933
964
  let hitMaxTurns = false;
934
965
  let usage;
935
966
  try {
936
- const lookText = trimmed.startsWith("<<") ? lookRewrite(trimmed) : null;
967
+ const lookText = trimmed.startsWith("<<") ? lookStatement(trimmed) : null;
937
968
  if (lookText !== null) {
938
969
  // <<LOOK: off-run READ on the side connection — for me, not the model.
939
- finalStatus = await runLook(lookText);
970
+ terminalResult = await runLook(lookText);
940
971
  }
941
972
  else if (trimmed.startsWith("<<")) {
942
973
  // Raw DSL: send to op.parse
943
974
  const result = await transport.rpc("op.parse", { text: trimmed });
944
- finalStatus = result.results[result.results.length - 1]?.status ?? 0;
975
+ terminalResult = result.results[result.results.length - 1] ?? { status: 0 };
945
976
  }
946
977
  else if (trimmed.startsWith("!")) {
947
978
  // `! cmd` — exec via the daemon (proposal-gated like any
948
979
  // side effect; output streams as stream/event traces).
949
980
  const command = trimmed.replace(/^!+\s*/, "");
950
- const result = await transport.rpc("op.exec", { command });
951
- finalStatus = result.status;
981
+ terminalResult = await transport.rpc("op.exec", { command });
952
982
  }
953
983
  else {
954
984
  // Prompt. `? ` = ask (read-only loop, flags.mode="ask");
@@ -959,6 +989,10 @@ export const runTui = async (transport, workspace, opts) => {
959
989
  loopParams.alias = opts.modelAlias; // display label
960
990
  if (opts.model !== undefined)
961
991
  loopParams.model = opts.model; // #90 client-resolved routing (precedence)
992
+ if (opts.childAlias !== undefined)
993
+ loopParams.childAlias = opts.childAlias;
994
+ if (opts.childModel !== undefined)
995
+ loopParams.childModel = opts.childModel;
962
996
  if (lineFlags !== undefined && Object.keys(lineFlags).length > 0)
963
997
  loopParams.flags = lineFlags;
964
998
  if (opts.maxTurns !== undefined)
@@ -967,22 +1001,19 @@ export const runTui = async (transport, workspace, opts) => {
967
1001
  if (openPaths.length > 0)
968
1002
  loopParams.openPaths = openPaths;
969
1003
  // The transport owns the ack→terminated bridge; done resolves
970
- // with the loop's outcome. A synchronous ACK failure surfaces as
971
- // RunAckError (caught below; 501 gets the .env pointer, #120).
1004
+ // with the loop's outcome. A pre-stream HTTP failure surfaces as
1005
+ // an exact ProblemError (caught below; 501 gets the .env pointer).
972
1006
  const t = await transport.run(promptText, loopParams).done;
973
- finalStatus = t.finalStatus;
1007
+ terminalResult = t.result;
974
1008
  hitMaxTurns = t.hitMaxTurns;
975
1009
  turnCount = t.turnIds?.length ?? 0;
976
1010
  usage = t.usage;
977
1011
  }
978
1012
  const wallMs = Date.now() - start;
979
- printAbove(renderSummary(turnCount, wallMs, finalStatus, hitMaxTurns, usage, usage?.promptBudget));
1013
+ printAbove(renderSummary(turnCount, wallMs, terminalResult, hitMaxTurns, usage, usage?.promptBudget));
980
1014
  }
981
1015
  catch (cause) {
982
- const msg = cause instanceof RunAckError && cause.status === 501
983
- ? cause.message + NO_MODEL_HINT
984
- : cause instanceof Error ? cause.message : String(cause);
985
- printAbove(` \x1b[31merror: ${msg}\x1b[0m`);
1016
+ printAbove(renderTuiFailure(cause));
986
1017
  }
987
1018
  finally {
988
1019
  inFlight = false;
@@ -999,6 +1030,7 @@ export const runTui = async (transport, workspace, opts) => {
999
1030
  process.stdout.write("\x1b[?2004l");
1000
1031
  process.stdin.off("data", onStdin);
1001
1032
  process.stdin.setRawMode?.(false);
1033
+ process.stdin.pause();
1002
1034
  // Steal pi's nicety: hand back the one-liner to pick this workspace up.
1003
1035
  process.stdout.write(`\n \x1b[2mresume this workspace: plurnk --workspace ${current.name}\x1b[0m\n`);
1004
1036
  resolve();