@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `pugi login --provider env` — env-var auth path ().
|
|
3
3
|
*
|
|
4
|
-
* the upstream tool,
|
|
4
|
+
* the upstream tool, peer CLI, and gh CLI all ship a way to authenticate via
|
|
5
5
|
* an environment variable so CI / container / scripted contexts can
|
|
6
6
|
* skip the device flow entirely. This module backs that path:
|
|
7
7
|
*
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Per-directory PUGI.md / AGENTS.md / CLAUDE.md / GEMINI.md traverse-up
|
|
3
3
|
* loader — β5a R4+P5.
|
|
4
4
|
*
|
|
5
|
-
* the upstream tool,
|
|
5
|
+
* the upstream tool, peer CLI, and Gemini CLI all support a "walk up from
|
|
6
6
|
* cwd to the workspace root, pick up agent-context markdown at every
|
|
7
7
|
* level" pattern. Without this, a `pugi explain` invoked from
|
|
8
8
|
* `apps/admin-api/` cannot see project-local conventions encoded in
|
package/dist/core/credentials.js
CHANGED
|
@@ -6,7 +6,7 @@ import { z } from 'zod';
|
|
|
6
6
|
* Local credentials store for the Pugi CLI.
|
|
7
7
|
*
|
|
8
8
|
* Stored at `~/.pugi/credentials.json` (mode 0o600). Mirrors the convention
|
|
9
|
-
*
|
|
9
|
+
* peer CLI uses (`~/.codex/auth.json`) and matches gh CLI's per-host
|
|
10
10
|
* token model. The store is intentionally file-based, not OS keychain —
|
|
11
11
|
* adding the native `keytar` dep would force per-platform native builds
|
|
12
12
|
* across npm distribution and complicate the install path. The 0600
|
|
@@ -257,6 +257,23 @@ export class AnvilEngineLoopClient {
|
|
|
257
257
|
message: 'runtime rate limit reached for this tenant',
|
|
258
258
|
};
|
|
259
259
|
}
|
|
260
|
+
// PUGI-490 (2026-06-03): structured envelope for upstream-proxy
|
|
261
|
+
// static error pages. When the response body is HTML (the upstream
|
|
262
|
+
// proxy's static 5xx page rather than a NestJS JSON envelope), the
|
|
263
|
+
// raw truncated HTML is useless to operators — it cannot point at
|
|
264
|
+
// what failed. Parse the `cf-ray` request id so the operator can
|
|
265
|
+
// look the request up in the proxy dashboard, and surface a
|
|
266
|
+
// remediation hint pointing at the engine VM logs.
|
|
267
|
+
const cfDetails = detectUpstreamProxyError(res, text);
|
|
268
|
+
if (cfDetails) {
|
|
269
|
+
return {
|
|
270
|
+
stop: 'error',
|
|
271
|
+
code: 'failed',
|
|
272
|
+
message: `runtime error (cf-ray: ${cfDetails.cfRay ?? 'unknown'}) — ` +
|
|
273
|
+
`upstream proxy returned ${res.status} static page. ` +
|
|
274
|
+
`Check engine VM logs for the correlating request id.`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
260
277
|
return {
|
|
261
278
|
stop: 'error',
|
|
262
279
|
code: 'failed',
|
|
@@ -278,4 +295,50 @@ export class AnvilEngineLoopClient {
|
|
|
278
295
|
}
|
|
279
296
|
}
|
|
280
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* PUGI-490 (2026-06-03): detect when the response body is a static HTML
|
|
300
|
+
* error page emitted by the upstream proxy (rather than a JSON envelope
|
|
301
|
+
* produced by admin-api). Returns the cf-ray request id when matched, so
|
|
302
|
+
* the CLI can surface a meaningful runtime-error envelope instead of
|
|
303
|
+
* truncating raw HTML in front of the operator.
|
|
304
|
+
*
|
|
305
|
+
* Heuristic: the body starts with `<!DOCTYPE` or `<html`, or carries the
|
|
306
|
+
* `cf-ray` response header. We require BOTH conditions when the body is
|
|
307
|
+
* non-empty (defense against admin-api accidentally emitting an HTML
|
|
308
|
+
* fragment) and accept just the header when the body is empty (some
|
|
309
|
+
* proxy 5xx variants ship empty bodies). The function is intentionally
|
|
310
|
+
* permissive — false positives produce a clearer error than false
|
|
311
|
+
* negatives, and the message still includes "upstream proxy" so the
|
|
312
|
+
* operator knows the call did not reach a Pugi controller.
|
|
313
|
+
*
|
|
314
|
+
* Exported for unit testing; not for runtime callers.
|
|
315
|
+
*/
|
|
316
|
+
export function detectUpstreamProxyError(res, body) {
|
|
317
|
+
if (res.status < 500)
|
|
318
|
+
return null;
|
|
319
|
+
// Pull cf-ray via the same getter shim the version-interceptor uses;
|
|
320
|
+
// it tolerates both real `Response.headers.get` and fixture/stub
|
|
321
|
+
// headers represented as plain objects.
|
|
322
|
+
const h = res.headers;
|
|
323
|
+
const readHeader = (name) => {
|
|
324
|
+
if (h && typeof h.get === 'function') {
|
|
325
|
+
return h.get(name);
|
|
326
|
+
}
|
|
327
|
+
if (h && typeof h === 'object') {
|
|
328
|
+
const lowered = h[name.toLowerCase()];
|
|
329
|
+
return lowered ?? null;
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
};
|
|
333
|
+
const cfRay = readHeader('cf-ray');
|
|
334
|
+
const bodyTrimmed = (body ?? '').trimStart();
|
|
335
|
+
const looksHtml = bodyTrimmed.startsWith('<!DOCTYPE') ||
|
|
336
|
+
bodyTrimmed.startsWith('<!doctype') ||
|
|
337
|
+
bodyTrimmed.startsWith('<html');
|
|
338
|
+
if (looksHtml)
|
|
339
|
+
return { cfRay };
|
|
340
|
+
if (cfRay && bodyTrimmed.length === 0)
|
|
341
|
+
return { cfRay };
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
281
344
|
//# sourceMappingURL=anvil-client.js.map
|
|
@@ -1127,7 +1127,7 @@ function extractPathArg(raw) {
|
|
|
1127
1127
|
const parsed = JSON.parse(raw);
|
|
1128
1128
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
1129
1129
|
const obj = parsed;
|
|
1130
|
-
// Accept canonical `path` OR the
|
|
1130
|
+
// Accept canonical `path` OR the peer-CLI-trained `filePath`
|
|
1131
1131
|
// alias so the filesChanged summary captures writes regardless of
|
|
1132
1132
|
// which key the model emitted. Without the alias the operator
|
|
1133
1133
|
// sees "Files modified: none" even when a write actually landed,
|
|
@@ -27,6 +27,14 @@ import { stripInternalFields } from './strip-internal-fields.js';
|
|
|
27
27
|
import { applyAskAnswer, gate as permissionGate, getToolClass, PermissionDenied, } from '../permissions/index.js';
|
|
28
28
|
import { RetryBudget, RetryBudgetExhausted, hashArgs } from '../retry-budget/index.js';
|
|
29
29
|
import { runPostEditDiagnostics, } from '../lsp/post-edit-diagnostics.js';
|
|
30
|
+
// PUGI-78 Phase 1: symbols.* tool wrappers + LSP client cache. The
|
|
31
|
+
// dispatcher resolves the LSP client by `lang` via `getOrStartLspClient`
|
|
32
|
+
// (cold-start cost amortised across the session) and hands it to the
|
|
33
|
+
// matching wrapper. The wrappers serialise the result for the engine
|
|
34
|
+
// envelope.
|
|
35
|
+
import { getOrStartLspClient } from '../lsp/cache.js';
|
|
36
|
+
import { symbolsCallHierarchyTool, symbolsCodeActionsTool, symbolsDiagnosticsTool, symbolsFindDefinitionTool, symbolsFindReferencesTool, symbolsFormatTool, symbolsHoverTool, symbolsImplementationsTool, symbolsListInFileTool, symbolsRenameTool, symbolsSignatureTool, symbolsTypeDefinitionTool, symbolsWorkspaceSymbolsTool, } from '../../tools/lsp-tools.js';
|
|
37
|
+
import { getGlobalSymbolCache } from '../lsp/symbol-cache.js';
|
|
30
38
|
/**
|
|
31
39
|
* Tool-bridge: turns the abstract tool registry into:
|
|
32
40
|
* 1. An OpenAI-shaped tools schema for `EngineLoopClient.send`.
|
|
@@ -94,6 +102,27 @@ const READ_ONLY_TOOLS = new Set([
|
|
|
94
102
|
// web_fetch. Plan mode keeps the tool available because reading the
|
|
95
103
|
// web is part of how a plan is researched.
|
|
96
104
|
'web_search',
|
|
105
|
+
// PUGI-78 Phase 1: symbols.* namespace is read-only in Phase 1
|
|
106
|
+
// (rename / format / code_actions return PREVIEW edits, the
|
|
107
|
+
// dispatcher applies via apply_patch in Phase 2). Plan-mode KEEPS
|
|
108
|
+
// these tools available — navigation / outline / call-hierarchy
|
|
109
|
+
// questions are the bread-and-butter of a planning loop, and the
|
|
110
|
+
// surface explicitly never mutates source in this phase. When Phase
|
|
111
|
+
// 2 lifts to "apply on confirm", the apply path moves OUT of
|
|
112
|
+
// symbols.* and into apply_patch, which is already plan-mode-gated.
|
|
113
|
+
'symbols_call_hierarchy',
|
|
114
|
+
'symbols_code_actions',
|
|
115
|
+
'symbols_diagnostics',
|
|
116
|
+
'symbols_find_definition',
|
|
117
|
+
'symbols_find_references',
|
|
118
|
+
'symbols_format',
|
|
119
|
+
'symbols_hover',
|
|
120
|
+
'symbols_implementations',
|
|
121
|
+
'symbols_list_in_file',
|
|
122
|
+
'symbols_rename',
|
|
123
|
+
'symbols_signature',
|
|
124
|
+
'symbols_type_definition',
|
|
125
|
+
'symbols_workspace_symbols',
|
|
97
126
|
]);
|
|
98
127
|
/**
|
|
99
128
|
* Tools the engine loop dispatches. β1 expands the M1 cornerstone six
|
|
@@ -152,6 +181,24 @@ const WIRED_TOOLS = new Set([
|
|
|
152
181
|
// through the same security gate as Layer A/B/C; not advertised in
|
|
153
182
|
// plan mode (mutation surface).
|
|
154
183
|
'multi_edit',
|
|
184
|
+
// PUGI-78 Phase 1: symbols.* namespace (13 LSP-bridged tools). All
|
|
185
|
+
// read-only in Phase 1 — `rename` / `format` / `code_actions`
|
|
186
|
+
// return PREVIEW edits; the dispatcher applies via apply_patch in
|
|
187
|
+
// Phase 2 (PUGI-134). The executor dispatch wires each name to the
|
|
188
|
+
// matching `symbols*Tool` wrapper in `src/tools/lsp-tools.ts`.
|
|
189
|
+
'symbols_call_hierarchy',
|
|
190
|
+
'symbols_code_actions',
|
|
191
|
+
'symbols_diagnostics',
|
|
192
|
+
'symbols_find_definition',
|
|
193
|
+
'symbols_find_references',
|
|
194
|
+
'symbols_format',
|
|
195
|
+
'symbols_hover',
|
|
196
|
+
'symbols_implementations',
|
|
197
|
+
'symbols_list_in_file',
|
|
198
|
+
'symbols_rename',
|
|
199
|
+
'symbols_signature',
|
|
200
|
+
'symbols_type_definition',
|
|
201
|
+
'symbols_workspace_symbols',
|
|
155
202
|
]);
|
|
156
203
|
export function buildToolsSchema(kind, options = { allowFetch: false, allowSearch: false }) {
|
|
157
204
|
const planMode = kind === 'plan';
|
|
@@ -372,6 +419,162 @@ export function buildToolsSchema(kind, options = { allowFetch: false, allowSearc
|
|
|
372
419
|
properties: { name: { type: 'string' } },
|
|
373
420
|
},
|
|
374
421
|
});
|
|
422
|
+
// PUGI-78 Phase 1: symbols.* namespace (13 tools). LSP-bridged
|
|
423
|
+
// symbol-aware operations that replace whole-file reads for
|
|
424
|
+
// navigation questions. Each tool returns 1-2 KB of shaped JSON
|
|
425
|
+
// vs 5-50 KB for the raw file read it replaces — 10-100x token
|
|
426
|
+
// savings per refactor turn. All read-only in Phase 1 — `rename`
|
|
427
|
+
// / `format` / `code_actions` return PREVIEW edits the dispatcher
|
|
428
|
+
// applies via apply_patch in a future ticket.
|
|
429
|
+
//
|
|
430
|
+
// Schemas use the same JSON-Schema shape as the rest of the
|
|
431
|
+
// toolbox: required positional args, additionalProperties: false.
|
|
432
|
+
// The `lang` field is a closed enum of the 5 supported language
|
|
433
|
+
// server slugs (ts/js/py/go/rust). The dispatcher resolves the
|
|
434
|
+
// LSP client by lang BEFORE calling the underlying primitive.
|
|
435
|
+
const symbolsLangEnum = { type: 'string', enum: ['ts', 'js', 'py', 'go', 'rust'], description: 'LSP language slug.' };
|
|
436
|
+
const symbolsPosArgs = {
|
|
437
|
+
lang: symbolsLangEnum,
|
|
438
|
+
file: { type: 'string', description: 'Workspace-relative file path.' },
|
|
439
|
+
line: { type: 'integer', minimum: 0, description: 'Zero-based line index.' },
|
|
440
|
+
col: { type: 'integer', minimum: 0, description: 'Zero-based character index.' },
|
|
441
|
+
};
|
|
442
|
+
toolDefs.push({
|
|
443
|
+
name: 'symbols_find_definition',
|
|
444
|
+
description: 'Locate the definition of the symbol at (line, col) in <file>. Returns {file, line, character}. PREFER over read for "where is X defined?" questions.',
|
|
445
|
+
parameters: {
|
|
446
|
+
type: 'object',
|
|
447
|
+
additionalProperties: false,
|
|
448
|
+
required: ['lang', 'file', 'line', 'col'],
|
|
449
|
+
properties: symbolsPosArgs,
|
|
450
|
+
},
|
|
451
|
+
}, {
|
|
452
|
+
name: 'symbols_find_references',
|
|
453
|
+
description: 'List every reference to the symbol at (line, col). Returns {file, line, character}[]. PREFER over grep for "find callers of X" questions.',
|
|
454
|
+
parameters: {
|
|
455
|
+
type: 'object',
|
|
456
|
+
additionalProperties: false,
|
|
457
|
+
required: ['lang', 'file', 'line', 'col'],
|
|
458
|
+
properties: symbolsPosArgs,
|
|
459
|
+
},
|
|
460
|
+
}, {
|
|
461
|
+
name: 'symbols_list_in_file',
|
|
462
|
+
description: 'Outline the symbols (functions, classes, methods) defined in <file>. Returns flat {name, kind, line, character, containerName}[]. PREFER over read for "outline" questions.',
|
|
463
|
+
parameters: {
|
|
464
|
+
type: 'object',
|
|
465
|
+
additionalProperties: false,
|
|
466
|
+
required: ['lang', 'file'],
|
|
467
|
+
properties: { lang: symbolsLangEnum, file: { type: 'string' } },
|
|
468
|
+
},
|
|
469
|
+
}, {
|
|
470
|
+
name: 'symbols_rename',
|
|
471
|
+
description: 'Preview a rename refactor for the symbol at (line, col) to <newName>. Returns {files, edits}[]. PREVIEW ONLY in Phase 1 — operator confirms then dispatches apply_patch.',
|
|
472
|
+
parameters: {
|
|
473
|
+
type: 'object',
|
|
474
|
+
additionalProperties: false,
|
|
475
|
+
required: ['lang', 'file', 'line', 'col', 'newName'],
|
|
476
|
+
properties: {
|
|
477
|
+
...symbolsPosArgs,
|
|
478
|
+
newName: { type: 'string', minLength: 1, description: 'New symbol name.' },
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
}, {
|
|
482
|
+
name: 'symbols_hover',
|
|
483
|
+
description: 'Type info + docstring at (line, col). Returns {content, range?}. Body capped at 4 KB.',
|
|
484
|
+
parameters: {
|
|
485
|
+
type: 'object',
|
|
486
|
+
additionalProperties: false,
|
|
487
|
+
required: ['lang', 'file', 'line', 'col'],
|
|
488
|
+
properties: symbolsPosArgs,
|
|
489
|
+
},
|
|
490
|
+
}, {
|
|
491
|
+
name: 'symbols_signature',
|
|
492
|
+
description: 'Function signature at a call site. Returns {label, parameters, activeParameter?}. NULL when cursor is not inside a call.',
|
|
493
|
+
parameters: {
|
|
494
|
+
type: 'object',
|
|
495
|
+
additionalProperties: false,
|
|
496
|
+
required: ['lang', 'file', 'line', 'col'],
|
|
497
|
+
properties: symbolsPosArgs,
|
|
498
|
+
},
|
|
499
|
+
}, {
|
|
500
|
+
name: 'symbols_workspace_symbols',
|
|
501
|
+
description: 'Workspace-wide fuzzy symbol search. Server-defined match (substring / fuzzy / prefix). Returns {name, file, line, kind, containerName?}[].',
|
|
502
|
+
parameters: {
|
|
503
|
+
type: 'object',
|
|
504
|
+
additionalProperties: false,
|
|
505
|
+
required: ['lang', 'query'],
|
|
506
|
+
properties: {
|
|
507
|
+
lang: symbolsLangEnum,
|
|
508
|
+
query: { type: 'string', minLength: 1, description: 'Symbol query.' },
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
}, {
|
|
512
|
+
name: 'symbols_call_hierarchy',
|
|
513
|
+
description: 'Incoming + outgoing callers for the symbol at (line, col). Returns {incoming, outgoing}[].',
|
|
514
|
+
parameters: {
|
|
515
|
+
type: 'object',
|
|
516
|
+
additionalProperties: false,
|
|
517
|
+
required: ['lang', 'file', 'line', 'col'],
|
|
518
|
+
properties: symbolsPosArgs,
|
|
519
|
+
},
|
|
520
|
+
}, {
|
|
521
|
+
name: 'symbols_implementations',
|
|
522
|
+
description: 'Concrete implementations of the interface / abstract method at (line, col). Returns flat references list.',
|
|
523
|
+
parameters: {
|
|
524
|
+
type: 'object',
|
|
525
|
+
additionalProperties: false,
|
|
526
|
+
required: ['lang', 'file', 'line', 'col'],
|
|
527
|
+
properties: symbolsPosArgs,
|
|
528
|
+
},
|
|
529
|
+
}, {
|
|
530
|
+
name: 'symbols_type_definition',
|
|
531
|
+
description: 'Type definition (vs value definition) of the symbol at (line, col). Returns the type declaration location.',
|
|
532
|
+
parameters: {
|
|
533
|
+
type: 'object',
|
|
534
|
+
additionalProperties: false,
|
|
535
|
+
required: ['lang', 'file', 'line', 'col'],
|
|
536
|
+
properties: symbolsPosArgs,
|
|
537
|
+
},
|
|
538
|
+
}, {
|
|
539
|
+
name: 'symbols_code_actions',
|
|
540
|
+
description: 'Quick-fix list at the given range. Returns {title, kind?, isPreferred?}[].',
|
|
541
|
+
parameters: {
|
|
542
|
+
type: 'object',
|
|
543
|
+
additionalProperties: false,
|
|
544
|
+
required: ['lang', 'file', 'startLine', 'startChar', 'endLine', 'endChar'],
|
|
545
|
+
properties: {
|
|
546
|
+
lang: symbolsLangEnum,
|
|
547
|
+
file: { type: 'string' },
|
|
548
|
+
startLine: { type: 'integer', minimum: 0 },
|
|
549
|
+
startChar: { type: 'integer', minimum: 0 },
|
|
550
|
+
endLine: { type: 'integer', minimum: 0 },
|
|
551
|
+
endChar: { type: 'integer', minimum: 0 },
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
}, {
|
|
555
|
+
name: 'symbols_format',
|
|
556
|
+
description: 'Formatter — returns the text edits the LSP server would apply. PREVIEW ONLY in Phase 1.',
|
|
557
|
+
parameters: {
|
|
558
|
+
type: 'object',
|
|
559
|
+
additionalProperties: false,
|
|
560
|
+
required: ['lang', 'file'],
|
|
561
|
+
properties: {
|
|
562
|
+
lang: symbolsLangEnum,
|
|
563
|
+
file: { type: 'string' },
|
|
564
|
+
tabSize: { type: 'integer', minimum: 1 },
|
|
565
|
+
insertSpaces: { type: 'boolean' },
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
}, {
|
|
569
|
+
name: 'symbols_diagnostics',
|
|
570
|
+
description: 'Cached LSP diagnostics (error / warning / info / hint) for <file>. Returns up to ~50 entries.',
|
|
571
|
+
parameters: {
|
|
572
|
+
type: 'object',
|
|
573
|
+
additionalProperties: false,
|
|
574
|
+
required: ['lang', 'file'],
|
|
575
|
+
properties: { lang: symbolsLangEnum, file: { type: 'string' } },
|
|
576
|
+
},
|
|
577
|
+
});
|
|
375
578
|
// β1 T5 → β1a r1 (gating fix): WebFetch wire-in. Schema
|
|
376
579
|
// mirrors the existing tool surface in
|
|
377
580
|
// `apps/pugi-cli/src/tools/web-fetch.ts`. SSRF guard runs inside the
|
|
@@ -989,6 +1192,16 @@ export function buildExecutor(input) {
|
|
|
989
1192
|
}
|
|
990
1193
|
return dispatchAgent(args, agentDispatch);
|
|
991
1194
|
}
|
|
1195
|
+
// PUGI-78 Phase 1: symbols.* namespace dispatch. Every name in
|
|
1196
|
+
// SYMBOLS_TOOL_NAMES routes through `dispatchSymbolsTool` which
|
|
1197
|
+
// resolves the LSP client by `lang`, calls the matching
|
|
1198
|
+
// `symbols*Tool` wrapper in `src/tools/lsp-tools.ts`, and
|
|
1199
|
+
// serialises the result for the engine envelope. Read-only —
|
|
1200
|
+
// matches the registry posture; the post-edit diagnostics hook
|
|
1201
|
+
// below is the only mutation the symbols.* surface participates in.
|
|
1202
|
+
if (SYMBOLS_TOOL_NAMES.has(name)) {
|
|
1203
|
+
return dispatchSymbolsTool(name, args, ctx);
|
|
1204
|
+
}
|
|
992
1205
|
return dispatchTool(name, args, ctx);
|
|
993
1206
|
};
|
|
994
1207
|
try {
|
|
@@ -1514,9 +1727,232 @@ const POST_EDIT_TOOLS = new Set(['edit', 'write', 'multi_edit']);
|
|
|
1514
1727
|
* OR `PUGI_LSP_POST_EDIT=1`. Off by default until dogfood validates
|
|
1515
1728
|
* the cold-start cost vs the model-loop benefit ().
|
|
1516
1729
|
*/
|
|
1730
|
+
/**
|
|
1731
|
+
* PUGI-78 Phase 1: dispatched-name allowlist for the symbols.* router.
|
|
1732
|
+
* Sourced from the same list as the JSON-schema additions in
|
|
1733
|
+
* `buildToolsSchema` + the registry entries in `tools/registry.ts` — a
|
|
1734
|
+
* mismatch would surface as either an advertised-but-unrouted tool
|
|
1735
|
+
* (codex review P1 from this PR) or an unknown-tool denial.
|
|
1736
|
+
*/
|
|
1737
|
+
const SYMBOLS_TOOL_NAMES = new Set([
|
|
1738
|
+
'symbols_call_hierarchy',
|
|
1739
|
+
'symbols_code_actions',
|
|
1740
|
+
'symbols_diagnostics',
|
|
1741
|
+
'symbols_find_definition',
|
|
1742
|
+
'symbols_find_references',
|
|
1743
|
+
'symbols_format',
|
|
1744
|
+
'symbols_hover',
|
|
1745
|
+
'symbols_implementations',
|
|
1746
|
+
'symbols_list_in_file',
|
|
1747
|
+
'symbols_rename',
|
|
1748
|
+
'symbols_signature',
|
|
1749
|
+
'symbols_type_definition',
|
|
1750
|
+
'symbols_workspace_symbols',
|
|
1751
|
+
]);
|
|
1752
|
+
/**
|
|
1753
|
+
* PUGI-78 Phase 1: dispatch a symbols.* tool call. Common pre-flight
|
|
1754
|
+
* (validate `lang`, resolve / spawn the LSP client via the warm cache,
|
|
1755
|
+
* build an `LspToolContext` from the engine `ToolContext`) lives here so
|
|
1756
|
+
* each per-tool branch stays a thin shim around the matching wrapper.
|
|
1757
|
+
*
|
|
1758
|
+
* Failure shape: the wrappers return a structured `{ok, value, reason}`
|
|
1759
|
+
* record — we JSON-stringify it for the engine envelope. The engine
|
|
1760
|
+
* adapter treats a string return as the tool result text; the wrappers
|
|
1761
|
+
* never throw (errors are caught and folded into the structured
|
|
1762
|
+
* `ok: false` record), so the dispatch path is safe to call without a
|
|
1763
|
+
* try/catch wrapper here.
|
|
1764
|
+
*/
|
|
1765
|
+
async function dispatchSymbolsTool(name, args, ctx) {
|
|
1766
|
+
const langRaw = args['lang'];
|
|
1767
|
+
const validLangs = ['ts', 'js', 'py', 'go', 'rust'];
|
|
1768
|
+
if (typeof langRaw !== 'string' || !validLangs.includes(langRaw)) {
|
|
1769
|
+
return JSON.stringify({
|
|
1770
|
+
ok: false,
|
|
1771
|
+
reason: 'invalid_argument',
|
|
1772
|
+
detail: `${name}: 'lang' must be one of ts | js | py | go | rust (got: ${langRaw})`,
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
const lang = langRaw;
|
|
1776
|
+
// Resolve (and lazily start) the LSP client. The cache holds the
|
|
1777
|
+
// client across the session so the cold start is paid once.
|
|
1778
|
+
const lspOpts = {
|
|
1779
|
+
cwd: ctx.root,
|
|
1780
|
+
...(ctx.settings.lsp ? { lspSettings: ctx.settings.lsp } : {}),
|
|
1781
|
+
};
|
|
1782
|
+
const lspResult = await getOrStartLspClient(lang, lspOpts);
|
|
1783
|
+
const lspToolCtx = {
|
|
1784
|
+
...ctx,
|
|
1785
|
+
...(lspResult.ok
|
|
1786
|
+
? { lspClients: new Map([[lang, lspResult.client]]) }
|
|
1787
|
+
: {}),
|
|
1788
|
+
symbolCache: getGlobalSymbolCache(),
|
|
1789
|
+
};
|
|
1790
|
+
// Validate required positional args per tool. Each tool that needs
|
|
1791
|
+
// a position rejects missing / non-finite / negative coordinates
|
|
1792
|
+
// with a structured `invalid_argument` envelope so the model sees
|
|
1793
|
+
// a clear correction signal instead of a silent (0,0) coercion that
|
|
1794
|
+
// would otherwise yield `lsp_not_found`.
|
|
1795
|
+
const requiresFile = name !== 'symbols_workspace_symbols';
|
|
1796
|
+
const requiresPosition = name === 'symbols_find_definition' ||
|
|
1797
|
+
name === 'symbols_find_references' ||
|
|
1798
|
+
name === 'symbols_hover' ||
|
|
1799
|
+
name === 'symbols_signature' ||
|
|
1800
|
+
name === 'symbols_implementations' ||
|
|
1801
|
+
name === 'symbols_type_definition' ||
|
|
1802
|
+
name === 'symbols_call_hierarchy' ||
|
|
1803
|
+
name === 'symbols_rename';
|
|
1804
|
+
const file = typeof args['file'] === 'string' ? args['file'] : '';
|
|
1805
|
+
if (requiresFile && file.length === 0) {
|
|
1806
|
+
return JSON.stringify({
|
|
1807
|
+
ok: false,
|
|
1808
|
+
reason: 'invalid_argument',
|
|
1809
|
+
detail: `${name}: 'file' must be a non-empty workspace-relative path`,
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
const lineRaw = args['line'];
|
|
1813
|
+
const colRaw = args['col'];
|
|
1814
|
+
if (requiresPosition) {
|
|
1815
|
+
if (typeof lineRaw !== 'number' || !Number.isFinite(lineRaw) || lineRaw < 0) {
|
|
1816
|
+
return JSON.stringify({
|
|
1817
|
+
ok: false,
|
|
1818
|
+
reason: 'invalid_argument',
|
|
1819
|
+
detail: `${name}: 'line' must be a non-negative integer`,
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
if (typeof colRaw !== 'number' || !Number.isFinite(colRaw) || colRaw < 0) {
|
|
1823
|
+
return JSON.stringify({
|
|
1824
|
+
ok: false,
|
|
1825
|
+
reason: 'invalid_argument',
|
|
1826
|
+
detail: `${name}: 'col' must be a non-negative integer`,
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
const line = typeof lineRaw === 'number' ? lineRaw : 0;
|
|
1831
|
+
const col = typeof colRaw === 'number' ? colRaw : 0;
|
|
1832
|
+
try {
|
|
1833
|
+
switch (name) {
|
|
1834
|
+
case 'symbols_find_definition': {
|
|
1835
|
+
const result = await symbolsFindDefinitionTool(lspToolCtx, lang, file, line, col);
|
|
1836
|
+
return JSON.stringify(result);
|
|
1837
|
+
}
|
|
1838
|
+
case 'symbols_find_references': {
|
|
1839
|
+
const result = await symbolsFindReferencesTool(lspToolCtx, lang, file, line, col);
|
|
1840
|
+
return JSON.stringify(result);
|
|
1841
|
+
}
|
|
1842
|
+
case 'symbols_list_in_file': {
|
|
1843
|
+
const result = await symbolsListInFileTool(lspToolCtx, lang, file);
|
|
1844
|
+
return JSON.stringify(result);
|
|
1845
|
+
}
|
|
1846
|
+
case 'symbols_rename': {
|
|
1847
|
+
const newName = typeof args['newName'] === 'string' ? args['newName'] : '';
|
|
1848
|
+
const result = await symbolsRenameTool(lspToolCtx, lang, file, line, col, newName);
|
|
1849
|
+
return JSON.stringify(result);
|
|
1850
|
+
}
|
|
1851
|
+
case 'symbols_hover': {
|
|
1852
|
+
const result = await symbolsHoverTool(lspToolCtx, lang, file, line, col);
|
|
1853
|
+
return JSON.stringify(result);
|
|
1854
|
+
}
|
|
1855
|
+
case 'symbols_signature': {
|
|
1856
|
+
const result = await symbolsSignatureTool(lspToolCtx, lang, file, line, col);
|
|
1857
|
+
return JSON.stringify(result);
|
|
1858
|
+
}
|
|
1859
|
+
case 'symbols_workspace_symbols': {
|
|
1860
|
+
const query = typeof args['query'] === 'string' ? args['query'] : '';
|
|
1861
|
+
const result = await symbolsWorkspaceSymbolsTool(lspToolCtx, lang, query);
|
|
1862
|
+
return JSON.stringify(result);
|
|
1863
|
+
}
|
|
1864
|
+
case 'symbols_call_hierarchy': {
|
|
1865
|
+
const result = await symbolsCallHierarchyTool(lspToolCtx, lang, file, line, col);
|
|
1866
|
+
return JSON.stringify(result);
|
|
1867
|
+
}
|
|
1868
|
+
case 'symbols_implementations': {
|
|
1869
|
+
const result = await symbolsImplementationsTool(lspToolCtx, lang, file, line, col);
|
|
1870
|
+
return JSON.stringify(result);
|
|
1871
|
+
}
|
|
1872
|
+
case 'symbols_type_definition': {
|
|
1873
|
+
const result = await symbolsTypeDefinitionTool(lspToolCtx, lang, file, line, col);
|
|
1874
|
+
return JSON.stringify(result);
|
|
1875
|
+
}
|
|
1876
|
+
case 'symbols_code_actions': {
|
|
1877
|
+
// Validate every coordinate of the range. Same gate as the
|
|
1878
|
+
// position-args check above — silent (0,0,0,0) coercion would
|
|
1879
|
+
// hide schema-noncompliant calls and yield an empty action
|
|
1880
|
+
// list instead of a clear correction signal.
|
|
1881
|
+
const coords = ['startLine', 'startChar', 'endLine', 'endChar'];
|
|
1882
|
+
for (const k of coords) {
|
|
1883
|
+
const v = args[k];
|
|
1884
|
+
if (typeof v !== 'number' || !Number.isFinite(v) || v < 0) {
|
|
1885
|
+
return JSON.stringify({
|
|
1886
|
+
ok: false,
|
|
1887
|
+
reason: 'invalid_argument',
|
|
1888
|
+
detail: `symbols_code_actions: '${k}' must be a non-negative integer`,
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
const startLine = args['startLine'];
|
|
1893
|
+
const startChar = args['startChar'];
|
|
1894
|
+
const endLine = args['endLine'];
|
|
1895
|
+
const endChar = args['endChar'];
|
|
1896
|
+
const result = await symbolsCodeActionsTool(lspToolCtx, lang, file, startLine, startChar, endLine, endChar);
|
|
1897
|
+
return JSON.stringify(result);
|
|
1898
|
+
}
|
|
1899
|
+
case 'symbols_format': {
|
|
1900
|
+
const tabSize = typeof args['tabSize'] === 'number' ? args['tabSize'] : undefined;
|
|
1901
|
+
const insertSpaces = typeof args['insertSpaces'] === 'boolean' ? args['insertSpaces'] : undefined;
|
|
1902
|
+
const options = {};
|
|
1903
|
+
if (typeof tabSize === 'number')
|
|
1904
|
+
options.tabSize = tabSize;
|
|
1905
|
+
if (typeof insertSpaces === 'boolean')
|
|
1906
|
+
options.insertSpaces = insertSpaces;
|
|
1907
|
+
const result = await symbolsFormatTool(lspToolCtx, lang, file, options);
|
|
1908
|
+
return JSON.stringify(result);
|
|
1909
|
+
}
|
|
1910
|
+
case 'symbols_diagnostics': {
|
|
1911
|
+
const result = await symbolsDiagnosticsTool(lspToolCtx, lang, file);
|
|
1912
|
+
return JSON.stringify(result);
|
|
1913
|
+
}
|
|
1914
|
+
default:
|
|
1915
|
+
return JSON.stringify({
|
|
1916
|
+
ok: false,
|
|
1917
|
+
reason: 'unknown_symbols_tool',
|
|
1918
|
+
detail: `${name} is not a recognised symbols.* tool`,
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
catch (error) {
|
|
1923
|
+
// The wrappers fold errors into structured ok:false records, so
|
|
1924
|
+
// this branch is defense-in-depth for a refactor regression. If
|
|
1925
|
+
// any wrapper ever throws, we surface a stable shape to the model
|
|
1926
|
+
// instead of leaking the raw exception type.
|
|
1927
|
+
return JSON.stringify({
|
|
1928
|
+
ok: false,
|
|
1929
|
+
reason: 'symbols_dispatch_error',
|
|
1930
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1517
1934
|
async function appendPostEditDiagnostics(name, args, ctx, result) {
|
|
1518
1935
|
if (!POST_EDIT_TOOLS.has(name))
|
|
1519
1936
|
return result;
|
|
1937
|
+
// PUGI-78 Phase 1 (codex P2 fix): a successful edit / write / multi_edit
|
|
1938
|
+
// invalidates the symbol cache for this workspace. Without this, a
|
|
1939
|
+
// subsequent symbols.* query inside the same 5-minute TTL window could
|
|
1940
|
+
// return stale line numbers / outlines / references / hover from
|
|
1941
|
+
// before the edit. We invalidate BEFORE the post-edit diagnostics
|
|
1942
|
+
// gate so the invalidation fires even when post-edit-diagnostics is
|
|
1943
|
+
// disabled (the cache concern is independent of the diagnostics
|
|
1944
|
+
// surface). The invalidation is process-global; subagents that share
|
|
1945
|
+
// the same Node process share the cache, so the invalidation
|
|
1946
|
+
// propagates without an additional hop.
|
|
1947
|
+
try {
|
|
1948
|
+
const cache = getGlobalSymbolCache();
|
|
1949
|
+
cache.invalidateWorkspace(ctx.root);
|
|
1950
|
+
}
|
|
1951
|
+
catch {
|
|
1952
|
+
// Defense-in-depth — cache invalidation must never block the
|
|
1953
|
+
// engine's tool envelope. A throw here is a soft contract
|
|
1954
|
+
// violation but recoverable.
|
|
1955
|
+
}
|
|
1520
1956
|
if (!isPostEditEnabled(ctx))
|
|
1521
1957
|
return result;
|
|
1522
1958
|
const paths = extractEditedPaths(name, args);
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*
|
|
26
26
|
* Brand voice: ASCII only, no emoji, no em-dashes, no marketing prose.
|
|
27
27
|
*/
|
|
28
|
-
/** Events the MVP actually fires. The
|
|
28
|
+
/** Events the MVP actually fires. The remaining events live in the */
|
|
29
29
|
/** type but no integration point emits them yet. */
|
|
30
30
|
export const MVP_HOOK_EVENTS = [
|
|
31
31
|
'SessionStart',
|
|
@@ -40,5 +40,7 @@ export const ALL_HOOK_EVENTS_V2 = [
|
|
|
40
40
|
'SubagentStop',
|
|
41
41
|
'PreCompact',
|
|
42
42
|
'Notification',
|
|
43
|
+
'WorktreeCreate',
|
|
44
|
+
'WorktreeRemove',
|
|
43
45
|
];
|
|
44
46
|
//# sourceMappingURL=events.js.map
|