praxis-agent 0.56.0 → 0.57.0

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 CHANGED
@@ -173,8 +173,9 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
173
173
  context budgets; print mode,
174
174
  structured JSON/JSONL, context compaction, tool loops, and bounded execution.
175
175
  - **Built-in tools** — read, write, edit, `ApplyPatch` for bounded ordered exact
176
- multi-file replacements, glob, search, shell, notebook, PDF, image, web,
177
- scheduled prompts, workflows, and worktrees.
176
+ multi-file replacements, configured plugin LSP navigation with fresh bounded
177
+ diagnostics after successful edits, glob, search, shell, notebook, PDF,
178
+ image, web, scheduled prompts, workflows, and worktrees.
178
179
  - **Shell lifecycle** — foreground Bash allows up to 10 minutes and carries a
179
180
  validated final working directory across calls in the same session without
180
181
  leaking state across sessions or overriding an explicit `/cd`.
@@ -12,11 +12,14 @@ export declare class ClaudeLspToolManager {
12
12
  environment?: Readonly<Record<string, string | undefined>>;
13
13
  });
14
14
  registry(base: ToolRegistry): ToolRegistry;
15
+ private discardConnectionsOutside;
16
+ private runtimeCwd;
15
17
  private validatedFile;
16
18
  private candidates;
17
19
  private languageId;
18
20
  private connection;
19
21
  execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
22
+ enrichMutationResult(call: ModelToolCall, context: ToolExecutionContext, result: ToolExecutionResult): Promise<ToolExecutionResult>;
20
23
  close(): Promise<void>;
21
24
  }
22
25
  //# sourceMappingURL=claude-lsp-tool.d.ts.map
@@ -1,16 +1,18 @@
1
1
  import { execFile, spawn, } from 'node:child_process';
2
2
  import { readFile, realpath, stat } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
- import { extname, resolve, sep } from 'node:path';
4
+ import { extname, isAbsolute, relative, resolve, sep } from 'node:path';
5
5
  import { pathToFileURL } from 'node:url';
6
6
  import { promisify } from 'node:util';
7
7
  import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
8
8
  import { redactSensitiveText, sanitizeChildEnvironment, sensitiveEnvironmentValues, } from '../platform/sensitive-data.js';
9
9
  import { formatClaudeLspResult } from './claude-lsp-formatters.js';
10
+ import { formatDiagnostics, parsePublishDiagnostics, } from './lsp-diagnostics.js';
10
11
  const MAX_MESSAGE_BYTES = 8 * 1024 * 1024;
11
12
  const MAX_RESULT_CHARS = 512 * 1024;
12
13
  const MAX_FILE_BYTES = 10_000_000;
13
14
  const REQUEST_TIMEOUT_MS = 15_000;
15
+ const DIAGNOSTICS_TIMEOUT_MS = 1_000;
14
16
  const execFileAsync = promisify(execFile);
15
17
  const NO_CALL_HIERARCHY = Symbol('no-call-hierarchy');
