@pugi/cli 0.1.0-beta.21 → 0.1.0-beta.23

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 (63) hide show
  1. package/dist/core/auth/env-provider.js +238 -0
  2. package/dist/core/bare-mode/index.js +107 -0
  3. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  4. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  5. package/dist/core/engine/native-pugi.js +55 -11
  6. package/dist/core/engine/prompts.js +30 -2
  7. package/dist/core/engine/tool-bridge.js +32 -0
  8. package/dist/core/feedback/queue.js +177 -0
  9. package/dist/core/feedback/submitter.js +145 -0
  10. package/dist/core/onboarding/marker.js +111 -0
  11. package/dist/core/onboarding/telemetry-state.js +108 -0
  12. package/dist/core/output-style/presets.js +176 -0
  13. package/dist/core/output-style/state.js +185 -0
  14. package/dist/core/permissions/index.js +1 -1
  15. package/dist/core/permissions/state.js +55 -0
  16. package/dist/core/pugi-md/context-injector.js +76 -0
  17. package/dist/core/pugi-md/walk-up.js +207 -0
  18. package/dist/core/release-notes/parser.js +241 -0
  19. package/dist/core/release-notes/state.js +116 -0
  20. package/dist/core/repl/session.js +482 -12
  21. package/dist/core/repl/slash-commands.js +134 -1
  22. package/dist/core/repl/workspace-context.js +22 -0
  23. package/dist/core/share/formatter.js +271 -0
  24. package/dist/core/share/redactor.js +221 -0
  25. package/dist/core/share/uploader.js +267 -0
  26. package/dist/core/theme/context.js +91 -0
  27. package/dist/core/theme/presets.js +228 -0
  28. package/dist/core/theme/state.js +181 -0
  29. package/dist/core/todos/invariant.js +10 -0
  30. package/dist/core/todos/state.js +177 -0
  31. package/dist/core/vim/keymap.js +288 -0
  32. package/dist/core/vim/state.js +92 -0
  33. package/dist/runtime/cli.js +603 -15
  34. package/dist/runtime/commands/doctor.js +21 -0
  35. package/dist/runtime/commands/feedback.js +184 -0
  36. package/dist/runtime/commands/onboarding.js +275 -0
  37. package/dist/runtime/commands/plan.js +143 -0
  38. package/dist/runtime/commands/release-notes.js +229 -0
  39. package/dist/runtime/commands/share.js +316 -0
  40. package/dist/runtime/commands/stickers.js +82 -0
  41. package/dist/runtime/commands/style.js +194 -0
  42. package/dist/runtime/commands/theme.js +196 -0
  43. package/dist/runtime/commands/vim.js +140 -0
  44. package/dist/runtime/version.js +1 -1
  45. package/dist/tools/registry.js +8 -0
  46. package/dist/tools/todo-write.js +184 -0
  47. package/dist/tui/compact-banner.js +28 -1
  48. package/dist/tui/conversation-pane.js +13 -0
  49. package/dist/tui/doctor-table.js +32 -17
  50. package/dist/tui/feedback-prompt.js +156 -0
  51. package/dist/tui/onboarding-wizard.js +240 -0
  52. package/dist/tui/repl-render.js +26 -3
  53. package/dist/tui/repl.js +9 -1
  54. package/dist/tui/stickers-art.js +136 -0
  55. package/dist/tui/style-table.js +28 -0
  56. package/dist/tui/theme-table.js +29 -0
  57. package/dist/tui/vim-input.js +267 -0
  58. package/package.json +2 -2
  59. package/dist/core/engine/compaction-hook.js +0 -154
  60. package/dist/core/init/scaffold.js +0 -195
  61. package/dist/core/repl/codebase-survey.js +0 -308
  62. package/dist/core/repl/init-interview.js +0 -457
  63. package/dist/core/repl/onboarding-state.js +0 -297
