@pugi/cli 0.1.0-beta.87 → 0.1.0-beta.88

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 (37) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +1 -1
  3. package/dist/core/agents/registry.js +1 -1
  4. package/dist/core/checkpoints/shadow-git.js +1 -1
  5. package/dist/core/context/compaction.js +1 -1
  6. package/dist/core/denial-tracking/state.js +1 -1
  7. package/dist/core/edits/fuzzy-ladder.js +1 -1
  8. package/dist/core/edits/layer-a-fuzzy-apply.js +1 -1
  9. package/dist/core/engine/anvil-client.js +13 -2
  10. package/dist/core/mcp/server-tools.js +1 -1
  11. package/dist/core/mcp/server.js +1 -1
  12. package/dist/core/memory/secret-scanner.js +6 -6
  13. package/dist/core/onboarding/ensure-initialized.js +1 -1
  14. package/dist/core/plans/plan-artifact.js +2 -2
  15. package/dist/core/repl/cap-warning.js +1 -1
  16. package/dist/core/routing/pre-flight-estimator.js +1 -1
  17. package/dist/core/settings.js +12 -0
  18. package/dist/index.js +8 -0
  19. package/dist/runtime/cli.js +68 -20
  20. package/dist/runtime/commands/config.js +41 -7
  21. package/dist/runtime/sigint-guard.js +272 -0
  22. package/dist/runtime/version.js +1 -1
  23. package/dist/skills/bundled/batch.js +2 -2
  24. package/dist/skills/bundled/index.js +3 -3
  25. package/dist/skills/bundled/loop.js +2 -2
  26. package/dist/skills/bundled/remember.js +1 -1
  27. package/dist/skills/bundled/simplify.js +1 -1
  28. package/dist/skills/bundled/skillify.js +2 -2
  29. package/dist/skills/bundled/stuck.js +1 -1
  30. package/dist/skills/bundled/verify.js +2 -2
  31. package/dist/testing/vcr.js +2 -2
  32. package/dist/tools/ask-user-question.js +66 -0
  33. package/dist/tools/bash.js +2 -2
  34. package/dist/tools/powershell.js +1 -1
  35. package/dist/tui/ask-user-question-chips.js +257 -0
  36. package/dist/tui/welcome-data.js +4 -4
  37. package/package.json +5 -4