16
18
  function lspSensitiveValues(definition) {
@@ -224,6 +226,9 @@ class LspConnection {
224
226
  buffer = Buffer.alloc(0);
225
227
  pending = new Map();
226
228
  documents = new Map();
229
+ diagnosticSnapshots = new Map();
230
+ diagnosticWaiters = new Set();
231
+ diagnosticGeneration = 0;
227
232
  stderr = '';
228
233
  exited = false;
229
234
  initialized = false;
@@ -253,6 +258,20 @@ class LspConnection {
253
258
  this.cleanupPending(id, pending);
254
259
  pending.reject(error);
255
260
  }
261
+ for (const waiter of [...this.diagnosticWaiters])
262
+ this.settleDiagnosticWaiter(waiter, undefined, error);
263
+ this.diagnosticSnapshots.clear();
264
+ }
265
+ settleDiagnosticWaiter(waiter, diagnostics, error) {
266
+ if (!this.diagnosticWaiters.delete(waiter))
267
+ return;
268
+ clearTimeout(waiter.timer);
269
+ if (waiter.signal && waiter.abort)
270
+ waiter.signal.removeEventListener('abort', waiter.abort);
271
+ if (error)
272
+ waiter.reject(error);
273
+ else
274
+ waiter.resolve(diagnostics ?? []);
256
275
  }
257
276
  cleanupPending(id, pending) {
258
277
  clearTimeout(pending.timer);
@@ -306,6 +325,29 @@ class LspConnection {
306
325
  if (!message || typeof message !== 'object' || Array.isArray(message))
307
326
  return;
308
327
  const value = message;
328
+ const publication = parsePublishDiagnostics(message);
329
+ if (publication) {
330
+ const document = [...this.documents.values()].find((candidate) => candidate.uri === publication.uri);
331
+ if (!document)
332
+ return;
333
+ if (publication.version !== undefined &&
334
+ publication.version !== document.version)
335
+ return;
336
+ const generation = ++this.diagnosticGeneration;
337
+ this.diagnosticSnapshots.set(publication.uri, {
338
+ generation,
339
+ diagnostics: publication.diagnostics,
340
+ });
341
+ const snapshot = this.diagnosticSnapshots.get(publication.uri);
342
+ for (const waiter of [...this.diagnosticWaiters]) {
343
+ if (waiter.uri !== publication.uri ||
344
+ !snapshot ||
345
+ snapshot.generation <= waiter.afterGeneration)
346
+ continue;
347
+ this.settleDiagnosticWaiter(waiter, snapshot.diagnostics);
348
+ }
349
+ return;
350
+ }
309
351
  if (value.id !== undefined && typeof value.method === 'string') {
310
352
  const result = value.method === 'workspace/configuration' &&
311
353
  value.params &&
@@ -444,7 +486,7 @@ class LspConnection {
444
486
  publishDiagnostics: {
445
487
  relatedInformation: true,
446
488
  tagSupport: { valueSet: [1, 2] },
447
- versionSupport: false,
489
+ versionSupport: true,
448
490
  codeDescriptionSupport: true,
449
491
  dataSupport: false,
450
492
  },
@@ -472,25 +514,82 @@ class LspConnection {
472
514
  }
473
515
  this.initialized = true;
474
516
  }
475
- async openDocument(filePath, languageId) {
476
- const text = await readFile(filePath, 'utf8');
517
+ synchronizeDocument(filePath, languageId, text) {
477
518
  const current = this.documents.get(filePath);
478
519
  const uri = pathToFileURL(filePath).href;
520
+ if (current?.text === text)
521
+ return false;
479
522
  if (!current) {
480
- this.documents.set(filePath, { text, version: 1 });
523
+ this.documents.set(filePath, { text, version: 1, uri });
481
524
  this.notify('textDocument/didOpen', {
482
525
  textDocument: { uri, languageId, version: 1, text },
483
526
  });
484
- return;
485
527
  }
486
- if (current.text === text)
487
- return;
488
- const version = current.version + 1;
489
- this.documents.set(filePath, { text, version });
490
- this.notify('textDocument/didChange', {
491
- textDocument: { uri, version },
492
- contentChanges: [{ text }],
528
+ else {
529
+ const version = current.version + 1;
530
+ this.documents.set(filePath, { text, version, uri });
531
+ this.notify('textDocument/didChange', {
532
+ textDocument: { uri, version },
533
+ contentChanges: [{ text }],
534
+ });
535
+ }
536
+ return true;
537
+ }
538
+ async openDocument(filePath, languageId) {
539
+ const text = await readFile(filePath, 'utf8');
540
+ this.synchronizeDocument(filePath, languageId, text);
541
+ }
542
+ async refreshDiagnostics(filePath, languageId, signal) {
543
+ if (signal?.aborted)
544
+ return [];
545
+ const text = await readFile(filePath, 'utf8');
546
+ if (signal?.aborted)
547
+ return [];
548
+ const uri = pathToFileURL(filePath).href;
549
+ const current = this.documents.get(filePath);
550
+ if (current && current.text === text)
551
+ return [];
552
+ const afterGeneration = this.diagnosticSnapshots.get(uri)?.generation ?? 0;
553
+ let waiter;
554
+ const result = new Promise((resolveResult, rejectResult) => {
555
+ const entry = {
556
+ uri,
557
+ afterGeneration,
558
+ resolve: resolveResult,
559
+ reject: rejectResult,
560
+ timer: setTimeout(() => {
561
+ this.settleDiagnosticWaiter(entry);
562
+ }, 1_000),
563
+ ...(signal ? { signal } : {}),
564
+ };
565
+ waiter = entry;
566
+ if (signal) {
567
+ entry.abort = () => {
568
+ this.settleDiagnosticWaiter(entry, undefined, new Error('LSP diagnostics refresh cancelled'));
569
+ };
570
+ if (signal.aborted) {
571
+ clearTimeout(entry.timer);
572
+ resolveResult([]);
573
+ return;
574
+ }
575
+ signal.addEventListener('abort', entry.abort, { once: true });
576
+ }
577
+ this.diagnosticWaiters.add(entry);
493
578
  });
579
+ if (signal?.aborted)
580
+ return result;
581
+ try {
582
+ this.synchronizeDocument(filePath, languageId, text);
583
+ }
584
+ catch (error) {
585
+ if (waiter) {
586
+ // The method throws the send failure directly. Resolve the detached
587
+ // waiter while cleaning it so it cannot become an unhandled rejection.
588
+ this.settleDiagnosticWaiter(waiter);
589
+ }
590
+ throw error;
591
+ }
592
+ return result.catch(() => []);
494
593
  }
495
594
  async execute(input, filePath, signal) {
496
595
  const uri = pathToFileURL(filePath).href;
@@ -527,6 +626,7 @@ class LspConnection {
527
626
  async close() {
528
627
  if (this.exited)
529
628
  return;
629
+ this.fail(new Error(`LSP server ${this.definition.name} is closing`));
530
630
  const exited = new Promise((resolveExit) => {
531
631
  this.child.once('close', () => resolveExit());
532
632
  });
@@ -567,6 +667,33 @@ export class ClaudeLspToolManager {
567
667
  registry(base) {
568
668
  return new ClaudeLspToolRegistry(base, this);
569
669
  }
670
+ async discardConnectionsOutside(runtimeCwd) {
671
+ const prefix = runtimeCwd === undefined ? undefined : `${runtimeCwd}\0`;
672
+ const staleKeys = new Set([
673
+ ...this.connections.keys(),
674
+ ...this.initializing.keys(),
675
+ ...this.restartCounts.keys(),
676
+ ].filter((key) => prefix === undefined || !key.startsWith(prefix)));
677
+ const staleConnections = [];
678
+ for (const key of staleKeys) {
679
+ const connection = this.connections.get(key);
680
+ if (connection)
681
+ staleConnections.push(connection);
682
+ this.connections.delete(key);
683
+ this.initializing.delete(key);
684
+ this.restartCounts.delete(key);
685
+ }
686
+ await Promise.allSettled(staleConnections.map((connection) => connection.close()));
687
+ }
688
+ async runtimeCwd() {
689
+ try {
690
+ return await realpath(this.options.cwdProvider());
691
+ }
692
+ catch (error) {
693
+ await this.discardConnectionsOutside();
694
+ throw error;
695
+ }
696
+ }
570
697
  async validatedFile(inputPath) {
571
698
  const cwd = this.options.cwdProvider();
572
699
  const expanded = inputPath === '~'
@@ -601,16 +728,23 @@ export class ClaudeLspToolManager {
601
728
  const extension = extname(filePath).toLowerCase();
602
729
  return (Object.entries(definition.extensionToLanguage).find(([candidate]) => candidate.toLowerCase() === extension)?.[1] ?? '');
603
730
  }
604
- async connection(definition, signal) {
605
- const runtimeCwd = await realpath(this.options.cwdProvider());
731
+ async connection(definition, signal, expectedCwd) {
732
+ const runtimeCwd = await this.runtimeCwd();
733
+ if (expectedCwd !== undefined && runtimeCwd !== expectedCwd) {
734
+ await this.discardConnectionsOutside(runtimeCwd);
735
+ throw new Error('LSP diagnostics cwd changed during collection');
736
+ }
737
+ if (signal?.aborted)
738
+ throw new Error('LSP connection cancelled');
606
739
  const serverCwd = await realpath(definition.workspaceFolder ?? runtimeCwd);
607
740
  const key = `${runtimeCwd}\0${definition.pluginName}\0${definition.name}`;
608
- const stale = [...this.connections.entries()].filter(([candidate]) => !candidate.startsWith(`${runtimeCwd}\0`));
609
- for (const [candidate, connection] of stale) {
610
- this.connections.delete(candidate);
611
- this.initializing.delete(candidate);
612
- this.restartCounts.delete(candidate);
613
- await connection.close();
741
+ await this.discardConnectionsOutside(runtimeCwd);
742
+ if (signal?.aborted)
743
+ throw new Error('LSP connection cancelled');
744
+ const confirmedCwd = await this.runtimeCwd();
745
+ if (expectedCwd !== undefined && confirmedCwd !== expectedCwd) {
746
+ await this.discardConnectionsOutside(confirmedCwd);
747
+ throw new Error('LSP diagnostics cwd changed during collection');
614
748
  }
615
749
  const initializing = this.initializing.get(key);
616
750
  if (initializing)
@@ -657,7 +791,7 @@ export class ClaudeLspToolManager {
657
791
  async execute(call, context) {
658
792
  try {
659
793
  const input = inputFrom(call.input);
660
- const cwd = await realpath(this.options.cwdProvider());
794
+ const cwd = await this.runtimeCwd();
661
795
  const { filePath, canonicalPath, size } = await this.validatedFile(input.filePath);
662
796
  if (size > MAX_FILE_BYTES) {
663
797
  return {
@@ -710,6 +844,96 @@ export class ClaudeLspToolManager {
710
844
  };
711
845
  }
712
846
  }
847
+ async enrichMutationResult(call, context, result) {
848
+ if (result.isError || (call.name !== 'Edit' && call.name !== 'ApplyPatch'))
849
+ return result;
850
+ if (context.signal?.aborted)
851
+ return result;
852
+ const deadline = new AbortController();
853
+ let resolveInterrupted;
854
+ const interrupted = new Promise((resolveResult) => {
855
+ resolveInterrupted = resolveResult;
856
+ });
857
+ const abort = () => {
858
+ deadline.abort();
859
+ resolveInterrupted?.(result);
860
+ };
861
+ context.signal?.addEventListener('abort', abort, { once: true });
862
+ const timer = setTimeout(() => abort(), DIAGNOSTICS_TIMEOUT_MS);
863
+ const attempt = (async () => {
864
+ const cwd = await this.runtimeCwd();
865
+ await this.discardConnectionsOutside(cwd);
866
+ const sameCwd = async () => {
867
+ try {
868
+ const runtimeCwd = await this.runtimeCwd();
869
+ if (runtimeCwd === cwd)
870
+ return true;
871
+ await this.discardConnectionsOutside(runtimeCwd);
872
+ }
873
+ catch {
874
+ // runtimeCwd() already discarded connection-local state.
875
+ }
876
+ return false;
877
+ };
878
+ if (deadline.signal.aborted || !(await sameCwd()))
879
+ return result;
880
+ const requested = call.name === 'Edit'
881
+ ? [call.input.file_path]
882
+ : Array.isArray(call.input.edits)
883
+ ? call.input.edits.map((edit) => edit?.file_path)
884
+ : [];
885
+ const targets = new Set();
886
+ for (const value of requested) {
887
+ if (typeof value !== 'string' || deadline.signal.aborted)
888
+ continue;
889
+ try {
890
+ const canonical = await realpath(resolve(cwd, value));
891
+ if (deadline.signal.aborted || !(await sameCwd()))
892
+ continue;
893
+ const relativePath = relative(cwd, canonical);
894
+ if (relativePath === '..' ||
895
+ relativePath.startsWith(`..${sep}`) ||
896
+ isAbsolute(relativePath) ||
897
+ !(await stat(canonical)).isFile())
898
+ continue;
899
+ targets.add(canonical);
900
+ }
901
+ catch {
902
+ // Successful mutations must remain successful when enrichment cannot inspect a target.
903
+ }
904
+ }
905
+ const records = [];
906
+ await Promise.all([...targets].flatMap((filePath) => this.candidates(filePath).map(async (definition) => {
907
+ if (deadline.signal.aborted)
908
+ return;
909
+ try {
910
+ const connection = await this.connection(definition, deadline.signal, cwd);
911
+ if (deadline.signal.aborted || !(await sameCwd()))
912
+ return;
913
+ const current = await connection.refreshDiagnostics(filePath, this.languageId(definition, filePath), deadline.signal);
914
+ if (deadline.signal.aborted || !(await sameCwd()))
915
+ return;
916
+ records.push(...current);
917
+ }
918
+ catch {
919
+ // Diagnostics are best effort and never change mutation semantics.
920
+ }
921
+ })));
922
+ if (records.length === 0 || deadline.signal.aborted || !(await sameCwd()))
923
+ return result;
924
+ const block = formatDiagnostics(records, cwd, this.options.servers.flatMap((definition) => lspSensitiveValues(definition)));
925
+ return block
926
+ ? { ...result, content: `${result.content}\n${block}` }
927
+ : result;
928
+ })().catch(() => result);
929
+ try {
930
+ return await Promise.race([attempt, interrupted]);
931
+ }
932
+ finally {
933
+ clearTimeout(timer);
934
+ context.signal?.removeEventListener('abort', abort);
935
+ }
936
+ }
713
937
  async close() {
714
938
  const connections = [...this.connections.values()];
715
939
  this.connections.clear();
@@ -742,10 +966,11 @@ class ClaudeLspToolRegistry {
742
966
  ? Promise.resolve(call)
743
967
  : this.base.prepare(call, context);
744
968
  }
745
- execute(call, context) {
746
- return call.name === 'LSP'
747
- ? this.manager.execute(call, context)
748
- : this.base.execute(call, context);
969
+ async execute(call, context) {
970
+ if (call.name === 'LSP')
971
+ return this.manager.execute(call, context);
972
+ const result = await this.base.execute(call, context);
973
+ return this.manager.enrichMutationResult(call, context, result);
749
974
  }
750
975
  }
751
976
  //# sourceMappingURL=claude-lsp-tool.js.map
@@ -0,0 +1,20 @@
1
+ export type DiagnosticSeverity = 'error' | 'warning' | 'information' | 'hint';
2
+ export interface LspDiagnosticRecord {
3
+ canonicalPath: string;
4
+ line: number;
5
+ column: number;
6
+ severity: DiagnosticSeverity;
7
+ code: string;
8
+ message: string;
9
+ }
10
+ export interface LspDiagnosticsPublication {
11
+ uri: string;
12
+ filePath: string;
13
+ version?: number;
14
+ diagnostics: readonly LspDiagnosticRecord[];
15
+ }
16
+ /** Parse one strict, id-less LSP publishDiagnostics notification. */
17
+ export declare function parsePublishDiagnostics(value: unknown): LspDiagnosticsPublication | null;
18
+ /** Format diagnostics as one bounded provider-visible block. */
19
+ export declare function formatDiagnostics(diagnostics: readonly LspDiagnosticRecord[], cwd: string, sensitiveValues?: readonly string[]): string | null;
20
+ //# sourceMappingURL=lsp-diagnostics.d.ts.map
@@ -0,0 +1,173 @@
1
+ import { relative } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { redactSensitiveText } from '../platform/sensitive-data.js';
4
+ const severityNames = {
5
+ 1: 'error',
6
+ 2: 'warning',
7
+ 3: 'information',
8
+ 4: 'hint',
9
+ };
10
+ const severityRank = {
11
+ error: 0,
12
+ warning: 1,
13
+ information: 2,
14
+ hint: 3,
15
+ };
16
+ function safeInteger(value) {
17
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
18
+ }
19
+ function recordObject(value) {
20
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
21
+ }
22
+ function localFilePath(uri) {
23
+ if (typeof uri !== 'string')
24
+ return null;
25
+ let parsed;
26
+ try {
27
+ parsed = new URL(uri);
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ if (parsed.protocol !== 'file:' || parsed.search || parsed.hash)
33
+ return null;
34
+ try {
35
+ return fileURLToPath(parsed);
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ /** Parse one strict, id-less LSP publishDiagnostics notification. */
42
+ export function parsePublishDiagnostics(value) {
43
+ try {
44
+ if (!recordObject(value) || value.jsonrpc !== '2.0')
45
+ return null;
46
+ if (value.id !== undefined ||
47
+ value.method !== 'textDocument/publishDiagnostics')
48
+ return null;
49
+ if (!recordObject(value.params))
50
+ return null;
51
+ const params = value.params;
52
+ const uri = params.uri;
53
+ const rawPath = localFilePath(uri);
54
+ if (!rawPath || typeof uri !== 'string')
55
+ return null;
56
+ const filePath = rawPath;
57
+ const version = params.version;
58
+ if (version !== undefined && !safeInteger(version))
59
+ return null;
60
+ if (!Array.isArray(params.diagnostics))
61
+ return null;
62
+ const diagnostics = [];
63
+ for (const value of params.diagnostics) {
64
+ if (!recordObject(value) || typeof value.message !== 'string')
65
+ return null;
66
+ if (!recordObject(value.range))
67
+ return null;
68
+ const range = value.range;
69
+ if (!recordObject(range.start) || !recordObject(range.end))
70
+ return null;
71
+ const start = range.start;
72
+ const end = range.end;
73
+ if (!safeInteger(start.line) ||
74
+ !safeInteger(start.character) ||
75
+ !safeInteger(end.line) ||
76
+ !safeInteger(end.character) ||
77
+ end.line < start.line ||
78
+ (end.line === start.line && end.character < start.character))
79
+ return null;
80
+ const severity = value.severity;
81
+ if (severity !== undefined &&
82
+ (!safeInteger(severity) || severity < 1 || severity > 4))
83
+ return null;
84
+ const code = value.code;
85
+ if (code !== undefined &&
86
+ !(typeof code === 'string' ||
87
+ (typeof code === 'number' && Number.isFinite(code))))
88
+ return null;
89
+ diagnostics.push({
90
+ canonicalPath: filePath,
91
+ line: start.line + 1,
92
+ column: start.character + 1,
93
+ severity: severityNames[severity ?? 1] ?? 'error',
94
+ code: code === undefined ? '' : String(code),
95
+ message: value.message,
96
+ });
97
+ }
98
+ return {
99
+ uri,
100
+ filePath,
101
+ ...(version === undefined ? {} : { version }),
102
+ diagnostics,
103
+ };
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ }
109
+ function compareText(a, b) {
110
+ return a < b ? -1 : a > b ? 1 : 0;
111
+ }
112
+ function sanitize(value, sensitiveValues) {
113
+ const redacted = redactSensitiveText(value, sensitiveValues);
114
+ let controlsCollapsed = '';
115
+ let inControls = false;
116
+ for (const character of redacted) {
117
+ const code = character.codePointAt(0) ?? 0;
118
+ const control = code <= 0x1f ||
119
+ (code >= 0x7f && code <= 0x9f) ||
120
+ code === 0x2028 ||
121
+ code === 0x2029;
122
+ if (control) {
123
+ if (!inControls)
124
+ controlsCollapsed += ' ';
125
+ inControls = true;
126
+ }
127
+ else {
128
+ controlsCollapsed += character;
129
+ inControls = false;
130
+ }
131
+ }
132
+ return controlsCollapsed.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
133
+ }
134
+ function relativePath(path, cwd) {
135
+ return relative(cwd, path).replaceAll('\\', '/').replace(/^\.\//u, '');
136
+ }
137
+ function compareRecords(a, b, cwd) {
138
+ const aPath = relativePath(a.canonicalPath, cwd);
139
+ const bPath = relativePath(b.canonicalPath, cwd);
140
+ return (compareText(aPath, bPath) ||
141
+ a.line - b.line ||
142
+ a.column - b.column ||
143
+ severityRank[a.severity] - severityRank[b.severity] ||
144
+ compareText(a.code, b.code) ||
145
+ compareText(a.message, b.message));
146
+ }
147
+ /** Format diagnostics as one bounded provider-visible block. */
148
+ export function formatDiagnostics(diagnostics, cwd, sensitiveValues = []) {
149
+ if (diagnostics.length === 0)
150
+ return null;
151
+ const sorted = [...diagnostics].sort((a, b) => compareRecords(a, b, cwd));
152
+ const marker = '… diagnostics truncated';
153
+ const lines = [];
154
+ let truncated = sorted.length > 8;
155
+ for (const diagnostic of sorted.slice(0, 8)) {
156
+ const code = sanitize(diagnostic.code, sensitiveValues);
157
+ const message = sanitize(diagnostic.message, sensitiveValues);
158
+ const line = `${sanitize(relativePath(diagnostic.canonicalPath, cwd), sensitiveValues)}:${diagnostic.line}:${diagnostic.column} ${sanitize(diagnostic.severity, sensitiveValues)}${code ? ` ${code}` : ''} ${message}`;
159
+ const candidate = `<diagnostics>\n${[...lines, line].join('\n')}\n</diagnostics>`;
160
+ if (Buffer.byteLength(candidate, 'utf8') > 4096) {
161
+ truncated = true;
162
+ break;
163
+ }
164
+ lines.push(line);
165
+ }
166
+ if (truncated) {
167
+ while (Buffer.byteLength(`<diagnostics>\n${[...lines, marker].join('\n')}\n</diagnostics>`, 'utf8') > 4096)
168
+ lines.pop();
169
+ lines.push(marker);
170
+ }
171
+ return `<diagnostics>\n${lines.join('\n')}\n</diagnostics>`;
172
+ }
173
+ //# sourceMappingURL=lsp-diagnostics.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.56.0",
3
+ "version": "0.57.0",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",
@@ -62,7 +62,7 @@
62
62
  "verify:ci-coverage": "node scripts/verify-ci-coverage.mjs",
63
63
  "verify:fixture-contracts": "node scripts/verify-fixture-contracts.mjs",
64
64
  "test:fixtures": "node scripts/run-fixture-contracts.mjs",
65
- "test:eval:baseline": "vitest run src/evals/coding-baseline.test.ts src/evals/project-eval-comparison.test.ts src/evals/apply-patch-admission.test.ts"
65
+ "test:eval:baseline": "vitest run src/evals/coding-baseline.test.ts src/evals/project-eval-comparison.test.ts src/evals/apply-patch-admission.test.ts src/evals/lsp-diagnostics-admission.test.ts"
66
66
  },
67
67
  "engines": {
68
68
  "node": ">=24"