pi-smart-compact 9.2.1 → 9.3.1

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.
@@ -4,172 +4,8 @@ var __require = import.meta.require;
4
4
  // scripts/provider-scenario-eval.ts
5
5
  import { ModelRegistry, ModelRuntime } from "@earendil-works/pi-coding-agent";
6
6
 
7
- // src/infra/llm-client.ts
8
- var _complete = null;
9
- var _completeSimple = null;
10
- var _stream = null;
11
- var _streamSimple = null;
12
- async function resolveComplete() {
13
- if (_complete)
14
- return _complete;
15
- const mod = await import("@earendil-works/pi-ai/compat");
16
- const fn = mod.complete;
17
- if (typeof fn !== "function")
18
- throw new Error("smart-compact: pi-ai /compat did not export complete()");
19
- _complete = fn;
20
- return fn;
21
- }
22
- async function resolveCompleteSimple() {
23
- if (_completeSimple)
24
- return _completeSimple;
25
- const mod = await import("@earendil-works/pi-ai/compat");
26
- const fn = mod.completeSimple;
27
- if (typeof fn !== "function")
28
- throw new Error("smart-compact: pi-ai /compat did not export completeSimple()");
29
- _completeSimple = fn;
30
- return fn;
31
- }
32
- async function resolveStream() {
33
- if (_stream)
34
- return _stream;
35
- const mod = await import("@earendil-works/pi-ai/compat");
36
- if (typeof mod.stream !== "function")
37
- throw new Error("smart-compact: pi-ai /compat did not export stream()");
38
- _stream = mod.stream;
39
- return _stream;
40
- }
41
- async function resolveStreamSimple() {
42
- if (_streamSimple)
43
- return _streamSimple;
44
- const mod = await import("@earendil-works/pi-ai/compat");
45
- if (typeof mod.streamSimple !== "function")
46
- throw new Error("smart-compact: pi-ai /compat did not export streamSimple()");
47
- _streamSimple = mod.streamSimple;
48
- return _streamSimple;
49
- }
50
- function isChatGptCodex(model) {
51
- if (model.api !== "openai-codex-responses")
52
- return false;
53
- return !model.baseUrl || model.baseUrl.includes("chatgpt.com");
54
- }
55
- function withCodexWireLimit(model, opts) {
56
- if (model.api !== "openai-codex-responses" || isChatGptCodex(model) || !opts.maxTokens)
57
- return opts;
58
- const previous = opts.onPayload;
59
- return {
60
- ...opts,
61
- onPayload: async (payload, requestModel) => {
62
- const transformed = await previous?.(payload, requestModel);
63
- const body = transformed ?? payload;
64
- return body && typeof body === "object" ? { ...body, max_output_tokens: opts.maxTokens } : body;
65
- }
66
- };
67
- }
68
- function resolveCodexWatchdogMs(maxTokens, configuredMs = 0) {
69
- if (configuredMs > 0)
70
- return configuredMs;
71
- return Math.min(90000, Math.max(15000, 1e4 + (maxTokens ?? 4096) * 8));
72
- }
73
- function streamedChars(event) {
74
- if (event.type === "text_delta" || event.type === "thinking_delta" || event.type === "toolcall_delta") {
75
- return event.delta.length;
76
- }
77
- return 0;
78
- }
79
- function assertSuccessful(message) {
80
- if (message.stopReason === "error" || message.stopReason === "aborted") {
81
- throw new Error(message.errorMessage || "LLM request failed");
82
- }
83
- return message;
84
- }
85
- async function withProviderDeadline(opts, invoke) {
86
- if (opts.signal?.aborted)
87
- throw new Error("LLM request aborted before dispatch");
88
- const controller = new AbortController;
89
- const watchdogMs = resolveCodexWatchdogMs(opts.maxTokens, opts.codexWatchdogMs);
90
- const abort = Promise.withResolvers();
91
- const abortFromCaller = () => {
92
- controller.abort(opts.signal?.reason);
93
- abort.reject(new Error("LLM request aborted by caller"));
94
- };
95
- opts.signal?.addEventListener("abort", abortFromCaller, { once: true });
96
- const timeout = Promise.withResolvers();
97
- const timer = setTimeout(() => {
98
- controller.abort("provider-watchdog");
99
- timeout.reject(new Error("Provider watchdog stopped generation after " + watchdogMs + "ms"));
100
- }, watchdogMs);
101
- if (typeof timer === "object" && "unref" in timer)
102
- timer.unref();
103
- try {
104
- return await Promise.race([
105
- invoke({ ...opts, signal: controller.signal }),
106
- abort.promise,
107
- timeout.promise
108
- ]);
109
- } finally {
110
- clearTimeout(timer);
111
- opts.signal?.removeEventListener("abort", abortFromCaller);
112
- }
113
- }
114
- async function completeChatGptCodex(model, body, opts) {
115
- const controller = new AbortController;
116
- const watchdogMs = resolveCodexWatchdogMs(opts.maxTokens, opts.codexWatchdogMs);
117
- let watchdogReason = null;
118
- let visibleChars = 0;
119
- const abortFromCaller = () => controller.abort(opts.signal?.reason);
120
- opts.signal?.addEventListener("abort", abortFromCaller, { once: true });
121
- if (opts.signal?.aborted)
122
- abortFromCaller();
123
- const timer = setTimeout(() => {
124
- watchdogReason = "time";
125
- controller.abort("codex-watchdog");
126
- }, watchdogMs);
127
- timer.unref?.();
128
- try {
129
- const limited = { ...opts, signal: controller.signal };
130
- const events = opts.reasoning === undefined ? (await resolveStream())(model, body, limited) : (await resolveStreamSimple())(model, body, limited);
131
- let final;
132
- for await (const event of events) {
133
- visibleChars += streamedChars(event);
134
- if (!watchdogReason && opts.maxTokens && visibleChars > opts.maxTokens * 3) {
135
- watchdogReason = "visible-output";
136
- controller.abort("codex-visible-output-cap");
137
- }
138
- if (event.type === "done")
139
- final = event.message;
140
- else if (event.type === "error")
141
- final = event.error;
142
- }
143
- if (watchdogReason) {
144
- throw new Error("Codex " + watchdogReason + " watchdog stopped generation after " + watchdogMs + "ms / " + visibleChars + " streamed chars");
145
- }
146
- if (!final)
147
- throw new Error("Codex stream ended without a final message");
148
- return assertSuccessful(final);
149
- } finally {
150
- clearTimeout(timer);
151
- opts.signal?.removeEventListener("abort", abortFromCaller);
152
- }
153
- }
154
- var rawLlmClient = {
155
- complete: async (model, body, originalOpts) => {
156
- const opts = withCodexWireLimit(model, originalOpts);
157
- return withProviderDeadline(opts, async (bounded) => {
158
- if (isChatGptCodex(model))
159
- return completeChatGptCodex(model, body, bounded);
160
- const response = bounded.reasoning === undefined ? await (await resolveComplete())(model, body, bounded) : await (await resolveCompleteSimple())(model, body, bounded);
161
- return assertSuccessful(response);
162
- });
163
- }
164
- };
165
- var defaultLlmClient = rawLlmClient;
166
- var _client = defaultLlmClient;
167
- function getLlmClient() {
168
- return _client;
169
- }
170
-
171
7
  // src/constants.ts
172
- var VERSION = "9.2.1";
8
+ var VERSION = "9.3.1";
173
9
  var SETTLED_TRIGGER_COOLDOWN_MS = 10 * 60000;
174
10
  var COMPACT_SYSTEM_PREFIX = "You are an expert conversation summarizer for a coding agent. " + "Produce structured markdown summaries. " + "Follow output format exactly. " + "Use EXACT names \u2014 never paraphrase code identifiers. " + "Trust deterministic extraction data over intuition.";
