@pugi/cli 0.1.0-beta.25 → 0.1.0-beta.26

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.
@@ -22,6 +22,7 @@ import { buildRuntimeConfig, fetchPersonaRoster, loadRuntimeConfig, openPugiSess
22
22
  import { PUGI_TAGLINE } from '@pugi/personas';
23
23
  import { resolveRoster, renderRosterTable } from './commands/roster.js';
24
24
  import { runDelegateCommand } from './commands/delegate.js';
25
+ import { runDispatchCommand } from './commands/dispatch.js';
25
26
  import { clearApiKey, DEFAULT_API_URL, listStoredCredentials, maskApiKey, normalizeApiUrl, purgeAllCredentials, readCredentialsFile, resolveActiveCredential, storeApiKey, switchActiveAccount, } from '../core/credentials.js';
26
27
  import { resolveAndValidateEnvLogin, } from '../core/auth/env-provider.js';
27
28
  import { runDeployCommand } from '../commands/deploy.js';
@@ -41,6 +42,12 @@ import { runRepoMapCommand } from './commands/repo-map.js';
41
42
  import { runReleaseNotesCommand, defaultReleaseNotesHome, } from './commands/release-notes.js';
42
43
  import { runUndoCommand } from './commands/undo.js';
43
44
  import { runCompactCommand } from './commands/compact.js';
45
+ import { runRewindCommand } from './commands/rewind.js';
46
+ import { runSessionsCommand } from './commands/sessions.js';
47
+ // Day 4 ADR-0063: persona-memory operator surface (list / recall / write /
48
+ // forget / sync). The runner is shared by `pugi memory` top-level and the
49
+ // in-REPL `/memory` slash so the two surfaces stay single-sourced.
50
+ import { runMemoryCommand } from './commands/memory.js';
44
51
  import { runBudgetCommand } from './commands/budget.js';
45
52
  import { BARE_MODE_BANNER, isBareMode, setBareMode, } from '../core/bare-mode/index.js';
46
53
  import { runCostCommand } from './commands/cost.js';
@@ -92,6 +99,11 @@ const handlers = {
92
99
  config: dispatchConfig,
93
100
  cost: dispatchCost,
94
101
  delegate: dispatchDelegate,
102
+ // Leak L10 (2026-05-27): `pugi dispatch list-cache-refs` /
103
+ // `clear-cache-refs` operate on `.pugi/cache-refs/` — the persisted
104
+ // prompt-cache inheritance handles for fork-subagent dispatches. The
105
+ // handler module lives in commands/dispatch.ts so the table stays narrow.
106
+ dispatch: dispatchSubagentCacheRefs,
95
107
  deploy: dispatchDeploy,
96
108
  doctor,
97
109
  explain: runEngineTask('explain'),
@@ -106,6 +118,10 @@ const handlers = {
106
118
  logout,
107
119
  lsp: dispatchLsp,
108
120
  mcp: dispatchMcp,
121
+ // ADR-0063 Day 4: `pugi memory list|recall|write|forget|sync`. Routes
122
+ // to `runMemoryCommand` (admin-api `/api/persona-memory` + offline
123
+ // queue at `~/.pugi/memory-queue.jsonl`).
124
+ memory: dispatchMemory,
109
125
  patch: dispatchPatch,
110
126
  permissions: dispatchPermissions,
111
127
  perms: dispatchPermissions,
@@ -159,6 +175,11 @@ const handlers = {
159
175
  vim: dispatchVim,
160
176
  undo: dispatchUndo,
161
177
  compact: dispatchCompact,
178
+ // Leak L9 (2026-05-27): `pugi rewind [N | --to <id>]` rolls the
179
+ // conversation back to a checkpoint by appending a tombstone marker
180
+ // to the NDJSON event log. The slash counterpart `/rewind` forwards
181
+ // to the same runner via session.ts.
182
+ rewind: dispatchRewind,
162
183
  // L19 (2026-05-27): `pugi usage` is an alias of `pugi cost` — same
163
184
  // handler, same flags. Operators trained on Claude Code expect either
164
185
  // verb to surface the per-model token + USD table.
@@ -343,6 +364,41 @@ async function dispatchPrivacy(args, flags, _session) {
343
364
  writeOutput: (payload, text) => writeOutput(flags, payload, text),
344
365
  });
345
366
  }
367
+ /**
368
+ * ADR-0063 Day 4 — `pugi memory <sub>` top-level dispatcher.
369
+ *
370
+ * Forwards to the shared `runMemoryCommand` runner. Exit codes:
371
+ *
372
+ * - 0 — happy paths (listed / recalled / written / forgot / synced /
373
+ * queued_offline / sync_noop / sync_partial)
374
+ * - 1 — unauthenticated / feature_disabled / unknown_sub
375
+ * - 2 — invalid_args
376
+ *
377
+ * `forget_not_found` exits 0 because the operator-visible behaviour
378
+ * (the memory is gone) matches their intent; the JSON envelope still
379
+ * carries the `forget_not_found` status flag for scripted callers.
380
+ */
381
+ async function dispatchMemory(args, flags, _session) {
382
+ const result = await runMemoryCommand(args, {
383
+ workspaceRoot: process.cwd(),
384
+ json: flags.json,
385
+ writeOutput: (payload, text) => writeOutput(flags, payload, text),
386
+ });
387
+ switch (result.status) {
388
+ case 'unauthenticated':
389
+ case 'feature_disabled':
390
+ case 'unknown_sub':
391
+ process.exitCode = 1;
392
+ return;
393
+ case 'invalid_args':
394
+ process.exitCode = 2;
395
+ return;
396
+ default:
397
+ // 'listed' | 'recalled' | 'written' | 'queued_offline' | 'forgot' |
398
+ // 'forget_not_found' | 'synced' | 'sync_partial' | 'sync_noop' — exit 0.
399
+ return;
400
+ }
401
+ }
346
402
  /**
347
403
  * Leak L18 (2026-05-27) — `pugi style` top-level dispatcher.
348
404
  *
@@ -570,6 +626,31 @@ async function dispatchBudget(args, flags, _session) {
570
626
  writeOutput: (payload, text) => writeOutput(flags, payload, text),
571
627
  });
572
628
  }
629
+ /**
630
+ * Leak L9 (2026-05-27) — `pugi rewind [N | --to <id>]` rolls the
631
+ * conversation back to a checkpoint by appending a tombstone marker to
632
+ * the NDJSON event log. Append-only: events stay durable; `pugi
633
+ * sessions undo-rewind` reverses the operation. The slash `/rewind`
634
+ * forwards through this same runner via session.ts.
635
+ */
636
+ async function dispatchRewind(args, flags, _session) {
637
+ const result = await runRewindCommand(args, {
638
+ workspaceRoot: process.cwd(),
639
+ writeOutput: (payload, text) => writeOutput(flags, payload, text),
640
+ });
641
+ if (result.status === 'failed_no_session'
642
+ || result.status === 'failed_store') {
643
+ process.exitCode = 1;
644
+ return;
645
+ }
646
+ if (result.status === 'failed_parse') {
647
+ process.exitCode = 2;
648
+ return;
649
+ }
650
+ if (result.status === 'noop_zero' || result.status === 'noop_empty') {
651
+ process.exitCode = 2;
652
+ }
653
+ }
573
654
  /**
574
655
  * Leak L6 — `pugi permissions [mode] [--persist] [--confirm]`.
575
656
  *
@@ -745,6 +826,19 @@ async function dispatchAgents(args, flags, _session) {
745
826
  writeOutput: (payload, text) => writeOutput(flags, payload, text),
746
827
  });
747
828
  }
829
+ /**
830
+ * Leak L10 (2026-05-27): `pugi dispatch <sub>` — operator-facing
831
+ * inspection + GC for fork-subagent prompt-cache inherit refs
832
+ * (.pugi/cache-refs/). Delegates to the standalone runner in
833
+ * commands/dispatch.ts so the cli.ts table stays under control.
834
+ */
835
+ async function dispatchSubagentCacheRefs(args, flags, _session) {
836
+ await runDispatchCommand(args, {
837
+ workspaceRoot: process.cwd(),
838
+ json: flags.json,
839
+ writeOutput: (payload, text) => writeOutput(flags, payload, text),
840
+ });
841
+ }
748
842
  /**
749
843
  * `pugi web <url>` — Sprint α6.15 Phase 1 quick-win subcommand.
750
844
  *
@@ -1526,6 +1620,17 @@ const COMMAND_HELP_BODIES = {
1526
1620
  'Slugs (Tier 1 alpha 7.5): dev qa pm devops researcher analyst designer',
1527
1621
  'frontend architect. `pugi roster` lists the live set.',
1528
1622
  ],
1623
+ dispatch: [
1624
+ 'pugi dispatch <sub> — inspect + GC fork-subagent prompt-cache inherit refs.',
1625
+ '',
1626
+ ' list-cache-refs Table of every active ref under .pugi/cache-refs/.',
1627
+ ' clear-cache-refs [--older-than 1h] Evict refs older than the window (default 24h).',
1628
+ '',
1629
+ 'Leak L10 (2026-05-27): when Mira spawns a child via the `agent` tool,',
1630
+ 'a prompt-cache handle is persisted so the child loop can request',
1631
+ 'parent-context reuse on the wire. These commands surface + clean up',
1632
+ 'the persisted refs.',
1633
+ ],
1529
1634
  roster: [
1530
1635
  'pugi roster — list the live Tier 1 personas + roles.',
1531
1636
  ],
@@ -1694,6 +1799,8 @@ async function help(args, flags, _session) {
1694
1799
  'Persona dispatch (α7.5):',
1695
1800
  ' pugi roster List the live Tier 1 personas + roles.',
1696
1801
  ' pugi delegate <slug> "<brief>" Dispatch a brief to one specialist.',
1802
+ ' pugi dispatch list-cache-refs Inspect fork-subagent prompt-cache inherit refs.',
1803
+ ' pugi dispatch clear-cache-refs GC stale cache refs (--older-than 1h).',
1697
1804
  '',
1698
1805
  'Plan decomposition (α6.8):',
1699
1806
  ' pugi plan --decompose <idea> Split a high-level idea into 3-7 components.',
@@ -3339,6 +3446,25 @@ async function handoff(args, flags, session) {
3339
3446
  writeOutput(flags, bundle, ['Pugi handoff bundle created', `Bundle: ${bundle.path}`].join('\n'));
3340
3447
  }
3341
3448
  async function sessions(args, flags, _session) {
3449
+ // L9 (2026-05-27): `pugi sessions undo-rewind [<session-id>]` rolls
3450
+ // back the latest /rewind by appending an inverse marker. Append-only,
3451
+ // reversible. Falls through to the legacy artifact-based handler when
3452
+ // the sub-command is not recognised.
3453
+ if (args[0] === 'undo-rewind') {
3454
+ const result = await runSessionsCommand(args, {
3455
+ workspaceRoot: process.cwd(),
3456
+ writeOutput: (payload, text) => writeOutput(flags, payload, text),
3457
+ });
3458
+ if (result) {
3459
+ if (result.status === 'failed_no_session' || result.status === 'failed_store') {
3460
+ process.exitCode = 1;
3461
+ }
3462
+ else if (result.status === 'noop_no_rewind') {
3463
+ process.exitCode = 2;
3464
+ }
3465
+ return;
3466
+ }
3467
+ }
3342
3468
  // α6.4: `pugi sessions --local` / `--search "query"` route to the
3343
3469
  // local SessionStore. The default surface stays artifact-based for
3344
3470
  // backward compat — operators who relied on the index.json view get
@@ -0,0 +1,126 @@
1
+ /**
2
+ * `pugi dispatch <sub>` — fork-subagent cache-handoff surface (Leak L10 — 2026-05-27).
3
+ *
4
+ * Sub-commands:
5
+ *
6
+ * - `pugi dispatch list-cache-refs` — show active cache-inherit refs
7
+ * persisted under `.pugi/cache-refs/`. Renders an aligned table or
8
+ * JSON depending on `--json`.
9
+ *
10
+ * - `pugi dispatch clear-cache-refs [--older-than 1h] [-v]` — GC stale
11
+ * refs. Without `--older-than`, defaults to 24h (the same cutoff
12
+ * the REPL boot-time auto-sweep uses). The flag accepts duration
13
+ * strings the `parseDuration` helper recognises (`1h`, `30m`,
14
+ * `7d`, `500ms`, ...).
15
+ *
16
+ * The command lives in its own module (not inline in cli.ts) so the
17
+ * dispatch table stays narrow and tests can drive it without spinning
18
+ * up the full CLI.
19
+ */
20
+ import { cleanupStaleCacheRefs, parseDuration, } from '../../core/dispatch/cache-cleanup.js';
21
+ import { listCacheRefs } from '../../core/dispatch/cache-handoff.js';
22
+ const USAGE = [
23
+ 'Usage:',
24
+ ' pugi dispatch list-cache-refs',
25
+ ' Show every active subagent cache-inherit',
26
+ ' reference persisted under .pugi/cache-refs/.',
27
+ '',
28
+ ' pugi dispatch clear-cache-refs [--older-than <duration>] [-v]',
29
+ ' Evict stale refs. Accepts 500ms / 30s / 5m /',
30
+ ' 1h / 7d. Default --older-than 24h.',
31
+ ].join('\n');
32
+ export async function runDispatchCommand(args, ctx) {
33
+ const sub = args[0];
34
+ if (!sub || sub === '--help' || sub === '-h') {
35
+ ctx.writeOutput({ command: 'dispatch', usage: USAGE.split('\n') }, USAGE);
36
+ return;
37
+ }
38
+ switch (sub) {
39
+ case 'list-cache-refs':
40
+ return runListCacheRefs(ctx);
41
+ case 'clear-cache-refs':
42
+ return runClearCacheRefs(args.slice(1), ctx);
43
+ default:
44
+ ctx.writeOutput({
45
+ ok: false,
46
+ error: `unknown subcommand: ${sub}`,
47
+ usage: USAGE.split('\n'),
48
+ }, `pugi dispatch: unknown subcommand '${sub}'\n\n${USAGE}`);
49
+ process.exitCode = 2;
50
+ return;
51
+ }
52
+ }
53
+ function runListCacheRefs(ctx) {
54
+ const refs = listCacheRefs(ctx.workspaceRoot);
55
+ if (ctx.json) {
56
+ ctx.writeOutput({ ok: true, count: refs.length, refs }, '');
57
+ return;
58
+ }
59
+ if (refs.length === 0) {
60
+ ctx.writeOutput({ ok: true, count: 0, refs: [] }, 'No active cache-inherit refs under .pugi/cache-refs/.');
61
+ return;
62
+ }
63
+ // Aligned column table. Widths derived from the data — no hardcoded
64
+ // sizes so a long childAgentId does not overflow the layout.
65
+ const header = ['CHILD AGENT', 'CACHE ID', 'PARENT SESSION', 'CREATED'];
66
+ const rows = refs.map((r) => [
67
+ r.childAgentId,
68
+ r.cacheId,
69
+ r.parentSessionId,
70
+ r.createdAt,
71
+ ]);
72
+ const widths = header.map((h, i) => Math.max(h.length, ...rows.map((row) => (row[i] ?? '').length)));
73
+ const lines = [];
74
+ lines.push(header.map((h, i) => h.padEnd(widths[i] ?? 0)).join(' '));
75
+ lines.push(widths.map((w) => '-'.repeat(w)).join(' '));
76
+ for (const row of rows) {
77
+ lines.push(row.map((cell, i) => (cell ?? '').padEnd(widths[i] ?? 0)).join(' '));
78
+ }
79
+ ctx.writeOutput({ ok: true, count: refs.length, refs }, lines.join('\n'));
80
+ }
81
+ function runClearCacheRefs(args, ctx) {
82
+ let olderThanArg;
83
+ let verbose = false;
84
+ for (let i = 0; i < args.length; i++) {
85
+ const arg = args[i];
86
+ if (arg === '--older-than') {
87
+ olderThanArg = args[i + 1];
88
+ i++;
89
+ continue;
90
+ }
91
+ if (arg && arg.startsWith('--older-than=')) {
92
+ olderThanArg = arg.slice('--older-than='.length);
93
+ continue;
94
+ }
95
+ if (arg === '-v' || arg === '--verbose') {
96
+ verbose = true;
97
+ continue;
98
+ }
99
+ }
100
+ // Default 24h matches the REPL boot-time auto-sweep cutoff.
101
+ const olderThanMs = olderThanArg ? parseDuration(olderThanArg) : 24 * 60 * 60 * 1000;
102
+ if (olderThanMs === null) {
103
+ ctx.writeOutput({
104
+ ok: false,
105
+ error: `invalid --older-than value: ${olderThanArg ?? '(missing)'}`,
106
+ usage: USAGE.split('\n'),
107
+ }, `pugi dispatch clear-cache-refs: invalid --older-than '${olderThanArg ?? '(missing)'}'\n\n${USAGE}`);
108
+ process.exitCode = 2;
109
+ return;
110
+ }
111
+ const result = cleanupStaleCacheRefs(ctx.workspaceRoot, {
112
+ olderThanMs,
113
+ verbose,
114
+ });
115
+ const summary = `Removed ${result.removedCount} ref(s) (${result.corruptCount} corrupt), kept ${result.keptCount}.`;
116
+ ctx.writeOutput({
117
+ ok: true,
118
+ olderThanMs,
119
+ removedCount: result.removedCount,
120
+ keptCount: result.keptCount,
121
+ corruptCount: result.corruptCount,
122
+ removed: result.removed,
123
+ corrupt: result.corrupt,
124
+ }, summary);
125
+ }
126
+ //# sourceMappingURL=dispatch.js.map