pi-smart-compact 9.3.1 → 9.4.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.
Files changed (72) hide show
  1. package/ARCHITECTURE.md +47 -10
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +18 -2
  4. package/dist/app/model-routing.d.ts +12 -0
  5. package/dist/app/model-routing.d.ts.map +1 -0
  6. package/dist/app/preflight.d.ts.map +1 -1
  7. package/dist/app/register-context-tools.d.ts +5 -0
  8. package/dist/app/register-context-tools.d.ts.map +1 -0
  9. package/dist/app/register-smart-compact-command.d.ts +13 -0
  10. package/dist/app/register-smart-compact-command.d.ts.map +1 -0
  11. package/dist/app/register-smart-compact-tool.d.ts +13 -0
  12. package/dist/app/register-smart-compact-tool.d.ts.map +1 -0
  13. package/dist/app/smart-compact-input.d.ts +1 -1
  14. package/dist/app/smart-compact-input.d.ts.map +1 -1
  15. package/dist/app/smart-compact-policy.d.ts +31 -0
  16. package/dist/app/smart-compact-policy.d.ts.map +1 -0
  17. package/dist/app/steps/extract.d.ts.map +1 -1
  18. package/dist/app/steps/metrics.d.ts.map +1 -1
  19. package/dist/app/steps/persist.d.ts.map +1 -1
  20. package/dist/app/steps/prepare.d.ts.map +1 -1
  21. package/dist/app/steps/tier.d.ts.map +1 -1
  22. package/dist/app/steps/window.d.ts +0 -1
  23. package/dist/app/steps/window.d.ts.map +1 -1
  24. package/dist/constants.d.ts +2 -1
  25. package/dist/constants.d.ts.map +1 -1
  26. package/dist/index.d.ts +2 -10
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +6353 -5565
  29. package/dist/infra/context-graph.d.ts +7 -3
  30. package/dist/infra/context-graph.d.ts.map +1 -1
  31. package/dist/infra/fs.d.ts +0 -1
  32. package/dist/infra/fs.d.ts.map +1 -1
  33. package/dist/infra/llm-client.d.ts +2 -1
  34. package/dist/infra/llm-client.d.ts.map +1 -1
  35. package/dist/infra/paths.d.ts +3 -14
  36. package/dist/infra/paths.d.ts.map +1 -1
  37. package/dist/phases/explore.d.ts +0 -8
  38. package/dist/phases/explore.d.ts.map +1 -1
  39. package/dist/phases/synthesize.d.ts.map +1 -1
  40. package/dist/phases/verify.d.ts.map +1 -1
  41. package/dist/provider-eval.js +38 -34
  42. package/dist/provider-scenario-eval.js +330 -215
  43. package/dist/telemetry-report.js +11 -7
  44. package/dist/types.d.ts +4 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/ui/backup-overlays.d.ts +9 -0
  47. package/dist/ui/backup-overlays.d.ts.map +1 -0
  48. package/dist/ui/metrics-dashboard-overlay.d.ts +12 -0
  49. package/dist/ui/metrics-dashboard-overlay.d.ts.map +1 -0
  50. package/dist/ui/open-loops-overlay.d.ts +4 -0
  51. package/dist/ui/open-loops-overlay.d.ts.map +1 -0
  52. package/dist/ui/overlays.d.ts +3 -28
  53. package/dist/ui/overlays.d.ts.map +1 -1
  54. package/dist/ui/settings-overlay.d.ts +5 -0
  55. package/dist/ui/settings-overlay.d.ts.map +1 -0
  56. package/dist/utils/backups.d.ts.map +1 -1
  57. package/dist/utils/cache.d.ts +2 -18
  58. package/dist/utils/cache.d.ts.map +1 -1
  59. package/dist/utils/config.d.ts +7 -0
  60. package/dist/utils/config.d.ts.map +1 -0
  61. package/dist/utils/damage.d.ts +4 -0
  62. package/dist/utils/damage.d.ts.map +1 -1
  63. package/dist/utils/extraction.d.ts +0 -2
  64. package/dist/utils/extraction.d.ts.map +1 -1
  65. package/dist/utils/fingerprint.d.ts.map +1 -1
  66. package/dist/utils/helpers.d.ts +3 -13
  67. package/dist/utils/helpers.d.ts.map +1 -1
  68. package/dist/utils/logger.d.ts +0 -1
  69. package/dist/utils/logger.d.ts.map +1 -1
  70. package/dist/utils/state.d.ts +1 -2
  71. package/dist/utils/state.d.ts.map +1 -1
  72. package/package.json +7 -4
@@ -5,7 +5,7 @@ var __require = import.meta.require;
5
5
  import { ModelRegistry, ModelRuntime } from "@earendil-works/pi-coding-agent";
6
6
 
7
7
  // src/constants.ts
8
- var VERSION = "9.3.1";
8
+ var VERSION = "9.4.0";
9
9
  var SETTLED_TRIGGER_COOLDOWN_MS = 10 * 60000;