@@ -78,14 +78,23 @@ export const SLASH_COMMAND_HELP = Object.freeze([
78
78
  // Settings
79
79
  { name: 'config', args: '', gloss: 'Show config', group: 'Settings', stub: true },
80
80
  { name: 'privacy', args: '', gloss: 'Show privacy mode + contract', group: 'Settings' },
81
- { name: 'permissions', args: '[mode] [--persist]', gloss: 'Show or flip permission mode (plan / ask / allow / bypass)', group: 'Settings' },
81
+ { name: 'permissions', args: '[mode] [--persist]', gloss: 'Show or flip permission mode (plan / ask / allow / bypass) (also: /plan)', group: 'Settings' },
82
+ { name: 'plan', args: '[--back | --persist] [<prompt>]', gloss: 'Switch to plan mode (read-only). Same as /permissions plan, slicker UX.', group: 'Settings' },
82
83
  { name: 'budget', args: '', gloss: 'Show usage budget', group: 'Settings', stub: true },
83
84
  { name: 'mcp', args: '[sub]', gloss: 'MCP servers — list / trust / deny / install / serve / perms', group: 'Settings' },
85
+ { name: 'style', args: '[name] [--persist|--reset|--list]', gloss: 'Output-style preset (default / terse / explanatory / russian-formal / casual)', group: 'Settings' },
86
+ { name: 'theme', args: '[name] [--persist|--reset|--list]', gloss: 'TUI color palette (default / dark / light / colorblind)', group: 'Settings' },
87
+ { name: 'onboarding', args: '[--reset|--non-interactive]', gloss: 'First-run wizard — auth / mode / style / MCP / telemetry (leak L25)', group: 'Settings' },
88
+ { name: 'vim', args: '[on|off|status]', gloss: 'Toggle vim-style modal editing in the input buffer (leak L26)', group: 'Settings' },
84
89
  { name: 'undo', args: '', gloss: 'Undo last write', group: 'Settings', stub: true },
85
90
  // Meta
86
91
  { name: 'help', args: '', gloss: 'Show this help overlay', group: 'Meta' },
87
92
  { name: 'version', args: '', gloss: 'Show CLI version', group: 'Meta' },
88
93
  { name: 'doctor', args: '', gloss: 'Environment health report (auth · API · Node · disk · MCP · …)', group: 'Meta' },
94
+ { name: 'stickers', args: '', gloss: 'show Pugi brand stickers (gimmick)', group: 'Meta' },
95
+ { name: 'feedback', args: '', gloss: 'file a bug / feature / general comment without leaving the REPL', group: 'Meta' },
96
+ { name: 'share', args: '[--gist|--pugi] [--redact] [--preview]', gloss: 'Export session transcript to gist / pugi.io (leak L20)', group: 'Meta' },
97
+ { name: 'release-notes', args: '[--reset]', gloss: 'Show changelog diff since last upgrade (leak L24)', group: 'Meta' },
89
98
  { name: 'quit', args: '', gloss: 'Exit the REPL', group: 'Meta' },
90
99
  ]);
