@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.
- package/dist/core/auth/env-provider.js +1 -1
- package/dist/core/context/markdown-traverse.js +1 -1
- package/dist/core/credentials.js +1 -1
- package/dist/core/engine/anvil-client.js +63 -0
- 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.js +1 -1
- package/dist/core/repl/ask.js +1 -1
- package/dist/core/repl/session.js +3 -3
- package/dist/core/repl/slash-commands.js +1 -1
- package/dist/core/settings.js +26 -0
- package/dist/core/worktree/include-parser.js +249 -0
- package/dist/runtime/cli.js +108 -8
- package/dist/runtime/commands/agents.js +1 -1
- package/dist/runtime/commands/hooks.js +3 -0
- package/dist/runtime/commands/review-consensus.js +1 -1
- package/dist/runtime/version.js +1 -1
- package/dist/runtime/worktree-bootstrap.js +579 -0
- package/dist/tools/lsp-tools.js +377 -1
- package/dist/tools/registry.js +23 -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/package.json +3 -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/tools/lsp-tools.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { callHierarchyAt, codeActionsAt, diagnosticsFor, findDefinition as symbolsFindDefinition, findImplementations, findReferences as symbolsFindReferences, findTypeDefinition, findWorkspaceSymbol, formatFile, hoverSymbol, listDocumentSymbols, renameSymbol, signatureAt, transportFromLspClient, } from '../core/lsp/symbol-tools.js';
|
|
1
2
|
import { gateOnCancellation, OperatorAbortedError } from './file-tools.js';
|
|
2
3
|
import { recordToolCall, recordToolResult } from '../core/session.js';
|
|
4
|
+
import { getGlobalSymbolCache, SymbolCache } from '../core/lsp/symbol-cache.js';
|
|
3
5
|
/** Cap for any single LSP tool's payload size. Keeps model context lean. */
|
|
4
6
|
const LSP_PAYLOAD_CAP_BYTES = 8 * 1024;
|
|
5
7
|
export async function lspHover(ctx, lang, file, line, col) {
|
|
@@ -184,6 +186,380 @@ function capDiagnostics(items) {
|
|
|
184
186
|
}
|
|
185
187
|
return { value: items.slice(0, 1), truncated: true };
|
|
186
188
|
}
|
|
189
|
+
/* ------------------------------------------------------------------------- */
|
|
190
|
+
/* PUGI-78 Phase 1: symbols.* namespace tools (13 categories). */
|
|
191
|
+
/* ------------------------------------------------------------------------- */
|
|
192
|
+
/**
|
|
193
|
+
* PUGI-78 Phase 1: symbols.find_definition.
|
|
194
|
+
*
|
|
195
|
+
* Pugi reads a whole file (~5-50 KB tokens) every time the model wants
|
|
196
|
+
* to know "where is foo defined?". The LSP wire here returns ~200 bytes
|
|
197
|
+
* (file + line + character). Two orders of magnitude reduction in token
|
|
198
|
+
* cost per refactor turn, hence the BIG-leverage classification in the
|
|
199
|
+
* spec (10-100x token savings per refactor).
|
|
200
|
+
*
|
|
201
|
+
* Failure folds to `lsp_unavailable` when no client is registered for
|
|
202
|
+
* the inferred language. The agent surface is expected to fall back to
|
|
203
|
+
* grep when this fires (the tool-preference prompt rule steers it).
|
|
204
|
+
*/
|
|
205
|
+
export async function symbolsFindDefinitionTool(ctx, lang, file, line, col) {
|
|
206
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_find_definition', `${lang}:${file}:${line}:${col}`);
|
|
207
|
+
return guard(ctx, 'symbols_find_definition', toolCallId, async () => {
|
|
208
|
+
const client = ctx.lspClients?.get(lang);
|
|
209
|
+
if (!client)
|
|
210
|
+
return unavailable(lang);
|
|
211
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
212
|
+
const cwd = workspaceForCache(ctx);
|
|
213
|
+
const key = SymbolCache.makeKey(lang, cwd, 'find_definition', { file, line, col });
|
|
214
|
+
const cached = cache?.get(key);
|
|
215
|
+
if (cached !== undefined) {
|
|
216
|
+
if (cached === null)
|
|
217
|
+
return notFound('definition');
|
|
218
|
+
return { ok: true, value: cached };
|
|
219
|
+
}
|
|
220
|
+
const result = await symbolsFindDefinition(transportFromLspClient(client), file, line, col);
|
|
221
|
+
cache?.set(key, result);
|
|
222
|
+
if (!result)
|
|
223
|
+
return notFound('definition');
|
|
224
|
+
return { ok: true, value: result };
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* PUGI-78 Phase 1: symbols.find_references.
|
|
229
|
+
*
|
|
230
|
+
* Returns the flat list of call sites for the symbol at (line, col).
|
|
231
|
+
* The 200-row cap from the legacy `lspReferences` carries over via
|
|
232
|
+
* `capSymbolReferences`.
|
|
233
|
+
*/
|
|
234
|
+
export async function symbolsFindReferencesTool(ctx, lang, file, line, col) {
|
|
235
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_find_references', `${lang}:${file}:${line}:${col}`);
|
|
236
|
+
return guard(ctx, 'symbols_find_references', toolCallId, async () => {
|
|
237
|
+
const client = ctx.lspClients?.get(lang);
|
|
238
|
+
if (!client)
|
|
239
|
+
return unavailable(lang);
|
|
240
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
241
|
+
const cwd = workspaceForCache(ctx);
|
|
242
|
+
const key = SymbolCache.makeKey(lang, cwd, 'find_references', { file, line, col });
|
|
243
|
+
const cached = cache?.get(key);
|
|
244
|
+
if (cached !== undefined) {
|
|
245
|
+
const capped = capSymbolReferences(cached);
|
|
246
|
+
return { ok: true, value: capped.value, ...(capped.truncated ? { truncated: true } : {}) };
|
|
247
|
+
}
|
|
248
|
+
const result = await symbolsFindReferences(transportFromLspClient(client), file, line, col);
|
|
249
|
+
cache?.set(key, result);
|
|
250
|
+
const capped = capSymbolReferences(result);
|
|
251
|
+
return { ok: true, value: capped.value, ...(capped.truncated ? { truncated: true } : {}) };
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* PUGI-78 Phase 1: symbols.list_in_file.
|
|
256
|
+
*
|
|
257
|
+
* Outline view — the document-symbol surface. Returns the flat list of
|
|
258
|
+
* top-level + nested symbols with their kind. Cache HIT on the same
|
|
259
|
+
* file across consecutive turns is the common case (the model usually
|
|
260
|
+
* lists, then dives into a single symbol).
|
|
261
|
+
*/
|
|
262
|
+
export async function symbolsListInFileTool(ctx, lang, file) {
|
|
263
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_list_in_file', `${lang}:${file}`);
|
|
264
|
+
return guard(ctx, 'symbols_list_in_file', toolCallId, async () => {
|
|
265
|
+
const client = ctx.lspClients?.get(lang);
|
|
266
|
+
if (!client)
|
|
267
|
+
return unavailable(lang);
|
|
268
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
269
|
+
const cwd = workspaceForCache(ctx);
|
|
270
|
+
const key = SymbolCache.makeKey(lang, cwd, 'list_in_file', { file });
|
|
271
|
+
const cached = cache?.get(key);
|
|
272
|
+
if (cached !== undefined) {
|
|
273
|
+
return { ok: true, value: [...cached] };
|
|
274
|
+
}
|
|
275
|
+
const result = await listDocumentSymbols(transportFromLspClient(client), file);
|
|
276
|
+
cache?.set(key, result);
|
|
277
|
+
return { ok: true, value: result };
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* PUGI-78 Phase 1: symbols.rename.
|
|
282
|
+
*
|
|
283
|
+
* Returns the workspace-edit preview the server proposes. The
|
|
284
|
+
* dispatcher applies via `apply_patch` in a future ticket; Phase 1 is
|
|
285
|
+
* read-only — the agent surface gets the affected file list + per-edit
|
|
286
|
+
* line/character so the model can summarize the change.
|
|
287
|
+
*/
|
|
288
|
+
export async function symbolsRenameTool(ctx, lang, file, line, col, newName) {
|
|
289
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_rename', `${lang}:${file}:${line}:${col}:${newName}`);
|
|
290
|
+
return guard(ctx, 'symbols_rename', toolCallId, async () => {
|
|
291
|
+
const client = ctx.lspClients?.get(lang);
|
|
292
|
+
if (!client)
|
|
293
|
+
return unavailable(lang);
|
|
294
|
+
// No cache for rename — the edit set depends on the live source
|
|
295
|
+
// file state. A subsequent rename of the same symbol after the
|
|
296
|
+
// first applied would otherwise return stale edits.
|
|
297
|
+
const result = await renameSymbol(transportFromLspClient(client), file, line, col, newName);
|
|
298
|
+
if (!result)
|
|
299
|
+
return notFound('rename');
|
|
300
|
+
return { ok: true, value: result };
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* PUGI-78 Phase 1: symbols.hover.
|
|
305
|
+
*
|
|
306
|
+
* Returns the hover content (type info + docstring) at the position.
|
|
307
|
+
* Body capped at 4 KB so a verbose generic does not blow context.
|
|
308
|
+
*/
|
|
309
|
+
export async function symbolsHoverTool(ctx, lang, file, line, col) {
|
|
310
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_hover', `${lang}:${file}:${line}:${col}`);
|
|
311
|
+
return guard(ctx, 'symbols_hover', toolCallId, async () => {
|
|
312
|
+
const client = ctx.lspClients?.get(lang);
|
|
313
|
+
if (!client)
|
|
314
|
+
return unavailable(lang);
|
|
315
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
316
|
+
const cwd = workspaceForCache(ctx);
|
|
317
|
+
const key = SymbolCache.makeKey(lang, cwd, 'hover', { file, line, col });
|
|
318
|
+
const cached = cache?.get(key);
|
|
319
|
+
if (cached !== undefined) {
|
|
320
|
+
if (cached === null)
|
|
321
|
+
return notFound('hover');
|
|
322
|
+
const next = {
|
|
323
|
+
ok: true,
|
|
324
|
+
value: { content: cached.content, ...(cached.range ? { range: cached.range } : {}) },
|
|
325
|
+
...(cached.truncated ? { truncated: true } : {}),
|
|
326
|
+
};
|
|
327
|
+
return next;
|
|
328
|
+
}
|
|
329
|
+
const result = await hoverSymbol(transportFromLspClient(client), file, line, col);
|
|
330
|
+
cache?.set(key, result);
|
|
331
|
+
if (!result)
|
|
332
|
+
return notFound('hover');
|
|
333
|
+
return {
|
|
334
|
+
ok: true,
|
|
335
|
+
value: { content: result.content, ...(result.range ? { range: result.range } : {}) },
|
|
336
|
+
...(result.truncated ? { truncated: true } : {}),
|
|
337
|
+
};
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* PUGI-78 Phase 1: symbols.signature.
|
|
342
|
+
*
|
|
343
|
+
* Function signature at a call site. Returns the active overload's
|
|
344
|
+
* label, parameters, and active-parameter index.
|
|
345
|
+
*/
|
|
346
|
+
export async function symbolsSignatureTool(ctx, lang, file, line, col) {
|
|
347
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_signature', `${lang}:${file}:${line}:${col}`);
|
|
348
|
+
return guard(ctx, 'symbols_signature', toolCallId, async () => {
|
|
349
|
+
const client = ctx.lspClients?.get(lang);
|
|
350
|
+
if (!client)
|
|
351
|
+
return unavailable(lang);
|
|
352
|
+
const result = await signatureAt(transportFromLspClient(client), file, line, col);
|
|
353
|
+
if (!result)
|
|
354
|
+
return notFound('signature');
|
|
355
|
+
return { ok: true, value: result };
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* PUGI-78 Phase 1: symbols.workspace_symbols.
|
|
360
|
+
*
|
|
361
|
+
* Workspace-wide fuzzy search. The server picks the matching algorithm
|
|
362
|
+
* (substring / fuzzy / prefix); we forward verbatim.
|
|
363
|
+
*/
|
|
364
|
+
export async function symbolsWorkspaceSymbolsTool(ctx, lang, query) {
|
|
365
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_workspace_symbols', `${lang}:${query}`);
|
|
366
|
+
return guard(ctx, 'symbols_workspace_symbols', toolCallId, async () => {
|
|
367
|
+
const client = ctx.lspClients?.get(lang);
|
|
368
|
+
if (!client)
|
|
369
|
+
return unavailable(lang);
|
|
370
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
371
|
+
const cwd = workspaceForCache(ctx);
|
|
372
|
+
const key = SymbolCache.makeKey(lang, cwd, 'workspace_symbols', { query });
|
|
373
|
+
const cached = cache?.get(key);
|
|
374
|
+
if (cached !== undefined) {
|
|
375
|
+
return { ok: true, value: [...cached] };
|
|
376
|
+
}
|
|
377
|
+
const result = await findWorkspaceSymbol(transportFromLspClient(client), query);
|
|
378
|
+
cache?.set(key, result);
|
|
379
|
+
return { ok: true, value: result };
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* PUGI-78 Phase 1: symbols.call_hierarchy.
|
|
384
|
+
*
|
|
385
|
+
* Returns the incoming + outgoing call edges at the symbol position.
|
|
386
|
+
* The two arrays are independent — a function may have many incoming
|
|
387
|
+
* callers and zero outgoing calls (a pure leaf), or vice versa.
|
|
388
|
+
*/
|
|
389
|
+
export async function symbolsCallHierarchyTool(ctx, lang, file, line, col) {
|
|
390
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_call_hierarchy', `${lang}:${file}:${line}:${col}`);
|
|
391
|
+
return guard(ctx, 'symbols_call_hierarchy', toolCallId, async () => {
|
|
392
|
+
const client = ctx.lspClients?.get(lang);
|
|
393
|
+
if (!client)
|
|
394
|
+
return unavailable(lang);
|
|
395
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
396
|
+
const cwd = workspaceForCache(ctx);
|
|
397
|
+
const key = SymbolCache.makeKey(lang, cwd, 'call_hierarchy', { file, line, col });
|
|
398
|
+
const cached = cache?.get(key);
|
|
399
|
+
if (cached !== undefined) {
|
|
400
|
+
return { ok: true, value: { incoming: [...cached.incoming], outgoing: [...cached.outgoing] } };
|
|
401
|
+
}
|
|
402
|
+
const result = await callHierarchyAt(transportFromLspClient(client), file, line, col);
|
|
403
|
+
cache?.set(key, result);
|
|
404
|
+
return { ok: true, value: result };
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* PUGI-78 Phase 1: symbols.implementations.
|
|
409
|
+
*
|
|
410
|
+
* Implementations of an interface / abstract method. Returns the flat
|
|
411
|
+
* list of concrete sites.
|
|
412
|
+
*/
|
|
413
|
+
export async function symbolsImplementationsTool(ctx, lang, file, line, col) {
|
|
414
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_implementations', `${lang}:${file}:${line}:${col}`);
|
|
415
|
+
return guard(ctx, 'symbols_implementations', toolCallId, async () => {
|
|
416
|
+
const client = ctx.lspClients?.get(lang);
|
|
417
|
+
if (!client)
|
|
418
|
+
return unavailable(lang);
|
|
419
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
420
|
+
const cwd = workspaceForCache(ctx);
|
|
421
|
+
const key = SymbolCache.makeKey(lang, cwd, 'implementations', { file, line, col });
|
|
422
|
+
const cached = cache?.get(key);
|
|
423
|
+
if (cached !== undefined) {
|
|
424
|
+
const capped = capSymbolReferences(cached);
|
|
425
|
+
return { ok: true, value: capped.value, ...(capped.truncated ? { truncated: true } : {}) };
|
|
426
|
+
}
|
|
427
|
+
const result = await findImplementations(transportFromLspClient(client), file, line, col);
|
|
428
|
+
cache?.set(key, result);
|
|
429
|
+
const capped = capSymbolReferences(result);
|
|
430
|
+
return { ok: true, value: capped.value, ...(capped.truncated ? { truncated: true } : {}) };
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* PUGI-78 Phase 1: symbols.type_definition.
|
|
435
|
+
*
|
|
436
|
+
* Type definition (vs value definition). Useful for "what type is foo?"
|
|
437
|
+
* questions when the model has a variable and wants its type declaration.
|
|
438
|
+
*/
|
|
439
|
+
export async function symbolsTypeDefinitionTool(ctx, lang, file, line, col) {
|
|
440
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_type_definition', `${lang}:${file}:${line}:${col}`);
|
|
441
|
+
return guard(ctx, 'symbols_type_definition', toolCallId, async () => {
|
|
442
|
+
const client = ctx.lspClients?.get(lang);
|
|
443
|
+
if (!client)
|
|
444
|
+
return unavailable(lang);
|
|
445
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
446
|
+
const cwd = workspaceForCache(ctx);
|
|
447
|
+
const key = SymbolCache.makeKey(lang, cwd, 'type_definition', { file, line, col });
|
|
448
|
+
const cached = cache?.get(key);
|
|
449
|
+
if (cached !== undefined) {
|
|
450
|
+
if (cached === null)
|
|
451
|
+
return notFound('type_definition');
|
|
452
|
+
return { ok: true, value: cached };
|
|
453
|
+
}
|
|
454
|
+
const result = await findTypeDefinition(transportFromLspClient(client), file, line, col);
|
|
455
|
+
cache?.set(key, result);
|
|
456
|
+
if (!result)
|
|
457
|
+
return notFound('type_definition');
|
|
458
|
+
return { ok: true, value: result };
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* PUGI-78 Phase 1: symbols.code_actions.
|
|
463
|
+
*
|
|
464
|
+
* Quick-fix list at the given range. Server-provided actions include
|
|
465
|
+
* auto-import, unused-import removal, refactor extractions, etc.
|
|
466
|
+
*/
|
|
467
|
+
export async function symbolsCodeActionsTool(ctx, lang, file, startLine, startChar, endLine, endChar) {
|
|
468
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_code_actions', `${lang}:${file}:${startLine}:${startChar}-${endLine}:${endChar}`);
|
|
469
|
+
return guard(ctx, 'symbols_code_actions', toolCallId, async () => {
|
|
470
|
+
const client = ctx.lspClients?.get(lang);
|
|
471
|
+
if (!client)
|
|
472
|
+
return unavailable(lang);
|
|
473
|
+
const result = await codeActionsAt(transportFromLspClient(client), file, startLine, startChar, endLine, endChar);
|
|
474
|
+
return { ok: true, value: result };
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* PUGI-78 Phase 1: symbols.format.
|
|
479
|
+
*
|
|
480
|
+
* Formatter — returns the text edits the LSP server would apply for
|
|
481
|
+
* `textDocument/formatting`. Phase 1 is read-only (returns the edits);
|
|
482
|
+
* the dispatcher composes + applies in a future ticket.
|
|
483
|
+
*/
|
|
484
|
+
export async function symbolsFormatTool(ctx, lang, file, options) {
|
|
485
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_format', `${lang}:${file}`);
|
|
486
|
+
return guard(ctx, 'symbols_format', toolCallId, async () => {
|
|
487
|
+
const client = ctx.lspClients?.get(lang);
|
|
488
|
+
if (!client)
|
|
489
|
+
return unavailable(lang);
|
|
490
|
+
const result = await formatFile(transportFromLspClient(client), file, options);
|
|
491
|
+
return { ok: true, value: result };
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* PUGI-78 Phase 1: symbols.diagnostics.
|
|
496
|
+
*
|
|
497
|
+
* Pulls the cached `publishDiagnostics` payload for the file. Mirrors
|
|
498
|
+
* the legacy `lspDiagnostics` tool but exposed via the namespaced
|
|
499
|
+
* `symbols.*` surface for discoverability.
|
|
500
|
+
*/
|
|
501
|
+
export async function symbolsDiagnosticsTool(ctx, lang, file) {
|
|
502
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_diagnostics', `${lang}:${file}`);
|
|
503
|
+
return guard(ctx, 'symbols_diagnostics', toolCallId, async () => {
|
|
504
|
+
const client = ctx.lspClients?.get(lang);
|
|
505
|
+
if (!client)
|
|
506
|
+
return unavailable(lang);
|
|
507
|
+
const result = await diagnosticsFor(transportFromLspClient(client), file);
|
|
508
|
+
const capped = capDiagnostics(result);
|
|
509
|
+
return { ok: true, value: capped.value, ...(capped.truncated ? { truncated: true } : {}) };
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
function workspaceForCache(ctx) {
|
|
513
|
+
// ToolContext exposes `root` (the workspace root). Fall back to a
|
|
514
|
+
// tagged anonymous workspace when somehow absent so the cache still
|
|
515
|
+
// keys deterministically.
|
|
516
|
+
const root = ctx.root;
|
|
517
|
+
if (typeof root === 'string' && root.length > 0)
|
|
518
|
+
return root;
|
|
519
|
+
return '<unknown>';
|
|
520
|
+
}
|
|
521
|
+
function getSymbolCacheFromCtx(ctx) {
|
|
522
|
+
// The agent dispatcher injects an optional cache via the ctx — when
|
|
523
|
+
// present we use the dispatcher-scoped cache so test harnesses can
|
|
524
|
+
// inject a deterministic clock. When absent, we fall back to the
|
|
525
|
+
// process-global cache.
|
|
526
|
+
if (ctx.symbolCache)
|
|
527
|
+
return ctx.symbolCache;
|
|
528
|
+
return getGlobalSymbolCache();
|
|
529
|
+
}
|
|
530
|
+
function notFound(verb) {
|
|
531
|
+
return {
|
|
532
|
+
ok: false,
|
|
533
|
+
reason: 'lsp_not_found',
|
|
534
|
+
detail: `LSP returned no ${verb} for the requested position.`,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function capSymbolReferences(items) {
|
|
538
|
+
const COUNT_CAP = 200;
|
|
539
|
+
if (items.length === 0)
|
|
540
|
+
return { value: items, truncated: false };
|
|
541
|
+
const trimmed = items.slice(0, COUNT_CAP);
|
|
542
|
+
const serialized = JSON.stringify(trimmed);
|
|
543
|
+
if (Buffer.byteLength(serialized, 'utf8') <= LSP_PAYLOAD_CAP_BYTES && trimmed.length === items.length) {
|
|
544
|
+
return { value: trimmed, truncated: false };
|
|
545
|
+
}
|
|
546
|
+
let upper = trimmed.length;
|
|
547
|
+
while (upper > 1) {
|
|
548
|
+
const half = Math.floor(upper / 2);
|
|
549
|
+
const sub = trimmed.slice(0, half);
|
|
550
|
+
if (Buffer.byteLength(JSON.stringify(sub), 'utf8') <= LSP_PAYLOAD_CAP_BYTES) {
|
|
551
|
+
return { value: sub, truncated: true };
|
|
552
|
+
}
|
|
553
|
+
upper = half;
|
|
554
|
+
}
|
|
555
|
+
return { value: trimmed.slice(0, 1), truncated: true };
|
|
556
|
+
}
|
|
187
557
|
/** Test-only surface so specs can poke truncation directly. */
|
|
188
|
-
export const __test__ = {
|
|
558
|
+
export const __test__ = {
|
|
559
|
+
truncate,
|
|
560
|
+
capLocations,
|
|
561
|
+
capDiagnostics,
|
|
562
|
+
capSymbolReferences,
|
|
563
|
+
LSP_PAYLOAD_CAP_BYTES,
|
|
564
|
+
};
|
|
189
565
|
//# sourceMappingURL=lsp-tools.js.map
|
package/dist/tools/registry.js
CHANGED
|
@@ -43,6 +43,29 @@ const registry = [
|
|
|
43
43
|
{ name: 'lsp_diagnostics', permission: 'read', risk: 'low', concurrencySafe: true, m1: true },
|
|
44
44
|
{ name: 'lsp_hover', permission: 'read', risk: 'low', concurrencySafe: true, m1: true },
|
|
45
45
|
{ name: 'lsp_references', permission: 'read', risk: 'low', concurrencySafe: true, m1: true },
|
|
46
|
+
// PUGI-78 Phase 1 — symbols.* namespace. 13 first-class tools that
|
|
47
|
+
// expose the full LSP symbol-aware surface (definition, references,
|
|
48
|
+
// hover, signature, document/workspace symbols, rename preview, call
|
|
49
|
+
// hierarchy, implementations, type definition, code actions,
|
|
50
|
+
// formatter, diagnostics). All read-only in Phase 1 — `rename` /
|
|
51
|
+
// `format` / `code_actions` return PREVIEW edits the dispatcher
|
|
52
|
+
// applies via apply_patch in a future ticket. Permission stays
|
|
53
|
+
// `read` because no workspace mutation happens on dispatch; risk
|
|
54
|
+
// stays `low` because the LSP server is local and the payload is
|
|
55
|
+
// capped at 8 KB per tool.
|
|
56
|
+
{ name: 'symbols_call_hierarchy', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
57
|
+
{ name: 'symbols_code_actions', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
58
|
+
{ name: 'symbols_diagnostics', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
59
|
+
{ name: 'symbols_find_definition', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
60
|
+
{ name: 'symbols_find_references', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
61
|
+
{ name: 'symbols_format', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
62
|
+
{ name: 'symbols_hover', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
63
|
+
{ name: 'symbols_implementations', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
64
|
+
{ name: 'symbols_list_in_file', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
65
|
+
{ name: 'symbols_rename', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
66
|
+
{ name: 'symbols_signature', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
67
|
+
{ name: 'symbols_type_definition', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
68
|
+
{ name: 'symbols_workspace_symbols', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
46
69
|
// β7 L5+T11: multi_edit dispatches an ordered batch of Layer A edits
|
|
47
70
|
// as a single transaction. Risk = medium (same chokepoints as `edit`).
|
|
48
71
|
// concurrencySafe = false because the journal serialises one dispatch
|
package/dist/tui/input-box.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
/**
|
|
3
3
|
* REPL input box - Sprint (REPL UX P0 wave 1).
|
|
4
4
|
*
|
|
5
|
-
* Bordered, cursor-aware input box matching the upstream tool /
|
|
5
|
+
* Bordered, cursor-aware input box matching the upstream tool / peer CLI
|
|
6
6
|
* aesthetics. Layered upgrade over :
|
|
7
7
|
*
|
|
8
8
|
* - Top divider (full-width cyan rule) anchors the input below the
|
package/dist/tui/render.js
CHANGED
|
@@ -26,7 +26,7 @@ export async function renderSplash(cliVersion) {
|
|
|
26
26
|
* Sentinel thrown when the user dismisses the login picker via Esc
|
|
27
27
|
* or `q`. The CLI dispatcher catches it, prints a one-line abort
|
|
28
28
|
* message, and exits 130 (the standard exit code for SIGINT-style
|
|
29
|
-
* user cancellations — matches gh CLI
|
|
29
|
+
* user cancellations — matches gh CLI and peer CLIs).
|
|
30
30
|
*/
|
|
31
31
|
export class LoginCancelledError extends Error {
|
|
32
32
|
constructor() {
|
package/dist/tui/repl.js
CHANGED
|
@@ -292,7 +292,7 @@ function MainArea({ state, personaNames, nowEpochMs, hideToolStream, toolStreamC
|
|
|
292
292
|
// The window over the transcript is small (last 12 rows) so the
|
|
293
293
|
// bottom of the frame stays anchored to the input box. New agents
|
|
294
294
|
// push the operator line up the screen, mirroring the upstream tool /
|
|
295
|
-
//
|
|
295
|
+
// peer CLI / Gemini CLI rendering.
|
|
296
296
|
const conversationSlice = state.transcript.slice(-CONVERSATION_WINDOW);
|
|
297
297
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ConversationPane, { rows: conversationSlice, personaNames: personaNames }), hideToolStream ? null : (_jsx(Box, { marginTop: 1, children: _jsx(ToolStreamPane, { calls: state.toolCalls, collapsed: toolStreamCollapsed }) })), _jsx(Box, { marginTop: 1, children: _jsx(AgentTreePane, { agents: state.agents, nowEpochMs: nowEpochMs }) })] }));
|
|
298
298
|
}
|
package/dist/tui/status-bar.js
CHANGED
|
@@ -137,7 +137,7 @@ export function connectionLabel(connection) {
|
|
|
137
137
|
* anomaly stands out vs the calm cyan baseline.
|
|
138
138
|
* 3. `tool_running` — surfaces the tool label when available
|
|
139
139
|
* (`tool: read`), falls back to `tool` when not.
|
|
140
|
-
* 4. `awaiting_response` — `dispatching` (matches
|
|
140
|
+
* 4. `awaiting_response` — `dispatching` (matches peer CLI's verb
|
|
141
141
|
* for the same state).
|
|
142
142
|
* 5. `completed` — `shipped` (matches the agent tree status glyph
|
|
143
143
|
* so the operator's eye links the two surfaces).
|
|
@@ -14,7 +14,7 @@ export function resolveDisplayedLatest(npmLatest, serverRecommended) {
|
|
|
14
14
|
: npmLatest;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
17
|
+
* peer-CLI-style corner banner: single line, dim orange, right
|
|
18
18
|
* aligned. Renders to the bottom of the REPL frame so it never
|
|
19
19
|
* displaces conversation content. Only mounts when an update is
|
|
20
20
|
* actually available; identical to no-op when the registry poll
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pugi/cli",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.89",
|
|
4
4
|
"description": "Pugi CLI - terminal-native software execution system",
|
|
5
5
|
"homepage": "https://pugi.io",
|
|
6
6
|
"repository": {
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
"undici": "^8.3.0",
|
|
63
63
|
"which": "^6.0.0",
|
|
64
64
|
"zod": "^3.23.0",
|
|
65
|
-
"@pugi/
|
|
66
|
-
"@pugi/
|
|
65
|
+
"@pugi/personas": "0.1.2",
|
|
66
|
+
"@pugi/sdk": "0.1.0-beta.89"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@types/node": "^22.0.0",
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
# Phase 1: the stub responder echoes the directive verbatim — Phase 2
|
|
5
5
|
# wires the slash command through to the live compaction loop. Until
|
|
6
6
|
# then this scenario protects the contract between scenario authoring
|
|
7
|
-
# and the harness envelope shape.
|
|
7
|
+
# and the harness envelope shape. The scenario covers envelope routing,
|
|
8
|
+
# not anti-name-chant, so the persona is free to identify itself on the
|
|
9
|
+
# initial turn.
|
|
8
10
|
|
|
9
11
|
> "/compact --force"
|
|
10
12
|
EXPECT: persona-turn contains "/compact"
|
|
11
|
-
EXPECT_NOT: persona-turn contains "Pugi"
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# scenario: identity
|
|
2
|
-
# title: Pugi self-identifies
|
|
2
|
+
# title: Pugi self-identifies on the very first turn
|
|
3
3
|
|
|
4
4
|
# CEO directive feedback_live_console_test_every_publish: every published
|
|
5
|
-
# beta must still answer "ты кто?" with the Pugi persona
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# beta must still answer "ты кто?" with the Pugi persona on turn 1. This
|
|
6
|
+
# is the single most-asked dialog in CEO dogfood and the regression that
|
|
7
|
+
# has bitten us most often. The intro fires once per session, so the
|
|
8
|
+
# initial envelope is expected to include the brand name verbatim.
|
|
8
9
|
|
|
9
10
|
> "ты кто?"
|
|
10
11
|
EXPECT: persona-turn contains "Pugi" OR "Пуджи"
|
|
11
|
-
EXPECT_NOT: persona-turn contains "
|
|
12
|
+
EXPECT_NOT: persona-turn contains "Мира"
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
# (real dispatch routing wires in Phase 2). The scenario is authored
|
|
6
6
|
# against the Phase 2 contract — for now it documents the intent and
|
|
7
7
|
# the harness keeps it as a soft EXPECT so the corpus stays loadable.
|
|
8
|
+
# The positive EXPECT already covers the brand name on turn 1, so no
|
|
9
|
+
# anti-name-chant assertion is needed here.
|
|
8
10
|
|
|
9
11
|
> "Hiroshi, please review the authentication module"
|
|
10
12
|
EXPECT: persona-turn contains "Pugi" OR "Hiroshi" OR "Хироси"
|
|
11
|
-
EXPECT_NOT: persona-turn contains "Pugi"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# scenario: walkback
|
|
2
2
|
# title: Esc-Esc walkback / rewind directive recognized
|
|
3
3
|
|
|
4
|
-
# CEO parity ask
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
4
|
+
# CEO parity ask: the operator must be able to walk back the last turn.
|
|
5
|
+
# Phase 1 ships the harness contract — the live walkback wiring lands
|
|
6
|
+
# later. The scenario asserts the stub responder acknowledges the
|
|
7
|
+
# directive verbatim so the contract holds while the real implementation
|
|
8
|
+
# lands behind it. Envelope-shape only, not anti-name-chant — the persona
|
|
9
|
+
# is free to identify itself on the initial turn.
|
|
9
10
|
|
|
10
11
|
> "/rewind 1"
|
|
11
12
|
EXPECT: persona-turn contains "/rewind"
|
|
12
|
-
EXPECT_NOT: persona-turn contains "Pugi"
|