10
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.";
11
11
  var PROFILES = {
@@ -43,6 +43,7 @@ var DEFAULT_CONFIG = {
43
43
  verificationModel: null,
44
44
  summaryThinkingLevel: "minimal",
45
45
  segmentationThinkingLevel: "minimal",
46
+ agentToolAccess: "inherit",
46
47
  autoTrigger: true,
47
48
  autoTriggerStrategy: "native-hook",
48
49
  autoTriggerTimeoutMs: 120000,
@@ -548,12 +549,15 @@ function assertSuccessful(message) {
548
549
  }
549
550
  return message;
550
551
  }
551
- async function withProviderDeadline(opts, invoke, modelId) {
552
+ function resolveProviderWatchdogMs(provider, maxTokens, configuredMs = 0) {
553
+ const multiplier = provider && configuredMs <= 0 ? getProviderCaps(provider).timeoutMultiplier : 1;
554
+ return Math.round(resolveCodexWatchdogMs(maxTokens, configuredMs) * multiplier);
555
+ }
556
+ async function withProviderDeadline(opts, invoke, provider) {
552
557
  if (opts.signal?.aborted)
553
558
  throw new Error("LLM request aborted before dispatch");
554
559
  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);
560
+ const watchdogMs = resolveProviderWatchdogMs(provider, opts.maxTokens, opts.codexWatchdogMs);
557
561
  const abort = Promise.withResolvers();
558
562
  const abortFromCaller = () => {
559
563
  controller.abort(opts.signal?.reason);
@@ -626,7 +630,7 @@ var rawLlmClient = {
626
630
  return completeChatGptCodex(model, body, bounded);
627
631
  const response = bounded.reasoning === undefined ? await (await resolveComplete())(model, body, bounded) : await (await resolveCompleteSimple())(model, body, bounded);
628
632
  return assertSuccessful(response);
629
- }, model.id);
633
+ }, model.provider);
630
634
  }
631
635
  };
632
636
  var defaultLlmClient = rawLlmClient;
@@ -718,35 +722,6 @@ function isKnownPathReference(ref, knownPaths) {
718
722
  });
719
723
  }
720
724
 
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
-
750
725
  // src/domain/tool-semantics.ts
751
726
  var PATH_KEYS = [
752
727
  "path",
@@ -815,6 +790,35 @@ function classifyToolOperation(args, toolName) {
815
790
  return "unknown";
816
791
  }
817
792
 
793
+ // src/utils/logger.ts
794
+ var DEBUG = process.env.DEBUG?.includes("smart-compact") ?? false;
795
+ function warn(msg, err) {
796
+ const detail = err instanceof Error ? err.message : err ?? "";
797
+ console.error(LOG_PREFIX + " " + msg + (detail ? ": " + detail : ""));
798
+ }
799
+
800
+ // src/infra/paths.ts
801
+ import path from "path";
802
+ import os from "os";
803
+ function home() {
804
+ const configured = process.env.HOME?.trim() || process.env.USERPROFILE?.trim();
805
+ return configured || os.homedir();
806
+ }
807
+ function piAgentDir() {
808
+ return path.join(home(), ".pi", "agent");
809
+ }
810
+ function cacheDir() {
811
+ return path.join(piAgentDir(), ".cache");
812
+ }
813
+ function smartCompactCacheDir() {
814
+ return path.join(cacheDir(), "smart-compact");
815
+ }
816
+ function metricsLogFile() {
817
+ return path.join(cacheDir(), "compact-metrics.jsonl");
818
+ }
819
+ function damageReportsFile() {
820
+ return path.join(smartCompactCacheDir(), "damage-reports.jsonl");
821
+ }
818
822
  // src/utils/file-ref-detect.ts
819
823
  var CODE_EXT_RE = /\.(ts|tsx|js|jsx|mjs|cjs|rs|py|go|java|rb|cs|cpp|c|h|hpp|swift|kt|scala|php|css|scss|html|json|yaml|yml|toml|md|mdx|sh|sql|tf|ini|env|lock|gradle|xml)$/i;
820
824
  var VERSION_RE = /^v?\d+(?:\.\d+)+(?:[-+][\w.-]+)?$/i;
@@ -2012,256 +2016,276 @@ function hasSemanticContradiction(source, target) {
2012
2016
  return conditional && !negative && !tokens.some((token) => CONDITION_MARKERS.has(token));
2013
2017
  });
2014
2018
  }