175
11
  var PROFILES = {
@@ -391,9 +227,6 @@ var EXPLORER_SYSTEM_PROMPT = `You are a conversation analyst. You have determini
391
227
  ` + `After exploration, output ONLY a JSON object (no markdown):
392
228
  ` + '{"boundaries":[{"afterIndex":N,"topic":"...","priority":"critical|high|normal|low","confidence":0.0-1.0}],"mainGoal":"...","sessionType":"implementation|review|debugging|discussion","enrichedConstraints":[...],"crossReferences":[...],"statusAssessment":{"done":[...],"inProgress":[...],"blocked":[...]},"criticalContext":[...],"keyDecisions":[...]}';
393
229
 
394
- // src/utils/cache.ts
395
- import fs2 from "fs";
396
-
397
230
  // src/utils/lru.ts
398
231
  function lruGet(m, key) {
399
232
  if (!m.has(key))
@@ -416,6 +249,188 @@ function lruSet(m, key, value, max) {
416
249
  }
417
250
 
418
251
  // src/utils/tokens.ts
252
+ var PROVIDER_MAP = {
253
+ "zai-anthropic": {
254
+ maxOutputTokens: 8192,
255
+ supportsTools: "probe",
256
+ jsonReliability: "high",
257
+ instructionFollowing: "high",
258
+ tokenRatioEstimate: 3.5,
259
+ concurrencyLimit: 3,
260
+ cacheStrategy: "anthropic",
261
+ timeoutMultiplier: 1.2,
262
+ singlePassTokenMultiplier: 1,
263
+ multimodal: "metadata-only"
264
+ },
265
+ "kimi-coding": {
266
+ maxOutputTokens: 8192,
267
+ supportsTools: "probe",
268
+ jsonReliability: "high",
269
+ instructionFollowing: "high",
270
+ tokenRatioEstimate: 3.5,
271
+ concurrencyLimit: 2,
272
+ cacheStrategy: "anthropic",
273
+ timeoutMultiplier: 1.5,
274
+ singlePassTokenMultiplier: 0.95,
275
+ multimodal: "metadata-only"
276
+ },
277
+ anthropic: {
278
+ maxOutputTokens: 8192,
279
+ supportsTools: true,
280
+ jsonReliability: "high",
281
+ instructionFollowing: "high",
282
+ tokenRatioEstimate: 3.5,
283
+ concurrencyLimit: 3,
284
+ cacheStrategy: "anthropic",
285
+ timeoutMultiplier: 1.2,
286
+ singlePassTokenMultiplier: 1,
287
+ multimodal: "native"
288
+ },
289
+ openai: {
290
+ maxOutputTokens: 16384,
291
+ supportsTools: true,
292
+ jsonReliability: "high",
293
+ instructionFollowing: "high",
294
+ tokenRatioEstimate: 4,
295
+ concurrencyLimit: 5,
296
+ cacheStrategy: "openai",
297
+ timeoutMultiplier: 1,
298
+ singlePassTokenMultiplier: 1.15,
299
+ multimodal: "native"
300
+ },
301
+ google: {
302
+ maxOutputTokens: 8192,
303
+ supportsTools: true,
304
+ jsonReliability: "high",
305
+ instructionFollowing: "high",
306
+ tokenRatioEstimate: 3.8,
307
+ concurrencyLimit: 3,
308
+ cacheStrategy: "openai",
309
+ timeoutMultiplier: 1.15,
310
+ singlePassTokenMultiplier: 1.1,
311
+ multimodal: "native"
312
+ },
313
+ deepseek: {
314
+ maxOutputTokens: 8192,
315
+ supportsTools: true,
316
+ jsonReliability: "medium",
317
+ instructionFollowing: "medium",
318
+ tokenRatioEstimate: 3.6,
319
+ concurrencyLimit: 2,
320
+ cacheStrategy: "none",
321
+ timeoutMultiplier: 1.5,
322
+ singlePassTokenMultiplier: 0.85,
323
+ multimodal: "metadata-only"
324
+ },
325
+ minimax: {
326
+ maxOutputTokens: 4096,
327
+ supportsTools: "probe",
328
+ jsonReliability: "medium",
329
+ instructionFollowing: "medium",
330
+ tokenRatioEstimate: 3.8,
331
+ concurrencyLimit: 2,
332
+ cacheStrategy: "anthropic",
333
+ timeoutMultiplier: 1.6,
334
+ singlePassTokenMultiplier: 0.8,
335
+ multimodal: "metadata-only"
336
+ },
337
+ "xiaomi-token-plan": {
338
+ maxOutputTokens: 8192,
339
+ supportsTools: "probe",
340
+ jsonReliability: "medium",
341
+ instructionFollowing: "medium",
342
+ tokenRatioEstimate: 3.3,
343
+ concurrencyLimit: 2,
344
+ cacheStrategy: "openai",
345
+ timeoutMultiplier: 1.35,
346
+ singlePassTokenMultiplier: 0.9,
347
+ multimodal: "metadata-only"
348
+ },
349
+ "xiaomi-mimo": {
350
+ maxOutputTokens: 8192,
351
+ supportsTools: "probe",
352
+ jsonReliability: "medium",
353
+ instructionFollowing: "medium",
354
+ tokenRatioEstimate: 3.3,
355
+ concurrencyLimit: 2,
356
+ cacheStrategy: "anthropic",
357
+ timeoutMultiplier: 1.35,
358
+ singlePassTokenMultiplier: 0.9,
359
+ multimodal: "metadata-only"
360
+ },
361
+ crofai: {
362
+ maxOutputTokens: 8192,
363
+ supportsTools: "probe",
364
+ jsonReliability: "medium",
365
+ instructionFollowing: "medium",
366
+ tokenRatioEstimate: 3.8,
367
+ concurrencyLimit: 3,
368
+ cacheStrategy: "none",
369
+ timeoutMultiplier: 1.2,
370
+ singlePassTokenMultiplier: 0.95,
371
+ multimodal: "metadata-only"
372
+ },
373
+ mistral: {
374
+ maxOutputTokens: 8192,
375
+ supportsTools: true,
376
+ jsonReliability: "high",
377
+ instructionFollowing: "high",
378
+ tokenRatioEstimate: 3.5,
379
+ concurrencyLimit: 3,
380
+ cacheStrategy: "openai",
381
+ timeoutMultiplier: 1.2,
382
+ singlePassTokenMultiplier: 1,
383
+ multimodal: "metadata-only"
384
+ },
385
+ xai: {
386
+ maxOutputTokens: 8192,
387
+ supportsTools: true,
388
+ jsonReliability: "medium",
389
+ instructionFollowing: "high",
390
+ tokenRatioEstimate: 3.8,
391
+ concurrencyLimit: 3,
392
+ cacheStrategy: "openai",
393
+ timeoutMultiplier: 1.2,
394
+ singlePassTokenMultiplier: 1,
395
+ multimodal: "native"
396
+ }
397
+ };
398
+ var PROVIDER_ALIASES = [
399
+ { pattern: /anthropic/i, provider: "anthropic" },
400
+ { pattern: /kimi/i, provider: "kimi-coding" },
401
+ { pattern: /zai/i, provider: "zai-anthropic" },
402
+ { pattern: /openai/i, provider: "openai" },
403
+ { pattern: /gpt/i, provider: "openai" },
404
+ { pattern: /google|gemini/i, provider: "google" },
405
+ { pattern: /deepseek/i, provider: "deepseek" },
406
+ { pattern: /minimax/i, provider: "minimax" },
407
+ { pattern: /xiaomi-mimo/i, provider: "xiaomi-mimo" },
408
+ { pattern: /xiaomi/i, provider: "xiaomi-token-plan" },
409
+ { pattern: /crofai/i, provider: "crofai" },
410
+ { pattern: /mistral/i, provider: "mistral" },
411
+ { pattern: /xai|grok/i, provider: "xai" }
412
+ ];
413
+ var DEFAULT_CAPS = {
414
+ maxOutputTokens: 8192,
415
+ supportsTools: "probe",
416
+ jsonReliability: "medium",
417
+ instructionFollowing: "medium",
418
+ tokenRatioEstimate: 3.8,
419
+ concurrencyLimit: 2,
420
+ cacheStrategy: "none",
421
+ timeoutMultiplier: 1.35,
422
+ singlePassTokenMultiplier: 0.9,
423
+ multimodal: "metadata-only"
424
+ };
425
+ function getProviderCaps(provider) {
426
+ if (PROVIDER_MAP[provider])
427
+ return PROVIDER_MAP[provider];
428
+ for (const { pattern, provider: key } of PROVIDER_ALIASES) {
429
+ if (pattern.test(provider))
430
+ return PROVIDER_MAP[key] ?? DEFAULT_CAPS;
431
+ }
432
+ return DEFAULT_CAPS;
433
+ }
419
434
  class TokenCalibrationStore {
420
435
  maxEntries;
421
436
  factors = new Map;
@@ -452,6 +467,177 @@ function calibrationKey(provider, model) {
452
467
  return model ? provider + "/" + model : provider + "/*";
453
468
  }
454
469
 
470
+ // src/infra/llm-client.ts
471
+ var _complete = null;
472
+ var _completeSimple = null;
473
+ var _stream = null;
474
+ var _streamSimple = null;
475
+ async function resolveComplete() {
476
+ if (_complete)
477
+ return _complete;
478
+ const mod = await import("@earendil-works/pi-ai/compat");
479
+ const fn = mod.complete;
480
+ if (typeof fn !== "function")
481
+ throw new Error("smart-compact: pi-ai /compat did not export complete()");
482
+ _complete = fn;
483
+ return fn;
484
+ }
485
+ async function resolveCompleteSimple() {
486
+ if (_completeSimple)
487
+ return _completeSimple;
488
+ const mod = await import("@earendil-works/pi-ai/compat");
489
+ const fn = mod.completeSimple;
490
+ if (typeof fn !== "function")
491
+ throw new Error("smart-compact: pi-ai /compat did not export completeSimple()");
492
+ _completeSimple = fn;
493
+ return fn;
494
+ }
495
+ async function resolveStream() {
496
+ if (_stream)
497
+ return _stream;
498
+ const mod = await import("@earendil-works/pi-ai/compat");
499
+ if (typeof mod.stream !== "function")
500
+ throw new Error("smart-compact: pi-ai /compat did not export stream()");
501
+ _stream = mod.stream;
502
+ return _stream;
503
+ }
504
+ async function resolveStreamSimple() {
505
+ if (_streamSimple)
506
+ return _streamSimple;
507
+ const mod = await import("@earendil-works/pi-ai/compat");
508
+ if (typeof mod.streamSimple !== "function")
509
+ throw new Error("smart-compact: pi-ai /compat did not export streamSimple()");
510
+ _streamSimple = mod.streamSimple;
511
+ return _streamSimple;
512
+ }
513
+ function isChatGptCodex(model) {
514
+ if (model.api !== "openai-codex-responses")
515
+ return false;
516
+ return !model.baseUrl || model.baseUrl.includes("chatgpt.com");
517
+ }
518
+ function withCodexWireLimit(model, opts) {
519
+ if (model.api !== "openai-codex-responses" || isChatGptCodex(model) || !opts.maxTokens)
520
+ return opts;
521
+ const previous = opts.onPayload;
522
+ return {
523
+ ...opts,
524
+ onPayload: async (payload, requestModel) => {
525
+ const transformed = await previous?.(payload, requestModel);
526
+ const body = transformed ?? payload;
527
+ return body && typeof body === "object" ? {
528
+ ...body,
529
+ max_output_tokens: opts.maxTokens
530
+ } : body;
531
+ }
532
+ };
533
+ }
534
+ function resolveCodexWatchdogMs(maxTokens, configuredMs = 0) {
535
+ if (configuredMs > 0)
536
+ return configuredMs;
537
+ return Math.min(90000, Math.max(15000, 1e4 + (maxTokens ?? 4096) * 8));
538
+ }
539
+ function streamedChars(event) {
540
+ if (event.type === "text_delta" || event.type === "thinking_delta" || event.type === "toolcall_delta") {
541
+ return event.delta.length;
542
+ }
543
+ return 0;
544
+ }
545
+ function assertSuccessful(message) {
546
+ if (message.stopReason === "error" || message.stopReason === "aborted") {
547
+ throw new Error(message.errorMessage || "LLM request failed");
548
+ }
549
+ return message;
550
+ }
551
+ async function withProviderDeadline(opts, invoke, modelId) {
552
+ if (opts.signal?.aborted)
553
+ throw new Error("LLM request aborted before dispatch");
554
+ const controller = new AbortController;
555
+ const multiplier = modelId && !((opts.codexWatchdogMs ?? 0) > 0) ? getProviderCaps(modelId).timeoutMultiplier : 1;
556
+ const watchdogMs = Math.round(resolveCodexWatchdogMs(opts.maxTokens, opts.codexWatchdogMs) * multiplier);
557
+ const abort = Promise.withResolvers();
558
+ const abortFromCaller = () => {
559
+ controller.abort(opts.signal?.reason);
560
+ abort.reject(new Error("LLM request aborted by caller"));
561
+ };
562
+ opts.signal?.addEventListener("abort", abortFromCaller, { once: true });
563
+ const timeout = Promise.withResolvers();
564
+ const timer = setTimeout(() => {
565
+ controller.abort("provider-watchdog");
566
+ timeout.reject(new Error("Provider watchdog stopped generation after " + watchdogMs + "ms"));
567
+ }, watchdogMs);
568
+ if (typeof timer === "object" && "unref" in timer)
569
+ timer.unref();
570
+ try {
571
+ return await Promise.race([
572
+ invoke({ ...opts, signal: controller.signal }),
573
+ abort.promise,
574
+ timeout.promise
575
+ ]);
576
+ } finally {
577
+ clearTimeout(timer);
578
+ opts.signal?.removeEventListener("abort", abortFromCaller);
579
+ }
580
+ }
581
+ async function completeChatGptCodex(model, body, opts) {
582
+ const controller = new AbortController;
583
+ const watchdogMs = resolveCodexWatchdogMs(opts.maxTokens, opts.codexWatchdogMs);
584
+ let watchdogReason = null;
585
+ let visibleChars = 0;
586
+ const abortFromCaller = () => controller.abort(opts.signal?.reason);
587
+ opts.signal?.addEventListener("abort", abortFromCaller, { once: true });
588
+ if (opts.signal?.aborted)
589
+ abortFromCaller();
590
+ const timer = setTimeout(() => {
591
+ watchdogReason = "time";
592
+ controller.abort("codex-watchdog");
593
+ }, watchdogMs);
594
+ timer.unref?.();
595
+ try {
596
+ const limited = { ...opts, signal: controller.signal };
597
+ const events = opts.reasoning === undefined ? (await resolveStream())(model, body, limited) : (await resolveStreamSimple())(model, body, limited);
598
+ let final;
599
+ for await (const event of events) {
600
+ visibleChars += streamedChars(event);
601
+ if (!watchdogReason && opts.maxTokens && visibleChars > opts.maxTokens * 3) {
602
+ watchdogReason = "visible-output";
603
+ controller.abort("codex-visible-output-cap");
604
+ }
605
+ if (event.type === "done")
606
+ final = event.message;
607
+ else if (event.type === "error")
608
+ final = event.error;
609
+ }
610
+ if (watchdogReason) {
611
+ throw new Error("Codex " + watchdogReason + " watchdog stopped generation after " + watchdogMs + "ms / " + visibleChars + " streamed chars");
612
+ }
613
+ if (!final)
614
+ throw new Error("Codex stream ended without a final message");
615
+ return assertSuccessful(final);
616
+ } finally {
617
+ clearTimeout(timer);
618
+ opts.signal?.removeEventListener("abort", abortFromCaller);
619
+ }
620
+ }
621
+ var rawLlmClient = {
622
+ complete: async (model, body, originalOpts) => {
623
+ const opts = withCodexWireLimit(model, originalOpts);
624
+ return withProviderDeadline(opts, async (bounded) => {
625
+ if (isChatGptCodex(model))
626
+ return completeChatGptCodex(model, body, bounded);
627
+ const response = bounded.reasoning === undefined ? await (await resolveComplete())(model, body, bounded) : await (await resolveCompleteSimple())(model, body, bounded);
628
+ return assertSuccessful(response);
629
+ }, model.id);
630
+ }
631
+ };
632
+ var defaultLlmClient = rawLlmClient;
633
+ var _client = defaultLlmClient;
634
+ function getLlmClient() {
635
+ return _client;
636
+ }
637
+
638
+ // src/utils/cache.ts
639
+ import fs2 from "fs";
640
+
455
641
  // src/utils/type-guards.ts
456
642
  function isRecord(value) {
457
643
  return typeof value === "object" && value !== null;
@@ -513,13 +699,54 @@ function buildUniquePathNeedles(filePath, allPaths) {
513
699
  });
514
700
  }
515
701
  function isKnownPathReference(ref, knownPaths) {
516
- const normalizedRef = normalizePath(ref);
702
+ const normalizedRef = normalizePath(ref).replace(/^\/+/, "");
703
+ if (!normalizedRef)
704
+ return false;
705
+ const pathShaped = normalizedRef.includes("/");
517
706
  return knownPaths.some((path) => {
518
- const normalizedPath = normalizePath(path);
519
- return normalizedPath === normalizedRef || normalizedPath.endsWith("/" + normalizedRef);
707
+ const normalizedPath = normalizePath(path).replace(/^\/+/, "");
708
+ if (normalizedPath === normalizedRef || normalizedPath.endsWith("/" + normalizedRef))
709
+ return true;
710
+ if (normalizedPath.endsWith(normalizedRef)) {
711
+ const boundary = normalizedPath[normalizedPath.length - normalizedRef.length - 1];
712
+ if (boundary && !/[\w./-]/.test(boundary))
713
+ return true;
714
+ }
715
+ if (!pathShaped)
716
+ return false;
717
+ return normalizedPath.split("/").some((_, index, parts) => parts.slice(index).join("/").startsWith(normalizedRef + "/"));
520
718
  });
521
719
  }
522
720
 
721
+ // src/utils/logger.ts
722
+ var DEBUG = process.env.DEBUG?.includes("smart-compact") ?? false;
723
+ function warn(msg, err) {
724
+ const detail = err instanceof Error ? err.message : err ?? "";
725
+ console.error(LOG_PREFIX + " " + msg + (detail ? ": " + detail : ""));
726
+ }
727
+
728
+ // src/infra/paths.ts
729
+ import path from "path";
730
+ import os from "os";
731
+ function home() {
732
+ return process.env.HOME ?? os.homedir();
733
+ }
734
+ function piAgentDir() {
735
+ return path.join(home(), ".pi", "agent");
736
+ }
737
+ function cacheDir() {
738
+ return path.join(piAgentDir(), ".cache");
739
+ }
740
+ function smartCompactCacheDir() {
741
+ return path.join(cacheDir(), "smart-compact");
742
+ }
743
+ function metricsLogFile() {
744
+ return path.join(cacheDir(), "compact-metrics.jsonl");
745
+ }
746
+ function damageReportsFile() {
747
+ return path.join(smartCompactCacheDir(), "damage-reports.jsonl");
748
+ }
749
+
523
750
  // src/domain/tool-semantics.ts
524
751
  var PATH_KEYS = [
525
752
  "path",
@@ -602,10 +829,20 @@ function isLikelyFileRef(candidate) {
602
829
  return CODE_EXT_RE.test(candidate);
603
830
  }
604
831
  function extractFileRefs(summary) {
605
- const candidates = summary.match(FILE_REF_CANDIDATE_RE) ?? [];
606
- return candidates.filter(isLikelyFileRef);
832
+ const matcher = new RegExp(FILE_REF_CANDIDATE_RE.source, FILE_REF_CANDIDATE_RE.flags);
833
+ const refs = [];
834
+ for (const match of summary.matchAll(matcher)) {
835
+ if (/[\\/]/.test(summary[(match.index ?? 0) + match[0].length] ?? ""))
836
+ continue;
837
+ if (isLikelyFileRef(match[0]))
838
+ refs.push(match[0]);
839
+ }
840
+ return refs;
607
841
  }
608
842
 
843
+ // src/domain/summary-parse.ts
844
+ import { createHash } from "crypto";
845
+
609
846
  // src/domain/summary-schema.ts
610
847
  function classifyHeading(raw) {
611
848
  const text = raw.replace(/^#+\s*/, "").replace(/[:\s]+$/, "").trim().toLowerCase();
@@ -645,6 +882,58 @@ var HEADING_RE = /^(#{1,3})\s+(.+?)\s*$/;
645
882
  function summaryEvidenceLine(value, maxLength) {
646
883
  return value.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, " ").replace(/\s+/g, " ").trim().replace(/^(?:(?:#{1,6}|[-*+]|>)\s+)+/, "").slice(0, maxLength).trim();
647
884
  }
885
+ function summaryPathLine(value) {
886
+ return JSON.stringify(value);
887
+ }
888
+ function compactPathLine(value, maxLength, digest) {
889
+ const minimal = JSON.stringify("#" + digest);
890
+ if (minimal.length >= maxLength)
891
+ return minimal;
892
+ const chars = Array.from(value.replace(/\\/g, "/"));
893
+ let low = 0;
894
+ let high = chars.length;
895
+ let best = minimal;
896
+ while (low <= high) {
897
+ const length = Math.floor((low + high) / 2);
898
+ const candidate = JSON.stringify("\u2026/" + chars.slice(-length).join("") + "#" + digest);
899
+ if (candidate.length <= maxLength) {
900
+ best = candidate;
901
+ low = length + 1;
902
+ } else {
903
+ high = length - 1;
904
+ }
905
+ }
906
+ return best;
907
+ }
908
+ function buildSummaryPathEvidence(paths, budgetTokens = PROFILES.balanced.summaryBudgetTokens) {
909
+ const unique = Array.from(new Set(paths.filter(Boolean)));
910
+ if (!unique.length)
911
+ return new Map;
912
+ const full = unique.map((path2) => [path2, summaryPathLine(path2)]);
913
+ const minimumPerLine = JSON.stringify("#" + "x".repeat(12)).length + 3;
914
+ const budgetChars = Math.max(unique.length * minimumPerLine, Math.min(20000, Math.max(4000, Math.floor(budgetTokens * 2))));
915
+ if (full.reduce((total, [, line]) => total + line.length + 3, 0) <= budgetChars) {
916
+ return new Map(full);
917
+ }
918
+ const digests = new Map;
919
+ const owners = new Map;
920
+ for (const path2 of unique) {
921
+ const fullDigest = createHash("sha256").update(path2).digest("base64url");
922
+ let digest = fullDigest.slice(0, 12);
923
+ const owner = owners.get(digest);
924
+ if (owner && owner !== path2) {
925
+ digest = fullDigest;
926
+ digests.set(owner, createHash("sha256").update(owner).digest("base64url"));
927
+ }
928
+ owners.set(digest, path2);
929
+ digests.set(path2, digest);
930
+ }
931
+ const perPath = Math.max(JSON.stringify("#" + "x".repeat(12)).length, Math.floor((budgetChars - unique.length * 3) / unique.length));
932
+ return new Map(unique.map((path2) => [
933
+ path2,
934
+ compactPathLine(path2, perPath, digests.get(path2) ?? "")
935
+ ]));
936
+ }
648
937
  function mergeBodies(first, second) {
649
938
  const seen = new Set;
650
939
  return [first, second].filter(Boolean).flatMap((body) => body.split(`
@@ -669,7 +958,11 @@ function parseSummary(markdown) {
669
958
  if (existing)
670
959
  existing.body = mergeBodies(existing.body, body);
671
960
  else
672
- sections.push({ kind: currentKind, heading: currentHeading.trim(), body });
961
+ sections.push({
962
+ kind: currentKind,
963
+ heading: currentHeading.trim(),
964
+ body
965
+ });
673
966
  };
674
967
  for (const line of lines) {
675
968
  const fenceMatch = line.match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
@@ -760,7 +1053,11 @@ function buildToolCallIndex(msgs) {
760
1053
  for (let t = 0;t < nested.length; t++) {
761
1054
  const tool = nested[t];
762
1055
  const id = nestedToolCallId(b.id, i, t, tool.id);
763
- idx.set(id, { name: tool.name, arguments: tool.arguments, msgIndex: i });
1056
+ idx.set(id, {
1057
+ name: tool.name,
1058
+ arguments: tool.arguments,
1059
+ msgIndex: i
1060
+ });
764
1061
  }
765
1062
  }
766
1063
  }
@@ -768,46 +1065,42 @@ function buildToolCallIndex(msgs) {
768
1065
  return idx;
769
1066
  }
770
1067
  var CONSTRAINT_PATTERNS = [
771
- { re: /\b(?:must|need|require|has to|important)\b.*\b(?:be|use|have|include|support)\b/i, cat: "requirement", conf: TUNING.CONFIDENCE_HIGH },
772
- { re: /\b(?:don't|never|avoid|shouldn't|must not|do not|no\s+(?:need|want))\b/i, cat: "prohibition", conf: TUNING.CONFIDENCE_MEDIUM },
773
- { re: /\b(?:prefer|like|want|would rather|should)\b.*\b(?:use|be|have|with)\b/i, cat: "preference", conf: TUNING.CONFIDENCE_LOW },
774
- { re: /(?<![A-Za-z0-9_])(?:yapma|kullanma|sak\u0131n|sak\u0131nha|asla(?:\s+(?:kullanma|yapma|getirme))?|bunu yapma)(?![A-Za-z0-9_])/iu, cat: "prohibition", conf: TUNING.CONFIDENCE_MEDIUM },
775
- { re: /(?<![A-Za-z0-9_])(?:kritik|kritikal|\u00F6nemli|onemli|\u015Fart|sart|zorunlu|\u015Fart ko\u015Ful|\u00F6nemli \u015Fart|kesinlikle|kesinlikle \u015Fart|b\u00F6yle olsun|b\u00F6yle yap\u0131n|\u015F\u00F6yle olsun|\u015F\u00F6yle yap\u0131n)(?![A-Za-z0-9_])/iu, cat: "requirement", conf: TUNING.CONFIDENCE_MEDIUM },
776
- { re: /(?<![A-Za-z0-9_])(?:tercih|isterim|olsun|kullanal\u0131m|yapal\u0131m|istiyorum)(?![A-Za-z0-9_])/iu, cat: "preference", conf: TUNING.CONFIDENCE_LOW }
1068
+ {
1069
+ re: /\b(?:must|need|require|has to|important)\b.*\b(?:be|use|have|include|support)\b/i,
1070
+ cat: "requirement",
1071
+ conf: TUNING.CONFIDENCE_HIGH
1072
+ },
1073
+ {
1074
+ re: /\b(?:don't|never|avoid|shouldn't|must not|do not|no\s+(?:need|want))\b/i,
1075
+ cat: "prohibition",
1076
+ conf: TUNING.CONFIDENCE_MEDIUM
1077
+ },
1078
+ {
1079
+ re: /\b(?:prefer|like|want|would rather|should)\b.*\b(?:use|be|have|with)\b/i,
1080
+ cat: "preference",
1081
+ conf: TUNING.CONFIDENCE_LOW
1082
+ },
1083
+ {
1084
+ re: /(?<![A-Za-z0-9_])(?:yapma|kullanma|sak\u0131n|sak\u0131nha|asla(?:\s+(?:kullanma|yapma|getirme))?|bunu yapma)(?![A-Za-z0-9_])/iu,
1085
+ cat: "prohibition",
1086
+ conf: TUNING.CONFIDENCE_MEDIUM
1087
+ },
1088
+ {
1089
+ re: /(?<![A-Za-z0-9_])(?:kritik|kritikal|\u00F6nemli|onemli|\u015Fart|sart|zorunlu|\u015Fart ko\u015Ful|\u00F6nemli \u015Fart|kesinlikle|kesinlikle \u015Fart|b\u00F6yle olsun|b\u00F6yle yap\u0131n|\u015F\u00F6yle olsun|\u015F\u00F6yle yap\u0131n)(?![A-Za-z0-9_])/iu,
1090
+ cat: "requirement",
1091
+ conf: TUNING.CONFIDENCE_MEDIUM
1092
+ },
1093
+ {
1094
+ re: /(?<![A-Za-z0-9_])(?:tercih|isterim|olsun|kullanal\u0131m|yapal\u0131m|istiyorum)(?![A-Za-z0-9_])/iu,
1095
+ cat: "preference",
1096
+ conf: TUNING.CONFIDENCE_LOW
1097
+ }
777
1098
  ];
778
1099
  function isDiagnosticConstraintText(text) {
779
1100
  const candidate = text.replace(/^\s*[-*]\s+/, "").trim();
780
1101
  return /^(?:\[[^\]]+\]\s*)?(?:npm\s+(?:error|warn|notice|audit|verbose|info)\b|(?:rg|grep):|command exited\b)/i.test(candidate);
781
1102
  }
782
1103
 
783
- // src/utils/logger.ts
784
- var DEBUG = process.env.DEBUG?.includes("smart-compact") ?? false;
785
- function warn(msg, err) {
786
- const detail = err instanceof Error ? err.message : err ?? "";
787
- console.error(LOG_PREFIX + " " + msg + (detail ? ": " + detail : ""));
788
- }
789
-
790
- // src/infra/paths.ts
791
- import path from "path";
792
- function home() {
793
- return process.env.HOME ?? "/tmp";
794
- }
795
- function piAgentDir() {
796
- return path.join(home(), ".pi", "agent");
797
- }
798
- function cacheDir() {
799
- return path.join(piAgentDir(), ".cache");
800
- }
801
- function smartCompactCacheDir() {
802
- return path.join(cacheDir(), "smart-compact");
803
- }
804
- function metricsLogFile() {
805
- return path.join(cacheDir(), "compact-metrics.jsonl");
806
- }
807
- function damageReportsFile() {
808
- return path.join(smartCompactCacheDir(), "damage-reports.jsonl");
809
- }
810
-
811
1104
  // src/infra/fs.ts
812
1105
  import fs from "fs";
813
1106
  function readJsonlTail(target, limit, maxBytes = 512 * 1024) {
@@ -850,7 +1143,10 @@ import crypto from "crypto";
850
1143
 
851
1144
  // src/domain/scrub.ts
852
1145
  var SECRET_PATTERNS = [
853
- { kind: "private-key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g },
1146
+ {
1147
+ kind: "private-key",
1148
+ regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g
1149
+ },
854
1150
  { kind: "aws-access-key", regex: /\bAKIA[0-9A-Z]{16}\b/g },
855
1151
  { kind: "google-api-key", regex: /\bAIza[0-9A-Za-z_-]{30,}\b/g },
856
1152
  { kind: "stripe-key", regex: /\b[rs]k_(?:live|test)_[0-9A-Za-z]{16,}\b/g },
@@ -859,8 +1155,15 @@ var SECRET_PATTERNS = [
859
1155
  { kind: "github-token", regex: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g },
860
1156
  { kind: "api-key", regex: /\bsk-(?:ant-)?[A-Za-z0-9_-]{20,}\b/g },
861
1157
  { kind: "slack-token", regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
862
- { kind: "jwt", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
863
- { kind: "bearer-token", regex: /\bBearer\s+[A-Za-z0-9._~+\/-]{12,}=*/gi, replacement: () => "Bearer [REDACTED:bearer-token]" },
1158
+ {
1159
+ kind: "jwt",
1160
+ regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g
1161
+ },
1162
+ {
1163
+ kind: "bearer-token",
1164
+ regex: /\bBearer\s+[A-Za-z0-9._~+/-]{12,}=*/gi,
1165
+ replacement: () => "Bearer [REDACTED:bearer-token]"
1166
+ },
864
1167
  {
865
1168
  kind: "connection-password",
866
1169
  regex: /\b([a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:)[^@\s/]+(@)/gi,
@@ -869,12 +1172,35 @@ var SECRET_PATTERNS = [
869
1172
  {
870
1173
  kind: "credential",
871
1174
  regex: /\b((?:[A-Za-z0-9]+[_-])*(?:api[_-]?key|access[_-]?token|auth[_-]?token|token|password|passwd|secret(?:[_-]?(?:access)?[_-]?key)?|client[_-]?secret)(?:[_-][A-Za-z0-9]+)*)\s*([:=])\s*["']?([^\s"']{16,})["']?/gi,
872
- replacement: (name, separator) => name + separator + "[REDACTED:credential]"
1175
+ replacement: (name, separator, value, match) => /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(value) ? match : name + separator + "[REDACTED:credential]"
873
1176
  }
874
1177
  ];
1178
+ function passesLuhn(candidate) {
1179
+ const digits = candidate.replace(/\D/g, "");
1180
+ if (digits.length < 13 || digits.length > 19)
1181
+ return false;
1182
+ if (/^(\d)\1+$/.test(digits))
1183
+ return false;
1184
+ let sum = 0, double = false;
1185
+ for (let i = digits.length - 1;i >= 0; i--) {
1186
+ let d = digits.charCodeAt(i) - 48;
1187
+ if (double) {
1188
+ d *= 2;
1189
+ if (d > 9)
1190
+ d -= 9;
1191
+ }
1192
+ sum += d;
1193
+ double = !double;
1194
+ }
1195
+ return sum % 10 === 0;
1196
+ }
875
1197
  var PII_PATTERNS = [
876
1198
  { kind: "email", regex: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi },
877
- { kind: "payment-card", regex: /\b(?:\d[ -]*?){13,19}\b/g },
1199
+ {
1200
+ kind: "payment-card",
1201
+ regex: /\b(?:\d[ -]*?){13,19}\b/g,
1202
+ replacement: (candidate) => passesLuhn(candidate) ? "[REDACTED:payment-card]" : candidate
1203
+ },
878
1204
  { kind: "phone", regex: /(?<![\w.])(?:\+?\d[\d ()-]{8,}\d)(?![\w.])/g }
879
1205
  ];
880
1206
  function redact(text, patterns) {
@@ -882,15 +1208,22 @@ function redact(text, patterns) {
882
1208
  let value = text;
883
1209
  for (const pattern of patterns) {
884
1210
  value = value.replace(pattern.regex, (...args) => {
885
- counts.set(pattern.kind, (counts.get(pattern.kind) ?? 0) + 1);
1211
+ const match = String(args[0]);
1212
+ let replacement = "[REDACTED:" + pattern.kind + "]";
886
1213
  if (pattern.replacement) {
887
1214
  const groups = args.slice(1, -2).map(String);
888
- return pattern.replacement(...groups);
1215
+ replacement = pattern.replacement(...groups, match);
889
1216
  }
890
- return "[REDACTED:" + pattern.kind + "]";
1217
+ if (replacement === match)
1218
+ return match;
1219
+ counts.set(pattern.kind, (counts.get(pattern.kind) ?? 0) + 1);
1220
+ return replacement;
891
1221
  });
892
1222
  }
893
- return { value, findings: [...counts].map(([kind, count]) => ({ kind, count })) };
1223
+ return {
1224
+ value,
1225
+ findings: [...counts].map(([kind, count]) => ({ kind, count }))
1226
+ };
894
1227
  }
895
1228
  function mergeFindings(target, findings) {
896
1229
  for (const finding of findings)
@@ -920,7 +1253,6 @@ var SECRET_KEY_NAMES = {
920
1253
  set_cookie: true,
921
1254
  otp: true,
922
1255
  one_time_password: true,
923
- pin: true,
924
1256
  passcode: true
925
1257
  };
926
1258
  function normalizeObjectKey(key) {
@@ -986,7 +1318,7 @@ class SecretScrubber {
986
1318
  const output = {};
987
1319
  seen.set(value2, output);
988
1320
  for (const [key, item] of Object.entries(value2)) {
989
- const carriesSecret = typeof item === "string" ? item.length > 0 : item != null;
1321
+ const carriesSecret = typeof item === "string" && item.length >= 8;
990
1322
  if (this.secretsEnabled && isSecretBearingKey(key) && carriesSecret) {
991
1323
  output[key] = "[REDACTED:credential]";
992
1324
  recordCredential();
@@ -997,7 +1329,10 @@ class SecretScrubber {
997
1329
  return output;
998
1330
  };
999
1331
  const value = visit(input);
1000
- return { value, findings: [...findings].map(([kind, count]) => ({ kind, count })) };
1332
+ return {
1333
+ value,
1334
+ findings: [...findings].map(([kind, count]) => ({ kind, count }))
1335
+ };
1001
1336
  }
1002
1337
  count() {
1003
1338
  return this.total;
@@ -1283,6 +1618,76 @@ function readMetricsLog(limit = 100) {
1283
1618
  // src/phases/verify.ts
1284
1619
  var HIGH_RISK_OUTCOME_RE = /(?:\ball\s+tests?\s+(?:pass|passed|passing)\b|\btests?\s+(?:pass|passed|passing)\b|\b(?:build|deployment|migration)\s+(?:completed|succeeded|passed|successful)\b|\b(?:deployed|published|released)\b|\b(?:bug|issue|error)\s+(?:fixed|resolved)\b|\bno\s+(?:errors?|failures?)\b|\bcompleted successfully\b|\btestler?\s+(?:ge\u00E7ti|ba\u015Far\u0131l\u0131)\b|\bba\u015Far\u0131yla\s+(?:tamamland\u0131|da\u011F\u0131t\u0131ld\u0131|yay\u0131nland\u0131)\b|\b(?:deploy edildi|yay\u0131nland\u0131|hata yok)\b)/iu;
1285
1620
  var NEGATED_OUTCOME_RE = /\b(?:not|never|pending|failed|failing|unresolved|hen\u00FCz|de\u011Fil|ba\u015Far\u0131s\u0131z)\b/iu;
1621
+ var NONE_BLOCKER_VALUE_RE = /^(?:none|no blockers?|yok)\s*(?:recorded|known)?[.!]?$/i;
1622
+ var BULLET_NONE_BLOCKER_RE = /^(?:[-*+]|\d+[.)])\s+(?:none|no blockers?|yok)\s*(?:recorded|known)?[.!]?$/i;
1623
+ var PATH_PLACEHOLDER_RE = /^(?:none|none recorded|no blockers?|yok)[.!]?$/i;
1624
+ function noneBlockerLineIndexes(lines) {
1625
+ const indexes = new Set;
1626
+ const nonEmpty = lines.map((line, index) => ({ index, text: line.trim() })).filter((item) => item.text);
1627
+ for (const item of nonEmpty) {
1628
+ if (BULLET_NONE_BLOCKER_RE.test(item.text))
1629
+ indexes.add(item.index);
1630
+ }
1631
+ if (nonEmpty.length === 1 && NONE_BLOCKER_VALUE_RE.test(nonEmpty[0].text)) {
1632
+ indexes.add(nonEmpty[0].index);
1633
+ }
1634
+ return indexes;
1635
+ }
1636
+ function collectListedPaths(body, expectedPaths) {
1637
+ const values = new Set;
1638
+ const encodedValues = new Set;
1639
+ for (const line of body.split(`
1640
+ `)) {
1641
+ const raw = line.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "").trim();
1642
+ if (!raw)
1643
+ continue;
1644
+ if (raw.startsWith('"')) {
1645
+ try {
1646
+ const decoded = JSON.parse(raw);
1647
+ if (typeof decoded === "string") {
1648
+ values.add(decoded);
1649
+ encodedValues.add(decoded);
1650
+ continue;
1651
+ }
1652
+ } catch {}
1653
+ }
1654
+ values.add(raw);
1655
+ if (expectedPaths.has(raw))
1656
+ continue;
1657
+ const unwrapped = raw.startsWith("`") && raw.endsWith("`") ? raw.slice(1, -1) : raw;
1658
+ const unchecked = unwrapped.replace(/^\[[ x]\]\s+/i, "");
1659
+ if (expectedPaths.has(unchecked))
1660
+ values.add(unchecked);
1661
+ }
1662
+ return {
1663
+ values,
1664
+ encodedValues,
1665
+ normalizedValues: new Set(Array.from(values, normalizePath))
1666
+ };
1667
+ }
1668
+ function decodePathDisplay(display) {
1669
+ try {
1670
+ const decoded = JSON.parse(display);
1671
+ return typeof decoded === "string" ? decoded : display;
1672
+ } catch {
1673
+ return display;
1674
+ }
1675
+ }
1676
+ function hasListedPath(listed, file, display, normalizedOwners) {
1677
+ const decodedDisplay = decodePathDisplay(display);
1678
+ if (listed.encodedValues.has(decodedDisplay))
1679
+ return true;
1680
+ if (PATH_PLACEHOLDER_RE.test(file))
1681
+ return false;
1682
+ if (listed.values.has(file))
1683
+ return true;
1684
+ for (const candidate of [file, decodedDisplay]) {
1685
+ const normalized = normalizePath(candidate);
1686
+ if (normalizedOwners.get(normalized) === 1 && listed.normalizedValues.has(normalized))
1687
+ return true;
1688
+ }
1689
+ return false;
1690
+ }
1286
1691
  function outcomeClaims(summary) {
1287
1692
  return Array.from(new Set(summary.split(/\r?\n/).map((line) => line.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "").replace(/^\[[ x]\]\s+/i, "").trim()).filter((line) => line.length > 0 && !line.startsWith("#")).filter((line) => HIGH_RISK_OUTCOME_RE.test(line)).filter((line) => /\bno\s+(?:errors?|failures?)\b/i.test(line) || !NEGATED_OUTCOME_RE.test(line)))).slice(0, 12);
1288
1693
  }
@@ -1301,6 +1706,28 @@ function classifyOutcomeClaim(claim) {
1301
1706
  return "generic";
1302
1707
  }
1303
1708
  var successfulToolEvidenceCache = new WeakMap;
1709
+ var sourceTextCache = new WeakMap;
1710
+ function sourceSupportsFileReference(ref, messages) {
1711
+ let texts = sourceTextCache.get(messages);
1712
+ if (!texts) {
1713
+ texts = messages.map((message) => extractText(message.content).replace(/\\/g, "/").toLowerCase());
1714
+ sourceTextCache.set(messages, texts);
1715
+ }
1716
+ const needle = ref.replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
1717
+ if (!needle)
1718
+ return false;
1719
+ for (const text of texts) {
1720
+ let index = text.indexOf(needle);
1721
+ while (index >= 0) {
1722
+ const before = text[index - 1] ?? "";
1723
+ const after = text[index + needle.length] ?? "";
1724
+ if ((!before || !/[\w.-]/.test(before)) && (!after || !/[\w.-]/.test(after)))
1725
+ return true;
1726
+ index = text.indexOf(needle, index + 1);
1727
+ }
1728
+ }
1729
+ return false;
1730
+ }
1304
1731
  function successfulToolEvidence(messages) {
1305
1732
  const cached = successfulToolEvidenceCache.get(messages);
1306
1733
  if (cached)
@@ -1419,8 +1846,72 @@ var SEMANTIC_STOP = new Set([
1419
1846
  "de\u011Fil",
1420
1847
  "olmadan"
1421
1848
  ]);
1849
+ var TR_SUFFIXES = [
1850
+ "lar\u0131",
1851
+ "leri",
1852
+ "\u0131n\u0131n",
1853
+ "inin",
1854
+ "unun",
1855
+ "\xFCn\xFCn",
1856
+ "\u0131nda",
1857
+ "inde",
1858
+ "unda",
1859
+ "\xFCnde",
1860
+ "m\u0131\u015F",
1861
+ "mi\u015F",
1862
+ "mu\u015F",
1863
+ "m\xFC\u015F",
1864
+ "lar",
1865
+ "ler",
1866
+ "\u0131n\u0131",
1867
+ "ini",
1868
+ "unu",
1869
+ "\xFCn\xFC",
1870
+ "\u0131na",
1871
+ "ine",
1872
+ "una",
1873
+ "\xFCne",
1874
+ "dan",
1875
+ "den",
1876
+ "tan",
1877
+ "ten",
1878
+ "d\u0131r",
1879
+ "dir",
1880
+ "dur",
1881
+ "d\xFCr",
1882
+ "t\u0131r",
1883
+ "tir",
1884
+ "tur",
1885
+ "t\xFCr",
1886
+ "yor",
1887
+ "mak",
1888
+ "mek",
1889
+ "da",
1890
+ "de",
1891
+ "ta",
1892
+ "te",
1893
+ "d\u0131",
1894
+ "di",
1895
+ "du",
1896
+ "d\xFC",
1897
+ "t\u0131",
1898
+ "ti",
1899
+ "tu",
1900
+ "t\xFC",
1901
+ "\u0131n",
1902
+ "in",
1903
+ "un",
1904
+ "\xFCn",
1905
+ "sa",
1906
+ "se"
1907
+ ];
1422
1908
  function stemToken(token) {
1423
1909
  const lower = token.toLocaleLowerCase();
1910
+ for (const suffix of TR_SUFFIXES) {
1911
+ if (lower.length >= 4 + suffix.length && lower.endsWith(suffix)) {
1912
+ return lower.slice(0, -suffix.length);
1913
+ }
1914
+ }
1424
1915
  if (lower.length > 6 && lower.endsWith("ing"))
1425
1916
  return lower.slice(0, -3);
1426
1917
  if (lower.length > 5 && lower.endsWith("ed"))
@@ -1539,11 +2030,15 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
1539
2030
  };
1540
2031
  const unresolvedEvidence = uniqueByText([
1541
2032
  ...extraction.errors.filter((error) => !error.resolved).map((error) => ({ message: error.message })),
1542
- ...(continuity?.unresolvedErrors ?? []).map((error) => ({ message: error.message }))
2033
+ ...(continuity?.unresolvedErrors ?? []).map((error) => ({
2034
+ message: error.message
2035
+ }))
1543
2036
  ], (item) => item.message);
1544
2037
  const resolvedEvidence = uniqueByText([
1545
2038
  ...extraction.errors.filter((error) => error.resolved).map((error) => ({ message: error.message })),
1546
- ...(continuity?.resolvedErrors ?? []).map((error) => ({ message: error.message }))
2039
+ ...(continuity?.resolvedErrors ?? []).map((error) => ({
2040
+ message: error.message
2041
+ }))
1547
2042
  ], (item) => item.message).slice(-5);
1548
2043
  const steeringConstraints = [
1549
2044
  evidence.steering?.focus ? { text: "Preserve detail about: " + evidence.steering.focus } : null,
@@ -1570,40 +2065,64 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
1570
2065
  score -= req.penalty;
1571
2066
  }
1572
2067
  }
1573
- const listedPaths = (kind) => new Set((findSection(parsed, kind)?.body ?? "").split(`
1574
- `).map((line) => line.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "").replace(/^\[[ x]\]\s+/i, "").trim()).map((line) => line.startsWith("`") && line.endsWith("`") ? line.slice(1, -1) : line).filter((line) => line.length > 0 && !/^none(?: recorded)?[.!]?$/i.test(line)).map(normalizePath));
1575
2068
  const modifiedPaths = extraction.modifiedFiles.map((file) => file.path);
2069
+ const readPaths = extraction.readFiles;
2070
+ const deletedEvidence = Array.from(new Set([...extraction.deletedFiles, ...continuity?.deletedFiles ?? []]));
2071
+ const requiredPaths = [...modifiedPaths, ...readPaths, ...deletedEvidence];
2072
+ const expectedPathSet = new Set(requiredPaths);
2073
+ const pathEvidence = buildSummaryPathEvidence(requiredPaths, evidence.summaryBudgetTokens);
2074
+ const normalizedOwnerSets = new Map;
2075
+ for (const file of requiredPaths) {
2076
+ const display = pathEvidence.get(file);
2077
+ for (const candidate of [
2078
+ file,
2079
+ ...display ? [decodePathDisplay(display)] : []
2080
+ ]) {
2081
+ const normalized = normalizePath(candidate);
2082
+ const owners = normalizedOwnerSets.get(normalized) ?? new Set;
2083
+ owners.add(file);
2084
+ normalizedOwnerSets.set(normalized, owners);
2085
+ }
2086
+ }
2087
+ const normalizedOwners = new Map(Array.from(normalizedOwnerSets, ([path2, owners]) => [path2, owners.size]));
2088
+ const listedPaths = (kind) => collectListedPaths(findSection(parsed, kind)?.body ?? "", expectedPathSet);
1576
2089
  const modifiedListed = listedPaths("files-modified");
1577
2090
  const readListed = listedPaths("files-read");
1578
2091
  const deletedListed = listedPaths("files-deleted");
1579
2092
  for (const file of modifiedPaths) {
1580
- if (!modifiedListed.has(normalizePath(file)))
2093
+ const display = pathEvidence.get(file);
2094
+ if (display && !hasListedPath(modifiedListed, file, display, normalizedOwners)) {
1581
2095
  gaps.push({ kind: "missing-file", path: file });
2096
+ }
1582
2097
  }
1583
- for (const file of extraction.readFiles) {
1584
- if (!readListed.has(normalizePath(file)))
2098
+ for (const file of readPaths) {
2099
+ const display = pathEvidence.get(file);
2100
+ if (display && !hasListedPath(readListed, file, display, normalizedOwners)) {
1585
2101
  gaps.push({ kind: "missing-read-file", path: file });
2102
+ }
1586
2103
  }
1587
- const deletedEvidence = Array.from(new Set([
1588
- ...extraction.deletedFiles,
1589
- ...continuity?.deletedFiles ?? []
1590
- ]));
1591
2104
  for (const file of deletedEvidence) {
1592
- if (!deletedListed.has(normalizePath(file)))
2105
+ const display = pathEvidence.get(file);
2106
+ if (display && !hasListedPath(deletedListed, file, display, normalizedOwners)) {
1593
2107
  gaps.push({ kind: "missing-deleted-file", path: file });
2108
+ }
1594
2109
  }
1595
2110
  score -= gaps.filter((gap) => gap.kind === "missing-file" || gap.kind === "missing-read-file" || gap.kind === "missing-deleted-file").length * 5;
1596
2111
  for (const error of unresolvedEvidence) {
1597
- const snippet = summaryEvidenceLine(error.message, TRUNC.ERROR_SNIPPET).toLowerCase();
2112
+ const snippet = summaryEvidenceLine(error.message, TRUNC.ERROR_SNIPPET).toLowerCase().replace(/\\/g, "/");
1598
2113
  if (snippet.length > 5 && !normalizedSummary.includes(snippet)) {
1599
2114
  gaps.push({ kind: "missing-error", message: error.message });
1600
2115
  score -= 5;
1601
2116
  }
1602
2117
  }
1603
2118
  for (const error of resolvedEvidence) {
1604
- const snippet = summaryEvidenceLine(error.message, TRUNC.ERROR_SNIPPET).toLowerCase();
2119
+ const snippet = summaryEvidenceLine(error.message, TRUNC.ERROR_SNIPPET).toLowerCase().replace(/\\/g, "/");
1605
2120
  if (snippet.length > 5 && !normalizedSummary.includes(snippet)) {
1606
- gaps.push({ kind: "missing-error", message: error.message, resolved: true });
2121
+ gaps.push({
2122
+ kind: "missing-error",
2123
+ message: error.message,
2124
+ resolved: true
2125
+ });
1607
2126
  score -= 2;
1608
2127
  }
1609
2128
  }
@@ -1618,7 +2137,10 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
1618
2137
  score -= 8;
1619
2138
  }
1620
2139
  if (hasSemanticContradiction(constraint.text, constraintTarget)) {
1621
- gaps.push({ kind: "inconsistency", detail: "semantic-contradiction: constraint contradicts " + constraint.text.slice(0, TRUNC.SNIPPET) });
2140
+ gaps.push({
2141
+ kind: "inconsistency",
2142
+ detail: "semantic-contradiction: constraint contradicts " + constraint.text.slice(0, TRUNC.SNIPPET)
2143
+ });
1622
2144
  score -= 20;
1623
2145
  }
1624
2146
  }
@@ -1629,32 +2151,52 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
1629
2151
  score -= 12;
1630
2152
  }
1631
2153
  if (hasSemanticContradiction(goalEvidence, goalTarget)) {
1632
- gaps.push({ kind: "inconsistency", detail: "semantic-contradiction: goal polarity or condition changed" });
2154
+ gaps.push({
2155
+ kind: "inconsistency",
2156
+ detail: "semantic-contradiction: goal polarity or condition changed"
2157
+ });
1633
2158
  score -= 20;
1634
2159
  }
1635
2160
  }
1636
- const groundedEvidenceFiles = [
2161
+ const groundedEvidence = [
1637
2162
  ...unresolvedEvidence.map((item) => item.message),
2163
+ ...resolvedEvidence.map((item) => item.message),
1638
2164
  ...constraintEvidence.map((item) => item.text),
1639
2165
  ...decisionEvidence.map((item) => item.summary),
1640
2166
  ...goalEvidence ? [goalEvidence] : [],
2167
+ ...extraction.lastUserMessages,
2168
+ ...extraction.timeline.map((item) => item.summary),
2169
+ ...extraction.topics.map((item) => item.primaryFile ?? ""),
1641
2170
  ...continuity?.openLoops.map((item) => item.summary) ?? [],
1642
2171
  ...continuity?.criticalContext ?? []
1643
- ].flatMap(extractFileRefs);
1644
- const knownFiles = Array.from(new Set([
2172
+ ];
2173
+ const groundedEvidenceFiles = groundedEvidence.flatMap((value) => [
2174
+ value,
2175
+ summaryEvidenceLine(value, TRUNC.ERROR_SNIPPET),
2176
+ summaryEvidenceLine(value, TRUNC.TOPIC_LABEL),
2177
+ summaryEvidenceLine(value, TRUNC.PREVIEW),
2178
+ summaryEvidenceLine(value, TRUNC.MESSAGE)
2179
+ ]).flatMap(extractFileRefs);
2180
+ const renderedPathEvidence = Array.from(pathEvidence.values()).flatMap((line) => [
2181
+ decodePathDisplay(line),
2182
+ line.startsWith('"') && line.endsWith('"') ? line.slice(1, -1) : line
2183
+ ]);
2184
+ const rawKnownFiles = Array.from(new Set([
1645
2185
  ...modifiedPaths,
1646
- ...extraction.readFiles,
1647
- ...extraction.deletedFiles,
2186
+ ...readPaths,
2187
+ ...deletedEvidence,
1648
2188
  ...extraction.referencedFiles ?? [],
1649
2189
  ...groundedEvidenceFiles,
2190
+ ...renderedPathEvidence,
2191
+ ...renderedPathEvidence.flatMap(extractFileRefs),
1650
2192
  ...continuity?.modifiedFiles ?? [],
1651
2193
  ...continuity?.readFiles ?? [],
1652
- ...continuity?.deletedFiles ?? [],
1653
2194
  ...(continuity?.unresolvedErrors ?? []).flatMap((error) => error.files),
1654
2195
  ...(continuity?.openLoops ?? []).flatMap((loop) => loop.files)
1655
2196
  ]));
1656
2197
  for (const ref of new Set(extractFileRefs(summary))) {
1657
- if (!isKnownPathReference(ref, knownFiles)) {
2198
+ const grounded = isKnownPathReference(ref, rawKnownFiles) || Boolean(evidence.sourceMessages && sourceSupportsFileReference(ref, evidence.sourceMessages));
2199
+ if (!grounded) {
1658
2200
  gaps.push({ kind: "fabricated-file", ref });
1659
2201
  score -= 4;
1660
2202
  }
@@ -1663,8 +2205,12 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
1663
2205
  if (progressSection) {
1664
2206
  const doneSection = progressSection.body.match(/###\s*Done[\s\S]*?(?=###|$)/i)?.[0] ?? "";
1665
2207
  const blockedSection = progressSection.body.match(/###\s*Blocked[\s\S]*?(?=###|$)/i)?.[0] ?? "";
1666
- if (unresolvedEvidence.length > 0 && /(?:none|no blockers?|yok)\s*(?:recorded|known)?[.!]?\s*$/im.test(blockedSection)) {
1667
- gaps.push({ kind: "inconsistency", detail: "blocked-none: Blocked says none despite unresolved errors" });
2208
+ const blockedLines = blockedSection.split(/\r?\n/).slice(1);
2209
+ if (unresolvedEvidence.length > 0 && noneBlockerLineIndexes(blockedLines).size > 0) {
2210
+ gaps.push({
2211
+ kind: "inconsistency",
2212
+ detail: "blocked-none: Blocked says none despite unresolved errors"
2213
+ });
1668
2214
  score -= 12;
1669
2215
  }
1670
2216
  const doneRefs = new Set(extractFileRefs(doneSection).map(normalizePath));
@@ -1678,7 +2224,10 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
1678
2224
  return uniqueNeedles.some((needle) => errorRefs.includes(normalizePath(needle)));
1679
2225
  });
1680
2226
  if (unresolved) {
1681
- gaps.push({ kind: "inconsistency", detail: file.path + " marked Done but has unresolved error" });
2227
+ gaps.push({
2228
+ kind: "inconsistency",
2229
+ detail: file.path + " marked Done but has unresolved error"
2230
+ });
1682
2231
  score -= 5;
1683
2232
  }
1684
2233
  }
@@ -1690,7 +2239,10 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
1690
2239
  score -= 8;
1691
2240
  }
1692
2241
  if (hasSemanticContradiction(decision.summary, decisionBody)) {
1693
- gaps.push({ kind: "inconsistency", detail: "semantic-contradiction: decision contradicts " + decision.summary.slice(0, TRUNC.SNIPPET) });
2242
+ gaps.push({
2243
+ kind: "inconsistency",
2244
+ detail: "semantic-contradiction: decision contradicts " + decision.summary.slice(0, TRUNC.SNIPPET)
2245
+ });
1694
2246
  score -= 20;
1695
2247
  }
1696
2248
  }