@pugi/cli 0.1.0-beta.88 → 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 (37) hide show
  1. package/dist/core/auth/env-provider.js +1 -1
  2. package/dist/core/context/markdown-traverse.js +1 -1
  3. package/dist/core/credentials.js +1 -1
  4. package/dist/core/engine/anvil-client.js +63 -0
  5. package/dist/core/engine/native-pugi.js +1 -1
  6. package/dist/core/engine/tool-bridge.js +436 -0
  7. package/dist/core/hooks/events.js +3 -1
  8. package/dist/core/hooks/registry.js +3 -0
  9. package/dist/core/hooks/worktree-events.js +158 -0
  10. package/dist/core/lsp/client.js +453 -0
  11. package/dist/core/lsp/server-detect.js +173 -0
  12. package/dist/core/lsp/symbol-cache.js +162 -0
  13. package/dist/core/lsp/symbol-tools.js +296 -4
  14. package/dist/core/mcp/server.js +1 -1
  15. package/dist/core/repl/ask.js +1 -1
  16. package/dist/core/repl/session.js +3 -3
  17. package/dist/core/repl/slash-commands.js +1 -1
  18. package/dist/core/settings.js +26 -0
  19. package/dist/core/worktree/include-parser.js +249 -0
  20. package/dist/runtime/cli.js +108 -8
  21. package/dist/runtime/commands/agents.js +1 -1
  22. package/dist/runtime/commands/hooks.js +3 -0
  23. package/dist/runtime/commands/review-consensus.js +1 -1
  24. package/dist/runtime/version.js +1 -1
  25. package/dist/runtime/worktree-bootstrap.js +579 -0
  26. package/dist/tools/lsp-tools.js +377 -1
  27. package/dist/tools/registry.js +23 -0
  28. package/dist/tui/input-box.js +1 -1
  29. package/dist/tui/render.js +1 -1
  30. package/dist/tui/repl.js +1 -1
  31. package/dist/tui/status-bar.js +1 -1
  32. package/dist/tui/update-banner.js +1 -1
  33. package/package.json +3 -3
  34. package/test/scenarios/compact-force.scenario.txt +3 -2
  35. package/test/scenarios/identity.scenario.txt +6 -5
  36. package/test/scenarios/persona-handoff.scenario.txt +2 -1
  37. package/test/scenarios/walkback.scenario.txt +6 -6
