@pugi/cli 0.1.0-beta.5 → 0.1.0-beta.51

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 (264) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/assets/pugi-mascot.ansi +15 -25
  3. package/assets/pugi-prozr2-mascot.ansi +9 -0
  4. package/bin/run.js +33 -1
  5. package/dist/commands/jobs-watch.js +201 -0
  6. package/dist/commands/jobs.js +15 -0
  7. package/dist/commands/smoke.js +133 -0
  8. package/dist/core/agent-progress/cleanup.js +134 -0
  9. package/dist/core/agent-progress/schema.js +144 -0
  10. package/dist/core/agent-progress/writer.js +101 -0
  11. package/dist/core/artifact-chain/dispatcher.js +148 -0
  12. package/dist/core/artifact-chain/exporter.js +164 -0
  13. package/dist/core/artifact-chain/state.js +243 -0
  14. package/dist/core/artifact-chain/steps.js +169 -0
  15. package/dist/core/auth/ensure-authenticated.js +129 -0
  16. package/dist/core/auth/env-provider.js +238 -0
  17. package/dist/core/auto-update/channels.js +122 -0
  18. package/dist/core/auto-update/checker.js +241 -0
  19. package/dist/core/auto-update/state.js +235 -0
  20. package/dist/core/bare-mode/index.js +107 -0
  21. package/dist/core/bash-classifier.js +400 -4
  22. package/dist/core/checkpoint/resumer.js +149 -0
  23. package/dist/core/checkpoint/rewinder.js +291 -0
  24. package/dist/core/codegraph/decision-store.js +248 -0
  25. package/dist/core/codegraph/detect-repo.js +459 -0
  26. package/dist/core/codegraph/install.js +134 -0
  27. package/dist/core/codegraph/offer-hook.js +220 -0
  28. package/dist/core/compact/auto-trigger.js +96 -0
  29. package/dist/core/compact/buffer-rewriter.js +115 -0
  30. package/dist/core/compact/summarizer.js +208 -0
  31. package/dist/core/compact/token-counter.js +108 -0
  32. package/dist/core/consensus/diff-capture.js +112 -3
  33. package/dist/core/context/index.js +7 -0
  34. package/dist/core/context/markdown-traverse.js +255 -0
  35. package/dist/core/cost/rate-card.js +129 -0
  36. package/dist/core/cost/tracker.js +221 -0
  37. package/dist/core/denial-tracking/index.js +8 -0
  38. package/dist/core/denial-tracking/state.js +264 -0
  39. package/dist/core/diagnostics/probe-runner.js +93 -0
  40. package/dist/core/diagnostics/probes/api.js +46 -0
  41. package/dist/core/diagnostics/probes/auth.js +86 -0
  42. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  43. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  44. package/dist/core/diagnostics/probes/config.js +72 -0
  45. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  46. package/dist/core/diagnostics/probes/disk.js +81 -0
  47. package/dist/core/diagnostics/probes/git.js +65 -0
  48. package/dist/core/diagnostics/probes/hooks.js +118 -0
  49. package/dist/core/diagnostics/probes/mcp.js +75 -0
  50. package/dist/core/diagnostics/probes/node.js +59 -0
  51. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  52. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  53. package/dist/core/diagnostics/probes/sandbox.js +40 -0
  54. package/dist/core/diagnostics/probes/session.js +74 -0
  55. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  56. package/dist/core/diagnostics/probes/workspace.js +63 -0
  57. package/dist/core/diagnostics/types.js +70 -0
  58. package/dist/core/dispatch/cache-cleanup.js +197 -0
  59. package/dist/core/dispatch/cache-handoff.js +295 -0
  60. package/dist/core/edits/dispatch.js +218 -2
  61. package/dist/core/edits/journal.js +199 -0
  62. package/dist/core/edits/layer-d-ast.js +557 -14
  63. package/dist/core/edits/verify-hook.js +273 -0
  64. package/dist/core/edits/worktree.js +322 -0
  65. package/dist/core/engine/anvil-client.js +115 -5
  66. package/dist/core/engine/auto-compact.js +179 -0
  67. package/dist/core/engine/budgets.js +155 -0
  68. package/dist/core/engine/context-prefix.js +155 -0
  69. package/dist/core/engine/intent.js +260 -0
  70. package/dist/core/engine/native-pugi.js +897 -211
  71. package/dist/core/engine/prompts.js +88 -2
  72. package/dist/core/engine/strip-internal-fields.js +124 -0
  73. package/dist/core/engine/tool-bridge.js +1045 -36
  74. package/dist/core/feedback/queue.js +177 -0
  75. package/dist/core/feedback/submitter.js +145 -0
  76. package/dist/core/file-cache.js +113 -1
  77. package/dist/core/hooks/events.js +44 -0
  78. package/dist/core/hooks/index.js +15 -0
  79. package/dist/core/hooks/registry.js +213 -0
  80. package/dist/core/hooks/runner.js +236 -0
  81. package/dist/core/hooks/v2/event-emitter.js +115 -0
  82. package/dist/core/hooks/v2/executor.js +282 -0
  83. package/dist/core/hooks/v2/index.js +25 -0
  84. package/dist/core/hooks/v2/lifecycle.js +104 -0
  85. package/dist/core/hooks/v2/loader.js +216 -0
  86. package/dist/core/hooks/v2/matcher.js +125 -0
  87. package/dist/core/hooks/v2/trust.js +143 -0
  88. package/dist/core/hooks/v2/types.js +86 -0
  89. package/dist/core/lsp/cache.js +105 -0
  90. package/dist/core/lsp/client.js +776 -0
  91. package/dist/core/lsp/language-detect.js +66 -0
  92. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  93. package/dist/core/mcp/client.js +75 -6
  94. package/dist/core/mcp/http-server.js +553 -0
  95. package/dist/core/mcp/orchestrator-tools.js +662 -0
  96. package/dist/core/mcp/permission.js +190 -0
  97. package/dist/core/mcp/registry.js +24 -2
  98. package/dist/core/mcp/server-tools.js +219 -0
  99. package/dist/core/mcp/server.js +397 -0
  100. package/dist/core/memory/dual-write.js +416 -0
  101. package/dist/core/memory/phase1-kinds.js +20 -0
  102. package/dist/core/memory-sync/queue.js +158 -0
  103. package/dist/core/onboarding/ensure-initialized.js +133 -0
  104. package/dist/core/onboarding/marker.js +111 -0
  105. package/dist/core/onboarding/telemetry-state.js +108 -0
  106. package/dist/core/output-style/presets.js +176 -0
  107. package/dist/core/output-style/state.js +185 -0
  108. package/dist/core/path-security.js +284 -2
  109. package/dist/core/permissions/auto-classifier.js +124 -0
  110. package/dist/core/permissions/circuit-breaker.js +83 -0
  111. package/dist/core/permissions/gate.js +278 -0
  112. package/dist/core/permissions/index.js +20 -0
  113. package/dist/core/permissions/mode.js +174 -0
  114. package/dist/core/permissions/state.js +241 -0
  115. package/dist/core/permissions/tool-class.js +93 -0
  116. package/dist/core/prd-check/parser.js +215 -0
  117. package/dist/core/prd-check/reporter.js +127 -0
  118. package/dist/core/prd-check/session-review.js +557 -0
  119. package/dist/core/prd-check/verifiers.js +223 -0
  120. package/dist/core/pugi-md/context-injector.js +76 -0
  121. package/dist/core/pugi-md/walk-up.js +207 -0
  122. package/dist/core/release-notes/parser.js +241 -0
  123. package/dist/core/release-notes/state.js +116 -0
  124. package/dist/core/repl/history.js +11 -1
  125. package/dist/core/repl/model-pricing.js +135 -0
  126. package/dist/core/repl/session.js +1897 -37
  127. package/dist/core/repl/slash-commands.js +430 -15
  128. package/dist/core/repl/store/session-store.js +31 -2
  129. package/dist/core/repl/workspace-context.js +22 -0
  130. package/dist/core/repo-map/build.js +125 -0
  131. package/dist/core/repo-map/cache.js +185 -0
  132. package/dist/core/repo-map/extractor.js +254 -0
  133. package/dist/core/repo-map/formatter.js +145 -0
  134. package/dist/core/repo-map/scanner.js +211 -0
  135. package/dist/core/retry-budget/budget.js +284 -0
  136. package/dist/core/retry-budget/index.js +5 -0
  137. package/dist/core/session.js +92 -0
  138. package/dist/core/settings.js +80 -0
  139. package/dist/core/share/formatter.js +271 -0
  140. package/dist/core/share/redactor.js +221 -0
  141. package/dist/core/share/uploader.js +267 -0
  142. package/dist/core/skills/defaults.js +457 -0
  143. package/dist/core/smoke/headless-driver.js +174 -0
  144. package/dist/core/smoke/orchestrator.js +194 -0
  145. package/dist/core/smoke/runner.js +238 -0
  146. package/dist/core/smoke/scenario-parser.js +316 -0
  147. package/dist/core/subagents/dispatcher-real.js +600 -0
  148. package/dist/core/subagents/dispatcher.js +113 -24
  149. package/dist/core/subagents/index.js +18 -5
  150. package/dist/core/subagents/isolation-matrix.js +213 -0
  151. package/dist/core/subagents/spawn.js +19 -4
  152. package/dist/core/telemetry/emitter.js +229 -0
  153. package/dist/core/telemetry/queue.js +251 -0
  154. package/dist/core/theme/context.js +91 -0
  155. package/dist/core/theme/presets.js +228 -0
  156. package/dist/core/theme/state.js +181 -0
  157. package/dist/core/todos/invariant.js +10 -0
  158. package/dist/core/todos/state.js +177 -0
  159. package/dist/core/transport/version-interceptor.js +166 -0
  160. package/dist/core/vim/keymap.js +288 -0
  161. package/dist/core/vim/state.js +92 -0
  162. package/dist/core/worktree-manager/cleanup.js +123 -0
  163. package/dist/core/worktree-manager/manager.js +303 -0
  164. package/dist/index.js +28 -0
  165. package/dist/runtime/bootstrap.js +190 -0
  166. package/dist/runtime/cli.js +3241 -343
  167. package/dist/runtime/commands/cancel.js +231 -0
  168. package/dist/runtime/commands/chain.js +489 -0
  169. package/dist/runtime/commands/codegraph-status.js +227 -0
  170. package/dist/runtime/commands/compact.js +297 -0
  171. package/dist/runtime/commands/cost.js +199 -0
  172. package/dist/runtime/commands/delegate.js +242 -11
  173. package/dist/runtime/commands/dispatch.js +126 -0
  174. package/dist/runtime/commands/doctor.js +412 -0
  175. package/dist/runtime/commands/feedback.js +184 -0
  176. package/dist/runtime/commands/hooks.js +184 -0
  177. package/dist/runtime/commands/lsp.js +368 -0
  178. package/dist/runtime/commands/mcp.js +879 -0
  179. package/dist/runtime/commands/memory.js +508 -0
  180. package/dist/runtime/commands/model.js +237 -0
  181. package/dist/runtime/commands/onboarding.js +275 -0
  182. package/dist/runtime/commands/patch.js +128 -0
  183. package/dist/runtime/commands/permissions.js +112 -0
  184. package/dist/runtime/commands/plan.js +143 -0
  185. package/dist/runtime/commands/prd-check.js +285 -0
  186. package/dist/runtime/commands/redo-blob-store.js +92 -0
  187. package/dist/runtime/commands/redo.js +361 -0
  188. package/dist/runtime/commands/release-notes.js +229 -0
  189. package/dist/runtime/commands/repo-map.js +95 -0
  190. package/dist/runtime/commands/report.js +299 -0
  191. package/dist/runtime/commands/resume.js +118 -0
  192. package/dist/runtime/commands/review-consensus.js +17 -2
  193. package/dist/runtime/commands/rewind.js +333 -0
  194. package/dist/runtime/commands/sessions.js +163 -0
  195. package/dist/runtime/commands/share.js +316 -0
  196. package/dist/runtime/commands/status.js +186 -0
  197. package/dist/runtime/commands/stickers.js +82 -0
  198. package/dist/runtime/commands/style.js +194 -0
  199. package/dist/runtime/commands/theme.js +196 -0
  200. package/dist/runtime/commands/undo.js +32 -0
  201. package/dist/runtime/commands/update.js +289 -0
  202. package/dist/runtime/commands/vim.js +140 -0
  203. package/dist/runtime/commands/worktree.js +177 -0
  204. package/dist/runtime/commands/worktrees.js +155 -0
  205. package/dist/runtime/headless-repl.js +195 -0
  206. package/dist/runtime/headless.js +543 -0
  207. package/dist/runtime/load-hooks-or-exit.js +71 -0
  208. package/dist/runtime/plan-decompose.js +531 -0
  209. package/dist/runtime/version.js +65 -0
  210. package/dist/tools/agent-tool.js +229 -0
  211. package/dist/tools/apply-patch.js +556 -0
  212. package/dist/tools/ask-user-question.js +213 -0
  213. package/dist/tools/ask-user.js +115 -0
  214. package/dist/tools/bash.js +203 -4
  215. package/dist/tools/file-tools.js +85 -14
  216. package/dist/tools/lsp-tools.js +189 -0
  217. package/dist/tools/mcp-tool.js +260 -0
  218. package/dist/tools/multi-edit.js +361 -0
  219. package/dist/tools/powershell.js +268 -0
  220. package/dist/tools/registry.js +51 -0
  221. package/dist/tools/skill-tool.js +96 -0
  222. package/dist/tools/tasks.js +208 -0
  223. package/dist/tools/todo-write.js +184 -0
  224. package/dist/tools/web-fetch.js +147 -2
  225. package/dist/tools/web-search.js +458 -0
  226. package/dist/tui/agent-progress-card.js +111 -0
  227. package/dist/tui/agent-tree.js +10 -0
  228. package/dist/tui/ask-modal.js +2 -2
  229. package/dist/tui/ask-user-question-prompt.js +192 -0
  230. package/dist/tui/compact-banner.js +81 -0
  231. package/dist/tui/conversation-pane.js +82 -8
  232. package/dist/tui/cost-table.js +111 -0
  233. package/dist/tui/doctor-table.js +46 -0
  234. package/dist/tui/feedback-prompt.js +156 -0
  235. package/dist/tui/input-box.js +218 -3
  236. package/dist/tui/markdown-render.js +4 -4
  237. package/dist/tui/onboarding-wizard.js +240 -0
  238. package/dist/tui/permissions-picker.js +86 -0
  239. package/dist/tui/render.js +35 -0
  240. package/dist/tui/repl-render.js +313 -35
  241. package/dist/tui/repl-splash-art.js +1 -1
  242. package/dist/tui/repl-splash-mascot.js +32 -8
  243. package/dist/tui/repl-splash.js +2 -2
  244. package/dist/tui/repl.js +85 -5
  245. package/dist/tui/splash.js +1 -1
  246. package/dist/tui/status-bar.js +94 -16
  247. package/dist/tui/status-table.js +7 -0
  248. package/dist/tui/stickers-art.js +136 -0
  249. package/dist/tui/style-table.js +28 -0
  250. package/dist/tui/theme-table.js +29 -0
  251. package/dist/tui/thinking-spinner.js +123 -0
  252. package/dist/tui/tool-stream-pane.js +52 -3
  253. package/dist/tui/update-banner.js +27 -2
  254. package/dist/tui/vim-input.js +267 -0
  255. package/dist/tui/welcome-banner.js +107 -0
  256. package/dist/tui/welcome-data.js +293 -0
  257. package/docs/examples/codegraph.mcp.json +10 -0
  258. package/package.json +13 -7
  259. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  260. package/test/scenarios/compact-force.scenario.txt +11 -0
  261. package/test/scenarios/identity.scenario.txt +11 -0
  262. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  263. package/test/scenarios/walkback.scenario.txt +12 -0
  264. package/dist/core/engine/compaction-hook.js +0 -154
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Wave 6 UX (2026-05-27) — `ensureInitialized` helper.
3
+ *
4
+ * Auto-init pre-flight for every Pugi command. Before this helper landed,
5
+ * the only entry points that exercised the init flow were:
6
+ *
7
+ * 1. The explicit `pugi init` CLI subcommand.
8
+ * 2. The REPL's `/init` slash (β1a r1).
9
+ * 3. Engine commands (`pugi code`, `pugi build`, `pugi sync`) which
10
+ * called the legacy `ensureInitialized` in `cli.ts` and threw
11
+ * `Error('Run pugi init first')` if the operator ran them in a
12
+ * directory without `.pugi/`.
13
+ *
14
+ * Read-only commands (`pugi explain`, `pugi review`, `pugi plan`,
15
+ * `pugi smoke`, `pugi chain new`, ...) silently no-op'd the `.pugi/`
16
+ * mirror inside the engine adapter, which made early dogfooding
17
+ * confusing — the operator saw a successful command but no session
18
+ * artifacts on disk and no idea why.
19
+ *
20
+ * Auto-init contract (matches CEO directive Wave 6, 2026-05-27):
21
+ *
22
+ * - `.pugi/` already exists → return `{ status: 'already' }` silently.
23
+ * - Interactive TTY + no `.pugi/` → prompt
24
+ * "No Pugi workspace found here. Initialize? (Y/n)".
25
+ * Default Y. On Y: run `scaffoldPugiWorkspace`, return `{ status:
26
+ * 'initialized' }`. On n: return `{ status: 'declined' }` so the
27
+ * caller can bail with a helpful message.
28
+ * - Non-interactive (CI / pipe / --json / --no-tty) + no `.pugi/`:
29
+ * default behaviour is conservative — return `{ status: 'declined',
30
+ * reason: 'non_interactive' }`. The caller decides how to surface
31
+ * this (engine commands bail with a clean error; read-only
32
+ * commands MAY continue with degraded semantics).
33
+ * - `--no-init` flag forces conservative posture even on interactive
34
+ * terminals (operator wants to fail fast).
35
+ *
36
+ * Session cache: a command pre-flight that already prompted for and
37
+ * scaffolded `.pugi/` MUST NOT re-prompt for the same workspace in the
38
+ * same process. The cache key is the absolute workspace root path. The
39
+ * cache is process-local (Map) — it does not persist across `pugi`
40
+ * invocations (a second `pugi code` in the same shell starts fresh and
41
+ * re-checks the filesystem).
42
+ *
43
+ * This module is intentionally framework-free: no Ink, no React, no
44
+ * readline. The prompt reader is injected via the `prompt` callback so
45
+ * the spec can drive the helper deterministically and the CLI can
46
+ * forward to its existing stdin-reader (`readSingleChoice` in cli.ts).
47
+ */
48
+ import { existsSync, statSync } from 'node:fs';
49
+ import { resolve } from 'node:path';
50
+ /**
51
+ * Process-local cache of workspaces that already passed the pre-flight
52
+ * gate. Keyed by absolute root path. The cache is intentionally
53
+ * additive-only — there is no eviction. A long-running REPL session
54
+ * stays in one workspace and we never want to re-prompt within it.
55
+ */
56
+ const initialisedCache = new Set();
57
+ /**
58
+ * Reset the cache. Exported for spec teardown — production callers
59
+ * never need this.
60
+ */
61
+ export function resetInitializedCache() {
62
+ initialisedCache.clear();
63
+ }
64
+ /**
65
+ * Detect `.pugi/` at `root`. Pure filesystem read; swallows permission
66
+ * errors (returns false). Exported so the spec can assert the same
67
+ * detection the helper uses without re-implementing the check.
68
+ */
69
+ export function hasPugiWorkspace(root) {
70
+ const path = resolve(root, '.pugi');
71
+ try {
72
+ if (!existsSync(path))
73
+ return false;
74
+ return statSync(path).isDirectory();
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ /**
81
+ * Auto-init pre-flight. Idempotent and process-cache aware — calling
82
+ * twice in the same process for the same workspace returns `already`
83
+ * the second time even if the filesystem state changed underneath.
84
+ *
85
+ * Implementation notes:
86
+ *
87
+ * - Returns `{ status: 'already' }` when `.pugi/` exists OR the cache
88
+ * remembers this workspace. The cache short-circuit means a second
89
+ * command in the same process never blocks on the prompt.
90
+ * - Interactive + missing → prompt. The default answer (empty input
91
+ * OR a leading `y` / `yes`) maps to scaffold. Anything else
92
+ * (`n`, `no`, `cancel`, whitespace + non-y) maps to declined.
93
+ * - Scaffolder failures propagate to the caller; the helper does
94
+ * NOT swallow them because a failed scaffold means the operator's
95
+ * command cannot continue anyway. Tests assert this.
96
+ */
97
+ export async function ensureInitialized(opts) {
98
+ const root = resolve(opts.cwd ?? process.cwd());
99
+ if (initialisedCache.has(root)) {
100
+ return { status: 'already', root };
101
+ }
102
+ if (hasPugiWorkspace(root)) {
103
+ initialisedCache.add(root);
104
+ return { status: 'already', root };
105
+ }
106
+ if (opts.skip) {
107
+ return { status: 'declined', root, reason: 'disabled' };
108
+ }
109
+ if (!opts.interactive) {
110
+ return { status: 'declined', root, reason: 'non_interactive' };
111
+ }
112
+ if (!opts.prompt) {
113
+ // Defensive — an interactive caller forgot к wire the prompt
114
+ // reader. Treat the same as non-interactive rather than throwing
115
+ // so the surrounding command can degrade gracefully.
116
+ return { status: 'declined', root, reason: 'non_interactive' };
117
+ }
118
+ const write = opts.write ?? ((line) => process.stderr.write(line));
119
+ write(`No Pugi workspace found at ${root}.\n`);
120
+ const answer = (await opts.prompt('Initialize a new Pugi workspace here? (Y/n) ')).trim().toLowerCase();
121
+ // Default = yes (empty input OR leading 'y'). Anything else = no.
122
+ // Mirrors the gh CLI / claude code prompt convention where the upper-
123
+ // case option in `(Y/n)` is the default-on-Enter answer.
124
+ const acceptedShort = answer === '' || answer === 'y' || answer === 'yes';
125
+ if (!acceptedShort) {
126
+ write('Initialization declined.\n');
127
+ return { status: 'declined', root, reason: 'user_declined' };
128
+ }
129
+ await opts.scaffold({ cwd: root });
130
+ initialisedCache.add(root);
131
+ return { status: 'initialized', root };
132
+ }
133
+ //# sourceMappingURL=ensure-initialized.js.map
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Leak L25 (2026-05-27) — Onboarding marker file.
3
+ *
4
+ * `~/.pugi/.onboarded` is a single, contentless marker. Its existence
5
+ * tells the bare-invocation hint check that the operator has already
6
+ * walked the `/onboarding` wizard at least once, so we no longer print
7
+ * the "Tip: run `pugi onboarding` to configure defaults" line on
8
+ * cold-start. The wizard re-runs cleanly — idempotency lives in the
9
+ * wizard itself, not in the marker.
10
+ *
11
+ * Why a marker file (and not just `~/.pugi/config.json`'s existence)?
12
+ *
13
+ * - The config file is touched the moment ANY surface writes a
14
+ * default — `pugi style terse --persist`, `pugi permissions ask`,
15
+ * `pugi config set …`. Using "config exists" as the proxy for
16
+ * "operator has onboarded" would silence the first-run hint for
17
+ * operators who never saw the wizard.
18
+ *
19
+ * - The marker is explicit: it is written ONLY by the wizard's exit
20
+ * step (or `pugi onboarding --mark-only` for the upgrade-path
21
+ * where we want to suppress the hint without forcing a re-walk).
22
+ *
23
+ * - Removing the marker (`rm ~/.pugi/.onboarded`) re-arms the hint
24
+ * without nuking the operator's accumulated config — useful for
25
+ * QA, support flows, and demo-machine resets.
26
+ *
27
+ * Path resolution mirrors the L6/L18 convention: `PUGI_HOME` env wins,
28
+ * else `~/.pugi`. The marker is touched as an empty file (no JSON, no
29
+ * timestamp payload) — readers MUST treat existence as the only signal
30
+ * so a future change to mtime semantics does not break us.
31
+ *
32
+ * IO contract:
33
+ * - `isOnboarded(env)` — pure read; never throws, returns false on
34
+ * any fs error so a corrupted home dir cannot hide the hint.
35
+ * - `markOnboarded(env)` — best-effort write; creates `<home>/.pugi/`
36
+ * if missing, mode 0o600 on the marker so it never lands in a
37
+ * world-readable backup.
38
+ * - `clearOnboarded(env)` — best-effort delete; absent file is a
39
+ * no-op (not an error). Used by `pugi onboarding --reset` and the
40
+ * spec teardown.
41
+ */
42
+ import { existsSync, mkdirSync, rmSync, writeFileSync, } from 'node:fs';
43
+ import { homedir } from 'node:os';
44
+ import { resolve } from 'node:path';
45
+ /**
46
+ * Env override for `~/.pugi`. Same convention as L6 / L18 — spec
47
+ * fixtures point this at a temp dir so a real developer machine never
48
+ * lands a stray marker.
49
+ */
50
+ export const PUGI_HOME_ENV = 'PUGI_HOME';
51
+ /**
52
+ * Marker basename. Hidden (leading dot) so it does not clutter `ls`
53
+ * inside `~/.pugi/` next to `config.json` / `session.json`.
54
+ */
55
+ const MARKER_BASENAME = '.onboarded';
56
+ /**
57
+ * Resolve the absolute path to the onboarding marker. Exported for the
58
+ * spec; production callers go through `isOnboarded` / `markOnboarded`.
59
+ */
60
+ export function onboardingMarkerPath(env = process.env) {
61
+ const home = env[PUGI_HOME_ENV] ?? resolve(homedir(), '.pugi');
62
+ return resolve(home, MARKER_BASENAME);
63
+ }
64
+ /**
65
+ * True when the marker exists. Pure read. Defensive: any fs error
66
+ * (race with deletion, permission flip) degrades to `false` — printing
67
+ * the hint twice is harmless, silently swallowing the wizard would
68
+ * surprise the operator.
69
+ */
70
+ export function isOnboarded(env = process.env) {
71
+ try {
72
+ return existsSync(onboardingMarkerPath(env));
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ }
78
+ /**
79
+ * Touch the marker. Creates `<home>/.pugi/` if missing. Idempotent —
80
+ * re-touching an existing marker is a no-op for the consumer (the file
81
+ * was already there; the hint was already suppressed).
82
+ *
83
+ * Best-effort: a write failure is swallowed because the wizard already
84
+ * completed its real work (mode + style + telemetry were persisted).
85
+ * The worst case is a redundant hint on the next `pugi` invocation —
86
+ * preferable to crashing the freshly-completed wizard with a stat EIO.
87
+ */
88
+ export function markOnboarded(env = process.env) {
89
+ const path = onboardingMarkerPath(env);
90
+ try {
91
+ mkdirSync(resolve(path, '..'), { recursive: true });
92
+ writeFileSync(path, '', { encoding: 'utf8', mode: 0o600 });
93
+ }
94
+ catch {
95
+ // intentionally swallowed — see header
96
+ }
97
+ }
98
+ /**
99
+ * Remove the marker. Used by `pugi onboarding --reset` (and the spec
100
+ * teardown). Absent file is a no-op; any other fs error is swallowed
101
+ * so a permission glitch never leaks out of the reset surface.
102
+ */
103
+ export function clearOnboarded(env = process.env) {
104
+ try {
105
+ rmSync(onboardingMarkerPath(env), { force: true });
106
+ }
107
+ catch {
108
+ // intentionally swallowed — see header
109
+ }
110
+ }
111
+ //# sourceMappingURL=marker.js.map
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Leak L25 (2026-05-27) — Telemetry consent state.
3
+ *
4
+ * The onboarding wizard's Step 5 asks the operator for telemetry
5
+ * consent. We persist the verdict in the user-tier
6
+ * `~/.pugi/config.json::telemetry` field so a future REPL boot can
7
+ * read it without re-asking. Choices mirror `core/settings.ts`'s
8
+ * `privacy.telemetry` enum:
9
+ *
10
+ * - `off` — no telemetry of any kind (default).
11
+ * - `anonymous` — counts + error categories only, no payloads.
12
+ * - `community` — anonymous + opt-in skill/usage panels.
13
+ *
14
+ * This module is intentionally narrow: it only owns the `telemetry`
15
+ * key inside `~/.pugi/config.json`. The full settings parsing lives in
16
+ * `core/settings.ts` (workspace-tier `.pugi/settings.json`); we do NOT
17
+ * route through it here because:
18
+ *
19
+ * 1. The settings schema is workspace-scoped — its file path is
20
+ * `<root>/.pugi/settings.json`, not `~/.pugi/config.json`.
21
+ * 2. The wizard records a user-level default that workspace settings
22
+ * can later override. Mixing the two would conflate scope.
23
+ * 3. Read-modify-write on a partial JSON file is the same pattern
24
+ * L6 / L18 use for adjacent keys — keeping it self-contained
25
+ * preserves the "one module, one key" invariant.
26
+ */
27
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
28
+ import { homedir } from 'node:os';
29
+ import { dirname, resolve } from 'node:path';
30
+ import { PUGI_HOME_ENV } from './marker.js';
31
+ export const TELEMETRY_CHOICES = Object.freeze([
32
+ 'off',
33
+ 'anonymous',
34
+ 'community',
35
+ ]);
36
+ export const DEFAULT_TELEMETRY = 'off';
37
+ /**
38
+ * Path to the user-tier config. Mirrors `userConfigPath()` from L18
39
+ * `output-style/state.ts` — duplicated here (not imported) to keep the
40
+ * marker + telemetry module self-contained. Any future drift between
41
+ * the two would surface a spec failure: both modules read the same
42
+ * file in the spec sandbox.
43
+ */
44
+ export function telemetryConfigPath(env = process.env) {
45
+ const home = env[PUGI_HOME_ENV] ?? resolve(homedir(), '.pugi');
46
+ return resolve(home, 'config.json');
47
+ }
48
+ /**
49
+ * Type guard for arbitrary string input (CLI argv, config.json
50
+ * deserialisation). Returns false for any non-string or out-of-set
51
+ * value so a malformed config degrades to the default verdict.
52
+ */
53
+ export function isTelemetryChoice(value) {
54
+ return (typeof value === 'string'
55
+ && TELEMETRY_CHOICES.includes(value));
56
+ }
57
+ /**
58
+ * Read the persisted telemetry verdict. Returns the default (`'off'`)
59
+ * when the file is absent, empty, malformed, or holds an unknown
60
+ * value. Never throws — the wizard re-asks every time it runs, so a
61
+ * defensive read is the right posture.
62
+ */
63
+ export function readTelemetryChoice(io = {}) {
64
+ const config = readConfigFile(telemetryConfigPath(io.env ?? process.env));
65
+ return isTelemetryChoice(config.telemetry) ? config.telemetry : DEFAULT_TELEMETRY;
66
+ }
67
+ /**
68
+ * Persist the telemetry verdict. Read-modify-write preserves any
69
+ * neighbouring keys (`outputStyle`, `defaultPermissionMode`, …) the
70
+ * other tier-state modules own.
71
+ */
72
+ export function writeTelemetryChoice(choice, io = {}) {
73
+ const path = telemetryConfigPath(io.env ?? process.env);
74
+ const config = readConfigFile(path);
75
+ config.telemetry = choice;
76
+ writeConfigFile(path, config);
77
+ }
78
+ function readConfigFile(path) {
79
+ if (!existsSync(path))
80
+ return {};
81
+ let raw;
82
+ try {
83
+ raw = readFileSync(path, 'utf8');
84
+ }
85
+ catch {
86
+ return {};
87
+ }
88
+ if (raw.trim().length === 0)
89
+ return {};
90
+ let parsed;
91
+ try {
92
+ parsed = JSON.parse(raw);
93
+ }
94
+ catch {
95
+ return {};
96
+ }
97
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
98
+ return {};
99
+ return parsed;
100
+ }
101
+ function writeConfigFile(path, config) {
102
+ mkdirSync(dirname(path), { recursive: true });
103
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, {
104
+ encoding: 'utf8',
105
+ mode: 0o600,
106
+ });
107
+ }
108
+ //# sourceMappingURL=telemetry-state.js.map
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Leak L18 (2026-05-27) — Output-style presets.
3
+ *
4
+ * Mirror of Claude Code's `/output-style` surface: a small closed set of
5
+ * named voice presets the operator can flip between at session start so
6
+ * the model's prose lands in the register they prefer. The preset
7
+ * compiles into an `<output-style>` rule block appended to the engine
8
+ * system prompt; tool-use / code-block formatting / file edits are NOT
9
+ * affected — the preset only steers prose register.
10
+ *
11
+ * Design contract:
12
+ *
13
+ * - The catalogue is intentionally tiny (5 entries) so the operator
14
+ * can hold the full surface in working memory. Adding entries means
15
+ * adding a row in `OUTPUT_STYLES` plus a spec assertion; there is
16
+ * no plugin surface today.
17
+ *
18
+ * - `default` is the only preset that emits NO rule block. The
19
+ * "current Pugi voice" already lives in the base engine prompt
20
+ * (jargon ban, brand voice, terse register), so re-stating it
21
+ * under `<output-style>` would double the model's instruction load
22
+ * for the most-common case. Other presets emit the block.
23
+ *
24
+ * - Rule-block prose stays terse and operator-grade (brandbook §08).
25
+ * No friendly hedging, no AI-assistant framing. The bullets are
26
+ * the model's contract; the section title carries the preset name
27
+ * so the model can self-correct mid-turn if it drifts ("I am in
28
+ * terse mode → drop articles").
29
+ *
30
+ * - The Russian-formal preset uses вы-form explicitly. Russian/
31
+ * Ukrainian chat is permitted by the base voice contract; this
32
+ * preset hardens the register for B2B / enterprise demo flows
33
+ * where ты-form reads as too casual.
34
+ *
35
+ * - The Casual preset RELAXES the jargon ban — contractions, jokes,
36
+ * informal phrasing are allowed. It does NOT lift the brand-voice
37
+ * em-dash / emoji ban; those are typographic, not register, and
38
+ * remain off across every preset.
39
+ *
40
+ * Test surface: `test/commands/output-style-presets.spec.ts` exercises
41
+ * the catalogue invariants (5 entries, unique slugs, every non-default
42
+ * preset emits a non-empty block, the block starts with the expected
43
+ * marker so the engine prompt appender can locate it for stripping).
44
+ */
45
+ /**
46
+ * The closed list of preset slugs in catalogue order. Mirror used by
47
+ * the CLI surface (`/style` table, `pugi style --list`) so the
48
+ * operator sees presets in a stable order regardless of catalogue
49
+ * iteration order.
50
+ */
51
+ export const OUTPUT_STYLE_SLUGS = Object.freeze([
52
+ 'default',
53
+ 'terse',
54
+ 'explanatory',
55
+ 'russian-formal',
56
+ 'casual',
57
+ ]);
58
+ /**
59
+ * Default slug used when no workspace-/user-level preference is set.
60
+ * Exported so `state.ts` and the CLI handler share one constant.
61
+ */
62
+ export const DEFAULT_OUTPUT_STYLE = 'default';
63
+ /**
64
+ * Catalogue keyed by slug. Frozen so callers cannot mutate the
65
+ * shared rows; the CLI handler returns slugs, not preset references,
66
+ * to keep the boundary clean.
67
+ */
68
+ export const OUTPUT_STYLES = Object.freeze({
69
+ default: Object.freeze({
70
+ slug: 'default',
71
+ title: 'Default',
72
+ gloss: 'Current Pugi voice (no override). Base engine prompt rules apply unchanged.',
73
+ rules: Object.freeze([]),
74
+ }),
75
+ terse: Object.freeze({
76
+ slug: 'terse',
77
+ title: 'Terse',
78
+ gloss: 'Fragments, dropped articles, one short sentence per turn.',
79
+ rules: Object.freeze([
80
+ 'Drop articles, fillers, hedging',
81
+ '1 short sentence per turn for prose answers',
82
+ 'Code blocks unchanged — never abbreviate code',
83
+ 'Quote errors verbatim with no paraphrase',
84
+ ]),
85
+ }),
86
+ explanatory: Object.freeze({
87
+ slug: 'explanatory',
88
+ title: 'Explanatory',
89
+ gloss: 'Verbose, walks reasoning step by step, links concepts.',
90
+ rules: Object.freeze([
91
+ 'Explain reasoning, not just the conclusion',
92
+ 'Cite relevant files + line numbers when grounding claims',
93
+ 'Link adjacent concepts the operator may want to chase',
94
+ 'Code blocks unchanged — annotate around, not inside',
95
+ ]),
96
+ }),
97
+ 'russian-formal': Object.freeze({
98
+ slug: 'russian-formal',
99
+ title: 'Russian formal',
100
+ gloss: 'Russian вы-form, professional register, no slang.',
101
+ rules: Object.freeze([
102
+ 'Pisat\' otvety po-russki (Russian prose; ASCII transliteration permitted in this rule block only)',
103
+ 'Address the operator using вы-form, never ты',
104
+ 'No slang, no contractions of Russian forms',
105
+ 'Code blocks + identifiers stay in English unchanged',
106
+ 'Error messages quoted verbatim in the original language',
107
+ ]),
108
+ }),
109
+ casual: Object.freeze({
110
+ slug: 'casual',
111
+ title: 'Casual',
112
+ gloss: 'Informal register, contractions OK, dry jokes welcome.',
113
+ rules: Object.freeze([
114
+ 'Contractions allowed (it\'s, don\'t, you\'re)',
115
+ 'Dry, deadpan jokes welcome when they do not displace signal',
116
+ 'No em-dashes, no emoji — typographic rules unchanged',
117
+ 'Stay terse — casual is register, not verbosity',
118
+ ]),
119
+ }),
120
+ });
121
+ /**
122
+ * Type-narrowing predicate. Used by the slash-command parser + state
123
+ * loader so an unknown string from operator input or a stale config
124
+ * file degrades to the default preset instead of crashing.
125
+ */
126
+ export function isOutputStyleSlug(value) {
127
+ return (typeof value === 'string'
128
+ && OUTPUT_STYLE_SLUGS.includes(value));
129
+ }
130
+ /**
131
+ * Compile a preset into the `<output-style>` rule block injected at
132
+ * the tail of the engine system prompt.
133
+ *
134
+ * Returns empty string when the preset is `default` (or any preset
135
+ * with an empty rules array). Empty string is a load-bearing signal —
136
+ * the engine prompt appender uses it to skip injection entirely so
137
+ * the model sees a clean prompt for the default register.
138
+ *
139
+ * The block opens with `<output-style>` and closes with `</output-style>`
140
+ * (XML-shaped marker, matching the engine prompt's existing `<intent>`
141
+ * grammar). The `Active style:` line gives the model a self-correction
142
+ * anchor when it drifts mid-turn.
143
+ */
144
+ export function compileStyleBlock(slug) {
145
+ const preset = OUTPUT_STYLES[slug];
146
+ if (preset.rules.length === 0)
147
+ return '';
148
+ const lines = [];
149
+ lines.push('<output-style>');
150
+ lines.push(` Active style: ${preset.slug}`);
151
+ for (const rule of preset.rules) {
152
+ lines.push(` - ${rule}`);
153
+ }
154
+ lines.push('</output-style>');
155
+ return lines.join('\n');
156
+ }
157
+ /**
158
+ * Render the preset catalogue as a plain-text table for the `/style`
159
+ * + `pugi style` surfaces. Marks the active slug with `*` so the
160
+ * operator can see at a glance which preset is in effect.
161
+ *
162
+ * Pure renderer (no fs, no env). Identical text is emitted from both
163
+ * the slash dispatcher and the top-level CLI command so operators
164
+ * trained on one surface read the same table on the other.
165
+ */
166
+ export function renderStyleTable(active) {
167
+ const slugWidth = Math.max('NAME'.length, ...OUTPUT_STYLE_SLUGS.map((slug) => slug.length));
168
+ const header = `${'NAME'.padEnd(slugWidth)} GLOSS`;
169
+ const rows = OUTPUT_STYLE_SLUGS.map((slug) => {
170
+ const preset = OUTPUT_STYLES[slug];
171
+ const marker = slug === active ? '*' : ' ';
172
+ return `${marker} ${slug.padEnd(slugWidth)} ${preset.gloss}`;
173
+ });
174
+ return [header, ...rows].join('\n');
175
+ }
176
+ //# sourceMappingURL=presets.js.map