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

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 (68) 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/auth/env-provider.js +1 -1
  5. package/dist/core/checkpoints/shadow-git.js +1 -1
  6. package/dist/core/context/compaction.js +1 -1
  7. package/dist/core/context/markdown-traverse.js +1 -1
  8. package/dist/core/credentials.js +1 -1
  9. package/dist/core/denial-tracking/state.js +1 -1
  10. package/dist/core/edits/fuzzy-ladder.js +1 -1
  11. package/dist/core/edits/layer-a-fuzzy-apply.js +1 -1
  12. package/dist/core/engine/anvil-client.js +76 -2
  13. package/dist/core/engine/native-pugi.js +1 -1
  14. package/dist/core/engine/tool-bridge.js +436 -0
  15. package/dist/core/hooks/events.js +3 -1
  16. package/dist/core/hooks/registry.js +3 -0
  17. package/dist/core/hooks/worktree-events.js +158 -0
  18. package/dist/core/lsp/client.js +453 -0
  19. package/dist/core/lsp/server-detect.js +173 -0
  20. package/dist/core/lsp/symbol-cache.js +162 -0
  21. package/dist/core/lsp/symbol-tools.js +296 -4
  22. package/dist/core/mcp/server-tools.js +1 -1
  23. package/dist/core/mcp/server.js +1 -1
  24. package/dist/core/memory/secret-scanner.js +6 -6
  25. package/dist/core/onboarding/ensure-initialized.js +1 -1
  26. package/dist/core/plans/plan-artifact.js +2 -2
  27. package/dist/core/repl/ask.js +1 -1
  28. package/dist/core/repl/cap-warning.js +1 -1
  29. package/dist/core/repl/session.js +3 -3
  30. package/dist/core/repl/slash-commands.js +1 -1
  31. package/dist/core/routing/pre-flight-estimator.js +1 -1
  32. package/dist/core/settings.js +38 -0
  33. package/dist/core/worktree/include-parser.js +249 -0
  34. package/dist/index.js +8 -0
  35. package/dist/runtime/cli.js +176 -28
  36. package/dist/runtime/commands/agents.js +1 -1
  37. package/dist/runtime/commands/config.js +41 -7
  38. package/dist/runtime/commands/hooks.js +3 -0
  39. package/dist/runtime/commands/review-consensus.js +1 -1
  40. package/dist/runtime/sigint-guard.js +272 -0
  41. package/dist/runtime/version.js +1 -1
  42. package/dist/runtime/worktree-bootstrap.js +579 -0
  43. package/dist/skills/bundled/batch.js +2 -2
  44. package/dist/skills/bundled/index.js +3 -3
  45. package/dist/skills/bundled/loop.js +2 -2
  46. package/dist/skills/bundled/remember.js +1 -1
  47. package/dist/skills/bundled/simplify.js +1 -1
  48. package/dist/skills/bundled/skillify.js +2 -2
  49. package/dist/skills/bundled/stuck.js +1 -1
  50. package/dist/skills/bundled/verify.js +2 -2
  51. package/dist/testing/vcr.js +2 -2
  52. package/dist/tools/ask-user-question.js +66 -0
  53. package/dist/tools/bash.js +2 -2
  54. package/dist/tools/lsp-tools.js +377 -1
  55. package/dist/tools/powershell.js +1 -1
  56. package/dist/tools/registry.js +23 -0
  57. package/dist/tui/ask-user-question-chips.js +257 -0
  58. package/dist/tui/input-box.js +1 -1
  59. package/dist/tui/render.js +1 -1
  60. package/dist/tui/repl.js +1 -1
  61. package/dist/tui/status-bar.js +1 -1
  62. package/dist/tui/update-banner.js +1 -1
  63. package/dist/tui/welcome-data.js +4 -4
  64. package/package.json +4 -3
  65. package/test/scenarios/compact-force.scenario.txt +3 -2
  66. package/test/scenarios/identity.scenario.txt +6 -5
  67. package/test/scenarios/persona-handoff.scenario.txt +2 -1
  68. package/test/scenarios/walkback.scenario.txt +6 -6