2015
- function verifySummary(summary, extraction, continuity = null, evidence = {}) {
2016
- const parsed = parseSummary(summary);
2017
- const gaps = [];
2018
- const lower = summary.toLowerCase().replace(/\\/g, "/");
2019
- const normalizedSummary = lower.replace(/\s+/g, " ");
2020
- let score = 100;
2021
- const uniqueByText = (items, text) => {
2022
- const seen = new Set;
2023
- return items.filter((item) => {
2024
- const key = text(item).toLowerCase().replace(/\s+/g, " ").trim();
2025
- if (!key || seen.has(key))
2026
- return false;
2027
- seen.add(key);
2028
- return true;
2029
- });
2030
- };
2031
- const unresolvedEvidence = uniqueByText([
2032
- ...extraction.errors.filter((error) => !error.resolved).map((error) => ({ message: error.message })),
2019
+ function addGap(accumulator, gap, penalty) {
2020
+ accumulator.gaps.push(gap);
2021
+ accumulator.score -= penalty;
2022
+ }
2023
+ function uniqueByText(items, text) {
2024
+ const seen = new Set;
2025
+ return items.filter((item) => {
2026
+ const key = text(item).toLowerCase().replace(/\s+/g, " ").trim();
2027
+ if (!key || seen.has(key))
2028
+ return false;
2029
+ seen.add(key);
2030
+ return true;
2031
+ });
2032
+ }
2033
+ function collectVerificationEvidence(extraction, continuity, evidence) {
2034
+ const unresolved = uniqueByText([
2035
+ ...extraction.errors.flatMap((error) => !error.resolved ? [{ message: error.message }] : []),
2033
2036
  ...(continuity?.unresolvedErrors ?? []).map((error) => ({
2034
2037
  message: error.message
2035
2038
  }))
2036
2039
  ], (item) => item.message);
2037
- const resolvedEvidence = uniqueByText([
2038
- ...extraction.errors.filter((error) => error.resolved).map((error) => ({ message: error.message })),
2040
+ const resolved = uniqueByText([
2041
+ ...extraction.errors.flatMap((error) => error.resolved ? [{ message: error.message }] : []),
2039
2042
  ...(continuity?.resolvedErrors ?? []).map((error) => ({
2040
2043
  message: error.message
2041
2044
  }))
2042
2045
  ], (item) => item.message).slice(-5);
2043
- const steeringConstraints = [
2044
- evidence.steering?.focus ? { text: "Preserve detail about: " + evidence.steering.focus } : null,
2045
- evidence.steering?.note ? { text: evidence.steering.note } : null
2046
- ].filter((item) => Boolean(item?.text.trim()));
2047
- const constraintEvidence = uniqueByText([
2048
- ...extraction.constraints.filter((item) => item.confidence >= 0.8).map((item) => ({ text: item.text })),
2049
- ...(continuity?.constraints ?? []).filter((item) => item.confidence >= 0.8).map((item) => ({ text: item.text })),
2046
+ const steeringConstraints = [];
2047
+ if (evidence.steering?.focus?.trim()) {
2048
+ steeringConstraints.push({
2049
+ text: "Preserve detail about: " + evidence.steering.focus
2050
+ });
2051
+ }
2052
+ if (evidence.steering?.note?.trim()) {
2053
+ steeringConstraints.push({ text: evidence.steering.note });
2054
+ }
2055
+ const constraints = uniqueByText([
2056
+ ...extraction.constraints.flatMap((item) => item.confidence >= 0.8 ? [{ text: item.text }] : []),
2057
+ ...(continuity?.constraints ?? []).flatMap((item) => item.confidence >= 0.8 ? [{ text: item.text }] : []),
2050
2058
  ...steeringConstraints
2051
2059
  ], (item) => item.text).filter((item) => !isDiagnosticConstraintText(item.text));
2052
- const decisionEvidence = uniqueByText([
2053
- ...extraction.decisions.filter((item) => item.type === "explicit").map((item) => ({ summary: item.summary })),
2054
- ...(continuity?.decisions ?? []).filter((item) => item.type === "explicit").map((item) => ({ summary: item.summary }))
2060
+ const decisions = uniqueByText([
2061
+ ...extraction.decisions.flatMap((item) => item.type === "explicit" ? [{ summary: item.summary }] : []),
2062
+ ...(continuity?.decisions ?? []).flatMap((item) => item.type === "explicit" ? [{ summary: item.summary }] : [])
2055
2063
  ], (item) => item.summary);
2056
- const goalEvidence = extraction.mainGoal ?? continuity?.goal ?? null;
2057
- const requiredSections = [
2064
+ return {
2065
+ unresolved,
2066
+ resolved,
2067
+ constraints,
2068
+ decisions,
2069
+ goal: extraction.mainGoal ?? continuity?.goal ?? null
2070
+ };
2071
+ }
2072
+ function verifyRequiredSections(parsed, accumulator) {
2073
+ const required = [
2058
2074
  { kind: "goal", penalty: 5 },
2059
2075
  { kind: "progress", penalty: 5 },
2060
2076
  { kind: "critical-context", penalty: 3 }
2061
2077
  ];
2062
- for (const req of requiredSections) {
2063
- if (!findSection(parsed, req.kind)) {
2064
- gaps.push({ kind: "missing-section", section: req.kind });
2065
- score -= req.penalty;
2078
+ for (const item of required) {
2079
+ if (!findSection(parsed, item.kind)) {
2080
+ addGap(accumulator, { kind: "missing-section", section: item.kind }, item.penalty);
2066
2081
  }
2067
2082
  }
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);
2083
+ }
2084
+ function verifyPathCoverage(parsed, extraction, continuity, evidence, accumulator) {
2085
+ const modified = extraction.modifiedFiles.map((file) => file.path);
2086
+ const read = extraction.readFiles;
2087
+ const deleted = Array.from(new Set([...extraction.deletedFiles, ...continuity?.deletedFiles ?? []]));
2088
+ const required = [...modified, ...read, ...deleted];
2089
+ const expected = new Set(required);
2090
+ const rendered = buildSummaryPathEvidence(required, evidence.summaryBudgetTokens);
2091
+ const ownerSets = new Map;
2092
+ for (const file of required) {
2093
+ const display = rendered.get(file);
2077
2094
  for (const candidate of [
2078
2095
  file,
2079
2096
  ...display ? [decodePathDisplay(display)] : []
2080
2097
  ]) {
2081
2098
  const normalized = normalizePath(candidate);
2082
- const owners = normalizedOwnerSets.get(normalized) ?? new Set;
2099
+ const owners = ownerSets.get(normalized) ?? new Set;
2083
2100
  owners.add(file);
2084
- normalizedOwnerSets.set(normalized, owners);
2101
+ ownerSets.set(normalized, owners);
2085
2102
  }
2086
2103
  }
2087
- const normalizedOwners = new Map(Array.from(normalizedOwnerSets, ([path2, owners]) => [path2, owners.size]));
2088
- const listedPaths = (kind) => collectListedPaths(findSection(parsed, kind)?.body ?? "", expectedPathSet);
2089
- const modifiedListed = listedPaths("files-modified");
2090
- const readListed = listedPaths("files-read");
2091
- const deletedListed = listedPaths("files-deleted");
2092
- for (const file of modifiedPaths) {
2093
- const display = pathEvidence.get(file);
2094
- if (display && !hasListedPath(modifiedListed, file, display, normalizedOwners)) {
2095
- gaps.push({ kind: "missing-file", path: file });
2104
+ const ownerCounts = new Map(Array.from(ownerSets, ([file, owners]) => [file, owners.size]));
2105
+ const listed = (kind) => collectListedPaths(findSection(parsed, kind)?.body ?? "", expected);
2106
+ const modifiedListed = listed("files-modified");
2107
+ const readListed = listed("files-read");
2108
+ const deletedListed = listed("files-deleted");
2109
+ for (const file of modified) {
2110
+ const display = rendered.get(file);
2111
+ if (display && !hasListedPath(modifiedListed, file, display, ownerCounts)) {
2112
+ addGap(accumulator, { kind: "missing-file", path: file }, 5);
2096
2113
  }
2097
2114
  }
2098
- for (const file of readPaths) {
2099
- const display = pathEvidence.get(file);
2100
- if (display && !hasListedPath(readListed, file, display, normalizedOwners)) {
2101
- gaps.push({ kind: "missing-read-file", path: file });
2115
+ for (const file of read) {
2116
+ const display = rendered.get(file);
2117
+ if (display && !hasListedPath(readListed, file, display, ownerCounts)) {
2118
+ addGap(accumulator, { kind: "missing-read-file", path: file }, 5);
2102
2119
  }
2103
2120
  }
2104
- for (const file of deletedEvidence) {
2105
- const display = pathEvidence.get(file);
2106
- if (display && !hasListedPath(deletedListed, file, display, normalizedOwners)) {
2107
- gaps.push({ kind: "missing-deleted-file", path: file });
2121
+ for (const file of deleted) {
2122
+ const display = rendered.get(file);
2123
+ if (display && !hasListedPath(deletedListed, file, display, ownerCounts)) {
2124
+ addGap(accumulator, { kind: "missing-deleted-file", path: file }, 5);
2108
2125
  }
2109
2126
  }
2110
- score -= gaps.filter((gap) => gap.kind === "missing-file" || gap.kind === "missing-read-file" || gap.kind === "missing-deleted-file").length * 5;
2111
- for (const error of unresolvedEvidence) {
2127
+ return { modified, read, deleted, rendered };
2128
+ }
2129
+ function verifyErrorEvidence(normalizedSummary, collected, accumulator) {
2130
+ for (const error of collected.unresolved) {
2112
2131
  const snippet = summaryEvidenceLine(error.message, TRUNC.ERROR_SNIPPET).toLowerCase().replace(/\\/g, "/");
2113
2132
  if (snippet.length > 5 && !normalizedSummary.includes(snippet)) {
2114
- gaps.push({ kind: "missing-error", message: error.message });
2115
- score -= 5;
2133
+ addGap(accumulator, { kind: "missing-error", message: error.message }, 5);
2116
2134
  }
2117
2135
  }
2118
- for (const error of resolvedEvidence) {
2136
+ for (const error of collected.resolved) {
2119
2137
  const snippet = summaryEvidenceLine(error.message, TRUNC.ERROR_SNIPPET).toLowerCase().replace(/\\/g, "/");
2120
2138
  if (snippet.length > 5 && !normalizedSummary.includes(snippet)) {
2121
- gaps.push({
2122
- kind: "missing-error",
2123
- message: error.message,
2124
- resolved: true
2125
- });
2126
- score -= 2;
2139
+ addGap(accumulator, { kind: "missing-error", message: error.message, resolved: true }, 2);
2127
2140
  }
2128
2141
  }
2142
+ }
2143
+ function verifySemanticCoverage(parsed, collected, accumulator) {
2129
2144
  const constraintTarget = [
2130
2145
  findSection(parsed, "constraints")?.body ?? "",
2131
2146
  findSection(parsed, "critical-context")?.body ?? ""
2132
2147
  ].join(`
2133
2148
  `);
2134
- for (const constraint of constraintEvidence) {
2149
+ for (const constraint of collected.constraints) {
2135
2150
  if (!hasSemanticEvidence(constraint.text, constraintTarget)) {
2136
- gaps.push({ kind: "missing-constraint", text: constraint.text });
2137
- score -= 8;
2151
+ addGap(accumulator, { kind: "missing-constraint", text: constraint.text }, 8);
2138
2152
  }
2139
2153
  if (hasSemanticContradiction(constraint.text, constraintTarget)) {
2140
- gaps.push({
2154
+ addGap(accumulator, {
2141
2155
  kind: "inconsistency",
2142
2156
  detail: "semantic-contradiction: constraint contradicts " + constraint.text.slice(0, TRUNC.SNIPPET)
2143
- });
2144
- score -= 20;
2157
+ }, 20);
2145
2158
  }
2146
2159
  }
2147
- if (goalEvidence) {
2160
+ if (collected.goal) {
2148
2161
  const goalTarget = findSection(parsed, "goal")?.body ?? "";
2149
- if (!hasSemanticEvidence(goalEvidence, goalTarget)) {
2150
- gaps.push({ kind: "missing-goal", goal: goalEvidence });
2151
- score -= 12;
2162
+ if (!hasSemanticEvidence(collected.goal, goalTarget)) {
2163
+ addGap(accumulator, { kind: "missing-goal", goal: collected.goal }, 12);
2152
2164
  }
2153
- if (hasSemanticContradiction(goalEvidence, goalTarget)) {
2154
- gaps.push({
2165
+ if (hasSemanticContradiction(collected.goal, goalTarget)) {
2166
+ addGap(accumulator, {
2155
2167
  kind: "inconsistency",
2156
2168
  detail: "semantic-contradiction: goal polarity or condition changed"
2157
- });
2158
- score -= 20;
2169
+ }, 20);
2159
2170
  }
2160
2171
  }
2172
+ const decisionBody = findSection(parsed, "decisions")?.body ?? "";
2173
+ for (const decision of collected.decisions) {
2174
+ if (!hasSemanticEvidence(decision.summary, decisionBody)) {
2175
+ addGap(accumulator, { kind: "missing-decision", summary: decision.summary }, 8);
2176
+ }
2177
+ if (hasSemanticContradiction(decision.summary, decisionBody)) {
2178
+ addGap(accumulator, {
2179
+ kind: "inconsistency",
2180
+ detail: "semantic-contradiction: decision contradicts " + decision.summary.slice(0, TRUNC.SNIPPET)
2181
+ }, 20);
2182
+ }
2183
+ }
2184
+ }
2185
+ function verifyFileReferences(summary, extraction, continuity, evidence, collected, paths, accumulator) {
2161
2186
  const groundedEvidence = [
2162
- ...unresolvedEvidence.map((item) => item.message),
2163
- ...resolvedEvidence.map((item) => item.message),
2164
- ...constraintEvidence.map((item) => item.text),
2165
- ...decisionEvidence.map((item) => item.summary),
2166
- ...goalEvidence ? [goalEvidence] : [],
2187
+ ...collected.unresolved.map((item) => item.message),
2188
+ ...collected.resolved.map((item) => item.message),
2189
+ ...collected.constraints.map((item) => item.text),
2190
+ ...collected.decisions.map((item) => item.summary),
2191
+ ...collected.goal ? [collected.goal] : [],
2167
2192
  ...extraction.lastUserMessages,
2168
2193
  ...extraction.timeline.map((item) => item.summary),
2169
2194
  ...extraction.topics.map((item) => item.primaryFile ?? ""),
2170
2195
  ...continuity?.openLoops.map((item) => item.summary) ?? [],
2171
2196
  ...continuity?.criticalContext ?? []
2172
2197
  ];
2173
- const groundedEvidenceFiles = groundedEvidence.flatMap((value) => [
2198
+ const groundedFiles = groundedEvidence.flatMap((value) => [
2174
2199
  value,
2175
2200
  summaryEvidenceLine(value, TRUNC.ERROR_SNIPPET),
2176
2201
  summaryEvidenceLine(value, TRUNC.TOPIC_LABEL),
2177
2202
  summaryEvidenceLine(value, TRUNC.PREVIEW),
2178
2203
  summaryEvidenceLine(value, TRUNC.MESSAGE)
2179
2204
  ]).flatMap(extractFileRefs);
2180
- const renderedPathEvidence = Array.from(pathEvidence.values()).flatMap((line) => [
2205
+ const renderedPaths = Array.from(paths.rendered.values()).flatMap((line) => [
2181
2206
  decodePathDisplay(line),
2182
2207
  line.startsWith('"') && line.endsWith('"') ? line.slice(1, -1) : line
2183
2208
  ]);
2184
- const rawKnownFiles = Array.from(new Set([
2185
- ...modifiedPaths,
2186
- ...readPaths,
2187
- ...deletedEvidence,
2209
+ const knownFiles = Array.from(new Set([
2210
+ ...paths.modified,
2211
+ ...paths.read,
2212
+ ...paths.deleted,
2188
2213
  ...extraction.referencedFiles ?? [],
2189
- ...groundedEvidenceFiles,
2190
- ...renderedPathEvidence,
2191
- ...renderedPathEvidence.flatMap(extractFileRefs),
2214
+ ...groundedFiles,
2215
+ ...renderedPaths,
2216
+ ...renderedPaths.flatMap(extractFileRefs),
2192
2217
  ...continuity?.modifiedFiles ?? [],
2193
2218
  ...continuity?.readFiles ?? [],
2194
2219
  ...(continuity?.unresolvedErrors ?? []).flatMap((error) => error.files),
2195
2220
  ...(continuity?.openLoops ?? []).flatMap((loop) => loop.files)
2196
2221
  ]));
2197
2222
  for (const ref of new Set(extractFileRefs(summary))) {
2198
- const grounded = isKnownPathReference(ref, rawKnownFiles) || Boolean(evidence.sourceMessages && sourceSupportsFileReference(ref, evidence.sourceMessages));
2199
- if (!grounded) {
2200
- gaps.push({ kind: "fabricated-file", ref });
2201
- score -= 4;
2202
- }
2223
+ const grounded = isKnownPathReference(ref, knownFiles) || Boolean(evidence.sourceMessages && sourceSupportsFileReference(ref, evidence.sourceMessages));
2224
+ if (!grounded)
2225
+ addGap(accumulator, { kind: "fabricated-file", ref }, 4);
2203
2226
  }
2204
- const progressSection = findSection(parsed, "progress");
2205
- if (progressSection) {
2206
- const doneSection = progressSection.body.match(/###\s*Done[\s\S]*?(?=###|$)/i)?.[0] ?? "";
2207
- const blockedSection = progressSection.body.match(/###\s*Blocked[\s\S]*?(?=###|$)/i)?.[0] ?? "";
2208
- const blockedLines = blockedSection.split(/\r?\n/).slice(1);
2209
- if (unresolvedEvidence.length > 0 && noneBlockerLineIndexes(blockedLines).size > 0) {
2210
- gaps.push({
2227
+ }
2228
+ function verifyProgressConsistency(parsed, extraction, collected, paths, accumulator) {
2229
+ const progress = findSection(parsed, "progress");
2230
+ if (!progress)
2231
+ return;
2232
+ const done = progress.body.match(/###\s*Done[\s\S]*?(?=###|$)/i)?.[0] ?? "";
2233
+ const blocked = progress.body.match(/###\s*Blocked[\s\S]*?(?=###|$)/i)?.[0] ?? "";
2234
+ if (collected.unresolved.length > 0 && noneBlockerLineIndexes(blocked.split(/\r?\n/).slice(1)).size > 0) {
2235
+ addGap(accumulator, {
2236
+ kind: "inconsistency",
2237
+ detail: "blocked-none: Blocked says none despite unresolved errors"
2238
+ }, 12);
2239
+ }
2240
+ const doneRefs = new Set(extractFileRefs(done).map(normalizePath));
2241
+ for (const file of extraction.modifiedFiles) {
2242
+ const needles = buildUniquePathNeedles(file.path, paths.modified);
2243
+ if (!needles.some((needle) => doneRefs.has(normalizePath(needle))))
2244
+ continue;
2245
+ const unresolved = collected.unresolved.find((error) => {
2246
+ const firstLine = error.message.split(/\r?\n/, 1)[0] ?? "";
2247
+ const refs = extractFileRefs(firstLine).map(normalizePath);
2248
+ return needles.some((needle) => refs.includes(normalizePath(needle)));
2249
+ });
2250
+ if (unresolved) {
2251
+ addGap(accumulator, {
2211
2252
  kind: "inconsistency",
2212
- detail: "blocked-none: Blocked says none despite unresolved errors"
2213
- });
2214
- score -= 12;
2215
- }
2216
- const doneRefs = new Set(extractFileRefs(doneSection).map(normalizePath));
2217
- for (const file of extraction.modifiedFiles) {
2218
- const uniqueNeedles = buildUniquePathNeedles(file.path, modifiedPaths);
2219
- if (!uniqueNeedles.some((needle) => doneRefs.has(normalizePath(needle))))
2220
- continue;
2221
- const unresolved = unresolvedEvidence.find((error) => {
2222
- const firstLine = error.message.split(/\r?\n/, 1)[0] ?? "";
2223
- const errorRefs = extractFileRefs(firstLine).map(normalizePath);
2224
- return uniqueNeedles.some((needle) => errorRefs.includes(normalizePath(needle)));
2225
- });
2226
- if (unresolved) {
2227
- gaps.push({
2228
- kind: "inconsistency",
2229
- detail: file.path + " marked Done but has unresolved error"
2230
- });
2231
- score -= 5;
2232
- }
2253
+ detail: file.path + " marked Done but has unresolved error"
2254
+ }, 5);
2233
2255
  }
2234
2256
  }
2235
- const decisionBody = findSection(parsed, "decisions")?.body ?? "";
2236
- for (const decision of decisionEvidence) {
2237
- if (!hasSemanticEvidence(decision.summary, decisionBody)) {
2238
- gaps.push({ kind: "missing-decision", summary: decision.summary });
2239
- score -= 8;
2240
- }
2241
- if (hasSemanticContradiction(decision.summary, decisionBody)) {
2242
- gaps.push({
2243
- kind: "inconsistency",
2244
- detail: "semantic-contradiction: decision contradicts " + decision.summary.slice(0, TRUNC.SNIPPET)
2245
- });
2246
- score -= 20;
2247
- }
2257
+ }
2258
+ function verifyOpenLoopsAndClaims(summary, parsed, extraction, continuity, evidence, collected, accumulator) {
2259
+ const unresolvedCount = collected.unresolved.length + (continuity?.openLoops.filter((loop) => loop.status !== "resolved").length ?? 0);
2260
+ if (unresolvedCount >= 1 && !findSection(parsed, "open-loops") && !summary.toLowerCase().replace(/\\/g, "/").includes("unresolved")) {
2261
+ addGap(accumulator, { kind: "missing-open-loops", unresolvedCount }, 5);
2248
2262
  }
2249
- const unresolvedCount = unresolvedEvidence.length + (continuity?.openLoops.filter((loop) => loop.status !== "resolved").length ?? 0);
2250
- if (unresolvedCount >= 1 && !findSection(parsed, "open-loops") && !lower.includes("unresolved")) {
2251
- gaps.push({ kind: "missing-open-loops", unresolvedCount });
2252
- score -= 5;
2253
- }
2254
- if (evidence.sourceMessages) {
2255
- const tools = successfulToolEvidence(evidence.sourceMessages);
2256
- for (const claim of outcomeClaims(summary)) {
2257
- if (!successfulToolSupportsClaim(claim, tools, extraction)) {
2258
- gaps.push({ kind: "unsupported-claim", claim });
2259
- score -= 20;
2260
- }
2263
+ if (!evidence.sourceMessages)
2264
+ return;
2265
+ const tools = successfulToolEvidence(evidence.sourceMessages);
2266
+ for (const claim of outcomeClaims(summary)) {
2267
+ if (!successfulToolSupportsClaim(claim, tools, extraction)) {
2268
+ addGap(accumulator, { kind: "unsupported-claim", claim }, 20);
2261
2269
  }
2262
2270
  }
2263
- const finalScore = Math.max(0, score);
2264
- return { ok: gaps.length === 0 && finalScore >= 85, gaps, score: finalScore };
2271
+ }
2272
+ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
2273
+ const parsed = parseSummary(summary);
2274
+ const accumulator = { gaps: [], score: 100 };
2275
+ const collected = collectVerificationEvidence(extraction, continuity, evidence);
2276
+ verifyRequiredSections(parsed, accumulator);
2277
+ const paths = verifyPathCoverage(parsed, extraction, continuity, evidence, accumulator);
2278
+ verifyErrorEvidence(summary.toLowerCase().replace(/\\/g, "/").replace(/\s+/g, " "), collected, accumulator);
2279
+ verifySemanticCoverage(parsed, collected, accumulator);
2280
+ verifyFileReferences(summary, extraction, continuity, evidence, collected, paths, accumulator);
2281
+ verifyProgressConsistency(parsed, extraction, collected, paths, accumulator);
2282
+ verifyOpenLoopsAndClaims(summary, parsed, extraction, continuity, evidence, collected, accumulator);
2283
+ const score = Math.max(0, accumulator.score);
2284
+ return {
2285
+ ok: accumulator.gaps.length === 0 && score >= 85,
2286
+ gaps: accumulator.gaps,
2287
+ score
2288
+ };
2265
2289
  }
2266
2290
 
2267
2291
  // scripts/provider-scenario-eval.ts
@@ -2282,12 +2306,35 @@ Latest request: fix the test, run the release gate, and do not publish.`,
2282
2306
  ],
2283
2307
  readFiles: ["package.json"],
2284
2308
  deletedFiles: [],
2285
- errors: [{ index: 10, tool: "test", message: "expiry boundary test still fails by one second", retryAttempted: true, resolved: false }],
2286
- decisions: [{ index: 4, type: "explicit", summary: "Reuse node:crypto and preserve the AuthService API" }],
2287
- constraints: [{ index: 1, text: "Do not add dependencies", category: "prohibition", confidence: 1 }],
2309
+ errors: [
2310
+ {
2311
+ index: 10,
2312
+ tool: "test",
2313
+ message: "expiry boundary test still fails by one second",
2314
+ retryAttempted: true,
2315
+ resolved: false
2316
+ }
2317
+ ],
2318
+ decisions: [
2319
+ {
2320
+ index: 4,
2321
+ type: "explicit",
2322
+ summary: "Reuse node:crypto and preserve the AuthService API"
2323
+ }
2324
+ ],
2325
+ constraints: [
2326
+ {
2327
+ index: 1,
2328
+ text: "Do not add dependencies",
2329
+ category: "prohibition",
2330
+ confidence: 1
2331
+ }
2332
+ ],
2288
2333
  topics: [],
2289
2334
  timeline: [],
2290
- lastUserMessages: ["Fix the test, run the release gate, and do not publish"],
2335
+ lastUserMessages: [
2336
+ "Fix the test, run the release gate, and do not publish"
2337
+ ],
2291
2338
  lastErrors: []
2292
2339
  }
2293
2340
  },
@@ -2301,14 +2348,35 @@ The network timeout was resolved. Open loop: add a regression check for two simu
2301
2348
  extraction: {
2302
2349
  mainGoal: "Stop duplicate background jobs",
2303
2350
  messageCount: 18,
2304
- modifiedFiles: [{ path: "src/queue.ts", toolCalls: 2, lastModifiedIndex: 13 }],
2351
+ modifiedFiles: [
2352
+ { path: "src/queue.ts", toolCalls: 2, lastModifiedIndex: 13 }
2353
+ ],
2305
2354
  readFiles: ["src/queue.ts", "src/worker.ts"],
2306
2355
  deletedFiles: [],
2307
2356
  errors: [],
2308
- decisions: [{ index: 9, type: "explicit", summary: "Commit the idempotency key before retry scheduling" }],
2309
- constraints: [{ index: 10, text: "Keep concurrency at two", category: "requirement", confidence: 1 }],
2357
+ decisions: [
2358
+ {
2359
+ index: 9,
2360
+ type: "explicit",
2361
+ summary: "Commit the idempotency key before retry scheduling"
2362
+ }
2363
+ ],
2364
+ constraints: [
2365
+ {
2366
+ index: 10,
2367
+ text: "Keep concurrency at two",
2368
+ category: "requirement",
2369
+ confidence: 1
2370
+ }
2371
+ ],
2310
2372
  topics: [],
2311
- timeline: [{ index: 16, event: "open-loop", summary: "Add a simultaneous-session regression check" }],
2373
+ timeline: [
2374
+ {
2375
+ index: 16,
2376
+ event: "open-loop",
2377
+ summary: "Add a simultaneous-session regression check"
2378
+ }
2379
+ ],
2312
2380
  lastUserMessages: ["Add the regression check next"],
2313
2381
  lastErrors: []
2314
2382
  }
@@ -2323,18 +2391,48 @@ Critical paths: src/app/run-smart-compact.ts, src/infra/llm-client.ts, scripts/e
2323
2391
  extraction: {
2324
2392
  mainGoal: "Prepare v8 without publishing",
2325
2393
  messageCount: 30,
2326
- modifiedFiles: [{ path: "src/app/run-smart-compact.ts", toolCalls: 2, lastModifiedIndex: 20 }],
2394
+ modifiedFiles: [
2395
+ {
2396
+ path: "src/app/run-smart-compact.ts",
2397
+ toolCalls: 2,
2398
+ lastModifiedIndex: 20
2399
+ }
2400
+ ],
2327
2401
  readFiles: ["src/infra/llm-client.ts", "scripts/eval-gate.ts"],
2328
2402
  deletedFiles: [],
2329
2403
  errors: [],
2330
- decisions: [{ index: 5, type: "explicit", summary: "Do not change the selected model automatically" }],
2404
+ decisions: [
2405
+ {
2406
+ index: 5,
2407
+ type: "explicit",
2408
+ summary: "Do not change the selected model automatically"
2409
+ }
2410
+ ],
2331
2411
  constraints: [
2332
- { index: 2, text: "Do not publish", category: "prohibition", confidence: 1 },
2333
- { index: 6, text: "Keep Pi peer dependencies as wildcards", category: "requirement", confidence: 1 }
2412
+ {
2413
+ index: 2,
2414
+ text: "Do not publish",
2415
+ category: "prohibition",
2416
+ confidence: 1
2417
+ },
2418
+ {
2419
+ index: 6,
2420
+ text: "Keep Pi peer dependencies as wildcards",
2421
+ category: "requirement",
2422
+ confidence: 1
2423
+ }
2334
2424
  ],
2335
2425
  topics: [],
2336
- timeline: [{ index: 28, event: "open-loop", summary: "Implement telemetry canary and dashboard confidence" }],
2337
- lastUserMessages: ["Continue provider evaluation, then telemetry and dashboard confidence"],
2426
+ timeline: [
2427
+ {
2428
+ index: 28,
2429
+ event: "open-loop",
2430
+ summary: "Implement telemetry canary and dashboard confidence"
2431
+ }
2432
+ ],
2433
+ lastUserMessages: [
2434
+ "Continue provider evaluation, then telemetry and dashboard confidence"
2435
+ ],
2338
2436
  lastErrors: []
2339
2437
  }
2340
2438
  }
@@ -2380,7 +2478,15 @@ for (const model of models) {
2380
2478
  const auth = await registry.getApiKeyAndHeaders(model);
2381
2479
  if (!auth.ok || !auth.apiKey) {
2382
2480
  for (const scenario of scenarios)
2383
- results.push({ model: label, scenario: scenario.name, score: 0, latencyMs: 0, input: 0, output: 0, error: "auth unavailable" });
2481
+ results.push({
2482
+ model: label,
2483
+ scenario: scenario.name,
2484
+ score: 0,
2485
+ latencyMs: 0,
2486
+ input: 0,
2487
+ output: 0,
2488
+ error: "auth unavailable"
2489
+ });
2384
2490
  continue;
2385
2491
  }
2386
2492
  for (const scenario of scenarios) {
@@ -2391,7 +2497,13 @@ for (const model of models) {
2391
2497
  try {
2392
2498
  const response = await rawLlmClient.complete(model, {
2393
2499
  systemPrompt,
2394
- messages: [{ role: "user", content: [{ type: "text", text: scenario.transcript }], timestamp: Date.now() }]
2500
+ messages: [
2501
+ {
2502
+ role: "user",
2503
+ content: [{ type: "text", text: scenario.transcript }],
2504
+ timestamp: Date.now()
2505
+ }
2506
+ ]
2395
2507
  }, {
2396
2508
  apiKey: auth.apiKey,
2397
2509
  headers: auth.headers,
@@ -2400,7 +2512,7 @@ for (const model of models) {
2400
2512
  codexWatchdogMs: 60000,
2401
2513
  signal: controller.signal
2402
2514
  });
2403
- const summary = response.content.filter((item) => item.type === "text").map((item) => item.text).join(`
2515
+ const summary = response.content.flatMap((item) => item.type === "text" ? [item.text] : []).join(`
2404
2516
  `).trim();
2405
2517
  results.push({
2406
2518
  model: label,
@@ -2425,12 +2537,15 @@ for (const model of models) {
2425
2537
  }
2426
2538
  }
2427
2539
  }
2540
+ function tableCell(value) {
2541
+ return String(value).replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
2542
+ }
2428
2543
  console.log(`# Live Provider Scenario Matrix
2429
2544
  `);
2430
2545
  console.log("| Model | Scenario | Verify | Latency | Input | Output | Status |");
2431
2546
  console.log("|---|---|---:|---:|---:|---:|---|");
2432
2547
  for (const result of results) {
2433
- console.log("| " + result.model + " | " + result.scenario + " | " + result.score + " | " + result.latencyMs + "ms | " + result.input + " | " + result.output + " | " + (result.error ? "error: " + result.error.replace(/\|/g, "\\|") : "ok") + " |");
2548
+ console.log("| " + tableCell(result.model) + " | " + tableCell(result.scenario) + " | " + result.score + " | " + result.latencyMs + "ms | " + result.input + " | " + result.output + " | " + (result.error ? "error: " + tableCell(result.error) : "ok") + " |");
2434
2549
  }
2435
2550
  console.log(`
2436
2551
  Advisory only: stage routes stay on the selected model unless explicitly configured.`);