@pugi/cli 0.1.0-alpha.3 → 0.1.0-alpha.5
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/README.md +20 -0
- package/dist/commands/jobs.js +245 -0
- package/dist/core/agents/registry.js +69 -0
- package/dist/core/bash-classifier.js +1001 -0
- package/dist/core/context/builder.js +114 -0
- package/dist/core/context/compaction-events.js +99 -0
- package/dist/core/context/compaction.js +602 -0
- package/dist/core/context/invariants.js +250 -0
- package/dist/core/context/markdown-loader.js +270 -0
- package/dist/core/engine/compaction-hook.js +154 -0
- package/dist/core/engine/index.js +5 -0
- package/dist/core/engine/prompts.js +42 -0
- package/dist/core/engine/tool-bridge.js +159 -61
- package/dist/core/hooks.js +415 -0
- package/dist/core/jobs/registry.js +462 -0
- package/dist/core/mcp/client.js +316 -0
- package/dist/core/mcp/registry.js +171 -0
- package/dist/core/mcp/trust.js +91 -0
- package/dist/core/permission.js +221 -116
- package/dist/core/repl/cap-warning.js +91 -0
- package/dist/core/repl/session.js +399 -0
- package/dist/core/repl/slash-commands.js +116 -0
- package/dist/core/session.js +168 -0
- package/dist/core/subagents/dispatcher.js +258 -0
- package/dist/core/subagents/index.js +26 -0
- package/dist/core/subagents/spawn.js +86 -0
- package/dist/core/trust.js +109 -0
- package/dist/runtime/cli.js +157 -45
- package/dist/runtime/commands/budget.js +192 -0
- package/dist/runtime/commands/config.js +231 -0
- package/dist/runtime/commands/privacy.js +107 -0
- package/dist/runtime/commands/undo.js +329 -0
- package/dist/tools/bash.js +660 -0
- package/dist/tui/agent-tree.js +66 -0
- package/dist/tui/conversation-pane.js +45 -0
- package/dist/tui/input-box.js +91 -0
- package/dist/tui/login-picker.js +69 -0
- package/dist/tui/render.js +68 -0
- package/dist/tui/repl-render.js +218 -0
- package/dist/tui/repl.js +152 -0
- package/dist/tui/splash-data.js +61 -0
- package/dist/tui/splash.js +31 -0
- package/dist/tui/status-bar.js +58 -0
- package/package.json +11 -5
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getJobRegistry, summarizeJobsForPrompt, } from '../jobs/registry.js';
|
|
1
2
|
/**
|
|
2
3
|
* System prompts for each engine command. Each prompt:
|
|
3
4
|
* - Anchors the model in Pugi's local-first contract (ADR-0037).
|
|
@@ -11,6 +12,12 @@
|
|
|
11
12
|
* the persona system prompt comes from the runtime (Anvil bridge
|
|
12
13
|
* prepends `oes-dev` / Sigma prompt automatically when configured); these
|
|
13
14
|
* prompts ride on top and scope the model to the current command.
|
|
15
|
+
*
|
|
16
|
+
* Sprint α5.9 (ADR-0056 PR-PUGI-CLI-M1-GAP-J): the system prompt picks up
|
|
17
|
+
* a `BACKGROUND JOBS:` snapshot appended at the tail so the agent loop
|
|
18
|
+
* knows what background bash work is currently on watch and can avoid
|
|
19
|
+
* spawning a duplicate. The snapshot is sourced from `JobRegistry` and
|
|
20
|
+
* formatted by `summarizeJobsForPrompt` so the surface is single-sourced.
|
|
14
21
|
*/
|
|
15
22
|
const COMMON_LOCAL_FIRST_PREAMBLE = [
|
|
16
23
|
'You are the Pugi CLI agent running locally inside the operator\'s repository.',
|
|
@@ -26,6 +33,13 @@ const EDIT_FLOW_RULES = [
|
|
|
26
33
|
'After your last tool call, summarise what you changed and what the operator should review.',
|
|
27
34
|
].join(' ');
|
|
28
35
|
export function systemPromptFor(kind) {
|
|
36
|
+
const base = baseSystemPromptFor(kind);
|
|
37
|
+
const snapshot = formatBackgroundJobsSnapshot(getJobRegistrySafely());
|
|
38
|
+
if (!snapshot)
|
|
39
|
+
return base;
|
|
40
|
+
return `${base}\n\n${snapshot}`;
|
|
41
|
+
}
|
|
42
|
+
function baseSystemPromptFor(kind) {
|
|
29
43
|
switch (kind) {
|
|
30
44
|
case 'code':
|
|
31
45
|
return [
|
|
@@ -64,6 +78,34 @@ export function systemPromptFor(kind) {
|
|
|
64
78
|
].join('\n\n');
|
|
65
79
|
}
|
|
66
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Builds the BACKGROUND JOBS snapshot block injected at the tail of
|
|
83
|
+
* the system prompt. Sync because the surrounding `systemPromptFor`
|
|
84
|
+
* builder is sync (the engine adapter does not await prompt
|
|
85
|
+
* assembly) and the JobRegistry's `listSync()` is essentially free
|
|
86
|
+
* (single JSON file read with sync fs primitives). Returns an empty
|
|
87
|
+
* string if the registry cannot be reached so the prompt assembly
|
|
88
|
+
* never crashes when the ledger is unavailable.
|
|
89
|
+
*/
|
|
90
|
+
export function formatBackgroundJobsSnapshot(registry) {
|
|
91
|
+
if (!registry)
|
|
92
|
+
return '';
|
|
93
|
+
try {
|
|
94
|
+
const entries = registry.listSync();
|
|
95
|
+
return summarizeJobsForPrompt(entries);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function getJobRegistrySafely() {
|
|
102
|
+
try {
|
|
103
|
+
return getJobRegistry();
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
67
109
|
/**
|
|
68
110
|
* Anvil persona slug to invoke per command. Today every command routes
|
|
69
111
|
* to `oes-dev` (Sigma) — the Tier-2 reviewer persona already configured
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { editTool, globTool, grepTool, readTool, writeTool, } from '../../tools/file-tools.js';
|
|
2
|
+
import { bashToolSync } from '../../tools/bash.js';
|
|
2
3
|
/**
|
|
3
4
|
* Tool-bridge: turns the abstract tool registry into:
|
|
4
5
|
* 1. An OpenAI-shaped tools schema for `EngineLoopClient.send`.
|
|
@@ -134,7 +135,7 @@ function requireString(obj, key) {
|
|
|
134
135
|
return v;
|
|
135
136
|
}
|
|
136
137
|
export function buildExecutor(input) {
|
|
137
|
-
const { kind, ctx } = input;
|
|
138
|
+
const { kind, ctx, hooks, sessionId } = input;
|
|
138
139
|
const planMode = kind === 'plan';
|
|
139
140
|
return async ({ name, arguments: argsRaw }) => {
|
|
140
141
|
if (!WIRED_TOOLS.has(name)) {
|
|
@@ -146,70 +147,167 @@ export function buildExecutor(input) {
|
|
|
146
147
|
// outcome, not a failure, because plan mode is doing its job.
|
|
147
148
|
throw new Error(`PLAN_MODE_REFUSED: ${name} is not allowed in plan mode`);
|
|
148
149
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
150
|
+
// Fire PreToolUse hooks. The match grammar takes the tool name and
|
|
151
|
+
// (when extractable) the target path. Each new tool dispatch starts a
|
|
152
|
+
// fresh dedup batch so a hook fires once per dispatch, not once per
|
|
153
|
+
// session.
|
|
154
|
+
if (hooks && sessionId) {
|
|
155
|
+
hooks.resetBatch();
|
|
156
|
+
const path = extractToolPath(name, argsRaw);
|
|
157
|
+
const preCtx = {
|
|
158
|
+
sessionId,
|
|
159
|
+
event: 'PreToolUse',
|
|
160
|
+
tool: name,
|
|
161
|
+
path,
|
|
162
|
+
payload: { tool: name, arguments: argsRaw },
|
|
163
|
+
};
|
|
164
|
+
// List the matching hooks BEFORE firing so we can correlate
|
|
165
|
+
// hook[i] with result[i] when checking onFailure: 'block'. The
|
|
166
|
+
// ordering of listMatching and fire is stable: fire iterates the
|
|
167
|
+
// same listMatching output internally.
|
|
168
|
+
const matchingPreHooks = hooks.listMatching(preCtx);
|
|
169
|
+
const preResults = await hooks.fire(preCtx);
|
|
170
|
+
for (let i = 0; i < matchingPreHooks.length; i += 1) {
|
|
171
|
+
const hook = matchingPreHooks[i];
|
|
172
|
+
const result = preResults[i];
|
|
173
|
+
if (hook && result && hook.onFailure === 'block' && !result.ok) {
|
|
174
|
+
throw new Error(`HOOK_BLOCKED: PreToolUse hook (${hook.run.slice(0, 80)}) refused ${name} (exit=${result.exitCode})`);
|
|
160
175
|
}
|
|
161
|
-
return content;
|
|
162
|
-
}
|
|
163
|
-
case 'write': {
|
|
164
|
-
const wargs = {
|
|
165
|
-
path: requireString(args, 'path'),
|
|
166
|
-
content: requireString(args, 'content'),
|
|
167
|
-
};
|
|
168
|
-
writeTool(ctx, wargs.path, wargs.content);
|
|
169
|
-
return `wrote ${wargs.path} (${wargs.content.length} bytes)`;
|
|
170
|
-
}
|
|
171
|
-
case 'edit': {
|
|
172
|
-
const eargs = {
|
|
173
|
-
path: requireString(args, 'path'),
|
|
174
|
-
oldString: requireString(args, 'oldString'),
|
|
175
|
-
newString: requireString(args, 'newString'),
|
|
176
|
-
};
|
|
177
|
-
editTool(ctx, eargs.path, eargs.oldString, eargs.newString);
|
|
178
|
-
return `edited ${eargs.path}`;
|
|
179
176
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
177
|
+
}
|
|
178
|
+
const args = parseArgs(argsRaw);
|
|
179
|
+
const dispatch = async () => {
|
|
180
|
+
return dispatchTool(name, args, ctx);
|
|
181
|
+
};
|
|
182
|
+
try {
|
|
183
|
+
const result = await dispatch();
|
|
184
|
+
if (hooks && sessionId) {
|
|
185
|
+
const path = extractToolPath(name, argsRaw);
|
|
186
|
+
await hooks.fire({
|
|
187
|
+
sessionId,
|
|
188
|
+
event: 'PostToolUse',
|
|
189
|
+
tool: name,
|
|
190
|
+
path,
|
|
191
|
+
payload: { tool: name, arguments: argsRaw, ok: true, result: result.slice(0, 1024) },
|
|
192
|
+
});
|
|
196
193
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (hooks && sessionId) {
|
|
198
|
+
const path = extractToolPath(name, argsRaw);
|
|
199
|
+
await hooks.fire({
|
|
200
|
+
sessionId,
|
|
201
|
+
event: 'PostToolUseFailure',
|
|
202
|
+
tool: name,
|
|
203
|
+
path,
|
|
204
|
+
payload: {
|
|
205
|
+
tool: name,
|
|
206
|
+
arguments: argsRaw,
|
|
207
|
+
ok: false,
|
|
208
|
+
error: error instanceof Error ? error.message : String(error),
|
|
209
|
+
},
|
|
210
|
+
});
|
|
208
211
|
}
|
|
209
|
-
|
|
210
|
-
// Exhaustive; unreachable because of the WIRED_TOOLS guard above.
|
|
211
|
-
throw new Error(`unhandled tool: ${name}`);
|
|
212
|
+
throw error;
|
|
212
213
|
}
|
|
213
214
|
};
|
|
214
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Best-effort extraction of the file path a tool targets. Returns
|
|
218
|
+
* undefined when the tool does not take a path or the path cannot be
|
|
219
|
+
* parsed cleanly — match rules with a `pathGlob` will then skip this
|
|
220
|
+
* dispatch, which is the safe default.
|
|
221
|
+
*/
|
|
222
|
+
function extractToolPath(name, argsRaw) {
|
|
223
|
+
if (name !== 'read' && name !== 'write' && name !== 'edit')
|
|
224
|
+
return undefined;
|
|
225
|
+
try {
|
|
226
|
+
const parsed = JSON.parse(argsRaw);
|
|
227
|
+
const path = parsed.path;
|
|
228
|
+
return typeof path === 'string' ? path : undefined;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function dispatchTool(name, args, ctx) {
|
|
235
|
+
switch (name) {
|
|
236
|
+
case 'read': {
|
|
237
|
+
const { path } = { path: requireString(args, 'path') };
|
|
238
|
+
const content = readTool(ctx, path);
|
|
239
|
+
// Cap the content surfaced back to the model so a 10MB file
|
|
240
|
+
// does not blow the context window. The model sees the head
|
|
241
|
+
// and a truncation marker; if it needs more it can grep.
|
|
242
|
+
const CAP = 32 * 1024;
|
|
243
|
+
if (content.length > CAP) {
|
|
244
|
+
return `${content.slice(0, CAP)}\n(...truncated at ${CAP} bytes; use grep or glob to narrow the read)`;
|
|
245
|
+
}
|
|
246
|
+
return content;
|
|
247
|
+
}
|
|
248
|
+
case 'write': {
|
|
249
|
+
const wargs = {
|
|
250
|
+
path: requireString(args, 'path'),
|
|
251
|
+
content: requireString(args, 'content'),
|
|
252
|
+
};
|
|
253
|
+
writeTool(ctx, wargs.path, wargs.content);
|
|
254
|
+
return `wrote ${wargs.path} (${wargs.content.length} bytes)`;
|
|
255
|
+
}
|
|
256
|
+
case 'edit': {
|
|
257
|
+
const eargs = {
|
|
258
|
+
path: requireString(args, 'path'),
|
|
259
|
+
oldString: requireString(args, 'oldString'),
|
|
260
|
+
newString: requireString(args, 'newString'),
|
|
261
|
+
};
|
|
262
|
+
editTool(ctx, eargs.path, eargs.oldString, eargs.newString);
|
|
263
|
+
return `edited ${eargs.path}`;
|
|
264
|
+
}
|
|
265
|
+
case 'grep': {
|
|
266
|
+
const gargs = { query: requireString(args, 'query') };
|
|
267
|
+
const matches = grepTool(ctx, gargs.query);
|
|
268
|
+
if (matches.length === 0)
|
|
269
|
+
return `no matches for ${gargs.query}`;
|
|
270
|
+
const head = matches.slice(0, 50);
|
|
271
|
+
const rendered = head.map((m) => `${m.path}:${m.line}: ${m.text}`).join('\n');
|
|
272
|
+
const more = matches.length > head.length ? `\n(... ${matches.length - head.length} more)` : '';
|
|
273
|
+
return `${matches.length} match(es):\n${rendered}${more}`;
|
|
274
|
+
}
|
|
275
|
+
case 'glob': {
|
|
276
|
+
const gargs = { pattern: requireString(args, 'pattern') };
|
|
277
|
+
const results = globTool(ctx, gargs.pattern);
|
|
278
|
+
if (results.length === 0)
|
|
279
|
+
return `no paths match ${gargs.pattern}`;
|
|
280
|
+
return `${results.length} path(s):\n${results.slice(0, 100).join('\n')}${results.length > 100 ? `\n(... ${results.length - 100} more)` : ''}`;
|
|
281
|
+
}
|
|
282
|
+
case 'bash': {
|
|
283
|
+
const bargs = { command: requireString(args, 'command') };
|
|
284
|
+
// The class-aware bash tool (sprint α5.2) replaces the legacy
|
|
285
|
+
// file-tools entry point. We use the sync variant here because
|
|
286
|
+
// dispatchTool's signature is sync; the async tool is reserved
|
|
287
|
+
// for the REPL path (sprint α5.7) where promises are first class.
|
|
288
|
+
const result = bashToolSync({ cmd: bargs.command }, {
|
|
289
|
+
root: ctx.root,
|
|
290
|
+
settings: ctx.settings,
|
|
291
|
+
session: ctx.session,
|
|
292
|
+
source: 'agent',
|
|
293
|
+
});
|
|
294
|
+
const parts = [
|
|
295
|
+
`exit=${result.exitCode}`,
|
|
296
|
+
result.stdout ? `stdout:\n${result.stdout}` : '',
|
|
297
|
+
result.stderr ? `stderr:\n${result.stderr}` : '',
|
|
298
|
+
];
|
|
299
|
+
if (result.artifactRef)
|
|
300
|
+
parts.push(`artifactRef=${result.artifactRef}`);
|
|
301
|
+
if (result.truncated)
|
|
302
|
+
parts.push('truncated=true');
|
|
303
|
+
if (result.timedOut)
|
|
304
|
+
parts.push('timedOut=true');
|
|
305
|
+
const body = parts.filter(Boolean).join('\n');
|
|
306
|
+
return body || '(no output)';
|
|
307
|
+
}
|
|
308
|
+
default:
|
|
309
|
+
// Exhaustive; unreachable because of the WIRED_TOOLS guard above.
|
|
310
|
+
throw new Error(`unhandled tool: ${name}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
215
313
|
//# sourceMappingURL=tool-bridge.js.map
|