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 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: .aim-control/plans/${plan.id}/plan.md`,
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: aim fuel set <percent>");
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: aim fuel calibrate <mission-id> <after-percent>");
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.0",
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",
@@ -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 `aim fuel set <percent>` to calibrate subscription limits."
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 `aim fuel set <percent>` if using subscriptions."
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 \`aim fuel calibrate ${mission.id} <after-percent>\`.`
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 `# AI Mission Report
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>AI Mission Report - ${escapeHtml(mission.label ?? mission.id)}</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; }