@pugi/cli 0.1.0-beta.20 → 0.1.0-beta.22

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 (40) hide show
  1. package/dist/core/bare-mode/index.js +107 -0
  2. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  3. package/dist/core/engine/native-pugi.js +21 -10
  4. package/dist/core/engine/prompts.js +30 -2
  5. package/dist/core/engine/tool-bridge.js +32 -0
  6. package/dist/core/feedback/queue.js +177 -0
  7. package/dist/core/feedback/submitter.js +145 -0
  8. package/dist/core/onboarding/marker.js +111 -0
  9. package/dist/core/onboarding/telemetry-state.js +108 -0
  10. package/dist/core/output-style/presets.js +176 -0
  11. package/dist/core/output-style/state.js +185 -0
  12. package/dist/core/permissions/index.js +1 -1
  13. package/dist/core/permissions/state.js +55 -0
  14. package/dist/core/repl/session.js +375 -12
  15. package/dist/core/repl/slash-commands.js +99 -1
  16. package/dist/core/repl/workspace-context.js +22 -0
  17. package/dist/core/share/formatter.js +271 -0
  18. package/dist/core/share/redactor.js +221 -0
  19. package/dist/core/share/uploader.js +267 -0
  20. package/dist/core/todos/invariant.js +10 -0
  21. package/dist/core/todos/state.js +177 -0
  22. package/dist/runtime/cli.js +386 -1
  23. package/dist/runtime/commands/doctor.js +8 -0
  24. package/dist/runtime/commands/feedback.js +184 -0
  25. package/dist/runtime/commands/onboarding.js +275 -0
  26. package/dist/runtime/commands/plan.js +143 -0
  27. package/dist/runtime/commands/share.js +316 -0
  28. package/dist/runtime/commands/stickers.js +82 -0
  29. package/dist/runtime/commands/style.js +194 -0
  30. package/dist/runtime/version.js +1 -1
  31. package/dist/tools/registry.js +8 -0
  32. package/dist/tools/todo-write.js +184 -0
  33. package/dist/tui/compact-banner.js +28 -1
  34. package/dist/tui/conversation-pane.js +13 -0
  35. package/dist/tui/feedback-prompt.js +156 -0
  36. package/dist/tui/onboarding-wizard.js +240 -0
  37. package/dist/tui/repl-render.js +9 -1
  38. package/dist/tui/stickers-art.js +136 -0
  39. package/dist/tui/style-table.js +22 -0
  40. package/package.json +2 -2