@@ -165,6 +165,44 @@ const pugiSettingsSchema = z.object({
165
165
  // keeps Zod's strip-pass from swallowing it before the chain reader
166
166
  // sees it. See `hook-chains.ts` for the full schema.
167
167
  hooks: z.any().optional(),
168
+ // PUGI-260 — persistent default for the 1M context tier opt-in.
169
+ // `pugi config set context.tier 1m` writes this; per-invocation
170
+ // `--context-tier=...` flags override it. When omitted, the CLI
171
+ // sends no `contextTier` field на the wire (server treats as
172
+ // `standard` routing). The closed enum mirrors the CLI flag и the
173
+ // admin-api DTO; an unrecognised value triggers a Zod parse error
174
+ // at load time rather than a silent fallback.
175
+ context: z
176
+ .object({
177
+ tier: z.enum(['1m', 'standard']).optional(),
178
+ })
179
+ .optional(),
180
+ // PUGI-487 - `pugi --worktree` flag governance.
181
+ //
182
+ // Two knobs control the user-facing --worktree flag introduced for
183
+ // parity with parallel-agent isolation patterns in other coding
184
+ // CLIs:
185
+ //
186
+ // - `baseRef`: which ref the new worktree branches FROM.
187
+ // `'fresh'` (default) resolves origin/<default-branch> so each
188
+ // parallel session starts from a clean trunk.
189
+ // `'head'` carries the operator's current local HEAD (including
190
+ // unpushed work) into the new tree.
191
+ //
192
+ // - `cleanupPeriodDays`: integer days. The daily sweep removes
193
+ // user-facing worktrees older than N days that have no
194
+ // uncommitted, untracked, or unpushed state. Default 7 mirrors
195
+ // a one-work-week window. Set to 0 to disable auto-cleanup.
196
+ //
197
+ // Both knobs are optional - the consumers (`bootstrapWorktree`,
198
+ // `runUserWorktreeCleanup`) carry their own defaults so a missing
199
+ // section produces standard behaviour.
200
+ worktree: z
201
+ .object({
202
+ baseRef: z.enum(['fresh', 'head']).optional(),
203
+ cleanupPeriodDays: z.number().int().min(0).max(365).optional(),
204
+ })
205
+ .optional(),
168
206
  });
