@open330/oac 2026.2.5
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/CHANGELOG.md +115 -0
- package/LICENSE +21 -0
- package/README.md +597 -0
- package/dist/budget/index.d.ts +117 -0
- package/dist/budget/index.js +23 -0
- package/dist/budget/index.js.map +1 -0
- package/dist/chunk-4IUL7ECC.js +3152 -0
- package/dist/chunk-4IUL7ECC.js.map +1 -0
- package/dist/chunk-5GAUWC3L.js +469 -0
- package/dist/chunk-5GAUWC3L.js.map +1 -0
- package/dist/chunk-6A37SKAJ.js +58 -0
- package/dist/chunk-6A37SKAJ.js.map +1 -0
- package/dist/chunk-7C7SC4TZ.js +358 -0
- package/dist/chunk-7C7SC4TZ.js.map +1 -0
- package/dist/chunk-CJAJ4MBO.js +475 -0
- package/dist/chunk-CJAJ4MBO.js.map +1 -0
- package/dist/chunk-LQC5DLT7.js +317 -0
- package/dist/chunk-LQC5DLT7.js.map +1 -0
- package/dist/chunk-OTPXGXO7.js +2368 -0
- package/dist/chunk-OTPXGXO7.js.map +1 -0
- package/dist/chunk-QPVNC7S4.js +1833 -0
- package/dist/chunk-QPVNC7S4.js.map +1 -0
- package/dist/cli/cli.d.ts +13 -0
- package/dist/cli/cli.js +16 -0
- package/dist/cli/cli.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +22 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/completion/index.d.ts +91 -0
- package/dist/completion/index.js +587 -0
- package/dist/completion/index.js.map +1 -0
- package/dist/config-DequKoFA.d.ts +1468 -0
- package/dist/core/index.d.ts +64 -0
- package/dist/core/index.js +87 -0
- package/dist/core/index.js.map +1 -0
- package/dist/dashboard/index.d.ts +14 -0
- package/dist/dashboard/index.js +1253 -0
- package/dist/dashboard/index.js.map +1 -0
- package/dist/discovery/index.d.ts +285 -0
- package/dist/discovery/index.js +50 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/event-bus-KiuR6e3P.d.ts +91 -0
- package/dist/execution/index.d.ts +215 -0
- package/dist/execution/index.js +27 -0
- package/dist/execution/index.js.map +1 -0
- package/dist/repo/index.d.ts +33 -0
- package/dist/repo/index.js +19 -0
- package/dist/repo/index.js.map +1 -0
- package/dist/tracking/index.d.ts +357 -0
- package/dist/tracking/index.js +15 -0
- package/dist/tracking/index.js.map +1 -0
- package/dist/types-CYCwgojB.d.ts +34 -0
- package/dist/types-Ck7IucqK.d.ts +195 -0
- package/docs/config-reference.md +271 -0
- package/docs/multi-agent-support-technical-spec.md +312 -0
- package/package.json +82 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
// src/budget/complexity.ts
|
|
2
|
+
var SOURCE_LOC_BASELINE = {
|
|
3
|
+
lint: 8,
|
|
4
|
+
todo: 16,
|
|
5
|
+
"test-gap": 48,
|
|
6
|
+
"dead-code": 36,
|
|
7
|
+
"github-issue": 88,
|
|
8
|
+
custom: 40
|
|
9
|
+
};
|
|
10
|
+
var SOURCE_COMPLEXITY_SCORE = {
|
|
11
|
+
lint: 0,
|
|
12
|
+
todo: 0,
|
|
13
|
+
"test-gap": 1,
|
|
14
|
+
"dead-code": 1,
|
|
15
|
+
"github-issue": 2,
|
|
16
|
+
custom: 1
|
|
17
|
+
};
|
|
18
|
+
var ESTIMATED_LOC_KEYS = [
|
|
19
|
+
"estimatedLoc",
|
|
20
|
+
"estimatedLOC",
|
|
21
|
+
"estimatedLocChanges",
|
|
22
|
+
"estimatedDiffSize",
|
|
23
|
+
"loc",
|
|
24
|
+
"locChanges",
|
|
25
|
+
"linesChanged",
|
|
26
|
+
"lineCount",
|
|
27
|
+
"diffSize",
|
|
28
|
+
"changeSize"
|
|
29
|
+
];
|
|
30
|
+
function parseNumericValue(value) {
|
|
31
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === "string") {
|
|
35
|
+
const parsed = Number.parseFloat(value);
|
|
36
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return void 0;
|
|
41
|
+
}
|
|
42
|
+
function readMetadataLocEstimate(metadata) {
|
|
43
|
+
for (const key of ESTIMATED_LOC_KEYS) {
|
|
44
|
+
const directValue = parseNumericValue(metadata[key]);
|
|
45
|
+
if (directValue !== void 0) {
|
|
46
|
+
return directValue;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const metrics = metadata.metrics;
|
|
50
|
+
if (metrics && typeof metrics === "object" && !Array.isArray(metrics)) {
|
|
51
|
+
const metricsRecord = metrics;
|
|
52
|
+
for (const key of ESTIMATED_LOC_KEYS) {
|
|
53
|
+
const metricValue = parseNumericValue(metricsRecord[key]);
|
|
54
|
+
if (metricValue !== void 0) {
|
|
55
|
+
return metricValue;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return void 0;
|
|
60
|
+
}
|
|
61
|
+
function estimateLocChanges(task) {
|
|
62
|
+
const metadataEstimate = readMetadataLocEstimate(task.metadata);
|
|
63
|
+
if (metadataEstimate !== void 0) {
|
|
64
|
+
return Math.max(1, Math.round(metadataEstimate));
|
|
65
|
+
}
|
|
66
|
+
const sourceBaseline = SOURCE_LOC_BASELINE[task.source] ?? SOURCE_LOC_BASELINE.custom;
|
|
67
|
+
const fileAdjustment = Math.max(task.targetFiles.length, 1) * 8;
|
|
68
|
+
return Math.max(sourceBaseline, fileAdjustment);
|
|
69
|
+
}
|
|
70
|
+
function analyzeTaskComplexity(task) {
|
|
71
|
+
const fileCount = task.targetFiles.length;
|
|
72
|
+
const locChanges = estimateLocChanges(task);
|
|
73
|
+
const fileScore = fileCount <= 1 ? 0 : fileCount <= 3 ? 1 : fileCount <= 6 ? 2 : 3;
|
|
74
|
+
const locScore = locChanges <= 20 ? 0 : locChanges <= 80 ? 1 : locChanges <= 200 ? 2 : 3;
|
|
75
|
+
const sourceScore = SOURCE_COMPLEXITY_SCORE[task.source] ?? SOURCE_COMPLEXITY_SCORE.custom;
|
|
76
|
+
const totalScore = fileScore + locScore + sourceScore;
|
|
77
|
+
if (totalScore <= 1) {
|
|
78
|
+
return "trivial";
|
|
79
|
+
}
|
|
80
|
+
if (totalScore <= 3) {
|
|
81
|
+
return "simple";
|
|
82
|
+
}
|
|
83
|
+
if (totalScore <= 6) {
|
|
84
|
+
return "moderate";
|
|
85
|
+
}
|
|
86
|
+
return "complex";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/budget/estimator.ts
|
|
90
|
+
import { readFile } from "fs/promises";
|
|
91
|
+
import { isAbsolute, resolve } from "path";
|
|
92
|
+
import PQueue from "p-queue";
|
|
93
|
+
|
|
94
|
+
// src/budget/providers/claude-counter.ts
|
|
95
|
+
import { get_encoding } from "tiktoken";
|
|
96
|
+
var CLAUDE_ENCODING = "cl100k_base";
|
|
97
|
+
var CLAUDE_INVOCATION_OVERHEAD = 1500;
|
|
98
|
+
var CLAUDE_MAX_CONTEXT_TOKENS = 2e5;
|
|
99
|
+
var encoder;
|
|
100
|
+
function getEncoder() {
|
|
101
|
+
encoder ??= get_encoding(CLAUDE_ENCODING);
|
|
102
|
+
return encoder;
|
|
103
|
+
}
|
|
104
|
+
var ClaudeTokenCounter = class {
|
|
105
|
+
invocationOverhead = CLAUDE_INVOCATION_OVERHEAD;
|
|
106
|
+
maxContextTokens = CLAUDE_MAX_CONTEXT_TOKENS;
|
|
107
|
+
encoding = CLAUDE_ENCODING;
|
|
108
|
+
countTokens(text) {
|
|
109
|
+
return getEncoder().encode(text).length;
|
|
110
|
+
}
|
|
111
|
+
/** Free the cached tiktoken encoder so it can be re-created on next use. */
|
|
112
|
+
reset() {
|
|
113
|
+
if (encoder) {
|
|
114
|
+
encoder.free();
|
|
115
|
+
encoder = void 0;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// src/budget/providers/codex-counter.ts
|
|
121
|
+
import { get_encoding as get_encoding2 } from "tiktoken";
|
|
122
|
+
var CODEX_INVOCATION_OVERHEAD = 1e3;
|
|
123
|
+
var CODEX_MAX_CONTEXT_TOKENS = 2e5;
|
|
124
|
+
var PRIMARY_ENCODING = "o200k_base";
|
|
125
|
+
var FALLBACK_ENCODING = "cl100k_base";
|
|
126
|
+
var encoder2;
|
|
127
|
+
var selectedEncoding;
|
|
128
|
+
function getEncoder2() {
|
|
129
|
+
if (encoder2) {
|
|
130
|
+
return encoder2;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
encoder2 = get_encoding2(PRIMARY_ENCODING);
|
|
134
|
+
selectedEncoding = PRIMARY_ENCODING;
|
|
135
|
+
} catch {
|
|
136
|
+
encoder2 = get_encoding2(FALLBACK_ENCODING);
|
|
137
|
+
selectedEncoding = FALLBACK_ENCODING;
|
|
138
|
+
}
|
|
139
|
+
return encoder2;
|
|
140
|
+
}
|
|
141
|
+
var CodexTokenCounter = class {
|
|
142
|
+
invocationOverhead = CODEX_INVOCATION_OVERHEAD;
|
|
143
|
+
maxContextTokens = CODEX_MAX_CONTEXT_TOKENS;
|
|
144
|
+
get encoding() {
|
|
145
|
+
getEncoder2();
|
|
146
|
+
return selectedEncoding ?? FALLBACK_ENCODING;
|
|
147
|
+
}
|
|
148
|
+
countTokens(text) {
|
|
149
|
+
return getEncoder2().encode(text).length;
|
|
150
|
+
}
|
|
151
|
+
/** Free the cached tiktoken encoder so it can be re-created on next use. */
|
|
152
|
+
reset() {
|
|
153
|
+
if (encoder2) {
|
|
154
|
+
encoder2.free();
|
|
155
|
+
encoder2 = void 0;
|
|
156
|
+
}
|
|
157
|
+
selectedEncoding = void 0;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// src/budget/estimator.ts
|
|
162
|
+
var ESTIMATION_PADDING_MULTIPLIER = 1.2;
|
|
163
|
+
var FALLBACK_CONFIDENCE = 0.5;
|
|
164
|
+
var COMPLEXITY_MULTIPLIERS = {
|
|
165
|
+
trivial: 0.5,
|
|
166
|
+
simple: 1,
|
|
167
|
+
moderate: 2,
|
|
168
|
+
complex: 3.5
|
|
169
|
+
};
|
|
170
|
+
var COMPLEXITY_CONFIDENCE = {
|
|
171
|
+
trivial: 0.9,
|
|
172
|
+
simple: 0.75,
|
|
173
|
+
moderate: 0.6,
|
|
174
|
+
complex: 0.4
|
|
175
|
+
};
|
|
176
|
+
var COMPLEXITY_ORDER = {
|
|
177
|
+
trivial: 0,
|
|
178
|
+
simple: 1,
|
|
179
|
+
moderate: 2,
|
|
180
|
+
complex: 3
|
|
181
|
+
};
|
|
182
|
+
var claudeCounter = new ClaudeTokenCounter();
|
|
183
|
+
var codexCounter = new CodexTokenCounter();
|
|
184
|
+
function resetCounters() {
|
|
185
|
+
claudeCounter.reset();
|
|
186
|
+
codexCounter.reset();
|
|
187
|
+
}
|
|
188
|
+
function getTokenCounter(provider) {
|
|
189
|
+
if (provider === "claude-code") {
|
|
190
|
+
return claudeCounter;
|
|
191
|
+
}
|
|
192
|
+
return codexCounter;
|
|
193
|
+
}
|
|
194
|
+
function approximateTokenCount(text) {
|
|
195
|
+
if (text.length === 0) {
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
199
|
+
}
|
|
200
|
+
function countTokensWithFallback(text, counter) {
|
|
201
|
+
try {
|
|
202
|
+
return {
|
|
203
|
+
tokens: counter.countTokens(text),
|
|
204
|
+
usedFallback: false
|
|
205
|
+
};
|
|
206
|
+
} catch {
|
|
207
|
+
return {
|
|
208
|
+
tokens: approximateTokenCount(text),
|
|
209
|
+
usedFallback: true
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function chooseConservativeComplexity(declaredComplexity, analyzedComplexity) {
|
|
214
|
+
return COMPLEXITY_ORDER[declaredComplexity] >= COMPLEXITY_ORDER[analyzedComplexity] ? declaredComplexity : analyzedComplexity;
|
|
215
|
+
}
|
|
216
|
+
function clamp(value, min, max) {
|
|
217
|
+
if (value < min) {
|
|
218
|
+
return min;
|
|
219
|
+
}
|
|
220
|
+
if (value > max) {
|
|
221
|
+
return max;
|
|
222
|
+
}
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
function resolveTargetFilePath(targetFile) {
|
|
226
|
+
return isAbsolute(targetFile) ? targetFile : resolve(process.cwd(), targetFile);
|
|
227
|
+
}
|
|
228
|
+
function safeStringify(value) {
|
|
229
|
+
try {
|
|
230
|
+
const serialized = JSON.stringify(value);
|
|
231
|
+
return serialized ?? "null";
|
|
232
|
+
} catch {
|
|
233
|
+
return "[unserializable]";
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function readContextFile(targetFile, counter) {
|
|
237
|
+
const resolvedPath = resolveTargetFilePath(targetFile);
|
|
238
|
+
try {
|
|
239
|
+
const content = await readFile(resolvedPath, "utf8");
|
|
240
|
+
const counted = countTokensWithFallback(content, counter);
|
|
241
|
+
return {
|
|
242
|
+
...counted,
|
|
243
|
+
missing: false
|
|
244
|
+
};
|
|
245
|
+
} catch {
|
|
246
|
+
return {
|
|
247
|
+
tokens: 0,
|
|
248
|
+
usedFallback: false,
|
|
249
|
+
missing: true
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function estimateTokens(task, provider) {
|
|
254
|
+
const counter = getTokenCounter(provider);
|
|
255
|
+
const uniqueTargetFiles = [...new Set(task.targetFiles)];
|
|
256
|
+
const fileQueue = new PQueue({ concurrency: 50 });
|
|
257
|
+
const fileResults = await Promise.all(
|
|
258
|
+
uniqueTargetFiles.map(
|
|
259
|
+
(targetFile) => fileQueue.add(() => readContextFile(targetFile, counter))
|
|
260
|
+
)
|
|
261
|
+
);
|
|
262
|
+
const repoStructureSeed = uniqueTargetFiles.join("\n");
|
|
263
|
+
const repoStructureCount = countTokensWithFallback(repoStructureSeed, counter);
|
|
264
|
+
const contextTokens = repoStructureCount.tokens + fileResults.reduce((sum, result) => sum + result.tokens, 0);
|
|
265
|
+
const promptSeed = [
|
|
266
|
+
`Task ID: ${task.id}`,
|
|
267
|
+
`Title: ${task.title}`,
|
|
268
|
+
`Source: ${task.source}`,
|
|
269
|
+
`Priority: ${task.priority}`,
|
|
270
|
+
`Description:
|
|
271
|
+
${task.description}`,
|
|
272
|
+
`Target Files:
|
|
273
|
+
${uniqueTargetFiles.join("\n") || "(none)"}`,
|
|
274
|
+
`Metadata: ${safeStringify(task.metadata)}`
|
|
275
|
+
].join("\n\n");
|
|
276
|
+
const promptContentCount = countTokensWithFallback(promptSeed, counter);
|
|
277
|
+
const promptTokens = counter.invocationOverhead + promptContentCount.tokens;
|
|
278
|
+
const analyzedComplexity = analyzeTaskComplexity(task);
|
|
279
|
+
const effectiveComplexity = chooseConservativeComplexity(task.complexity, analyzedComplexity);
|
|
280
|
+
const expectedOutputTokens = Math.ceil(
|
|
281
|
+
contextTokens * COMPLEXITY_MULTIPLIERS[effectiveComplexity]
|
|
282
|
+
);
|
|
283
|
+
const rawTotalTokens = contextTokens + promptTokens + expectedOutputTokens;
|
|
284
|
+
const totalEstimatedTokens = Math.ceil(rawTotalTokens * ESTIMATION_PADDING_MULTIPLIER);
|
|
285
|
+
const usedFallback = repoStructureCount.usedFallback || promptContentCount.usedFallback || fileResults.some((result) => result.usedFallback);
|
|
286
|
+
const missingFileCount = fileResults.filter((result) => result.missing).length;
|
|
287
|
+
let confidence = COMPLEXITY_CONFIDENCE[effectiveComplexity];
|
|
288
|
+
if (usedFallback) {
|
|
289
|
+
confidence = Math.min(confidence, FALLBACK_CONFIDENCE);
|
|
290
|
+
}
|
|
291
|
+
if (missingFileCount > 0) {
|
|
292
|
+
confidence -= Math.min(0.25, missingFileCount * 0.05);
|
|
293
|
+
}
|
|
294
|
+
if (uniqueTargetFiles.length === 0) {
|
|
295
|
+
confidence -= 0.1;
|
|
296
|
+
}
|
|
297
|
+
if (task.complexity !== analyzedComplexity) {
|
|
298
|
+
confidence -= 0.05;
|
|
299
|
+
}
|
|
300
|
+
const feasible = totalEstimatedTokens <= counter.maxContextTokens;
|
|
301
|
+
return {
|
|
302
|
+
taskId: task.id,
|
|
303
|
+
providerId: provider,
|
|
304
|
+
contextTokens,
|
|
305
|
+
promptTokens,
|
|
306
|
+
expectedOutputTokens,
|
|
307
|
+
totalEstimatedTokens,
|
|
308
|
+
confidence: clamp(confidence, 0.1, 0.95),
|
|
309
|
+
feasible
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
var EPIC_CONTEXT_OVERHEAD = 1.2;
|
|
313
|
+
async function estimateEpicTokens(epic, provider) {
|
|
314
|
+
if (epic.subtasks.length === 0) {
|
|
315
|
+
return 0;
|
|
316
|
+
}
|
|
317
|
+
const epicQueue = new PQueue({ concurrency: 10 });
|
|
318
|
+
const subtaskEstimates = await Promise.all(
|
|
319
|
+
epic.subtasks.map(
|
|
320
|
+
(task) => epicQueue.add(() => estimateTokens(task, provider))
|
|
321
|
+
)
|
|
322
|
+
);
|
|
323
|
+
const subtaskTotal = subtaskEstimates.reduce(
|
|
324
|
+
(sum, estimate) => sum + estimate.totalEstimatedTokens,
|
|
325
|
+
0
|
|
326
|
+
);
|
|
327
|
+
return Math.ceil(subtaskTotal * EPIC_CONTEXT_OVERHEAD);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/budget/planner.ts
|
|
331
|
+
var DEFAULT_RESERVE_PERCENT = 0.1;
|
|
332
|
+
var MIN_CONFIDENCE_THRESHOLD = 0.5;
|
|
333
|
+
var TOO_COMPLEX_BUDGET_SHARE = 0.6;
|
|
334
|
+
function normalizeBudget(budget) {
|
|
335
|
+
if (!Number.isFinite(budget) || budget <= 0) {
|
|
336
|
+
return 0;
|
|
337
|
+
}
|
|
338
|
+
return Math.floor(budget);
|
|
339
|
+
}
|
|
340
|
+
function getFallbackEstimate(task) {
|
|
341
|
+
return {
|
|
342
|
+
taskId: task.id,
|
|
343
|
+
providerId: "unknown",
|
|
344
|
+
contextTokens: 0,
|
|
345
|
+
promptTokens: 0,
|
|
346
|
+
expectedOutputTokens: 0,
|
|
347
|
+
totalEstimatedTokens: Number.MAX_SAFE_INTEGER,
|
|
348
|
+
confidence: 0,
|
|
349
|
+
feasible: false
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function classifyDeferredReason(task, estimate, effectiveBudget) {
|
|
353
|
+
if (!estimate.feasible) {
|
|
354
|
+
return "budget_exceeded";
|
|
355
|
+
}
|
|
356
|
+
if (estimate.confidence < MIN_CONFIDENCE_THRESHOLD) {
|
|
357
|
+
return "low_confidence";
|
|
358
|
+
}
|
|
359
|
+
if (task.complexity === "complex" && effectiveBudget > 0 && estimate.totalEstimatedTokens > effectiveBudget * TOO_COMPLEX_BUDGET_SHARE) {
|
|
360
|
+
return "too_complex";
|
|
361
|
+
}
|
|
362
|
+
return void 0;
|
|
363
|
+
}
|
|
364
|
+
function scoreByPriorityPerToken(task, estimate) {
|
|
365
|
+
if (estimate.totalEstimatedTokens <= 0) {
|
|
366
|
+
return task.priority;
|
|
367
|
+
}
|
|
368
|
+
return task.priority / estimate.totalEstimatedTokens;
|
|
369
|
+
}
|
|
370
|
+
function buildExecutionPlan(tasks, estimates, budget) {
|
|
371
|
+
const totalBudget = normalizeBudget(budget);
|
|
372
|
+
const reserveTokens = Math.floor(totalBudget * DEFAULT_RESERVE_PERCENT);
|
|
373
|
+
const effectiveBudget = Math.max(0, totalBudget - reserveTokens);
|
|
374
|
+
const deferredTasks = [];
|
|
375
|
+
const candidates = [];
|
|
376
|
+
for (const task of tasks) {
|
|
377
|
+
const estimate = estimates.get(task.id) ?? getFallbackEstimate(task);
|
|
378
|
+
const reason = classifyDeferredReason(task, estimate, effectiveBudget);
|
|
379
|
+
if (reason) {
|
|
380
|
+
deferredTasks.push({ task, estimate, reason });
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
candidates.push({ task, estimate });
|
|
384
|
+
}
|
|
385
|
+
candidates.sort((left, right) => {
|
|
386
|
+
const ratioDifference = scoreByPriorityPerToken(right.task, right.estimate) - scoreByPriorityPerToken(left.task, left.estimate);
|
|
387
|
+
if (ratioDifference !== 0) {
|
|
388
|
+
return ratioDifference;
|
|
389
|
+
}
|
|
390
|
+
const priorityDifference = right.task.priority - left.task.priority;
|
|
391
|
+
if (priorityDifference !== 0) {
|
|
392
|
+
return priorityDifference;
|
|
393
|
+
}
|
|
394
|
+
return left.estimate.totalEstimatedTokens - right.estimate.totalEstimatedTokens;
|
|
395
|
+
});
|
|
396
|
+
const selectedTasks = [];
|
|
397
|
+
let budgetUsed = 0;
|
|
398
|
+
for (const candidate of candidates) {
|
|
399
|
+
const nextBudgetUsed = budgetUsed + candidate.estimate.totalEstimatedTokens;
|
|
400
|
+
if (nextBudgetUsed <= effectiveBudget) {
|
|
401
|
+
budgetUsed = nextBudgetUsed;
|
|
402
|
+
selectedTasks.push({
|
|
403
|
+
task: candidate.task,
|
|
404
|
+
estimate: candidate.estimate,
|
|
405
|
+
cumulativeBudgetUsed: budgetUsed
|
|
406
|
+
});
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
deferredTasks.push({
|
|
410
|
+
task: candidate.task,
|
|
411
|
+
estimate: candidate.estimate,
|
|
412
|
+
reason: "budget_exceeded"
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
totalBudget,
|
|
417
|
+
selectedTasks,
|
|
418
|
+
deferredTasks,
|
|
419
|
+
reserveTokens,
|
|
420
|
+
remainingTokens: Math.max(0, effectiveBudget - budgetUsed)
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function buildEpicExecutionPlan(epics, budget) {
|
|
424
|
+
const totalBudget = normalizeBudget(budget);
|
|
425
|
+
const reserveTokens = Math.floor(totalBudget * DEFAULT_RESERVE_PERCENT);
|
|
426
|
+
const effectiveBudget = Math.max(0, totalBudget - reserveTokens);
|
|
427
|
+
const sorted = [...epics].sort((a, b) => b.priority - a.priority);
|
|
428
|
+
const selectedEpics = [];
|
|
429
|
+
const deferredEpics = [];
|
|
430
|
+
let budgetUsed = 0;
|
|
431
|
+
for (const epic of sorted) {
|
|
432
|
+
const tokens = epic.estimatedTokens;
|
|
433
|
+
const nextBudgetUsed = budgetUsed + tokens;
|
|
434
|
+
if (nextBudgetUsed <= effectiveBudget) {
|
|
435
|
+
budgetUsed = nextBudgetUsed;
|
|
436
|
+
selectedEpics.push({
|
|
437
|
+
epic,
|
|
438
|
+
estimatedTokens: tokens,
|
|
439
|
+
cumulativeBudgetUsed: budgetUsed
|
|
440
|
+
});
|
|
441
|
+
} else {
|
|
442
|
+
deferredEpics.push({
|
|
443
|
+
epic,
|
|
444
|
+
estimatedTokens: tokens,
|
|
445
|
+
reason: "budget_exceeded"
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
totalBudget,
|
|
451
|
+
selectedEpics,
|
|
452
|
+
deferredEpics,
|
|
453
|
+
reserveTokens,
|
|
454
|
+
remainingTokens: Math.max(0, effectiveBudget - budgetUsed)
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export {
|
|
459
|
+
estimateLocChanges,
|
|
460
|
+
analyzeTaskComplexity,
|
|
461
|
+
ClaudeTokenCounter,
|
|
462
|
+
CodexTokenCounter,
|
|
463
|
+
resetCounters,
|
|
464
|
+
estimateTokens,
|
|
465
|
+
estimateEpicTokens,
|
|
466
|
+
buildExecutionPlan,
|
|
467
|
+
buildEpicExecutionPlan
|
|
468
|
+
};
|
|
469
|
+
//# sourceMappingURL=chunk-5GAUWC3L.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/budget/complexity.ts","../src/budget/estimator.ts","../src/budget/providers/claude-counter.ts","../src/budget/providers/codex-counter.ts","../src/budget/planner.ts"],"sourcesContent":["import type { Task, TaskComplexity, TaskSource } from \"./estimator.js\";\n\nconst SOURCE_LOC_BASELINE: Record<TaskSource, number> = {\n lint: 8,\n todo: 16,\n \"test-gap\": 48,\n \"dead-code\": 36,\n \"github-issue\": 88,\n custom: 40,\n};\n\nconst SOURCE_COMPLEXITY_SCORE: Record<TaskSource, number> = {\n lint: 0,\n todo: 0,\n \"test-gap\": 1,\n \"dead-code\": 1,\n \"github-issue\": 2,\n custom: 1,\n};\n\nconst ESTIMATED_LOC_KEYS = [\n \"estimatedLoc\",\n \"estimatedLOC\",\n \"estimatedLocChanges\",\n \"estimatedDiffSize\",\n \"loc\",\n \"locChanges\",\n \"linesChanged\",\n \"lineCount\",\n \"diffSize\",\n \"changeSize\",\n] as const;\n\nfunction parseNumericValue(value: unknown): number | undefined {\n if (typeof value === \"number\" && Number.isFinite(value) && value >= 0) {\n return value;\n }\n\n if (typeof value === \"string\") {\n const parsed = Number.parseFloat(value);\n if (Number.isFinite(parsed) && parsed >= 0) {\n return parsed;\n }\n }\n\n return undefined;\n}\n\nfunction readMetadataLocEstimate(metadata: Record<string, unknown>): number | undefined {\n for (const key of ESTIMATED_LOC_KEYS) {\n const directValue = parseNumericValue(metadata[key]);\n if (directValue !== undefined) {\n return directValue;\n }\n }\n\n const metrics = metadata.metrics;\n if (metrics && typeof metrics === \"object\" && !Array.isArray(metrics)) {\n const metricsRecord = metrics as Record<string, unknown>;\n for (const key of ESTIMATED_LOC_KEYS) {\n const metricValue = parseNumericValue(metricsRecord[key]);\n if (metricValue !== undefined) {\n return metricValue;\n }\n }\n }\n\n return undefined;\n}\n\nexport function estimateLocChanges(task: Task): number {\n const metadataEstimate = readMetadataLocEstimate(task.metadata);\n if (metadataEstimate !== undefined) {\n return Math.max(1, Math.round(metadataEstimate));\n }\n\n const sourceBaseline = SOURCE_LOC_BASELINE[task.source] ?? SOURCE_LOC_BASELINE.custom;\n const fileAdjustment = Math.max(task.targetFiles.length, 1) * 8;\n\n return Math.max(sourceBaseline, fileAdjustment);\n}\n\nexport function analyzeTaskComplexity(task: Task): TaskComplexity {\n const fileCount = task.targetFiles.length;\n const locChanges = estimateLocChanges(task);\n\n const fileScore = fileCount <= 1 ? 0 : fileCount <= 3 ? 1 : fileCount <= 6 ? 2 : 3;\n const locScore = locChanges <= 20 ? 0 : locChanges <= 80 ? 1 : locChanges <= 200 ? 2 : 3;\n const sourceScore = SOURCE_COMPLEXITY_SCORE[task.source] ?? SOURCE_COMPLEXITY_SCORE.custom;\n\n const totalScore = fileScore + locScore + sourceScore;\n\n if (totalScore <= 1) {\n return \"trivial\";\n }\n\n if (totalScore <= 3) {\n return \"simple\";\n }\n\n if (totalScore <= 6) {\n return \"moderate\";\n }\n\n return \"complex\";\n}\n","import { readFile } from \"node:fs/promises\";\nimport { isAbsolute, resolve } from \"node:path\";\n\nimport PQueue from \"p-queue\";\nimport type { Epic } from \"../core/types.js\";\nimport { analyzeTaskComplexity } from \"./complexity.js\";\nimport { ClaudeTokenCounter } from \"./providers/claude-counter.js\";\nimport { CodexTokenCounter } from \"./providers/codex-counter.js\";\n\nexport type AgentProviderId = \"claude-code\" | \"codex\" | \"opencode\" | string;\n\nexport type TaskSource = \"lint\" | \"todo\" | \"test-gap\" | \"dead-code\" | \"github-issue\" | \"custom\";\n\nexport type TaskComplexity = \"trivial\" | \"simple\" | \"moderate\" | \"complex\";\n\nexport type ExecutionMode = \"new-pr\" | \"update-pr\" | \"direct-commit\";\n\nexport interface Task {\n id: string;\n source: TaskSource;\n title: string;\n description: string;\n targetFiles: string[];\n priority: number;\n complexity: TaskComplexity;\n executionMode: ExecutionMode;\n linkedIssue?: {\n number: number;\n url: string;\n labels: string[];\n };\n metadata: Record<string, unknown>;\n discoveredAt: string;\n}\n\nexport interface TokenEstimate {\n taskId: string;\n providerId: AgentProviderId;\n contextTokens: number;\n promptTokens: number;\n expectedOutputTokens: number;\n totalEstimatedTokens: number;\n confidence: number;\n feasible: boolean;\n estimatedCostUsd?: number;\n}\n\nexport interface TokenCounter {\n countTokens(text: string): number;\n readonly invocationOverhead: number;\n readonly maxContextTokens: number;\n /** Free any cached resources (e.g. tiktoken encoders). Optional. */\n reset?(): void;\n}\n\ninterface TokenCountResult {\n tokens: number;\n usedFallback: boolean;\n}\n\ninterface ContextFileResult extends TokenCountResult {\n missing: boolean;\n}\n\nconst ESTIMATION_PADDING_MULTIPLIER = 1.2;\nconst FALLBACK_CONFIDENCE = 0.5;\n\nconst COMPLEXITY_MULTIPLIERS: Record<TaskComplexity, number> = {\n trivial: 0.5,\n simple: 1,\n moderate: 2,\n complex: 3.5,\n};\n\nconst COMPLEXITY_CONFIDENCE: Record<TaskComplexity, number> = {\n trivial: 0.9,\n simple: 0.75,\n moderate: 0.6,\n complex: 0.4,\n};\n\nconst COMPLEXITY_ORDER: Record<TaskComplexity, number> = {\n trivial: 0,\n simple: 1,\n moderate: 2,\n complex: 3,\n};\n\nconst claudeCounter = new ClaudeTokenCounter();\nconst codexCounter = new CodexTokenCounter();\n\n/**\n * Free cached tiktoken encoders held by the module-level counter singletons.\n * Useful for reclaiming memory in long-lived processes or between test runs.\n */\nexport function resetCounters(): void {\n claudeCounter.reset();\n codexCounter.reset();\n}\n\nfunction getTokenCounter(provider: AgentProviderId): TokenCounter {\n if (provider === \"claude-code\") {\n return claudeCounter;\n }\n\n return codexCounter;\n}\n\nfunction approximateTokenCount(text: string): number {\n if (text.length === 0) {\n return 0;\n }\n\n return Math.max(1, Math.ceil(text.length / 4));\n}\n\nfunction countTokensWithFallback(text: string, counter: TokenCounter): TokenCountResult {\n try {\n return {\n tokens: counter.countTokens(text),\n usedFallback: false,\n };\n } catch {\n return {\n tokens: approximateTokenCount(text),\n usedFallback: true,\n };\n }\n}\n\nfunction chooseConservativeComplexity(\n declaredComplexity: TaskComplexity,\n analyzedComplexity: TaskComplexity,\n): TaskComplexity {\n return COMPLEXITY_ORDER[declaredComplexity] >= COMPLEXITY_ORDER[analyzedComplexity]\n ? declaredComplexity\n : analyzedComplexity;\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n if (value < min) {\n return min;\n }\n\n if (value > max) {\n return max;\n }\n\n return value;\n}\n\nfunction resolveTargetFilePath(targetFile: string): string {\n return isAbsolute(targetFile) ? targetFile : resolve(process.cwd(), targetFile);\n}\n\nfunction safeStringify(value: unknown): string {\n try {\n const serialized = JSON.stringify(value);\n return serialized ?? \"null\";\n } catch {\n return \"[unserializable]\";\n }\n}\n\nasync function readContextFile(\n targetFile: string,\n counter: TokenCounter,\n): Promise<ContextFileResult> {\n const resolvedPath = resolveTargetFilePath(targetFile);\n\n try {\n const content = await readFile(resolvedPath, \"utf8\");\n const counted = countTokensWithFallback(content, counter);\n\n return {\n ...counted,\n missing: false,\n };\n } catch {\n return {\n tokens: 0,\n usedFallback: false,\n missing: true,\n };\n }\n}\n\nexport async function estimateTokens(\n task: Task,\n provider: AgentProviderId,\n): Promise<TokenEstimate> {\n const counter = getTokenCounter(provider);\n const uniqueTargetFiles = [...new Set(task.targetFiles)];\n\n const fileQueue = new PQueue({ concurrency: 50 });\n const fileResults = await Promise.all(\n uniqueTargetFiles.map((targetFile) =>\n fileQueue.add(() => readContextFile(targetFile, counter)) as Promise<ContextFileResult>,\n ),\n );\n\n const repoStructureSeed = uniqueTargetFiles.join(\"\\n\");\n const repoStructureCount = countTokensWithFallback(repoStructureSeed, counter);\n\n const contextTokens =\n repoStructureCount.tokens + fileResults.reduce((sum, result) => sum + result.tokens, 0);\n\n const promptSeed = [\n `Task ID: ${task.id}`,\n `Title: ${task.title}`,\n `Source: ${task.source}`,\n `Priority: ${task.priority}`,\n `Description:\\n${task.description}`,\n `Target Files:\\n${uniqueTargetFiles.join(\"\\n\") || \"(none)\"}`,\n `Metadata: ${safeStringify(task.metadata)}`,\n ].join(\"\\n\\n\");\n\n const promptContentCount = countTokensWithFallback(promptSeed, counter);\n const promptTokens = counter.invocationOverhead + promptContentCount.tokens;\n\n const analyzedComplexity = analyzeTaskComplexity(task);\n const effectiveComplexity = chooseConservativeComplexity(task.complexity, analyzedComplexity);\n const expectedOutputTokens = Math.ceil(\n contextTokens * COMPLEXITY_MULTIPLIERS[effectiveComplexity],\n );\n\n const rawTotalTokens = contextTokens + promptTokens + expectedOutputTokens;\n const totalEstimatedTokens = Math.ceil(rawTotalTokens * ESTIMATION_PADDING_MULTIPLIER);\n\n const usedFallback =\n repoStructureCount.usedFallback ||\n promptContentCount.usedFallback ||\n fileResults.some((result) => result.usedFallback);\n\n const missingFileCount = fileResults.filter((result) => result.missing).length;\n\n let confidence = COMPLEXITY_CONFIDENCE[effectiveComplexity];\n if (usedFallback) {\n confidence = Math.min(confidence, FALLBACK_CONFIDENCE);\n }\n\n if (missingFileCount > 0) {\n confidence -= Math.min(0.25, missingFileCount * 0.05);\n }\n\n if (uniqueTargetFiles.length === 0) {\n confidence -= 0.1;\n }\n\n if (task.complexity !== analyzedComplexity) {\n confidence -= 0.05;\n }\n\n const feasible = totalEstimatedTokens <= counter.maxContextTokens;\n\n return {\n taskId: task.id,\n providerId: provider,\n contextTokens,\n promptTokens,\n expectedOutputTokens,\n totalEstimatedTokens,\n confidence: clamp(confidence, 0.1, 0.95),\n feasible,\n };\n}\n\n// ── Epic token estimation ────────────────────────────────────\n\nconst EPIC_CONTEXT_OVERHEAD = 1.2; // 20% overhead for shared module understanding\n\n/**\n * Estimate tokens for an entire epic by summing subtask estimates\n * plus a 20% context overhead for shared module understanding.\n */\nexport async function estimateEpicTokens(epic: Epic, provider: AgentProviderId): Promise<number> {\n if (epic.subtasks.length === 0) {\n return 0;\n }\n\n const epicQueue = new PQueue({ concurrency: 10 });\n const subtaskEstimates = await Promise.all(\n epic.subtasks.map((task) =>\n epicQueue.add(() => estimateTokens(task, provider)) as Promise<TokenEstimate>,\n ),\n );\n\n const subtaskTotal = subtaskEstimates.reduce(\n (sum, estimate) => sum + estimate.totalEstimatedTokens,\n 0,\n );\n\n return Math.ceil(subtaskTotal * EPIC_CONTEXT_OVERHEAD);\n}\n","import { type Tiktoken, get_encoding } from \"tiktoken\";\n\nconst CLAUDE_ENCODING = \"cl100k_base\";\nconst CLAUDE_INVOCATION_OVERHEAD = 1_500;\nconst CLAUDE_MAX_CONTEXT_TOKENS = 200_000;\n\nlet encoder: Tiktoken | undefined;\n\nfunction getEncoder(): Tiktoken {\n encoder ??= get_encoding(CLAUDE_ENCODING);\n return encoder;\n}\n\nexport class ClaudeTokenCounter {\n readonly invocationOverhead = CLAUDE_INVOCATION_OVERHEAD;\n readonly maxContextTokens = CLAUDE_MAX_CONTEXT_TOKENS;\n readonly encoding = CLAUDE_ENCODING;\n\n countTokens(text: string): number {\n return getEncoder().encode(text).length;\n }\n\n /** Free the cached tiktoken encoder so it can be re-created on next use. */\n reset(): void {\n if (encoder) {\n encoder.free();\n encoder = undefined;\n }\n }\n}\n","import { type Tiktoken, get_encoding } from \"tiktoken\";\n\nconst CODEX_INVOCATION_OVERHEAD = 1_000;\nconst CODEX_MAX_CONTEXT_TOKENS = 200_000;\nconst PRIMARY_ENCODING = \"o200k_base\";\nconst FALLBACK_ENCODING = \"cl100k_base\";\n\ntype SupportedCodexEncoding = typeof PRIMARY_ENCODING | typeof FALLBACK_ENCODING;\n\nlet encoder: Tiktoken | undefined;\nlet selectedEncoding: SupportedCodexEncoding | undefined;\n\nfunction getEncoder(): Tiktoken {\n if (encoder) {\n return encoder;\n }\n\n try {\n encoder = get_encoding(PRIMARY_ENCODING);\n selectedEncoding = PRIMARY_ENCODING;\n } catch {\n encoder = get_encoding(FALLBACK_ENCODING);\n selectedEncoding = FALLBACK_ENCODING;\n }\n\n return encoder;\n}\n\nexport class CodexTokenCounter {\n readonly invocationOverhead = CODEX_INVOCATION_OVERHEAD;\n readonly maxContextTokens = CODEX_MAX_CONTEXT_TOKENS;\n\n get encoding(): SupportedCodexEncoding {\n getEncoder();\n return selectedEncoding ?? FALLBACK_ENCODING;\n }\n\n countTokens(text: string): number {\n return getEncoder().encode(text).length;\n }\n\n /** Free the cached tiktoken encoder so it can be re-created on next use. */\n reset(): void {\n if (encoder) {\n encoder.free();\n encoder = undefined;\n }\n selectedEncoding = undefined;\n }\n}\n","import type { Epic } from \"../core/types.js\";\nimport type { AgentProviderId, Task, TokenEstimate } from \"./estimator.js\";\n\nconst DEFAULT_RESERVE_PERCENT = 0.1;\nconst MIN_CONFIDENCE_THRESHOLD = 0.5;\nconst TOO_COMPLEX_BUDGET_SHARE = 0.6;\n\ntype DeferredReason = \"budget_exceeded\" | \"low_confidence\" | \"too_complex\";\n\nexport interface ExecutionPlan {\n totalBudget: number;\n selectedTasks: Array<{\n task: Task;\n estimate: TokenEstimate;\n cumulativeBudgetUsed: number;\n }>;\n deferredTasks: Array<{\n task: Task;\n estimate: TokenEstimate;\n reason: DeferredReason;\n }>;\n reserveTokens: number;\n remainingTokens: number;\n}\n\ninterface CandidateTask {\n task: Task;\n estimate: TokenEstimate;\n}\n\nfunction normalizeBudget(budget: number): number {\n if (!Number.isFinite(budget) || budget <= 0) {\n return 0;\n }\n\n return Math.floor(budget);\n}\n\nfunction getFallbackEstimate(task: Task): TokenEstimate {\n return {\n taskId: task.id,\n providerId: \"unknown\" as AgentProviderId,\n contextTokens: 0,\n promptTokens: 0,\n expectedOutputTokens: 0,\n totalEstimatedTokens: Number.MAX_SAFE_INTEGER,\n confidence: 0,\n feasible: false,\n };\n}\n\nfunction classifyDeferredReason(\n task: Task,\n estimate: TokenEstimate,\n effectiveBudget: number,\n): DeferredReason | undefined {\n if (!estimate.feasible) {\n return \"budget_exceeded\";\n }\n\n if (estimate.confidence < MIN_CONFIDENCE_THRESHOLD) {\n return \"low_confidence\";\n }\n\n if (\n task.complexity === \"complex\" &&\n effectiveBudget > 0 &&\n estimate.totalEstimatedTokens > effectiveBudget * TOO_COMPLEX_BUDGET_SHARE\n ) {\n return \"too_complex\";\n }\n\n return undefined;\n}\n\nfunction scoreByPriorityPerToken(task: Task, estimate: TokenEstimate): number {\n if (estimate.totalEstimatedTokens <= 0) {\n return task.priority;\n }\n\n return task.priority / estimate.totalEstimatedTokens;\n}\n\nexport function buildExecutionPlan(\n tasks: Task[],\n estimates: Map<string, TokenEstimate>,\n budget: number,\n): ExecutionPlan {\n const totalBudget = normalizeBudget(budget);\n const reserveTokens = Math.floor(totalBudget * DEFAULT_RESERVE_PERCENT);\n const effectiveBudget = Math.max(0, totalBudget - reserveTokens);\n\n const deferredTasks: ExecutionPlan[\"deferredTasks\"] = [];\n const candidates: CandidateTask[] = [];\n\n for (const task of tasks) {\n const estimate = estimates.get(task.id) ?? getFallbackEstimate(task);\n const reason = classifyDeferredReason(task, estimate, effectiveBudget);\n\n if (reason) {\n deferredTasks.push({ task, estimate, reason });\n continue;\n }\n\n candidates.push({ task, estimate });\n }\n\n candidates.sort((left, right) => {\n const ratioDifference =\n scoreByPriorityPerToken(right.task, right.estimate) -\n scoreByPriorityPerToken(left.task, left.estimate);\n\n if (ratioDifference !== 0) {\n return ratioDifference;\n }\n\n const priorityDifference = right.task.priority - left.task.priority;\n if (priorityDifference !== 0) {\n return priorityDifference;\n }\n\n return left.estimate.totalEstimatedTokens - right.estimate.totalEstimatedTokens;\n });\n\n const selectedTasks: ExecutionPlan[\"selectedTasks\"] = [];\n let budgetUsed = 0;\n\n for (const candidate of candidates) {\n const nextBudgetUsed = budgetUsed + candidate.estimate.totalEstimatedTokens;\n\n if (nextBudgetUsed <= effectiveBudget) {\n budgetUsed = nextBudgetUsed;\n selectedTasks.push({\n task: candidate.task,\n estimate: candidate.estimate,\n cumulativeBudgetUsed: budgetUsed,\n });\n continue;\n }\n\n deferredTasks.push({\n task: candidate.task,\n estimate: candidate.estimate,\n reason: \"budget_exceeded\",\n });\n }\n\n return {\n totalBudget,\n selectedTasks,\n deferredTasks,\n reserveTokens,\n remainingTokens: Math.max(0, effectiveBudget - budgetUsed),\n };\n}\n\n// ── Epic execution plan ─────────────────────────────────────\n\nexport interface EpicExecutionPlan {\n totalBudget: number;\n selectedEpics: Array<{\n epic: Epic;\n estimatedTokens: number;\n cumulativeBudgetUsed: number;\n }>;\n deferredEpics: Array<{\n epic: Epic;\n estimatedTokens: number;\n reason: \"budget_exceeded\";\n }>;\n reserveTokens: number;\n remainingTokens: number;\n}\n\n/**\n * Build an execution plan for epics. Unlike task-level planning, epics are\n * sorted purely by priority (not priority/tokens ratio) since they are\n * already coherent units of work.\n */\nexport function buildEpicExecutionPlan(epics: Epic[], budget: number): EpicExecutionPlan {\n const totalBudget = normalizeBudget(budget);\n const reserveTokens = Math.floor(totalBudget * DEFAULT_RESERVE_PERCENT);\n const effectiveBudget = Math.max(0, totalBudget - reserveTokens);\n\n // Sort by priority descending (highest priority first)\n const sorted = [...epics].sort((a, b) => b.priority - a.priority);\n\n const selectedEpics: EpicExecutionPlan[\"selectedEpics\"] = [];\n const deferredEpics: EpicExecutionPlan[\"deferredEpics\"] = [];\n let budgetUsed = 0;\n\n for (const epic of sorted) {\n const tokens = epic.estimatedTokens;\n const nextBudgetUsed = budgetUsed + tokens;\n\n if (nextBudgetUsed <= effectiveBudget) {\n budgetUsed = nextBudgetUsed;\n selectedEpics.push({\n epic,\n estimatedTokens: tokens,\n cumulativeBudgetUsed: budgetUsed,\n });\n } else {\n deferredEpics.push({\n epic,\n estimatedTokens: tokens,\n reason: \"budget_exceeded\",\n });\n }\n }\n\n return {\n totalBudget,\n selectedEpics,\n deferredEpics,\n reserveTokens,\n remainingTokens: Math.max(0, effectiveBudget - budgetUsed),\n };\n}\n"],"mappings":"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","names":["get_encoding","encoder","getEncoder"]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// src/core/memory.ts
|
|
2
|
+
var DEFAULT_PRESSURE_RATIO = 0.85;
|
|
3
|
+
function getMemorySnapshot(pressureRatio = DEFAULT_PRESSURE_RATIO) {
|
|
4
|
+
const mem = process.memoryUsage();
|
|
5
|
+
const heapUsedMB = mem.heapUsed / 1048576;
|
|
6
|
+
const heapTotalMB = mem.heapTotal / 1048576;
|
|
7
|
+
const rssUsedMB = mem.rss / 1048576;
|
|
8
|
+
const heapPressure = mem.heapTotal > 0 ? mem.heapUsed / mem.heapTotal : 0;
|
|
9
|
+
return {
|
|
10
|
+
heapUsedMB: Math.round(heapUsedMB * 10) / 10,
|
|
11
|
+
heapTotalMB: Math.round(heapTotalMB * 10) / 10,
|
|
12
|
+
rssUsedMB: Math.round(rssUsedMB * 10) / 10,
|
|
13
|
+
heapPressure: Math.round(heapPressure * 1e3) / 1e3,
|
|
14
|
+
isUnderPressure: heapPressure >= pressureRatio
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function createMemoryMonitor(options) {
|
|
18
|
+
const intervalMs = options.intervalMs ?? 5e3;
|
|
19
|
+
const pressureRatio = options.pressureRatio ?? DEFAULT_PRESSURE_RATIO;
|
|
20
|
+
let wasPressured = false;
|
|
21
|
+
const timer = setInterval(() => {
|
|
22
|
+
const snapshot = getMemorySnapshot(pressureRatio);
|
|
23
|
+
if (snapshot.isUnderPressure && !wasPressured) {
|
|
24
|
+
wasPressured = true;
|
|
25
|
+
options.onPressure(snapshot);
|
|
26
|
+
} else if (!snapshot.isUnderPressure && wasPressured) {
|
|
27
|
+
wasPressured = false;
|
|
28
|
+
options.onRelief?.(snapshot);
|
|
29
|
+
}
|
|
30
|
+
}, intervalMs);
|
|
31
|
+
if (typeof timer === "object" && "unref" in timer) {
|
|
32
|
+
timer.unref();
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
stop() {
|
|
36
|
+
clearInterval(timer);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/core/utils.ts
|
|
42
|
+
function truncate(value, maxLength, ellipsis = "\u2026") {
|
|
43
|
+
if (value.length <= maxLength) {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
return `${value.slice(0, Math.max(0, maxLength - ellipsis.length))}${ellipsis}`;
|
|
47
|
+
}
|
|
48
|
+
function isRecord(value) {
|
|
49
|
+
return typeof value === "object" && value !== null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
getMemorySnapshot,
|
|
54
|
+
createMemoryMonitor,
|
|
55
|
+
truncate,
|
|
56
|
+
isRecord
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=chunk-6A37SKAJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/memory.ts","../src/core/utils.ts"],"sourcesContent":["/**\n * Memory pressure monitoring utilities.\n *\n * Provides lightweight heap-usage checks that callers can use to\n * throttle work when the process is approaching memory limits.\n */\n\n/** Default threshold: pause work when heap usage exceeds 85% of the limit. */\nconst DEFAULT_PRESSURE_RATIO = 0.85;\n\nexport interface MemorySnapshot {\n heapUsedMB: number;\n heapTotalMB: number;\n rssUsedMB: number;\n /** Ratio of heapUsed / heapTotal (0–1). */\n heapPressure: number;\n /** True when heapPressure exceeds the configured threshold. */\n isUnderPressure: boolean;\n}\n\n/**\n * Take a snapshot of current memory usage.\n *\n * @param pressureRatio - Threshold ratio (0–1) above which `isUnderPressure`\n * is set to `true`. Defaults to 0.85.\n */\nexport function getMemorySnapshot(pressureRatio = DEFAULT_PRESSURE_RATIO): MemorySnapshot {\n const mem = process.memoryUsage();\n const heapUsedMB = mem.heapUsed / 1_048_576;\n const heapTotalMB = mem.heapTotal / 1_048_576;\n const rssUsedMB = mem.rss / 1_048_576;\n const heapPressure = mem.heapTotal > 0 ? mem.heapUsed / mem.heapTotal : 0;\n\n return {\n heapUsedMB: Math.round(heapUsedMB * 10) / 10,\n heapTotalMB: Math.round(heapTotalMB * 10) / 10,\n rssUsedMB: Math.round(rssUsedMB * 10) / 10,\n heapPressure: Math.round(heapPressure * 1000) / 1000,\n isUnderPressure: heapPressure >= pressureRatio,\n };\n}\n\n/**\n * Creates a monitor that periodically checks memory pressure and calls\n * `onPressure` when the threshold is exceeded. The monitor can be used\n * to pause a PQueue or reduce concurrency under load.\n *\n * Returns a `stop` function to clear the interval.\n */\nexport function createMemoryMonitor(options: {\n /** Polling interval in milliseconds (default: 5 000). */\n intervalMs?: number;\n /** Heap pressure ratio threshold (default: 0.85). */\n pressureRatio?: number;\n /** Called when heap pressure exceeds the threshold. */\n onPressure: (snapshot: MemorySnapshot) => void;\n /** Called when heap pressure drops back below the threshold. */\n onRelief?: (snapshot: MemorySnapshot) => void;\n}): { stop: () => void } {\n const intervalMs = options.intervalMs ?? 5_000;\n const pressureRatio = options.pressureRatio ?? DEFAULT_PRESSURE_RATIO;\n let wasPressured = false;\n\n const timer = setInterval(() => {\n const snapshot = getMemorySnapshot(pressureRatio);\n\n if (snapshot.isUnderPressure && !wasPressured) {\n wasPressured = true;\n options.onPressure(snapshot);\n } else if (!snapshot.isUnderPressure && wasPressured) {\n wasPressured = false;\n options.onRelief?.(snapshot);\n }\n }, intervalMs);\n\n // Unref so this doesn't keep the process alive\n if (typeof timer === \"object\" && \"unref\" in timer) {\n timer.unref();\n }\n\n return {\n stop() {\n clearInterval(timer);\n },\n };\n}\n\n","/**\n * Shared utility functions used across the codebase.\n */\n\n/**\n * Truncate a string to `maxLength`, appending an ellipsis when trimmed.\n * Defaults to the unicode ellipsis `\"…\"` (1 char). Pass `\"...\"` for the\n * three-dot ASCII variant.\n */\nexport function truncate(\n value: string,\n maxLength: number,\n ellipsis = \"…\",\n): string {\n if (value.length <= maxLength) {\n return value;\n }\n return `${value.slice(0, Math.max(0, maxLength - ellipsis.length))}${ellipsis}`;\n}\n\n/**\n * Type guard: returns `true` when `value` is a non-null object\n * (i.e.\\ a `Record<string, unknown>`).\n */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\n"],"mappings":";AAQA,IAAM,yBAAyB;AAkBxB,SAAS,kBAAkB,gBAAgB,wBAAwC;AACxF,QAAM,MAAM,QAAQ,YAAY;AAChC,QAAM,aAAa,IAAI,WAAW;AAClC,QAAM,cAAc,IAAI,YAAY;AACpC,QAAM,YAAY,IAAI,MAAM;AAC5B,QAAM,eAAe,IAAI,YAAY,IAAI,IAAI,WAAW,IAAI,YAAY;AAExE,SAAO;AAAA,IACL,YAAY,KAAK,MAAM,aAAa,EAAE,IAAI;AAAA,IAC1C,aAAa,KAAK,MAAM,cAAc,EAAE,IAAI;AAAA,IAC5C,WAAW,KAAK,MAAM,YAAY,EAAE,IAAI;AAAA,IACxC,cAAc,KAAK,MAAM,eAAe,GAAI,IAAI;AAAA,IAChD,iBAAiB,gBAAgB;AAAA,EACnC;AACF;AASO,SAAS,oBAAoB,SASX;AACvB,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,gBAAgB,QAAQ,iBAAiB;AAC/C,MAAI,eAAe;AAEnB,QAAM,QAAQ,YAAY,MAAM;AAC9B,UAAM,WAAW,kBAAkB,aAAa;AAEhD,QAAI,SAAS,mBAAmB,CAAC,cAAc;AAC7C,qBAAe;AACf,cAAQ,WAAW,QAAQ;AAAA,IAC7B,WAAW,CAAC,SAAS,mBAAmB,cAAc;AACpD,qBAAe;AACf,cAAQ,WAAW,QAAQ;AAAA,IAC7B;AAAA,EACF,GAAG,UAAU;AAGb,MAAI,OAAO,UAAU,YAAY,WAAW,OAAO;AACjD,UAAM,MAAM;AAAA,EACd;AAEA,SAAO;AAAA,IACL,OAAO;AACL,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF;AACF;;;AC5EO,SAAS,SACd,OACA,WACA,WAAW,UACH;AACR,MAAI,MAAM,UAAU,WAAW;AAC7B,WAAO;AAAA,EACT;AACA,SAAO,GAAG,MAAM,MAAM,GAAG,KAAK,IAAI,GAAG,YAAY,SAAS,MAAM,CAAC,CAAC,GAAG,QAAQ;AAC/E;AAMO,SAAS,SAAS,OAAkD;AACzE,SAAO,OAAO,UAAU,YAAY,UAAU;AAChD;","names":[]}
|