runcap 0.1.0 → 0.1.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.
- package/bin/runcap.mjs +5 -3
- package/package.json +1 -1
- package/src/mission-control.mjs +55 -5
package/bin/runcap.mjs
CHANGED
|
@@ -106,13 +106,15 @@ try {
|
|
|
106
106
|
"",
|
|
107
107
|
`Runcap plan: ${plan.id}`,
|
|
108
108
|
`Goal: ${plan.goal}`,
|
|
109
|
+
`Estimated cost: ${plan.budget.costRange} (${plan.budget.costPrecision})`,
|
|
110
|
+
`Recommended hard cap: ${plan.budget.recommendedCap}`,
|
|
109
111
|
`Budget risk: ${plan.budget.risk}`,
|
|
110
112
|
`Expected waste reduction: ${plan.budget.expectedWasteReduction}`,
|
|
111
113
|
`Planning model: ${plan.routing.planningTier}`,
|
|
112
114
|
`Execution model: ${plan.routing.executionTier}`,
|
|
113
115
|
`Proof: ${plan.quality.proof}`,
|
|
114
116
|
`Stop rule: ${plan.stopRule}`,
|
|
115
|
-
`Report: .
|
|
117
|
+
`Report: .runcap/plans/${plan.id}/plan.md`,
|
|
116
118
|
""
|
|
117
119
|
].join("\n"));
|
|
118
120
|
} else if (command === "plans") {
|
|
@@ -147,13 +149,13 @@ try {
|
|
|
147
149
|
const subcommand = args[1] ?? "show";
|
|
148
150
|
if (subcommand === "set") {
|
|
149
151
|
const value = Number(args[2]);
|
|
150
|
-
if (!Number.isFinite(value)) throw new Error("Usage:
|
|
152
|
+
if (!Number.isFinite(value)) throw new Error("Usage: runcap fuel set <percent>");
|
|
151
153
|
console.log(await recordFuel(value));
|
|
152
154
|
} else if (subcommand === "calibrate") {
|
|
153
155
|
const id = args[2];
|
|
154
156
|
const after = Number(args[3]);
|
|
155
157
|
if (!id || !Number.isFinite(after)) {
|
|
156
|
-
throw new Error("Usage:
|
|
158
|
+
throw new Error("Usage: runcap fuel calibrate <mission-id> <after-percent>");
|
|
157
159
|
}
|
|
158
160
|
console.log(await calibrateFuel(id, after));
|
|
159
161
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runcap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Cap every agent run before it starts: estimate cost, set a hard ceiling that stops the run, rescue stuck agents. Local, MIT, nothing uploaded.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/mission-control.mjs
CHANGED
|
@@ -495,7 +495,7 @@ export async function showStatus(options = {}) {
|
|
|
495
495
|
await ensureStore();
|
|
496
496
|
const fuel = await readFuel();
|
|
497
497
|
const fuelLine = fuel.currentPercent === null
|
|
498
|
-
? "Fuel: unknown. Run `
|
|
498
|
+
? "Fuel: unknown. Run `runcap fuel set <percent>` to calibrate subscription limits."
|
|
499
499
|
: `Fuel: ${fuel.currentPercent}% (${fuel.source}, confidence: ${fuel.confidence})`;
|
|
500
500
|
if (options.includeFuelOnly) return fuelLine;
|
|
501
501
|
|
|
@@ -595,6 +595,7 @@ function buildAiWorkPlan(goal, { quality = "high", fuelPercent = null, snapshot
|
|
|
595
595
|
const routing = routeTask({ taskType, budgetRisk, quality, hasVerification });
|
|
596
596
|
const proof = proofForTask({ taskType, hasVerification });
|
|
597
597
|
const missions = missionBreakdown({ taskType, budgetRisk, proof });
|
|
598
|
+
const cost = estimatePlanCost({ budgetRisk, bigSignals, words, taskType, quality });
|
|
598
599
|
return {
|
|
599
600
|
id: createPlanId(cleanGoal),
|
|
600
601
|
createdAt: new Date().toISOString(),
|
|
@@ -609,6 +610,12 @@ function buildAiWorkPlan(goal, { quality = "high", fuelPercent = null, snapshot
|
|
|
609
610
|
budget: {
|
|
610
611
|
risk: budgetRisk,
|
|
611
612
|
expectedWasteReduction,
|
|
613
|
+
costLowUsd: cost.lowUsd,
|
|
614
|
+
costHighUsd: cost.highUsd,
|
|
615
|
+
costRange: cost.range,
|
|
616
|
+
recommendedCapUsd: cost.recommendedCapUsd,
|
|
617
|
+
recommendedCap: cost.recommendedCap,
|
|
618
|
+
costPrecision: cost.precision,
|
|
612
619
|
reason: budgetRisk === "High"
|
|
613
620
|
? "The goal is broad or fuel is low. A single agent run is likely to waste context and repeat work."
|
|
614
621
|
: "The goal can be controlled with smaller missions and proof checkpoints."
|
|
@@ -629,6 +636,49 @@ function buildAiWorkPlan(goal, { quality = "high", fuelPercent = null, snapshot
|
|
|
629
636
|
};
|
|
630
637
|
}
|
|
631
638
|
|
|
639
|
+
// Estimate a USD cost RANGE for an agent run from scope signals, priced against
|
|
640
|
+
// the sourced table. Deliberately a range, not an oracle: agent runs are
|
|
641
|
+
// stochastic. The recommended cap sits above the high end so a normal run
|
|
642
|
+
// completes but a runaway loop is stopped.
|
|
643
|
+
function estimatePlanCost({ budgetRisk, bigSignals, words, taskType, quality }) {
|
|
644
|
+
// Base expected total tokens (input+output across the whole run, including
|
|
645
|
+
// the agent re-reading context on each loop). Software runs loop more.
|
|
646
|
+
let baseTokens = taskType === "software" ? 220000 : 120000;
|
|
647
|
+
baseTokens += words * 1500;
|
|
648
|
+
baseTokens += bigSignals * 350000;
|
|
649
|
+
if (budgetRisk === "High") baseTokens *= 2.4;
|
|
650
|
+
else if (budgetRisk === "Medium") baseTokens *= 1.5;
|
|
651
|
+
|
|
652
|
+
// Premium-model blended price ($/token): planning on a strong model is the
|
|
653
|
+
// expensive case, so we price the headline range against it to avoid
|
|
654
|
+
// under-promising. Opus-class: ~$5/M in, ~$25/M out, assume ~30% output.
|
|
655
|
+
const blendedPerToken = quality === "cheap"
|
|
656
|
+
? (0.75 * 0.7 + 4.5 * 0.3) / 1_000_000 // cheap tier (gpt-5.4-mini)
|
|
657
|
+
: (5 * 0.7 + 25 * 0.3) / 1_000_000; // strong tier (opus-class)
|
|
658
|
+
|
|
659
|
+
const mid = baseTokens * blendedPerToken;
|
|
660
|
+
// Range: runs vary widely, so +-45% around the midpoint.
|
|
661
|
+
const lowUsd = round2(mid * 0.55);
|
|
662
|
+
const highUsd = round2(mid * 1.45);
|
|
663
|
+
// Cap above the high end (1.5x) so a normal run finishes, a loop is killed.
|
|
664
|
+
const recommendedCapUsd = roundCap(highUsd * 1.5);
|
|
665
|
+
return {
|
|
666
|
+
lowUsd,
|
|
667
|
+
highUsd,
|
|
668
|
+
recommendedCapUsd,
|
|
669
|
+
range: `$${lowUsd.toFixed(2)}-$${highUsd.toFixed(2)}`,
|
|
670
|
+
recommendedCap: `$${recommendedCapUsd.toFixed(2)}`,
|
|
671
|
+
precision: "calculated_estimate_not_provider_bill"
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function round2(n) { return Math.round(n * 100) / 100; }
|
|
676
|
+
function roundCap(n) {
|
|
677
|
+
// Round caps to a friendly number: nearest $1 under $20, nearest $5 above.
|
|
678
|
+
if (n < 20) return Math.max(1, Math.ceil(n));
|
|
679
|
+
return Math.ceil(n / 5) * 5;
|
|
680
|
+
}
|
|
681
|
+
|
|
632
682
|
function classifyTask(lower) {
|
|
633
683
|
if (/code|bug|test|build|app|api|database|typescript|react|python|deploy|auth|repo|github/.test(lower)) return "software";
|
|
634
684
|
if (/video|script|post|content|image|marketing|copy|campaign|linkedin|youtube/.test(lower)) return "creative";
|
|
@@ -1249,7 +1299,7 @@ function shortSummary(mission) {
|
|
|
1249
1299
|
|
|
1250
1300
|
function formatPreflight({ command, preflight, fuel }) {
|
|
1251
1301
|
const fuelLine = fuel.currentPercent === null
|
|
1252
|
-
? "Fuel: unknown. Set it with `
|
|
1302
|
+
? "Fuel: unknown. Set it with `runcap fuel set <percent>` if using subscriptions."
|
|
1253
1303
|
: `Fuel: ${fuel.currentPercent}% (${fuel.confidence} confidence)`;
|
|
1254
1304
|
const scopeAdvice = preflight.scopeRisk === "high"
|
|
1255
1305
|
? "Do not launch as one broad mission. Split into one vertical slice with a verification command."
|
|
@@ -1273,7 +1323,7 @@ function formatPreflight({ command, preflight, fuel }) {
|
|
|
1273
1323
|
|
|
1274
1324
|
function formatReport(mission) {
|
|
1275
1325
|
const fuel = mission.fuelUsedPercent === null
|
|
1276
|
-
? `Fuel: before ${mission.fuelBefore ?? "unknown"}%, after unknown. Calibrate with \`
|
|
1326
|
+
? `Fuel: before ${mission.fuelBefore ?? "unknown"}%, after unknown. Calibrate with \`runcap fuel calibrate ${mission.id} <after-percent>\`.`
|
|
1277
1327
|
: `Fuel used: ${mission.fuelUsedPercent}% (source: manual calibration, confidence: high).`;
|
|
1278
1328
|
const errorLines = mission.errors.length
|
|
1279
1329
|
? mission.errors.map((error) => `- ${error.kind} (${error.confidence}): ${error.raw}`).join("\n")
|
|
@@ -1287,7 +1337,7 @@ function formatReport(mission) {
|
|
|
1287
1337
|
` Next action: ${rec.nextAction}`,
|
|
1288
1338
|
` Rescue prompt: ${rec.prompt}`
|
|
1289
1339
|
].join("\n")).join("\n\n");
|
|
1290
|
-
return `#
|
|
1340
|
+
return `# Runcap Mission Report
|
|
1291
1341
|
|
|
1292
1342
|
Mission: ${mission.id}
|
|
1293
1343
|
Command: \`${mission.command.join(" ")}\`
|
|
@@ -1396,7 +1446,7 @@ function formatHtmlReport(mission) {
|
|
|
1396
1446
|
<head>
|
|
1397
1447
|
<meta charset="utf-8">
|
|
1398
1448
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1399
|
-
<title>
|
|
1449
|
+
<title>Runcap Mission Report - ${escapeHtml(mission.label ?? mission.id)}</title>
|
|
1400
1450
|
<style>
|
|
1401
1451
|
:root { color-scheme: dark; --bg:#0f1115; --panel:#181c22; --soft:#202630; --line:#303946; --text:#f5f7fb; --muted:#a7b0bd; --accent:#70d6ff; --warn:#ffd166; --bad:#ff6b6b; --good:#55d78a; }
|
|
1402
1452
|
* { box-sizing:border-box; }
|