@@ -2019,7 +2019,7 @@ export class ReplSession {
2019
2019
  // Surface the operator's choice as a transcript row so the
2020
2020
  // conversation reads linearly. The label of the chosen option
2021
2021
  // (or the literal custom input) is more readable than the bare
2022
- // value - Codex CLI's "you chose: Vercel" pattern.
2022
+ // value - peer CLI's "you chose: Vercel" pattern.
2023
2023
  const humanLabel = humanLabelForVerdict(tag, sanitisedVerdict);
2024
2024
  this.appendOperatorLine(humanLabel);
2025
2025
  // Local-origin modals (operator typed `/ask`) never need an
@@ -4105,7 +4105,7 @@ export function colorizeQuotaRow(row, pct) {
4105
4105
  * string and emit a synthesised `ToolCallEntry`. Returns null when no
4106
4106
  * known tool pattern matches.
4107
4107
  *
4108
- * The grammar mirrors the way the upstream tool, Codex CLI, and Gemini CLI
4108
+ * The grammar mirrors the way the upstream tool, peer CLI, and Gemini CLI
4109
4109
  * display tool calls in their tool stream panes:
4110
4110
  *
4111
4111
  * Read(path)
@@ -4293,7 +4293,7 @@ function encodePlanReviewVerdictLocal(result) {
4293
4293
  }
4294
4294
  /**
4295
4295
  * Compose the human-readable transcript line that records the
4296
- * operator's ask verdict. Mirrors Codex CLI's "you chose: <label>"
4296
+ * operator's ask verdict. Mirrors peer CLI's "you chose: <label>"
4297
4297
  * pattern so the conversation reads linearly.
4298
4298
  */
4299
4299
  function humanLabelForVerdict(tag, verdict) {
@@ -4,7 +4,7 @@
4
4
  * The REPL input box surfaces a palette of slash commands the operator
5
5
  * can run from inside a persistent session. The wave-2 expansion (CEO
6
6
  *) grows the surface from 6 to 20 commands so the `/help`
7
- * overlay matches the breadth the upstream tool / Codex CLI operators expect.
7
+ * overlay matches the breadth the upstream tool / peer CLI operators expect.
8
8
  *
9
9
  * The registry is pure: each `parseSlashCommand` call returns a
10
10
  * `SlashCommandResult` describing what the REPL session should do next.
@@ -177,6 +177,32 @@ const pugiSettingsSchema = z.object({
177
177
  tier: z.enum(['1m', 'standard']).optional(),
178
178
  })
179
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(),
180
206
  });
181
207
  /**
182
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
@@ -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
1666
  // (Sprint , ). The REPL is the customer-facing surface
1601
- // that brings Pugi to parity with the upstream tool / Codex CLI. When the
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
@@ -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
  }
@@ -5864,7 +5949,7 @@ function parseProviderFlag(args) {
5864
5949
  * `--json` / `--no-tty` / CI markers were not supplied. We only
5865
5950
  * prompt or render Ink surfaces when a human is plausibly watching
5866
5951
  * the screen. The multi-condition gate matches the upstream tool, gh CLI,
5867
- * Codex CLI, and the npm CLI conventions.
5952
+ * peer CLI, and the npm CLI conventions.
5868
5953
  */
5869
5954
  function isInteractive(flags) {
5870
5955
  if (flags.json)
@@ -6023,7 +6108,7 @@ async function login(args, flags, _session) {
6023
6108
  * Render the interactive Ink picker shown when `pugi login` runs on
6024
6109
  * a TTY with no token args. Returns the chosen provider, or `null`
6025
6110
  * when the user dismisses the picker via Esc / q. Mirrors the
6026
- * the upstream tool / Codex CLI auth picker UX.
6111
+ * the upstream tool / peer CLI auth picker UX.
6027
6112
  *
6028
6113
  * The Ink import is dynamic so a non-interactive `pugi <anything>`
6029
6114
  * never pays the React+Ink module-load cost. ESM dynamic-import is
@@ -7154,21 +7239,36 @@ function notImplemented(command) {
7154
7239
  }
7155
7240
  function ensurePugiGitIgnore(cwd, created, skipped) {
7156
7241
  const gitignorePath = resolve(cwd, '.gitignore');
7157
- 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/'];
7158
7246
  if (!existsSync(gitignorePath)) {
7159
- writeFileSync(gitignorePath, `${marker}\n`, { encoding: 'utf8', mode: 0o600 });
7247
+ writeFileSync(gitignorePath, `${markers.join('\n')}\n`, { encoding: 'utf8', mode: 0o600 });
7160
7248
  created.push(gitignorePath);
7161
7249
  return;
7162
7250
  }
7163
7251
  const current = readFileSync(gitignorePath, 'utf8');
7164
7252
  const lines = current.split('\n').map((line) => line.trim());
7165
- 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) {
7166
7265
  skipped.push(gitignorePath);
7167
7266
  return;
7168
7267
  }
7169
- 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`;
7170
7270
  writeFileSync(gitignorePath, next, { encoding: 'utf8' });
7171
- created.push(`${gitignorePath} (+${marker})`);
7271
+ created.push(`${gitignorePath} (+${toAppend.join(', ')})`);
7172
7272
  }
7173
7273
  /**
7174
7274
  * Compute the workspace label surfaced in the REPL header bar
@@ -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:'];
@@ -101,6 +101,9 @@ function runList(ctx, flags) {
101
101
  SubagentStop: [],
102
102
  PreCompact: [],
103
103
  Notification: [],
104
+ // PUGI-487 - worktree lifecycle events.
105
+ WorktreeCreate: [],
106
+ WorktreeRemove: [],
104
107
  };
105
108
  for (const event of ALL_HOOK_EVENTS_V2) {
106
109
  perEvent[event] = config.list(event).map((entry) => ({
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `pugi review --consensus` — customer-facing triple-review .
3
3
  *
4
- * The differentiator: the upstream tool ships single-Claude review, Codex CLI
4
+ * The differentiator: the upstream tool ships single-Claude review, peer CLI
5
5
  * ships single-GPT review, Gemini CLI ships single-Gemini review. Pugi
6
6
  * ships a 3-model consensus gate as a first-class command so customers
7
7
  * get the same production-readiness signal we use internally - without the
@@ -44,7 +44,7 @@ export function sanitizeSemver(raw) {
44
44
  * during import). When bumping the CLI version BOTH literals must be
45
45
  * updated; the release smoke-test (`pack:smoke`) verifies they agree.
46
46
  */
47
- export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.88');
47
+ export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.89');
48
48
  /**
49
49
  * Outbound: the CLI's installed semver. Read at request time by
50
50
  * `version-interceptor.ts` and injected on every `fetch` call.