@@ -0,0 +1,257 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * AskUserQuestionChips — multi-question short-format chip renderer
4
+ * for the structured AskUserQuestion tool (PUGI-480).
5
+ *
6
+ * Renders up to 3 question chips side-by-side, each with up to ~5 short
7
+ * (≤ 5-word) option labels. Replaces paragraph-wall prompts с a compact
8
+ * keyboard-driven picker:
9
+ *
10
+ * Pugi: 2 quick choices
11
+ * ┌────────────────────────┐
12
+ * │ Stack │
13
+ * │ ▸ 1 Browser JS │
14
+ * │ 2 React │
15
+ * │ 3 Other... │
16
+ * └────────────────────────┘
17
+ * ┌────────────────────────┐
18
+ * │ Size │
19
+ * │ ▸ 1 9x9 (classic) │
20
+ * │ 2 16x16 │
21
+ * │ 3 Custom │
22
+ * └────────────────────────┘
23
+ * [Enter] use highlighted defaults · [↑↓] navigate · [s] skip
24
+ *
25
+ * Contract:
26
+ * - questions[].options[].label MUST be ≤ 5 words (renderer truncates
27
+ * overflow с ellipsis defensively; the schema validator rejects at
28
+ * the tool entry point so the model never produces overflow legally).
29
+ * - First option of each question is pre-highlighted (default).
30
+ * - Enter без movement applies defaults across ALL questions.
31
+ * - 1-9 jump к the matching option in the focused question.
32
+ * - ↑↓ navigate within the focused question.
33
+ * - ←→ (and Tab/Shift-Tab) move between questions.
34
+ * - [s] skips the focused question (uses its default) and advances to
35
+ * the next; [s] on the last question commits.
36
+ * - [Esc] cancels the entire dialog (user_cancelled).
37
+ *
38
+ * Non-TTY fallback: when the `forceFallback` prop is true, renders a
39
+ * static numbered text list and does NOT mount the Ink useInput hook.
40
+ * The component itself does NOT sniff `process.stdin.isTTY` — that gate
41
+ * lives in the upstream REPL wiring, which either mounts this chip
42
+ * component (TTY path) or emits the legacy `[user_input_required]`
43
+ * envelope (non-TTY path). Keeping the gate в the caller keeps the
44
+ * component testable under ink-testing-library (which mocks stdin but
45
+ * не sets isTTY).
46
+ *
47
+ * Brand voice gate: ASCII glyphs only, zero attribution to external AIs,
48
+ * no em-dashes. Power-word neutral so localised variants land cleanly.
49
+ */
50
+ import { useMemo, useState } from 'react';
51
+ import { Box, Text, useInput } from 'ink';
52
+ /**
53
+ * Hard truncation cap. The schema validator enforces ≤ 5 words per
54
+ * label, but the renderer still defends in depth: a malicious or
55
+ * legacy payload with a 14-word label MUST not blow the chip width.
56
+ */
57
+ export const ASK_CHIPS_LABEL_WORD_CAP = 5;
58
+ export const ASK_CHIPS_LABEL_CHAR_CAP = 22;
59
+ export const ASK_CHIPS_QUESTION_CAP = 3;
60
+ export const ASK_CHIPS_SKIP_LABEL = 'Skip — use defaults';
61
+ /**
62
+ * Truncate a label к the configured word / character caps. Always
63
+ * append "…" when truncation happens so the operator knows the chip
64
+ * is hiding content.
65
+ */
66
+ export function truncateLabel(raw) {
67
+ const trimmed = raw.trim().replace(/\s+/gu, ' ');
68
+ // Word cap (whitespace-delimited). 5-word rule is the canonical limit.
69
+ const words = trimmed.split(' ');
70
+ let candidate = trimmed;
71
+ if (words.length > ASK_CHIPS_LABEL_WORD_CAP) {
72
+ candidate = `${words.slice(0, ASK_CHIPS_LABEL_WORD_CAP).join(' ')}…`;
73
+ }
74
+ // Char cap (defense-in-depth — protects chip width even с short-word
75
+ // payloads like "AAAAAAAAAAAAAAAAAAAAA" that pass the word gate).
76
+ if (candidate.length > ASK_CHIPS_LABEL_CHAR_CAP) {
77
+ candidate = `${candidate.slice(0, ASK_CHIPS_LABEL_CHAR_CAP - 1)}…`;
78
+ }
79
+ return candidate;
80
+ }
81
+ /**
82
+ * The component renders в interactive mode by default. The non-TTY
83
+ * gate is the caller's responsibility — see ask-user-question-chips.tsx
84
+ * header docs. Production REPL wiring should check `process.stdin.isTTY`
85
+ * upstream и pass `forceFallback={true}` when stdin is not a TTY, OR
86
+ * skip mounting the chip component entirely и emit the legacy
87
+ * `[user_input_required]` envelope.
88
+ */
89
+ function shouldUseFallback(forceFallback) {
90
+ return forceFallback === true;
91
+ }
92
+ /**
93
+ * Build the default selections vector (all first options) so Enter
94
+ * без movement applies a coherent set, and so non-TTY fallback can
95
+ * commit immediately.
96
+ */
97
+ function buildDefaults(questions) {
98
+ return questions.map((q) => {
99
+ const first = q.options[0];
100
+ return {
101
+ header: q.header,
102
+ pickedIndex: 0,
103
+ pickedLabel: first ? truncateLabel(first.label) : '',
104
+ skipped: false,
105
+ };
106
+ });
107
+ }
108
+ export function AskUserQuestionChips(props) {
109
+ const useFallback = shouldUseFallback(props.forceFallback);
110
+ // Defensive cap: never render more than ASK_CHIPS_QUESTION_CAP chips.
111
+ // Schema enforces this at the tool boundary; the renderer still trims
112
+ // в case a legacy / malicious payload sneaks through.
113
+ const questions = useMemo(() => props.questions.slice(0, ASK_CHIPS_QUESTION_CAP), [props.questions]);
114
+ // Cursor state: which question is focused, and which option within
115
+ // each question is highlighted. Initialised к first option (default).
116
+ const [focusedQuestion, setFocusedQuestion] = useState(0);
117
+ const [cursorPerQuestion, setCursorPerQuestion] = useState(() => questions.map(() => 0));
118
+ // Per-question explicit skip flag. Skipped questions still emit their
119
+ // default index, but flagged `skipped: true` so downstream can audit.
120
+ const [skipped, setSkipped] = useState(() => questions.map(() => false));
121
+ function commit(skipMask = skipped) {
122
+ const selections = questions.map((q, qIdx) => {
123
+ const optionIdx = cursorPerQuestion[qIdx] ?? 0;
124
+ const opt = q.options[optionIdx] ?? q.options[0];
125
+ return {
126
+ header: q.header,
127
+ pickedIndex: optionIdx,
128
+ pickedLabel: opt ? truncateLabel(opt.label) : '',
129
+ skipped: skipMask[qIdx] === true,
130
+ };
131
+ });
132
+ props.onResolve({ selections, cancelled: false });
133
+ }
134
+ function cancel() {
135
+ props.onResolve({ selections: buildDefaults(questions), cancelled: true });
136
+ }
137
+ useInput((input, key) => {
138
+ // Esc cancels the entire AskUserQuestion (user_cancelled).
139
+ if (key.escape) {
140
+ cancel();
141
+ return;
142
+ }
143
+ // Enter commits the current selection (applies defaults across
144
+ // any questions the operator did not touch).
145
+ if (key.return) {
146
+ commit();
147
+ return;
148
+ }
149
+ // [s] skips the focused question (uses default), advances to next.
150
+ // [s] on the LAST question commits the whole set.
151
+ if (input === 's' || input === 'S') {
152
+ const nextSkipped = skipped.slice();
153
+ nextSkipped[focusedQuestion] = true;
154
+ if (focusedQuestion >= questions.length - 1) {
155
+ setSkipped(nextSkipped);
156
+ commit(nextSkipped);
157
+ return;
158
+ }
159
+ setSkipped(nextSkipped);
160
+ setFocusedQuestion((q) => Math.min(q + 1, questions.length - 1));
161
+ return;
162
+ }
163
+ // ↑/↓ navigate within the focused question.
164
+ if (key.upArrow) {
165
+ setCursorPerQuestion((prev) => {
166
+ const next = prev.slice();
167
+ const opts = questions[focusedQuestion]?.options ?? [];
168
+ const len = opts.length;
169
+ if (len === 0)
170
+ return prev;
171
+ const cur = next[focusedQuestion] ?? 0;
172
+ next[focusedQuestion] = (cur - 1 + len) % len;
173
+ return next;
174
+ });
175
+ return;
176
+ }
177
+ if (key.downArrow) {
178
+ setCursorPerQuestion((prev) => {
179
+ const next = prev.slice();
180
+ const opts = questions[focusedQuestion]?.options ?? [];
181
+ const len = opts.length;
182
+ if (len === 0)
183
+ return prev;
184
+ const cur = next[focusedQuestion] ?? 0;
185
+ next[focusedQuestion] = (cur + 1) % len;
186
+ return next;
187
+ });
188
+ return;
189
+ }
190
+ // ←/→ (or Tab/Shift-Tab) navigate between questions.
191
+ if (key.leftArrow || (key.shift && key.tab)) {
192
+ setFocusedQuestion((q) => (q - 1 + questions.length) % questions.length);
193
+ return;
194
+ }
195
+ if (key.rightArrow || key.tab) {
196
+ setFocusedQuestion((q) => (q + 1) % questions.length);
197
+ return;
198
+ }
199
+ // 1-9 jumps к the matching option in the focused question.
200
+ const numeric = Number.parseInt(input, 10);
201
+ if (!Number.isNaN(numeric) && numeric >= 1 && numeric <= 9) {
202
+ const opts = questions[focusedQuestion]?.options ?? [];
203
+ const target = numeric - 1;
204
+ if (target >= 0 && target < opts.length) {
205
+ setCursorPerQuestion((prev) => {
206
+ const next = prev.slice();
207
+ next[focusedQuestion] = target;
208
+ return next;
209
+ });
210
+ }
211
+ return;
212
+ }
213
+ }, { isActive: props.inert !== true && useFallback === false });
214
+ // ─── Non-TTY fallback ────────────────────────────────────────────────
215
+ // When stdin is not a TTY we still need to render SOMETHING — but
216
+ // we cannot drive keystrokes, so we emit a numbered text list per
217
+ // question and let the upstream envelope handler ask the operator.
218
+ if (useFallback) {
219
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: _jsx(Text, { bold: true, children: `Pugi: ${questions.length} quick ${questions.length === 1 ? 'choice' : 'choices'}` }) }), questions.map((q, qIdx) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: `${q.header}` }), q.options.map((opt, oIdx) => (_jsx(Text, { children: ` ${oIdx + 1}. ${truncateLabel(opt.label)}${oIdx === 0 ? ' (default)' : ''}` }, `q-${qIdx}-o-${oIdx}`)))] }, `q-${qIdx}-${q.header}`))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, italic: true, children: `(non-interactive — defaults apply)` }) })] }));
220
+ }
221
+ // ─── Interactive Ink render ─────────────────────────────────────────
222
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: _jsx(Text, { bold: true, children: `Pugi: ${questions.length} quick ${questions.length === 1 ? 'choice' : 'choices'}` }) }), _jsx(Box, { flexDirection: "row", marginTop: 1, children: questions.map((q, qIdx) => {
223
+ const isFocused = qIdx === focusedQuestion;
224
+ const cursor = cursorPerQuestion[qIdx] ?? 0;
225
+ const isSkipped = skipped[qIdx] === true;
226
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: isFocused ? 'cyan' : 'gray', paddingX: 1, marginRight: 1, minWidth: 24, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: isFocused ? 'cyan' : undefined, children: q.header }), isSkipped ? (_jsx(Text, { dimColor: true, italic: true, children: ' (skipped)' })) : null] }), q.options.map((opt, oIdx) => {
227
+ const isHighlighted = isFocused && oIdx === cursor;
228
+ const label = truncateLabel(opt.label);
229
+ const isSkipOption = label === ASK_CHIPS_SKIP_LABEL ||
230
+ opt.label === ASK_CHIPS_SKIP_LABEL;
231
+ return (_jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? 'cyan' : undefined, bold: isHighlighted, children: isHighlighted ? '▸ ' : ' ' }), _jsx(Text, { color: isHighlighted ? 'cyan' : undefined, children: `${oIdx + 1} ` }), isSkipOption ? (_jsx(Text, { dimColor: true, italic: true, children: label })) : (_jsx(Text, { bold: isHighlighted, children: label }))] }, `opt-${qIdx}-${oIdx}-${opt.label}`));
232
+ })] }, `chip-${qIdx}-${q.header}`));
233
+ }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: `[Enter] use highlighted defaults · [↑↓] navigate · [←→] switch · [s] skip · [Esc] cancel` }) })] }));
234
+ }
235
+ /**
236
+ * Encode the chip verdict into the persona-recognisable turn-injection
237
+ * string. Mirrors `encodeAskUserQuestionVerdict` from the single-question
238
+ * prompt so the model sees a consistent grammar.
239
+ *
240
+ * Examples:
241
+ * - { cancelled: true } → "[ASK-USER-QUESTION:cancelled]"
242
+ * - All defaults → "[ASK-USER-QUESTION:answered] Stack=Browser JS; Size=9x9 (classic)"
243
+ * - One skipped → "[ASK-USER-QUESTION:answered] Stack=Browser JS; Size=(skipped: 9x9)"
244
+ */
245
+ export function encodeAskUserQuestionChipsVerdict(result) {
246
+ if (result.cancelled)
247
+ return '[ASK-USER-QUESTION:cancelled]';
248
+ if (result.selections.length === 0)
249
+ return '[ASK-USER-QUESTION:cancelled]';
250
+ const parts = result.selections.map((sel) => {
251
+ if (sel.skipped)
252
+ return `${sel.header}=(skipped: ${sel.pickedLabel})`;
253
+ return `${sel.header}=${sel.pickedLabel}`;
254
+ });
255
+ return `[ASK-USER-QUESTION:answered] ${parts.join('; ')}`;
256
+ }
257
+ //# sourceMappingURL=ask-user-question-chips.js.map
@@ -9,12 +9,12 @@
9
9
  *
