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

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';
@@ -35,12 +36,19 @@ import { isOnboarded } from '../core/onboarding/marker.js';
35
36
  import { runPrivacyCommand } from './commands/privacy.js';
36
37
  import { runReport } from './commands/report.js';
37
38
  import { runDoctorCommand, defaultHome as defaultDoctorHome } from './commands/doctor.js';
39
+ import { parsePrdCheckArgs, runPrdCheckCommand, } from './commands/prd-check.js';
38
40
  import { runStatusCommand, defaultStatusHome, } from './commands/status.js';
39
41
  import { runStickersCommand } from './commands/stickers.js';
40
42
  import { runRepoMapCommand } from './commands/repo-map.js';
41
43
  import { runReleaseNotesCommand, defaultReleaseNotesHome, } from './commands/release-notes.js';
42
44
  import { runUndoCommand } from './commands/undo.js';
43
45
  import { runCompactCommand } from './commands/compact.js';
46
+ import { runRewindCommand } from './commands/rewind.js';
47
+ import { runSessionsCommand } from './commands/sessions.js';
48
+ // Day 4 ADR-0063: persona-memory operator surface (list / recall / write /
49
+ // forget / sync). The runner is shared by `pugi memory` top-level and the
50
+ // in-REPL `/memory` slash so the two surfaces stay single-sourced.
51
+ import { runMemoryCommand } from './commands/memory.js';
44
52
  import { runBudgetCommand } from './commands/budget.js';
45
53
  import { BARE_MODE_BANNER, isBareMode, setBareMode, } from '../core/bare-mode/index.js';
46
54
  import { runCostCommand } from './commands/cost.js';
