@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
@@ -22,17 +22,27 @@ import { PUGI_TAGLINE } from '@pugi/personas';
22
22
  import { resolveRoster, renderRosterTable } from './commands/roster.js';
23
23
  import { runDelegateCommand } from './commands/delegate.js';
24
24
  import { clearApiKey, DEFAULT_API_URL, listStoredCredentials, maskApiKey, normalizeApiUrl, purgeAllCredentials, readCredentialsFile, resolveActiveCredential, storeApiKey, switchActiveAccount, } from '../core/credentials.js';
25
+ import { resolveAndValidateEnvLogin, } from '../core/auth/env-provider.js';
25
26
  import { runDeployCommand } from '../commands/deploy.js';
26
27
  import { runJobsCommand } from '../commands/jobs.js';
27
28
  import { runConfigCommand } from './commands/config.js';
29
+ import { runStyleCommand } from './commands/style.js';
30
+ import { runThemeCommand } from './commands/theme.js';
31
+ import { runOnboardingCommand } from './commands/onboarding.js';
32
+ import { runVimCommand } from './commands/vim.js';
33
+ import { isOnboarded } from '../core/onboarding/marker.js';
28
34
  import { runPrivacyCommand } from './commands/privacy.js';
29
35
  import { runReport } from './commands/report.js';
30
36
  import { runDoctorCommand, defaultHome as defaultDoctorHome } from './commands/doctor.js';
31
37
  import { runStatusCommand, defaultStatusHome, } from './commands/status.js';
38
+ import { runStickersCommand } from './commands/stickers.js';
39
+ import { runReleaseNotesCommand, defaultReleaseNotesHome, } from './commands/release-notes.js';
32
40
  import { runUndoCommand } from './commands/undo.js';
33
41
  import { runCompactCommand } from './commands/compact.js';
34
42
  import { runBudgetCommand } from './commands/budget.js';
43
+ import { BARE_MODE_BANNER, isBareMode, setBareMode, } from '../core/bare-mode/index.js';
35
44
  import { runCostCommand } from './commands/cost.js';
45
+ import { runShareCommand } from './commands/share.js';
36
46
  import { runSkillsCommand } from './commands/skills.js';
37
47
  import { installDefaultSkills } from '../core/skills/defaults.js';
38
48
  import { runAgentsCommand } from './commands/agents.js';
@@ -43,6 +53,7 @@ import { resolveWorkspaceLabel } from '../core/repl/workspace-context.js';
43
53
  import { runReviewConsensus } from './commands/review-consensus.js';
44
54
  import { runMcpCommand } from './commands/mcp.js';
45
55
  import { runPermissionsCommand } from './commands/permissions.js';
56
+ import { runPlanCommand } from './commands/plan.js';
46
57
  import { parsePermissionMode } from '../core/permissions/index.js';
47
58
  import { DECOMPOSE_PROMPT_SUFFIX, parseDecompositionFromText, writeDecomposition, } from './plan-decompose.js';
48
59
  import { FtsSyntaxError, SqliteSessionStore, resolveProjectStoreDir } from '../core/repl/store/index.js';
@@ -94,9 +105,15 @@ const handlers = {
94
105
  patch: dispatchPatch,
95
106
  permissions: dispatchPermissions,
96
107
  perms: dispatchPermissions,
97
- plan: runEngineTask('plan'),
108
+ plan: dispatchPlan,
98
109
  'plan-review': dispatchPlanReview,
99
110
  privacy: dispatchPrivacy,
111
+ // L24 (2026-05-27): `pugi release-notes` shows the bundled CHANGELOG
112
+ // diff between the operator's last-seen version + installed version.
113
+ // The slash counterpart `/release-notes` shares this handler via the
114
+ // shared `runReleaseNotesCommand` runner.
115
+ 'release-notes': releaseNotes,
116
+ releaseNotes,
100
117
  // PAVF-7 (2026-05-27): `pugi report --from-error` captures the
101
118
  // most-recent failed session as a redacted bundle so operators can
102
119
  // file clean bug reports without manual log-grepping.
@@ -105,9 +122,29 @@ const handlers = {
105
122
  resume,
106
123
  roster: dispatchRoster,
107
124
  sessions,
125
+ share: dispatchShare,
108
126
  skills: dispatchSkills,
109
127
  status,
128
+ stickers,
129
+ // Leak L21 (2026-05-27): in-CLI feedback collector. Shares the
130
+ // same handler as the in-REPL `/feedback` slash; the wrapper just
131
+ // routes TTY vs non-TTY before mounting Ink.
132
+ feedback: dispatchFeedback,
110
133
  sync,
134
+ style: dispatchStyle,
135
+ // Leak L30 (2026-05-27): `pugi theme` flips the local TUI color
136
+ // palette (orthogonal to `pugi style` — that one steers engine
137
+ // prose register). 4 presets: default / dark / light / colorblind.
138
+ theme: dispatchTheme,
139
+ // Leak L25 (2026-05-27): `pugi onboarding` walks the new operator
140
+ // through auth / mode / style / MCP / telemetry. Idempotent;
141
+ // `--reset` clears the marker file so the bare-invocation hint
142
+ // re-arms without nuking persisted defaults.
143
+ onboarding: dispatchOnboarding,
144
+ // Leak L26 (2026-05-27): `pugi vim` toggles vim-style modal editing
145
+ // in the REPL input buffer. Bare invocation toggles, `on`/`off`
146
+ // sets explicitly; preference persists in ~/.pugi/config.json.
147
+ vim: dispatchVim,
111
148
  undo: dispatchUndo,
112
149
  compact: dispatchCompact,
113
150
  // L19 (2026-05-27): `pugi usage` is an alias of `pugi cost` — same
@@ -288,6 +325,101 @@ async function dispatchPrivacy(args, flags, _session) {
288
325
  writeOutput: (payload, text) => writeOutput(flags, payload, text),
289
326
  });
290
327
  }