169
207
  /**
170
208
  * #20 — the upstream tool drop-in compat ingest.
@@ -0,0 +1,249 @@
1
+ /**
2
+ * `.worktreeinclude` parser - PUGI-487.
3
+ *
4
+ * The `.worktreeinclude` file lives at the repository root and carries
5
+ * gitignore-syntax patterns. Files that:
6
+ *
7
+ * 1. Match at least one pattern in `.worktreeinclude`, AND
8
+ * 2. Are git-ignored (untracked-and-ignored OR explicitly ignored),
9
+ *
10
+ * get COPIED into a fresh `pugi --worktree <name>` tree right after
11
+ * `git worktree add` finishes. Tracked files are NEVER copied because
12
+ * git already brings them along on its own, and double-writing would
13
+ * corrupt the worktree index.
14
+ *
15
+ * Why a separate include file instead of `.gitignore` negation
16
+ * patterns?
17
+ *
18
+ * - The common case is "I have an untracked `.env` with API keys that
19
+ * every parallel session needs". Listing it in `.gitignore` with
20
+ * a negation would force-add it to the repo, defeating the point.
21
+ * - Operators want a single-file, copy-pasteable manifest of WHAT
22
+ * leaves the main tree, not a derived view across `.gitignore`
23
+ * semantics.
24
+ *
25
+ * Pattern grammar (subset of gitignore):
26
+ *
27
+ * - Lines starting with `#` are comments. Blank lines are skipped.
28
+ * - Trailing `/` means directory-only and applies recursively to
29
+ * every file under the directory.
30
+ * - Leading `!` is a negation pattern that REMOVES matches from
31
+ * prior includes. Multiple negations are processed in order.
32
+ * - `*` matches any character except `/`.
33
+ * - `**` matches any number of path segments.
34
+ * - `?` matches a single non-`/` character.
35
+ * - Patterns with NO `/` match anywhere in the tree (e.g. `.env`
36
+ * matches both `./.env` and `./apps/foo/.env`).
37
+ * - Patterns starting with `/` are anchored to the repository root.
38
+ *
39
+ * Brand voice: ASCII only, no emoji, no banned words.
40
+ */
41
+ import { spawnSync } from 'node:child_process';
42
+ import { existsSync, readFileSync, statSync } from 'node:fs';
43
+ import { resolve, sep, posix } from 'node:path';
44
+ /** Default file name. */
45
+ export const WORKTREE_INCLUDE_FILENAME = '.worktreeinclude';
46
+ /**
47
+ * Parse the textual contents of a `.worktreeinclude` file into a rule
48
+ * list. Pure function - does not touch the filesystem. Exposed for
49
+ * spec coverage and the cleanup/dry-run path.
50
+ */
51
+ export function parseWorktreeIncludeText(text, source = WORKTREE_INCLUDE_FILENAME) {
52
+ const rules = [];
53
+ const lines = text.split(/\r?\n/);
54
+ for (const raw of lines) {
55
+ const line = raw.trim();
56
+ if (line.length === 0)
57
+ continue;
58
+ if (line.startsWith('#'))
59
+ continue;
60
+ let body = line;
61
+ let negate = false;
62
+ if (body.startsWith('!')) {
63
+ negate = true;
64
+ body = body.slice(1);
65
+ }
66
+ if (body.length === 0)
67
+ continue;
68
+ let directoryOnly = false;
69
+ if (body.endsWith('/')) {
70
+ directoryOnly = true;
71
+ body = body.slice(0, -1);
72
+ }
73
+ let anchored = false;
74
+ if (body.startsWith('/')) {
75
+ anchored = true;
76
+ body = body.slice(1);
77
+ }
78
+ else if (body.includes('/')) {
79
+ // a non-leading slash also anchors per gitignore semantics
80
+ anchored = true;
81
+ }
82
+ if (body.length === 0)
83
+ continue;
84
+ rules.push({
85
+ raw: line,
86
+ pattern: body,
87
+ negate,
88
+ directoryOnly,
89
+ anchored,
90
+ });
91
+ }
92
+ return { rules, source };
93
+ }
94
+ /**
95
+ * Load + parse `.worktreeinclude` from a repository root. Returns an
96
+ * empty rule set when the file is absent or unreadable - a missing
97
+ * include file is the normal case (no extras to copy).
98
+ */
99
+ export function loadWorktreeInclude(repoRoot) {
100
+ const path = resolve(repoRoot, WORKTREE_INCLUDE_FILENAME);
101
+ if (!existsSync(path)) {
102
+ return { rules: [], source: path };
103
+ }
104
+ try {
105
+ const text = readFileSync(path, 'utf8');
106
+ return parseWorktreeIncludeText(text, path);
107
+ }
108
+ catch {
109
+ return { rules: [], source: path };
110
+ }
111
+ }
112
+ const REGEX_ESCAPE = /[.+^${}()|[\]\\]/;
113
+ /**
114
+ * Convert a gitignore-style glob pattern into a regex source string.
115
+ * Subset implementation: covers `*`, `**`, `?`, and escapes the rest.
116
+ *
117
+ * Uses index-based scanning so `**` is detected and consumed before a
118
+ * single `*` substitution could eat the doubled form.
119
+ */
120
+ function globToRegexSource(pattern) {
121
+ let out = '';
122
+ for (let i = 0; i < pattern.length; i += 1) {
123
+ const ch = pattern.charAt(i);
124
+ if (ch === '*') {
125
+ if (pattern.charAt(i + 1) === '*') {
126
+ out += '.*';
127
+ i += 1;
128
+ continue;
129
+ }
130
+ out += '[^/]*';
131
+ continue;
132
+ }
133
+ if (ch === '?') {
134
+ out += '[^/]';
135
+ continue;
136
+ }
137
+ if (REGEX_ESCAPE.test(ch)) {
138
+ out += '\\' + ch;
139
+ continue;
140
+ }
141
+ out += ch;
142
+ }
143
+ return out;
144
+ }
145
+ /**
146
+ * Decide whether `relPath` (a forward-slash, no-leading-slash path
147
+ * relative to the repo root) is matched by the given rule list. The
148
+ * last matching rule wins so a negation after an include drops the
149
+ * entry and an include after a negation re-adds it.
150
+ */
151
+ export function matchesWorktreeInclude(relPath, rules, isDirectory = false) {
152
+ const normalized = relPath.replace(/\\+/g, '/');
153
+ let matched = false;
154
+ for (const rule of rules) {
155
+ if (rule.directoryOnly && !isDirectory) {
156
+ // directory-only rule still matches files UNDER the dir (handled
157
+ // below). Falls through to the file-match attempt.
158
+ }
159
+ const source = buildRegex(rule);
160
+ const re = new RegExp('^' + source + '$');
161
+ if (re.test(normalized)) {
162
+ matched = !rule.negate;
163
+ continue;
164
+ }
165
+ if (rule.directoryOnly) {
166
+ const reUnder = new RegExp('^' + source + '/.*$');
167
+ if (reUnder.test(normalized)) {
168
+ matched = !rule.negate;
169
+ }
170
+ }
171
+ }
172
+ return matched;
173
+ }
174
+ function buildRegex(rule) {
175
+ const body = globToRegexSource(rule.pattern);
176
+ if (rule.anchored) {
177
+ return body;
178
+ }
179
+ // Unanchored pattern matches anywhere in the tree. Allow a
180
+ // segment-aligned match by accepting either start-of-string or a
181
+ // `/` boundary before the pattern body.
182
+ return '(?:.*/)?' + body;
183
+ }
184
+ /**
185
+ * Walk `repoRoot` and return every git-ignored file path that matches
186
+ * the include rules. Tracked files are excluded because they ride
187
+ * along with git automatically.
188
+ *
189
+ * Implementation note: rather than re-implement gitignore evaluation
190
+ * the default lister shells out to
191
+ * `git ls-files --others --ignored --exclude-standard` to get the
192
+ * authoritative ignored-untracked list. Tracked files never appear in
193
+ * that output, so the result is exactly the safe-to-copy candidate
194
+ * pool.
195
+ */
196
+ export function collectWorktreeIncludeFiles(repoRoot, rules, options = {}) {
197
+ if (rules.length === 0)
198
+ return [];
199
+ const lister = options.listIgnored ?? defaultListIgnored;
200
+ const stat = options.isDirectory ?? defaultIsDirectory;
201
+ const candidates = lister(resolve(repoRoot));
202
+ const out = [];
203
+ for (const candidate of candidates) {
204
+ const rel = candidate.replace(/\\+/g, '/').replace(/^\.\//, '');
205
+ if (rel.length === 0)
206
+ continue;
207
+ if (rel.startsWith('.pugi/') || rel.startsWith('.claude/')) {
208
+ // Never copy Pugi or .claude state into the worktree. Those
209
+ // dirs are owned by the runtime and the include file should
210
+ // not be able to leak audit logs or stale session state.
211
+ continue;
212
+ }
213
+ const absPath = resolve(repoRoot, rel);
214
+ const isDir = stat(absPath);
215
+ if (matchesWorktreeInclude(rel, rules, isDir)) {
216
+ out.push(rel);
217
+ }
218
+ }
219
+ return out;
220
+ }
221
+ function defaultListIgnored(repoRoot) {
222
+ // `-z` emits NUL-separated paths, the only safe form when filenames
223
+ // may contain spaces or newlines.
224
+ const result = spawnSync('git', ['ls-files', '--others', '--ignored', '--exclude-standard', '-z'], {
225
+ cwd: repoRoot,
226
+ encoding: 'buffer',
227
+ shell: false,
228
+ maxBuffer: 64 * 1024 * 1024,
229
+ });
230
+ if (result.status !== 0 || !result.stdout)
231
+ return [];
232
+ const text = result.stdout.toString('utf8');
233
+ return text.split('\u0000').filter((s) => s.length > 0);
234
+ }
235
+ function defaultIsDirectory(absPath) {
236
+ try {
237
+ return statSync(absPath).isDirectory();
238
+ }
239
+ catch {
240
+ return false;
241
+ }
242
+ }
243
+ /** Cross-platform path-join in posix form (for matching semantics). */
244
+ export function joinRel(parent, child) {
245
+ return posix.join(parent.replace(/\\+/g, '/'), child);
246
+ }
247
+ /** Re-export the system separator so consumers do not have to import it. */
248
+ export const PATH_SEP = sep;
249
+ //# sourceMappingURL=include-parser.js.map
package/dist/index.js CHANGED
@@ -1,6 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { runCli } from './runtime/cli.js';
3
3
  import { PugiCliUpgradeRequiredError } from './core/transport/version-interceptor.js';
4
+ import { installSigintGuard } from './runtime/sigint-guard.js';
5
+ // PUGI-469 — install the top-level double-press Ctrl+C exit guard
6
+ // BEFORE any other init. Operators reported single ^C exiting the
7
+ // CLI; the guard requires a second press inside a 2s window (or
8
+ // emits a clean session-end envelope on the headless path) so a
9
+ // stray keystroke never kills the session. See the module header
10
+ // in `runtime/sigint-guard.ts` for the full behavior spec.
11
+ installSigintGuard();
4
12
  runCli(process.argv.slice(2)).catch((error) => {
5
13
  // PR-CLI-SERVER-VERSION-HANDSHAKE . When the admin-api returns
6
14
  // 426 Upgrade Required, the engine transport throws a typed
@@ -49,7 +49,7 @@ import { runUndoCommand } from './commands/undo.js';
49
49
  import { runCompactCommand } from './commands/compact.js';
50
50
  import { runRewindCommand } from './commands/rewind.js';
51
51
  import { runSessionsCommand } from './commands/sessions.js';
52
- // Day 4 ADR-0063: persona-memory operator surface (list / recall / write /
52
+ // Day 4 : persona-memory operator surface (list / recall / write /
53
53
  // forget / sync). The runner is shared by `pugi memory` top-level and the
54
54
  // in-REPL `/memory` slash so the two surfaces stay single-sourced.
55
55
  import { runMemoryCommand } from './commands/memory.js';
@@ -64,7 +64,7 @@ import { runRecipeCommand } from './commands/recipe.js';
64
64
  import { installDefaultSkills } from '../core/skills/defaults.js';
65
65
  // Backlog : bundled-skills batch 1 (stuck / simplify /
66
66
  // remember). Backlog : batch 2 (batch / verify / loop /
67
- // skillify, Nuekkis clean-room).
67
+ // skillify, external independent implementation).
68
68
  // Imported through the dedicated registry so future batches only append
69
69
  // to one barrel.
70
70
  import { runRememberCommand, runSimplifyCommand, runStuckCommand, runBatchCommand, runVerifyCommand, runLoopCommand, runSkillifyCommand, } from '../skills/bundled/index.js';
@@ -151,7 +151,7 @@ const handlers = {
151
151
  logout,
152
152
  lsp: dispatchLsp,
153
153
  mcp: dispatchMcp,
154
- // ADR-0063 Day 4: `pugi memory list|recall|write|forget|sync`. Routes
154
+ // Day 4: `pugi memory list|recall|write|forget|sync`. Routes
155
155
  // to `runMemoryCommand` (admin-api `/api/persona-memory` + offline
156
156
  // queue at `~/.pugi/memory-queue.jsonl`).
157
157
  memory: dispatchMemory,
@@ -233,7 +233,7 @@ const handlers = {
233
233
  // when no approval gate is wired. The existing `pugi memory write`
234
234
  // surface keeps its silent-enqueue behaviour for back-compat.
235
235
  remember: dispatchRemember,
236
- // Backlog — bundled-skills batch 2 (Nuekkis clean-room).
236
+ // Backlog — bundled-skills batch 2 (external independent implementation).
237
237
  // `pugi batch` fans out a YAML recipe of independent engine tasks
238
238
  // through up to --concurrency=N subprocesses (hard cap 30 per the
239
239
  // Mac safety carve-out). Aggregates results into
@@ -447,7 +447,7 @@ async function dispatchPrivacy(args, flags, _session) {
447
447
  });
448
448
  }
449
449
  /**
450
- * ADR-0063 Day 4 — `pugi memory <sub>` top-level dispatcher.
450
+ * Day 4 — `pugi memory <sub>` top-level dispatcher.
451
451
  *
452
452
  * Forwards to the shared `runMemoryCommand` runner. Exit codes:
453
453
  *
@@ -1596,9 +1596,75 @@ export async function runCli(argv) {
1596
1596
  // because the welcome banner is the authoritative surface — a stray
1597
1597
  // stderr line above the alt-screen flicker would race against the
1598
1598
  // banner paint on slow terminals.
1599
+ // PUGI-487 - `pugi --worktree [<name>]` dispatch. When the flag is
1600
+ // present on a bare invocation we create an isolated git worktree at
1601
+ // `.claude/worktrees/<name>/` and chdir into it BEFORE the REPL mounts.
1602
+ // The flag is mutually exclusive with `--print` and `--headless`
1603
+ // (already short-circuited above) and with `--bare` (which disables
1604
+ // every project-discovery surface the worktree relies on). We refuse
1605
+ // those combinations with a clear error rather than silently dropping
1606
+ // the flag.
1607
+ if (typeof flags.worktree === 'string' && isBareInvocation) {
1608
+ if (flags.bare) {
1609
+ process.stderr.write('--worktree cannot be combined with --bare; bare mode disables project discovery.\n');
1610
+ process.exitCode = 2;
1611
+ return;
1612
+ }
1613
+ const { bootstrapWorktree } = await import('./worktree-bootstrap.js');
1614
+ // PUGI-487 codex P2: honour `worktree.baseRef` from .pugi/settings.json
1615
+ // so an operator who set `head` (carry unpushed local work into the
1616
+ // new tree) actually gets that behaviour. The schema is optional;
1617
+ // `loadSettings` never throws so a bad/missing file degrades to the
1618
+ // default `fresh` branching.
1619
+ let configuredBaseRef;
1620
+ try {
1621
+ const { loadSettings } = await import('../core/settings.js');
1622
+ const settings = loadSettings(process.cwd());
1623
+ configuredBaseRef = settings.worktree?.baseRef;
1624
+ }
1625
+ catch {
1626
+ // best-effort: a malformed settings file falls back to the
1627
+ // bootstrap's own default rather than aborting the worktree.
1628
+ }
1629
+ const envelope = await bootstrapWorktree({
1630
+ repoRoot: process.cwd(),
1631
+ nameArg: flags.worktree.length === 0 ? undefined : flags.worktree,
1632
+ ...(configuredBaseRef ? { baseRef: configuredBaseRef } : {}),
1633
+ });
1634
+ if (!envelope.ok) {
1635
+ if (flags.json) {
1636
+ process.stdout.write(JSON.stringify(envelope) + '\n');
1637
+ }
1638
+ else {
1639
+ process.stderr.write(`pugi --worktree failed: ${envelope.message ?? envelope.reason ?? 'unknown error'}\n`);
1640
+ }
1641
+ process.exitCode = envelope.reason === 'untrusted_workspace' ? 3 : 1;
1642
+ return;
1643
+ }
1644
+ // chdir into the new tree so the REPL boot below treats it as the
1645
+ // workspace root. The flag does not change the rest of the bare-
1646
+ // invocation flow except for the cwd.
1647
+ try {
1648
+ process.chdir(envelope.directory);
1649
+ }
1650
+ catch (error) {
1651
+ const message = error instanceof Error ? error.message : String(error);
1652
+ process.stderr.write(`failed to enter worktree ${envelope.directory}: ${message}\n`);
1653
+ process.exitCode = 1;
1654
+ return;
1655
+ }
1656
+ if (flags.json) {
1657
+ process.stdout.write(JSON.stringify(envelope) + '\n');
1658
+ }
1659
+ else {
1660
+ process.stderr.write(`pugi --worktree ${envelope.name} -> ${envelope.directory} (branch ${envelope.branch}, copied ${envelope.copiedFiles.length} include file(s))\n`);
1661
+ }
1662
+ // Fall through to the bare-REPL branch below so the REPL mounts
1663
+ // inside the freshly-checked-out worktree.
1664
+ }
1599
1665
  // Bare `pugi` on a TTY enters the REPL-by-default agentic session
1600
- // (Sprint , ADR-0056). The REPL is the customer-facing surface
1601
- // that brings Pugi to parity with the upstream tool / Codex CLI. When the
1666
+ // (Sprint , ). The REPL is the customer-facing surface
1667
+ // that brings Pugi to parity with the upstream tool / peer CLI. When the
1602
1668
  // operator has no credentials yet, we fall back to the splash
1603
1669
  // so the install-time `pugi` surface still shows the wordmark +
1604
1670
  // quick-start hints. Non-TTY (CI, pipes, `--no-tty`) also falls
@@ -1824,7 +1890,7 @@ function parseArgs(argv) {
1824
1890
  }
1825
1891
  else if (arg === '--council') {
1826
1892
  // Backlog — opt-in council mode (Karpathy llm-council
1827
- // pattern, MIT clean-room TS port). Pairs with `--triple
1893
+ // pattern, MIT independent implementation TS port). Pairs with `--triple
1828
1894
  // --commit <SHA>`; the dispatch wraps the multi-provider
1829
1895
  // fan-out with an anonymous peer-review stage + chairman
1830
1896
  // synthesis. Costs ~2× tokens; explicit opt-in only.
@@ -2134,6 +2200,25 @@ function parseArgs(argv) {
2134
2200
  else if (arg.startsWith('--filter=')) {
2135
2201
  flags.smokeFilter = arg.slice('--filter='.length);
2136
2202
  }
2203
+ else if (arg === '--worktree') {
2204
+ // PUGI-487 - user-facing parallel-isolation flag. Consumes
2205
+ // the next token as the name unless it starts with `--`
2206
+ // (treat as missing => auto-generate). The empty string is
2207
+ // the sentinel for "flag present without value".
2208
+ const next = argv[index + 1];
2209
+ if (typeof next === 'string' && next.length > 0 && !next.startsWith('--')) {
2210
+ flags.worktree = next;
2211
+ index += 1;
2212
+ }
2213
+ else {
2214
+ flags.worktree = '';
2215
+ }
2216
+ }
2217
+ else if (arg.startsWith('--worktree=')) {
2218
+ // PUGI-487 - `--worktree=<name>` form. Empty value also
2219
+ // triggers the auto-name path.
2220
+ flags.worktree = arg.slice('--worktree='.length);
2221
+ }
2137
2222
  else {
2138
2223
  args.push(arg);
2139
2224
  }
@@ -4101,7 +4186,7 @@ async function performTripleProviderReview(root, session, flags, prompt) {
4101
4186
  // Server-side the controller also accepts `?council=true` query
4102
4187
  // string + `mode: 'council'` body shorthand; the CLI sends the
4103
4188
  // explicit boolean for forward compatibility. Inspired by
4104
- // Karpathy llm-council pattern (MIT). Clean-room TS port.
4189
+ // Karpathy llm-council pattern (MIT). independent implementation TS port.
4105
4190
  ...(flags.council ? { council: true } : {}),
4106
4191
  });
4107
4192
  writeFileSync(requestPath, `${JSON.stringify(requestBody, null, 2)}\n`, {
@@ -4961,6 +5046,38 @@ export function setHeadlessWriters(writers) {
4961
5046
  headlessStdoutWriter = writers.stdout ?? null;
4962
5047
  headlessStderrWriter = writers.stderr ?? null;
4963
5048
  }
5049
+ /**
5050
+ * PUGI-260 — read the persisted `contextTier` default из
5051
+ * `~/.pugi/config.json` (written by `pugi config set context.tier 1m`).
5052
+ * Returns `undefined` when the config file is missing, the key is
5053
+ * unset, or the value is invalid. Failures are silent — a malformed
5054
+ * persisted config must NEVER break the per-invocation dispatch path;
5055
+ * the flag default just falls back к "no preference" and the request
5056
+ * goes out on the standard lane.
5057
+ *
5058
+ * Kept inline (rather than importing from `commands/config.ts`) so the
5059
+ * dispatch path does not pull в the full `pugi config` command tree
5060
+ * during a `pugi code "..."` cold start — the import graph stays narrow.
5061
+ */
5062
+ function readPersistedContextTier() {
5063
+ try {
5064
+ const home = process.env.PUGI_HOME ?? resolve(homedir(), '.pugi');
5065
+ const path = resolve(home, 'config.json');
5066
+ if (!existsSync(path))
5067
+ return undefined;
5068
+ const raw = readFileSync(path, 'utf8');
5069
+ if (raw.trim() === '')
5070
+ return undefined;
5071
+ const parsed = JSON.parse(raw);
5072
+ if (parsed.contextTier === '1m' || parsed.contextTier === 'standard') {
5073
+ return parsed.contextTier;
5074
+ }
5075
+ return undefined;
5076
+ }
5077
+ catch {
5078
+ return undefined;
5079
+ }
5080
+ }
4964
5081
  function runEngineTask(kind) {
4965
5082
  return async (args, flags, session) => {
4966
5083
  const label = commandLabel(kind);
@@ -5250,18 +5367,29 @@ function runEngineTask(kind) {
5250
5367
  // point); `allowParallelAgents=false` strips the `agent` tool from
5251
5368
  // the schema so quick / standard tiers cannot accidentally fan out.
5252
5369
  intensityProfile,
5253
- // Task — 1M context tier opt-in. The operator passes
5370
+ // PUGI-260 — 1M context tier opt-in. The operator passes
5254
5371
  // `--context-tier=1m` to request the long-context lane. Server
5255
- // enforces Team-tier ($199) entitlement and rewrites the model к
5256
- // the `pugi-1m` alias. Lower tiers receive HTTP 402 with an
5257
- // upgrade-path message rendered by the dispatch error handler.
5258
- // `flags.contextTier` is intentionally NOT defaulted к 'standard'
5259
- // here `undefined` and `'standard'` are wire-equivalent (admin-
5260
- // api treats both as no-op), so the wire stays clean for callers
5261
- // that omit the flag entirely.
5262
- ...(flags.contextTier !== undefined
5263
- ? { contextTier: flags.contextTier }
5264
- : {}),
5372
+ // enforces Builder ($99) или Team ($199) entitlement и rewrites
5373
+ // the model к the `pugi-1m` alias. Lower tiers receive HTTP 402
5374
+ // with `reason: 'context_tier_requires_upgrade'` + the
5375
+ // `X-Pugi-Quota-Exceeded: context_tier_requires_upgrade` response
5376
+ // header rendered by the dispatch error handler.
5377
+ //
5378
+ // Resolution order:
5379
+ // 1. `--context-tier=1m|standard` flag (per-invocation override).
5380
+ // 2. `~/.pugi/config.json::contextTier` (persistent default,
5381
+ // set via `pugi config set context.tier 1m`).
5382
+ // 3. Omitted on the wire — admin-api treats omitted == standard.
5383
+ //
5384
+ // `'standard'` is wire-equivalent to omitted (the gate short-
5385
+ // circuits when `request.contextTier !== '1m'`), so the wire stays
5386
+ // clean for callers that omit the flag entirely.
5387
+ ...(() => {
5388
+ const effectiveTier = flags.contextTier ?? readPersistedContextTier();
5389
+ return effectiveTier !== undefined
5390
+ ? { contextTier: effectiveTier }
5391
+ : {};
5392
+ })(),
5265
5393
  });
5266
5394
  const toolCallId = recordToolCall(session, `engine:${adapter.name}`, `${label}: ${prompt}`);
5267
5395
  const taskId = `${kind}-${Date.now()}`;
@@ -5821,7 +5949,7 @@ function parseProviderFlag(args) {
5821
5949
  * `--json` / `--no-tty` / CI markers were not supplied. We only
5822
5950
  * prompt or render Ink surfaces when a human is plausibly watching
5823
5951
  * the screen. The multi-condition gate matches the upstream tool, gh CLI,
5824
- * Codex CLI, and the npm CLI conventions.
5952
+ * peer CLI, and the npm CLI conventions.
5825
5953
  */
5826
5954
  function isInteractive(flags) {
5827
5955
  if (flags.json)
@@ -5980,7 +6108,7 @@ async function login(args, flags, _session) {
5980
6108
  * Render the interactive Ink picker shown when `pugi login` runs on
5981
6109
  * a TTY with no token args. Returns the chosen provider, or `null`
5982
6110
  * when the user dismisses the picker via Esc / q. Mirrors the
5983
- * the upstream tool / Codex CLI auth picker UX.
6111
+ * the upstream tool / peer CLI auth picker UX.
5984
6112
  *
5985
6113
  * The Ink import is dynamic so a non-interactive `pugi <anything>`
5986
6114
  * never pays the React+Ink module-load cost. ESM dynamic-import is
@@ -7111,21 +7239,36 @@ function notImplemented(command) {
7111
7239
  }
7112
7240
  function ensurePugiGitIgnore(cwd, created, skipped) {
7113
7241
  const gitignorePath = resolve(cwd, '.gitignore');
7114
- const marker = '.pugi/';
7242
+ // PUGI-487 - also ensure `.claude/worktrees/` is git-ignored so the
7243
+ // user-facing `pugi --worktree` flag does not surface its created
7244
+ // trees as untracked status noise.
7245
+ const markers = ['.pugi/', '.claude/worktrees/'];
7115
7246
  if (!existsSync(gitignorePath)) {
7116
- writeFileSync(gitignorePath, `${marker}\n`, { encoding: 'utf8', mode: 0o600 });
7247
+ writeFileSync(gitignorePath, `${markers.join('\n')}\n`, { encoding: 'utf8', mode: 0o600 });
7117
7248
  created.push(gitignorePath);
7118
7249
  return;
7119
7250
  }
7120
7251
  const current = readFileSync(gitignorePath, 'utf8');
7121
7252
  const lines = current.split('\n').map((line) => line.trim());
7122
- if (lines.includes(marker) || lines.includes('/.pugi/') || lines.includes('.pugi')) {
7253
+ const equivalents = {
7254
+ '.pugi/': ['.pugi/', '/.pugi/', '.pugi'],
7255
+ '.claude/worktrees/': ['.claude/worktrees/', '/.claude/worktrees/', '.claude/worktrees'],
7256
+ };
7257
+ const toAppend = [];
7258
+ for (const marker of markers) {
7259
+ const eq = equivalents[marker] ?? [marker];
7260
+ const present = eq.some((variant) => lines.includes(variant));
7261
+ if (!present)
7262
+ toAppend.push(marker);
7263
+ }
7264
+ if (toAppend.length === 0) {
7123
7265
  skipped.push(gitignorePath);
7124
7266
  return;
7125
7267
  }
7126
- const next = current.endsWith('\n') ? `${current}${marker}\n` : `${current}\n${marker}\n`;
7268
+ const trailing = current.endsWith('\n') ? '' : '\n';
7269
+ const next = `${current}${trailing}${toAppend.join('\n')}\n`;
7127
7270
  writeFileSync(gitignorePath, next, { encoding: 'utf8' });
7128
- created.push(`${gitignorePath} (+${marker})`);
7271
+ created.push(`${gitignorePath} (+${toAppend.join(', ')})`);
7129
7272
  }
7130
7273
  /**
7131
7274
  * Compute the workspace label surfaced in the REPL header bar
@@ -7810,7 +7953,12 @@ export const __test__ = {
7810
7953
  // Backlog — `parseArgs` exposed under the test namespace so the
7811
7954
  // council-flag spec can assert flag-to-CliFlags mapping without
7812
7955
  // standing up a full execFileSync harness. Inspired by Karpathy
7813
- // llm-council pattern (MIT). Clean-room TS port.
7956
+ // llm-council pattern (MIT). independent implementation TS port.
7814
7957
  parseArgs,
7958
+ // PUGI-260 — exposed под the test namespace so the persisted-
7959
+ // contextTier-fallback spec can exercise the actual reader (с its
7960
+ // env-driven $PUGI_HOME redirect) without going через the full
7961
+ // engine-task dispatch path.
7962
+ readPersistedContextTier,
7815
7963
  };
7816
7964
  //# sourceMappingURL=cli.js.map
@@ -94,7 +94,7 @@ async function runAgentsList(args, ctx) {
94
94
  return;
95
95
  }
96
96
  if (verdicts.length === 0) {
97
- ctx.writeOutput({ command: 'agents.list', agents: [] }, 'No agents installed. Try `pugi agents install gh:anthropics/claude-code-agents/code-reviewer.md@main`.');
97
+ ctx.writeOutput({ command: 'agents.list', agents: [] }, 'No agents installed. Try `pugi agents install gh:example-org/coding-agents/code-reviewer.md@main`.');
98
98
  return;
99
99
  }
100
100
  const lines = ['Installed agents:'];