@pugi/cli 0.1.0-alpha.7 → 0.1.0-alpha.8

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.
@@ -32,6 +32,9 @@ import { evaluateCap, describeVerdict } from './cap-warning.js';
32
32
  import { parseSlashCommand } from './slash-commands.js';
33
33
  import { webFetchTool } from '../../tools/web-fetch.js';
34
34
  import { loadSettings } from '../settings.js';
35
+ import { getJobRegistry } from '../jobs/registry.js';
36
+ import { existsSync, readdirSync, statSync } from 'node:fs';
37
+ import { resolve as resolvePath } from 'node:path';
35
38
  const MAX_TRANSCRIPT_ROWS = 500;
36
39
  const MAX_RECONNECT_ATTEMPTS = 10;
37
40
  const RECONNECT_BASE_MS = 250;
@@ -45,6 +48,18 @@ export class ReplSession {
45
48
  reconnectAttempt = 0;
46
49
  reconnectTimer;
47
50
  closed = false;
51
+ /**
52
+ * Last non-trivial step.detail recorded per taskId. The server streams
53
+ * the persona reply incrementally via `agent.step` events whose
54
+ * `detail` field carries the cumulative model output. `agent.completed`
55
+ * arrives last and previously overwrote the visible detail to the
56
+ * literal string `'shipped'` while the transcript line said only
57
+ * `shipped.` — the actual reply text was lost. By caching the last
58
+ * non-trivial detail here, we can flush it into the transcript when
59
+ * the agent completes so the operator sees what the persona actually
60
+ * said. CEO wave-2 fix 2026-05-25.
61
+ */
62
+ lastStepDetail = new Map();
48
63
  constructor(options) {
49
64
  this.options = options;
50
65
  this.state = {
@@ -139,8 +154,112 @@ export class ReplSession {
139
154
  await this.dispatchWebFetch(verdict.url);
140
155
  return verdict;
141
156
  }
157
+ case 'clear': {
158
+ this.clearTranscript();
159
+ return verdict;
160
+ }
161
+ case 'version': {
162
+ this.appendSystemLine(`pugi ${this.options.cliVersion}`);
163
+ return verdict;
164
+ }
165
+ case 'jobs': {
166
+ await this.dispatchJobs();
167
+ return verdict;
168
+ }
169
+ case 'diff': {
170
+ this.dispatchDiff();
171
+ return verdict;
172
+ }
173
+ case 'cost': {
174
+ this.dispatchCost();
175
+ return verdict;
176
+ }
177
+ case 'status': {
178
+ this.dispatchStatus();
179
+ return verdict;
180
+ }
181
+ case 'stub': {
182
+ this.appendSystemLine(verdict.message);
183
+ return verdict;
184
+ }
142
185
  }
143
186
  }
187
+ /**
188
+ * Reset the conversation transcript. The agent registry stays intact
189
+ * so the operator can `/clear` to declutter the chat pane without
190
+ * losing visibility into running dispatches.
191
+ */
192
+ clearTranscript() {
193
+ this.patch({ transcript: [] });
194
+ }
195
+ /* ------------- Tier 1 / Tier 2 wired handlers -------------- */
196
+ async dispatchJobs() {
197
+ try {
198
+ const registry = getJobRegistry();
199
+ const entries = await registry.list();
200
+ if (entries.length === 0) {
201
+ this.appendSystemLine('No background jobs tracked.');
202
+ return;
203
+ }
204
+ this.appendSystemLine(`Background jobs (${entries.length}):`);
205
+ for (const entry of entries) {
206
+ const id = entry.id.replace(/^pj-/, '').slice(0, 8);
207
+ const status = entry.status;
208
+ const cmd = entry.command.length > 48 ? `${entry.command.slice(0, 47)}…` : entry.command;
209
+ this.appendSystemLine(` ${id} ${status.padEnd(10)} ${cmd}`);
210
+ }
211
+ }
212
+ catch (error) {
213
+ this.appendSystemLine(`/jobs failed: ${this.errorMessage(error)}`);
214
+ }
215
+ }
216
+ dispatchDiff() {
217
+ try {
218
+ const artifactsRoot = resolvePath(process.cwd(), '.pugi', 'artifacts');
219
+ if (!existsSync(artifactsRoot)) {
220
+ this.appendSystemLine('No pending diffs (.pugi/artifacts/ not found).');
221
+ return;
222
+ }
223
+ const subdirs = readdirSync(artifactsRoot, { withFileTypes: true })
224
+ .filter((d) => d.isDirectory())
225
+ .map((d) => d.name);
226
+ const diffs = [];
227
+ for (const name of subdirs) {
228
+ const candidate = resolvePath(artifactsRoot, name, 'diff.patch');
229
+ if (existsSync(candidate)) {
230
+ const size = statSync(candidate).size;
231
+ diffs.push(` ${name}/diff.patch (${size} bytes)`);
232
+ }
233
+ }
234
+ if (diffs.length === 0) {
235
+ this.appendSystemLine('No pending diffs.');
236
+ return;
237
+ }
238
+ this.appendSystemLine(`Pending diffs (${diffs.length}):`);
239
+ for (const line of diffs)
240
+ this.appendSystemLine(line);
241
+ }
242
+ catch (error) {
243
+ this.appendSystemLine(`/diff failed: ${this.errorMessage(error)}`);
244
+ }
245
+ }
246
+ dispatchCost() {
247
+ const { tokensDownstreamTotal, agents } = this.state;
248
+ const active = agents.filter((a) => a.status === 'queued' || a.status === 'thinking').length;
249
+ const lineTokens = `Tokens this session: ${tokensDownstreamTotal.toLocaleString()} (in+out).`;
250
+ const lineAgents = `Active dispatches: ${active} of cap.`;
251
+ this.appendSystemLine(lineTokens);
252
+ this.appendSystemLine(lineAgents);
253
+ this.appendSystemLine('Full per-persona budget breakdown lands in α6.5.');
254
+ }
255
+ dispatchStatus() {
256
+ const sessionId = this.state.sessionId ?? '(unbound)';
257
+ const reach = this.state.connection;
258
+ this.appendSystemLine(`Backend: ${this.options.apiUrl} (${reach}).`);
259
+ this.appendSystemLine(`Session: ${sessionId}.`);
260
+ this.appendSystemLine(`Workspace: ${this.state.workspaceLabel}.`);
261
+ this.appendSystemLine(`CLI: pugi ${this.state.cliVersion}.`);
262
+ }
144
263
  /**
145
264
  * Fetch one URL via the web_fetch tool and inject the resulting
146
265
  * Markdown into the transcript as an operator-attributed brief. The
@@ -307,6 +426,12 @@ export class ReplSession {
307
426
  return;
308
427
  }
309
428
  case 'agent.step': {
429
+ // Cache the running detail per task so we can surface the
430
+ // model's actual reply when agent.completed lands (otherwise
431
+ // the reply disappears under the literal 'shipped' patch).
432
+ if (event.detail && event.detail.trim().length > 0) {
433
+ this.lastStepDetail.set(event.taskId, event.detail);
434
+ }
310
435
  this.patch({
311
436
  agents: this.state.agents.map((a) => a.taskId === event.taskId
312
437
  ? { ...a, status: 'thinking', detail: event.detail }
@@ -330,18 +455,41 @@ export class ReplSession {
330
455
  }
331
456
  case 'agent.completed': {
332
457
  const target = this.state.agents.find((a) => a.taskId === event.taskId);
458
+ const finalDetail = this.lastStepDetail.get(event.taskId);
459
+ this.lastStepDetail.delete(event.taskId);
333
460
  this.patch({
334
461
  agents: this.state.agents.map((a) => a.taskId === event.taskId
335
462
  ? { ...a, status: 'shipped', detail: 'shipped' }
336
463
  : a),
337
464
  });
338
465
  if (target) {
339
- this.appendPersonaLine(target.personaSlug, 'shipped.');
466
+ // If the persona actually produced a reply via incremental
467
+ // agent.step events, render that reply in the transcript so
468
+ // the operator sees the full answer. The threshold (>4 chars
469
+ // + not the queued placeholder) filters out the no-op
470
+ // "shipped"/"queued for dispatch" placeholders the dispatcher
471
+ // emits before the model speaks. Multi-line replies are
472
+ // emitted line by line so the conversation pane wraps each
473
+ // sentence on its own row.
474
+ if (finalDetail
475
+ && finalDetail !== 'queued for dispatch'
476
+ && finalDetail.trim().length > 4) {
477
+ for (const line of finalDetail.split('\n')) {
478
+ const trimmed = line.trim();
479
+ if (trimmed.length > 0) {
480
+ this.appendPersonaLine(target.personaSlug, trimmed);
481
+ }
482
+ }
483
+ }
484
+ else {
485
+ this.appendPersonaLine(target.personaSlug, 'shipped.');
486
+ }
340
487
  }
341
488
  return;
342
489
  }
343
490
  case 'agent.blocked': {
344
491
  const target = this.state.agents.find((a) => a.taskId === event.taskId);
492
+ this.lastStepDetail.delete(event.taskId);
345
493
  this.patch({
346
494
  agents: this.state.agents.map((a) => a.taskId === event.taskId
347
495
  ? { ...a, status: 'blocked', detail: event.detail }
@@ -354,6 +502,7 @@ export class ReplSession {
354
502
  }
355
503
  case 'agent.failed': {
356
504
  const target = this.state.agents.find((a) => a.taskId === event.taskId);
505
+ this.lastStepDetail.delete(event.taskId);
357
506
  this.patch({
358
507
  agents: this.state.agents.map((a) => a.taskId === event.taskId
359
508
  ? { ...a, status: 'failed', detail: event.error }
@@ -1,32 +1,99 @@
1
1
  /**
2
- * REPL slash command registry - Sprint α5.7 (ADR-0056 PR-PUGI-CLI-REPL-DEFAULT).
2
+ * REPL slash command registry Sprint α5.7, expanded α6.14 wave 2.
3
3
  *
4
- * The REPL input box surfaces a small palette of slash commands that the
5
- * operator can run from inside a persistent session: dispatch a brief,
6
- * inspect the agent roster, stop a running persona, open the help
7
- * overlay, or quit. The registry is intentionally narrow at M1 -
8
- * complementary surfaces (`/sync`, `/handoff`, `/budget`) ship as proper
9
- * subcommands in α5.8+ and stay reachable from a non-REPL `pugi
10
- * <command>` invocation.
4
+ * The REPL input box surfaces a palette of slash commands the operator
5
+ * can run from inside a persistent session. The wave-2 expansion (CEO
6
+ * 2026-05-25) grows the surface from 6 to 20 commands so the `/help`
7
+ * overlay matches the breadth Claude Code / Codex CLI operators expect.
11
8
  *
12
- * The registry is pure: each entry returns a `SlashCommandResult`
13
- * describing what the REPL session should do next. The session module
14
- * owns the side effects (network calls, dispatcher invocation, exit).
15
- * Keeping the surface pure lets the unit test exercise every shape
16
- * without standing up an Ink runtime or an Anvil endpoint.
9
+ * The registry is pure: each `parseSlashCommand` call returns a
10
+ * `SlashCommandResult` describing what the REPL session should do next.
11
+ * The session module owns the side effects (network calls, dispatcher
12
+ * invocation, exit, transcript clear). Keeping the surface pure lets
13
+ * the unit test exercise every shape without standing up an Ink runtime
14
+ * or an Anvil endpoint.
15
+ *
16
+ * Tiering (per CEO wave-2 spec):
17
+ *
18
+ * Tier 1 — wired against real state (3 + existing 6 = 9 wired):
19
+ * brief, agents, stop, help, quit, web, clear, version, jobs.
20
+ *
21
+ * Tier 2 — best-effort wiring against existing surfaces (3):
22
+ * diff, cost, status.
23
+ *
24
+ * Tier 3 — deterministic stubs ("coming in αX.Y") (8):
25
+ * compact, resume, memory, config, privacy, budget, mcp, undo.
17
26
  *
18
27
  * Brand voice (brandbook §08): power words `brief / dispatch / stop /
19
- * agents / quit`. Tagline `Brief it. It ships.` reserved for `/quit`
20
- * confirmation and `/help` footer - never inline.
28
+ * agents / quit / shipped`. Tagline `Brief it. It ships.` reserved for
29
+ * `/quit` confirmation and `/help` footer never inline.
21
30
  */
22
31
  import { listRoles } from '../agents/registry.js';
32
+ /**
33
+ * Deterministic stub copy returned by the Tier 3 commands. Spec'd
34
+ * inline so the unit test can pin the exact text without poking at
35
+ * the help overlay. The version tag at the end maps to the sprint we
36
+ * intend to land the real wiring in.
37
+ */
38
+ export const SLASH_STUB_MESSAGES = Object.freeze({
39
+ brief: '',
40
+ agents: '',
41
+ stop: '',
42
+ help: '',
43
+ quit: '',
44
+ web: '',
45
+ clear: '',
46
+ version: '',
47
+ jobs: '',
48
+ diff: '',
49
+ cost: '',
50
+ status: '',
51
+ compact: 'Manual context compaction lands in α6.5.',
52
+ resume: 'Resume last session lands in α6.4 once SQLite session.db lands.',
53
+ memory: 'Session memory editor lands in α6.5.',
54
+ config: 'Run `pugi config list` from a fresh shell for the full surface; in-REPL editor lands in α6.5.',
55
+ privacy: 'Run `pugi privacy show` from a fresh shell; in-REPL toggle lands in α6.5.',
56
+ budget: 'Run `pugi budget` from a fresh shell; in-REPL summary lands in α6.5.',
57
+ mcp: 'Run `pugi config mcp list` from a fresh shell; in-REPL palette lands in α6.5.',
58
+ undo: 'Run `pugi undo` from a fresh shell; in-REPL undo lands in α6.5.',
59
+ });
23
60
  export const SLASH_COMMAND_HELP = Object.freeze([
24
- { name: 'brief', args: '<text>', gloss: 'Dispatch a brief to the workforce' },
25
- { name: 'agents', args: '', gloss: 'List the on-watch agent roster' },
26
- { name: 'stop', args: '<persona>', gloss: 'Stop one agent by persona slug' },
27
- { name: 'help', args: '', gloss: 'Show this help overlay' },
28
- { name: 'quit', args: '', gloss: 'Exit the REPL (session resumes via pugi resume)' },
29
- { name: 'web', args: '<url>', gloss: 'Fetch a URL and brief Pugi on the page' },
61
+ // Workforce dispatch
62
+ { name: 'brief', args: '<text>', gloss: 'Dispatch a brief to the workforce', group: 'Workforce dispatch' },
63
+ { name: 'agents', args: '', gloss: 'List the on-watch agent roster', group: 'Workforce dispatch' },
64
+ { name: 'stop', args: '<persona>', gloss: 'Stop one agent by persona slug', group: 'Workforce dispatch' },
65
+ { name: 'jobs', args: '', gloss: 'List background jobs', group: 'Workforce dispatch' },
66
+ // Session
67
+ { name: 'clear', args: '', gloss: 'Clear conversation pane', group: 'Session' },
68
+ { name: 'resume', args: '', gloss: 'Resume last session (α6.4)', group: 'Session', stub: true },
69
+ { name: 'compact', args: '', gloss: 'Manual context compaction (α6.5)', group: 'Session', stub: true },
70
+ { name: 'memory', args: '', gloss: 'Session memory editor (α6.5)', group: 'Session', stub: true },
71
+ // Pugi tools
72
+ { name: 'web', args: '<url>', gloss: 'Fetch a URL into context', group: 'Pugi tools' },
73
+ { name: 'diff', args: '', gloss: 'Show pending diff', group: 'Pugi tools' },
74
+ { name: 'cost', args: '', gloss: 'Token usage + budget', group: 'Pugi tools' },
75
+ { name: 'status', args: '', gloss: 'Backend + tenant status', group: 'Pugi tools' },
76
+ // Settings
77
+ { name: 'config', args: '', gloss: 'Show config', group: 'Settings', stub: true },
78
+ { name: 'privacy', args: '', gloss: 'Show privacy mode', group: 'Settings', stub: true },
79
+ { name: 'budget', args: '', gloss: 'Show usage budget', group: 'Settings', stub: true },
80
+ { name: 'mcp', args: '', gloss: 'List MCP servers', group: 'Settings', stub: true },
81
+ { name: 'undo', args: '', gloss: 'Undo last write', group: 'Settings', stub: true },
82
+ // Meta
83
+ { name: 'help', args: '', gloss: 'Show this help overlay', group: 'Meta' },
84
+ { name: 'version', args: '', gloss: 'Show CLI version', group: 'Meta' },
85
+ { name: 'quit', args: '', gloss: 'Exit the REPL', group: 'Meta' },
86
+ ]);
87
+ /**
88
+ * Ordered list of groups. Drives the `/help` overlay sectioning so the
89
+ * operator reads commands by intent (dispatch first, meta last).
90
+ */
91
+ export const SLASH_COMMAND_GROUPS = Object.freeze([
92
+ 'Workforce dispatch',
93
+ 'Session',
94
+ 'Pugi tools',
95
+ 'Settings',
96
+ 'Meta',
30
97
  ]);
31
98
  /**
32
99
  * Parse one line of input from the REPL. The contract:
@@ -34,13 +101,15 @@ export const SLASH_COMMAND_HELP = Object.freeze([
34
101
  * - Empty / whitespace-only input returns `noop` with the original
35
102
  * text so the REPL can ignore it without printing anything.
36
103
  * - Input that does not start with `/` is treated as an implicit
37
- * `/brief <text>` - the most-common operator action.
104
+ * `/brief <text>` the most-common operator action.
38
105
  * - `/<name> [args]` resolves the name against the registry; unknown
39
106
  * names return `error` so the REPL can render a one-line tip
40
107
  * instead of silently dropping the input.
108
+ * - Tier 3 stubs return `{ kind: 'stub', name, message }` so the REPL
109
+ * can render the deterministic "coming in αX.Y" copy uniformly.
41
110
  *
42
111
  * The function never throws. Bad input maps to a structured result the
43
- * REPL can render - the alternative (throwing from a keystroke handler)
112
+ * REPL can render the alternative (throwing from a keystroke handler)
44
113
  * would unmount Ink mid-frame.
45
114
  */
46
115
  export function parseSlashCommand(input) {
@@ -98,6 +167,40 @@ export function parseSlashCommand(input) {
98
167
  }
99
168
  return { kind: 'web', url: tail };
100
169
  }
170
+ case 'clear':
171
+ case 'cls': {
172
+ return { kind: 'clear' };
173
+ }
174
+ case 'version':
175
+ case 'v': {
176
+ return { kind: 'version' };
177
+ }
178
+ case 'jobs': {
179
+ return { kind: 'jobs' };
180
+ }
181
+ case 'diff': {
182
+ return { kind: 'diff' };
183
+ }
184
+ case 'cost': {
185
+ return { kind: 'cost' };
186
+ }
187
+ case 'status': {
188
+ return { kind: 'status' };
189
+ }
190
+ case 'compact':
191
+ case 'resume':
192
+ case 'memory':
193
+ case 'config':
194
+ case 'privacy':
195
+ case 'budget':
196
+ case 'mcp':
197
+ case 'undo': {
198
+ return {
199
+ kind: 'stub',
200
+ name: name,
201
+ message: SLASH_STUB_MESSAGES[name],
202
+ };
203
+ }
101
204
  default: {
102
205
  return {
103
206
  kind: 'error',
@@ -35,7 +35,7 @@ import { runBudgetCommand } from './commands/budget.js';
35
35
  * packages/pugi-sdk/package.json); the publish workflow validates the
36
36
  * three are in lockstep.
37
37
  */
38
- const PUGI_CLI_VERSION = '0.1.0-alpha.7';
38
+ const PUGI_CLI_VERSION = '0.1.0-alpha.8';
39
39
  const handlers = {
40
40
  accounts,
41
41
  build: runEngineTask('build_task'),
@@ -456,7 +456,7 @@ export function InputBox(props) {
456
456
  : Math.min(paletteIndex, paletteView.rows.length - 1);
457
457
  const divider = '─'.repeat(innerWidth);
458
458
  const focusedMatch = search ? search.matches[search.focusedIndex] : undefined;
459
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", dimColor: true, children: divider }), _jsx(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, flexDirection: "column", children: search ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '(reverse-i-search) ' }), _jsx(Text, { children: `\`${search.query}\`: ` }), _jsx(Text, { color: "yellow", children: focusedMatch ? focusedMatch.brief : '(no match)' })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `Ctrl+R next · Ctrl+S prev · Enter accept · Esc cancel · ${search.matches.length} match${search.matches.length === 1 ? '' : 'es'}` }) })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '› ' }), _jsx(Text, { children: renderLineWithCursor(line, cursor, cursorVisible) })] })) }), line.length > innerWidth - 4 ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: '┊ ' }), _jsx(Text, { dimColor: true, children: 'line wraps - Enter still submits' })] })) : null, _jsx(SlashPalette, { rows: paletteView.rows, focusedIndex: clampedPaletteIndex, totalBeforeLimit: paletteView.totalBeforeLimit }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: '↑/↓ history · Ctrl+R search · / commands · Enter brief · Esc cancel · Ctrl+C ×2 exit' }) })] }));
459
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", dimColor: true, children: divider }), _jsx(Box, { paddingX: 1, flexDirection: "column", children: search ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '(reverse-i-search) ' }), _jsx(Text, { children: `\`${search.query}\`: ` }), _jsx(Text, { color: "yellow", children: focusedMatch ? focusedMatch.brief : '(no match)' })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `Ctrl+R next · Ctrl+S prev · Enter accept · Esc cancel · ${search.matches.length} match${search.matches.length === 1 ? '' : 'es'}` }) })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '› ' }), _jsx(Text, { children: renderLineWithCursor(line, cursor, cursorVisible) })] })) }), _jsx(Text, { color: "cyan", dimColor: true, children: divider }), line.length > innerWidth - 4 ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: '┊ ' }), _jsx(Text, { dimColor: true, children: 'line wraps - Enter still submits' })] })) : null, _jsx(SlashPalette, { rows: paletteView.rows, focusedIndex: clampedPaletteIndex, totalBeforeLimit: paletteView.totalBeforeLimit }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: '↑/↓ history · Ctrl+R search · / commands · Enter brief · Esc cancel · Ctrl+C ×2 exit' }) })] }));
460
460
  }
461
461
  /**
462
462
  * Render the line with the cursor glyph inserted at `cursor`. The cursor
package/dist/tui/repl.js CHANGED
@@ -26,7 +26,7 @@ import { InputBox } from './input-box.js';
26
26
  import { StatusBar } from './status-bar.js';
27
27
  import { UpdateBanner } from './update-banner.js';
28
28
  import { slugForCwd } from '../core/repl/history.js';
29
- import { SLASH_COMMAND_HELP } from '../core/repl/slash-commands.js';
29
+ import { SLASH_COMMAND_HELP, SLASH_COMMAND_GROUPS } from '../core/repl/slash-commands.js';
30
30
  const TICK_INTERVAL_MS = 200;
31
31
  const PULSE_INTERVAL_MS = 700;
32
32
  export function Repl(props) {
@@ -114,7 +114,27 @@ function MainArea({ state, personaNames, nowEpochMs, }) {
114
114
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ConversationPane, { rows: conversationSlice, personaNames: personaNames }), _jsx(Box, { marginTop: 1, children: _jsx(AgentTree, { agents: state.agents, nowEpochMs: nowEpochMs }) })] }));
115
115
  }
116
116
  function HelpOverlay() {
117
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Pugi REPL help" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: SLASH_COMMAND_HELP.map((row) => (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: ` /${row.name} ${row.args}`.padEnd(22, ' ') }), _jsx(Text, { dimColor: true, children: row.gloss })] }, row.name))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: `${PUGI_TAGLINE}` }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: 'Press any key to dismiss.' }) })] }));
117
+ // Group commands by their `group` field so the operator scans the
118
+ // palette by intent (dispatch → session → tools → settings → meta).
119
+ // The α6.14 wave-2 expansion grew the surface from 6 to 20 commands;
120
+ // a flat list would force the operator to read 20 rows top-to-bottom
121
+ // every time. Grouping cuts perceived complexity dramatically.
122
+ const grouped = new Map();
123
+ for (const row of SLASH_COMMAND_HELP) {
124
+ const list = grouped.get(row.group);
125
+ if (list) {
126
+ grouped.set(row.group, [...list, row]);
127
+ }
128
+ else {
129
+ grouped.set(row.group, [row]);
130
+ }
131
+ }
132
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Pugi REPL help" }), SLASH_COMMAND_GROUPS.map((group) => {
133
+ const rows = grouped.get(group);
134
+ if (!rows || rows.length === 0)
135
+ return null;
136
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: ` -- ${group} --` }), rows.map((row) => (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: ` /${row.name} ${row.args}`.padEnd(22, ' ') }), _jsx(Text, { dimColor: true, children: row.gloss })] }, row.name)))] }, group));
137
+ }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: `${PUGI_TAGLINE}` }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: 'Press any key to dismiss.' }) })] }));
118
138
  }
119
139
  function RosterOverlay() {
120
140
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "On-watch roster" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: THE_TEN.map((persona) => (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: ` ${persona.name.padEnd(10, ' ')}` }), _jsx(Text, { dimColor: true, children: `${persona.role.padEnd(20, ' ')}` }), _jsx(Text, { children: persona.oneLiner })] }, persona.slug))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: 'Press any key to dismiss.' }) })] }));
@@ -138,6 +158,15 @@ function applyVerdictSideEffects(verdict, handlers) {
138
158
  case 'web':
139
159
  case 'error':
140
160
  case 'noop':
161
+ case 'clear':
162
+ case 'version':
163
+ case 'jobs':
164
+ case 'diff':
165
+ case 'cost':
166
+ case 'status':
167
+ case 'stub':
168
+ // All non-overlay verdicts: the session module already appended
169
+ // any operator-visible system lines. No further UI side effect.
141
170
  return;
142
171
  }
143
172
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pugi/cli",
3
- "version": "0.1.0-alpha.7",
3
+ "version": "0.1.0-alpha.8",
4
4
  "description": "Pugi CLI — terminal-native software execution system",
5
5
  "homepage": "https://pugi.io",
6
6
  "repository": {
@@ -47,7 +47,7 @@
47
47
  "undici": "^8.3.0",
48
48
  "zod": "^3.23.0",
49
49
  "@pugi/personas": "0.1.0",
50
- "@pugi/sdk": "0.1.0-alpha.7"
50
+ "@pugi/sdk": "0.1.0-alpha.8"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/node": "^22.0.0",