91
100
  /**
@@ -317,6 +326,54 @@ export function parseSlashCommand(input) {
317
326
  // skills.
318
327
  return { kind: 'init' };
319
328
  }
329
+ case 'plan': {
330
+ // Leak L7: `/plan [--back | --persist] [<prompt>]`.
331
+ //
332
+ // Argument grammar (single line, no quoting):
333
+ // /plan -> enter plan mode + banner
334
+ // /plan --back -> restore previous mode
335
+ // /plan --persist -> enter + write global config
336
+ // /plan <prompt...> -> enter + run one-shot engine
337
+ // /plan --auto-back <prompt...> -> enter + run + restore mode
338
+ //
339
+ // The parser pulls the flags off the head of the tail; whatever
340
+ // remains is the prompt. `--back` + a non-empty prompt and
341
+ // `--back` + `--auto-back` are both refused as `error` because
342
+ // they conflict at the verb level.
343
+ const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
344
+ let back = false;
345
+ let persist = false;
346
+ let autoBack = false;
347
+ const promptTokens = [];
348
+ for (const token of tokens) {
349
+ if (token === '--back') {
350
+ back = true;
351
+ }
352
+ else if (token === '--persist') {
353
+ persist = true;
354
+ }
355
+ else if (token === '--auto-back') {
356
+ autoBack = true;
357
+ }
358
+ else {
359
+ promptTokens.push(token);
360
+ }
361
+ }
362
+ const prompt = promptTokens.join(' ');
363
+ if (back && prompt.length > 0) {
364
+ return {
365
+ kind: 'error',
366
+ message: '/plan --back does not accept a prompt; revert first, then dispatch.',
367
+ };
368
+ }
369
+ if (back && autoBack) {
370
+ return {
371
+ kind: 'error',
372
+ message: '/plan --back and --auto-back cannot be combined.',
373
+ };
374
+ }
375
+ return { kind: 'plan', back, persist, autoBack, prompt };
376
+ }
320
377
  case 'mcp': {
321
378
  // β4 Sl7: tokenize the tail. Empty tail -> `list` (matches CLI).
322
379
  // Quoting / shell-escapes are NOT supported — the slash surface is
@@ -325,6 +382,45 @@ export function parseSlashCommand(input) {
325
382
  const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
326
383
  return { kind: 'mcp', args: tokens };
327
384
  }
385
+ case 'style':
386
+ case 'output-style': {
387
+ // Leak L18 (2026-05-27): forward the tokenized tail unchanged so
388
+ // the slash + top-level CLI surfaces share one parser inside
389
+ // `runStyleCommand`. Quoting / multi-word args are not used (the
390
+ // preset slugs are single tokens by contract).
391
+ const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
392
+ return { kind: 'style', args: tokens };
393
+ }
394
+ case 'theme':
395
+ case 'palette':
396
+ case 'colors': {
397
+ // Leak L30 (2026-05-27): forward the tokenized tail unchanged so
398
+ // the slash + top-level `pugi theme` surfaces share one parser
399
+ // inside `runThemeCommand`. Aliases `/palette` and `/colors`
400
+ // exist because the leak-landscape audit found operators reach
401
+ // for either word interchangeably — same surface, same handler.
402
+ const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
403
+ return { kind: 'theme', args: tokens };
404
+ }
405
+ case 'onboarding':
406
+ case 'onboard':
407
+ case 'setup': {
408
+ // Leak L25 (2026-05-27): forward the tokenized tail unchanged.
409
+ // The slash always routes through the non-interactive snapshot
410
+ // path (the REPL already owns the Ink tree); the runner picks
411
+ // it up from `ctx.interactive = false` in the session
412
+ // dispatcher.
413
+ const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
414
+ return { kind: 'onboarding', args: tokens };
415
+ }
416
+ case 'vim': {
417
+ // Leak L26 (2026-05-27): forward the tokenized tail unchanged so
418
+ // the slash + top-level CLI surfaces share one parser inside
419
+ // `runVimCommand`. Subcommands are single tokens (on / off /
420
+ // status); a bare `/vim` toggles.
421
+ const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
422
+ return { kind: 'vim', args: tokens };
423
+ }
328
424
  case 'doctor':
329
425
  case 'health': {
330
426
  // L17 (2026-05-27): run the probe sweep inline. Tail is ignored —
@@ -341,6 +437,43 @@ export function parseSlashCommand(input) {
341
437
  // fresh shell.
342
438
  return { kind: 'compact' };
343
439
  }
440
+ case 'stickers': {
441
+ // Leak L33 (2026-05-27): brand-personality gimmick. Tail args
442
+ // are ignored — the surface is intentionally parameterless. The
443
+ // session module delegates to the shared `runStickersCommand`
444
+ // so the slash + top-level paths stay single-sourced.
445
+ return { kind: 'stickers' };
446
+ }
447
+ case 'feedback': {
448
+ // Leak L21 (2026-05-27): in-CLI feedback collector. The wizard
449
+ // collects category/rating/comment/context/confirm interactively
450
+ // so the slash surface is parameterless. Tail args are reserved
451
+ // for a future `--message=...` quick-path; today they are
452
+ // accepted but ignored so the operator-level UX matches
453
+ // Claude Code's `/feedback`.
454
+ return { kind: 'feedback' };
455
+ }
456
+ case 'share': {
457
+ // Leak L20 (2026-05-27): forward the tokenized arg list verbatim
458
+ // so the session module (which owns the network + readline
459
+ // affordances) can hand them to runShareCommand. Defaults: no
460
+ // tokens means "auto-pick target + prompt for confirmation".
461
+ const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
462
+ return { kind: 'share', args: tokens };
463
+ }
464
+ case 'release-notes':
465
+ case 'releasenotes':
466
+ case 'changelog': {
467
+ // Leak L24 (2026-05-27): changelog diff between last-seen +
468
+ // installed CLI version. Tail args are tokenized so `--reset`
469
+ // can flip the marker-clear bit; no other flags are honoured —
470
+ // the surface mirrors the CLI top-level's intentional minimalism.
471
+ // `changelog` alias matches operator muscle memory from npm /
472
+ // cargo / brew, all of which ship `changelog` subcommands.
473
+ const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
474
+ const reset = tokens.includes('--reset') || tokens.includes('-r');
475
+ return { kind: 'release-notes', reset };
476
+ }
344
477
  case 'memory':
345
478
  case 'config':
346
479
  case 'budget':
@@ -27,6 +27,16 @@
27
27
  import { existsSync, readFileSync, statSync } from 'node:fs';
28
28
  import { basename, resolve as resolvePath } from 'node:path';
29
29
  import { slugForCwd } from './history.js';
30
+ import { isBareMode } from '../bare-mode/index.js';
31
+ /**
32
+ * Workspace summary shown when the operator launched with `--bare` (or
33
+ * `PUGI_BARE=1`). Leak L22: bare mode disables project auto-discovery
34
+ * across the CLI, so we never read `.pugi/PUGI.md` and never advertise
35
+ * a real workspace label to admin-api. Explicit string so the splash +
36
+ * status bar agree, and so operators triaging "why is Mira ignoring
37
+ * my repo" see a clear cause.
38
+ */
39
+ export const BARE_MODE_WORKSPACE_LABEL = '(bare mode - auto-discovery disabled)';
30
40
  /** Cap on the PUGI.md head we forward. Mirrors the admin-api clamp. */
