@colin4k1024/tsp 2.5.1 → 2.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lib/install-surface.js +5 -0
- package/commands/dashboard.md +105 -0
- package/commands/goal.md +142 -0
- package/commands/heartbeat.md +129 -0
- package/commands/triage.md +108 -0
- package/hooks/harness-context-monitor.js +4 -0
- package/hooks/harness-statusline.js +34 -11
- package/hooks/hooks.json +23 -23
- package/manifests/install-modules.json +98 -31
- package/package.json +2 -1
- package/schemas/goal.schema.json +172 -0
- package/scripts/hooks/session-start-goal-resume.js +95 -0
- package/scripts/hooks/suggest-compact.js +122 -19
- package/scripts/lib/blame-attribution.js +210 -0
- package/scripts/lib/completion-oracle.js +351 -0
- package/scripts/lib/heartbeat-scheduler.js +265 -0
- package/scripts/lib/install/request.js +1 -1
- package/scripts/lib/install-manifests.js +9 -1
- package/scripts/lib/install-targets/cangming-home.js +143 -0
- package/scripts/lib/install-targets/codewhale-home.js +187 -0
- package/scripts/lib/install-targets/registry.js +5 -1
- package/scripts/lib/transcript-usage.js +183 -0
- package/scripts/lib/wave-cost-advisor.js +155 -0
- package/scripts/test-cangming-install.js +105 -0
- package/skills/goal-convergence/SKILL.md +150 -0
- package/skills/loop-heartbeat/SKILL.md +120 -0
- package/skills/mcp-connector-bridge/SKILL.md +132 -0
- package/skills/rework-loop/SKILL.md +131 -0
|
@@ -16,12 +16,13 @@ const crypto = require('crypto');
|
|
|
16
16
|
const AUTO_COMPACT_BUFFER_PCT = 16.5;
|
|
17
17
|
const DEFAULT_CONTEXT_LIMIT = 200000;
|
|
18
18
|
const DEFAULT_DEBOUNCE_CALLS = 8;
|
|
19
|
-
const STALE_BRIDGE_SECONDS =
|
|
19
|
+
const STALE_BRIDGE_SECONDS = 120;
|
|
20
20
|
|
|
21
21
|
const URGENCY = [
|
|
22
22
|
[95, 'critical'],
|
|
23
23
|
[85, 'high'],
|
|
24
24
|
[70, 'medium'],
|
|
25
|
+
[65, 'advisory'],
|
|
25
26
|
[0, 'low'],
|
|
26
27
|
];
|
|
27
28
|
|
|
@@ -64,8 +65,11 @@ function sessionKey(data) {
|
|
|
64
65
|
return raw.replace(/[^a-zA-Z0-9_-]/g, '').slice(-64) || 'default';
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
// Use PID + cwd hash to differentiate sessions in the same directory
|
|
69
|
+
const pidKey = `pid-${process.pid}`;
|
|
67
70
|
const cwd = data.cwd || process.cwd();
|
|
68
|
-
|
|
71
|
+
const cwdHash = crypto.createHash('sha256').update(String(cwd)).digest('hex').slice(0, 16);
|
|
72
|
+
return `${pidKey}-${cwdHash}`;
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
function readBridgeMetrics(sessionId) {
|
|
@@ -95,7 +99,30 @@ function readBridgeMetrics(sessionId) {
|
|
|
95
99
|
|
|
96
100
|
function resolveContextMetrics(data) {
|
|
97
101
|
const contextLimit = toNumber(process.env.CLAUDE_CONTEXT_LIMIT) || DEFAULT_CONTEXT_LIMIT;
|
|
98
|
-
|
|
102
|
+
let cw = {};
|
|
103
|
+
if (data.context_window && typeof data.context_window === 'object') {
|
|
104
|
+
cw = data.context_window;
|
|
105
|
+
} else if (data.context_window != null) {
|
|
106
|
+
// Malformed: context_window is present but not an object.
|
|
107
|
+
// Attempt to extract a numeric value (some Claude Code versions
|
|
108
|
+
// may pass context_size as a bare number).
|
|
109
|
+
const numericValue = toNumber(data.context_window);
|
|
110
|
+
if (numericValue != null && numericValue > 0) {
|
|
111
|
+
const usagePct = clampPct((numericValue / contextLimit) * 100);
|
|
112
|
+
return {
|
|
113
|
+
usagePct,
|
|
114
|
+
remainingPct: null,
|
|
115
|
+
contextLimit,
|
|
116
|
+
contextSize: numericValue,
|
|
117
|
+
source: 'stdin.context_window_raw',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (process.env.STRATEGIC_COMPACT_DEBUG === '1') {
|
|
121
|
+
process.stderr.write(
|
|
122
|
+
`[strategic-compact] context_window is ${typeof data.context_window}: ${JSON.stringify(data.context_window).slice(0, 100)}\n`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
99
126
|
|
|
100
127
|
const stdinUsed = toNumber(cw.used_percentage);
|
|
101
128
|
if (stdinUsed != null) {
|
|
@@ -133,6 +160,27 @@ function resolveContextMetrics(data) {
|
|
|
133
160
|
};
|
|
134
161
|
}
|
|
135
162
|
|
|
163
|
+
// Transcript JSONL usage parsing (CCometixLine approach)
|
|
164
|
+
const transcriptPath = data.transcript_path;
|
|
165
|
+
const modelId = (data.model && data.model.id) || process.env.CLAUDE_MODEL || null;
|
|
166
|
+
if (transcriptPath && typeof transcriptPath === 'string') {
|
|
167
|
+
try {
|
|
168
|
+
const { resolveTranscriptMetrics } = require('../lib/transcript-usage');
|
|
169
|
+
const transcriptMetrics = resolveTranscriptMetrics(transcriptPath, modelId);
|
|
170
|
+
if (transcriptMetrics) {
|
|
171
|
+
return {
|
|
172
|
+
usagePct: clampPct(transcriptMetrics.usagePct),
|
|
173
|
+
remainingPct: null,
|
|
174
|
+
contextLimit: transcriptMetrics.contextLimit,
|
|
175
|
+
contextSize: transcriptMetrics.contextTokens,
|
|
176
|
+
source: 'transcript_usage',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
} catch (_) {
|
|
180
|
+
// transcript parsing failed — fall through
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
136
184
|
const bridge = readBridgeMetrics(sessionKey(data));
|
|
137
185
|
if (bridge) {
|
|
138
186
|
return {
|
|
@@ -142,6 +190,26 @@ function resolveContextMetrics(data) {
|
|
|
142
190
|
};
|
|
143
191
|
}
|
|
144
192
|
|
|
193
|
+
// Final fallback: estimate from transcript file size
|
|
194
|
+
if (transcriptPath && typeof transcriptPath === 'string') {
|
|
195
|
+
try {
|
|
196
|
+
const stat = fs.statSync(transcriptPath);
|
|
197
|
+
const estimatedTokens = Math.round(stat.size * 0.25);
|
|
198
|
+
if (estimatedTokens > 0) {
|
|
199
|
+
const usagePct = clampPct((estimatedTokens / contextLimit) * 100);
|
|
200
|
+
return {
|
|
201
|
+
usagePct,
|
|
202
|
+
remainingPct: null,
|
|
203
|
+
contextLimit,
|
|
204
|
+
contextSize: estimatedTokens,
|
|
205
|
+
source: 'transcript_size',
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
} catch (_) {
|
|
209
|
+
// transcript not accessible — fall through
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
145
213
|
return null;
|
|
146
214
|
}
|
|
147
215
|
|
|
@@ -215,12 +283,6 @@ function shouldEmit(sessionId, urgency, usagePct) {
|
|
|
215
283
|
const debounceCalls =
|
|
216
284
|
toNumber(process.env.STRATEGIC_COMPACT_DEBOUNCE_CALLS) || DEFAULT_DEBOUNCE_CALLS;
|
|
217
285
|
const statePath = path.join(os.tmpdir(), `harness-strategic-compact-${sessionId}.json`);
|
|
218
|
-
const nextState = {
|
|
219
|
-
lastUrgency: urgency,
|
|
220
|
-
lastUsagePct: usagePct,
|
|
221
|
-
callsSinceEmit: 0,
|
|
222
|
-
updatedAt: new Date().toISOString(),
|
|
223
|
-
};
|
|
224
286
|
|
|
225
287
|
try {
|
|
226
288
|
let previous = null;
|
|
@@ -229,22 +291,38 @@ function shouldEmit(sessionId, urgency, usagePct) {
|
|
|
229
291
|
}
|
|
230
292
|
|
|
231
293
|
const callsSinceEmit = (previous?.callsSinceEmit || 0) + 1;
|
|
232
|
-
const severityOrder = { low: 0,
|
|
294
|
+
const severityOrder = { low: 0, advisory: 1, medium: 2, high: 3, critical: 4 };
|
|
233
295
|
const escalated = severityOrder[urgency] > severityOrder[previous?.lastUrgency || 'low'];
|
|
234
296
|
const usageJumped = usagePct - (previous?.lastUsagePct || 0) >= 8;
|
|
235
297
|
const repeatDue = callsSinceEmit >= debounceCalls;
|
|
236
298
|
|
|
237
299
|
if (!previous || escalated || usageJumped || repeatDue) {
|
|
238
|
-
fs.writeFileSync(statePath, JSON.stringify(
|
|
300
|
+
fs.writeFileSync(statePath, JSON.stringify({
|
|
301
|
+
lastUrgency: urgency,
|
|
302
|
+
lastUsagePct: usagePct,
|
|
303
|
+
callsSinceEmit: 0,
|
|
304
|
+
updatedAt: new Date().toISOString(),
|
|
305
|
+
}));
|
|
239
306
|
return true;
|
|
240
307
|
}
|
|
241
308
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
309
|
+
// Only write if callsSinceEmit actually changed
|
|
310
|
+
if (callsSinceEmit > 1) {
|
|
311
|
+
fs.writeFileSync(statePath, JSON.stringify({
|
|
312
|
+
lastUrgency: previous.lastUrgency || urgency,
|
|
313
|
+
lastUsagePct: previous.lastUsagePct || usagePct,
|
|
314
|
+
callsSinceEmit,
|
|
315
|
+
updatedAt: new Date().toISOString(),
|
|
316
|
+
}));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (process.env.STRATEGIC_COMPACT_DEBUG === '1') {
|
|
320
|
+
process.stderr.write(
|
|
321
|
+
`[strategic-compact] debounce suppressed: callsSinceEmit=${callsSinceEmit}, ` +
|
|
322
|
+
`urgency=${urgency}, lastUrgency=${previous?.lastUrgency}\n`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
248
326
|
return false;
|
|
249
327
|
} catch (_) {
|
|
250
328
|
return true;
|
|
@@ -254,6 +332,7 @@ function shouldEmit(sessionId, urgency, usagePct) {
|
|
|
254
332
|
function buildContextMessage({ usagePct, remainingPct, urgency, savings, suggestions, source }) {
|
|
255
333
|
const remainingPart = remainingPct == null ? '' : ` | remaining: ${clampPct(remainingPct)}%`;
|
|
256
334
|
const actionByUrgency = {
|
|
335
|
+
advisory: 'Context usage is approaching the compaction threshold. Be mindful of context budget; avoid starting large new explorations.',
|
|
257
336
|
medium: 'Finish the current small step, then run `/compact` before more broad reading or implementation.',
|
|
258
337
|
high: 'Stop new exploration, preserve decisions/todos/validation results, and ask the user to run `/compact`.',
|
|
259
338
|
critical: 'Context is nearly exhausted. Do not start new tool chains; ask the user to run `/compact` now.',
|
|
@@ -273,14 +352,38 @@ function buildHookOutput(rawInput) {
|
|
|
273
352
|
try {
|
|
274
353
|
data = JSON.parse(rawInput || '{}');
|
|
275
354
|
} catch (_) {
|
|
355
|
+
if (process.env.STRATEGIC_COMPACT_DEBUG === '1') {
|
|
356
|
+
process.stderr.write('[strategic-compact] failed to parse stdin JSON\n');
|
|
357
|
+
}
|
|
276
358
|
return null;
|
|
277
359
|
}
|
|
278
360
|
|
|
279
361
|
const metrics = resolveContextMetrics(data);
|
|
280
|
-
if (!metrics)
|
|
362
|
+
if (!metrics) {
|
|
363
|
+
if (process.env.STRATEGIC_COMPACT_DEBUG === '1') {
|
|
364
|
+
const sessionId = sessionKey(data);
|
|
365
|
+
const bridgePath = path.join(os.tmpdir(), `harness-ctx-${sessionId}.json`);
|
|
366
|
+
const bridgeExists = fs.existsSync(bridgePath);
|
|
367
|
+
process.stderr.write(
|
|
368
|
+
`[strategic-compact] no metrics available. ` +
|
|
369
|
+
`context_window=${JSON.stringify(data.context_window || 'missing')}, ` +
|
|
370
|
+
`session=${sessionId}, bridge_exists=${bridgeExists}, ` +
|
|
371
|
+
`env_size=${process.env.CLAUDE_CONTEXT_SIZE || 'unset'}, ` +
|
|
372
|
+
`env_limit=${process.env.CLAUDE_CONTEXT_LIMIT || 'unset'}\n`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
281
377
|
|
|
282
378
|
const urgency = getUrgency(metrics.usagePct);
|
|
283
|
-
if (urgency === 'low')
|
|
379
|
+
if (urgency === 'low') {
|
|
380
|
+
if (process.env.STRATEGIC_COMPACT_DEBUG === '1') {
|
|
381
|
+
process.stderr.write(
|
|
382
|
+
`[strategic-compact] below threshold: ${metrics.usagePct}% (source: ${metrics.source})\n`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
284
387
|
|
|
285
388
|
const sessionId = sessionKey(data);
|
|
286
389
|
if (!shouldEmit(sessionId, urgency, metrics.usagePct)) return null;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* blame-attribution.js
|
|
5
|
+
*
|
|
6
|
+
* Maps goal iteration failures to specific code changes using git blame/diff.
|
|
7
|
+
* Produces targeted rework briefs that constrain the next iteration's scope.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
|
|
14
|
+
const MAX_REWORK_ATTEMPTS = 3;
|
|
15
|
+
|
|
16
|
+
function parseFailureLocations(failOutput) {
|
|
17
|
+
const locations = [];
|
|
18
|
+
const lines = failOutput.split('\n');
|
|
19
|
+
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
// Match common test failure patterns:
|
|
22
|
+
// "FAIL src/auth/refresh.test.ts"
|
|
23
|
+
// "● Auth > should refresh tokens"
|
|
24
|
+
// "at Object.<anonymous> (src/auth/refresh.ts:23:5)"
|
|
25
|
+
// "src/auth/refresh.ts(23,5): error TS2345"
|
|
26
|
+
const fileLineMatch = line.match(/([a-zA-Z0-9_\-./]+\.[a-z]{1,4})[:(](\d+)/);
|
|
27
|
+
if (fileLineMatch) {
|
|
28
|
+
const filePath = fileLineMatch[1];
|
|
29
|
+
const lineNumber = parseInt(fileLineMatch[2], 10);
|
|
30
|
+
if (!filePath.includes('node_modules') && !filePath.includes('.git')) {
|
|
31
|
+
locations.push({ file: filePath, line: lineNumber, context: line.trim() });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Match "FAIL" prefixed paths
|
|
36
|
+
const failMatch = line.match(/FAIL\s+(.+\.[a-z]{1,4})/);
|
|
37
|
+
if (failMatch) {
|
|
38
|
+
locations.push({ file: failMatch[1].trim(), line: null, context: line.trim() });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Deduplicate by file
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
return locations.filter(loc => {
|
|
45
|
+
const key = `${loc.file}:${loc.line || 0}`;
|
|
46
|
+
if (seen.has(key)) return false;
|
|
47
|
+
seen.add(key);
|
|
48
|
+
return true;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getChangedFiles(cwd) {
|
|
53
|
+
try {
|
|
54
|
+
const output = execSync('git diff --name-only HEAD 2>/dev/null || git diff --name-only', {
|
|
55
|
+
encoding: 'utf-8',
|
|
56
|
+
cwd,
|
|
57
|
+
timeout: 10000,
|
|
58
|
+
}).trim();
|
|
59
|
+
return output ? output.split('\n').filter(Boolean) : [];
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getFileDiff(filePath, cwd) {
|
|
66
|
+
try {
|
|
67
|
+
return execSync(`git diff HEAD -- "${filePath}" 2>/dev/null || git diff -- "${filePath}"`, {
|
|
68
|
+
encoding: 'utf-8',
|
|
69
|
+
cwd,
|
|
70
|
+
timeout: 10000,
|
|
71
|
+
}).trim();
|
|
72
|
+
} catch {
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function intersectFailuresWithChanges(failureLocations, changedFiles) {
|
|
78
|
+
return failureLocations.filter(loc => {
|
|
79
|
+
const normalizedFail = loc.file.replace(/^\.\//, '');
|
|
80
|
+
return changedFiles.some(changed => {
|
|
81
|
+
const normalizedChanged = changed.replace(/^\.\//, '');
|
|
82
|
+
return normalizedFail === normalizedChanged ||
|
|
83
|
+
normalizedFail.endsWith(normalizedChanged) ||
|
|
84
|
+
normalizedChanged.endsWith(normalizedFail);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildReworkBrief(goal, oracleResult, blameResult) {
|
|
90
|
+
const iteration = goal.currentIteration;
|
|
91
|
+
const failingFiles = blameResult.blamedLocations.map(l => l.file);
|
|
92
|
+
const uniqueFiles = [...new Set(failingFiles)];
|
|
93
|
+
|
|
94
|
+
const brief = [
|
|
95
|
+
'## REWORK BRIEF',
|
|
96
|
+
'',
|
|
97
|
+
`**Goal:** ${goal.objective}`,
|
|
98
|
+
`**Iteration:** ${iteration} (rework attempt ${blameResult.attemptCount} for this scope)`,
|
|
99
|
+
'',
|
|
100
|
+
'**Failing evidence:**',
|
|
101
|
+
...blameResult.blamedLocations.map(l =>
|
|
102
|
+
`- ${l.file}${l.line ? ':' + l.line : ''} — ${l.context.slice(0, 100)}`
|
|
103
|
+
),
|
|
104
|
+
'',
|
|
105
|
+
'**Root cause (blame):**',
|
|
106
|
+
...blameResult.changedFiles.slice(0, 5).map(f => `- Changed: ${f}`),
|
|
107
|
+
'',
|
|
108
|
+
'**Constraint:**',
|
|
109
|
+
...uniqueFiles.map(f => `- ONLY modify: ${f}`),
|
|
110
|
+
'- DO NOT touch test files unless they are the source of the bug',
|
|
111
|
+
'- DO NOT expand scope beyond the blamed files',
|
|
112
|
+
'',
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
if (oracleResult.nextHint) {
|
|
116
|
+
brief.push('**Oracle hint:**', oracleResult.nextHint, '');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return brief.join('\n');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function analyzeBlame(goal, oracleResult, cwd) {
|
|
123
|
+
const failOutput = (oracleResult.reasons || []).join('\n') +
|
|
124
|
+
'\n' + (oracleResult.conditionResults || [])
|
|
125
|
+
.filter(r => !r.passed)
|
|
126
|
+
.map(r => r.output)
|
|
127
|
+
.join('\n');
|
|
128
|
+
|
|
129
|
+
const failureLocations = parseFailureLocations(failOutput);
|
|
130
|
+
const changedFiles = getChangedFiles(cwd);
|
|
131
|
+
const blamedLocations = intersectFailuresWithChanges(failureLocations, changedFiles);
|
|
132
|
+
|
|
133
|
+
// Determine if escalation is needed
|
|
134
|
+
const shouldEscalate = blamedLocations.length === 0 && failureLocations.length > 0;
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
failureLocations,
|
|
138
|
+
changedFiles,
|
|
139
|
+
blamedLocations: blamedLocations.length > 0 ? blamedLocations : failureLocations.slice(0, 5),
|
|
140
|
+
intersection: blamedLocations.length > 0,
|
|
141
|
+
shouldEscalate,
|
|
142
|
+
escalationReason: shouldEscalate ? 'no_blame_intersection' : null,
|
|
143
|
+
attemptCount: 1, // caller should update from tracking store
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Rework tracking
|
|
148
|
+
|
|
149
|
+
function getReworkTrackingPath() {
|
|
150
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
151
|
+
return path.join(home, '.claude', 'rework-tracking.json');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function loadReworkTracking() {
|
|
155
|
+
const trackPath = getReworkTrackingPath();
|
|
156
|
+
if (!fs.existsSync(trackPath)) return {};
|
|
157
|
+
try {
|
|
158
|
+
return JSON.parse(fs.readFileSync(trackPath, 'utf-8'));
|
|
159
|
+
} catch {
|
|
160
|
+
return {};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function saveReworkTracking(tracking) {
|
|
165
|
+
const trackPath = getReworkTrackingPath();
|
|
166
|
+
const dir = path.dirname(trackPath);
|
|
167
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
168
|
+
fs.writeFileSync(trackPath, JSON.stringify(tracking, null, 2), 'utf-8');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function recordReworkAttempt(filePath, outcome) {
|
|
172
|
+
const tracking = loadReworkTracking();
|
|
173
|
+
if (!tracking[filePath]) {
|
|
174
|
+
tracking[filePath] = { attempts: 0, outcomes: [], lastAttempt: null, totalCost: 0 };
|
|
175
|
+
}
|
|
176
|
+
tracking[filePath].attempts += 1;
|
|
177
|
+
tracking[filePath].outcomes.push(outcome);
|
|
178
|
+
tracking[filePath].lastAttempt = new Date().toISOString();
|
|
179
|
+
saveReworkTracking(tracking);
|
|
180
|
+
return tracking[filePath];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function shouldEscalateFile(filePath) {
|
|
184
|
+
const tracking = loadReworkTracking();
|
|
185
|
+
const record = tracking[filePath];
|
|
186
|
+
if (!record) return false;
|
|
187
|
+
return record.attempts >= MAX_REWORK_ATTEMPTS;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function getPersistentTroubleSpots() {
|
|
191
|
+
const tracking = loadReworkTracking();
|
|
192
|
+
return Object.entries(tracking)
|
|
193
|
+
.filter(([, record]) => record.attempts >= MAX_REWORK_ATTEMPTS)
|
|
194
|
+
.map(([file, record]) => ({ file, ...record }));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
MAX_REWORK_ATTEMPTS,
|
|
199
|
+
parseFailureLocations,
|
|
200
|
+
getChangedFiles,
|
|
201
|
+
getFileDiff,
|
|
202
|
+
intersectFailuresWithChanges,
|
|
203
|
+
buildReworkBrief,
|
|
204
|
+
analyzeBlame,
|
|
205
|
+
loadReworkTracking,
|
|
206
|
+
saveReworkTracking,
|
|
207
|
+
recordReworkAttempt,
|
|
208
|
+
shouldEscalateFile,
|
|
209
|
+
getPersistentTroubleSpots,
|
|
210
|
+
};
|