@pugi/cli 0.1.0-beta.3 → 0.1.0-beta.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/assets/pugi-mascot.ansi +15 -40
- package/bin/run.js +33 -1
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +15 -0
- package/dist/core/agent-progress/cleanup.js +134 -0
- package/dist/core/agent-progress/schema.js +144 -0
- package/dist/core/agent-progress/writer.js +101 -0
- package/dist/core/artifact-chain/dispatcher.js +148 -0
- package/dist/core/artifact-chain/exporter.js +164 -0
- package/dist/core/artifact-chain/state.js +243 -0
- package/dist/core/artifact-chain/steps.js +169 -0
- package/dist/core/auth/env-provider.js +238 -0
- package/dist/core/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -0
- package/dist/core/bare-mode/index.js +107 -0
- package/dist/core/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -0
- package/dist/core/compact/auto-trigger.js +96 -0
- package/dist/core/compact/buffer-rewriter.js +115 -0
- package/dist/core/compact/summarizer.js +208 -0
- package/dist/core/compact/token-counter.js +108 -0
- package/dist/core/consensus/diff-capture.js +73 -0
- package/dist/core/context/index.js +7 -0
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/cost/rate-card.js +129 -0
- package/dist/core/cost/tracker.js +221 -0
- package/dist/core/denial-tracking/index.js +8 -0
- package/dist/core/denial-tracking/state.js +264 -0
- package/dist/core/diagnostics/probe-runner.js +93 -0
- package/dist/core/diagnostics/probes/api.js +46 -0
- package/dist/core/diagnostics/probes/auth.js +86 -0
- package/dist/core/diagnostics/probes/bare-mode.js +42 -0
- package/dist/core/diagnostics/probes/cli-version.js +127 -0
- package/dist/core/diagnostics/probes/config.js +72 -0
- package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
- package/dist/core/diagnostics/probes/disk.js +81 -0
- package/dist/core/diagnostics/probes/git.js +65 -0
- package/dist/core/diagnostics/probes/mcp.js +75 -0
- package/dist/core/diagnostics/probes/node.js +59 -0
- package/dist/core/diagnostics/probes/pnpm.js +36 -0
- package/dist/core/diagnostics/probes/pugi-md.js +89 -0
- package/dist/core/diagnostics/probes/session.js +74 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +442 -0
- package/dist/core/diagnostics/probes/workspace.js +63 -0
- package/dist/core/diagnostics/types.js +70 -0
- package/dist/core/dispatch/cache-cleanup.js +197 -0
- package/dist/core/dispatch/cache-handoff.js +295 -0
- package/dist/core/edits/dispatch.js +218 -2
- package/dist/core/edits/journal.js +199 -0
- package/dist/core/edits/layer-d-ast.js +557 -14
- package/dist/core/edits/verify-hook.js +273 -0
- package/dist/core/edits/worktree.js +111 -18
- package/dist/core/engine/anvil-client.js +115 -5
- package/dist/core/engine/budgets.js +89 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +852 -210
- package/dist/core/engine/prompts.js +89 -6
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +972 -33
- package/dist/core/feedback/queue.js +177 -0
- package/dist/core/feedback/submitter.js +145 -0
- package/dist/core/file-cache.js +113 -1
- package/dist/core/hooks/events.js +44 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +213 -0
- package/dist/core/hooks/runner.js +236 -0
- package/dist/core/init/scaffold.js +195 -0
- package/dist/core/lsp/cache.js +105 -0
- package/dist/core/lsp/client.js +174 -29
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/mcp/client.js +75 -6
- package/dist/core/mcp/http-server.js +553 -0
- package/dist/core/mcp/permission.js +190 -0
- package/dist/core/mcp/registry.js +24 -2
- package/dist/core/mcp/server-tools.js +219 -0
- package/dist/core/mcp/server.js +397 -0
- package/dist/core/memory/dual-write.js +416 -0
- package/dist/core/memory/dual-write.spec.js +297 -0
- package/dist/core/memory/phase1-kinds.js +20 -0
- package/dist/core/memory-sync/queue.js +158 -0
- package/dist/core/memory-sync/queue.spec.js +105 -0
- package/dist/core/onboarding/marker.js +111 -0
- package/dist/core/onboarding/telemetry-state.js +108 -0
- package/dist/core/output-style/presets.js +176 -0
- package/dist/core/output-style/state.js +185 -0
- package/dist/core/permissions/gate.js +187 -0
- package/dist/core/permissions/index.js +18 -0
- package/dist/core/permissions/mode.js +102 -0
- package/dist/core/permissions/state.js +215 -0
- package/dist/core/permissions/tool-class.js +93 -0
- package/dist/core/prd-check/parser.js +215 -0
- package/dist/core/prd-check/reporter.js +127 -0
- package/dist/core/prd-check/verifiers.js +223 -0
- package/dist/core/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/codebase-survey.js +308 -0
- package/dist/core/repl/history.js +11 -1
- package/dist/core/repl/init-interview.js +457 -0
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/onboarding-state.js +297 -0
- package/dist/core/repl/session.js +1486 -30
- package/dist/core/repl/slash-commands.js +345 -9
- package/dist/core/repl/store/session-store.js +31 -2
- package/dist/core/repl/workspace-context.js +22 -0
- package/dist/core/repo-map/build.js +125 -0
- package/dist/core/repo-map/cache.js +185 -0
- package/dist/core/repo-map/extractor.js +254 -0
- package/dist/core/repo-map/formatter.js +145 -0
- package/dist/core/repo-map/scanner.js +211 -0
- package/dist/core/retry-budget/budget.js +284 -0
- package/dist/core/retry-budget/index.js +5 -0
- package/dist/core/session.js +44 -0
- package/dist/core/settings.js +80 -0
- package/dist/core/share/formatter.js +271 -0
- package/dist/core/share/redactor.js +221 -0
- package/dist/core/share/uploader.js +267 -0
- package/dist/core/skills/defaults.js +457 -0
- package/dist/core/subagents/dispatcher-real.js +600 -0
- package/dist/core/subagents/dispatcher.js +113 -24
- package/dist/core/subagents/index.js +18 -5
- package/dist/core/subagents/isolation-matrix.js +213 -0
- package/dist/core/subagents/spawn.js +19 -4
- package/dist/core/telemetry/emitter.js +229 -0
- package/dist/core/telemetry/queue.js +251 -0
- package/dist/core/theme/context.js +91 -0
- package/dist/core/theme/presets.js +228 -0
- package/dist/core/theme/state.js +181 -0
- package/dist/core/todos/invariant.js +10 -0
- package/dist/core/todos/state.js +177 -0
- package/dist/core/transport/version-interceptor.js +166 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/index.js +28 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +2595 -278
- package/dist/runtime/commands/chain.js +489 -0
- package/dist/runtime/commands/compact.js +297 -0
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +312 -0
- package/dist/runtime/commands/dispatch.js +126 -0
- package/dist/runtime/commands/doctor.js +390 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/hooks.js +184 -0
- package/dist/runtime/commands/lsp.js +212 -28
- package/dist/runtime/commands/mcp.js +824 -0
- package/dist/runtime/commands/memory.js +508 -0
- package/dist/runtime/commands/memory.spec.js +174 -0
- package/dist/runtime/commands/model.js +237 -0
- package/dist/runtime/commands/onboarding.js +275 -0
- package/dist/runtime/commands/patch.js +17 -0
- package/dist/runtime/commands/permissions.js +87 -0
- package/dist/runtime/commands/plan.js +143 -0
- package/dist/runtime/commands/prd-check.js +235 -0
- package/dist/runtime/commands/release-notes.js +229 -0
- package/dist/runtime/commands/repo-map.js +95 -0
- package/dist/runtime/commands/report.js +299 -0
- package/dist/runtime/commands/resume.js +118 -0
- package/dist/runtime/commands/review-consensus.js +17 -2
- package/dist/runtime/commands/rewind.js +333 -0
- package/dist/runtime/commands/roster.js +117 -0
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/commands/share.js +316 -0
- package/dist/runtime/commands/status.js +178 -0
- package/dist/runtime/commands/stickers.js +82 -0
- package/dist/runtime/commands/style.js +194 -0
- package/dist/runtime/commands/theme.js +196 -0
- package/dist/runtime/commands/update.js +289 -0
- package/dist/runtime/commands/vim.js +140 -0
- package/dist/runtime/commands/worktree.js +50 -6
- package/dist/runtime/headless.js +543 -0
- package/dist/runtime/load-hooks-or-exit.js +71 -0
- package/dist/runtime/plan-decompose.js +531 -0
- package/dist/runtime/version.js +65 -0
- package/dist/tools/agent-tool.js +229 -0
- package/dist/tools/apply-patch.js +281 -39
- package/dist/tools/ask-user-question.js +213 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/file-tools.js +85 -14
- package/dist/tools/mcp-tool.js +260 -0
- package/dist/tools/multi-edit.js +361 -0
- package/dist/tools/registry.js +30 -2
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tools/todo-write.js +184 -0
- package/dist/tools/web-fetch.js +147 -2
- package/dist/tools/web-search.js +458 -0
- package/dist/tui/agent-progress-card.js +111 -0
- package/dist/tui/agent-tree.js +10 -0
- package/dist/tui/ask-modal.js +2 -2
- package/dist/tui/ask-user-question-prompt.js +192 -0
- package/dist/tui/compact-banner.js +81 -0
- package/dist/tui/conversation-pane.js +82 -8
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +46 -2
- package/dist/tui/markdown-render.js +4 -4
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/repl-render.js +293 -35
- package/dist/tui/repl-splash.js +2 -2
- package/dist/tui/repl.js +45 -13
- package/dist/tui/splash.js +1 -1
- package/dist/tui/status-bar.js +94 -16
- package/dist/tui/status-table.js +7 -0
- package/dist/tui/stickers-art.js +136 -0
- package/dist/tui/style-table.js +28 -0
- package/dist/tui/theme-table.js +29 -0
- package/dist/tui/tool-stream-pane.js +7 -0
- package/dist/tui/update-banner.js +20 -2
- package/dist/tui/vim-input.js +267 -0
- package/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +9 -6
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-map scanner — Leak L28 (2026-05-27).
|
|
3
|
+
*
|
|
4
|
+
* Walks the workspace via `fs.readdirSync` (sync, depth-first), filters
|
|
5
|
+
* to a recognised set of source-language extensions, and applies the
|
|
6
|
+
* shared `PugiIgnore` matcher so the same exclusion rules used by the
|
|
7
|
+
* three-tier context skeleton also gate the repo-map.
|
|
8
|
+
*
|
|
9
|
+
* Why a stand-alone scanner (vs. reusing the α6.5 skeleton walker):
|
|
10
|
+
*
|
|
11
|
+
* 1. The skeleton walker emits a flat `IndexArtifact[]` of every
|
|
12
|
+
* ignore-respecting file (markdown, configs, schemas, etc.) for
|
|
13
|
+
* the working-set heuristic. The repo-map ONLY needs source
|
|
14
|
+
* files — markdown headings and JSON keys are not "definitions"
|
|
15
|
+
* in the L28 sense. Filtering downstream is cheap, but the
|
|
16
|
+
* scanner gets to short-circuit on extension before stat'ing
|
|
17
|
+
* the file, which matters for monorepos with thousands of
|
|
18
|
+
* non-source artefacts (lockfiles, schemas, fixtures).
|
|
19
|
+
*
|
|
20
|
+
* 2. We need mtime + size per file so `cache.ts` can invalidate
|
|
21
|
+
* stale entries without re-parsing. The skeleton walker
|
|
22
|
+
* surfaces only paths.
|
|
23
|
+
*
|
|
24
|
+
* 3. The L28 contract caps the walk at `MAX_SRC_FILES` (5000) and
|
|
25
|
+
* individual files at `MAX_FILE_BYTES` (200 KiB). When the cap
|
|
26
|
+
* trips the scanner returns a `{ skipped: 'too-large' }`
|
|
27
|
+
* verdict rather than partial data — the consumer must decide
|
|
28
|
+
* whether to fall back to a no-op map or surface a hint к the
|
|
29
|
+
* operator. Surfacing partial data would silently bias the
|
|
30
|
+
* injected summary toward whichever subtree the walker happened
|
|
31
|
+
* to traverse first.
|
|
32
|
+
*
|
|
33
|
+
* The output is sorted (POSIX path string compare) so two runs over
|
|
34
|
+
* the same workspace produce byte-identical `repo-map.json` caches —
|
|
35
|
+
* `cache.ts` relies on stable ordering for its hash-free freshness
|
|
36
|
+
* check. POSIX-style separators are used in `relPath` regardless of
|
|
37
|
+
* platform so the cache file stays portable.
|
|
38
|
+
*
|
|
39
|
+
* Pure module surface: no logging, no network. Errors during readdir
|
|
40
|
+
* on a single subtree (permission denied, symlink loop) are swallowed
|
|
41
|
+
* and the walker continues — repo-map is a best-effort context
|
|
42
|
+
* enrichment, never a gate.
|
|
43
|
+
*/
|
|
44
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
45
|
+
import { join, posix, relative, resolve, sep } from 'node:path';
|
|
46
|
+
/**
|
|
47
|
+
* Hard ceiling on total source files surfaced by a single scan. The
|
|
48
|
+
* engine context budget is the binding constraint — a 5K-file repo
|
|
49
|
+
* already overflows the 2K-token injection cap so going higher buys
|
|
50
|
+
* nothing but walker latency. Repos above the cap fall back к the
|
|
51
|
+
* `{ skipped: 'too-large' }` verdict.
|
|
52
|
+
*/
|
|
53
|
+
export const MAX_SRC_FILES = 5000;
|
|
54
|
+
/**
|
|
55
|
+
* Per-file size cap. Files larger than this are skipped — they are
|
|
56
|
+
* almost always generated (compiled JS, vendored libs, encoded blobs)
|
|
57
|
+
* and add noise without signal. The 200 KiB threshold mirrors the
|
|
58
|
+
* α6.5 skeleton walker's own `MAX_FILE_BYTES` so the two scans agree
|
|
59
|
+
* on "what counts as a source file".
|
|
60
|
+
*/
|
|
61
|
+
export const MAX_FILE_BYTES = 200 * 1024;
|
|
62
|
+
/**
|
|
63
|
+
* Source-language extensions the extractor knows how to parse. Adding
|
|
64
|
+
* a language here without a matching extractor branch is a silent
|
|
65
|
+
* no-op (the file shows up в the scan but extracts zero symbols);
|
|
66
|
+
* the spec asserts the symmetry so a future PR cannot drift the two
|
|
67
|
+
* lists out of sync.
|
|
68
|
+
*/
|
|
69
|
+
export const SUPPORTED_EXTENSIONS = Object.freeze([
|
|
70
|
+
'.ts',
|
|
71
|
+
'.tsx',
|
|
72
|
+
'.js',
|
|
73
|
+
'.jsx',
|
|
74
|
+
'.mjs',
|
|
75
|
+
'.cjs',
|
|
76
|
+
'.md',
|
|
77
|
+
'.mdx',
|
|
78
|
+
]);
|
|
79
|
+
const defaultReaddir = (path) => readdirSync(path, { withFileTypes: true });
|
|
80
|
+
const defaultStat = (path) => {
|
|
81
|
+
const s = statSync(path);
|
|
82
|
+
return { size: s.size, mtimeMs: s.mtimeMs };
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Walk the workspace once and return every source file the extractor
|
|
86
|
+
* is willing to parse. The function is deliberately synchronous —
|
|
87
|
+
* the underlying walks are CPU-bound, не I/O-bound, and the sync
|
|
88
|
+
* call avoids the promise overhead that dominates for thousands of
|
|
89
|
+
* small files. The L28 engine boot path runs this on a Node `setImmediate`
|
|
90
|
+
* so the main thread is not blocked.
|
|
91
|
+
*/
|
|
92
|
+
export function scanRepoForMap(options) {
|
|
93
|
+
const root = resolve(options.root);
|
|
94
|
+
const readdir = options.readdir ?? defaultReaddir;
|
|
95
|
+
const stat = options.stat ?? defaultStat;
|
|
96
|
+
const maxFiles = options.maxFiles ?? MAX_SRC_FILES;
|
|
97
|
+
const maxFileBytes = options.maxFileBytes ?? MAX_FILE_BYTES;
|
|
98
|
+
const ignore = options.ignore;
|
|
99
|
+
const files = [];
|
|
100
|
+
let walked = 0;
|
|
101
|
+
let skippedLarge = 0;
|
|
102
|
+
let skippedIgnored = 0;
|
|
103
|
+
let tooLarge = false;
|
|
104
|
+
/**
|
|
105
|
+
* Depth-first recursion. We push dirs into a manual stack instead of
|
|
106
|
+
* recursing in JS because deep monorepos (Nx with 100+ packages)
|
|
107
|
+
* have approached the v8 default stack limit on Windows runners
|
|
108
|
+
* before; an explicit stack is one less thing to debug.
|
|
109
|
+
*/
|
|
110
|
+
const stack = [root];
|
|
111
|
+
while (stack.length > 0) {
|
|
112
|
+
const dir = stack.pop();
|
|
113
|
+
let entries;
|
|
114
|
+
try {
|
|
115
|
+
entries = readdir(dir);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Permission denied / symlink loop / mid-flight delete — keep
|
|
119
|
+
// walking. Repo-map is best-effort context, never a gate.
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
const abs = join(dir, entry.name);
|
|
124
|
+
const isDir = entry.isDirectory();
|
|
125
|
+
if (ignore.isIgnored(abs, isDir)) {
|
|
126
|
+
skippedIgnored += 1;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (isDir) {
|
|
130
|
+
stack.push(abs);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (!entry.isFile()) {
|
|
134
|
+
// Symlinks, sockets, FIFOs etc. Skip silently — they are not
|
|
135
|
+
// source code and stat'ing them can throw on broken links.
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
walked += 1;
|
|
139
|
+
const ext = extOf(entry.name);
|
|
140
|
+
if (!SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
let statResult;
|
|
144
|
+
try {
|
|
145
|
+
statResult = stat(abs);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// File vanished between readdir and stat — skip.
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (statResult.size > maxFileBytes) {
|
|
152
|
+
skippedLarge += 1;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
// Workspace-relative POSIX path. `relative` returns the host
|
|
156
|
+
// separator on Windows; normalise to forward slashes so the
|
|
157
|
+
// cache file is portable.
|
|
158
|
+
const rel = relative(root, abs).split(sep).join(posix.sep);
|
|
159
|
+
files.push({
|
|
160
|
+
relPath: rel,
|
|
161
|
+
absPath: abs,
|
|
162
|
+
ext,
|
|
163
|
+
sizeBytes: statResult.size,
|
|
164
|
+
mtimeMs: statResult.mtimeMs,
|
|
165
|
+
});
|
|
166
|
+
if (files.length > maxFiles) {
|
|
167
|
+
tooLarge = true;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (tooLarge)
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
if (tooLarge) {
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
root,
|
|
178
|
+
skipped: {
|
|
179
|
+
reason: 'too-large',
|
|
180
|
+
walked,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
// Sort by POSIX path for stable cache output. Two runs over the
|
|
185
|
+
// same workspace yield byte-identical JSON so the cache hash check
|
|
186
|
+
// is a simple `mtime + size` per entry without a content digest.
|
|
187
|
+
files.sort((a, b) => (a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0));
|
|
188
|
+
return {
|
|
189
|
+
ok: true,
|
|
190
|
+
root,
|
|
191
|
+
files,
|
|
192
|
+
stats: {
|
|
193
|
+
walked,
|
|
194
|
+
kept: files.length,
|
|
195
|
+
skippedLarge,
|
|
196
|
+
skippedIgnored,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Lowercase extension including the leading dot, or '' when the
|
|
202
|
+
* filename has no extension. Mirrors `node:path.extname` semantics —
|
|
203
|
+
* inlined so the scanner has zero per-iteration call overhead.
|
|
204
|
+
*/
|
|
205
|
+
function extOf(name) {
|
|
206
|
+
const dot = name.lastIndexOf('.');
|
|
207
|
+
if (dot < 0 || dot === 0)
|
|
208
|
+
return '';
|
|
209
|
+
return name.slice(dot).toLowerCase();
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=scanner.js.map
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leak L31 — Per-command tool retry budget (Claude Code parity).
|
|
3
|
+
*
|
|
4
|
+
* Claude Code limits the number of times the model may retry the SAME
|
|
5
|
+
* tool with the SAME arguments inside a single operator-input cycle.
|
|
6
|
+
* Once the cap is hit, the dispatcher hard-refuses and surfaces a
|
|
7
|
+
* sentinel string telling the model that this exact call has exhausted
|
|
8
|
+
* its retry budget. The model is expected (via system-prompt rule) to
|
|
9
|
+
* either change approach or ask the operator for guidance instead of
|
|
10
|
+
* looping forever on a transient failure.
|
|
11
|
+
*
|
|
12
|
+
* Why per-cycle, not per-session: a retry budget that persists across
|
|
13
|
+
* operator turns would surprise the operator. After the operator says
|
|
14
|
+
* "try again" the model rightly retries; the budget must reset when a
|
|
15
|
+
* fresh brief arrives. The simplest reset boundary is the executor
|
|
16
|
+
* lifetime — `buildExecutor` is called once per `runEngineLoop` and
|
|
17
|
+
* the loop drives exactly one operator-input cycle. Constructing the
|
|
18
|
+
* budget inside `buildExecutor` therefore gives us per-cycle scoping
|
|
19
|
+
* "for free" via closure lifetime; no external clear() call is needed
|
|
20
|
+
* from production callsites. The exported `clear()` exists so tests
|
|
21
|
+
* and a future hook surface (PreToolUse) can introspect the state.
|
|
22
|
+
*
|
|
23
|
+
* Hash design: same tool + same canonical args = same bucket. We
|
|
24
|
+
* canonicalise the args record by sorting object keys (stable across
|
|
25
|
+
* model output ordering) and then sha256 the JSON. The model emits
|
|
26
|
+
* `arguments` as a raw JSON string; we parse, canonicalise, hash. If
|
|
27
|
+
* parse fails we hash the raw string verbatim — that way an
|
|
28
|
+
* unparseable repeat still counts toward the cap (otherwise the model
|
|
29
|
+
* could loop on syntactic noise variants forever).
|
|
30
|
+
*
|
|
31
|
+
* Env overrides:
|
|
32
|
+
* PUGI_RETRY_BUDGET_<TOOLNAME>=<N> — override a single tool's cap.
|
|
33
|
+
* Toolname matches DEFAULT_CAPS
|
|
34
|
+
* keys verbatim, uppercased
|
|
35
|
+
* (PUGI_RETRY_BUDGET_BASH=8).
|
|
36
|
+
* PUGI_RETRY_BUDGET_DEFAULT=<N> — override the fallback cap for
|
|
37
|
+
* any tool not in DEFAULT_CAPS.
|
|
38
|
+
* PUGI_RETRY_BUDGET_DISABLED=1 — warn-only mode. `shouldAllow`
|
|
39
|
+
* still records but always
|
|
40
|
+
* returns `allowed: true`. The
|
|
41
|
+
* count is preserved so
|
|
42
|
+
* diagnostics can still surface
|
|
43
|
+
* the pattern.
|
|
44
|
+
*/
|
|
45
|
+
import { createHash } from 'node:crypto';
|
|
46
|
+
/**
|
|
47
|
+
* Default per-tool retry caps. Tuned per leak research:
|
|
48
|
+
*
|
|
49
|
+
* bash — 5 (most volatile; transient flakes common)
|
|
50
|
+
* edit — 3 (deterministic; repeat = real bug)
|
|
51
|
+
* write — 3 (same)
|
|
52
|
+
* read — 10 (cheap; legitimate re-reads after edits)
|
|
53
|
+
* search/grep/glob — 10 (cheap; exploration loop)
|
|
54
|
+
* web_fetch — 5 (transient network; not infinite)
|
|
55
|
+
* default — 5 (any tool not in the table)
|
|
56
|
+
*
|
|
57
|
+
* Operators override per-tool via `PUGI_RETRY_BUDGET_<NAME>` env vars.
|
|
58
|
+
* Caps are bounded `[1, 1000]` after override to defend against typo
|
|
59
|
+
* runaway (e.g. `PUGI_RETRY_BUDGET_BASH=5000000`).
|
|
60
|
+
*/
|
|
61
|
+
export const DEFAULT_CAPS = Object.freeze({
|
|
62
|
+
bash: 5,
|
|
63
|
+
edit: 3,
|
|
64
|
+
write: 3,
|
|
65
|
+
read: 10,
|
|
66
|
+
search: 10,
|
|
67
|
+
grep: 10,
|
|
68
|
+
glob: 10,
|
|
69
|
+
web_fetch: 5,
|
|
70
|
+
default: 5,
|
|
71
|
+
});
|
|
72
|
+
/**
|
|
73
|
+
* Lower / upper bound for any resolved cap. Defends against:
|
|
74
|
+
* - PUGI_RETRY_BUDGET_BASH=0 -> first call instantly denied
|
|
75
|
+
* - PUGI_RETRY_BUDGET_BASH=99999 -> effectively unbounded loop
|
|
76
|
+
*/
|
|
77
|
+
export const MIN_CAP = 1;
|
|
78
|
+
export const MAX_CAP = 1000;
|
|
79
|
+
/**
|
|
80
|
+
* Per-cycle retry budget. One instance per `buildExecutor` call.
|
|
81
|
+
*
|
|
82
|
+
* Not thread-safe: the executor is single-threaded by construction
|
|
83
|
+
* (Node event loop + sequential await in dispatcher). If a future
|
|
84
|
+
* executor parallelises tool dispatch it must serialise the budget
|
|
85
|
+
* mutation explicitly.
|
|
86
|
+
*/
|
|
87
|
+
export class RetryBudget {
|
|
88
|
+
counts = new Map();
|
|
89
|
+
capCache = new Map();
|
|
90
|
+
env;
|
|
91
|
+
programmaticCaps;
|
|
92
|
+
constructor(options = {}) {
|
|
93
|
+
this.env = options.env ?? process.env;
|
|
94
|
+
this.programmaticCaps = options.caps ?? {};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Returns true when PUGI_RETRY_BUDGET_DISABLED=1. In disabled mode
|
|
98
|
+
* `shouldAllow` still records attempts but always allows the
|
|
99
|
+
* dispatch — useful for operators triaging a false-positive without
|
|
100
|
+
* a code change.
|
|
101
|
+
*/
|
|
102
|
+
isDisabled() {
|
|
103
|
+
return this.env.PUGI_RETRY_BUDGET_DISABLED === '1';
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Record one dispatch attempt. Idempotent on the bucket key (tool
|
|
107
|
+
* + argHash). Call this BEFORE the dispatch (or after `shouldAllow`
|
|
108
|
+
* but before `dispatch()` resolves) so a thrown dispatch counts.
|
|
109
|
+
*/
|
|
110
|
+
recordAttempt(toolName, argHash) {
|
|
111
|
+
const key = `${toolName}::${argHash}`;
|
|
112
|
+
const next = (this.counts.get(key) ?? 0) + 1;
|
|
113
|
+
this.counts.set(key, next);
|
|
114
|
+
return next;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Returns the current count for (tool, argHash) WITHOUT mutating.
|
|
118
|
+
*/
|
|
119
|
+
peek(toolName, argHash) {
|
|
120
|
+
return this.counts.get(`${toolName}::${argHash}`) ?? 0;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Resolve the effective cap for a tool.
|
|
124
|
+
*
|
|
125
|
+
* Precedence:
|
|
126
|
+
* 1. PUGI_RETRY_BUDGET_<TOOL_UPPER>=<N> (env)
|
|
127
|
+
* 2. programmaticCaps[toolName] (constructor)
|
|
128
|
+
* 3. DEFAULT_CAPS[toolName] (this module)
|
|
129
|
+
* 4. PUGI_RETRY_BUDGET_DEFAULT=<N> (env fallback)
|
|
130
|
+
* 5. DEFAULT_CAPS.default (final fallback)
|
|
131
|
+
*
|
|
132
|
+
* Bounded by [MIN_CAP, MAX_CAP] post-resolution. Invalid (NaN, ≤0,
|
|
133
|
+
* non-integer) env values are ignored and the next layer wins.
|
|
134
|
+
*/
|
|
135
|
+
capFor(toolName) {
|
|
136
|
+
const cached = this.capCache.get(toolName);
|
|
137
|
+
if (cached !== undefined)
|
|
138
|
+
return cached;
|
|
139
|
+
const envKey = `PUGI_RETRY_BUDGET_${toolName.toUpperCase()}`;
|
|
140
|
+
const envCap = parseCapEnv(this.env[envKey]);
|
|
141
|
+
const programmaticCap = this.programmaticCaps[toolName];
|
|
142
|
+
const defaultCap = DEFAULT_CAPS[toolName];
|
|
143
|
+
const fallbackEnvCap = parseCapEnv(this.env.PUGI_RETRY_BUDGET_DEFAULT);
|
|
144
|
+
// DEFAULT_CAPS.default is hard-coded above; cast keeps the type-
|
|
145
|
+
// narrower happy without leaking `| undefined` through the index
|
|
146
|
+
// access (tsc cannot prove the literal key exists).
|
|
147
|
+
const finalFallback = DEFAULT_CAPS.default;
|
|
148
|
+
let resolved;
|
|
149
|
+
if (envCap !== undefined) {
|
|
150
|
+
resolved = envCap;
|
|
151
|
+
}
|
|
152
|
+
else if (programmaticCap !== undefined) {
|
|
153
|
+
resolved = programmaticCap;
|
|
154
|
+
}
|
|
155
|
+
else if (defaultCap !== undefined) {
|
|
156
|
+
resolved = defaultCap;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
resolved = fallbackEnvCap ?? finalFallback;
|
|
160
|
+
}
|
|
161
|
+
const bounded = Math.min(MAX_CAP, Math.max(MIN_CAP, resolved));
|
|
162
|
+
this.capCache.set(toolName, bounded);
|
|
163
|
+
return bounded;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Should this dispatch be allowed? Caller passes the current count
|
|
167
|
+
* BEFORE recording — i.e. shouldAllow returns true when count < cap,
|
|
168
|
+
* then recordAttempt fires, bringing count up to cap. The next
|
|
169
|
+
* identical call sees count === cap and is refused.
|
|
170
|
+
*
|
|
171
|
+
* In disabled mode `allowed` is forced to true; `count` and `cap`
|
|
172
|
+
* still reflect reality so logs / diagnostics can spot the pattern.
|
|
173
|
+
*/
|
|
174
|
+
shouldAllow(toolName, argHash) {
|
|
175
|
+
const cap = this.capFor(toolName);
|
|
176
|
+
const count = this.peek(toolName, argHash);
|
|
177
|
+
const disabled = this.isDisabled();
|
|
178
|
+
const allowed = disabled ? true : count < cap;
|
|
179
|
+
return { allowed, count, cap, argHash, disabled };
|
|
180
|
+
}
|
|
181
|
+
/** Reset all state. Used between operator-input cycles when the
|
|
182
|
+
* budget instance is reused (most callers throw the instance away
|
|
183
|
+
* per cycle, so clear() is mostly for tests and hook surfaces). */
|
|
184
|
+
clear() {
|
|
185
|
+
this.counts.clear();
|
|
186
|
+
this.capCache.clear();
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Snapshot the current state for diagnostics. Returns a plain
|
|
190
|
+
* object so it round-trips through JSON.stringify cleanly.
|
|
191
|
+
*/
|
|
192
|
+
snapshot() {
|
|
193
|
+
const out = [];
|
|
194
|
+
for (const [key, count] of this.counts) {
|
|
195
|
+
const sep = key.indexOf('::');
|
|
196
|
+
if (sep < 0)
|
|
197
|
+
continue;
|
|
198
|
+
out.push({ tool: key.slice(0, sep), argHash: key.slice(sep + 2), count });
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Hash the model's tool-call arguments into a stable key. Same
|
|
205
|
+
* canonical args = same hash regardless of JSON whitespace / key
|
|
206
|
+
* order. Unparseable JSON is hashed verbatim so the budget still
|
|
207
|
+
* catches syntactically degenerate retry loops.
|
|
208
|
+
*/
|
|
209
|
+
export function hashArgs(argsRaw) {
|
|
210
|
+
const canonical = canonicalise(argsRaw);
|
|
211
|
+
return createHash('sha256').update(canonical).digest('hex');
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Canonicalise a raw JSON arg string. Object keys are sorted
|
|
215
|
+
* recursively. Arrays preserve order (semantic). Primitives untouched.
|
|
216
|
+
* On parse failure, returns the original string prefixed with `raw:`
|
|
217
|
+
* so a malformed-args repeat still hashes to the same bucket.
|
|
218
|
+
*/
|
|
219
|
+
function canonicalise(argsRaw) {
|
|
220
|
+
try {
|
|
221
|
+
const parsed = JSON.parse(argsRaw);
|
|
222
|
+
return JSON.stringify(sortKeys(parsed));
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return `raw:${argsRaw}`;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function sortKeys(value) {
|
|
229
|
+
if (value === null || typeof value !== 'object')
|
|
230
|
+
return value;
|
|
231
|
+
if (Array.isArray(value))
|
|
232
|
+
return value.map(sortKeys);
|
|
233
|
+
const obj = value;
|
|
234
|
+
const sorted = {};
|
|
235
|
+
for (const k of Object.keys(obj).sort()) {
|
|
236
|
+
sorted[k] = sortKeys(obj[k]);
|
|
237
|
+
}
|
|
238
|
+
return sorted;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Parse and bound a `PUGI_RETRY_BUDGET_*` env var. Returns `undefined`
|
|
242
|
+
* for any non-positive-integer string so the resolver can fall
|
|
243
|
+
* through to the next precedence layer. Bounded by [MIN_CAP, MAX_CAP]
|
|
244
|
+
* is NOT applied here — `capFor` clamps after the final layer wins,
|
|
245
|
+
* matching the "operator typo defends against runaway" requirement
|
|
246
|
+
* without silently swallowing a meaningful low value (e.g.
|
|
247
|
+
* `PUGI_RETRY_BUDGET_BASH=1` should clamp to MIN_CAP=1, which it
|
|
248
|
+
* does naturally since 1 >= MIN_CAP).
|
|
249
|
+
*/
|
|
250
|
+
function parseCapEnv(raw) {
|
|
251
|
+
if (raw === undefined || raw === '')
|
|
252
|
+
return undefined;
|
|
253
|
+
const n = Number(raw);
|
|
254
|
+
if (!Number.isInteger(n) || n <= 0)
|
|
255
|
+
return undefined;
|
|
256
|
+
return n;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Sentinel emitted to the model when the budget is exhausted. The
|
|
260
|
+
* format is stable so the engine adapter, spec layer, and operator
|
|
261
|
+
* dashboards can pattern-match on it.
|
|
262
|
+
*/
|
|
263
|
+
export function retryBudgetExhaustedSentinel(toolName, cap) {
|
|
264
|
+
return `RETRY_BUDGET_EXHAUSTED: ${toolName} exceeded ${cap} attempts with these args. Operator must intervene.`;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Typed error thrown by the tool-bridge when the cap is hit. Carries
|
|
268
|
+
* the sentinel string so the engine loop can pattern-match without
|
|
269
|
+
* re-parsing. `instanceof RetryBudgetExhausted` is the canonical
|
|
270
|
+
* downstream test.
|
|
271
|
+
*/
|
|
272
|
+
export class RetryBudgetExhausted extends Error {
|
|
273
|
+
toolName;
|
|
274
|
+
cap;
|
|
275
|
+
argHash;
|
|
276
|
+
constructor(toolName, cap, argHash) {
|
|
277
|
+
super(retryBudgetExhaustedSentinel(toolName, cap));
|
|
278
|
+
this.name = 'RetryBudgetExhausted';
|
|
279
|
+
this.toolName = toolName;
|
|
280
|
+
this.cap = cap;
|
|
281
|
+
this.argHash = argHash;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
//# sourceMappingURL=budget.js.map
|
package/dist/core/session.js
CHANGED
|
@@ -18,6 +18,50 @@ export function openSession(root) {
|
|
|
18
18
|
enabled,
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Leak L12 MVP — fire the `SessionStart` lifecycle event for all hooks
|
|
23
|
+
* declared in `~/.pugi/hooks-mvp.json`. Single-call surface; the REPL
|
|
24
|
+
* boot path invokes this once after `openSession`. Best-effort: any
|
|
25
|
+
* failure (missing config, hook spawn error) is swallowed so a
|
|
26
|
+
* misconfigured hook can never crash the REPL.
|
|
27
|
+
*
|
|
28
|
+
* Returns the number of hooks that fired (0 when no config / no
|
|
29
|
+
* matching hooks). Tests assert on the return value as the
|
|
30
|
+
* single-call invariant.
|
|
31
|
+
*/
|
|
32
|
+
export async function fireSessionStartMvp(session) {
|
|
33
|
+
try {
|
|
34
|
+
const { loadHooksConfig, fireHooks } = await import('./hooks/index.js');
|
|
35
|
+
// Defense-in-depth: `loadHooksConfig` is contractually non-null
|
|
36
|
+
// (returns `HooksConfig.empty(path)` when the file is absent), but
|
|
37
|
+
// the dynamic import boundary above can in principle return an
|
|
38
|
+
// unexpected shape if the module is mis-resolved at runtime. Guard
|
|
39
|
+
// the optional-chained `isEmpty()` call so a malformed loader can
|
|
40
|
+
// never raise `TypeError: Cannot read properties of undefined` and
|
|
41
|
+
// crash the REPL boot path. Belt-and-suspenders with the
|
|
42
|
+
// surrounding try/catch — the catch still swallows everything else.
|
|
43
|
+
const config = loadHooksConfig();
|
|
44
|
+
if (!config || config.isEmpty())
|
|
45
|
+
return 0;
|
|
46
|
+
const outcome = await fireHooks({
|
|
47
|
+
config,
|
|
48
|
+
event: 'SessionStart',
|
|
49
|
+
payload: {
|
|
50
|
+
event: 'SessionStart',
|
|
51
|
+
sessionId: session.id,
|
|
52
|
+
workspaceRoot: session.root,
|
|
53
|
+
startedAt: new Date().toISOString(),
|
|
54
|
+
},
|
|
55
|
+
workspaceRoot: session.root,
|
|
56
|
+
});
|
|
57
|
+
return outcome.results.length;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// SessionStart is never blocking — log nothing, return 0. A
|
|
61
|
+
// broken `hooks-mvp.json` is surfaced via `pugi hooks doctor`.
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
21
65
|
export function recordCommandStarted(session, command) {
|
|
22
66
|
if (!session.enabled)
|
|
23
67
|
return;
|
package/dist/core/settings.js
CHANGED
|
@@ -28,6 +28,17 @@ const pugiSettingsSchema = z.object({
|
|
|
28
28
|
telemetry: z.enum(['off', 'anonymous', 'community']).default('off'),
|
|
29
29
|
})
|
|
30
30
|
.default({}),
|
|
31
|
+
// beta.13 P1 fix 2026-05-26: ui.cyberZoo gates the cyber-zoo splash +
|
|
32
|
+
// ambient art in the REPL. Schema must declare the key explicitly
|
|
33
|
+
// because Zod's strip pass swallows unknown keys, which is how the
|
|
34
|
+
// initial `pugi init` write (which serialises `ui.cyberZoo`) was
|
|
35
|
+
// bypassed by the runtime reader — the value never made it past the
|
|
36
|
+
// schema gate so admin-api always saw the historical 'on' default.
|
|
37
|
+
ui: z
|
|
38
|
+
.object({
|
|
39
|
+
cyberZoo: z.enum(['on', 'off']).default('on'),
|
|
40
|
+
})
|
|
41
|
+
.default({}),
|
|
31
42
|
artifacts: z
|
|
32
43
|
.object({
|
|
33
44
|
defaultPath: z.string().default('.pugi/artifacts'),
|
|
@@ -38,6 +49,12 @@ const pugiSettingsSchema = z.object({
|
|
|
38
49
|
// fetcher. Default-off matches the spec posture; the schema must
|
|
39
50
|
// declare it explicitly because Zod's strict-pass strips unknown
|
|
40
51
|
// keys and would silently swallow the operator's intent.
|
|
52
|
+
//
|
|
53
|
+
// β1b T4 (2026-05-26): added `web.search.enabled` to gate the
|
|
54
|
+
// Brave-Search-backed `web_search` tool. Distinct from `web.fetch`
|
|
55
|
+
// because search queries themselves are an egress event that can
|
|
56
|
+
// leak operator intent — an operator may want fetch without
|
|
57
|
+
// implicitly enabling search-as-egress.
|
|
41
58
|
web: z
|
|
42
59
|
.object({
|
|
43
60
|
fetch: z
|
|
@@ -45,6 +62,69 @@ const pugiSettingsSchema = z.object({
|
|
|
45
62
|
enabled: z.boolean().optional(),
|
|
46
63
|
})
|
|
47
64
|
.optional(),
|
|
65
|
+
search: z
|
|
66
|
+
.object({
|
|
67
|
+
enabled: z.boolean().optional(),
|
|
68
|
+
})
|
|
69
|
+
.optional(),
|
|
70
|
+
})
|
|
71
|
+
.optional(),
|
|
72
|
+
// β7 L9 — per-language LSP toggle. When omitted, every supported
|
|
73
|
+
// server is available subject to binary detection on PATH. When
|
|
74
|
+
// present, only languages set to `true` are launched (false silently
|
|
75
|
+
// skips that language even if the binary is installed). Use this in
|
|
76
|
+
// workspaces where a heavyweight server (rust-analyzer indexing a
|
|
77
|
+
// monorepo, pyright on a fresh venv) wastes resources for the
|
|
78
|
+
// current task. The `pugi lsp servers` subcommand surfaces the
|
|
79
|
+
// current toggle state per server.
|
|
80
|
+
//
|
|
81
|
+
// Schema is intentionally permissive (`optional()` on the section AND
|
|
82
|
+
// on every per-language flag) so a partial config keeps the
|
|
83
|
+
// backwards-compatible "every language enabled" default.
|
|
84
|
+
lsp: z
|
|
85
|
+
.object({
|
|
86
|
+
typescript: z.boolean().optional(),
|
|
87
|
+
javascript: z.boolean().optional(),
|
|
88
|
+
python: z.boolean().optional(),
|
|
89
|
+
go: z.boolean().optional(),
|
|
90
|
+
rust: z.boolean().optional(),
|
|
91
|
+
// Leak L15 (2026-05-27): post-edit auto-diagnostics. When `true`,
|
|
92
|
+
// a successful `edit`/`write`/`multi_edit` triggers a diagnostic
|
|
93
|
+
// pull on the touched file(s) and the result is appended to the
|
|
94
|
+
// tool envelope so the model can self-correct in the same turn.
|
|
95
|
+
// Off by default — the cold-start of `typescript-language-server`
|
|
96
|
+
// is heavy enough that we opt in explicitly until dogfood proves
|
|
97
|
+
// the throughput trade is worth it. Also enabled via env var
|
|
98
|
+
// `PUGI_LSP_POST_EDIT=1` for CI / one-off operator probes.
|
|
99
|
+
postEditDiagnostics: z.boolean().optional(),
|
|
100
|
+
})
|
|
101
|
+
.optional(),
|
|
102
|
+
// β1 Pl9 (#74) — per-command budget overrides. Optional. Partial
|
|
103
|
+
// overrides merge against the β1 defaults in
|
|
104
|
+
// `core/engine/budgets.ts::beta1DefaultBudgets`. The schema is
|
|
105
|
+
// intentionally loose at the leaf (positive integers) so a typo lands
|
|
106
|
+
// a deterministic `BudgetConfigError` at `resolveBudget()` instead of
|
|
107
|
+
// a Zod parse error two layers up.
|
|
108
|
+
budgets: z
|
|
109
|
+
.object({
|
|
110
|
+
code: z
|
|
111
|
+
.object({ maxTokens: z.number().int().positive().optional(), maxToolCalls: z.number().int().positive().optional() })
|
|
112
|
+
.optional(),
|
|
113
|
+
fix: z
|
|
114
|
+
.object({ maxTokens: z.number().int().positive().optional(), maxToolCalls: z.number().int().positive().optional() })
|
|
115
|
+
.optional(),
|
|
116
|
+
build: z
|
|
117
|
+
.object({ maxTokens: z.number().int().positive().optional(), maxToolCalls: z.number().int().positive().optional() })
|
|
118
|
+
.optional(),
|
|
119
|
+
plan: z
|
|
120
|
+
.object({ maxTokens: z.number().int().positive().optional(), maxToolCalls: z.number().int().positive().optional() })
|
|
121
|
+
.optional(),
|
|
122
|
+
explain: z
|
|
123
|
+
.object({ maxTokens: z.number().int().positive().optional(), maxToolCalls: z.number().int().positive().optional() })
|
|
124
|
+
.optional(),
|
|
125
|
+
review_triple: z
|
|
126
|
+
.object({ maxTokens: z.number().int().positive().optional(), maxToolCalls: z.number().int().positive().optional() })
|
|
127
|
+
.optional(),
|
|
48
128
|
})
|
|
49
129
|
.optional(),
|
|
50
130
|
});
|