@ryuenn3123/agentic-senior-core 4.1.0 → 4.2.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.
Files changed (50) hide show
  1. package/.agent-context/prompts/compact-natural-mode.md +100 -0
  2. package/.agent-context/prompts/init-project.md +1 -0
  3. package/.agent-context/prompts/refactor.md +1 -0
  4. package/.agent-context/review-checklists/pr-checklist.md +1 -0
  5. package/.agent-context/rules/architecture.md +10 -0
  6. package/.agent-context/rules/naming-conv.md +6 -3
  7. package/.agent-context/state/README.md +2 -1
  8. package/AGENTS.md +6 -8
  9. package/README.md +95 -117
  10. package/benchmarks/README.md +60 -0
  11. package/benchmarks/compact-natural-mode/fixtures.mjs +359 -0
  12. package/benchmarks/compact-natural-mode/scorer.mjs +331 -0
  13. package/benchmarks/runtime-token-saver/fixtures.mjs +714 -0
  14. package/bin/agentic-senior-core.js +6 -0
  15. package/bin/ascx.js +23 -0
  16. package/lib/cli/adaptive-context/catalog.mjs +428 -0
  17. package/lib/cli/adaptive-context/file-signals.mjs +100 -0
  18. package/lib/cli/adaptive-context/implications.mjs +44 -0
  19. package/lib/cli/adaptive-context.mjs +365 -0
  20. package/lib/cli/ascx/adapters/git-diff.mjs +223 -0
  21. package/lib/cli/ascx/adapters/git-status.mjs +145 -0
  22. package/lib/cli/ascx/adapters/npm-run-build.mjs +99 -0
  23. package/lib/cli/ascx/adapters/npm-test.mjs +120 -0
  24. package/lib/cli/ascx/adapters/rg.mjs +39 -0
  25. package/lib/cli/ascx/fixture-evaluator.mjs +180 -0
  26. package/lib/cli/ascx/formatter.mjs +47 -0
  27. package/lib/cli/ascx/lexer.mjs +129 -0
  28. package/lib/cli/ascx/runtime.mjs +192 -0
  29. package/lib/cli/ascx/tee-writer.mjs +63 -0
  30. package/lib/cli/ascx/token-estimate.mjs +15 -0
  31. package/lib/cli/backup.mjs +37 -4
  32. package/lib/cli/commands/context.mjs +140 -0
  33. package/lib/cli/commands/init.mjs +14 -2
  34. package/lib/cli/commands/optimize.mjs +143 -2
  35. package/lib/cli/commands/upgrade/design-intent-seed.mjs +46 -0
  36. package/lib/cli/commands/upgrade/token-optimization-state.mjs +51 -0
  37. package/lib/cli/commands/upgrade.mjs +34 -45
  38. package/lib/cli/compiler.mjs +9 -0
  39. package/lib/cli/project-scaffolder/prompt-builders.mjs +1 -0
  40. package/lib/cli/token-optimization.mjs +161 -6
  41. package/lib/cli/utils.mjs +15 -1
  42. package/package.json +10 -3
  43. package/scripts/adaptive-context/fixtures.mjs +188 -0
  44. package/scripts/adaptive-context-benchmark.mjs +9 -0
  45. package/scripts/ascx-runtime-token-saver-benchmark.mjs +9 -0
  46. package/scripts/build-release-benchmark-bundle.mjs +1 -3
  47. package/scripts/clean-local-artifacts.mjs +2 -0
  48. package/scripts/compact-natural-mode-benchmark.mjs +9 -0
  49. package/scripts/validate/config.mjs +6 -0
  50. package/scripts/validate.mjs +2 -0