10
10
  * ╭────── Pugi v0.1.0-beta.46 ──────╮
11
11
  * │ │ Tips for getting started
12
- * │ Welcome back Yurii! │ Run /init to create PUGI.md...
12
+ * │ Welcome back operator! │ Run /init to create PUGI.md...
13
13
  * │ │ ───────────
14
14
  * │ ▗ ▗ ▖ ▖ │ What's new
15
15
  * │ ▘▘ ▝▝ │ * 0.1.0-beta.26 — RAG ...
16
16
  * │ Sonnet 4.6 (1M context) · Founder
17
- * │ yuriy.bulah@gmail.com · pugi-io Org
17
+ * │ operator@gmail.com · pugi-io Org
18
18
  * │ <workspace>
19
19
  * ╰──────────────────────────────────╯
20
20
  *
@@ -79,8 +79,8 @@ function decodeJwtPayload(token) {
79
79
  /**
80
80
  * Derive a greeting first-name from an email's local part. The split
81
81
  * is intentionally aggressive: dot-separated, hyphen-separated, and
82
- * digit-stripped so `yuriy.bulah@gmail.com` resolves к "Yuriy" instead
83
- * of "yuriy.bulah". Title-cases the first segment. Falls back к
82
+ * digit-stripped so `operator@gmail.com` resolves к "operator" instead
83
+ * of "operator". Title-cases the first segment. Falls back к
84
84
  * "operator" when no email is available.
85
85
  *
86
86
  * Export so the spec can lock the heuristic against edge cases (numeric
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pugi/cli",
3
- "version": "0.1.0-beta.87",
3
+ "version": "0.1.0-beta.88",
4
4
  "description": "Pugi CLI - terminal-native software execution system",
5
5
  "homepage": "https://pugi.io",
6
6
  "repository": {
@@ -62,8 +62,8 @@
62
62
  "undici": "^8.3.0",
63
63
  "which": "^6.0.0",
64
64
  "zod": "^3.23.0",
65
- "@pugi/personas": "0.1.2",
66
- "@pugi/sdk": "0.1.0-beta.87"
65
+ "@pugi/sdk": "0.1.0-beta.88",
66
+ "@pugi/personas": "0.1.2"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@types/node": "^22.0.0",
@@ -87,6 +87,7 @@
87
87
  "doctor": "tsx src/index.ts doctor --json",
88
88
  "check:version-lockstep": "bash ../../scripts/check-version-lockstep.sh",
89
89
  "pack:smoke": "pnpm run check:version-lockstep && node scripts/pack-smoke.mjs",
90
- "release-gate": "node scripts/secret-scanner.mjs"
90
+ "release-gate": "node scripts/secret-scanner.mjs",
91
+ "scan:tarball": "STAGE=$(mktemp -d) && npm pack --pack-destination \"$STAGE\" >/dev/null && bash ../../tools/scrub/scan-tarball.sh \"$STAGE\"/*.tgz; RC=$?; rm -rf \"$STAGE\"; exit $RC"
91
92
  }
92
93
  }