@@ -92,6 +100,11 @@ const handlers = {
92
100
  config: dispatchConfig,
93
101
  cost: dispatchCost,
94
102
  delegate: dispatchDelegate,
103
+ // Leak L10 (2026-05-27): `pugi dispatch list-cache-refs` /
104
+ // `clear-cache-refs` operate on `.pugi/cache-refs/` — the persisted
105
+ // prompt-cache inheritance handles for fork-subagent dispatches. The
106
+ // handler module lives in commands/dispatch.ts so the table stays narrow.
107
+ dispatch: dispatchSubagentCacheRefs,
95
108
  deploy: dispatchDeploy,
96
109
  doctor,
97
110
  explain: runEngineTask('explain'),
@@ -106,11 +119,20 @@ const handlers = {
106
119
  logout,
107
120
  lsp: dispatchLsp,
108
121
  mcp: dispatchMcp,
122
+ // ADR-0063 Day 4: `pugi memory list|recall|write|forget|sync`. Routes
123
+ // to `runMemoryCommand` (admin-api `/api/persona-memory` + offline
124
+ // queue at `~/.pugi/memory-queue.jsonl`).
125
+ memory: dispatchMemory,
109
126
  patch: dispatchPatch,
110
127
  permissions: dispatchPermissions,
111
128
  perms: dispatchPermissions,
112
129
  plan: dispatchPlan,
113
130
  'plan-review': dispatchPlanReview,
131
+ // Wave 6 (2026-05-27): `pugi prd-check` verifies PRD acceptance
132
+ // criteria against committed code/tests/docs/commands BEFORE an
133
+ // operator (or autonomous agent) claims a feature done. Same
134
+ // handler powers the in-REPL `/prd-check` slash via session.ts.
135
+ 'prd-check': dispatchPrdCheck,
114
136
  privacy: dispatchPrivacy,
115
137
  // L24 (2026-05-27): `pugi release-notes` shows the bundled CHANGELOG
116
138
  // diff between the operator's last-seen version + installed version.
@@ -159,6 +181,11 @@ const handlers = {
159
181
  vim: dispatchVim,
160
182
  undo: dispatchUndo,
161
183
  compact: dispatchCompact,
184
+ // Leak L9 (2026-05-27): `pugi rewind [N | --to <id>]` rolls the
185
+ // conversation back to a checkpoint by appending a tombstone marker
186
+ // to the NDJSON event log. The slash counterpart `/rewind` forwards
187
+ // to the same runner via session.ts.
188
+ rewind: dispatchRewind,
162
189
  // L19 (2026-05-27): `pugi usage` is an alias of `pugi cost` — same
163
190
  // handler, same flags. Operators trained on Claude Code expect either
164
191
  // verb to surface the per-model token + USD table.
@@ -343,6 +370,41 @@ async function dispatchPrivacy(args, flags, _session) {
343
370
  writeOutput: (payload, text) => writeOutput(flags, payload, text),
344
371
  });
345
372
  }
373
+ /**
374
+ * ADR-0063 Day 4 — `pugi memory <sub>` top-level dispatcher.
375
+ *
376
+ * Forwards to the shared `runMemoryCommand` runner. Exit codes:
377
+ *
378
+ * - 0 — happy paths (listed / recalled / written / forgot / synced /
379
+ * queued_offline / sync_noop / sync_partial)
380
+ * - 1 — unauthenticated / feature_disabled / unknown_sub
381
+ * - 2 — invalid_args
382
+ *
383
+ * `forget_not_found` exits 0 because the operator-visible behaviour
384
+ * (the memory is gone) matches their intent; the JSON envelope still
385
+ * carries the `forget_not_found` status flag for scripted callers.
386
+ */
387
+ async function dispatchMemory(args, flags, _session) {
388
+ const result = await runMemoryCommand(args, {
389
+ workspaceRoot: process.cwd(),
390
+ json: flags.json,
391
+ writeOutput: (payload, text) => writeOutput(flags, payload, text),
392
+ });
393
+ switch (result.status) {
394
+ case 'unauthenticated':
395
+ case 'feature_disabled':
396
+ case 'unknown_sub':
397
+ process.exitCode = 1;
398
+ return;
399
+ case 'invalid_args':
400
+ process.exitCode = 2;
401
+ return;
402
+ default:
403
+ // 'listed' | 'recalled' | 'written' | 'queued_offline' | 'forgot' |
404
+ // 'forget_not_found' | 'synced' | 'sync_partial' | 'sync_noop' — exit 0.
405
+ return;
406
+ }
407
+ }
346
408
  /**
347
409
  * Leak L18 (2026-05-27) — `pugi style` top-level dispatcher.
348
410
  *
@@ -570,6 +632,31 @@ async function dispatchBudget(args, flags, _session) {
570
632
  writeOutput: (payload, text) => writeOutput(flags, payload, text),
571
633
  });
572
634
  }
635
+ /**
636
+ * Leak L9 (2026-05-27) — `pugi rewind [N | --to <id>]` rolls the
637
+ * conversation back to a checkpoint by appending a tombstone marker to
638
+ * the NDJSON event log. Append-only: events stay durable; `pugi
639
+ * sessions undo-rewind` reverses the operation. The slash `/rewind`
640
+ * forwards through this same runner via session.ts.
641
+ */
642
+ async function dispatchRewind(args, flags, _session) {
643
+ const result = await runRewindCommand(args, {
644
+ workspaceRoot: process.cwd(),
645
+ writeOutput: (payload, text) => writeOutput(flags, payload, text),
646
+ });
647
+ if (result.status === 'failed_no_session'
648
+ || result.status === 'failed_store') {
649
+ process.exitCode = 1;
650
+ return;
651
+ }
652
+ if (result.status === 'failed_parse') {
653
+ process.exitCode = 2;
654
+ return;
655
+ }
656
+ if (result.status === 'noop_zero' || result.status === 'noop_empty') {
657
+ process.exitCode = 2;
658
+ }
659
+ }
573
660
  /**
574
661
  * Leak L6 — `pugi permissions [mode] [--persist] [--confirm]`.
575
662
  *
@@ -745,6 +832,19 @@ async function dispatchAgents(args, flags, _session) {
745
832
  writeOutput: (payload, text) => writeOutput(flags, payload, text),
746
833
  });
747
834
  }
835
+ /**
836
+ * Leak L10 (2026-05-27): `pugi dispatch <sub>` — operator-facing
837
+ * inspection + GC for fork-subagent prompt-cache inherit refs
838
+ * (.pugi/cache-refs/). Delegates to the standalone runner in
839
+ * commands/dispatch.ts so the cli.ts table stays under control.
840
+ */
841
+ async function dispatchSubagentCacheRefs(args, flags, _session) {
842
+ await runDispatchCommand(args, {
843
+ workspaceRoot: process.cwd(),
844
+ json: flags.json,
845
+ writeOutput: (payload, text) => writeOutput(flags, payload, text),
846
+ });
847
+ }
748
848
  /**
749
849
  * `pugi web <url>` — Sprint α6.15 Phase 1 quick-win subcommand.
750
850
  *
@@ -1526,6 +1626,17 @@ const COMMAND_HELP_BODIES = {
1526
1626
  'Slugs (Tier 1 alpha 7.5): dev qa pm devops researcher analyst designer',
1527
1627
  'frontend architect. `pugi roster` lists the live set.',
1528
1628
  ],
1629
+ dispatch: [
1630
+ 'pugi dispatch <sub> — inspect + GC fork-subagent prompt-cache inherit refs.',
1631
+ '',
1632
+ ' list-cache-refs Table of every active ref under .pugi/cache-refs/.',
1633
+ ' clear-cache-refs [--older-than 1h] Evict refs older than the window (default 24h).',
1634
+ '',
1635
+ 'Leak L10 (2026-05-27): when Mira spawns a child via the `agent` tool,',
1636
+ 'a prompt-cache handle is persisted so the child loop can request',
1637
+ 'parent-context reuse on the wire. These commands surface + clean up',
1638
+ 'the persisted refs.',
1639
+ ],
1529
1640
  roster: [
1530
1641
  'pugi roster — list the live Tier 1 personas + roles.',
1531
1642
  ],
@@ -1536,6 +1647,22 @@ const COMMAND_HELP_BODIES = {
1536
1647
  'event log, settings), permission mode, and the capability matrix per',
1537
1648
  'engine adapter. Safe to run anywhere; no network calls.',
1538
1649
  ],
1650
+ 'prd-check': [
1651
+ 'pugi prd-check <prd-path> | --all — Wave 6 verified-deliverable gate.',
1652
+ '',
1653
+ 'Reads a markdown PRD, parses the acceptance-criteria section, and',
1654
+ 'runs verifiers against committed artifacts:',
1655
+ ' file:<path> fs.existsSync',
1656
+ ' test:<spec> spec file exists + has ≥1 test()/it() block',
1657
+ ' doc:<path> doc exists + has > 100 chars',
1658
+ ' command:<name> CLI registry contains the command',
1659
+ ' route:METHOD /p best-effort grep of controllers',
1660
+ '',
1661
+ ' --all Scan docs/prd/**.md instead of one file.',
1662
+ ' --json Emit a structured envelope to stdout.',
1663
+ '',
1664
+ 'Exit codes: 0 healthy · 1 failing · 2 unparsed / arg error.',
1665
+ ],
1539
1666
  status: [
1540
1667
  'pugi status — concise session snapshot.',
1541
1668
  '',
@@ -1694,6 +1821,8 @@ async function help(args, flags, _session) {
1694
1821
  'Persona dispatch (α7.5):',
1695
1822
  ' pugi roster List the live Tier 1 personas + roles.',
1696
1823
  ' pugi delegate <slug> "<brief>" Dispatch a brief to one specialist.',
1824
+ ' pugi dispatch list-cache-refs Inspect fork-subagent prompt-cache inherit refs.',
1825
+ ' pugi dispatch clear-cache-refs GC stale cache refs (--older-than 1h).',
1697
1826
  '',
1698
1827
  'Plan decomposition (α6.8):',
1699
1828
  ' pugi plan --decompose <idea> Split a high-level idea into 3-7 components.',
@@ -1757,6 +1886,48 @@ async function doctor(_args, flags, _session) {
1757
1886
  writeOutput: (payload, text) => writeOutput(flags, payload, text),
1758
1887
  });
1759
1888
  }
1889
+ /**
1890
+ * `pugi prd-check` — Wave 6 verified-deliverable gate (2026-05-27).
1891
+ *
1892
+ * Reads `docs/prd/<feature>.md` (or any explicit path), parses the
1893
+ * acceptance-criteria section, and runs file / test / doc / command
1894
+ * / route verifiers per criterion. Same handler powers the in-REPL
1895
+ * `/prd-check` slash via session.ts so the verdict is identical
1896
+ * between surfaces.
1897
+ *
1898
+ * The `knownCommands` set is sourced from the same `handlers` map
1899
+ * used by the CLI dispatcher (one source of truth), so a PRD that
1900
+ * mentions `pugi <name>` resolves against the EXACT registry the
1901
+ * shell exposes.
1902
+ *
1903
+ * Exit codes (from reporter.exitCodeFor):
1904
+ * 0 — healthy (every criterion PASS or SKIPPED)
1905
+ * 1 — failing (≥1 FAIL)
1906
+ * 2 — unparsed (PRD has no acceptance section) OR arg error
1907
+ */
1908
+ async function dispatchPrdCheck(args, flags, _session) {
1909
+ const parsed = parsePrdCheckArgs(args, { jsonDefault: flags.json });
1910
+ if (!parsed.ok) {
1911
+ writeOutput(flags, { ok: false, error: parsed.error }, parsed.error);
1912
+ process.exitCode = 2;
1913
+ return;
1914
+ }
1915
+ await runPrdCheckCommand({
1916
+ cwd: process.cwd(),
1917
+ ...(parsed.prdPath !== undefined ? { prdPath: parsed.prdPath } : {}),
1918
+ flags: parsed.flags,
1919
+ knownCommands: knownCommandNames(),
1920
+ writeOutput: (payload, text) => writeOutput(flags, payload, text),
1921
+ });
1922
+ }
1923
+ /**
1924
+ * Snapshot the set of registered CLI command names — used by the
1925
+ * prd-check `command:` verifier so PRD mentions of `pugi <name>`
1926
+ * resolve against the exact same registry the shell exposes.
1927
+ */
1928
+ function knownCommandNames() {
1929
+ return new Set(Object.keys(handlers));
1930
+ }
1760
1931
  /**
1761
1932
  * `pugi update` — Leak L27 (2026-05-27). Channel-aware npm registry
1762
1933
  * probe + optional shell-out to `npm install -g @pugi/cli@<tag>`.
@@ -3339,6 +3510,25 @@ async function handoff(args, flags, session) {
3339
3510
  writeOutput(flags, bundle, ['Pugi handoff bundle created', `Bundle: ${bundle.path}`].join('\n'));
3340
3511
  }
3341
3512
  async function sessions(args, flags, _session) {
3513
+ // L9 (2026-05-27): `pugi sessions undo-rewind [<session-id>]` rolls
3514
+ // back the latest /rewind by appending an inverse marker. Append-only,
3515
+ // reversible. Falls through to the legacy artifact-based handler when
3516
+ // the sub-command is not recognised.
3517
+ if (args[0] === 'undo-rewind') {
3518
+ const result = await runSessionsCommand(args, {
3519
+ workspaceRoot: process.cwd(),
3520
+ writeOutput: (payload, text) => writeOutput(flags, payload, text),
3521
+ });
3522
+ if (result) {
3523
+ if (result.status === 'failed_no_session' || result.status === 'failed_store') {
3524
+ process.exitCode = 1;
3525
+ }
3526
+ else if (result.status === 'noop_no_rewind') {
3527
+ process.exitCode = 2;
3528
+ }
3529
+ return;
3530
+ }
3531
+ }
3342
3532
  // α6.4: `pugi sessions --local` / `--search "query"` route to the
3343
3533
  // local SessionStore. The default surface stays artifact-based for
3344
3534
  // 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