328
+ /**
329
+ * Leak L18 (2026-05-27) — `pugi style` top-level dispatcher.
330
+ *
331
+ * Forwards to the shared `runStyleCommand` runner. The REPL `/style`
332
+ * slash uses the same runner via a dynamic import inside
333
+ * `core/repl/session.ts` so the two surfaces stay single-sourced.
334
+ *
335
+ * Exit-code policy:
336
+ * - 0 — show / switch / reset / list happy paths
337
+ * - 1 — unknown preset slug
338
+ * - 2 — conflicting flags (`--reset` + positional / `--reset --persist`)
339
+ *
340
+ * The runner returns the code; we attach it to `process.exitCode` so
341
+ * subsequent dispatch wrappers do not clobber it on success.
342
+ */
343
+ async function dispatchStyle(args, flags, _session) {
344
+ const rc = await runStyleCommand(args, {
345
+ workspaceRoot: process.cwd(),
346
+ writeOutput: (payload, text) => writeOutput(flags, payload, text),
347
+ });
348
+ if (rc !== 0)
349
+ process.exitCode = rc;
350
+ }
351
+ /**
352
+ * Leak L30 (2026-05-27) — `pugi theme` top-level dispatcher.
353
+ *
354
+ * Forwards to the shared `runThemeCommand` runner. The REPL `/theme`
355
+ * slash uses the same runner via a dynamic import inside
356
+ * `core/repl/session.ts` so the two surfaces stay single-sourced.
357
+ *
358
+ * Exit-code policy mirrors `dispatchStyle`:
359
+ * - 0 — show / switch / reset / list happy paths
360
+ * - 1 — unknown preset slug
361
+ * - 2 — conflicting flags (`--reset` + positional / `--reset --persist`)
362
+ *
363
+ * The runner returns the code; we attach it to `process.exitCode` so
364
+ * subsequent dispatch wrappers do not clobber it on success.
365
+ */
366
+ async function dispatchTheme(args, flags, _session) {
367
+ const rc = await runThemeCommand(args, {
368
+ workspaceRoot: process.cwd(),
369
+ writeOutput: (payload, text) => writeOutput(flags, payload, text),
370
+ });
371
+ if (rc !== 0)
372
+ process.exitCode = rc;
373
+ }
374
+ /**
375
+ * Leak L25 (2026-05-27) — `pugi onboarding` top-level dispatcher.
376
+ *
377
+ * Walks the new operator through auth / permission mode / output
378
+ * style / MCP / telemetry consent. The Ink wizard mounts only when
379
+ * stdin is a TTY and `--json` is not set; otherwise we dump the
380
+ * current snapshot + hints in the non-interactive envelope so
381
+ * scripted callers see the same structured payload.
382
+ *
383
+ * Auth status: we resolve credentials once up front and pass the
384
+ * boolean to the runner; the wizard surfaces a `pugi login` hint
385
+ * when auth is missing but DOES NOT block — local defaults are still
386
+ * configurable without an active credential.
387
+ *
388
+ * Exit-code policy:
389
+ * 0 — completed / cancelled / non-interactive / reset
390
+ * 2 — conflicting / unknown flags
391
+ */
392
+ async function dispatchOnboarding(args, flags, _session) {
393
+ const credential = resolveActiveCredential();
394
+ const rc = await runOnboardingCommand(args, {
395
+ workspaceRoot: process.cwd(),
396
+ env: process.env,
397
+ authPresent: credential !== null,
398
+ interactive: isInteractive(flags) && !flags.json,
399
+ writeOutput: (payload, text) => writeOutput(flags, payload, text),
400
+ });
401
+ if (rc !== 0)
402
+ process.exitCode = rc;
403
+ }
404
+ /**
405
+ * Leak L26 (2026-05-27) — `pugi vim` top-level dispatcher.
406
+ *
407
+ * Forwards to the shared `runVimCommand` runner. The REPL `/vim` slash
408
+ * uses the same runner via a dynamic import inside
409
+ * `core/repl/session.ts` so the two surfaces stay single-sourced.
410
+ *
411
+ * Exit-code policy:
412
+ * - 0 — show / enable / disable / toggle happy paths
413
+ * - 2 — unknown subcommand (e.g. `pugi vim chaos`) or too many args
414
+ */
415
+ async function dispatchVim(args, flags, _session) {
416
+ const rc = await runVimCommand(args, {
417
+ env: process.env,
418
+ writeOutput: (payload, text) => writeOutput(flags, payload, text),
419
+ });
420
+ if (rc !== 0)
421
+ process.exitCode = rc;
422
+ }
291
423
  /**
292
424
  * PAVF-7 (2026-05-27): `pugi report --from-error` — bundle the most-
293
425
  * recent failed session into a redacted local report so operators can
@@ -438,6 +570,124 @@ async function dispatchCost(args, flags, _session) {
438
570
  writeOutput: (payload, text) => writeOutput(flags, payload, text),
439
571
  });
440
572
  }
573
+ /**
574
+ * Leak L20 (2026-05-27): `pugi share` top-level surface. Exports the
575
+ * current session transcript as Markdown to gist (default when `gh` is
576
+ * available) or pugi.io (--pugi). The handler delegates to
577
+ * `runShareCommand` so the slash surface (`/share`) and the shell
578
+ * surface share one code path. JSON output mode is honoured via the
579
+ * shared `writeOutput` wrapper.
580
+ */
581
+ async function dispatchShare(args, flags, _session) {
582
+ await runShareCommand(args, {
583
+ workspaceRoot: process.cwd(),
584
+ cliVersion: PUGI_CLI_VERSION,
585
+ writeOutput: (payload, text) => writeOutput(flags, payload, text),
586
+ });
587
+ }
588
+ /**
589
+ * Leak L7 — `pugi plan [--back | --persist | <prompt...>]`.
590
+ *
591
+ * Quick mode-switch shortcut + optional one-shot engine dispatch. Slash
592
+ * surface `/plan` shares the same `runPlanCommand` helper so the
593
+ * workspace-state writes go through one code path. Argument grammar:
594
+ *
595
+ * pugi plan -> set workspace mode = plan + banner
596
+ * pugi plan --back -> restore the mode that was active
597
+ * before the most recent /plan entry
598
+ * pugi plan --persist -> set + also write ~/.pugi/config.json
599
+ * pugi plan <prompt...> -> set + run `runEngineTask('plan')`
600
+ * with the prompt (existing offline /
601
+ * engine path; the permission gate now
602
+ * sees plan as workspace state)
603
+ * pugi plan <prompt> --auto-back -> ALSO restore previous mode once
604
+ * the engine returns (defaults to
605
+ * leaving the operator in plan
606
+ * mode so they can iterate)
607
+ *
608
+ * The handler intentionally intercepts the mode-switch flags BEFORE
609
+ * delegating to `runEngineTask('plan')` for the prompt path. Without
610
+ * this wrapper, `pugi plan` (no args) would error out of the engine
611
+ * task ("requires a prompt") which is the legacy behaviour; the L7
612
+ * spec wants bare `pugi plan` to be the mode switch.
613
+ */
614
+ async function dispatchPlan(args, flags, session) {
615
+ // Strip `--back` / `--auto-back` from the positional args — the global
616
+ // parseArgs does not consume them (they are command-local). Anything
617
+ // else stays in `prompt` so the engine sees the operator's text
618
+ // verbatim. The flag parser keeps both `--back` and the spelling
619
+ // variants the operator might type from muscle memory after using
620
+ // `git checkout --` style flows.
621
+ let back = false;
622
+ let autoBack = false;
623
+ const remaining = [];
624
+ for (const arg of args) {
625
+ if (arg === '--back') {
626
+ back = true;
627
+ }
628
+ else if (arg === '--auto-back') {
629
+ autoBack = true;
630
+ }
631
+ else {
632
+ remaining.push(arg);
633
+ }
634
+ }
635
+ const hasPrompt = remaining.length > 0;
636
+ const persist = Boolean(flags.persist);
637
+ // --back and a prompt are mutually exclusive — back is a revert action,
638
+ // not a dispatch one. Refuse the combination with a clear hint instead
639
+ // of silently dropping one or the other.
640
+ if (back && hasPrompt) {
641
+ 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.');
642
+ process.exitCode = 2;
643
+ return;
644
+ }
645
+ // --back + --auto-back is incoherent (auto-back applies to the
646
+ // dispatch path) — refuse rather than degrade silently.
647
+ if (back && autoBack) {
648
+ writeOutput(flags, { ok: false, error: 'pugi plan --back and --auto-back cannot be combined.' }, 'pugi plan --back and --auto-back cannot be combined.');
649
+ process.exitCode = 2;
650
+ return;
651
+ }
652
+ // When a prompt is going to be dispatched in --json mode, suppress
653
+ // the human-readable banner writes so the engine task remains the
654
+ // single JSON emitter on stdout. The mode write still happens. In
655
+ // human (non --json) mode the banner prints normally so the operator
656
+ // sees the gate-state change before the engine starts thinking.
657
+ const sinkSilent = hasPrompt && flags.json;
658
+ const writeLine = (line) => {
659
+ if (sinkSilent)
660
+ return;
661
+ writeOutput(flags, { text: line }, line);
662
+ };
663
+ const result = await runPlanCommand({ back, persist }, {
664
+ workspaceRoot: process.cwd(),
665
+ writeOutput: writeLine,
666
+ });
667
+ // No prompt → mode-switch only. Done.
668
+ if (!hasPrompt)
669
+ return;
670
+ // Prompt present → fall through to the existing engine task with the
671
+ // remaining args. The workspace mode is now `plan` (or stayed `plan`
672
+ // if already there); the engine sees the same plan-task semantics it
673
+ // always has — read-only schema + executor refusal sentinel — but the
674
+ // permission GATE now also enforces plan independently.
675
+ try {
676
+ await runEngineTask('plan')(remaining, flags, session);
677
+ }
678
+ finally {
679
+ // --auto-back restores the previous mode AFTER the engine returns
680
+ // (success OR failure) so the operator's gate state mirrors a normal
681
+ // `--back` invocation. Without --auto-back the operator stays in
682
+ // plan and can iterate / inspect before acting.
683
+ if (autoBack && (result.verdict === 'entered' || result.verdict === 'persisted')) {
684
+ await runPlanCommand({ back: true, persist: false }, {
685
+ workspaceRoot: process.cwd(),
686
+ writeOutput: writeLine,
687
+ });
688
+ }
689
+ }
690
+ }
441
691
  async function dispatchSkills(args, flags, _session) {
442
692
  await runSkillsCommand(args, {
443
693
  workspaceRoot: process.cwd(),
@@ -567,6 +817,21 @@ async function dispatchWorktree(args, flags, _session) {
567
817
  }
568
818
  export async function runCli(argv) {
569
819
  const { command, args, flags, isBareInvocation } = parseArgs(argv);
820
+ // Leak L22 — print the one-line bare banner once per invocation when
821
+ // the flag is active and stdout is NOT bound for JSON consumption. The
822
+ // banner goes to stderr so it never lands in a `--json` envelope or a
823
+ // pipe-captured stdout stream; operators see it on the terminal,
824
+ // scripted callers stay clean. Suppressed for `pugi version` / `pugi
825
+ // help` (short, scripted-friendly surfaces) and when the operator
826
+ // sets PUGI_BARE without the flag (avoids double-printing across
827
+ // scripted nested invocations).
828
+ if (flags.bare &&
829
+ !flags.json &&
830
+ command !== 'version' &&
831
+ command !== 'help' &&
832
+ argv.includes('--bare')) {
833
+ process.stderr.write(`${BARE_MODE_BANNER}\n`);
834
+ }
570
835
  // β-headless dispatch (CEO directive 2026-05-27 "нужно тестирование по
571
836
  // кругу"): when `--print <brief>` is set we route to the headless
572
837
  // runner BEFORE the REPL / splash / command branches. The runner
@@ -598,6 +863,19 @@ export async function runCli(argv) {
598
863
  process.exitCode = exitCode;
599
864
  return;
600
865
  }
866
+ // Leak L25 (2026-05-27): first-run hint. When the operator types a
867
+ // bare `pugi` on a real TTY AND the onboarding marker is absent, drop
868
+ // a one-line hint on stderr BEFORE the REPL splash mounts. Stderr so
869
+ // the line never lands in a `--json` envelope or a scripted stdout
870
+ // pipe; suppressed when --json is set or the operator already walked
871
+ // the wizard. The marker check is best-effort — a fs glitch returns
872
+ // false and we print the hint, which is harmless.
873
+ if (isBareInvocation
874
+ && isInteractive(flags)
875
+ && !flags.json
876
+ && !isOnboarded(process.env)) {
877
+ process.stderr.write('Tip: run `pugi onboarding` to configure defaults.\n');
878
+ }
601
879
  // Bare `pugi` on a TTY enters the REPL-by-default agentic session
602
880
  // (Sprint α5.7, ADR-0056). The REPL is the customer-facing surface
603
881
  // that brings Pugi to parity with Claude Code / Codex CLI. When the
@@ -697,8 +975,31 @@ function parseArgs(argv) {
697
975
  // surface.
698
976
  persist: false,
699
977
  confirm: false,
978
+ // Leak L22 — `--bare` flag (skip project auto-discovery). Default
979
+ // honors the env var so a wrapper script that exports PUGI_BARE=1
980
+ // keeps the bit even when the operator forgets the flag, and the
981
+ // explicit flag overrides on the way through the loop below.
982
+ bare: isBareMode(),
983
+ // Leak L33 — `--ascii-only` for `pugi stickers`. Default off so the
984
+ // interactive surface keeps its boxed renderer; opt-in via flag
985
+ // for pipe / script use.
986
+ asciiOnly: false,
987
+ // Leak L24 — `--reset` for `pugi release-notes`. Default off so a
988
+ // bare invocation only surfaces new sections. Opt-in to force the
989
+ // full bundled changelog к re-render (clears the on-disk marker).
990
+ reset: false,
700
991
  };
701
992
  const args = [];
993
+ // Leak L22: scan for `--bare` BEFORE the early-return short-circuits
994
+ // below. Operators may pass `pugi --bare --version` or `pugi --bare
995
+ // --help` and the short-circuit return must still flip the bare bit
996
+ // so subprocesses + env-consulting modules see the activated state.
997
+ // The bit is idempotent — re-applied inside the main loop below for
998
+ // non-short-circuit paths.
999
+ if (argv.includes('--bare')) {
1000
+ flags.bare = true;
1001
+ setBareMode();
1002
+ }
702
1003
  // Sprint 2E: `pugi --version` / `-v` are universal install-test conventions
703
1004
  // (npm uses --version on every published bin, Homebrew formula uses it in
704
1005
  // the test block). Normalize them to the `version` command so users can
@@ -767,6 +1068,20 @@ function parseArgs(argv) {
767
1068
  // at the global level for consistency with --no-splash / --no-tool-stream.
768
1069
  flags.noDefaults = true;
769
1070
  }
1071
+ else if (arg === '--ascii-only') {
1072
+ // Leak L33 — `pugi stickers --ascii-only` skips the Ink boxed
1073
+ // renderer. Parsed globally so the dispatcher can pass the flag
1074
+ // through to runStickersCommand without per-command argv slicing.
1075
+ flags.asciiOnly = true;
1076
+ }
1077
+ else if (arg === '--reset') {
1078
+ // Leak L24 — `pugi release-notes --reset` clears the on-disk
1079
+ // `~/.pugi/.last-seen-version` marker so the full bundled
1080
+ // changelog re-renders. Parsed globally for symmetry with the
1081
+ // rest of the flag grammar; `runReleaseNotesCommand` is the
1082
+ // single consumer today.
1083
+ flags.reset = true;
1084
+ }
770
1085
  else if (arg === '--decompose') {
771
1086
  // α6.8 EXTEND PR1: plan-only flag. Other engine commands ignore
772
1087
  // it. Parsed globally for symmetry with the rest of the flag
@@ -917,6 +1232,16 @@ function parseArgs(argv) {
917
1232
  // acknowledgement).
918
1233
  flags.confirm = true;
919
1234
  }
1235
+ else if (arg === '--bare') {
1236
+ // Leak L22: disable project auto-discovery for this invocation.
1237
+ // Set BOTH the parsed flag and the process env so downstream
1238
+ // modules consulting `isBareMode()` (markdown-traverse callsite,
1239
+ // REPL auto-init gate, doctor probe, subprocess spawns) see a
1240
+ // coherent activated state without re-threading the bit through
1241
+ // every call signature.
1242
+ flags.bare = true;
1243
+ setBareMode();
1244
+ }
920
1245
  else {
921
1246
  args.push(arg);
922
1247
  }
@@ -1044,6 +1369,28 @@ const COMMAND_HELP_BODIES = {
1044
1369
  ' pugi config get privacy',
1045
1370
  ' pugi config set privacy=<mode>',
1046
1371
  ],
1372
+ share: [
1373
+ 'pugi share — export the current session transcript (leak L20).',
1374
+ '',
1375
+ 'Reads .pugi/events.jsonl, formats it as Markdown, and uploads to',
1376
+ 'either a GitHub Gist (`gh`-backed, default when `gh` is available)',
1377
+ 'or pugi.io (--pugi). Always prompts before upload unless --yes is',
1378
+ 'set. Refuses upload entirely if the transcript carries an active',
1379
+ '`Bearer ` credential — re-run with --redact to scrub it first.',
1380
+ '',
1381
+ 'Flags:',
1382
+ ' --gist Force gist target; refuses if gh CLI is absent.',
1383
+ ' --pugi Force pugi.io target (requires `pugi login`).',
1384
+ ' --redact Run PII scrubber before upload.',
1385
+ ' --preview Print the transcript to stdout WITHOUT upload.',
1386
+ ' --yes, -y Skip the y/n confirmation prompt.',
1387
+ ' --json Emit a structured JSON envelope only.',
1388
+ '',
1389
+ 'Examples:',
1390
+ ' pugi share Auto-pick + confirm.',
1391
+ ' pugi share --preview --redact See what would be shared.',
1392
+ ' pugi share --gist --redact --yes Scripted secret-gist upload.',
1393
+ ],
1047
1394
  cost: [
1048
1395
  'pugi cost — token + USD breakdown for the current Pugi session.',
1049
1396
  '',
@@ -1093,7 +1440,8 @@ const COMMAND_HELP_BODIES = {
1093
1440
  'Interactive picker by default (browser OAuth / PAT / env). Non-interactive:',
1094
1441
  ' --provider device Device-flow OAuth.',
1095
1442
  ' --provider token --token <jwt> Pass a JWT directly.',
1096
- ' --provider env --env PUGI_API_KEY Read from an env var.',
1443
+ ' --provider env Read PUGI_API_KEY (or --key) + verify via /api/pugi/health.',
1444
+ ' --provider env --key <value> --skip-validate Explicit key, no probe (CI bootstrap).',
1097
1445
  ],
1098
1446
  accounts: [
1099
1447
  'pugi accounts — manage stored credentials across endpoints.',
@@ -1155,6 +1503,45 @@ const COMMAND_HELP_BODIES = {
1155
1503
  'Useful in shell scripts that need a human-confirm before a destructive',
1156
1504
  'step. Exits 0 on yes, 1 on no, 2 on cancel.',
1157
1505
  ],
1506
+ stickers: [
1507
+ 'pugi stickers — show a Pugi brand sticker (gimmick).',
1508
+ '',
1509
+ 'Picks one of the curated pug-face ASCII variants at random and footers',
1510
+ 'it with a rotating brand quote. Brand-personality surface — never a gate.',
1511
+ '',
1512
+ ' --json Emit a structured envelope (id · caption · quote).',
1513
+ ' --ascii-only Plain stdout (no box, no dim accents) for scripting.',
1514
+ '',
1515
+ 'Also available as /stickers from inside the REPL.',
1516
+ ],
1517
+ feedback: [
1518
+ 'pugi feedback — file a bug / feature / general comment from the CLI.',
1519
+ '',
1520
+ 'Interactive five-step wizard:',
1521
+ ' 1. category (bug / feature / general / praise)',
1522
+ ' 2. rating (1-5 stars)',
1523
+ ' 3. comment (multi-line, Ctrl-D submits)',
1524
+ ' 4. include redacted last 5 turns? (y/n, default n)',
1525
+ ' 5. confirm submit (y/n, default y)',
1526
+ '',
1527
+ 'On network failure the envelope is appended to',
1528
+ '.pugi/feedback-queue.jsonl and drained on the next online session.',
1529
+ '',
1530
+ 'Also available as /feedback from inside the REPL.',
1531
+ ],
1532
+ 'release-notes': [
1533
+ 'pugi release-notes — show what changed since you last upgraded.',
1534
+ '',
1535
+ 'Reads the bundled CHANGELOG.md, slices to sections strictly newer than',
1536
+ '~/.pugi/.last-seen-version, renders Markdown to stdout, then bumps the',
1537
+ 'last-seen marker to the installed CLI version. Re-running is a no-op',
1538
+ 'until you upgrade again.',
1539
+ '',
1540
+ ' --json Emit a structured envelope (sections + meta).',
1541
+ ' --reset Clear last-seen marker; re-render every section.',
1542
+ '',
1543
+ 'Also available as /release-notes from inside the REPL.',
1544
+ ],
1158
1545
  deploy: [
1159
1546
  'pugi deploy — trigger a vendor deployment from the bound Git source.',
1160
1547
  '',
@@ -1248,6 +1635,10 @@ async function help(args, flags, _session) {
1248
1635
  ' Pairs with PUGI_HIDE_TOOL_STREAM=1.',
1249
1636
  ' --no-defaults Skip bundled default-skills install on',
1250
1637
  ' `pugi init`. Pairs with PUGI_INIT_NO_DEFAULTS=1.',
1638
+ ' --bare Disable project auto-discovery — no PUGI.md /',
1639
+ ' AGENTS.md / CLAUDE.md / GEMINI.md walk-up, no',
1640
+ ' auto-init of .pugi/, no persona auto-load.',
1641
+ ' Pairs with PUGI_BARE=1.',
1251
1642
  '',
1252
1643
  PUGI_TAGLINE,
1253
1644
  'Execution defaults to local. Use --remote or --web to create a handoff bundle.',
@@ -1301,6 +1692,111 @@ async function status(_args, flags, _session) {
1301
1692
  writeOutput: (payload, text) => writeOutput(flags, payload, text),
1302
1693
  });
1303
1694
  }
1695
+ /**
1696
+ * `pugi stickers` — Leak L33 (2026-05-27). Brand-personality gimmick
1697
+ * mirroring Claude Code's `/stickers` easter egg. Picks one curated
1698
+ * pug-face ASCII variant at random + footers it with a rotating quote
1699
+ * from the Pugi brand corpus. Always exits 0 — never a gate.
1700
+ *
1701
+ * The handler stays thin: corpus + picker + pure renderers live in
1702
+ * `tui/stickers-art.tsx`; this wrapper just hands the resolved result
1703
+ * к the shared `writeOutput` helper so `--json` keeps producing a
1704
+ * structured envelope (id + caption + quote + meta) for scripted
1705
+ * callers. The `--ascii-only` flag drops the box decoration in the
1706
+ * non-JSON path so pipes (`pugi stickers --ascii-only | lolcat`) get
1707
+ * clean plain-text frames.
1708
+ *
1709
+ * The same handler powers the in-REPL `/stickers` slash, which routes
1710
+ * the text through the conversation system pane line-buffer.
1711
+ */
1712
+ async function stickers(_args, flags, _session) {
1713
+ runStickersCommand({
1714
+ json: flags.json,
1715
+ asciiOnly: flags.asciiOnly,
1716
+ writeOutput: (payload, text) => writeOutput(flags, payload, text),
1717
+ });
1718
+ }
1719
+ /**
1720
+ * `pugi feedback` — Leak L21 (2026-05-27). In-CLI feedback collector.
1721
+ *
1722
+ * Five-step wizard:
1723
+ * 1. category (bug / feature / general / praise)
1724
+ * 2. rating (1-5)
1725
+ * 3. comment (multi-line, Ctrl-D submits)
1726
+ * 4. include redacted session context? (y/n, default n)
1727
+ * 5. confirm submit (y/n, default y)
1728
+ *
1729
+ * POSTs to `<apiUrl>/api/pugi/feedback`. On transient failure (404,
1730
+ * 5xx, network error) the envelope is appended to
1731
+ * `<cwd>/.pugi/feedback-queue.jsonl`. On next online session the
1732
+ * background flusher drains the queue silently.
1733
+ *
1734
+ * Non-TTY callers (CI, pipes) get a one-line "non-interactive — re-run
1735
+ * in a real terminal" stub. The feedback wizard is intentionally
1736
+ * TTY-only — scripting a star-rating + multi-line comment from a
1737
+ * shell pipe would just produce low-signal noise.
1738
+ */
1739
+ async function dispatchFeedback(_args, flags, _session) {
1740
+ if (!isInteractive(flags)) {
1741
+ writeOutput(flags, {
1742
+ ok: false,
1743
+ error: 'pugi feedback requires an interactive terminal. Re-run from a real TTY.',
1744
+ }, 'pugi feedback: non-interactive shell — re-run from a real terminal.');
1745
+ process.exitCode = 2;
1746
+ return;
1747
+ }
1748
+ const { renderFeedbackPrompt } = await import('../tui/feedback-prompt.js');
1749
+ const { runFeedbackCommand, renderFeedbackToast } = await import('./commands/feedback.js');
1750
+ const { submitFeedback } = await import('../core/feedback/submitter.js');
1751
+ const verdict = await renderFeedbackPrompt();
1752
+ if (verdict.cancelled || !verdict.draft) {
1753
+ writeOutput(flags, { ok: true, kind: 'cancelled' }, 'Feedback cancelled. Nothing was sent.');
1754
+ return;
1755
+ }
1756
+ // Best-effort credential resolution. Anonymous submission is allowed
1757
+ // (the server may still accept it for ungated `/api/pugi/feedback`
1758
+ // routes); on no-credential we route the POST through an empty
1759
+ // bearer + the operator gets the 4xx → "rejected" toast if the
1760
+ // server requires auth.
1761
+ const credential = resolveActiveCredential(process.env);
1762
+ const apiUrl = credential?.apiUrl ?? (process.env.PUGI_API_URL || 'https://api.pugi.io');
1763
+ const apiKey = credential?.apiKey ?? '';
1764
+ const result = await runFeedbackCommand({
1765
+ cwd: process.cwd(),
1766
+ cliVersion: PUGI_CLI_VERSION,
1767
+ submit: async (env) => submitFeedback(env, { apiUrl, apiKey }),
1768
+ draft: verdict.draft,
1769
+ // `pugi feedback` from a fresh shell has no live transcript — the
1770
+ // session-context provider is omitted. The REPL slash variant
1771
+ // wires this in via `runFeedbackSlash` (session.ts).
1772
+ });
1773
+ writeOutput(flags, { ok: true, result }, renderFeedbackToast(result));
1774
+ }
1775
+ /**
1776
+ * `pugi release-notes` — Leak L24 (2026-05-27). Diff between the
1777
+ * last-seen + installed CLI versions, rendered from the bundled
1778
+ * `apps/pugi-cli/CHANGELOG.md`. Bumps `~/.pugi/.last-seen-version`
1779
+ * to the installed version on every successful render so the next
1780
+ * invocation is a no-op until the operator upgrades again.
1781
+ *
1782
+ * The handler stays thin: parser, slicer, and state I/O all live in
1783
+ * `core/release-notes/`. This wrapper just hands ambient state to
1784
+ * `runReleaseNotesCommand` so `--json` keeps producing the same
1785
+ * envelope from both the top-level shell + the in-REPL `/release-notes`
1786
+ * slash dispatcher.
1787
+ *
1788
+ * Always exits 0 — the command is informational, never a gate. Read
1789
+ * failures, missing CHANGELOG, and write failures all degrade to a
1790
+ * structured envelope with a human-readable footer.
1791
+ */
1792
+ async function releaseNotes(_args, flags, _session) {
1793
+ runReleaseNotesCommand({
1794
+ home: defaultReleaseNotesHome(),
1795
+ json: flags.json,
1796
+ reset: flags.reset,
1797
+ writeOutput: (payload, text) => writeOutput(flags, payload, text),
1798
+ });
1799
+ }
1304
1800
  /**
1305
1801
  * Programmatic init scaffolder. Idempotent — every helper call is a
1306
1802
  * `*_IfMissing` write, so re-running over an existing .pugi/ workspace
@@ -3844,7 +4340,7 @@ async function login(args, flags, _session) {
3844
4340
  if (args.includes('--help') || args.includes('-h')) {
3845
4341
  writeOutput(flags, {
3846
4342
  command: 'login',
3847
- usage: 'pugi login [--provider device|token|env] [--token <PAT>] [--token-stdin] [--label <name>] [--api-url <url>]',
4343
+ usage: 'pugi login [--provider device|token|env] [--token <PAT>] [--token-stdin] [--key <value>] [--skip-validate] [--label <name>] [--api-url <url>]',
3848
4344
  }, [
3849
4345
  'Usage: pugi login [options]',
3850
4346
  '',
@@ -3856,19 +4352,27 @@ async function login(args, flags, _session) {
3856
4352
  'Non-interactive options:',
3857
4353
  ' --provider device Run the device-flow login (recommended).',
3858
4354
  ' --provider token Store an API key passed via --token / --token-stdin / PUGI_LOGIN_TOKEN.',
3859
- ' --provider env Promote PUGI_API_KEY from the environment into the store.',
4355
+ ' --provider env Read PUGI_API_KEY (or --key) and verify it via /api/pugi/health.',
3860
4356
  ' --token <PAT> Inline API key (visible in `ps`).',
3861
4357
  ' --token-stdin Read API key from stdin (gh-CLI style).',
4358
+ ' --key <value> Explicit key for --provider env; beats PUGI_API_KEY.',
4359
+ ' --skip-validate Skip the /api/pugi/health probe for --provider env (CI bootstrap).',
3862
4360
  ' --label <name> Short label surfaced in `pugi accounts list`.',
3863
4361
  ' --api-url <url> Override the Anvil endpoint (self-hosted).',
3864
4362
  ' --no-device-flow Refuse the device flow; fail fast in CI without a token.',
3865
4363
  '',
4364
+ 'Environment variables:',
4365
+ ' PUGI_API_KEY Read by --provider env. Pass --key to override.',
4366
+ ' PUGI_LOGIN_TOKEN Read by --provider token in non-interactive shells.',
4367
+ ' PUGI_API_URL Override the Anvil endpoint (same as --api-url).',
4368
+ '',
3866
4369
  'Examples:',
3867
4370
  ' pugi login # interactive picker on a TTY',
3868
4371
  ' pugi login --provider device # explicit browser OAuth',
3869
4372
  ' pugi login --provider token --token sk-xx # paste in a key',
3870
4373
  ' echo $TOKEN | pugi login --provider token --token-stdin',
3871
- ' PUGI_API_KEY=sk-xx pugi login --provider env',
4374
+ ' PUGI_API_KEY=pugi_xxx pugi login --provider env',
4375
+ ' pugi login --provider env --key pugi_xxx # explicit key beats env',
3872
4376
  ].join('\n'));
3873
4377
  return;
3874
4378
  }
@@ -3891,6 +4395,11 @@ async function login(args, flags, _session) {
3891
4395
  const apiUrlOverride = extractApiUrlFlag(args);
3892
4396
  const labelFlag = extractLabelFlag(args);
3893
4397
  const provider = parseProviderFlag(args);
4398
+ // Leak L35 (2026-05-27): `--key` is the explicit-arg path for
4399
+ // `--provider env`; `--skip-validate` bypasses the /api/pugi/health
4400
+ // probe (CI bootstrap before the network is up).
4401
+ const envExplicitKey = extractKeyFlag(args);
4402
+ const envSkipValidate = args.includes('--skip-validate');
3894
4403
  const apiUrl = normalizeApiUrl(apiUrlOverride ?? process.env.PUGI_API_URL ?? DEFAULT_API_URL);
3895
4404
  // Path 1: explicit --provider trumps everything else.
3896
4405
  if (provider) {
@@ -3901,6 +4410,8 @@ async function login(args, flags, _session) {
3901
4410
  explicitToken: tokenFromArgs,
3902
4411
  tokenStdinFlag,
3903
4412
  noDeviceFlow,
4413
+ envExplicitKey,
4414
+ envSkipValidate,
3904
4415
  });
3905
4416
  return;
3906
4417
  }
@@ -3948,6 +4459,8 @@ async function login(args, flags, _session) {
3948
4459
  flags,
3949
4460
  label: labelFlag,
3950
4461
  noDeviceFlow,
4462
+ envExplicitKey,
4463
+ envSkipValidate,
3951
4464
  });
3952
4465
  return;
3953
4466
  }
@@ -4166,16 +4679,28 @@ async function dispatchLoginProvider(provider, ctx) {
4166
4679
  return;
4167
4680
  }
4168
4681
  case 'env': {
4169
- const envKey = process.env.PUGI_API_KEY;
4170
- if (!envKey) {
4171
- throw new Error('pugi login --provider env requires PUGI_API_KEY to be exported in the current shell.');
4682
+ // Leak L35 (2026-05-27): resolve the env / --key candidate,
4683
+ // run the local format check, then probe `/api/pugi/health`
4684
+ // BEFORE persisting. A bad token never lands on disk so the
4685
+ // next `pugi <anything>` does not silently 401 against the
4686
+ // cabinet. `--skip-validate` opts out for CI bootstrap.
4687
+ const resolved = await resolveAndValidateEnvLogin({
4688
+ apiUrl: ctx.apiUrl,
4689
+ explicitKey: ctx.envExplicitKey,
4690
+ env: process.env,
4691
+ skipValidate: ctx.envSkipValidate ?? false,
4692
+ });
4693
+ if (resolved.kind !== 'ok') {
4694
+ reportEnvLoginFailure(resolved, ctx.flags);
4695
+ return;
4172
4696
  }
4173
4697
  storeAndAnnounceToken({
4174
4698
  apiUrl: ctx.apiUrl,
4175
- apiKey: envKey,
4699
+ apiKey: resolved.token,
4176
4700
  label: ctx.label,
4177
4701
  source: 'env',
4178
4702
  flags: ctx.flags,
4703
+ validatedLatencyMs: resolved.latencyMs > 0 ? resolved.latencyMs : undefined,
4179
4704
  });
4180
4705
  return;
4181
4706
  }
@@ -4194,6 +4719,15 @@ function storeAndAnnounceToken(input) {
4194
4719
  label: input.label,
4195
4720
  source: input.source,
4196
4721
  });
4722
+ const textLines = [
4723
+ `Pugi logged in for ${record.apiUrl}`,
4724
+ `Method: ${input.source}${record.label ? ` (${record.label})` : ''}`,
4725
+ `Token: ${maskApiKey(record.apiKey)}`,
4726
+ ];
4727
+ if (typeof input.validatedLatencyMs === 'number') {
4728
+ textLines.push(`Verified via /api/pugi/health in ${input.validatedLatencyMs}ms`);
4729
+ }
4730
+ textLines.push('Stored at ~/.pugi/credentials.json (mode 0600). Run `pugi whoami` to verify.');
4197
4731
  writeOutput(input.flags, {
4198
4732
  status: 'logged_in',
4199
4733
  apiUrl: record.apiUrl,
@@ -4201,12 +4735,55 @@ function storeAndAnnounceToken(input) {
4201
4735
  label: record.label ?? null,
4202
4736
  createdAt: record.createdAt,
4203
4737
  source: input.source,
4204
- }, [
4205
- `Pugi logged in for ${record.apiUrl}`,
4206
- `Method: ${input.source}${record.label ? ` (${record.label})` : ''}`,
4207
- `Token: ${maskApiKey(record.apiKey)}`,
4208
- 'Stored at ~/.pugi/credentials.json (mode 0600). Run `pugi whoami` to verify.',
4209
- ].join('\n'));
4738
+ ...(typeof input.validatedLatencyMs === 'number'
4739
+ ? { validatedLatencyMs: input.validatedLatencyMs }
4740
+ : {}),
4741
+ }, textLines.join('\n'));
4742
+ }
4743
+ /**
4744
+ * Render a typed `EnvLoginFailure` from `resolveAndValidateEnvLogin`
4745
+ * onto the surrounding CLI surface. Maps the failure kind to:
4746
+ * - an exit code (1 by default; 2 for invalid format so a CI step
4747
+ * can disambiguate "missing key" vs "key shape wrong" without
4748
+ * parsing stderr; 4 for network / server errors so retry logic
4749
+ * can distinguish transient failures from credential failures)
4750
+ * - a structured JSON payload for `--json` consumers
4751
+ * - a human-readable stderr line for the interactive path
4752
+ *
4753
+ * The token itself is never echoed — only the validator's own message
4754
+ * (which the env-provider module composed without the secret in it).
4755
+ */
4756
+ function reportEnvLoginFailure(failure, flags) {
4757
+ const exitCode = (() => {
4758
+ switch (failure.kind) {
4759
+ case 'missing':
4760
+ return 1;
4761
+ case 'invalid-format':
4762
+ return 2;
4763
+ case 'unauthorized':
4764
+ return 3;
4765
+ case 'network-error':
4766
+ case 'server-error':
4767
+ return 4;
4768
+ case 'unexpected-status':
4769
+ return 5;
4770
+ default: {
4771
+ const exhaustive = failure;
4772
+ return Number(exhaustive) || 1;
4773
+ }
4774
+ }
4775
+ })();
4776
+ const payload = {
4777
+ status: 'login_failed',
4778
+ kind: failure.kind,
4779
+ message: failure.message,
4780
+ };
4781
+ if ('status' in failure)
4782
+ payload.httpStatus = failure.status;
4783
+ if ('cause' in failure && failure.cause)
4784
+ payload.cause = failure.cause;
4785
+ writeOutput(flags, payload, failure.message);
4786
+ process.exitCode = exitCode;
4210
4787
  }
4211
4788
  /**
4212
4789
  * OAuth 2.0 Device Authorization Grant client (RFC 8628). Renders
@@ -4962,6 +5539,17 @@ function extractApiUrlFlag(args) {
4962
5539
  function extractLabelFlag(args) {
4963
5540
  return extractNamedFlagValue(args, 'label');
4964
5541
  }
5542
+ /**
5543
+ * `pugi login --provider env --key <value>` — explicit key arg that
5544
+ * beats `PUGI_API_KEY` env. Same precedence rule as `gh auth login
5545
+ * --with-token`, `aws configure set`, and `pugi config`: the most
5546
+ * specific operator intent (a typed flag) overrides the ambient
5547
+ * environment so an operator can override a stale `PUGI_API_KEY`
5548
+ * from their shell rc without unsetting it first.
5549
+ */
5550
+ function extractKeyFlag(args) {
5551
+ return extractNamedFlagValue(args, 'key');
5552
+ }
4965
5553
  /**
4966
5554
  * `pugi jobs` — surface the persistent JobRegistry on the CLI.
4967
5555
  * Sprint α5.9 (ADR-0056 PR-PUGI-CLI-M1-GAP-J). Subcommand parsing