@pugi/cli 0.1.0-beta.5 → 0.1.0-beta.50
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.
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/assets/pugi-mascot.ansi +15 -25
- package/assets/pugi-prozr2-mascot.ansi +9 -0
- package/bin/run.js +33 -1
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +15 -0
- package/dist/commands/smoke.js +133 -0
- package/dist/core/agent-progress/cleanup.js +134 -0
- package/dist/core/agent-progress/schema.js +144 -0
- package/dist/core/agent-progress/writer.js +101 -0
- package/dist/core/artifact-chain/dispatcher.js +148 -0
- package/dist/core/artifact-chain/exporter.js +164 -0
- package/dist/core/artifact-chain/state.js +243 -0
- package/dist/core/artifact-chain/steps.js +169 -0
- package/dist/core/auth/ensure-authenticated.js +129 -0
- package/dist/core/auth/env-provider.js +238 -0
- package/dist/core/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -0
- package/dist/core/bare-mode/index.js +107 -0
- package/dist/core/bash-classifier.js +400 -4
- package/dist/core/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -0
- package/dist/core/codegraph/decision-store.js +248 -0
- package/dist/core/codegraph/detect-repo.js +459 -0
- package/dist/core/codegraph/install.js +134 -0
- package/dist/core/codegraph/offer-hook.js +220 -0
- package/dist/core/compact/auto-trigger.js +96 -0
- package/dist/core/compact/buffer-rewriter.js +115 -0
- package/dist/core/compact/summarizer.js +208 -0
- package/dist/core/compact/token-counter.js +108 -0
- package/dist/core/consensus/diff-capture.js +112 -3
- package/dist/core/context/index.js +7 -0
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/cost/rate-card.js +129 -0
- package/dist/core/cost/tracker.js +221 -0
- package/dist/core/denial-tracking/index.js +8 -0
- package/dist/core/denial-tracking/state.js +264 -0
- package/dist/core/diagnostics/probe-runner.js +93 -0
- package/dist/core/diagnostics/probes/api.js +46 -0
- package/dist/core/diagnostics/probes/auth.js +86 -0
- package/dist/core/diagnostics/probes/bare-mode.js +42 -0
- package/dist/core/diagnostics/probes/cli-version.js +127 -0
- package/dist/core/diagnostics/probes/config.js +72 -0
- package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
- package/dist/core/diagnostics/probes/disk.js +81 -0
- package/dist/core/diagnostics/probes/git.js +65 -0
- package/dist/core/diagnostics/probes/hooks.js +118 -0
- package/dist/core/diagnostics/probes/mcp.js +75 -0
- package/dist/core/diagnostics/probes/node.js +59 -0
- package/dist/core/diagnostics/probes/pnpm.js +36 -0
- package/dist/core/diagnostics/probes/pugi-md.js +89 -0
- package/dist/core/diagnostics/probes/sandbox.js +40 -0
- package/dist/core/diagnostics/probes/session.js +74 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
- package/dist/core/diagnostics/probes/workspace.js +63 -0
- package/dist/core/diagnostics/types.js +70 -0
- package/dist/core/dispatch/cache-cleanup.js +197 -0
- package/dist/core/dispatch/cache-handoff.js +295 -0
- package/dist/core/edits/dispatch.js +218 -2
- package/dist/core/edits/journal.js +199 -0
- package/dist/core/edits/layer-d-ast.js +557 -14
- package/dist/core/edits/verify-hook.js +273 -0
- package/dist/core/edits/worktree.js +322 -0
- package/dist/core/engine/anvil-client.js +115 -5
- package/dist/core/engine/budgets.js +98 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +860 -211
- package/dist/core/engine/prompts.js +88 -2
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1045 -36
- package/dist/core/feedback/queue.js +177 -0
- package/dist/core/feedback/submitter.js +145 -0
- package/dist/core/file-cache.js +113 -1
- package/dist/core/hooks/events.js +44 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +213 -0
- package/dist/core/hooks/runner.js +236 -0
- package/dist/core/hooks/v2/event-emitter.js +115 -0
- package/dist/core/hooks/v2/executor.js +282 -0
- package/dist/core/hooks/v2/index.js +25 -0
- package/dist/core/hooks/v2/lifecycle.js +104 -0
- package/dist/core/hooks/v2/loader.js +216 -0
- package/dist/core/hooks/v2/matcher.js +125 -0
- package/dist/core/hooks/v2/trust.js +143 -0
- package/dist/core/hooks/v2/types.js +86 -0
- package/dist/core/lsp/cache.js +105 -0
- package/dist/core/lsp/client.js +776 -0
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/mcp/client.js +75 -6
- package/dist/core/mcp/http-server.js +553 -0
- package/dist/core/mcp/orchestrator-tools.js +662 -0
- package/dist/core/mcp/permission.js +190 -0
- package/dist/core/mcp/registry.js +24 -2
- package/dist/core/mcp/server-tools.js +219 -0
- package/dist/core/mcp/server.js +397 -0
- package/dist/core/memory/dual-write.js +416 -0
- package/dist/core/memory/phase1-kinds.js +20 -0
- package/dist/core/memory-sync/queue.js +158 -0
- package/dist/core/onboarding/ensure-initialized.js +133 -0
- package/dist/core/onboarding/marker.js +111 -0
- package/dist/core/onboarding/telemetry-state.js +108 -0
- package/dist/core/output-style/presets.js +176 -0
- package/dist/core/output-style/state.js +185 -0
- package/dist/core/path-security.js +284 -2
- package/dist/core/permissions/auto-classifier.js +124 -0
- package/dist/core/permissions/circuit-breaker.js +83 -0
- package/dist/core/permissions/gate.js +278 -0
- package/dist/core/permissions/index.js +20 -0
- package/dist/core/permissions/mode.js +174 -0
- package/dist/core/permissions/state.js +241 -0
- package/dist/core/permissions/tool-class.js +93 -0
- package/dist/core/prd-check/parser.js +215 -0
- package/dist/core/prd-check/reporter.js +127 -0
- package/dist/core/prd-check/session-review.js +557 -0
- package/dist/core/prd-check/verifiers.js +223 -0
- package/dist/core/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/history.js +11 -1
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/session.js +1897 -37
- package/dist/core/repl/slash-commands.js +430 -15
- package/dist/core/repl/store/session-store.js +31 -2
- package/dist/core/repl/workspace-context.js +22 -0
- package/dist/core/repo-map/build.js +125 -0
- package/dist/core/repo-map/cache.js +185 -0
- package/dist/core/repo-map/extractor.js +254 -0
- package/dist/core/repo-map/formatter.js +145 -0
- package/dist/core/repo-map/scanner.js +211 -0
- package/dist/core/retry-budget/budget.js +284 -0
- package/dist/core/retry-budget/index.js +5 -0
- package/dist/core/session.js +92 -0
- package/dist/core/settings.js +80 -0
- package/dist/core/share/formatter.js +271 -0
- package/dist/core/share/redactor.js +221 -0
- package/dist/core/share/uploader.js +267 -0
- package/dist/core/skills/defaults.js +457 -0
- package/dist/core/smoke/headless-driver.js +174 -0
- package/dist/core/smoke/orchestrator.js +194 -0
- package/dist/core/smoke/runner.js +238 -0
- package/dist/core/smoke/scenario-parser.js +316 -0
- package/dist/core/subagents/dispatcher-real.js +600 -0
- package/dist/core/subagents/dispatcher.js +113 -24
- package/dist/core/subagents/index.js +18 -5
- package/dist/core/subagents/isolation-matrix.js +213 -0
- package/dist/core/subagents/spawn.js +19 -4
- package/dist/core/telemetry/emitter.js +229 -0
- package/dist/core/telemetry/queue.js +251 -0
- package/dist/core/theme/context.js +91 -0
- package/dist/core/theme/presets.js +228 -0
- package/dist/core/theme/state.js +181 -0
- package/dist/core/todos/invariant.js +10 -0
- package/dist/core/todos/state.js +177 -0
- package/dist/core/transport/version-interceptor.js +166 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/core/worktree-manager/cleanup.js +123 -0
- package/dist/core/worktree-manager/manager.js +303 -0
- package/dist/index.js +28 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +3241 -343
- package/dist/runtime/commands/cancel.js +231 -0
- package/dist/runtime/commands/chain.js +489 -0
- package/dist/runtime/commands/codegraph-status.js +227 -0
- package/dist/runtime/commands/compact.js +297 -0
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +242 -11
- package/dist/runtime/commands/dispatch.js +126 -0
- package/dist/runtime/commands/doctor.js +412 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/hooks.js +184 -0
- package/dist/runtime/commands/lsp.js +368 -0
- package/dist/runtime/commands/mcp.js +879 -0
- package/dist/runtime/commands/memory.js +508 -0
- package/dist/runtime/commands/model.js +237 -0
- package/dist/runtime/commands/onboarding.js +275 -0
- package/dist/runtime/commands/patch.js +128 -0
- package/dist/runtime/commands/permissions.js +112 -0
- package/dist/runtime/commands/plan.js +143 -0
- package/dist/runtime/commands/prd-check.js +285 -0
- package/dist/runtime/commands/redo-blob-store.js +92 -0
- package/dist/runtime/commands/redo.js +361 -0
- package/dist/runtime/commands/release-notes.js +229 -0
- package/dist/runtime/commands/repo-map.js +95 -0
- package/dist/runtime/commands/report.js +299 -0
- package/dist/runtime/commands/resume.js +118 -0
- package/dist/runtime/commands/review-consensus.js +17 -2
- package/dist/runtime/commands/rewind.js +333 -0
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/commands/share.js +316 -0
- package/dist/runtime/commands/status.js +186 -0
- package/dist/runtime/commands/stickers.js +82 -0
- package/dist/runtime/commands/style.js +194 -0
- package/dist/runtime/commands/theme.js +196 -0
- package/dist/runtime/commands/undo.js +32 -0
- package/dist/runtime/commands/update.js +289 -0
- package/dist/runtime/commands/vim.js +140 -0
- package/dist/runtime/commands/worktree.js +177 -0
- package/dist/runtime/commands/worktrees.js +155 -0
- package/dist/runtime/headless-repl.js +195 -0
- package/dist/runtime/headless.js +543 -0
- package/dist/runtime/load-hooks-or-exit.js +71 -0
- package/dist/runtime/plan-decompose.js +531 -0
- package/dist/runtime/version.js +65 -0
- package/dist/tools/agent-tool.js +229 -0
- package/dist/tools/apply-patch.js +556 -0
- package/dist/tools/ask-user-question.js +213 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/bash.js +203 -4
- package/dist/tools/file-tools.js +85 -14
- package/dist/tools/lsp-tools.js +189 -0
- package/dist/tools/mcp-tool.js +260 -0
- package/dist/tools/multi-edit.js +361 -0
- package/dist/tools/powershell.js +268 -0
- package/dist/tools/registry.js +51 -0
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tools/todo-write.js +184 -0
- package/dist/tools/web-fetch.js +147 -2
- package/dist/tools/web-search.js +458 -0
- package/dist/tui/agent-progress-card.js +111 -0
- package/dist/tui/agent-tree.js +10 -0
- package/dist/tui/ask-modal.js +2 -2
- package/dist/tui/ask-user-question-prompt.js +192 -0
- package/dist/tui/compact-banner.js +81 -0
- package/dist/tui/conversation-pane.js +82 -8
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +218 -3
- package/dist/tui/markdown-render.js +4 -4
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/permissions-picker.js +86 -0
- package/dist/tui/render.js +35 -0
- package/dist/tui/repl-render.js +313 -35
- package/dist/tui/repl-splash-art.js +1 -1
- package/dist/tui/repl-splash-mascot.js +32 -8
- package/dist/tui/repl-splash.js +2 -2
- package/dist/tui/repl.js +85 -5
- package/dist/tui/splash.js +1 -1
- package/dist/tui/status-bar.js +94 -16
- package/dist/tui/status-table.js +7 -0
- package/dist/tui/stickers-art.js +136 -0
- package/dist/tui/style-table.js +28 -0
- package/dist/tui/theme-table.js +29 -0
- package/dist/tui/thinking-spinner.js +123 -0
- package/dist/tui/tool-stream-pane.js +52 -3
- package/dist/tui/update-banner.js +27 -2
- package/dist/tui/vim-input.js +267 -0
- package/dist/tui/welcome-banner.js +107 -0
- package/dist/tui/welcome-data.js +293 -0
- package/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +12 -6
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +11 -0
- package/test/scenarios/identity.scenario.txt +11 -0
- package/test/scenarios/persona-handoff.scenario.txt +11 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- 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
|