claude-overnight 1.0.1 → 1.2.0
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/README.md +54 -12
- package/dist/index.js +104 -29
- package/dist/planner.d.ts +11 -1
- package/dist/planner.js +37 -3
- package/dist/swarm.d.ts +17 -2
- package/dist/swarm.js +57 -3
- package/dist/types.d.ts +10 -1
- package/dist/ui.d.ts +9 -2
- package/dist/ui.js +131 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,12 +33,15 @@ claude-overnight
|
|
|
33
33
|
● Sonnet — Sonnet 4.6 · Best for everyday tasks
|
|
34
34
|
○ Opus — Opus 4.6 · Most capable
|
|
35
35
|
|
|
36
|
-
④ Usage:
|
|
36
|
+
④ Usage cap:
|
|
37
37
|
● 90% · leave 10% for other work
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
⑤ Allow extra usage (billed separately):
|
|
40
|
+
● No · stop when plan limits are reached
|
|
41
|
+
|
|
42
|
+
╭──────────────────────────────────────────────────╮
|
|
43
|
+
│ sonnet · budget 200 · 5× · flex · cap 90% · no extra │
|
|
44
|
+
╰──────────────────────────────────────────────────╯
|
|
42
45
|
|
|
43
46
|
✓ 5 themes → review, press Run, walk away
|
|
44
47
|
|
|
@@ -52,7 +55,7 @@ claude-overnight
|
|
|
52
55
|
◆ Assessing... ✓ Vision met
|
|
53
56
|
```
|
|
54
57
|
|
|
55
|
-
You interact once (objective, budget, model, review themes), then everything runs autonomously — thinking, planning, executing, reflecting, steering. Rate-limited? It waits and retries. Crash? Resume where you left off.
|
|
58
|
+
You interact once (objective, budget, model, review themes), then everything runs autonomously — thinking, planning, executing, reflecting, steering. Rate-limited? It waits and retries. Crash? Resume where you left off. Capped at usage limit? Pick up next time with full context preserved.
|
|
56
59
|
|
|
57
60
|
## How it works
|
|
58
61
|
|
|
@@ -62,7 +65,7 @@ For budgets > 15, the tool launches **architect agents** that explore your codeb
|
|
|
62
65
|
|
|
63
66
|
### 2. Orchestration
|
|
64
67
|
|
|
65
|
-
An orchestrator agent reads all design documents and synthesizes concrete execution tasks — grounded in real files and patterns the architects found. No guesswork.
|
|
68
|
+
An orchestrator agent reads all design documents and synthesizes concrete execution tasks — grounded in real files and patterns the architects found. No guesswork. The task plan is also written to a file for resilience — if orchestration is interrupted, partial results survive.
|
|
66
69
|
|
|
67
70
|
### 3. Iterative execution
|
|
68
71
|
|
|
@@ -97,20 +100,30 @@ Every run gets its own folder in `.claude-overnight/runs/`. Nothing is ever over
|
|
|
97
100
|
run.json, sessions/
|
|
98
101
|
```
|
|
99
102
|
|
|
100
|
-
|
|
103
|
+
Any run that stops before the steering system declares the objective complete — capped at usage limit, Ctrl+C, crash, rate limit timeout, steering failure — is automatically resumable:
|
|
101
104
|
|
|
102
105
|
```
|
|
103
|
-
⚠
|
|
106
|
+
⚠ Unfinished run
|
|
104
107
|
╭──────────────────────────────────────────────────╮
|
|
105
108
|
│ refactor auth, add tests, update docs │
|
|
106
|
-
│ 50/200 sessions ·
|
|
109
|
+
│ 50/200 sessions · 150 remaining · $69.16 │
|
|
107
110
|
│ 34 merged · 16 unmerged · 0 failed branches │
|
|
108
111
|
╰──────────────────────────────────────────────────╯
|
|
109
112
|
|
|
110
113
|
Resume │ Fresh │ Quit
|
|
111
114
|
```
|
|
112
115
|
|
|
113
|
-
On resume: unmerged branches auto-merge, the wave loop continues, all context is preserved.
|
|
116
|
+
On resume: unmerged branches auto-merge, the wave loop continues, all context is preserved. Designs and reflections stay on disk until the objective is truly complete.
|
|
117
|
+
|
|
118
|
+
If the thinking phase succeeds but orchestration crashes, the next run detects the orphaned design docs and reuses them — no re-running $9 worth of architect agents:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
✓ Reusing 5 design docs (from prior attempt)
|
|
122
|
+
Focus 0: Project Wizard UI vs VISION.md Flow
|
|
123
|
+
Focus 1: Team Load and Rebalancer Surface
|
|
124
|
+
Focus 2: Code Health After Swarm Wave
|
|
125
|
+
...
|
|
126
|
+
```
|
|
114
127
|
|
|
115
128
|
**Knowledge carries forward** — new runs inherit knowledge from completed previous runs. Thinking agents and steering see what past runs built. Run 2 knows run 1 already built the auth system.
|
|
116
129
|
|
|
@@ -162,6 +175,8 @@ claude-overnight "fix auth bug in src/auth.ts" "add tests for user model"
|
|
|
162
175
|
| `--concurrency=N` | `5` | Parallel agents |
|
|
163
176
|
| `--model=NAME` | prompted | Worker model (planner uses best available) |
|
|
164
177
|
| `--usage-cap=N` | unlimited | Stop at N% utilization |
|
|
178
|
+
| `--allow-extra-usage` | off | Allow extra/overage usage (billed separately) |
|
|
179
|
+
| `--extra-usage-budget=N` | — | Max $ for extra usage (implies --allow-extra-usage) |
|
|
165
180
|
| `--timeout=SECONDS` | `300` | Inactivity timeout per agent |
|
|
166
181
|
| `--no-flex` | — | Disable multi-wave steering |
|
|
167
182
|
| `--dry-run` | — | Show planned tasks without running |
|
|
@@ -180,15 +195,42 @@ claude-overnight "fix auth bug in src/auth.ts" "add tests for user model"
|
|
|
180
195
|
| `mergeStrategy` | `"yolo" \| "branch"` | `"yolo"` | Merge into HEAD or new branch |
|
|
181
196
|
| `usageCap` | `number (0-100)` | unlimited | Stop at N% utilization |
|
|
182
197
|
|
|
198
|
+
## Usage controls
|
|
199
|
+
|
|
200
|
+
### Extra usage protection
|
|
201
|
+
|
|
202
|
+
By default, extra/overage usage is **blocked**. When your plan's rate limits are exhausted, the run stops cleanly and is resumable. You control this in the interactive prompt (step ⑤) or via CLI flags:
|
|
203
|
+
|
|
204
|
+
- `--allow-extra-usage` — opt in to extra usage (billed separately)
|
|
205
|
+
- `--extra-usage-budget=20` — allow up to $20 of extra usage, then stop
|
|
206
|
+
|
|
207
|
+
### Live controls during execution
|
|
208
|
+
|
|
209
|
+
Press these keys while agents are running:
|
|
210
|
+
|
|
211
|
+
| Key | Action |
|
|
212
|
+
|---|---|
|
|
213
|
+
| `b` | Change remaining budget (number of sessions) |
|
|
214
|
+
| `t` | Change usage cap threshold (0-100%) |
|
|
215
|
+
| `q` | Graceful stop (press twice to force quit) |
|
|
216
|
+
|
|
217
|
+
Changes take effect between waves — active agents finish their current task.
|
|
218
|
+
|
|
219
|
+
### Multi-window usage display
|
|
220
|
+
|
|
221
|
+
The usage bar cycles through all rate limit windows (5h, 7d, etc.) every 3 seconds, showing utilization per window. Usage info is shown during all phases — thinking, orchestration, steering, and execution.
|
|
222
|
+
|
|
183
223
|
## Rate limits
|
|
184
224
|
|
|
185
225
|
Built for unattended runs lasting hours or days.
|
|
186
226
|
|
|
187
227
|
- **Hard block**: pauses until the rate limit window resets, then resumes
|
|
188
228
|
- **Soft throttle**: slows dispatch at >75% utilization
|
|
229
|
+
- **Extra usage guard**: detects overage billing and stops unless explicitly allowed
|
|
230
|
+
- **Cooldown between phases**: waits for rate limit reset after thinking before starting orchestration
|
|
189
231
|
- **Retry with backoff**: transient errors (429, overloaded) retry automatically
|
|
190
|
-
- **Usage cap**: set a ceiling, active agents finish, no new ones start
|
|
191
|
-
- **Planner retries**: steering and orchestration
|
|
232
|
+
- **Usage cap**: set a ceiling, active agents finish, no new ones start — run is resumable
|
|
233
|
+
- **Planner retries**: steering and orchestration retry on rate limits (30s/60s/120s backoff) with full context
|
|
192
234
|
|
|
193
235
|
## Worktrees and merging
|
|
194
236
|
|
package/dist/index.js
CHANGED
|
@@ -11,8 +11,8 @@ import { planTasks, refinePlan, detectModelTier, steerWave, identifyThemes, buil
|
|
|
11
11
|
import { startRenderLoop, renderSummary } from "./ui.js";
|
|
12
12
|
// ── CLI flag parsing ──
|
|
13
13
|
function parseCliFlags(argv) {
|
|
14
|
-
const known = new Set(["concurrency", "model", "timeout", "budget", "usage-cap"]);
|
|
15
|
-
const booleans = new Set(["--dry-run", "-h", "--help", "-v", "--version", "--no-flex"]);
|
|
14
|
+
const known = new Set(["concurrency", "model", "timeout", "budget", "usage-cap", "extra-usage-budget"]);
|
|
15
|
+
const booleans = new Set(["--dry-run", "-h", "--help", "-v", "--version", "--no-flex", "--allow-extra-usage"]);
|
|
16
16
|
const flags = {};
|
|
17
17
|
const positional = [];
|
|
18
18
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -523,6 +523,8 @@ async function main() {
|
|
|
523
523
|
--concurrency=N Max parallel agents ${chalk.dim("(default: 5)")}
|
|
524
524
|
--model=NAME Worker model override ${chalk.dim("(planner always uses best available)")}
|
|
525
525
|
--usage-cap=N Stop at N% utilization ${chalk.dim("(e.g. 90 to save 10% for other work)")}
|
|
526
|
+
--allow-extra-usage Allow extra/overage usage ${chalk.dim("(default: stop when plan limits hit)")}
|
|
527
|
+
--extra-usage-budget=N Max $ for extra usage ${chalk.dim("(implies --allow-extra-usage)")}
|
|
526
528
|
--timeout=SECONDS Agent inactivity timeout ${chalk.dim("(default: 300s, kills only silent agents)")}
|
|
527
529
|
--no-flex Disable adaptive multi-wave planning ${chalk.dim("(run all tasks in one shot)")}
|
|
528
530
|
|
|
@@ -606,9 +608,18 @@ async function main() {
|
|
|
606
608
|
console.log(chalk.dim(`\n ${completedRuns.length} previous run${completedRuns.length > 1 ? "s" : ""}`));
|
|
607
609
|
for (const r of completedRuns.slice(0, 3)) {
|
|
608
610
|
const date = r.state.startedAt?.slice(0, 10) || "unknown";
|
|
609
|
-
const obj = r.state.objective?.slice(0,
|
|
611
|
+
const obj = r.state.objective?.slice(0, 50) || "";
|
|
610
612
|
const cost = r.state.accCost > 0 ? ` · $${r.state.accCost.toFixed(0)}` : "";
|
|
611
|
-
|
|
613
|
+
const merged = r.state.branches.filter(b => b.status === "merged").length;
|
|
614
|
+
console.log(chalk.dim(` ${date} · ${r.state.accCompleted} done · ${merged} merged${cost}${obj ? ` · ${obj}` : ""}${obj.length >= 50 ? "…" : ""}`));
|
|
615
|
+
// Show status if available
|
|
616
|
+
let status = "";
|
|
617
|
+
try {
|
|
618
|
+
status = readFileSync(join(r.dir, "status.md"), "utf-8").trim().split("\n")[0].slice(0, 80);
|
|
619
|
+
}
|
|
620
|
+
catch { }
|
|
621
|
+
if (status)
|
|
622
|
+
console.log(chalk.dim(` ${status}`));
|
|
612
623
|
}
|
|
613
624
|
}
|
|
614
625
|
// ── Resume detection ──
|
|
@@ -628,10 +639,11 @@ async function main() {
|
|
|
628
639
|
lastStatus = readFileSync(join(incomplete.dir, "status.md"), "utf-8").trim().slice(0, 120);
|
|
629
640
|
}
|
|
630
641
|
catch { }
|
|
631
|
-
|
|
642
|
+
const label = "Unfinished run";
|
|
643
|
+
console.log(chalk.yellow(`\n ⚠ ${label}`));
|
|
632
644
|
const boxLines = [
|
|
633
645
|
`${obj}${obj.length >= 50 ? "…" : ""}`,
|
|
634
|
-
`${prev.accCompleted}/${prev.budget} sessions · ${prev.
|
|
646
|
+
`${prev.accCompleted}/${prev.budget} sessions · ${prev.remaining} remaining · $${prev.accCost.toFixed(2)}`,
|
|
635
647
|
];
|
|
636
648
|
if (lastStatus)
|
|
637
649
|
boxLines.push(lastStatus);
|
|
@@ -667,6 +679,8 @@ async function main() {
|
|
|
667
679
|
let concurrency;
|
|
668
680
|
let objective = fileCfg?.objective;
|
|
669
681
|
let usageCap;
|
|
682
|
+
let allowExtraUsage = false;
|
|
683
|
+
let extraUsageBudget;
|
|
670
684
|
if (!nonInteractive) {
|
|
671
685
|
// ① Objective
|
|
672
686
|
while (true) {
|
|
@@ -714,13 +728,29 @@ async function main() {
|
|
|
714
728
|
const ans = await ask(` ${chalk.cyan("③")} ${chalk.dim("Worker model [claude-sonnet-4-6]:")} `);
|
|
715
729
|
workerModel = ans || "claude-sonnet-4-6";
|
|
716
730
|
}
|
|
717
|
-
// ④ Usage
|
|
718
|
-
usageCap = await select(`${chalk.cyan("④")} Usage:`, [
|
|
731
|
+
// ④ Usage cap
|
|
732
|
+
usageCap = await select(`${chalk.cyan("④")} Usage cap:`, [
|
|
719
733
|
{ name: "Unlimited", value: undefined, hint: "full capacity, wait through rate limits" },
|
|
720
734
|
{ name: "90%", value: 0.9, hint: "leave 10% for other work" },
|
|
721
735
|
{ name: "75%", value: 0.75, hint: "conservative, plenty of headroom" },
|
|
722
736
|
{ name: "50%", value: 0.5, hint: "use half, keep the rest" },
|
|
723
737
|
]);
|
|
738
|
+
// ⑤ Extra usage
|
|
739
|
+
const extraChoice = await select(`${chalk.cyan("⑤")} Allow extra usage ${chalk.dim("(billed separately)")}:`, [
|
|
740
|
+
{ name: "No", value: "no", hint: "stop when plan limits are reached" },
|
|
741
|
+
{ name: "Yes, with $ limit", value: "budget", hint: "set a spending cap" },
|
|
742
|
+
{ name: "Yes, unlimited", value: "unlimited", hint: "keep going no matter what" },
|
|
743
|
+
]);
|
|
744
|
+
if (extraChoice === "budget") {
|
|
745
|
+
const budgetAns = await ask(` ${chalk.dim("Max extra usage $:")} `);
|
|
746
|
+
extraUsageBudget = parseFloat(budgetAns);
|
|
747
|
+
if (!extraUsageBudget || extraUsageBudget <= 0)
|
|
748
|
+
extraUsageBudget = 5;
|
|
749
|
+
allowExtraUsage = true;
|
|
750
|
+
}
|
|
751
|
+
else if (extraChoice === "unlimited") {
|
|
752
|
+
allowExtraUsage = true;
|
|
753
|
+
}
|
|
724
754
|
concurrency = Math.min(5, budget);
|
|
725
755
|
// Config summary box
|
|
726
756
|
const parts = [];
|
|
@@ -737,6 +767,10 @@ async function main() {
|
|
|
737
767
|
parts.push("flex");
|
|
738
768
|
if (usageCap != null)
|
|
739
769
|
parts.push(`cap ${Math.round(usageCap * 100)}%`);
|
|
770
|
+
if (allowExtraUsage)
|
|
771
|
+
parts.push(extraUsageBudget ? `extra $${extraUsageBudget}` : "extra ∞");
|
|
772
|
+
else
|
|
773
|
+
parts.push("no extra");
|
|
740
774
|
if (completedRuns.length > 0)
|
|
741
775
|
parts.push(`${completedRuns.length} prior`);
|
|
742
776
|
const inner = parts.join(chalk.dim(" · "));
|
|
@@ -770,6 +804,17 @@ async function main() {
|
|
|
770
804
|
else {
|
|
771
805
|
usageCap = fileCfg?.usageCap != null ? fileCfg.usageCap / 100 : undefined;
|
|
772
806
|
}
|
|
807
|
+
// Extra usage: default OFF for non-interactive
|
|
808
|
+
allowExtraUsage = argv.includes("--allow-extra-usage");
|
|
809
|
+
const extraBudgetFlag = cliFlags["extra-usage-budget"];
|
|
810
|
+
if (extraBudgetFlag != null) {
|
|
811
|
+
extraUsageBudget = parseFloat(extraBudgetFlag);
|
|
812
|
+
if (isNaN(extraUsageBudget) || extraUsageBudget <= 0) {
|
|
813
|
+
console.error(chalk.red(` --extra-usage-budget must be a positive number`));
|
|
814
|
+
process.exit(1);
|
|
815
|
+
}
|
|
816
|
+
allowExtraUsage = true;
|
|
817
|
+
}
|
|
773
818
|
}
|
|
774
819
|
validateConcurrency(concurrency);
|
|
775
820
|
const permissionMode = fileCfg?.permissionMode ?? "auto";
|
|
@@ -779,7 +824,8 @@ async function main() {
|
|
|
779
824
|
const mergeStrategy = fileCfg?.mergeStrategy ?? "yolo";
|
|
780
825
|
if (nonInteractive) {
|
|
781
826
|
const capStr = usageCap != null ? ` cap=${Math.round(usageCap * 100)}%` : "";
|
|
782
|
-
|
|
827
|
+
const extraStr = allowExtraUsage ? (extraUsageBudget ? ` extra=$${extraUsageBudget}` : " extra=∞") : " extra=off";
|
|
828
|
+
console.log(chalk.dim(` ${workerModel} concurrency=${concurrency} worktrees=${useWorktrees} merge=${mergeStrategy} perms=${permissionMode}${capStr}${extraStr}`));
|
|
783
829
|
}
|
|
784
830
|
// ── Flex mode: adaptive multi-wave planning ──
|
|
785
831
|
let flex = !argv.includes("--no-flex") && (fileCfg?.flexiblePlan ?? objective != null) && objective != null && (budget ?? 10) > 2;
|
|
@@ -862,7 +908,17 @@ async function main() {
|
|
|
862
908
|
mkdirSync(designDir, { recursive: true });
|
|
863
909
|
const existingDesigns = readMdDir(designDir);
|
|
864
910
|
if (existingDesigns) {
|
|
865
|
-
|
|
911
|
+
const designFiles = readdirSync(designDir).filter(f => f.endsWith(".md")).sort();
|
|
912
|
+
console.log(chalk.green(`\n ✓ Reusing ${designFiles.length} design docs`) + chalk.dim(` (from prior attempt)`));
|
|
913
|
+
for (const f of designFiles) {
|
|
914
|
+
try {
|
|
915
|
+
const firstLine = readFileSync(join(designDir, f), "utf-8").split("\n")[0].replace(/^#+\s*/, "").trim();
|
|
916
|
+
if (firstLine)
|
|
917
|
+
console.log(chalk.dim(` ${firstLine.slice(0, 80)}`));
|
|
918
|
+
}
|
|
919
|
+
catch { }
|
|
920
|
+
}
|
|
921
|
+
console.log("");
|
|
866
922
|
}
|
|
867
923
|
else {
|
|
868
924
|
const thinkingTasks = buildThinkingTasks(objective, themes, designDir, plannerModel, previousKnowledge || undefined);
|
|
@@ -874,9 +930,9 @@ async function main() {
|
|
|
874
930
|
useWorktrees: false,
|
|
875
931
|
mergeStrategy: "yolo",
|
|
876
932
|
agentTimeoutMs,
|
|
877
|
-
usageCap,
|
|
933
|
+
usageCap, allowExtraUsage, extraUsageBudget,
|
|
878
934
|
});
|
|
879
|
-
const stopThinkRender = startRenderLoop(thinkingSwarm);
|
|
935
|
+
const stopThinkRender = startRenderLoop(thinkingSwarm, { remaining: 0, usageCap, dirty: false });
|
|
880
936
|
try {
|
|
881
937
|
await thinkingSwarm.run();
|
|
882
938
|
}
|
|
@@ -1026,11 +1082,12 @@ async function main() {
|
|
|
1026
1082
|
let currentSwarm;
|
|
1027
1083
|
let remaining;
|
|
1028
1084
|
let currentTasks;
|
|
1085
|
+
const liveConfig = { remaining: 0, usageCap, dirty: false };
|
|
1029
1086
|
let waveNum;
|
|
1030
1087
|
const waveHistory = [];
|
|
1031
1088
|
let accCost, accCompleted, accFailed, accTools;
|
|
1032
1089
|
let accIn = 0, accOut = 0;
|
|
1033
|
-
let lastCapped = false, lastAborted = false;
|
|
1090
|
+
let lastCapped = false, lastAborted = false, objectiveComplete = false;
|
|
1034
1091
|
let lastWaveKind;
|
|
1035
1092
|
let reflectionBudgetUsed;
|
|
1036
1093
|
const branches = [];
|
|
@@ -1053,6 +1110,8 @@ async function main() {
|
|
|
1053
1110
|
concurrency = resumeState.concurrency;
|
|
1054
1111
|
flex = resumeState.flex;
|
|
1055
1112
|
usageCap = resumeState.usageCap;
|
|
1113
|
+
allowExtraUsage = resumeState.allowExtraUsage ?? false;
|
|
1114
|
+
extraUsageBudget = resumeState.extraUsageBudget;
|
|
1056
1115
|
console.log(chalk.green(`\n ✓ Resumed`) + chalk.dim(` · wave ${waveNum + 1} · ${remaining} remaining · $${accCost.toFixed(2)} spent\n`));
|
|
1057
1116
|
}
|
|
1058
1117
|
else {
|
|
@@ -1074,6 +1133,8 @@ async function main() {
|
|
|
1074
1133
|
lastWaveKind = "execute";
|
|
1075
1134
|
reflectionBudgetUsed = 0;
|
|
1076
1135
|
}
|
|
1136
|
+
liveConfig.remaining = remaining;
|
|
1137
|
+
liveConfig.usageCap = usageCap;
|
|
1077
1138
|
const maxReflectionBudget = Math.max(2, Math.ceil((budget ?? 10) * 0.05));
|
|
1078
1139
|
// For flex + branch strategy: create one target branch, waves merge via yolo into it
|
|
1079
1140
|
let runBranch;
|
|
@@ -1111,14 +1172,15 @@ async function main() {
|
|
|
1111
1172
|
if (currentTasks.length > remaining)
|
|
1112
1173
|
currentTasks = currentTasks.slice(0, remaining);
|
|
1113
1174
|
if (flex) {
|
|
1114
|
-
|
|
1175
|
+
const costSoFar = accCost > 0 ? ` · $${accCost.toFixed(2)} spent` : "";
|
|
1176
|
+
console.log(chalk.cyan(`\n ◆ Wave ${waveNum + 1}`) + chalk.dim(` · ${currentTasks.length} tasks · ${remaining} remaining${costSoFar}\n`));
|
|
1115
1177
|
}
|
|
1116
1178
|
const swarm = new Swarm({
|
|
1117
1179
|
tasks: currentTasks, concurrency, cwd, model: workerModel, permissionMode, allowedTools,
|
|
1118
|
-
useWorktrees, mergeStrategy: waveMerge, agentTimeoutMs, usageCap,
|
|
1180
|
+
useWorktrees, mergeStrategy: waveMerge, agentTimeoutMs, usageCap, allowExtraUsage, extraUsageBudget,
|
|
1119
1181
|
});
|
|
1120
1182
|
currentSwarm = swarm;
|
|
1121
|
-
const stopRender = startRenderLoop(swarm);
|
|
1183
|
+
const stopRender = startRenderLoop(swarm, liveConfig);
|
|
1122
1184
|
try {
|
|
1123
1185
|
await swarm.run();
|
|
1124
1186
|
}
|
|
@@ -1143,6 +1205,13 @@ async function main() {
|
|
|
1143
1205
|
accFailed += swarm.failed;
|
|
1144
1206
|
accTools += swarm.agents.reduce((sum, a) => sum + a.toolCalls, 0);
|
|
1145
1207
|
remaining -= swarm.completed + swarm.failed;
|
|
1208
|
+
// Apply live config changes if user adjusted budget/threshold mid-wave
|
|
1209
|
+
if (liveConfig.dirty) {
|
|
1210
|
+
remaining = liveConfig.remaining;
|
|
1211
|
+
usageCap = liveConfig.usageCap;
|
|
1212
|
+
liveConfig.dirty = false;
|
|
1213
|
+
}
|
|
1214
|
+
liveConfig.remaining = remaining;
|
|
1146
1215
|
lastCapped = swarm.cappedOut;
|
|
1147
1216
|
lastAborted = swarm.aborted;
|
|
1148
1217
|
recordBranches(swarm, branches);
|
|
@@ -1150,7 +1219,7 @@ async function main() {
|
|
|
1150
1219
|
saveRunState(runDir, {
|
|
1151
1220
|
id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective, budget: budget ?? tasks.length,
|
|
1152
1221
|
remaining, workerModel, plannerModel, concurrency, permissionMode,
|
|
1153
|
-
usageCap, flex, useWorktrees, mergeStrategy, waveNum, currentTasks,
|
|
1222
|
+
usageCap, allowExtraUsage, extraUsageBudget, flex, useWorktrees, mergeStrategy, waveNum, currentTasks,
|
|
1154
1223
|
lastWaveKind, reflectionBudgetUsed, accCost, accCompleted, accFailed,
|
|
1155
1224
|
branches, phase: "steering", startedAt: new Date(runStartedAt).toISOString(), cwd,
|
|
1156
1225
|
});
|
|
@@ -1193,6 +1262,7 @@ async function main() {
|
|
|
1193
1262
|
if (steer.done || steer.action === "done") {
|
|
1194
1263
|
console.log(chalk.green(` \u2713 ${steer.reasoning}\n`));
|
|
1195
1264
|
steerDone = true;
|
|
1265
|
+
objectiveComplete = true;
|
|
1196
1266
|
remaining = 0; // exit outer loop too
|
|
1197
1267
|
break;
|
|
1198
1268
|
}
|
|
@@ -1215,10 +1285,10 @@ async function main() {
|
|
|
1215
1285
|
tasks: reflTasks, concurrency: 2, cwd,
|
|
1216
1286
|
model: plannerModel, permissionMode,
|
|
1217
1287
|
useWorktrees: false, mergeStrategy: "yolo",
|
|
1218
|
-
agentTimeoutMs, usageCap,
|
|
1288
|
+
agentTimeoutMs, usageCap, allowExtraUsage, extraUsageBudget,
|
|
1219
1289
|
});
|
|
1220
1290
|
currentSwarm = reflSwarm;
|
|
1221
|
-
const stopReflRender = startRenderLoop(reflSwarm);
|
|
1291
|
+
const stopReflRender = startRenderLoop(reflSwarm, liveConfig);
|
|
1222
1292
|
try {
|
|
1223
1293
|
await reflSwarm.run();
|
|
1224
1294
|
}
|
|
@@ -1245,6 +1315,7 @@ async function main() {
|
|
|
1245
1315
|
// action === "execute"
|
|
1246
1316
|
if (steer.tasks.length === 0) {
|
|
1247
1317
|
console.log(chalk.green(` \u2713 ${steer.reasoning}\n`));
|
|
1318
|
+
objectiveComplete = true;
|
|
1248
1319
|
remaining = 0;
|
|
1249
1320
|
break;
|
|
1250
1321
|
}
|
|
@@ -1262,22 +1333,26 @@ async function main() {
|
|
|
1262
1333
|
}
|
|
1263
1334
|
waveNum++;
|
|
1264
1335
|
}
|
|
1265
|
-
//
|
|
1336
|
+
// Only truly "done" if steering explicitly completed the objective (or non-flex single wave with budget exhausted)
|
|
1337
|
+
const trulyDone = objectiveComplete || (!flex && remaining <= 0);
|
|
1338
|
+
const finalPhase = trulyDone ? "done" : "capped";
|
|
1266
1339
|
saveRunState(runDir, {
|
|
1267
1340
|
id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective ?? "", budget: budget ?? tasks.length,
|
|
1268
1341
|
remaining, workerModel, plannerModel, concurrency, permissionMode,
|
|
1269
|
-
usageCap, flex, useWorktrees, mergeStrategy, waveNum, currentTasks: [],
|
|
1342
|
+
usageCap, allowExtraUsage, extraUsageBudget, flex, useWorktrees, mergeStrategy, waveNum, currentTasks: [],
|
|
1270
1343
|
lastWaveKind, reflectionBudgetUsed, accCost, accCompleted, accFailed,
|
|
1271
|
-
branches, phase:
|
|
1344
|
+
branches, phase: finalPhase, startedAt: new Date(runStartedAt).toISOString(), cwd,
|
|
1272
1345
|
});
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1346
|
+
if (trulyDone) {
|
|
1347
|
+
try {
|
|
1348
|
+
rmSync(join(runDir, "designs"), { recursive: true, force: true });
|
|
1349
|
+
}
|
|
1350
|
+
catch { }
|
|
1351
|
+
try {
|
|
1352
|
+
rmSync(join(runDir, "reflections"), { recursive: true, force: true });
|
|
1353
|
+
}
|
|
1354
|
+
catch { }
|
|
1279
1355
|
}
|
|
1280
|
-
catch { }
|
|
1281
1356
|
// Switch back if we created a run branch
|
|
1282
1357
|
if (runBranch && originalRef) {
|
|
1283
1358
|
try {
|
package/dist/planner.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import type { Task, PermMode } from "./types.js";
|
|
1
|
+
import type { Task, PermMode, RateLimitWindow } from "./types.js";
|
|
2
|
+
/** Rate limit info emitted by planner queries for UI display. */
|
|
3
|
+
export interface PlannerRateLimitInfo {
|
|
4
|
+
utilization: number;
|
|
5
|
+
status: string;
|
|
6
|
+
isUsingOverage: boolean;
|
|
7
|
+
windows: Map<string, RateLimitWindow>;
|
|
8
|
+
resetsAt?: number;
|
|
9
|
+
costUsd: number;
|
|
10
|
+
}
|
|
2
11
|
export interface WaveSummary {
|
|
3
12
|
wave: number;
|
|
4
13
|
kind: "execute" | "reflect" | "think";
|
|
@@ -27,6 +36,7 @@ export interface RunMemory {
|
|
|
27
36
|
}
|
|
28
37
|
export type ModelTier = "opus" | "sonnet" | "haiku" | "unknown";
|
|
29
38
|
export declare function detectModelTier(model: string): ModelTier;
|
|
39
|
+
export declare function getPlannerRateLimitInfo(): PlannerRateLimitInfo;
|
|
30
40
|
export declare function planTasks(objective: string, cwd: string, plannerModel: string, workerModel: string, permissionMode: PermMode, budget: number | undefined, concurrency: number, onLog: (text: string) => void, flexNote?: string, outFile?: string): Promise<Task[]>;
|
|
31
41
|
export declare function identifyThemes(objective: string, count: number, model: string, permissionMode: PermMode): Promise<string[]>;
|
|
32
42
|
export declare function buildThinkingTasks(objective: string, themes: string[], designDir: string, plannerModel: string, previousKnowledge?: string): Task[];
|
package/dist/planner.js
CHANGED
|
@@ -172,7 +172,13 @@ async function runPlannerQuery(prompt, opts, onLog) {
|
|
|
172
172
|
}
|
|
173
173
|
throw new Error("Planner query failed after retries");
|
|
174
174
|
}
|
|
175
|
+
/** Shared mutable rate limit state that planner queries write to for UI display. Reset per query. */
|
|
176
|
+
let _plannerRateLimitInfo = {
|
|
177
|
+
utilization: 0, status: "", isUsingOverage: false, windows: new Map(), costUsd: 0,
|
|
178
|
+
};
|
|
179
|
+
export function getPlannerRateLimitInfo() { return _plannerRateLimitInfo; }
|
|
175
180
|
async function runPlannerQueryOnce(prompt, opts, onLog) {
|
|
181
|
+
_plannerRateLimitInfo = { utilization: 0, status: "", isUsingOverage: false, windows: new Map(), costUsd: 0 };
|
|
176
182
|
let resultText = "";
|
|
177
183
|
const startedAt = Date.now();
|
|
178
184
|
const pq = query({
|
|
@@ -191,14 +197,18 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
|
|
|
191
197
|
// Progress ticker — fast updates with compact format
|
|
192
198
|
let lastLogText = "";
|
|
193
199
|
let toolCount = 0;
|
|
200
|
+
let costUsd = 0;
|
|
194
201
|
const ticker = setInterval(() => {
|
|
195
202
|
const elapsed = Math.round((Date.now() - startedAt) / 1000);
|
|
196
203
|
const m = Math.floor(elapsed / 60);
|
|
197
204
|
const s = elapsed % 60;
|
|
198
205
|
const timeStr = m > 0 ? `${m}m ${s}s` : `${s}s`;
|
|
199
206
|
const toolStr = toolCount > 0 ? ` · ${toolCount} tools` : "";
|
|
207
|
+
const costStr = costUsd > 0 ? ` · $${costUsd.toFixed(3)}` : "";
|
|
208
|
+
const rlPct = _plannerRateLimitInfo.utilization;
|
|
209
|
+
const rlStr = rlPct > 0 ? ` · ${Math.round(rlPct * 100)}%` : "";
|
|
200
210
|
const extra = lastLogText ? ` · ${lastLogText}` : "";
|
|
201
|
-
onLog(`${timeStr}${toolStr}${extra}`);
|
|
211
|
+
onLog(`${timeStr}${toolStr}${costStr}${rlStr}${extra}`);
|
|
202
212
|
}, 500);
|
|
203
213
|
let lastActivity = Date.now();
|
|
204
214
|
let timer;
|
|
@@ -235,11 +245,35 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
|
|
|
235
245
|
}
|
|
236
246
|
}
|
|
237
247
|
}
|
|
248
|
+
if (msg.type === "rate_limit_event") {
|
|
249
|
+
const info = msg.rate_limit_info;
|
|
250
|
+
if (info) {
|
|
251
|
+
_plannerRateLimitInfo.utilization = info.utilization ?? 0;
|
|
252
|
+
_plannerRateLimitInfo.status = info.status ?? "";
|
|
253
|
+
if (info.isUsingOverage)
|
|
254
|
+
_plannerRateLimitInfo.isUsingOverage = true;
|
|
255
|
+
if (info.resetsAt)
|
|
256
|
+
_plannerRateLimitInfo.resetsAt = info.resetsAt;
|
|
257
|
+
if (info.rateLimitType) {
|
|
258
|
+
_plannerRateLimitInfo.windows.set(info.rateLimitType, {
|
|
259
|
+
type: info.rateLimitType,
|
|
260
|
+
utilization: info.utilization ?? 0,
|
|
261
|
+
status: info.status,
|
|
262
|
+
resetsAt: info.resetsAt,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
238
267
|
if (msg.type === "result") {
|
|
268
|
+
const r = msg;
|
|
269
|
+
if (typeof r.total_cost_usd === "number") {
|
|
270
|
+
costUsd = r.total_cost_usd;
|
|
271
|
+
_plannerRateLimitInfo.costUsd += costUsd;
|
|
272
|
+
}
|
|
239
273
|
if (msg.subtype === "success")
|
|
240
|
-
resultText =
|
|
274
|
+
resultText = r.result || "";
|
|
241
275
|
else
|
|
242
|
-
throw new Error(`Planner failed: ${
|
|
276
|
+
throw new Error(`Planner failed: ${r.result || msg.subtype}`);
|
|
243
277
|
}
|
|
244
278
|
}
|
|
245
279
|
};
|
package/dist/swarm.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Task, AgentState, SwarmPhase, PermMode, MergeStrategy } from "./types.js";
|
|
1
|
+
import type { Task, AgentState, SwarmPhase, PermMode, MergeStrategy, RateLimitWindow } from "./types.js";
|
|
2
2
|
export interface SwarmConfig {
|
|
3
3
|
tasks: Task[];
|
|
4
4
|
concurrency: number;
|
|
@@ -12,6 +12,10 @@ export interface SwarmConfig {
|
|
|
12
12
|
mergeStrategy?: MergeStrategy;
|
|
13
13
|
/** Stop dispatching new tasks when rate-limit utilization reaches this fraction (0-1). */
|
|
14
14
|
usageCap?: number;
|
|
15
|
+
/** Allow agents to use extra usage (overage billing). Default false. */
|
|
16
|
+
allowExtraUsage?: boolean;
|
|
17
|
+
/** Max $ to spend on extra usage before stopping. Only applies when allowExtraUsage is true. */
|
|
18
|
+
extraUsageBudget?: number;
|
|
15
19
|
}
|
|
16
20
|
export interface MergeResult {
|
|
17
21
|
branch: string;
|
|
@@ -42,6 +46,14 @@ export declare class Swarm {
|
|
|
42
46
|
rateLimitUtilization: number;
|
|
43
47
|
rateLimitStatus: string;
|
|
44
48
|
rateLimitResetsAt?: number;
|
|
49
|
+
/** Per-window rate limit snapshots (updated on every rate_limit_event). */
|
|
50
|
+
rateLimitWindows: Map<string, RateLimitWindow>;
|
|
51
|
+
/** Whether any agent is currently using extra/overage usage. */
|
|
52
|
+
isUsingOverage: boolean;
|
|
53
|
+
/** Why overage is disabled (if applicable). */
|
|
54
|
+
overageDisabledReason?: string;
|
|
55
|
+
/** Accumulated cost from extra/overage usage only. */
|
|
56
|
+
overageCostUsd: number;
|
|
45
57
|
private queue;
|
|
46
58
|
private config;
|
|
47
59
|
private nextId;
|
|
@@ -50,7 +62,9 @@ export declare class Swarm {
|
|
|
50
62
|
private cleanedUp;
|
|
51
63
|
logFile?: string;
|
|
52
64
|
readonly model: string | undefined;
|
|
53
|
-
|
|
65
|
+
usageCap: number | undefined;
|
|
66
|
+
readonly allowExtraUsage: boolean;
|
|
67
|
+
readonly extraUsageBudget: number | undefined;
|
|
54
68
|
constructor(config: SwarmConfig);
|
|
55
69
|
get active(): number;
|
|
56
70
|
get pending(): number;
|
|
@@ -60,6 +74,7 @@ export declare class Swarm {
|
|
|
60
74
|
logSequence: number;
|
|
61
75
|
log(agentId: number, text: string): void;
|
|
62
76
|
private worker;
|
|
77
|
+
private capForOverage;
|
|
63
78
|
private throttle;
|
|
64
79
|
private runAgent;
|
|
65
80
|
private autoCommit;
|
package/dist/swarm.js
CHANGED
|
@@ -22,6 +22,14 @@ export class Swarm {
|
|
|
22
22
|
rateLimitUtilization = 0;
|
|
23
23
|
rateLimitStatus = "";
|
|
24
24
|
rateLimitResetsAt;
|
|
25
|
+
/** Per-window rate limit snapshots (updated on every rate_limit_event). */
|
|
26
|
+
rateLimitWindows = new Map();
|
|
27
|
+
/** Whether any agent is currently using extra/overage usage. */
|
|
28
|
+
isUsingOverage = false;
|
|
29
|
+
/** Why overage is disabled (if applicable). */
|
|
30
|
+
overageDisabledReason;
|
|
31
|
+
/** Accumulated cost from extra/overage usage only. */
|
|
32
|
+
overageCostUsd = 0;
|
|
25
33
|
queue;
|
|
26
34
|
config;
|
|
27
35
|
nextId = 0;
|
|
@@ -30,7 +38,9 @@ export class Swarm {
|
|
|
30
38
|
cleanedUp = false;
|
|
31
39
|
logFile;
|
|
32
40
|
model;
|
|
33
|
-
usageCap;
|
|
41
|
+
usageCap; // mutable — can be changed live
|
|
42
|
+
allowExtraUsage;
|
|
43
|
+
extraUsageBudget;
|
|
34
44
|
constructor(config) {
|
|
35
45
|
if (!config.tasks.length) {
|
|
36
46
|
throw new Error("SwarmConfig: tasks array must not be empty");
|
|
@@ -52,6 +62,8 @@ export class Swarm {
|
|
|
52
62
|
this.config = config;
|
|
53
63
|
this.model = config.model;
|
|
54
64
|
this.usageCap = config.usageCap;
|
|
65
|
+
this.allowExtraUsage = config.allowExtraUsage ?? false;
|
|
66
|
+
this.extraUsageBudget = config.extraUsageBudget;
|
|
55
67
|
this.queue = [...config.tasks];
|
|
56
68
|
this.total = config.tasks.length;
|
|
57
69
|
}
|
|
@@ -119,14 +131,33 @@ export class Swarm {
|
|
|
119
131
|
}
|
|
120
132
|
this.log(-1, `Worker finished (${tasksProcessed} tasks)`);
|
|
121
133
|
}
|
|
134
|
+
capForOverage(reason) {
|
|
135
|
+
if (this.cappedOut)
|
|
136
|
+
return;
|
|
137
|
+
this.cappedOut = true;
|
|
138
|
+
this.queue.length = 0;
|
|
139
|
+
this.log(-1, reason);
|
|
140
|
+
}
|
|
122
141
|
async throttle() {
|
|
142
|
+
if (this.cappedOut)
|
|
143
|
+
return;
|
|
123
144
|
// Usage cap: stop dispatching when utilization exceeds user's cap
|
|
124
|
-
const cap = this.
|
|
145
|
+
const cap = this.usageCap;
|
|
125
146
|
if (cap != null && cap < 1 && this.rateLimitUtilization >= cap) {
|
|
126
147
|
this.cappedOut = true;
|
|
127
148
|
this.log(-1, `Usage cap ${Math.round(cap * 100)}% reached (at ${Math.round(this.rateLimitUtilization * 100)}%) — finishing active agents, no new tasks`);
|
|
128
149
|
return;
|
|
129
150
|
}
|
|
151
|
+
// Extra usage enforcement: stop if overage detected and not allowed
|
|
152
|
+
if (this.isUsingOverage && !this.allowExtraUsage) {
|
|
153
|
+
this.capForOverage(`Extra usage detected but not allowed — stopping dispatch`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Extra usage budget enforcement
|
|
157
|
+
if (this.isUsingOverage && this.extraUsageBudget != null && this.overageCostUsd >= this.extraUsageBudget) {
|
|
158
|
+
this.capForOverage(`Extra usage budget $${this.extraUsageBudget} reached ($${this.overageCostUsd.toFixed(2)} spent) — stopping dispatch`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
130
161
|
// Hard block: rate limit rejected — wait until reset
|
|
131
162
|
if (this.rateLimitResetsAt) {
|
|
132
163
|
const resetTarget = this.rateLimitResetsAt;
|
|
@@ -604,6 +635,8 @@ export class Swarm {
|
|
|
604
635
|
const cost = safeAdd(r.total_cost_usd);
|
|
605
636
|
agent.costUsd = cost;
|
|
606
637
|
this.totalCostUsd += cost;
|
|
638
|
+
if (this.isUsingOverage)
|
|
639
|
+
this.overageCostUsd += cost;
|
|
607
640
|
if (r.usage) {
|
|
608
641
|
this.totalInputTokens += safeAdd(r.usage.input_tokens);
|
|
609
642
|
this.totalOutputTokens += safeAdd(r.usage.output_tokens);
|
|
@@ -629,8 +662,29 @@ export class Swarm {
|
|
|
629
662
|
if (info.status === "rejected" && info.resetsAt) {
|
|
630
663
|
this.rateLimitResetsAt = info.resetsAt;
|
|
631
664
|
}
|
|
665
|
+
// Track per-window state
|
|
666
|
+
const windowType = info.rateLimitType;
|
|
667
|
+
if (windowType) {
|
|
668
|
+
this.rateLimitWindows.set(windowType, {
|
|
669
|
+
type: windowType,
|
|
670
|
+
utilization: info.utilization ?? 0,
|
|
671
|
+
status: info.status,
|
|
672
|
+
resetsAt: info.resetsAt,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
// Track overage state
|
|
676
|
+
if (info.isUsingOverage) {
|
|
677
|
+
this.isUsingOverage = true;
|
|
678
|
+
}
|
|
679
|
+
if (info.overageDisabledReason) {
|
|
680
|
+
this.overageDisabledReason = info.overageDisabledReason;
|
|
681
|
+
}
|
|
682
|
+
if (this.isUsingOverage && !this.allowExtraUsage) {
|
|
683
|
+
this.capForOverage(`Extra usage detected but not allowed — stopping dispatch`);
|
|
684
|
+
}
|
|
632
685
|
const pct = info.utilization != null ? `${Math.round(info.utilization * 100)}%` : "";
|
|
633
|
-
this.
|
|
686
|
+
const overageTag = this.isUsingOverage ? " [EXTRA]" : "";
|
|
687
|
+
this.log(agent.id, `Rate: ${info.status} ${pct}${overageTag}${windowType ? ` (${windowType})` : ""}`);
|
|
634
688
|
break;
|
|
635
689
|
}
|
|
636
690
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -103,6 +103,13 @@ export interface BranchRecord {
|
|
|
103
103
|
filesChanged: number;
|
|
104
104
|
costUsd: number;
|
|
105
105
|
}
|
|
106
|
+
/** Per-window rate limit snapshot (matches SDK rateLimitType). */
|
|
107
|
+
export interface RateLimitWindow {
|
|
108
|
+
type: string;
|
|
109
|
+
utilization: number;
|
|
110
|
+
status: string;
|
|
111
|
+
resetsAt?: number;
|
|
112
|
+
}
|
|
106
113
|
/** Persisted run state for crash recovery and resume. */
|
|
107
114
|
export interface RunState {
|
|
108
115
|
id: string;
|
|
@@ -114,6 +121,8 @@ export interface RunState {
|
|
|
114
121
|
concurrency: number;
|
|
115
122
|
permissionMode: PermMode;
|
|
116
123
|
usageCap?: number;
|
|
124
|
+
allowExtraUsage: boolean;
|
|
125
|
+
extraUsageBudget?: number;
|
|
117
126
|
flex: boolean;
|
|
118
127
|
useWorktrees: boolean;
|
|
119
128
|
mergeStrategy: MergeStrategy;
|
|
@@ -125,7 +134,7 @@ export interface RunState {
|
|
|
125
134
|
accCompleted: number;
|
|
126
135
|
accFailed: number;
|
|
127
136
|
branches: BranchRecord[];
|
|
128
|
-
phase: "executing" | "steering" | "reflecting" | "done";
|
|
137
|
+
phase: "executing" | "steering" | "reflecting" | "capped" | "done";
|
|
129
138
|
startedAt: string;
|
|
130
139
|
cwd: string;
|
|
131
140
|
}
|
package/dist/ui.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { Swarm } from "./swarm.js";
|
|
2
|
-
export declare function renderFrame(swarm: Swarm): string;
|
|
3
|
-
|
|
2
|
+
export declare function renderFrame(swarm: Swarm, showHotkeys?: boolean): string;
|
|
3
|
+
/** Mutable config that can be changed live during execution. */
|
|
4
|
+
export interface LiveConfig {
|
|
5
|
+
remaining: number;
|
|
6
|
+
usageCap: number | undefined;
|
|
7
|
+
/** Set by hotkey handler when user changes a value. Cleared after main loop reads it. */
|
|
8
|
+
dirty: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function startRenderLoop(swarm: Swarm, liveConfig?: LiveConfig): () => void;
|
|
4
11
|
export declare function renderSummary(swarm: Swarm): string;
|
package/dist/ui.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
const SPINNER = ["|", "/", "-", "\\"];
|
|
3
|
+
const WINDOW_SHORT_NAMES = {
|
|
4
|
+
five_hour: "5h", seven_day: "7d", seven_day_opus: "7d op",
|
|
5
|
+
seven_day_sonnet: "7d sn", overage: "extra",
|
|
6
|
+
};
|
|
3
7
|
function colorEvent(text) {
|
|
4
8
|
if (text === "Done" || text.startsWith("Merged ") || text.startsWith("Committed "))
|
|
5
9
|
return chalk.green(text);
|
|
@@ -11,7 +15,7 @@ function colorEvent(text) {
|
|
|
11
15
|
return chalk.yellow(text);
|
|
12
16
|
return text;
|
|
13
17
|
}
|
|
14
|
-
export function renderFrame(swarm) {
|
|
18
|
+
export function renderFrame(swarm, showHotkeys = false) {
|
|
15
19
|
const w = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
|
16
20
|
const out = [];
|
|
17
21
|
// ── Header ──
|
|
@@ -43,33 +47,58 @@ export function renderFrame(swarm) {
|
|
|
43
47
|
: "";
|
|
44
48
|
out.push(chalk.gray(` \u2191 ${tokIn} in \u2193 ${tokOut} out`) +
|
|
45
49
|
(cost ? ` ${cost}` : ""));
|
|
46
|
-
// ── Usage bar ──
|
|
50
|
+
// ── Usage bar(s) — cycle through windows every 3s ──
|
|
51
|
+
const windows = Array.from(swarm.rateLimitWindows.values());
|
|
47
52
|
const rlPct = swarm.rateLimitUtilization;
|
|
48
|
-
if (rlPct > 0 || swarm.rateLimitResetsAt || swarm.cappedOut) {
|
|
53
|
+
if (rlPct > 0 || swarm.rateLimitResetsAt || swarm.cappedOut || windows.length > 0) {
|
|
49
54
|
const barW = Math.min(30, w - 40);
|
|
50
|
-
const filled = Math.round(rlPct * barW);
|
|
51
55
|
const capFrac = swarm.usageCap;
|
|
52
56
|
const capMark = capFrac != null && capFrac < 1 ? Math.round(capFrac * barW) : -1;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
// Show primary usage bar
|
|
58
|
+
const renderBar = (pct, windowLabel) => {
|
|
59
|
+
const filled = Math.round(pct * barW);
|
|
60
|
+
let barStr = "";
|
|
61
|
+
for (let i = 0; i < barW; i++) {
|
|
62
|
+
if (i === capMark)
|
|
63
|
+
barStr += chalk.yellow("\u2502");
|
|
64
|
+
else if (i < filled)
|
|
65
|
+
barStr += pct > 0.9 ? chalk.red("\u2588") : pct > 0.75 ? chalk.yellow("\u2588") : chalk.blue("\u2588");
|
|
66
|
+
else
|
|
67
|
+
barStr += chalk.gray("\u2591");
|
|
68
|
+
}
|
|
69
|
+
let label = `${Math.round(pct * 100)}% used`;
|
|
70
|
+
if (swarm.cappedOut) {
|
|
71
|
+
if (swarm.isUsingOverage && !swarm.allowExtraUsage) {
|
|
72
|
+
label = chalk.red("Extra usage blocked — stopping");
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
label = chalk.yellow(`Capped at ${capFrac != null ? Math.round(capFrac * 100) : 100}% — finishing active`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else if (swarm.rateLimitResetsAt) {
|
|
79
|
+
const waitSec = Math.max(0, Math.ceil((swarm.rateLimitResetsAt - Date.now()) / 1000));
|
|
80
|
+
const mm = Math.floor(waitSec / 60);
|
|
81
|
+
const ss = waitSec % 60;
|
|
82
|
+
label = chalk.red(`Waiting for reset ${mm > 0 ? `${mm}m ${ss}s` : `${ss}s`}`);
|
|
83
|
+
}
|
|
84
|
+
if (swarm.isUsingOverage && !swarm.cappedOut)
|
|
85
|
+
label += chalk.red(" [EXTRA USAGE]");
|
|
86
|
+
const prefix = windowLabel ? chalk.dim(windowLabel.padEnd(6)) : chalk.dim("Usage ");
|
|
87
|
+
out.push(` ${prefix}${barStr} ${label}`);
|
|
88
|
+
};
|
|
89
|
+
if (windows.length > 1) {
|
|
90
|
+
// Cycle through windows every 3 seconds
|
|
91
|
+
const cycleIdx = Math.floor(Date.now() / 3000) % windows.length;
|
|
92
|
+
const win = windows[cycleIdx];
|
|
93
|
+
const shortName = WINDOW_SHORT_NAMES[win.type] ?? win.type.replace(/_/g, " ");
|
|
94
|
+
renderBar(win.utilization, shortName);
|
|
95
|
+
// Show dots indicator for which window we're viewing
|
|
96
|
+
const dots = windows.map((_, i) => i === cycleIdx ? "●" : "○").join("");
|
|
97
|
+
out[out.length - 1] += chalk.dim(` ${dots}`);
|
|
65
98
|
}
|
|
66
|
-
else
|
|
67
|
-
|
|
68
|
-
const mm = Math.floor(waitSec / 60);
|
|
69
|
-
const ss = waitSec % 60;
|
|
70
|
-
label = chalk.red(`Waiting for reset ${mm > 0 ? `${mm}m ${ss}s` : `${ss}s`}`);
|
|
99
|
+
else {
|
|
100
|
+
renderBar(rlPct);
|
|
71
101
|
}
|
|
72
|
-
out.push(` ${chalk.dim("Usage")} ${barStr} ${label}`);
|
|
73
102
|
}
|
|
74
103
|
out.push("");
|
|
75
104
|
// ── Agent table ──
|
|
@@ -115,6 +144,8 @@ export function renderFrame(swarm) {
|
|
|
115
144
|
: chalk.cyan(`[${entry.agentId}]`);
|
|
116
145
|
out.push(chalk.gray(` ${t} `) + tag + ` ${colorEvent(truncate(entry.text, w - 22))}`);
|
|
117
146
|
}
|
|
147
|
+
if (showHotkeys)
|
|
148
|
+
out.push(chalk.dim(" [b] budget [t] threshold [q] stop"));
|
|
118
149
|
out.push("");
|
|
119
150
|
return out.join("\n");
|
|
120
151
|
}
|
|
@@ -170,7 +201,7 @@ function fmtDur(ms) {
|
|
|
170
201
|
return `${m}m ${s % 60}s`;
|
|
171
202
|
return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
172
203
|
}
|
|
173
|
-
export function startRenderLoop(swarm) {
|
|
204
|
+
export function startRenderLoop(swarm, liveConfig) {
|
|
174
205
|
if (!process.stdout.isTTY) {
|
|
175
206
|
return startPlainLog(swarm);
|
|
176
207
|
}
|
|
@@ -180,17 +211,93 @@ export function startRenderLoop(swarm) {
|
|
|
180
211
|
catch {
|
|
181
212
|
return () => { };
|
|
182
213
|
}
|
|
214
|
+
// Live hotkey input state
|
|
215
|
+
let inputMode = "none";
|
|
216
|
+
let inputBuf = "";
|
|
217
|
+
const hasHotkeys = !!liveConfig && !!process.stdin.isTTY;
|
|
218
|
+
const render = () => {
|
|
219
|
+
let frame = renderFrame(swarm, hasHotkeys);
|
|
220
|
+
if (inputMode !== "none") {
|
|
221
|
+
const label = inputMode === "budget" ? "New budget (remaining sessions)" : "New usage cap (0-100%)";
|
|
222
|
+
frame += `\n ${chalk.cyan(">")} ${label}: ${inputBuf}█`;
|
|
223
|
+
}
|
|
224
|
+
return frame;
|
|
225
|
+
};
|
|
183
226
|
const interval = setInterval(() => {
|
|
184
227
|
try {
|
|
185
228
|
process.stdout.write("\x1B[H\x1B[J");
|
|
186
|
-
process.stdout.write(
|
|
229
|
+
process.stdout.write(render());
|
|
187
230
|
}
|
|
188
231
|
catch {
|
|
189
232
|
clearInterval(interval);
|
|
190
233
|
}
|
|
191
234
|
}, 250);
|
|
235
|
+
// Keyboard listener for live controls
|
|
236
|
+
let keyHandler;
|
|
237
|
+
if (liveConfig && process.stdin.isTTY) {
|
|
238
|
+
try {
|
|
239
|
+
process.stdin.setRawMode(true);
|
|
240
|
+
process.stdin.resume();
|
|
241
|
+
}
|
|
242
|
+
catch { }
|
|
243
|
+
keyHandler = (buf) => {
|
|
244
|
+
const s = buf.toString();
|
|
245
|
+
if (inputMode !== "none") {
|
|
246
|
+
if (s === "\r" || s === "\n") {
|
|
247
|
+
const val = parseFloat(inputBuf);
|
|
248
|
+
if (inputMode === "budget" && !isNaN(val) && val > 0) {
|
|
249
|
+
liveConfig.remaining = Math.round(val);
|
|
250
|
+
liveConfig.dirty = true;
|
|
251
|
+
swarm.log(-1, `Budget changed to ${liveConfig.remaining} remaining`);
|
|
252
|
+
}
|
|
253
|
+
else if (inputMode === "threshold" && !isNaN(val) && val >= 0 && val <= 100) {
|
|
254
|
+
const frac = val / 100;
|
|
255
|
+
liveConfig.usageCap = frac > 0 ? frac : undefined;
|
|
256
|
+
liveConfig.dirty = true;
|
|
257
|
+
swarm.usageCap = liveConfig.usageCap;
|
|
258
|
+
swarm.log(-1, `Usage cap changed to ${val > 0 ? val + "%" : "unlimited"}`);
|
|
259
|
+
}
|
|
260
|
+
inputMode = "none";
|
|
261
|
+
inputBuf = "";
|
|
262
|
+
}
|
|
263
|
+
else if (s === "\x1B" || s === "\x03") {
|
|
264
|
+
inputMode = "none";
|
|
265
|
+
inputBuf = "";
|
|
266
|
+
}
|
|
267
|
+
else if (s === "\x7F") {
|
|
268
|
+
inputBuf = inputBuf.slice(0, -1);
|
|
269
|
+
}
|
|
270
|
+
else if (/^[0-9.]$/.test(s)) {
|
|
271
|
+
inputBuf += s;
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (s === "b" || s === "B") {
|
|
276
|
+
inputMode = "budget";
|
|
277
|
+
inputBuf = "";
|
|
278
|
+
}
|
|
279
|
+
else if (s === "t" || s === "T") {
|
|
280
|
+
inputMode = "threshold";
|
|
281
|
+
inputBuf = "";
|
|
282
|
+
}
|
|
283
|
+
else if (s === "q" || s === "Q" || s === "\x03") {
|
|
284
|
+
if (swarm.aborted)
|
|
285
|
+
process.exit(0); // second press = force quit
|
|
286
|
+
swarm.abort();
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
process.stdin.on("data", keyHandler);
|
|
290
|
+
}
|
|
192
291
|
return () => {
|
|
193
292
|
clearInterval(interval);
|
|
293
|
+
if (keyHandler) {
|
|
294
|
+
process.stdin.removeListener("data", keyHandler);
|
|
295
|
+
try {
|
|
296
|
+
process.stdin.setRawMode(false);
|
|
297
|
+
process.stdin.pause();
|
|
298
|
+
}
|
|
299
|
+
catch { }
|
|
300
|
+
}
|
|
194
301
|
try {
|
|
195
302
|
process.stdout.write("\x1B[H\x1B[J");
|
|
196
303
|
process.stdout.write(renderFrame(swarm));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Run 10, 100, or 1000 Claude agents overnight. Parallel autonomous AI coding with thinking waves, iterative quality steering, crash recovery, and rate limit handling. Built on the Claude Agent SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|