@@ -25,14 +25,20 @@ import { clearApiKey, DEFAULT_API_URL, listStoredCredentials, maskApiKey, normal
25
25
  import { runDeployCommand } from '../commands/deploy.js';
26
26
  import { runJobsCommand } from '../commands/jobs.js';
27
27
  import { runConfigCommand } from './commands/config.js';
28
+ import { runStyleCommand } from './commands/style.js';
29
+ import { runOnboardingCommand } from './commands/onboarding.js';
30
+ import { isOnboarded } from '../core/onboarding/marker.js';
28
31
  import { runPrivacyCommand } from './commands/privacy.js';
29
32
  import { runReport } from './commands/report.js';
30
33
  import { runDoctorCommand, defaultHome as defaultDoctorHome } from './commands/doctor.js';
31
34
  import { runStatusCommand, defaultStatusHome, } from './commands/status.js';
35
+ import { runStickersCommand } from './commands/stickers.js';
32
36
  import { runUndoCommand } from './commands/undo.js';
33
37
  import { runCompactCommand } from './commands/compact.js';
34
38
  import { runBudgetCommand } from './commands/budget.js';
39
+ import { BARE_MODE_BANNER, isBareMode, setBareMode, } from '../core/bare-mode/index.js';
35
40
  import { runCostCommand } from './commands/cost.js';
41
+ import { runShareCommand } from './commands/share.js';
36
42
  import { runSkillsCommand } from './commands/skills.js';
37
43
  import { installDefaultSkills } from '../core/skills/defaults.js';
38
44
  import { runAgentsCommand } from './commands/agents.js';
@@ -43,6 +49,7 @@ import { resolveWorkspaceLabel } from '../core/repl/workspace-context.js';
43
49
  import { runReviewConsensus } from './commands/review-consensus.js';
44
50
  import { runMcpCommand } from './commands/mcp.js';
45
51
  import { runPermissionsCommand } from './commands/permissions.js';
52
+ import { runPlanCommand } from './commands/plan.js';
46
53
  import { parsePermissionMode } from '../core/permissions/index.js';
47
54
  import { DECOMPOSE_PROMPT_SUFFIX, parseDecompositionFromText, writeDecomposition, } from './plan-decompose.js';
48
55
  import { FtsSyntaxError, SqliteSessionStore, resolveProjectStoreDir } from '../core/repl/store/index.js';
@@ -94,7 +101,7 @@ const handlers = {
94
101
  patch: dispatchPatch,
95
102
  permissions: dispatchPermissions,
96
103
  perms: dispatchPermissions,
97
- plan: runEngineTask('plan'),
104
+ plan: dispatchPlan,
98
105
  'plan-review': dispatchPlanReview,
99
106
  privacy: dispatchPrivacy,
100
107
  // PAVF-7 (2026-05-27): `pugi report --from-error` captures the
@@ -105,9 +112,21 @@ const handlers = {
105
112
  resume,
106
113
  roster: dispatchRoster,
107
114
  sessions,
115
+ share: dispatchShare,
108
116
  skills: dispatchSkills,
109
117
  status,
118
+ stickers,
119
+ // Leak L21 (2026-05-27): in-CLI feedback collector. Shares the
120
+ // same handler as the in-REPL `/feedback` slash; the wrapper just
121
+ // routes TTY vs non-TTY before mounting Ink.
122
+ feedback: dispatchFeedback,
110
123
  sync,
124
+ style: dispatchStyle,
125
+ // Leak L25 (2026-05-27): `pugi onboarding` walks the new operator
126
+ // through auth / mode / style / MCP / telemetry. Idempotent;
127
+ // `--reset` clears the marker file so the bare-invocation hint
128
+ // re-arms without nuking persisted defaults.
129
+ onboarding: dispatchOnboarding,
111
130
  undo: dispatchUndo,
112
131
  compact: dispatchCompact,
113
132
  // L19 (2026-05-27): `pugi usage` is an alias of `pugi cost` — same
@@ -288,6 +307,59 @@ async function dispatchPrivacy(args, flags, _session) {
288
307
  writeOutput: (payload, text) => writeOutput(flags, payload, text),
289
308
  });
290
309
  }
310
+ /**
311
+ * Leak L18 (2026-05-27) — `pugi style` top-level dispatcher.
312
+ *
313
+ * Forwards to the shared `runStyleCommand` runner. The REPL `/style`
314
+ * slash uses the same runner via a dynamic import inside
315
+ * `core/repl/session.ts` so the two surfaces stay single-sourced.
316
+ *
317
+ * Exit-code policy:
318
+ * - 0 — show / switch / reset / list happy paths
319
+ * - 1 — unknown preset slug
320
+ * - 2 — conflicting flags (`--reset` + positional / `--reset --persist`)
321
+ *
322
+ * The runner returns the code; we attach it to `process.exitCode` so
323
+ * subsequent dispatch wrappers do not clobber it on success.
324
+ */
325
+ async function dispatchStyle(args, flags, _session) {
326
+ const rc = await runStyleCommand(args, {
327
+ workspaceRoot: process.cwd(),
328
+ writeOutput: (payload, text) => writeOutput(flags, payload, text),
329
+ });
330
+ if (rc !== 0)
331
+ process.exitCode = rc;
332
+ }
333
+ /**
334
+ * Leak L25 (2026-05-27) — `pugi onboarding` top-level dispatcher.
335
+ *
336
+ * Walks the new operator through auth / permission mode / output
337
+ * style / MCP / telemetry consent. The Ink wizard mounts only when
338
+ * stdin is a TTY and `--json` is not set; otherwise we dump the
339
+ * current snapshot + hints in the non-interactive envelope so
340
+ * scripted callers see the same structured payload.
341
+ *
342
+ * Auth status: we resolve credentials once up front and pass the
343
+ * boolean to the runner; the wizard surfaces a `pugi login` hint
344
+ * when auth is missing but DOES NOT block — local defaults are still
345
+ * configurable without an active credential.
346
+ *
347
+ * Exit-code policy:
348
+ * 0 — completed / cancelled / non-interactive / reset
349
+ * 2 — conflicting / unknown flags
350
+ */
351
+ async function dispatchOnboarding(args, flags, _session) {
352
+ const credential = resolveActiveCredential();
353
+ const rc = await runOnboardingCommand(args, {
354
+ workspaceRoot: process.cwd(),
355
+ env: process.env,
356
+ authPresent: credential !== null,
357
+ interactive: isInteractive(flags) && !flags.json,
358
+ writeOutput: (payload, text) => writeOutput(flags, payload, text),
359
+ });
360
+ if (rc !== 0)
361
+ process.exitCode = rc;
362
+ }
291
363
  /**
292
364
  * PAVF-7 (2026-05-27): `pugi report --from-error` — bundle the most-
293
365
  * recent failed session into a redacted local report so operators can
@@ -438,6 +510,124 @@ async function dispatchCost(args, flags, _session) {
438
510
  writeOutput: (payload, text) => writeOutput(flags, payload, text),
439
511
  });
440
512
  }
513
+ /**
514
+ * Leak L20 (2026-05-27): `pugi share` top-level surface. Exports the
515
+ * current session transcript as Markdown to gist (default when `gh` is
516
+ * available) or pugi.io (--pugi). The handler delegates to
517
+ * `runShareCommand` so the slash surface (`/share`) and the shell
518
+ * surface share one code path. JSON output mode is honoured via the
519
+ * shared `writeOutput` wrapper.
520
+ */
521
+ async function dispatchShare(args, flags, _session) {
522
+ await runShareCommand(args, {
523
+ workspaceRoot: process.cwd(),
524
+ cliVersion: PUGI_CLI_VERSION,
525
+ writeOutput: (payload, text) => writeOutput(flags, payload, text),
526
+ });
527
+ }
528
+ /**
529
+ * Leak L7 — `pugi plan [--back | --persist | <prompt...>]`.
530
+ *
531
+ * Quick mode-switch shortcut + optional one-shot engine dispatch. Slash
532
+ * surface `/plan` shares the same `runPlanCommand` helper so the
533
+ * workspace-state writes go through one code path. Argument grammar:
534
+ *
535
+ * pugi plan -> set workspace mode = plan + banner
536
+ * pugi plan --back -> restore the mode that was active
537
+ * before the most recent /plan entry
538
+ * pugi plan --persist -> set + also write ~/.pugi/config.json
539
+ * pugi plan <prompt...> -> set + run `runEngineTask('plan')`
540
+ * with the prompt (existing offline /
541
+ * engine path; the permission gate now
542
+ * sees plan as workspace state)
543
+ * pugi plan <prompt> --auto-back -> ALSO restore previous mode once
544
+ * the engine returns (defaults to
545
+ * leaving the operator in plan
546
+ * mode so they can iterate)
547
+ *
548
+ * The handler intentionally intercepts the mode-switch flags BEFORE
549
+ * delegating to `runEngineTask('plan')` for the prompt path. Without
550
+ * this wrapper, `pugi plan` (no args) would error out of the engine
551
+ * task ("requires a prompt") which is the legacy behaviour; the L7
552
+ * spec wants bare `pugi plan` to be the mode switch.
553
+ */
554
+ async function dispatchPlan(args, flags, session) {
555
+ // Strip `--back` / `--auto-back` from the positional args — the global
556
+ // parseArgs does not consume them (they are command-local). Anything
557
+ // else stays in `prompt` so the engine sees the operator's text
558
+ // verbatim. The flag parser keeps both `--back` and the spelling
559
+ // variants the operator might type from muscle memory after using
560
+ // `git checkout --` style flows.
561
+ let back = false;
562
+ let autoBack = false;
563
+ const remaining = [];
564
+ for (const arg of args) {
565
+ if (arg === '--back') {
566
+ back = true;
567
+ }
568
+ else if (arg === '--auto-back') {
569
+ autoBack = true;
570
+ }
571
+ else {
572
+ remaining.push(arg);
573
+ }
574
+ }
575
+ const hasPrompt = remaining.length > 0;
576
+ const persist = Boolean(flags.persist);
577
+ // --back and a prompt are mutually exclusive — back is a revert action,
578
+ // not a dispatch one. Refuse the combination with a clear hint instead
579
+ // of silently dropping one or the other.
580
+ if (back && hasPrompt) {
581
+ writeOutput(flags, { ok: false, error: 'pugi plan --back does not accept a prompt; revert first, then dispatch.' }, 'pugi plan --back does not accept a prompt; revert first, then dispatch.');
582
+ process.exitCode = 2;
583
+ return;
584
+ }
585
+ // --back + --auto-back is incoherent (auto-back applies to the
586
+ // dispatch path) — refuse rather than degrade silently.
587
+ if (back && autoBack) {
588
+ writeOutput(flags, { ok: false, error: 'pugi plan --back and --auto-back cannot be combined.' }, 'pugi plan --back and --auto-back cannot be combined.');
589
+ process.exitCode = 2;
590
+ return;
591
+ }
592
+ // When a prompt is going to be dispatched in --json mode, suppress
593
+ // the human-readable banner writes so the engine task remains the
594
+ // single JSON emitter on stdout. The mode write still happens. In
595
+ // human (non --json) mode the banner prints normally so the operator
596
+ // sees the gate-state change before the engine starts thinking.
597
+ const sinkSilent = hasPrompt && flags.json;
598
+ const writeLine = (line) => {
599
+ if (sinkSilent)
600
+ return;
601
+ writeOutput(flags, { text: line }, line);
602
+ };
603
+ const result = await runPlanCommand({ back, persist }, {
604
+ workspaceRoot: process.cwd(),
605
+ writeOutput: writeLine,
606
+ });
607
+ // No prompt → mode-switch only. Done.
608
+ if (!hasPrompt)
609
+ return;
610
+ // Prompt present → fall through to the existing engine task with the
611
+ // remaining args. The workspace mode is now `plan` (or stayed `plan`
612
+ // if already there); the engine sees the same plan-task semantics it
613
+ // always has — read-only schema + executor refusal sentinel — but the
614
+ // permission GATE now also enforces plan independently.
615
+ try {
616
+ await runEngineTask('plan')(remaining, flags, session);
617
+ }
618
+ finally {
619
+ // --auto-back restores the previous mode AFTER the engine returns
620
+ // (success OR failure) so the operator's gate state mirrors a normal
621
+ // `--back` invocation. Without --auto-back the operator stays in
622
+ // plan and can iterate / inspect before acting.
623
+ if (autoBack && (result.verdict === 'entered' || result.verdict === 'persisted')) {
624
+ await runPlanCommand({ back: true, persist: false }, {
625
+ workspaceRoot: process.cwd(),
626
+ writeOutput: writeLine,
627
+ });
628
+ }
629
+ }
630
+ }
441
631
  async function dispatchSkills(args, flags, _session) {
442
632
  await runSkillsCommand(args, {
443
633
  workspaceRoot: process.cwd(),
@@ -567,6 +757,21 @@ async function dispatchWorktree(args, flags, _session) {
567
757
  }
568
758
  export async function runCli(argv) {
569
759
  const { command, args, flags, isBareInvocation } = parseArgs(argv);
760
+ // Leak L22 — print the one-line bare banner once per invocation when
761
+ // the flag is active and stdout is NOT bound for JSON consumption. The
762
+ // banner goes to stderr so it never lands in a `--json` envelope or a
763
+ // pipe-captured stdout stream; operators see it on the terminal,
764
+ // scripted callers stay clean. Suppressed for `pugi version` / `pugi
765
+ // help` (short, scripted-friendly surfaces) and when the operator
766
+ // sets PUGI_BARE without the flag (avoids double-printing across
767
+ // scripted nested invocations).
768
+ if (flags.bare &&
769
+ !flags.json &&
770
+ command !== 'version' &&
771
+ command !== 'help' &&
772
+ argv.includes('--bare')) {
773
+ process.stderr.write(`${BARE_MODE_BANNER}\n`);
774
+ }
570
775
  // β-headless dispatch (CEO directive 2026-05-27 "нужно тестирование по
571
776
  // кругу"): when `--print <brief>` is set we route to the headless
572
777
  // runner BEFORE the REPL / splash / command branches. The runner
@@ -598,6 +803,19 @@ export async function runCli(argv) {
598
803
  process.exitCode = exitCode;
599
804
  return;
600
805
  }
806
+ // Leak L25 (2026-05-27): first-run hint. When the operator types a
807
+ // bare `pugi` on a real TTY AND the onboarding marker is absent, drop
808
+ // a one-line hint on stderr BEFORE the REPL splash mounts. Stderr so
809
+ // the line never lands in a `--json` envelope or a scripted stdout
810
+ // pipe; suppressed when --json is set or the operator already walked
811
+ // the wizard. The marker check is best-effort — a fs glitch returns
812
+ // false and we print the hint, which is harmless.
813
+ if (isBareInvocation
814
+ && isInteractive(flags)
815
+ && !flags.json
816
+ && !isOnboarded(process.env)) {
817
+ process.stderr.write('Tip: run `pugi onboarding` to configure defaults.\n');
818
+ }
601
819
  // Bare `pugi` on a TTY enters the REPL-by-default agentic session
602
820
  // (Sprint α5.7, ADR-0056). The REPL is the customer-facing surface
603
821
  // that brings Pugi to parity with Claude Code / Codex CLI. When the
@@ -697,8 +915,27 @@ function parseArgs(argv) {
697
915
  // surface.
698
916
  persist: false,
699
917
  confirm: false,
918
+ // Leak L22 — `--bare` flag (skip project auto-discovery). Default
919
+ // honors the env var so a wrapper script that exports PUGI_BARE=1
920
+ // keeps the bit even when the operator forgets the flag, and the
921
+ // explicit flag overrides on the way through the loop below.
922
+ bare: isBareMode(),
923
+ // Leak L33 — `--ascii-only` for `pugi stickers`. Default off so the
924
+ // interactive surface keeps its boxed renderer; opt-in via flag
925
+ // for pipe / script use.
926
+ asciiOnly: false,
700
927
  };
701
928
  const args = [];
929
+ // Leak L22: scan for `--bare` BEFORE the early-return short-circuits
930
+ // below. Operators may pass `pugi --bare --version` or `pugi --bare
931
+ // --help` and the short-circuit return must still flip the bare bit
932
+ // so subprocesses + env-consulting modules see the activated state.
933
+ // The bit is idempotent — re-applied inside the main loop below for
934
+ // non-short-circuit paths.
935
+ if (argv.includes('--bare')) {
936
+ flags.bare = true;
937
+ setBareMode();
938
+ }
702
939
  // Sprint 2E: `pugi --version` / `-v` are universal install-test conventions
703
940
  // (npm uses --version on every published bin, Homebrew formula uses it in
704
941
  // the test block). Normalize them to the `version` command so users can
@@ -767,6 +1004,12 @@ function parseArgs(argv) {
767
1004
  // at the global level for consistency with --no-splash / --no-tool-stream.
768
1005
  flags.noDefaults = true;
769
1006
  }
1007
+ else if (arg === '--ascii-only') {
1008
+ // Leak L33 — `pugi stickers --ascii-only` skips the Ink boxed
1009
+ // renderer. Parsed globally so the dispatcher can pass the flag
1010
+ // through to runStickersCommand without per-command argv slicing.
1011
+ flags.asciiOnly = true;
1012
+ }
770
1013
  else if (arg === '--decompose') {
771
1014
  // α6.8 EXTEND PR1: plan-only flag. Other engine commands ignore
772
1015
  // it. Parsed globally for symmetry with the rest of the flag
@@ -917,6 +1160,16 @@ function parseArgs(argv) {
917
1160
  // acknowledgement).
918
1161
  flags.confirm = true;
919
1162
  }
1163
+ else if (arg === '--bare') {
1164
+ // Leak L22: disable project auto-discovery for this invocation.
1165
+ // Set BOTH the parsed flag and the process env so downstream
1166
+ // modules consulting `isBareMode()` (markdown-traverse callsite,
1167
+ // REPL auto-init gate, doctor probe, subprocess spawns) see a
1168
+ // coherent activated state without re-threading the bit through
1169
+ // every call signature.
1170
+ flags.bare = true;
1171
+ setBareMode();
1172
+ }
920
1173
  else {
921
1174
  args.push(arg);
922
1175
  }
@@ -1044,6 +1297,28 @@ const COMMAND_HELP_BODIES = {
1044
1297
  ' pugi config get privacy',
1045
1298
  ' pugi config set privacy=<mode>',
1046
1299
  ],
1300
+ share: [
1301
+ 'pugi share — export the current session transcript (leak L20).',
1302
+ '',
1303
+ 'Reads .pugi/events.jsonl, formats it as Markdown, and uploads to',
1304
+ 'either a GitHub Gist (`gh`-backed, default when `gh` is available)',
1305
+ 'or pugi.io (--pugi). Always prompts before upload unless --yes is',
1306
+ 'set. Refuses upload entirely if the transcript carries an active',
1307
+ '`Bearer ` credential — re-run with --redact to scrub it first.',
1308
+ '',
1309
+ 'Flags:',
1310
+ ' --gist Force gist target; refuses if gh CLI is absent.',
1311
+ ' --pugi Force pugi.io target (requires `pugi login`).',
1312
+ ' --redact Run PII scrubber before upload.',
1313
+ ' --preview Print the transcript to stdout WITHOUT upload.',
1314
+ ' --yes, -y Skip the y/n confirmation prompt.',
1315
+ ' --json Emit a structured JSON envelope only.',
1316
+ '',
1317
+ 'Examples:',
1318
+ ' pugi share Auto-pick + confirm.',
1319
+ ' pugi share --preview --redact See what would be shared.',
1320
+ ' pugi share --gist --redact --yes Scripted secret-gist upload.',
1321
+ ],
1047
1322
  cost: [
1048
1323
  'pugi cost — token + USD breakdown for the current Pugi session.',
1049
1324
  '',
@@ -1155,6 +1430,32 @@ const COMMAND_HELP_BODIES = {
1155
1430
  'Useful in shell scripts that need a human-confirm before a destructive',
1156
1431
  'step. Exits 0 on yes, 1 on no, 2 on cancel.',
1157
1432
  ],
1433
+ stickers: [
1434
+ 'pugi stickers — show a Pugi brand sticker (gimmick).',
1435
+ '',
1436
+ 'Picks one of the curated pug-face ASCII variants at random and footers',
1437
+ 'it with a rotating brand quote. Brand-personality surface — never a gate.',
1438
+ '',
1439
+ ' --json Emit a structured envelope (id · caption · quote).',
1440
+ ' --ascii-only Plain stdout (no box, no dim accents) for scripting.',
1441
+ '',
1442
+ 'Also available as /stickers from inside the REPL.',
1443
+ ],
1444
+ feedback: [
1445
+ 'pugi feedback — file a bug / feature / general comment from the CLI.',
1446
+ '',
1447
+ 'Interactive five-step wizard:',
1448
+ ' 1. category (bug / feature / general / praise)',
1449
+ ' 2. rating (1-5 stars)',
1450
+ ' 3. comment (multi-line, Ctrl-D submits)',
1451
+ ' 4. include redacted last 5 turns? (y/n, default n)',
1452
+ ' 5. confirm submit (y/n, default y)',
1453
+ '',
1454
+ 'On network failure the envelope is appended to',
1455
+ '.pugi/feedback-queue.jsonl and drained on the next online session.',
1456
+ '',
1457
+ 'Also available as /feedback from inside the REPL.',
1458
+ ],
1158
1459
  deploy: [
1159
1460
  'pugi deploy — trigger a vendor deployment from the bound Git source.',
1160
1461
  '',
@@ -1248,6 +1549,10 @@ async function help(args, flags, _session) {
1248
1549
  ' Pairs with PUGI_HIDE_TOOL_STREAM=1.',
1249
1550
  ' --no-defaults Skip bundled default-skills install on',
1250
1551
  ' `pugi init`. Pairs with PUGI_INIT_NO_DEFAULTS=1.',
1552
+ ' --bare Disable project auto-discovery — no PUGI.md /',
1553
+ ' AGENTS.md / CLAUDE.md / GEMINI.md walk-up, no',
1554
+ ' auto-init of .pugi/, no persona auto-load.',
1555
+ ' Pairs with PUGI_BARE=1.',
1251
1556
  '',
1252
1557
  PUGI_TAGLINE,
1253
1558
  'Execution defaults to local. Use --remote or --web to create a handoff bundle.',
@@ -1301,6 +1606,86 @@ async function status(_args, flags, _session) {
1301
1606
  writeOutput: (payload, text) => writeOutput(flags, payload, text),
1302
1607
  });
1303
1608
  }
1609
+ /**
1610
+ * `pugi stickers` — Leak L33 (2026-05-27). Brand-personality gimmick
1611
+ * mirroring Claude Code's `/stickers` easter egg. Picks one curated
1612
+ * pug-face ASCII variant at random + footers it with a rotating quote
1613
+ * from the Pugi brand corpus. Always exits 0 — never a gate.
1614
+ *
1615
+ * The handler stays thin: corpus + picker + pure renderers live in
1616
+ * `tui/stickers-art.tsx`; this wrapper just hands the resolved result
1617
+ * к the shared `writeOutput` helper so `--json` keeps producing a
1618
+ * structured envelope (id + caption + quote + meta) for scripted
1619
+ * callers. The `--ascii-only` flag drops the box decoration in the
1620
+ * non-JSON path so pipes (`pugi stickers --ascii-only | lolcat`) get
1621
+ * clean plain-text frames.
1622
+ *
1623
+ * The same handler powers the in-REPL `/stickers` slash, which routes
1624
+ * the text through the conversation system pane line-buffer.
1625
+ */
1626
+ async function stickers(_args, flags, _session) {
1627
+ runStickersCommand({
1628
+ json: flags.json,
1629
+ asciiOnly: flags.asciiOnly,
1630
+ writeOutput: (payload, text) => writeOutput(flags, payload, text),
1631
+ });
1632
+ }
1633
+ /**
1634
+ * `pugi feedback` — Leak L21 (2026-05-27). In-CLI feedback collector.
1635
+ *
1636
+ * Five-step wizard:
1637
+ * 1. category (bug / feature / general / praise)
1638
+ * 2. rating (1-5)
1639
+ * 3. comment (multi-line, Ctrl-D submits)
1640
+ * 4. include redacted session context? (y/n, default n)
1641
+ * 5. confirm submit (y/n, default y)
1642
+ *
1643
+ * POSTs to `<apiUrl>/api/pugi/feedback`. On transient failure (404,
1644
+ * 5xx, network error) the envelope is appended to
1645
+ * `<cwd>/.pugi/feedback-queue.jsonl`. On next online session the
1646
+ * background flusher drains the queue silently.
1647
+ *
1648
+ * Non-TTY callers (CI, pipes) get a one-line "non-interactive — re-run
1649
+ * in a real terminal" stub. The feedback wizard is intentionally
1650
+ * TTY-only — scripting a star-rating + multi-line comment from a
1651
+ * shell pipe would just produce low-signal noise.
1652
+ */
1653
+ async function dispatchFeedback(_args, flags, _session) {
1654
+ if (!isInteractive(flags)) {
1655
+ writeOutput(flags, {
1656
+ ok: false,
1657
+ error: 'pugi feedback requires an interactive terminal. Re-run from a real TTY.',
1658
+ }, 'pugi feedback: non-interactive shell — re-run from a real terminal.');
1659
+ process.exitCode = 2;
1660
+ return;
1661
+ }
1662
+ const { renderFeedbackPrompt } = await import('../tui/feedback-prompt.js');
1663
+ const { runFeedbackCommand, renderFeedbackToast } = await import('./commands/feedback.js');
1664
+ const { submitFeedback } = await import('../core/feedback/submitter.js');
1665
+ const verdict = await renderFeedbackPrompt();
1666
+ if (verdict.cancelled || !verdict.draft) {
1667
+ writeOutput(flags, { ok: true, kind: 'cancelled' }, 'Feedback cancelled. Nothing was sent.');
1668
+ return;
1669
+ }
1670
+ // Best-effort credential resolution. Anonymous submission is allowed
1671
+ // (the server may still accept it for ungated `/api/pugi/feedback`
1672
+ // routes); on no-credential we route the POST through an empty
1673
+ // bearer + the operator gets the 4xx → "rejected" toast if the
1674
+ // server requires auth.
1675
+ const credential = resolveActiveCredential(process.env);
1676
+ const apiUrl = credential?.apiUrl ?? (process.env.PUGI_API_URL || 'https://api.pugi.io');
1677
+ const apiKey = credential?.apiKey ?? '';
1678
+ const result = await runFeedbackCommand({
1679
+ cwd: process.cwd(),
1680
+ cliVersion: PUGI_CLI_VERSION,
1681
+ submit: async (env) => submitFeedback(env, { apiUrl, apiKey }),
1682
+ draft: verdict.draft,
1683
+ // `pugi feedback` from a fresh shell has no live transcript — the
1684
+ // session-context provider is omitted. The REPL slash variant
1685
+ // wires this in via `runFeedbackSlash` (session.ts).
1686
+ });
1687
+ writeOutput(flags, { ok: true, result }, renderFeedbackToast(result));
1688
+ }
1304
1689
  /**
1305
1690
  * Programmatic init scaffolder. Idempotent — every helper call is a
1306
1691
  * `*_IfMissing` write, so re-running over an existing .pugi/ workspace
@@ -49,6 +49,7 @@ import { probeMcp } from '../../core/diagnostics/probes/mcp.js';
49
49
  import { probeConfig } from '../../core/diagnostics/probes/config.js';
50
50
  import { probeSession } from '../../core/diagnostics/probes/session.js';
51
51
  import { probeDenialTracking } from '../../core/diagnostics/probes/denial-tracking.js';
52
+ import { probeBareMode } from '../../core/diagnostics/probes/bare-mode.js';
52
53
  /**
53
54
  * Default API URL when no PUGI_API_URL env override is set. Mirrors
54
55
  * the constant in `core/credentials.ts` (kept local to avoid an
@@ -206,6 +207,13 @@ export function buildDefaultProbes(ctx, options = {}) {
206
207
  ...(options.denialTracking ? { tracker: options.denialTracking } : {}),
207
208
  }),
208
209
  },
210
+ // Leak L22 (2026-05-27): BARE MODE row. Always present so the JSON
211
+ // schema stays stable; status flips to `ok` when `--bare` or
212
+ // `PUGI_BARE=1` is active, otherwise `skipped`.
213
+ {
214
+ name: 'BARE MODE',
215
+ run: async () => probeBareMode({ env: ctx.env }),
216
+ },
209
217
  ];
210
218
  return probes;
211
219
  }