@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.
- package/CHANGELOG.md +36 -0
- package/LICENSE +1 -1
- package/dist/core/agents/registry.js +1 -1
- package/dist/core/auth/env-provider.js +1 -1
- package/dist/core/checkpoints/shadow-git.js +1 -1
- package/dist/core/context/compaction.js +1 -1
- package/dist/core/context/markdown-traverse.js +1 -1
- package/dist/core/credentials.js +1 -1
- package/dist/core/denial-tracking/state.js +1 -1
- package/dist/core/edits/fuzzy-ladder.js +1 -1
- package/dist/core/edits/layer-a-fuzzy-apply.js +1 -1
- package/dist/core/engine/anvil-client.js +76 -2
- package/dist/core/engine/native-pugi.js +1 -1
- package/dist/core/engine/tool-bridge.js +436 -0
- package/dist/core/hooks/events.js +3 -1
- package/dist/core/hooks/registry.js +3 -0
- package/dist/core/hooks/worktree-events.js +158 -0
- package/dist/core/lsp/client.js +453 -0
- package/dist/core/lsp/server-detect.js +173 -0
- package/dist/core/lsp/symbol-cache.js +162 -0
- package/dist/core/lsp/symbol-tools.js +296 -4
- package/dist/core/mcp/server-tools.js +1 -1
- package/dist/core/mcp/server.js +1 -1
- package/dist/core/memory/secret-scanner.js +6 -6
- package/dist/core/onboarding/ensure-initialized.js +1 -1
- package/dist/core/plans/plan-artifact.js +2 -2
- package/dist/core/repl/ask.js +1 -1
- package/dist/core/repl/cap-warning.js +1 -1
- package/dist/core/repl/session.js +3 -3
- package/dist/core/repl/slash-commands.js +1 -1
- package/dist/core/routing/pre-flight-estimator.js +1 -1
- package/dist/core/settings.js +38 -0
- package/dist/core/worktree/include-parser.js +249 -0
- package/dist/index.js +8 -0
- package/dist/runtime/cli.js +176 -28
- package/dist/runtime/commands/agents.js +1 -1
- package/dist/runtime/commands/config.js +41 -7
- package/dist/runtime/commands/hooks.js +3 -0
- package/dist/runtime/commands/review-consensus.js +1 -1
- package/dist/runtime/sigint-guard.js +272 -0
- package/dist/runtime/version.js +1 -1
- package/dist/runtime/worktree-bootstrap.js +579 -0
- package/dist/skills/bundled/batch.js +2 -2
- package/dist/skills/bundled/index.js +3 -3
- package/dist/skills/bundled/loop.js +2 -2
- package/dist/skills/bundled/remember.js +1 -1
- package/dist/skills/bundled/simplify.js +1 -1
- package/dist/skills/bundled/skillify.js +2 -2
- package/dist/skills/bundled/stuck.js +1 -1
- package/dist/skills/bundled/verify.js +2 -2
- package/dist/testing/vcr.js +2 -2
- package/dist/tools/ask-user-question.js +66 -0
- package/dist/tools/bash.js +2 -2
- package/dist/tools/lsp-tools.js +377 -1
- package/dist/tools/powershell.js +1 -1
- package/dist/tools/registry.js +23 -0
- package/dist/tui/ask-user-question-chips.js +257 -0
- package/dist/tui/input-box.js +1 -1
- package/dist/tui/render.js +1 -1
- package/dist/tui/repl.js +1 -1
- package/dist/tui/status-bar.js +1 -1
- package/dist/tui/update-banner.js +1 -1
- package/dist/tui/welcome-data.js +4 -4
- package/package.json +4 -3
- package/test/scenarios/compact-force.scenario.txt +3 -2
- package/test/scenarios/identity.scenario.txt +6 -5
- package/test/scenarios/persona-handoff.scenario.txt +2 -1
- package/test/scenarios/walkback.scenario.txt +6 -6
package/dist/core/settings.js
CHANGED
|
@@ -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
|
package/dist/runtime/cli.js
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
-
//
|
|
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 (
|
|
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
|
-
*
|
|
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 ,
|
|
1601
|
-
// that brings Pugi to parity with the upstream tool /
|
|
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
|
|
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).
|
|
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
|
-
//
|
|
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
|
|
5256
|
-
// the `pugi-1m` alias. Lower tiers receive HTTP 402
|
|
5257
|
-
//
|
|
5258
|
-
// `
|
|
5259
|
-
//
|
|
5260
|
-
//
|
|
5261
|
-
//
|
|
5262
|
-
|
|
5263
|
-
|
|
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
|
-
*
|
|
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 /
|
|
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
|
-
|
|
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, `${
|
|
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
|
-
|
|
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
|
|
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} (+${
|
|
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).
|
|
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:
|
|
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:'];
|