31
41
  const PUGI_MD_HEAD_LIMIT = 200;
32
42
  /**
@@ -48,6 +58,18 @@ export const UNBOUND_WORKSPACE_LABEL = '(not bound - run /init OR cd into projec
48
58
  export function resolveWorkspaceContext(cwd) {
49
59
  const normalised = resolvePath(cwd);
50
60
  const slug = slugForCwd(normalised);
61
+ // Leak L22 (2026-05-27): `--bare` short-circuits BEFORE any PUGI.md
62
+ // / project-marker reads so the resolver never advertises a real
63
+ // workspace summary to admin-api. The cwd + slug still travel for
64
+ // telemetry, but the model + Mira treat the session as if launched
65
+ // from a fresh, unbound directory.
66
+ if (isBareMode()) {
67
+ return {
68
+ workspaceCwd: normalised,
69
+ workspaceSlug: slug,
70
+ workspaceSummary: BARE_MODE_WORKSPACE_LABEL,
71
+ };
72
+ }
51
73
  // α6.14.2 wave 5: when the cwd has no project markers, prefer the
52
74
  // explicit "not bound" summary so admin-api's prompt builder knows
53
75
  // not to fabricate a workspace context for Mira/Pugi. The cwd +
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Markdown transcript formatter used by `pugi share` (Leak L20, 2026-05-27).
3
+ *
4
+ * Walks the session's `.pugi/events.jsonl` audit log and reconstructs a
5
+ * Markdown document the operator (and downstream Gist / pugi.io readers)
6
+ * can read top-to-bottom. The format is intentionally human-first — turn
7
+ * headers, code-block-wrapped tool I/O, and a small front-matter block
8
+ * with session metadata. Machine-readability is a non-goal here; the
9
+ * JSONL log remains the source of truth for tooling.
10
+ *
11
+ * Why we reconstruct from JSONL instead of from the live REPL state:
12
+ *
13
+ * - The CLI top-level `pugi share` command runs from a fresh shell and
14
+ * has no in-memory REPL state to read; only the event log is
15
+ * persisted.
16
+ * - The in-REPL `/share` slash uses the same handler so behaviour is
17
+ * identical regardless of entry point. Operators sharing a session
18
+ * from inside the REPL get the exact same output they would get from
19
+ * a follow-up shell command.
20
+ * - The JSONL log is append-only and survives REPL crashes, so a
21
+ * `--share` after a crash is the most useful debug surface.
22
+ *
23
+ * Event vocabulary the formatter knows about (see
24
+ * `packages/pugi-sdk/src/audit-trace.ts` for the schema):
25
+ *
26
+ * session.created Session boundary; emits front matter.
27
+ * session.command_started One-line "running pugi <cmd>" header.
28
+ * session.command_completed Status line ("success" / "error").
29
+ * tool_call Markdown turn header + inputSummary
30
+ * rendered as a fenced block.
31
+ * tool_result "Result (status):" + outputSummary
32
+ * rendered as a fenced block.
33
+ * file_mutation Inline `path` + operation summary.
34
+ * subagent.* Indented "[subagent <role>] ..." line.
35
+ * hook.* Quiet "[hook <event>] ..." line.
36
+ * compaction.* "[compaction <tier>] ..." line.
37
+ *
38
+ * Unknown event types are surfaced as a single italic line ("[event
39
+ * type=...]") so a future event added to the SDK does not silently
40
+ * vanish from the transcript.
41
+ *
42
+ * Performance: the formatter is O(n) over the events file, runs entirely
43
+ * in memory, and is bounded by the session log size (currently capped at
44
+ * a few MB per session). No streaming I/O is needed for typical sessions
45
+ * — the operator does not run /share against multi-GB logs.
46
+ */
47
+ /**
48
+ * Format a session's event log as Markdown. Pure — no I/O. The caller
49
+ * reads `.pugi/events.jsonl` and hands the contents in.
50
+ */
51
+ export function formatTranscript(input) {
52
+ const now = input.now ? input.now() : new Date();
53
+ const events = parseEvents(input.eventsJsonl);
54
+ const filteredSessionId = pickSessionId(input.sessionId, events);
55
+ const sessionEvents = events.filter((e) => typeof e.raw.sessionId !== 'string' || e.raw.sessionId === filteredSessionId);
56
+ const lines = [];
57
+ // Front matter — a fenced block at the top so downstream readers can
58
+ // grok the context before any turn content. Not YAML front matter
59
+ // (`---`) because Gist + pugi.io render Markdown directly; the fenced
60
+ // approach renders predictably without a parser.
61
+ lines.push('# Pugi session transcript');
62
+ lines.push('');
63
+ lines.push('```');
64
+ lines.push(`session_id: ${filteredSessionId}`);
65
+ lines.push(`workspace: ${input.workspaceRoot}`);
66
+ lines.push(`cli_version: ${input.cliVersion}`);
67
+ lines.push(`exported_at: ${now.toISOString()}`);
68
+ lines.push(`event_count: ${sessionEvents.length}`);
69
+ lines.push('```');
70
+ lines.push('');
71
+ if (sessionEvents.length === 0) {
72
+ lines.push('_No events recorded for this session._');
73
+ return {
74
+ markdown: `${lines.join('\n')}\n`,
75
+ turnCount: 0,
76
+ eventCount: 0,
77
+ };
78
+ }
79
+ let turnCount = 0;
80
+ for (const event of sessionEvents) {
81
+ const rendered = renderEvent(event);
82
+ if (rendered === null)
83
+ continue;
84
+ lines.push(...rendered.lines);
85
+ lines.push('');
86
+ if (rendered.isTurn)
87
+ turnCount += 1;
88
+ }
89
+ return {
90
+ markdown: `${lines.join('\n').replace(/\n{3,}/g, '\n\n')}\n`,
91
+ turnCount,
92
+ eventCount: sessionEvents.length,
93
+ };
94
+ }
95
+ /**
96
+ * Parse the JSONL log. Malformed lines are skipped silently — the file
97
+ * is append-only and may have partial-write tail rows. Returning the
98
+ * stable typed shape lets the formatter walk without re-checking every
99
+ * field.
100
+ */
101
+ function parseEvents(raw) {
102
+ const out = [];
103
+ for (const line of raw.split('\n')) {
104
+ const trimmed = line.trim();
105
+ if (trimmed.length === 0)
106
+ continue;
107
+ try {
108
+ const parsed = JSON.parse(trimmed);
109
+ const type = typeof parsed.type === 'string' ? parsed.type : '';
110
+ const timestamp = typeof parsed.timestamp === 'string' ? parsed.timestamp : '';
111
+ if (type.length === 0)
112
+ continue;
113
+ out.push({ raw: parsed, type, timestamp });
114
+ }
115
+ catch {
116
+ // partial-write or corrupt row; skip without affecting the rest
117
+ }
118
+ }
119
+ return out;
120
+ }
121
+ /**
122
+ * Resolve the effective session id. When the operator passes a non-empty
123
+ * value we honour it. When they pass an empty string / placeholder
124
+ * (`'no-session'`), we fall back to the newest `session.created` event
125
+ * id in the file. Last-resort fallback is the literal placeholder so the
126
+ * transcript still renders something meaningful in the front matter.
127
+ */
128
+ function pickSessionId(provided, events) {
129
+ if (provided && provided !== 'no-session')
130
+ return provided;
131
+ for (let i = events.length - 1; i >= 0; i -= 1) {
132
+ const e = events[i];
133
+ if (!e)
134
+ continue;
135
+ if (e.type === 'session' && e.raw.name === 'created' && typeof e.raw.sessionId === 'string') {
136
+ return e.raw.sessionId;
137
+ }
138
+ }
139
+ return provided || 'unknown-session';
140
+ }
141
+ /**
142
+ * Format one event as a Markdown block. Returns `null` to suppress the
143
+ * event entirely (e.g. session.created is captured in front matter so we
144
+ * skip it here).
145
+ */
146
+ function renderEvent(event) {
147
+ const ts = event.timestamp || '';
148
+ switch (event.type) {
149
+ case 'session': {
150
+ const name = String(event.raw.name ?? '');
151
+ if (name === 'created')
152
+ return null; // captured by front matter
153
+ if (name === 'command_started') {
154
+ const command = String(event.raw.command ?? '');
155
+ return {
156
+ lines: [`## ${ts} — command \`${escapeInline(command)}\``],
157
+ isTurn: false,
158
+ };
159
+ }
160
+ if (name === 'command_completed') {
161
+ const command = String(event.raw.command ?? '');
162
+ const status = String(event.raw.status ?? 'unknown');
163
+ return {
164
+ lines: [`_command \`${escapeInline(command)}\` finished: ${status}_`],
165
+ isTurn: false,
166
+ };
167
+ }
168
+ return {
169
+ lines: [`_session ${name}_`],
170
+ isTurn: false,
171
+ };
172
+ }
173
+ case 'tool_call': {
174
+ const tool = String(event.raw.tool ?? 'unknown');
175
+ const summary = String(event.raw.inputSummary ?? '');
176
+ const out = [`### ${ts} — tool \`${escapeInline(tool)}\``];
177
+ if (summary.length > 0) {
178
+ out.push('');
179
+ out.push('Input:');
180
+ out.push(fenced(summary));
181
+ }
182
+ return { lines: out, isTurn: true };
183
+ }
184
+ case 'tool_result': {
185
+ const status = String(event.raw.status ?? 'unknown');
186
+ const summary = String(event.raw.outputSummary ?? '');
187
+ const out = [`Result (${status}):`];
188
+ if (summary.length > 0) {
189
+ out.push(fenced(summary));
190
+ }
191
+ return { lines: out, isTurn: false };
192
+ }
193
+ case 'file_mutation': {
194
+ const path = String(event.raw.path ?? '');
195
+ const op = String(event.raw.operation ?? '');
196
+ return {
197
+ lines: [`- file ${op}: \`${escapeInline(path)}\``],
198
+ isTurn: false,
199
+ };
200
+ }
201
+ case 'subagent.spawned':
202
+ case 'subagent.tool_call':
203
+ case 'subagent.completed':
204
+ case 'subagent.blocked':
205
+ case 'subagent.failed': {
206
+ const role = String(event.raw.role ?? '');
207
+ const persona = String(event.raw.personaSlug ?? '');
208
+ const detail = String(event.raw.detail ?? event.raw.error ?? event.raw.toolName ?? '');
209
+ const tail = detail.length > 0 ? ` ${detail}` : '';
210
+ return {
211
+ lines: [`_[subagent ${role} / ${persona}] ${event.type}${tail}_`],
212
+ isTurn: false,
213
+ };
214
+ }
215
+ case 'hook.invoked':
216
+ case 'hook.result':
217
+ case 'hook.skipped': {
218
+ const ev = String(event.raw.event ?? '');
219
+ const reason = String(event.raw.reason ?? event.raw.runSummary ?? event.raw.matchSummary ?? '');
220
+ const tail = reason.length > 0 ? ` ${reason}` : '';
221
+ return {
222
+ lines: [`_[hook ${ev}] ${event.type.replace('hook.', '')}${tail}_`],
223
+ isTurn: false,
224
+ };
225
+ }
226
+ case 'compaction.started':
227
+ case 'compaction.completed':
228
+ case 'compaction.skipped':
229
+ case 'compaction.invariant_violated': {
230
+ const tier = String(event.raw.tier ?? '');
231
+ return {
232
+ lines: [`_[compaction ${tier}] ${event.type.replace('compaction.', '')}_`],
233
+ isTurn: false,
234
+ };
235
+ }
236
+ default: {
237
+ return {
238
+ lines: [`_[event type=${event.type}]_`],
239
+ isTurn: false,
240
+ };
241
+ }
242
+ }
243
+ }
244
+ /**
245
+ * Wrap a string in a fenced code block. Pick a fence length that does
246
+ * not collide with backtick runs inside the content. Markdown 0.30 allows
247
+ * variable-length fences; we pick the shortest that is longer than the
248
+ * longest run inside the content (min 3, max 7).
249
+ */
250
+ function fenced(content) {
251
+ const longestRun = (content.match(/`+/g) ?? [])
252
+ .map((s) => s.length)
253
+ .reduce((max, n) => (n > max ? n : max), 0);
254
+ const fenceLen = Math.min(7, Math.max(3, longestRun + 1));
255
+ const fence = '`'.repeat(fenceLen);
256
+ // Trim trailing whitespace inside content so the closing fence sits
257
+ // tight against the body; preserve leading whitespace (matters for code).
258
+ return `${fence}\n${content.replace(/\s+$/u, '')}\n${fence}`;
259
+ }
260
+ /**
261
+ * Escape inline-Markdown specials (backtick, pipe) inside a span that we
262
+ * are wrapping in inline code. The closing backtick rule says a `<code>`
263
+ * span can contain backticks as long as the fence length differs — for
264
+ * simplicity we replace bare backticks with a Unicode look-alike when
265
+ * they appear in identifier-like positions (e.g. paths or tool names).
266
+ * Backticks in real content go through `fenced()` instead.
267
+ */
268
+ function escapeInline(text) {
269
+ return text.replace(/`/g, 'ˋ');
270
+ }
271
+ //# sourceMappingURL=formatter.js.map