@@ -0,0 +1,365 @@
1
+ import {
2
+ DOCS_BY_LABEL,
3
+ PROMPT_CATALOG,
4
+ RULE_FAMILY_CATALOG,
5
+ STATE_BY_LABEL,
6
+ } from './adaptive-context/catalog.mjs';
7
+ import { CONTEXT_FILE_CATALOG } from './adaptive-context/file-signals.mjs';
8
+ import { IMPLICATION_CATALOG } from './adaptive-context/implications.mjs';
9
+
10
+ const CONTEXT_BUDGET_POLICY = Object.freeze({
11
+ maxRecommendedRuleCount: 5,
12
+ maxFallbackRuleCount: 7,
13
+ });
14
+
15
+ export function getRuleFamilyCatalog() {
16
+ return RULE_FAMILY_CATALOG.map((ruleFamily) => ({ ...ruleFamily }));
17
+ }
18
+
19
+ export function normalizeRequestText(requestText) {
20
+ return String(requestText || '')
21
+ .trim()
22
+ .toLowerCase()
23
+ .replace(/\s+/g, ' ');
24
+ }
25
+
26
+ function findMatchedTriggers(normalizedRequestText, triggers) {
27
+ return triggers.filter((triggerText) => triggerMatchesRequest(normalizedRequestText, triggerText));
28
+ }
29
+
30
+ function escapeRegExp(rawText) {
31
+ return rawText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
32
+ }
33
+
34
+ function triggerMatchesRequest(normalizedRequestText, triggerText) {
35
+ if (!normalizedRequestText || !triggerText) {
36
+ return false;
37
+ }
38
+
39
+ if (/^[a-z0-9 ]+$/.test(triggerText)) {
40
+ const triggerPattern = new RegExp(`(^|[^a-z0-9])${escapeRegExp(triggerText)}([^a-z0-9]|$)`);
41
+ return triggerPattern.test(normalizedRequestText);
42
+ }
43
+
44
+ return normalizedRequestText.includes(triggerText);
45
+ }
46
+
47
+ function compareCatalogEntries(leftEntry, rightEntry) {
48
+ return leftEntry.priority - rightEntry.priority || leftEntry.label.localeCompare(rightEntry.label);
49
+ }
50
+
51
+ function normalizeContextFilePath(contextFilePath) {
52
+ return String(contextFilePath || '')
53
+ .trim()
54
+ .replace(/\\/g, '/')
55
+ .replace(/\/+/g, '/')
56
+ .toLowerCase();
57
+ }
58
+
59
+ function normalizeContextFiles(contextFiles) {
60
+ if (!Array.isArray(contextFiles)) {
61
+ return [];
62
+ }
63
+
64
+ return contextFiles
65
+ .map(normalizeContextFilePath)
66
+ .filter(Boolean);
67
+ }
68
+
69
+ function uniqueSortedValues(values) {
70
+ return Array.from(new Set(values)).sort((leftValue, rightValue) => leftValue.localeCompare(rightValue));
71
+ }
72
+
73
+ function collectDocsForLabels(labels) {
74
+ const selectedDocs = ['docs/doc-index.md'];
75
+
76
+ for (const label of labels) {
77
+ selectedDocs.push(...(DOCS_BY_LABEL[label] || []));
78
+ }
79
+
80
+ return uniqueSortedValues(selectedDocs);
81
+ }
82
+
83
+ function collectStateForLabels(labels) {
84
+ const selectedState = ['.agent-context/state/onboarding-report.json'];
85
+
86
+ for (const label of labels) {
87
+ selectedState.push(...(STATE_BY_LABEL[label] || []));
88
+ }
89
+
90
+ return uniqueSortedValues(selectedState);
91
+ }
92
+
93
+ function collectPromptsForRequest(normalizedRequestText, labels) {
94
+ const selectedPrompts = [];
95
+
96
+ for (const promptEntry of PROMPT_CATALOG) {
97
+ const labelMatched = Array.isArray(promptEntry.labels)
98
+ && promptEntry.labels.some((label) => labels.includes(label));
99
+ const triggerMatched = findMatchedTriggers(normalizedRequestText, promptEntry.triggers).length > 0;
100
+
101
+ if (labelMatched || triggerMatched) {
102
+ selectedPrompts.push(promptEntry.promptPath);
103
+ }
104
+ }
105
+
106
+ return uniqueSortedValues(selectedPrompts);
107
+ }
108
+
109
+ function createRuleEntryMap() {
110
+ const ruleEntryMap = new Map();
111
+
112
+ for (const ruleFamily of RULE_FAMILY_CATALOG) {
113
+ ruleEntryMap.set(ruleFamily.label, {
114
+ ...ruleFamily,
115
+ matchedTriggers: [],
116
+ matchedFiles: [],
117
+ matchedImplications: [],
118
+ });
119
+ }
120
+
121
+ return ruleEntryMap;
122
+ }
123
+
124
+ function getMatchedRuleEntries(ruleEntryMap) {
125
+ return Array.from(ruleEntryMap.values())
126
+ .filter((ruleFamily) => {
127
+ return ruleFamily.matchedTriggers.length > 0
128
+ || ruleFamily.matchedFiles.length > 0
129
+ || ruleFamily.matchedImplications.length > 0;
130
+ })
131
+ .sort(compareCatalogEntries);
132
+ }
133
+
134
+ function pushUniqueSignal(values, nextValue) {
135
+ if (nextValue && !values.includes(nextValue)) {
136
+ values.push(nextValue);
137
+ }
138
+ }
139
+
140
+ function applyDirectTextSignals(ruleEntryMap, normalizedRequestText) {
141
+ for (const ruleFamily of RULE_FAMILY_CATALOG) {
142
+ const matchedTriggers = findMatchedTriggers(normalizedRequestText, ruleFamily.triggers);
143
+ const targetRuleEntry = ruleEntryMap.get(ruleFamily.label);
144
+
145
+ for (const matchedTrigger of matchedTriggers) {
146
+ pushUniqueSignal(targetRuleEntry.matchedTriggers, matchedTrigger);
147
+ }
148
+ }
149
+ }
150
+
151
+ function applyContextFileSignals(ruleEntryMap, normalizedContextFiles) {
152
+ for (const contextFilePath of normalizedContextFiles) {
153
+ for (const fileSignal of CONTEXT_FILE_CATALOG) {
154
+ if (!fileSignal.patterns.some((filePattern) => filePattern.test(contextFilePath))) {
155
+ continue;
156
+ }
157
+
158
+ const targetRuleEntry = ruleEntryMap.get(fileSignal.label);
159
+ pushUniqueSignal(targetRuleEntry.matchedFiles, contextFilePath);
160
+ }
161
+ }
162
+ }
163
+
164
+ function applyImplicationSignals(ruleEntryMap, normalizedRequestText) {
165
+ const matchedLabelSet = new Set(getMatchedRuleEntries(ruleEntryMap).map((ruleFamily) => ruleFamily.label));
166
+
167
+ for (const implicationRule of IMPLICATION_CATALOG) {
168
+ const hasRequiredLabel = implicationRule.requiresAnyLabel.some((label) => matchedLabelSet.has(label));
169
+ if (!hasRequiredLabel) {
170
+ continue;
171
+ }
172
+
173
+ const matchedTriggers = findMatchedTriggers(normalizedRequestText, implicationRule.triggers);
174
+ if (matchedTriggers.length === 0) {
175
+ continue;
176
+ }
177
+
178
+ const targetRuleEntry = ruleEntryMap.get(implicationRule.label);
179
+ const implicationDetail = `${implicationRule.reason}: ${matchedTriggers.join(', ')}`;
180
+ pushUniqueSignal(targetRuleEntry.matchedImplications, implicationDetail);
181
+ matchedLabelSet.add(implicationRule.label);
182
+ }
183
+ }
184
+
185
+ function countEvidenceSignals(evidenceEntry) {
186
+ return evidenceEntry.matchedTriggers.length
187
+ + evidenceEntry.matchedFiles.length
188
+ + evidenceEntry.matchedImplications.length;
189
+ }
190
+
191
+ function resolveUncertainty(labels, evidenceEntries, normalizedRequestText) {
192
+ if (!normalizedRequestText) {
193
+ return 'high';
194
+ }
195
+
196
+ if (labels.length === 0) {
197
+ return 'high';
198
+ }
199
+
200
+ const hasSparseEvidence = evidenceEntries.some((evidenceEntry) => countEvidenceSignals(evidenceEntry) === 1);
201
+ const hasManyLabels = labels.length >= 6;
202
+
203
+ if (hasManyLabels || hasSparseEvidence) {
204
+ return 'medium';
205
+ }
206
+
207
+ return 'low';
208
+ }
209
+
210
+ function buildContextBudget(labels, evidenceEntries) {
211
+ const selectedRuleCount = labels.length;
212
+ const evidenceSignalCount = evidenceEntries.reduce(
213
+ (totalCount, evidenceEntry) => totalCount + countEvidenceSignals(evidenceEntry),
214
+ 0
215
+ );
216
+ const overRecommendedRuleCount = Math.max(
217
+ 0,
218
+ selectedRuleCount - CONTEXT_BUDGET_POLICY.maxRecommendedRuleCount
219
+ );
220
+ const overFallbackRuleCount = Math.max(
221
+ 0,
222
+ selectedRuleCount - CONTEXT_BUDGET_POLICY.maxFallbackRuleCount
223
+ );
224
+ let status = 'within-budget';
225
+
226
+ if (overFallbackRuleCount > 0) {
227
+ status = 'fallback-required';
228
+ } else if (overRecommendedRuleCount > 0) {
229
+ status = 'wide-context';
230
+ }
231
+
232
+ return {
233
+ status,
234
+ selectedRuleCount,
235
+ evidenceSignalCount,
236
+ maxRecommendedRuleCount: CONTEXT_BUDGET_POLICY.maxRecommendedRuleCount,
237
+ maxFallbackRuleCount: CONTEXT_BUDGET_POLICY.maxFallbackRuleCount,
238
+ overRecommendedRuleCount,
239
+ overFallbackRuleCount,
240
+ };
241
+ }
242
+
243
+ export function buildSelectedContextManifest(options = {}) {
244
+ const {
245
+ contextFiles = [],
246
+ requestId = 'adhoc-request',
247
+ requestText = '',
248
+ } = options;
249
+
250
+ const normalizedRequestText = normalizeRequestText(requestText);
251
+ const normalizedContextFiles = normalizeContextFiles(contextFiles);
252
+ const ruleEntryMap = createRuleEntryMap();
253
+
254
+ applyDirectTextSignals(ruleEntryMap, normalizedRequestText);
255
+ applyContextFileSignals(ruleEntryMap, normalizedContextFiles);
256
+ applyImplicationSignals(ruleEntryMap, normalizedRequestText);
257
+
258
+ const matchedRuleEntries = getMatchedRuleEntries(ruleEntryMap);
259
+
260
+ const labels = matchedRuleEntries.map((ruleFamily) => ruleFamily.label);
261
+ const selectedRules = matchedRuleEntries.map((ruleFamily) => ruleFamily.rulePath);
262
+ const selectedRuleSet = new Set(selectedRules);
263
+ const skippedRules = RULE_FAMILY_CATALOG
264
+ .map((ruleFamily) => ruleFamily.rulePath)
265
+ .filter((rulePath) => !selectedRuleSet.has(rulePath))
266
+ .sort((leftPath, rightPath) => leftPath.localeCompare(rightPath));
267
+ const evidenceEntries = matchedRuleEntries.map((ruleFamily) => ({
268
+ label: ruleFamily.label,
269
+ matchedTriggers: ruleFamily.matchedTriggers,
270
+ matchedFiles: ruleFamily.matchedFiles,
271
+ matchedImplications: ruleFamily.matchedImplications,
272
+ }));
273
+ const uncertainty = resolveUncertainty(labels, evidenceEntries, normalizedRequestText);
274
+ const budget = buildContextBudget(labels, evidenceEntries);
275
+
276
+ return {
277
+ schemaVersion: 'adaptive-context-manifest-v1',
278
+ requestId,
279
+ contextFiles: normalizedContextFiles,
280
+ labels,
281
+ selectedRules,
282
+ selectedPrompts: collectPromptsForRequest(normalizedRequestText, labels),
283
+ selectedDocs: collectDocsForLabels(labels),
284
+ selectedState: collectStateForLabels(labels),
285
+ skippedRules,
286
+ uncertainty,
287
+ budget,
288
+ fallbackRequired: uncertainty === 'high' || budget.status === 'fallback-required',
289
+ evidence: evidenceEntries,
290
+ };
291
+ }
292
+
293
+ function compareRequiredLabels(requiredLabels, actualLabels) {
294
+ const actualLabelSet = new Set(actualLabels);
295
+
296
+ return requiredLabels.filter((requiredLabel) => !actualLabelSet.has(requiredLabel));
297
+ }
298
+
299
+ function compareExtraLabels(expectedLabels, actualLabels) {
300
+ const expectedLabelSet = new Set(expectedLabels);
301
+
302
+ return actualLabels.filter((actualLabel) => !expectedLabelSet.has(actualLabel));
303
+ }
304
+
305
+ export function evaluateAdaptiveContextFixtures(fixtures) {
306
+ const fixtureResults = fixtures.map((fixtureEntry) => {
307
+ const manifest = buildSelectedContextManifest({
308
+ requestId: fixtureEntry.id,
309
+ requestText: fixtureEntry.requestText,
310
+ });
311
+ const missedRequiredLabels = compareRequiredLabels(fixtureEntry.requiredLabels, manifest.labels);
312
+ const extraLabels = compareExtraLabels(fixtureEntry.allowedLabels || fixtureEntry.requiredLabels, manifest.labels);
313
+
314
+ return {
315
+ id: fixtureEntry.id,
316
+ passed: missedRequiredLabels.length === 0,
317
+ missedRequiredLabels,
318
+ extraLabels,
319
+ manifest,
320
+ };
321
+ });
322
+ const failedFixtures = fixtureResults.filter((fixtureResult) => !fixtureResult.passed);
323
+ const missedRequiredLabelCount = fixtureResults.reduce(
324
+ (totalCount, fixtureResult) => totalCount + fixtureResult.missedRequiredLabels.length,
325
+ 0
326
+ );
327
+ const extraLabelCount = fixtureResults.reduce(
328
+ (totalCount, fixtureResult) => totalCount + fixtureResult.extraLabels.length,
329
+ 0
330
+ );
331
+ const selectedRuleCounts = fixtureResults.map((fixtureResult) => fixtureResult.manifest.budget.selectedRuleCount);
332
+ const maxSelectedRuleCount = Math.max(...selectedRuleCounts, 0);
333
+ const totalSelectedRuleCount = selectedRuleCounts.reduce(
334
+ (totalCount, selectedRuleCount) => totalCount + selectedRuleCount,
335
+ 0
336
+ );
337
+ const overRecommendedFixtureCount = fixtureResults.filter((fixtureResult) => {
338
+ return fixtureResult.manifest.budget.status !== 'within-budget';
339
+ }).length;
340
+ const fallbackFixtureCount = fixtureResults.filter((fixtureResult) => {
341
+ return fixtureResult.manifest.fallbackRequired;
342
+ }).length;
343
+
344
+ return {
345
+ reportName: 'adaptive-context-benchmark',
346
+ generatedAt: new Date().toISOString(),
347
+ fixtureCount: fixtureResults.length,
348
+ passed: failedFixtures.length === 0,
349
+ passedCount: fixtureResults.length - failedFixtures.length,
350
+ failedCount: failedFixtures.length,
351
+ missedRequiredLabelCount,
352
+ extraLabelCount,
353
+ budgetSummary: {
354
+ maxSelectedRuleCount,
355
+ averageSelectedRuleCount: fixtureResults.length === 0
356
+ ? 0
357
+ : Number((totalSelectedRuleCount / fixtureResults.length).toFixed(2)),
358
+ overRecommendedFixtureCount,
359
+ fallbackFixtureCount,
360
+ maxRecommendedRuleCount: CONTEXT_BUDGET_POLICY.maxRecommendedRuleCount,
361
+ maxFallbackRuleCount: CONTEXT_BUDGET_POLICY.maxFallbackRuleCount,
362
+ },
363
+ results: fixtureResults,
364
+ };
365
+ }
@@ -0,0 +1,223 @@
1
+ const MAX_VISIBLE_FILES = 12;
2
+ const MAX_VISIBLE_HUNKS_PER_FILE = 3;
3
+ const MAX_VISIBLE_CHANGE_LINES_PER_HUNK = 6;
4
+
5
+ function normalizeDiffPath(rawPath) {
6
+ return String(rawPath || '')
7
+ .replace(/^a\//u, '')
8
+ .replace(/^b\//u, '')
9
+ .trim();
10
+ }
11
+
12
+ function pushUnique(lines, nextLine) {
13
+ const normalizedLine = String(nextLine || '').trimEnd();
14
+ if (normalizedLine && !lines.includes(normalizedLine)) {
15
+ lines.push(normalizedLine);
16
+ }
17
+ }
18
+
19
+ function createDiffFile(rawHeaderLine) {
20
+ const headerMatch = rawHeaderLine.match(/^diff --git\s+(.+?)\s+(.+)$/u);
21
+ const oldPath = normalizeDiffPath(headerMatch?.[1] || '');
22
+ const newPath = normalizeDiffPath(headerMatch?.[2] || oldPath);
23
+
24
+ return {
25
+ path: newPath || oldPath || 'unknown',
26
+ oldPath,
27
+ additions: 0,
28
+ deletions: 0,
29
+ hunks: [],
30
+ markers: [],
31
+ isGeneratedLike: false,
32
+ };
33
+ }
34
+
35
+ function isGeneratedLikePath(filePath) {
36
+ return /(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb|dist\/|build\/|coverage\/|\.min\.(?:js|css)|\.snap$|generated)/iu.test(filePath);
37
+ }
38
+
39
+ function currentHunk(diffFile) {
40
+ return diffFile.hunks[diffFile.hunks.length - 1] || null;
41
+ }
42
+
43
+ function parseDiffFiles(lines) {
44
+ const diffFiles = [];
45
+ let activeFile = null;
46
+
47
+ for (const line of lines) {
48
+ if (line.startsWith('diff --git ')) {
49
+ activeFile = createDiffFile(line);
50
+ activeFile.isGeneratedLike = isGeneratedLikePath(activeFile.path);
51
+ diffFiles.push(activeFile);
52
+ continue;
53
+ }
54
+
55
+ if (!activeFile) {
56
+ continue;
57
+ }
58
+
59
+ if (line.startsWith('new file mode')) {
60
+ pushUnique(activeFile.markers, 'new file');
61
+ continue;
62
+ }
63
+
64
+ if (line.startsWith('deleted file mode')) {
65
+ pushUnique(activeFile.markers, 'deleted file');
66
+ continue;
67
+ }
68
+
69
+ if (line.startsWith('rename from ')) {
70
+ pushUnique(activeFile.markers, `rename from ${line.slice('rename from '.length).trim()}`);
71
+ continue;
72
+ }
73
+
74
+ if (line.startsWith('rename to ')) {
75
+ const renamedPath = line.slice('rename to '.length).trim();
76
+ activeFile.path = renamedPath || activeFile.path;
77
+ activeFile.isGeneratedLike = isGeneratedLikePath(activeFile.path);
78
+ pushUnique(activeFile.markers, `rename to ${renamedPath}`);
79
+ continue;
80
+ }
81
+
82
+ if (line.startsWith('Binary files ') || line.startsWith('GIT binary patch')) {
83
+ pushUnique(activeFile.markers, 'binary file changed');
84
+ continue;
85
+ }
86
+
87
+ if (line.startsWith('@@ ')) {
88
+ activeFile.hunks.push({
89
+ header: line.trim(),
90
+ changeLines: [],
91
+ });
92
+ continue;
93
+ }
94
+
95
+ if (line.startsWith('+++ ') || line.startsWith('--- ')) {
96
+ continue;
97
+ }
98
+
99
+ if (line.startsWith('+')) {
100
+ activeFile.additions += 1;
101
+ const hunk = currentHunk(activeFile);
102
+ if (hunk && !activeFile.isGeneratedLike) {
103
+ pushUnique(hunk.changeLines, line);
104
+ }
105
+ continue;
106
+ }
107
+
108
+ if (line.startsWith('-')) {
109
+ activeFile.deletions += 1;
110
+ const hunk = currentHunk(activeFile);
111
+ if (hunk && !activeFile.isGeneratedLike) {
112
+ pushUnique(hunk.changeLines, line);
113
+ }
114
+ }
115
+ }
116
+
117
+ return diffFiles;
118
+ }
119
+
120
+ function formatDiffFile(diffFile) {
121
+ const outputLines = [];
122
+ let truncated = false;
123
+ const markers = diffFile.markers.length > 0
124
+ ? ` [${diffFile.markers.join(', ')}]`
125
+ : '';
126
+ const generatedMarker = diffFile.isGeneratedLike ? ' [generated/noisy path]' : '';
127
+
128
+ outputLines.push(`- ${diffFile.path} (+${diffFile.additions} -${diffFile.deletions})${markers}${generatedMarker}`);
129
+
130
+ if (diffFile.isGeneratedLike) {
131
+ outputLines.push(' detail: omitted generated/noisy diff; raw tee required for exact lines');
132
+ return { outputLines, truncated: true };
133
+ }
134
+
135
+ const visibleHunks = diffFile.hunks.slice(0, MAX_VISIBLE_HUNKS_PER_FILE);
136
+ for (const hunk of visibleHunks) {
137
+ outputLines.push(` ${hunk.header}`);
138
+ const visibleChangeLines = hunk.changeLines.slice(0, MAX_VISIBLE_CHANGE_LINES_PER_HUNK);
139
+ for (const changeLine of visibleChangeLines) {
140
+ outputLines.push(` ${changeLine}`);
141
+ }
142
+
143
+ if (hunk.changeLines.length > visibleChangeLines.length) {
144
+ truncated = true;
145
+ outputLines.push(` ... truncated ${hunk.changeLines.length - visibleChangeLines.length} more changed lines in hunk`);
146
+ }
147
+ }
148
+
149
+ if (diffFile.hunks.length > visibleHunks.length) {
150
+ truncated = true;
151
+ outputLines.push(` ... truncated ${diffFile.hunks.length - visibleHunks.length} more hunks`);
152
+ }
153
+
154
+ if (diffFile.hunks.length === 0 && diffFile.markers.length === 0) {
155
+ outputLines.push(' detail: no parseable hunks; raw tee required for exact diff');
156
+ truncated = true;
157
+ }
158
+
159
+ return { outputLines, truncated };
160
+ }
161
+
162
+ export function compressGitDiffOutput({ stdout, stderr, exitCode }) {
163
+ const rawOutput = [stdout, stderr].filter(Boolean).join('\n');
164
+
165
+ if (exitCode === 0 && rawOutput.trim() === '') {
166
+ return {
167
+ filterName: 'git-diff-summary',
168
+ confident: true,
169
+ truncated: false,
170
+ output: 'git diff: no changes',
171
+ preservedFields: {
172
+ changedFileList: true,
173
+ },
174
+ };
175
+ }
176
+
177
+ const lines = rawOutput.split(/\r?\n/u);
178
+ const diffFiles = parseDiffFiles(lines);
179
+
180
+ if (diffFiles.length === 0) {
181
+ return {
182
+ filterName: 'git-diff-raw-parse-uncertain',
183
+ confident: false,
184
+ truncated: false,
185
+ output: rawOutput,
186
+ preservedFields: {},
187
+ };
188
+ }
189
+
190
+ const outputLines = [
191
+ 'git diff summary:',
192
+ `files: ${diffFiles.length}`,
193
+ ];
194
+ let truncated = false;
195
+ const visibleFiles = diffFiles.slice(0, MAX_VISIBLE_FILES);
196
+
197
+ for (const diffFile of visibleFiles) {
198
+ const formattedFile = formatDiffFile(diffFile);
199
+ outputLines.push(...formattedFile.outputLines);
200
+ truncated = truncated || formattedFile.truncated;
201
+ }
202
+
203
+ if (diffFiles.length > visibleFiles.length) {
204
+ truncated = true;
205
+ outputLines.push(`... truncated ${diffFiles.length - visibleFiles.length} more files`);
206
+ }
207
+
208
+ if (truncated) {
209
+ outputLines.push('truncation: raw diff available in tee output');
210
+ }
211
+
212
+ return {
213
+ filterName: 'git-diff-summary',
214
+ confident: true,
215
+ truncated,
216
+ output: outputLines.join('\n'),
217
+ preservedFields: {
218
+ changedFileList: true,
219
+ hunkHeaders: diffFiles.some((diffFile) => diffFile.hunks.length > 0),
220
+ binaryMarker: diffFiles.some((diffFile) => diffFile.markers.includes('binary file changed')),
221
+ },
222
+ };
223
+ }