feat(seats): memory, the Playwright MCP config, and the seats library
:Memory: MemAvailable and the reserve policy, the sampler of a pass's peak tree PSS (one sweep, a pause of three sweeps at least, stopped with the driver), and the calibration file every driver on the machine shares, re-read and written under its lock. :Mcp: the pinned @playwright/mcp 0.0.83 arguments and the --mcp-config of one pass, compact and in a fixed key order. :Seats: the dotenv loader (token lines only, TOKEN7 inert), the one-word probe (run under the supervisor, a missing binary reported with its errno text), the window maths, the ranking, the Fable merge, the editor-bundle lookup of the claude binary, the redaction of anything token-shaped, and the report. This libc++ has no tzdb, so a zone by name converts through the C library with TZ set, reading the system's zoneinfo. tests/Seats: 92 checks against reference vectors, numbered in order, all green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e8a58f893f
commit
8ffbd6cf5d
9 changed files with 1764 additions and 3 deletions
44
implementations/Crafter.Review-Mcp.cpp
Normal file
44
implementations/Crafter.Review-Mcp.cpp
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module Crafter.Review:Mcp_impl;
|
||||
import :Mcp;
|
||||
import :Json;
|
||||
import :Text;
|
||||
import :Tools;
|
||||
import std;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace Crafter::Review::Mcp {
|
||||
std::vector<std::string> DefaultArgs() {
|
||||
return {"-y", std::format("@playwright/mcp@{}", PlaywrightMcpVersion), "--headless", "--isolated", "--browser", "chromium"};
|
||||
}
|
||||
|
||||
Json PlaywrightConfig(const Json& functional, const std::string& outputDir) {
|
||||
const Json& mcp = Dyn::Or(Dyn::Get(Dyn::Or(functional, Dyn::EmptyDict()), "mcp"), Dyn::EmptyDict());
|
||||
Json args = Json::array();
|
||||
const Json& configured = Dyn::Get(mcp, "args");
|
||||
if (!configured.is_null()) {
|
||||
for (const Json& arg : configured) args.push_back(arg);
|
||||
} else {
|
||||
for (const std::string& arg : DefaultArgs()) args.push_back(arg);
|
||||
}
|
||||
if (!Dyn::In(Json("--output-dir"), args)) {
|
||||
args.push_back("--output-dir");
|
||||
args.push_back(outputDir);
|
||||
}
|
||||
const Json& command = Dyn::Get(mcp, "command");
|
||||
return {{"mcpServers", {{std::string(Tools::McpServer), {{"command", Dyn::Truthy(command) ? command : Json("npx")}, {"args", std::move(args)}}}}}};
|
||||
}
|
||||
|
||||
std::string ConfigJson(const Json& config) {
|
||||
return config.dump(-1, ' ', true, Json::error_handler_t::replace);
|
||||
}
|
||||
|
||||
fs::path OutputDir(const fs::path& scratch, std::string_view label) {
|
||||
const fs::path dir = scratch / "playwright" / Dyn::Replace(Dyn::Replace(label, "/", "_"), " ", "_");
|
||||
fs::create_directories(dir);
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
214
implementations/Crafter.Review-Memory.cpp
Normal file
214
implementations/Crafter.Review-Memory.cpp
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module Crafter.Review:Memory_impl;
|
||||
import :Memory;
|
||||
import :Json;
|
||||
import :Text;
|
||||
import :Platform;
|
||||
import std;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace Crafter::Review::Memory {
|
||||
std::optional<std::int64_t> Available(const fs::path& path) {
|
||||
const std::map<std::string, std::int64_t> info = Platform::MemInfo(path);
|
||||
const auto found = info.find("MemAvailable");
|
||||
if (found == info.end()) return std::nullopt;
|
||||
return found->second;
|
||||
}
|
||||
|
||||
std::int64_t Reserve(const std::optional<std::string>& configured, const fs::path& path) {
|
||||
if (configured && !configured->empty()) {
|
||||
const std::optional<double> value = Dyn::ParseFloat(*configured);
|
||||
if (!value) throw std::invalid_argument(Dyn::FloatError(*configured));
|
||||
return static_cast<std::int64_t>(*value);
|
||||
}
|
||||
const std::map<std::string, std::int64_t> info = Platform::MemInfo(path);
|
||||
const auto total = info.find("MemTotal");
|
||||
return std::max<std::int64_t>(2048, (total == info.end() ? 0 : total->second) / 10);
|
||||
}
|
||||
|
||||
std::int64_t Reserve() {
|
||||
return Reserve(Platform::GetEnv("REVIEW_GATES_MEM_RESERVE_MB"));
|
||||
}
|
||||
|
||||
struct Sampler::State {
|
||||
double interval = 3.0;
|
||||
Measure measure;
|
||||
Scan scan;
|
||||
std::mutex lock;
|
||||
std::condition_variable wake;
|
||||
std::map<std::string, std::int64_t> watched;
|
||||
std::map<std::string, double> peaks;
|
||||
bool started = false;
|
||||
bool stopping = false;
|
||||
std::thread thread;
|
||||
|
||||
double Sample() {
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
std::vector<std::pair<std::string, std::int64_t>> items;
|
||||
{
|
||||
const std::lock_guard guard(lock);
|
||||
items.assign(watched.begin(), watched.end());
|
||||
}
|
||||
if (!items.empty()) {
|
||||
const std::map<std::int64_t, std::vector<std::int64_t>> kids = scan();
|
||||
for (const auto& [key, pid] : items) {
|
||||
const double value = measure(pid, kids);
|
||||
const std::lock_guard guard(lock);
|
||||
const auto peak = peaks.find(key);
|
||||
if (peak != peaks.end()) peak->second = std::max(peak->second, value);
|
||||
}
|
||||
}
|
||||
return std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count();
|
||||
}
|
||||
|
||||
void Loop() {
|
||||
double sweep = 0.0;
|
||||
std::unique_lock guard(lock);
|
||||
while (!stopping) {
|
||||
wake.wait_for(guard, std::chrono::duration<double>(Pause(interval, sweep)), [this] { return stopping; });
|
||||
if (stopping) break;
|
||||
guard.unlock();
|
||||
try {
|
||||
sweep = Sample();
|
||||
} catch (const std::exception&) {
|
||||
// A measurement never stops the run.
|
||||
sweep = interval;
|
||||
}
|
||||
guard.lock();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Sampler::Sampler(double interval, Measure measure, Scan scan) : state(std::make_shared<State>()) {
|
||||
state->interval = interval;
|
||||
state->measure = measure ? std::move(measure) : Measure([](std::int64_t pid, const std::map<std::int64_t, std::vector<std::int64_t>>& kids) {
|
||||
return Platform::PssMb(Platform::Tree(pid, kids));
|
||||
});
|
||||
state->scan = scan ? std::move(scan) : Scan([] { return Platform::PpidMap(); });
|
||||
}
|
||||
|
||||
Sampler::~Sampler() {
|
||||
{
|
||||
const std::lock_guard guard(state->lock);
|
||||
state->stopping = true;
|
||||
}
|
||||
state->wake.notify_all();
|
||||
if (state->thread.joinable()) state->thread.join();
|
||||
}
|
||||
|
||||
void Sampler::Watch(const std::string& key, std::int64_t pid) {
|
||||
const std::lock_guard guard(state->lock);
|
||||
state->watched[key] = pid;
|
||||
state->peaks[key] = 0.0;
|
||||
if (!state->started) {
|
||||
state->started = true;
|
||||
state->thread = std::thread([owner = state.get()] { owner->Loop(); });
|
||||
}
|
||||
}
|
||||
|
||||
double Sampler::Sample() {
|
||||
return state->Sample();
|
||||
}
|
||||
|
||||
std::optional<double> Sampler::Done(const std::string& key) {
|
||||
const std::lock_guard guard(state->lock);
|
||||
state->watched.erase(key);
|
||||
const auto peak = state->peaks.find(key);
|
||||
if (peak == state->peaks.end()) return std::nullopt;
|
||||
const double value = peak->second;
|
||||
state->peaks.erase(peak);
|
||||
if (value == 0.0) return std::nullopt;
|
||||
return Dyn::Round(value, 1);
|
||||
}
|
||||
|
||||
double Sampler::Pause(double interval, double sweep) {
|
||||
return std::max(interval, 3 * sweep);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// A peak worth keeping: a number, no bool, above zero.
|
||||
bool Positive(const Json& value) {
|
||||
if (!value.is_number()) return false;
|
||||
return value.is_number_float() ? value.get<double>() > 0.0 : (value.is_number_unsigned() ? value.get<std::uint64_t>() > 0 : value.get<std::int64_t>() > 0);
|
||||
}
|
||||
|
||||
double AsDouble(const Json& value) {
|
||||
return value.is_number_float() ? value.get<double>() : (value.is_number_unsigned() ? static_cast<double>(value.get<std::uint64_t>()) : static_cast<double>(value.get<std::int64_t>()));
|
||||
}
|
||||
|
||||
Json Clean(const Json& value) {
|
||||
Json out = Json::array();
|
||||
if (!value.is_array()) return out;
|
||||
for (const Json& item : value) {
|
||||
if (Positive(item)) out.push_back(item);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
Calibration::Calibration(fs::path path, const fs::path& repo, std::map<std::string, std::int64_t> defaults)
|
||||
: path(std::move(path)), defaults(std::move(defaults)) {
|
||||
std::error_code error;
|
||||
this->repo = fs::weakly_canonical(fs::absolute(repo), error).string();
|
||||
}
|
||||
|
||||
Json Calibration::Load() const {
|
||||
try {
|
||||
Json document = Dyn::LoadJsonFile(path);
|
||||
return document.is_object() ? document : Json::object();
|
||||
} catch (const std::exception&) {
|
||||
return Json::object();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Json> Calibration::Peaks(std::string_view environment) const {
|
||||
const Json document = Load();
|
||||
const Json& mine = Dyn::Get(document, repo);
|
||||
const Json peaks = Clean(mine.is_object() ? Dyn::Get(mine, environment) : Json());
|
||||
return {peaks.begin(), peaks.end()};
|
||||
}
|
||||
|
||||
Calibration::Cost Calibration::CostOf(std::string_view environment) const {
|
||||
const std::vector<Json> peaks = Peaks(environment);
|
||||
if (!peaks.empty()) {
|
||||
// max(): the first of equal maxima.
|
||||
const Json* best = &peaks.front();
|
||||
for (const Json& peak : peaks) {
|
||||
if (AsDouble(peak) > AsDouble(*best)) best = &peak;
|
||||
}
|
||||
return {*best, std::format("the highest of its last {} measured peaks", peaks.size())};
|
||||
}
|
||||
const auto found = defaults.find(std::string(environment));
|
||||
std::int64_t fallback = 1024;
|
||||
if (found != defaults.end()) {
|
||||
fallback = found->second;
|
||||
} else if (!defaults.empty()) {
|
||||
fallback = std::ranges::max(std::views::values(defaults));
|
||||
}
|
||||
return {Json(fallback), "the default measured 2026-10-22; none measured in this repository yet"};
|
||||
}
|
||||
|
||||
bool Calibration::Record(std::string_view environment, double megabytes) {
|
||||
if (!(megabytes > 0.0) || !std::isfinite(megabytes)) return false;
|
||||
try {
|
||||
const std::lock_guard guard(lock);
|
||||
// Without the lock the write is still made.
|
||||
const Platform::FileLock fileLock(std::format("{}.lock", path.string()));
|
||||
Json document = Load();
|
||||
Json mine = Dyn::Get(document, repo);
|
||||
if (!mine.is_object()) mine = Json::object();
|
||||
Json peaks = Clean(Dyn::Get(mine, environment));
|
||||
peaks.push_back(Dyn::Round(megabytes, 1));
|
||||
if (peaks.size() > Keep) peaks.erase(peaks.begin(), peaks.begin() + static_cast<std::ptrdiff_t>(peaks.size() - Keep));
|
||||
mine[std::string(environment)] = std::move(peaks);
|
||||
document[repo] = std::move(mine);
|
||||
Platform::WriteAtomic(path, DumpFile(document), 0666);
|
||||
return true;
|
||||
} catch (const std::exception&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
997
implementations/Crafter.Review-Seats.cpp
Normal file
997
implementations/Crafter.Review-Seats.cpp
Normal file
|
|
@ -0,0 +1,997 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
module Crafter.Review:Seats_impl;
|
||||
import :Seats;
|
||||
import :Json;
|
||||
import :Text;
|
||||
import :Stream;
|
||||
import :Platform;
|
||||
import :Process;
|
||||
import :Crypto;
|
||||
import std;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace Crafter::Review::Seats {
|
||||
namespace {
|
||||
constexpr std::array<std::string_view, 7> Weekdays = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
|
||||
// en-GB, as Intl.DateTimeFormat abbreviates them (September is "Sept").
|
||||
constexpr std::array<std::string_view, 12> Months = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"};
|
||||
constexpr std::array<std::string_view, 4> ModelFamilies = {"fable", "opus", "sonnet", "haiku"};
|
||||
|
||||
const std::string& EnvOf(const std::map<std::string, std::string>& env, const std::string& key) {
|
||||
static const std::string none;
|
||||
const auto found = env.find(key);
|
||||
return found == env.end() ? none : found->second;
|
||||
}
|
||||
|
||||
double AsDouble(const Json& value) {
|
||||
if (value.is_number_float()) return value.get<double>();
|
||||
if (value.is_boolean()) return value.get<bool>() ? 1.0 : 0.0;
|
||||
if (value.is_number_unsigned()) return static_cast<double>(value.get<std::uint64_t>());
|
||||
if (value.is_number_integer()) return static_cast<double>(value.get<std::int64_t>());
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// The value as a float, or nullopt for one that is none.
|
||||
std::optional<double> FloatOf(const Json& value) {
|
||||
if (value.is_number() || value.is_boolean()) return AsDouble(value);
|
||||
if (value.is_string()) return Dyn::ParseFloat(value.get_ref<const std::string&>());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// An integer when n is integral, else n.
|
||||
Json IntIfWhole(double n) {
|
||||
if (std::isfinite(n) && n == std::floor(n) && std::fabs(n) < 9.0e18) return Json(static_cast<std::int64_t>(n));
|
||||
return Json(n);
|
||||
}
|
||||
|
||||
// A number field as a float, 0 when it is falsy.
|
||||
double NumberOr0(const Json& value) {
|
||||
return Dyn::Truthy(value) ? AsDouble(value) : 0.0;
|
||||
}
|
||||
|
||||
bool Less(const Json& a, const Json& b) {
|
||||
return AsDouble(a) < AsDouble(b);
|
||||
}
|
||||
|
||||
// An ASCII literal, case-insensitive as re.I reads it, at `at`.
|
||||
bool MatchCi(std::u32string_view text, std::size_t at, std::string_view word) {
|
||||
if (at + word.size() > text.size()) return false;
|
||||
for (std::size_t i = 0; i < word.size(); ++i) {
|
||||
const char p = word[i];
|
||||
if (p >= 'a' && p <= 'z') {
|
||||
if (Dyn::IgnoreCaseLetter(text[at + i]) != p) return false;
|
||||
} else if (text[at + i] != static_cast<char32_t>(static_cast<std::uint8_t>(p))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ContainsCi(std::string_view text, std::string_view word) {
|
||||
const std::u32string decoded = Dyn::Decode(text);
|
||||
for (std::size_t at = 0; at + word.size() <= decoded.size(); ++at) {
|
||||
if (MatchCi(decoded, at, word)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// seats.CREDIT_RE: the organization's credit balance is gone, or a
|
||||
// spend limit was reached.
|
||||
bool CreditText(std::string_view text) {
|
||||
constexpr std::array<std::string_view, 9> Phrases = {
|
||||
"credit balance too low", "credit balance is too low", "add funds", "reached your api usage limits", "reached your specified api usage limits",
|
||||
"reached your workspace api usage limits", "reached your specified workspace api usage limits", "crossed its monthly api usage threshold",
|
||||
"enforced_spend_limit_reached",
|
||||
};
|
||||
return std::ranges::any_of(Phrases, [&](std::string_view phrase) { return ContainsCi(text, phrase); });
|
||||
}
|
||||
|
||||
std::string ResultText(const Json& result) {
|
||||
return Dyn::StrOr(Dyn::Get(Dyn::Or(result, Dyn::EmptyDict()), "result"), "");
|
||||
}
|
||||
|
||||
bool TokenChar(char c) {
|
||||
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-';
|
||||
}
|
||||
|
||||
std::size_t RunOf(std::string_view s, std::size_t at, const std::function<bool(char)>& in) {
|
||||
std::size_t end = at;
|
||||
while (end < s.size() && in(s[end])) ++end;
|
||||
return end - at;
|
||||
}
|
||||
|
||||
// re.sub(pattern, replacement, text) for a pattern given as a matcher:
|
||||
// the length of the match at a position, or 0.
|
||||
std::string SubAll(std::string_view text, const std::function<std::size_t(std::string_view, std::size_t)>& match, std::string_view replacement) {
|
||||
std::string out;
|
||||
std::size_t at = 0;
|
||||
while (at < text.size()) {
|
||||
const std::size_t length = match(text, at);
|
||||
if (length > 0) {
|
||||
out += replacement;
|
||||
at += length;
|
||||
} else {
|
||||
out.push_back(text[at]);
|
||||
++at;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}
|
||||
std::size_t Jwt(std::string_view s, std::size_t at) {
|
||||
if (!s.substr(at).starts_with("eyJ")) return 0;
|
||||
std::size_t i = at + 3;
|
||||
for (std::int32_t part = 0; part < 3; ++part) {
|
||||
if (part > 0) {
|
||||
if (i >= s.size() || s[i] != '.') return 0;
|
||||
++i;
|
||||
}
|
||||
const std::size_t run = RunOf(s, i, TokenChar);
|
||||
if (run < 10) return 0;
|
||||
i += run;
|
||||
}
|
||||
return i - at;
|
||||
}
|
||||
|
||||
// sk-ant-[A-Za-z0-9_-]{20,}
|
||||
std::size_t AnthropicKey(std::string_view s, std::size_t at) {
|
||||
if (!s.substr(at).starts_with("sk-ant-")) return 0;
|
||||
const std::size_t run = RunOf(s, at + 7, TokenChar);
|
||||
return run >= 20 ? 7 + run : 0;
|
||||
}
|
||||
|
||||
// Bearer\s+[A-Za-z0-9._~+/=-]{16,}
|
||||
std::size_t Bearer(std::string_view s, std::size_t at) {
|
||||
if (!s.substr(at).starts_with("Bearer")) return 0;
|
||||
const std::size_t gap = Dyn::SpaceRun(s, at + 6);
|
||||
if (gap == 0) return 0;
|
||||
const std::size_t run = RunOf(s, at + 6 + gap, [](char c) { return TokenChar(c) || std::string_view("._~+/=").contains(c); });
|
||||
return run >= 16 ? 6 + gap + run : 0;
|
||||
}
|
||||
|
||||
// [A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}, as the regex engine
|
||||
// backs off: the local part runs up to the @; the domain is the
|
||||
// longest prefix of its run that ends in a dot and two letters or more.
|
||||
std::size_t Email(std::string_view s, std::size_t at) {
|
||||
auto local = [](char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || std::string_view("._%+-").contains(c); };
|
||||
auto domain = [](char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.' || c == '-'; };
|
||||
auto letter = [](char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); };
|
||||
const std::size_t name = RunOf(s, at, local);
|
||||
if (name == 0 || at + name >= s.size() || s[at + name] != '@') return 0;
|
||||
const std::size_t start = at + name + 1;
|
||||
const std::size_t run = RunOf(s, start, domain);
|
||||
for (std::size_t k = run; k-- > 1;) {
|
||||
if (s[start + k] != '.') continue;
|
||||
const std::size_t letters = RunOf(s, start + k + 1, letter);
|
||||
if (letters >= 2) return name + 1 + k + 1 + letters;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// shutil.which(name, path=...): an executable file on the path.
|
||||
std::optional<std::string> Which(std::string_view name, std::string_view path) {
|
||||
for (const std::string& dir : Dyn::Split(path, ":")) {
|
||||
// An empty PATH entry gives the bare name.
|
||||
const fs::path candidate = dir.empty() ? fs::path(name) : fs::path(dir) / name;
|
||||
std::error_code error;
|
||||
if (fs::is_regular_file(candidate, error) && access(candidate.c_str(), X_OK) == 0) return candidate.string();
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// _version_key: claude-code-<major>.<minor>.<patch>.
|
||||
std::array<std::int64_t, 3> VersionKey(std::string_view name) {
|
||||
const std::size_t at = name.find("claude-code-");
|
||||
if (at == std::string_view::npos) return {0, 0, 0};
|
||||
for (std::size_t from = at; from != std::string_view::npos; from = name.find("claude-code-", from + 1)) {
|
||||
std::array<std::int64_t, 3> parts {};
|
||||
std::size_t i = from + 12;
|
||||
bool ok = true;
|
||||
for (std::int32_t p = 0; p < 3 && ok; ++p) {
|
||||
if (p > 0) {
|
||||
if (i >= name.size() || name[i] != '.') {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
const std::size_t digits = Dyn::DecimalRun(name, i);
|
||||
if (digits == 0) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
parts[static_cast<std::size_t>(p)] = Dyn::ParseInt(name.substr(i, digits)).value_or(0);
|
||||
i += digits;
|
||||
}
|
||||
if (ok) return parts;
|
||||
}
|
||||
return {0, 0, 0};
|
||||
}
|
||||
|
||||
const Json& Window(const Json& seat, std::string_view key) {
|
||||
return Dyn::Get(Dyn::Or(Dyn::Get(seat, "windows"), Dyn::EmptyDict()), key);
|
||||
}
|
||||
|
||||
std::string SeatName(const Json& seat) {
|
||||
const Json& id = Dyn::At(seat, "seat");
|
||||
return Dyn::Equal(id, Json(0)) ? std::string("local") : std::format("seat {}", Dyn::Str(id));
|
||||
}
|
||||
|
||||
std::string SeatCell(const Json& seat) {
|
||||
const Json& id = Dyn::At(seat, "seat");
|
||||
return Dyn::Equal(id, Json(0)) ? std::string("local") : Dyn::Str(id);
|
||||
}
|
||||
|
||||
std::int64_t Inflight(const Json& board, const std::map<std::int32_t, std::string>& keys, const Json& seat) {
|
||||
const auto key = keys.find(Dyn::At(seat, "seat").get<std::int32_t>());
|
||||
if (key == keys.end() || !board.is_object()) return 0;
|
||||
const Json& record = Dyn::Get(Dyn::Or(Dyn::Get(board, "seats"), Dyn::EmptyDict()), key->second);
|
||||
return static_cast<std::int64_t>(AsDouble(Dyn::Get(Dyn::Or(record, Dyn::EmptyDict()), "inflight")));
|
||||
}
|
||||
|
||||
std::vector<Json> SortedBySeat(const std::vector<Json>& seats) {
|
||||
std::vector<Json> out = seats;
|
||||
std::ranges::stable_sort(out, [](const Json& a, const Json& b) { return Less(Dyn::At(a, "seat"), Dyn::At(b, "seat")); });
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
double JsRound(double x) {
|
||||
return std::floor(x + 0.5);
|
||||
}
|
||||
|
||||
std::string Fmt(const Json& value) {
|
||||
if ((value.is_number() || value.is_boolean())) {
|
||||
const double n = AsDouble(value);
|
||||
if (std::isfinite(n) && n == std::floor(n)) return std::to_string(static_cast<std::int64_t>(n));
|
||||
}
|
||||
return Dyn::Str(value);
|
||||
}
|
||||
|
||||
Json DefaultConfig() {
|
||||
return {{"maxUtil", 85}, {"lensCostPct", 10}, {"waitMaxMin", 15}, {"policy", "spread"}, {"handoverPct", 90}};
|
||||
}
|
||||
|
||||
Json ConfigFromEnv(const std::map<std::string, std::string>& env) {
|
||||
Json config = DefaultConfig();
|
||||
const Json defaults = DefaultConfig();
|
||||
for (const auto& [key, variable] : std::array<std::pair<std::string_view, std::string_view>, 4>{{
|
||||
{"maxUtil", "REVIEW_GATES_SEAT_MAX_UTIL"}, {"lensCostPct", "REVIEW_GATES_LENS_COST_PCT"},
|
||||
{"waitMaxMin", "REVIEW_GATES_SEAT_WAIT_MIN"}, {"handoverPct", "REVIEW_GATES_SEAT_HANDOVER_PCT"}}}) {
|
||||
const auto found = env.find(std::string(variable));
|
||||
Json value = defaults.at(std::string(key));
|
||||
if (found != env.end()) {
|
||||
const std::optional<double> n = Dyn::ParseFloat(found->second);
|
||||
if (n && !Dyn::Strip(found->second).empty() && std::isfinite(*n) && *n >= 0) value = IntIfWhole(*n);
|
||||
}
|
||||
config[std::string(key)] = value;
|
||||
}
|
||||
config["policy"] = EnvOf(env, "REVIEW_GATES_SEAT_POLICY") == "pack" ? "pack" : "spread";
|
||||
return config;
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::int32_t, std::string>> SeatTokensFromEnv(const std::map<std::string, std::string>& env) {
|
||||
std::vector<std::pair<std::int32_t, std::string>> out;
|
||||
for (std::int32_t i = 1; i <= MaxSeats; ++i) {
|
||||
std::string token = Dyn::Strip(EnvOf(env, std::format("CLAUDE_CODE_OAUTH_TOKEN{}", i)));
|
||||
if (token.empty() && i == 1) token = Dyn::Strip(EnvOf(env, "CLAUDE_CODE_OAUTH_TOKEN"));
|
||||
if (!token.empty()) out.emplace_back(i, token);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::int32_t, std::string>> ApiKeysFromEnv(const std::map<std::string, std::string>& env) {
|
||||
std::vector<std::pair<std::int32_t, std::string>> out;
|
||||
for (std::int32_t i = 1; i <= MaxSeats; ++i) {
|
||||
const std::string key = Dyn::Strip(EnvOf(env, std::format("ANTHROPIC_API_KEY{}", i)));
|
||||
if (!key.empty()) out.emplace_back(i, key);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::int32_t, std::string>> OpenRouterKeysFromEnv(const std::map<std::string, std::string>& env) {
|
||||
std::vector<std::pair<std::int32_t, std::string>> out;
|
||||
for (std::int32_t i = 1; i <= MaxSeats; ++i) {
|
||||
std::string key = Dyn::Strip(EnvOf(env, std::format("OPENROUTER_KEY{}", i)));
|
||||
if (key.empty() && i == 1) key = Dyn::Strip(EnvOf(env, "OPENROUTER_KEY"));
|
||||
if (!key.empty()) out.emplace_back(i, key);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> SeatTokensFromEnvFile(std::string_view text) {
|
||||
std::map<std::string, std::string> env;
|
||||
for (const std::string& raw : Dyn::Split(text, "\n")) {
|
||||
// ^\s*(?:export\s+)?(NAME)\s*=\s*(.*?)\s*$
|
||||
std::string_view line(raw);
|
||||
std::size_t at = Dyn::SpaceRun(line, 0);
|
||||
if (line.substr(at).starts_with("export")) {
|
||||
const std::size_t gap = Dyn::SpaceRun(line, at + 6);
|
||||
if (gap > 0) at += 6 + gap;
|
||||
}
|
||||
std::string name;
|
||||
for (const std::string_view stem : {std::string_view("CLAUDE_CODE_OAUTH_TOKEN"), std::string_view("ANTHROPIC_API_KEY"), std::string_view("OPENROUTER_KEY")}) {
|
||||
if (!line.substr(at).starts_with(stem)) continue;
|
||||
std::size_t end = at + stem.size();
|
||||
const bool digit = end < line.size() && line[end] >= '1' && line[end] <= '6';
|
||||
if (digit) ++end;
|
||||
if (stem == "ANTHROPIC_API_KEY" && !digit) break;
|
||||
name = std::string(line.substr(at, end - at));
|
||||
at = end;
|
||||
break;
|
||||
}
|
||||
if (name.empty()) continue;
|
||||
at += Dyn::SpaceRun(line, at);
|
||||
if (at >= line.size() || line[at] != '=') continue;
|
||||
++at;
|
||||
at += Dyn::SpaceRun(line, at);
|
||||
std::string value = Dyn::RStrip(line.substr(at));
|
||||
if (value.size() >= 2 && (value.front() == '"' || value.front() == '\'') && value.back() == value.front()) value = value.substr(1, value.size() - 2);
|
||||
env[name] = value;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
Json Pct(const Json& utilization) {
|
||||
const std::optional<double> n = FloatOf(utilization);
|
||||
if (!n || !std::isfinite(*n) || *n < 0) return 0;
|
||||
return IntIfWhole(JsRound(*n * 1000) / 10);
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::int64_t IntOf(const Json& value) {
|
||||
const std::optional<double> n = FloatOf(value);
|
||||
if (!n || !std::isfinite(*n)) return 0;
|
||||
return static_cast<std::int64_t>(*n);
|
||||
}
|
||||
}
|
||||
|
||||
Json ParseRateLimitInfo(const Json& raw) {
|
||||
if (!raw.is_object()) return nullptr;
|
||||
Json windows = Json::object();
|
||||
const Json& unified = Dyn::Get(raw, "unifiedWindows");
|
||||
if (unified.is_object()) {
|
||||
for (auto it = unified.begin(); it != unified.end(); ++it) {
|
||||
if (!it.value().is_object()) continue;
|
||||
windows[it.key()] = {{"utilization", Pct(Dyn::Get(it.value(), "utilization"))}, {"resetsAt", IntOf(Dyn::Get(it.value(), "resetsAt"))}};
|
||||
}
|
||||
}
|
||||
const std::int64_t resets = IntOf(Dyn::Get(raw, "resetsAt"));
|
||||
return {
|
||||
{"status", Dyn::StrOr(Dyn::Get(raw, "status"), "")},
|
||||
{"rateLimitType", Dyn::Truthy(Dyn::Get(raw, "rateLimitType")) ? Json(Dyn::Str(Dyn::At(raw, "rateLimitType"))) : Json()},
|
||||
{"resetsAt", resets ? Json(resets) : Json()},
|
||||
{"windows", std::move(windows)},
|
||||
// overageDisabledReason, e.g. out_of_credits: a limit is a hard 429,
|
||||
// not a charge.
|
||||
{"overageReason", Dyn::Truthy(Dyn::Get(raw, "overageDisabledReason")) ? Json(Dyn::Str(Dyn::At(raw, "overageDisabledReason"))) : Json()},
|
||||
};
|
||||
}
|
||||
|
||||
Json ParseStream(std::string_view text) {
|
||||
Json result;
|
||||
Json rate;
|
||||
Json session;
|
||||
if (Dyn::Strip(text).empty()) return {{"result", nullptr}, {"rateLimit", nullptr}, {"sessionId", nullptr}};
|
||||
for (const std::string& raw : Dyn::Split(text, "\n")) {
|
||||
const std::string line = Dyn::Strip(raw);
|
||||
if (line.empty()) continue;
|
||||
Json o;
|
||||
try {
|
||||
o = ParseLoose(line);
|
||||
} catch (const std::exception&) {
|
||||
continue;
|
||||
}
|
||||
if (!o.is_object()) continue;
|
||||
const Json& id = Dyn::Get(o, "session_id");
|
||||
if (!Dyn::Truthy(session) && id.is_string()) {
|
||||
const std::string& value = id.get_ref<const std::string&>();
|
||||
const bool shaped = value.size() == 36 && std::ranges::all_of(value, [](char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') || c == '-'; });
|
||||
if (shaped) session = id;
|
||||
}
|
||||
if (Dyn::Get(o, "type") == "result") {
|
||||
result = o;
|
||||
} else if (Dyn::Get(o, "type") == "rate_limit_event") {
|
||||
const Json parsed = ParseRateLimitInfo(Dyn::Get(o, "rate_limit_info"));
|
||||
if (Dyn::Truthy(parsed)) rate = parsed;
|
||||
}
|
||||
}
|
||||
return {{"result", result}, {"rateLimit", rate}, {"sessionId", session}};
|
||||
}
|
||||
|
||||
std::vector<std::string> ProbeArgs(std::string_view model) {
|
||||
return {"--print", "ok", "--system-prompt", "Reply ok.", "--model", std::string(model), "--max-turns", "1", "--tools", "",
|
||||
"--strict-mcp-config", "--disable-slash-commands", "--output-format", "stream-json", "--verbose"};
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> ProbeEnv(std::string_view token, const std::string& configDir, const std::map<std::string, std::string>& base) {
|
||||
const std::string& home = EnvOf(base, "HOME");
|
||||
return {
|
||||
{"PATH", EnvOf(base, "PATH")},
|
||||
{"HOME", home.empty() ? fs::temp_directory_path().string() : home},
|
||||
{"CLAUDE_CONFIG_DIR", configDir},
|
||||
{"CLAUDE_CODE_OAUTH_TOKEN", std::string(token)},
|
||||
{"MAX_THINKING_TOKENS", "0"},
|
||||
{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", "1"},
|
||||
{"DISABLE_AUTOUPDATER", "1"},
|
||||
};
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> LocalEnv(const std::map<std::string, std::string>& base) {
|
||||
std::map<std::string, std::string> env = base;
|
||||
for (const std::string_view key : {"CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"}) env.erase(std::string(key));
|
||||
env["MAX_THINKING_TOKENS"] = "0";
|
||||
env["CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"] = "1";
|
||||
env["DISABLE_AUTOUPDATER"] = "1";
|
||||
return env;
|
||||
}
|
||||
|
||||
fs::path WriteApiKeyApproval(const fs::path& configDir, std::string_view key) {
|
||||
const fs::path path = configDir / ".claude.json";
|
||||
if (fs::exists(path)) return path;
|
||||
const std::size_t length = Dyn::Length(key);
|
||||
const std::string tail = Dyn::Slice(key, static_cast<std::int64_t>(length) - std::min<std::int64_t>(20, static_cast<std::int64_t>(length)));
|
||||
const Json approval = {{"hasCompletedOnboarding", true}, {"customApiKeyResponses", {{"approved", {tail}}, {"rejected", Json::array()}}}};
|
||||
// O_EXCL: two passes of a fresh seat write it in the same instant; the
|
||||
// contents are the same, so the one that loses leaves the other's.
|
||||
const fs::path temp = std::format("{}.{}.{}.tmp", path.string(), Platform::PublishedPid(), TokenHex(4));
|
||||
{
|
||||
std::ofstream out(temp, std::ios::binary);
|
||||
out << DumpSpaced(approval);
|
||||
}
|
||||
fs::permissions(temp, fs::perms::owner_read | fs::perms::owner_write, fs::perm_options::replace);
|
||||
std::error_code error;
|
||||
if (!fs::exists(path)) fs::rename(temp, path, error);
|
||||
fs::remove(temp, error);
|
||||
return path;
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> ApiEnv(std::string_view key, const std::string& configDir, const std::map<std::string, std::string>& base) {
|
||||
// Minimal by construction: no CLAUDE_CODE_ENTRYPOINT of a launcher (it
|
||||
// turns the subscriber credits' answer into "Credit balance is too
|
||||
// low").
|
||||
std::map<std::string, std::string> env = ProbeEnv("", configDir, base);
|
||||
env.erase("CLAUDE_CODE_OAUTH_TOKEN");
|
||||
env["ANTHROPIC_API_KEY"] = std::string(key);
|
||||
WriteApiKeyApproval(configDir, key);
|
||||
return env;
|
||||
}
|
||||
|
||||
bool LooksLikeCreditExhaustion(const Json& result, std::string_view text) {
|
||||
return CreditText(ResultText(result)) || CreditText(text);
|
||||
}
|
||||
|
||||
bool LooksLikeAuthFailure(const Json& result, std::string_view text) {
|
||||
const Json& status = Dyn::Get(Dyn::Or(result, Dyn::EmptyDict()), "api_error_status");
|
||||
if (Dyn::Equal(status, Json(401)) || Dyn::Equal(status, Json(403))) return true;
|
||||
return Stream::AuthText(ResultText(result)) || Stream::AuthText(text);
|
||||
}
|
||||
|
||||
std::string ClipReason(std::string_view input, std::span<const std::string> secrets) {
|
||||
std::string s(input);
|
||||
for (const std::string& secret : secrets) {
|
||||
if (!secret.empty()) s = Dyn::Replace(s, secret, "[redacted:token]");
|
||||
}
|
||||
s = SubAll(s, Jwt, "[redacted:jwt]");
|
||||
s = SubAll(s, AnthropicKey, "[redacted:token]");
|
||||
s = SubAll(s, Bearer, "Bearer [redacted]");
|
||||
s = SubAll(s, Email, "[redacted:email]");
|
||||
// re.sub(r'\s+', ' ', s).strip()[:160]
|
||||
std::string collapsed;
|
||||
for (std::size_t at = 0; at < s.size();) {
|
||||
const std::size_t gap = Dyn::SpaceRun(s, at);
|
||||
if (gap > 0) {
|
||||
collapsed.push_back(' ');
|
||||
at += gap;
|
||||
continue;
|
||||
}
|
||||
const std::size_t used = Dyn::CodePointAt(s, at).second;
|
||||
collapsed.append(s, at, used);
|
||||
at += used;
|
||||
}
|
||||
return Dyn::Slice(Dyn::Strip(collapsed), 0, 160);
|
||||
}
|
||||
|
||||
Json ProbeSeat(std::int32_t seat, const std::string& token, const ProbeOptions& options) {
|
||||
const std::string claude = options.claudeBin.empty() ? FindClaudeBin() : options.claudeBin;
|
||||
Json base = {{"seat", seat}, {"probedWith", options.model}, {"windows", Json::object()}};
|
||||
auto with = [&](Json extra) {
|
||||
Json out = base;
|
||||
for (auto it = extra.begin(); it != extra.end(); ++it) out[it.key()] = it.value();
|
||||
return out;
|
||||
};
|
||||
const std::vector<std::string> secrets = {token};
|
||||
std::optional<fs::path> configDir;
|
||||
if (!options.local) {
|
||||
for (;;) {
|
||||
configDir = fs::temp_directory_path() / std::format("rg-seat-{}-{}", seat, TokenHex(4));
|
||||
if (fs::create_directory(*configDir)) break;
|
||||
}
|
||||
fs::permissions(*configDir, fs::perms::owner_all, fs::perm_options::replace);
|
||||
}
|
||||
const std::map<std::string, std::string> parent = Platform::Environment();
|
||||
const std::map<std::string, std::string> env = options.local ? LocalEnv(parent) : (options.api ? ApiEnv(token, configDir->string(), parent) : ProbeEnv(token, configDir->string(), parent));
|
||||
std::vector<std::string> argv = {claude};
|
||||
std::ranges::move(ProbeArgs(options.model), std::back_inserter(argv));
|
||||
if (options.local) {
|
||||
argv.emplace_back("--setting-sources");
|
||||
argv.emplace_back("");
|
||||
argv.emplace_back("--no-session-persistence");
|
||||
}
|
||||
auto cleanup = [&] {
|
||||
std::error_code error;
|
||||
if (configDir) fs::remove_all(*configDir, error);
|
||||
};
|
||||
// A binary that cannot be started is said here, as an OSError, before
|
||||
// the supervisor would only report it as an exit code.
|
||||
const std::string path = claude.contains('/') ? claude : Which(claude, EnvOf(env, "PATH")).value_or("");
|
||||
if (path.empty() || access(path.c_str(), X_OK) != 0) {
|
||||
cleanup();
|
||||
return with({{"state", "unknown"}, {"error", ClipReason(std::format("probe spawn failed: {}: {}", Dyn::ErrnoText(2), Dyn::Repr(claude)), secrets)}});
|
||||
}
|
||||
Process::Captured run;
|
||||
try {
|
||||
run = Process::Run(Process::Supervised(argv), std::nullopt, std::chrono::milliseconds(static_cast<std::int64_t>(options.timeoutS * 1000)), {}, env);
|
||||
} catch (const std::exception& error) {
|
||||
cleanup();
|
||||
return with({{"state", "unknown"}, {"error", ClipReason(std::format("probe spawn failed: {}", error.what()), secrets)}});
|
||||
}
|
||||
cleanup();
|
||||
if (run.timedOut) return with({{"state", "unknown"}, {"error", std::format("probe timeout after {} ms", static_cast<std::int64_t>(options.timeoutS * 1000))}});
|
||||
const std::string out = Dyn::UniversalNewlines(Dyn::DecodeReplace(run.out));
|
||||
const std::string err = Dyn::UniversalNewlines(Dyn::DecodeReplace(run.err));
|
||||
const std::int32_t code = Process::SupervisedReturnCode(run.returnCode);
|
||||
const Json parsed = ParseStream(out);
|
||||
const Json& rate = parsed.at("rateLimit");
|
||||
const Json& result = parsed.at("result");
|
||||
const Json cost = Dyn::Get(Dyn::Or(result, Dyn::EmptyDict()), "total_cost_usd");
|
||||
auto said = [&](std::string_view fallback) {
|
||||
const std::string text = ResultText(result);
|
||||
return !text.empty() ? text : (!err.empty() ? err : std::string(fallback));
|
||||
};
|
||||
if (options.api) {
|
||||
// No rate windows on a Console key: the probe's result line is the
|
||||
// whole reading.
|
||||
if (Dyn::Truthy(result) && !Dyn::Truthy(Dyn::Get(result, "is_error"))) return with({{"state", "ok"}, {"costUsd", cost}});
|
||||
if (LooksLikeCreditExhaustion(result, err)) return with({{"state", "out"}, {"error", ClipReason(said("credit balance too low"), secrets)}});
|
||||
if (LooksLikeAuthFailure(result, err)) return with({{"state", "invalid"}, {"error", ClipReason(said("authentication failed"), secrets)}});
|
||||
return with({{"state", "unknown"}, {"error", ClipReason(std::format("probe failed (exit {}) {}", code, said("")), secrets)}});
|
||||
}
|
||||
if (Dyn::Truthy(rate) && Dyn::Truthy(rate.at("windows"))) {
|
||||
const auto [util, binding] = SeatUtil(rate.at("windows"));
|
||||
Json resets = Dyn::Get(Dyn::Or(Dyn::Get(rate.at("windows"), "five_hour"), Dyn::EmptyDict()), "resetsAt");
|
||||
if (!Dyn::Truthy(resets)) resets = Dyn::Get(rate, "resetsAt");
|
||||
if (rate.at("status") == "rejected") {
|
||||
// The one-word probe was refused: the seat takes nothing until
|
||||
// its reset, whatever its windows read.
|
||||
std::string why = std::format("{} rejected", Dyn::StrOr(Dyn::Get(rate, "rateLimitType"), "window"));
|
||||
if (Dyn::Truthy(Dyn::Get(rate, "overageReason"))) why += std::format(", overage {}", Dyn::Str(rate.at("overageReason")));
|
||||
const Json limitedUtil = AsDouble(util) > 100 ? util : Json(100);
|
||||
return with({{"state", "limited"}, {"windows", rate.at("windows")}, {"util", limitedUtil},
|
||||
{"binding", Dyn::Truthy(Dyn::Get(rate, "rateLimitType")) ? rate.at("rateLimitType") : Json(binding)}, {"resetsAt", resets}, {"error", why}});
|
||||
}
|
||||
return with({{"state", "ok"}, {"windows", rate.at("windows")}, {"util", util}, {"binding", binding}, {"resetsAt", resets}});
|
||||
}
|
||||
if (LooksLikeAuthFailure(result, err)) return with({{"state", "invalid"}, {"error", ClipReason(said("authentication failed"), secrets)}});
|
||||
return with({{"state", "unknown"}, {"error", ClipReason(std::format("no rate_limit_event (exit {}) {}", code, said("")), secrets)}});
|
||||
}
|
||||
|
||||
double Quantize(double util) {
|
||||
return JsRound(util / RankQuantum) * RankQuantum;
|
||||
}
|
||||
|
||||
std::pair<Json, std::string> BindingWindow(const Json& windows) {
|
||||
Json util = 0;
|
||||
std::string binding;
|
||||
for (auto it = windows.begin(); it != windows.end(); ++it) {
|
||||
const Json& value = Dyn::At(it.value(), "utilization");
|
||||
if (Less(util, value) || binding.empty()) {
|
||||
util = value;
|
||||
binding = it.key();
|
||||
}
|
||||
}
|
||||
return {util, binding};
|
||||
}
|
||||
|
||||
std::pair<Json, std::string> SeatUtil(const Json& windows) {
|
||||
const auto [util, binding] = BindingWindow(windows);
|
||||
return {Json(static_cast<std::int64_t>(Quantize(AsDouble(util)))), binding};
|
||||
}
|
||||
|
||||
std::string ModelFamily(std::string_view model) {
|
||||
const std::u32string text = Dyn::Decode(model);
|
||||
for (std::size_t at = 0; at < text.size(); ++at) {
|
||||
for (const std::string_view family : ModelFamilies) {
|
||||
if (MatchCi(text, at, family)) return std::string(family);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
bool IsFableWindow(std::string_view key) {
|
||||
return key == FableWindow || ModelFamily(key) == "fable";
|
||||
}
|
||||
|
||||
const Json* FableWindowOf(const Json& seat) {
|
||||
const Json& windows = Dyn::Or(Dyn::Get(seat, "windows"), Dyn::EmptyDict());
|
||||
for (auto it = windows.begin(); it != windows.end(); ++it) {
|
||||
if (IsFableWindow(it.key())) return &it.value();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool LongWindowsOk(const Json& seat) {
|
||||
const Json& windows = Dyn::Or(Dyn::Get(seat, "windows"), Dyn::EmptyDict());
|
||||
for (auto it = windows.begin(); it != windows.end(); ++it) {
|
||||
if (it.key() == "five_hour" || IsFableWindow(it.key())) continue;
|
||||
if (AsDouble(Dyn::At(it.value(), "utilization")) >= LongWindowCutoff) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsEligible(const Json& seat, const Json& config) {
|
||||
return Dyn::At(seat, "state") == "ok" && NumberOr0(Dyn::Get(seat, "util")) <= AsDouble(config.at("maxUtil")) && LongWindowsOk(seat);
|
||||
}
|
||||
|
||||
bool FableOpen(const Json& seat, const Json& config) {
|
||||
const Json& fable = Dyn::Or(Dyn::Get(seat, "fable"), Dyn::EmptyDict());
|
||||
if (Dyn::Get(fable, "state") == "limited" || (Dyn::At(seat, "state") == "limited" && IsFableWindow(Dyn::StrOr(Dyn::Get(seat, "binding"), "")))) return false;
|
||||
const Json* w = FableWindowOf(seat);
|
||||
if (!w) return true;
|
||||
const double u = AsDouble(Dyn::At(*w, "utilization"));
|
||||
return u <= AsDouble(config.at("maxUtil")) && u < LongWindowCutoff;
|
||||
}
|
||||
|
||||
Json PickNext(const std::vector<Json>& seats, const Json& config) {
|
||||
if (seats.empty()) return {{"kind", "stop"}, {"status", "error"}, {"reason", "no seat token configured"}};
|
||||
std::vector<Json> usable;
|
||||
for (const Json& s : SortedBySeat(seats)) {
|
||||
if (Dyn::At(s, "state") != "invalid") usable.push_back(s);
|
||||
}
|
||||
if (usable.empty()) {
|
||||
std::vector<std::string> ids;
|
||||
for (const Json& s : seats) ids.push_back(Dyn::Str(Dyn::At(s, "seat")));
|
||||
return {{"kind", "stop"}, {"status", "error"}, {"reason", std::format("no valid seat token (seat{} {} rejected)", seats.size() == 1 ? "" : "s", Dyn::Join(ids, ", "))}};
|
||||
}
|
||||
if (std::ranges::all_of(usable, [](const Json& s) { return Dyn::At(s, "state") == "unknown"; })) {
|
||||
return {{"kind", "seat"}, {"seat", usable[0].at("seat")}, {"source", "round-robin"}};
|
||||
}
|
||||
const double maxUtil = AsDouble(config.at("maxUtil"));
|
||||
const double cost = AsDouble(config.at("lensCostPct"));
|
||||
auto projected = [&](const Json& s) {
|
||||
return Dyn::At(s, "state") == "unknown" || Dyn::Get(s, "util").is_null() ? maxUtil : AsDouble(Dyn::At(s, "util"));
|
||||
};
|
||||
auto longOk = [&](const Json& s) { return Dyn::At(s, "state") == "unknown" || LongWindowsOk(s); };
|
||||
std::vector<const Json*> pool;
|
||||
for (const Json& s : usable) {
|
||||
if (projected(s) + cost <= maxUtil && longOk(s)) pool.push_back(&s);
|
||||
}
|
||||
const Json* chosen = nullptr;
|
||||
auto least = [&](const std::vector<const Json*>& candidates) {
|
||||
const Json* best = candidates.front();
|
||||
for (const Json* s : candidates) {
|
||||
if (projected(*s) < projected(*best)) best = s;
|
||||
}
|
||||
return best;
|
||||
};
|
||||
if (!pool.empty()) {
|
||||
chosen = config.at("policy") == "pack" ? pool.front() : least(pool);
|
||||
} else {
|
||||
// Nothing fits under the ceiling: the most headroom among the seats
|
||||
// whose long windows are open, else any.
|
||||
std::vector<const Json*> candidates;
|
||||
for (const Json& s : usable) {
|
||||
if (longOk(s)) candidates.push_back(&s);
|
||||
}
|
||||
if (candidates.empty()) {
|
||||
for (const Json& s : usable) candidates.push_back(&s);
|
||||
}
|
||||
chosen = least(candidates);
|
||||
}
|
||||
return {{"kind", "seat"}, {"seat", chosen->at("seat")}, {"source", "probe"}};
|
||||
}
|
||||
|
||||
bool NeedsHaiku(const Json& seat) {
|
||||
const Json& state = Dyn::At(seat, "state");
|
||||
return state == "unknown" || state == "invalid" || (state == "limited" && IsFableWindow(Dyn::StrOr(Dyn::Get(seat, "binding"), "")));
|
||||
}
|
||||
|
||||
std::string FableState(const Json& fable) {
|
||||
if (Dyn::At(fable, "state") == "limited") return IsFableWindow(Dyn::StrOr(Dyn::Get(fable, "binding"), "")) ? "limited" : "ok";
|
||||
return Dyn::Str(Dyn::At(fable, "state"));
|
||||
}
|
||||
|
||||
Json MergeFableProbe(Json base, const Json& fable) {
|
||||
if (const Json* w = FableWindowOf(fable)) {
|
||||
Json windows = Json::object();
|
||||
const Json& own = Dyn::Or(Dyn::Get(base, "windows"), Dyn::EmptyDict());
|
||||
for (auto it = own.begin(); it != own.end(); ++it) {
|
||||
if (!IsFableWindow(it.key())) windows[it.key()] = it.value();
|
||||
}
|
||||
windows[std::string(FableWindow)] = *w;
|
||||
base["windows"] = std::move(windows);
|
||||
}
|
||||
if (Dyn::At(base, "state") == "ok") {
|
||||
// The seat's utilisation is its own windows' — the Fable window
|
||||
// gates Fable work only (FableOpen): a drained Fable window makes an
|
||||
// Opus seat, never an idle one.
|
||||
Json own = Json::object();
|
||||
for (auto it = base.at("windows").begin(); it != base.at("windows").end(); ++it) {
|
||||
if (!IsFableWindow(it.key())) own[it.key()] = it.value();
|
||||
}
|
||||
const auto [util, binding] = SeatUtil(own);
|
||||
base["util"] = util;
|
||||
base["binding"] = binding;
|
||||
}
|
||||
base["fable"] = {{"state", FableState(fable)}};
|
||||
const Json& state = Dyn::At(fable, "state");
|
||||
if (state != "ok" && state != "limited" && Dyn::Truthy(Dyn::Get(fable, "error"))) base["fable"]["error"] = fable.at("error");
|
||||
return base;
|
||||
}
|
||||
|
||||
std::string FindClaudeBin(const std::map<std::string, std::string>& env, const std::string& home) {
|
||||
for (const std::string_view variable : {"CLAUDE_BIN", "REVIEW_GATES_CLAUDE_BIN"}) {
|
||||
if (const std::string& value = EnvOf(env, std::string(variable)); !value.empty()) return value;
|
||||
}
|
||||
if (const std::string& path = EnvOf(env, "PATH"); !path.empty()) {
|
||||
if (const std::optional<std::string> found = Which("claude", path)) return *found;
|
||||
}
|
||||
std::vector<std::string> found;
|
||||
for (const std::string_view extensions : {".vscode-oss/extensions", ".vscode/extensions", ".vscode-insiders/extensions", ".cursor/extensions"}) {
|
||||
const fs::path dir = fs::path(home) / extensions;
|
||||
std::error_code error;
|
||||
if (!fs::is_directory(dir, error)) continue;
|
||||
std::vector<std::string> names;
|
||||
for (const fs::directory_entry& entry : fs::directory_iterator(dir, error)) {
|
||||
const std::string name = entry.path().filename().string();
|
||||
if (name.starts_with("anthropic.claude-code-")) names.push_back(name);
|
||||
}
|
||||
std::ranges::stable_sort(names, [](const std::string& a, const std::string& b) { return VersionKey(a) > VersionKey(b); });
|
||||
for (const std::string& name : names) {
|
||||
const fs::path binary = dir / name / "resources" / "native-binary" / "claude";
|
||||
if (fs::exists(binary, error)) found.push_back(binary.string());
|
||||
}
|
||||
}
|
||||
for (const fs::path local : {fs::path(home) / ".claude" / "local" / "claude", fs::path(home) / ".local" / "bin" / "claude"}) {
|
||||
std::error_code error;
|
||||
if (fs::exists(local, error)) found.push_back(local.string());
|
||||
}
|
||||
return found.empty() ? std::string("claude") : found.front();
|
||||
}
|
||||
|
||||
std::string FindClaudeBin() {
|
||||
return FindClaudeBin(Platform::Environment(), Platform::Home());
|
||||
}
|
||||
|
||||
Zone ZoneNamed(std::string_view name) {
|
||||
// ZoneInfo(name): a zone the system's zoneinfo holds, else none.
|
||||
std::error_code error;
|
||||
const bool known = name == "UTC" || fs::is_regular_file(fs::path("/usr/share/zoneinfo") / name, error);
|
||||
return {known ? std::string(name) : std::string(), std::string(name)};
|
||||
}
|
||||
|
||||
Zone LocalZone() {
|
||||
std::string name = Dyn::LStrip(Platform::GetEnv("TZ").value_or(""), ":");
|
||||
if (name.empty()) {
|
||||
std::error_code error;
|
||||
const std::string target = fs::weakly_canonical("/etc/localtime", error).string();
|
||||
const std::size_t at = target.find("/zoneinfo/");
|
||||
if (at != std::string::npos) name = target.substr(at + 10);
|
||||
}
|
||||
if (!name.empty()) {
|
||||
Zone zone = ZoneNamed(name);
|
||||
if (!zone.name.empty()) return zone;
|
||||
}
|
||||
return {"", name.empty() ? Platform::LocalTime("%Z") : name};
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The broken-down time of `t` in the zone: the C library's, with TZ
|
||||
// set to the zone's name for the call.
|
||||
std::tm InZone(std::time_t t, const Zone& zone) {
|
||||
static std::mutex lock;
|
||||
const std::lock_guard guard(lock);
|
||||
std::tm out {};
|
||||
if (zone.name.empty()) {
|
||||
localtime_r(&t, &out);
|
||||
return out;
|
||||
}
|
||||
const char* before = getenv("TZ");
|
||||
const std::optional<std::string> saved = before ? std::optional<std::string>(before) : std::nullopt;
|
||||
setenv("TZ", zone.name.c_str(), 1);
|
||||
tzset();
|
||||
localtime_r(&t, &out);
|
||||
if (saved) {
|
||||
setenv("TZ", saved->c_str(), 1);
|
||||
} else {
|
||||
unsetenv("TZ");
|
||||
}
|
||||
tzset();
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
std::string FormatWhen(double epoch, double now, const Zone& zone) {
|
||||
const auto diffMin = static_cast<std::int64_t>(JsRound((epoch - now) / 60));
|
||||
std::string relative;
|
||||
if (diffMin <= 0) {
|
||||
relative = "now";
|
||||
} else if (diffMin < 60) {
|
||||
relative = std::format("in {} min", diffMin);
|
||||
} else if (diffMin < 48 * 60) {
|
||||
relative = std::format("in {}h {:02d}m", diffMin / 60, diffMin % 60);
|
||||
} else {
|
||||
relative = std::format("in {}d {}h", diffMin / 1440, (diffMin % 1440) / 60);
|
||||
}
|
||||
const std::tm d = InZone(static_cast<std::time_t>(std::floor(epoch)), zone);
|
||||
std::string clock = std::format("{:02d}:{:02d}", d.tm_hour, d.tm_min);
|
||||
if (epoch - now > 24 * 3600) {
|
||||
// Monday is 0 here; tm_wday counts from Sunday.
|
||||
const auto index = static_cast<std::size_t>((d.tm_wday + 6) % 7);
|
||||
clock = std::format("{} {:02d} {}, {}", Weekdays[index], d.tm_mday, Months[static_cast<std::size_t>(d.tm_mon)], clock);
|
||||
}
|
||||
return std::format("{} ({})", clock, relative);
|
||||
}
|
||||
|
||||
std::string RenderTable(const std::vector<std::vector<std::string>>& rows) {
|
||||
std::vector<std::size_t> widths(rows.at(0).size(), 0);
|
||||
for (const std::vector<std::string>& row : rows) {
|
||||
for (std::size_t i = 0; i < widths.size(); ++i) widths[i] = std::max(widths[i], Dyn::Length(row.at(i)));
|
||||
}
|
||||
auto line = [&](const std::vector<std::string>& row) {
|
||||
std::vector<std::string> cells;
|
||||
for (std::size_t i = 0; i < widths.size(); ++i) cells.push_back(Dyn::LJust(row[i], widths[i]));
|
||||
return std::format("| {} |", Dyn::Join(cells, " | "));
|
||||
};
|
||||
std::vector<std::string> out = {line(rows[0])};
|
||||
std::vector<std::string> dashes;
|
||||
for (const std::size_t width : widths) dashes.emplace_back(width + 2, '-');
|
||||
out.push_back(std::format("|{}|", Dyn::Join(dashes, "|")));
|
||||
for (std::size_t r = 1; r < rows.size(); ++r) out.push_back(line(rows[r]));
|
||||
return Dyn::Join(out, "\n");
|
||||
}
|
||||
|
||||
std::string RenderStatusReport(const std::vector<Json>& seats, const Json& config, double now, const Zone& zone, const Json& board,
|
||||
const std::map<std::int32_t, std::string>& keys) {
|
||||
const std::vector<Json> ordered = SortedBySeat(seats);
|
||||
const bool anyInflight = std::ranges::any_of(ordered, [&](const Json& s) { return Inflight(board, keys, s) != 0; });
|
||||
std::vector<std::vector<std::string>> rows;
|
||||
std::vector<std::string> header = {"seat", "5h", "resets", "7d", "resets", "fable 7d"};
|
||||
if (anyInflight) header.emplace_back("in flight");
|
||||
rows.push_back(header);
|
||||
for (const Json& s : ordered) {
|
||||
auto w = [&](std::string_view k) {
|
||||
if (!Dyn::Has(Dyn::Or(Dyn::Get(s, "windows"), Dyn::EmptyDict()), k)) return std::string("—");
|
||||
return std::format("{}%", Fmt(Dyn::At(Window(s, k), "utilization")));
|
||||
};
|
||||
auto when = [&](std::string_view k) {
|
||||
const Json& at = Dyn::Get(Dyn::Or(Window(s, k), Dyn::EmptyDict()), "resetsAt");
|
||||
return Dyn::Truthy(at) ? FormatWhen(AsDouble(at), now, zone) : std::string("—");
|
||||
};
|
||||
const Json* fw = FableWindowOf(s);
|
||||
const Json& f = Dyn::Get(s, "fable");
|
||||
std::string fable;
|
||||
if (Dyn::Truthy(f) && Dyn::Get(f, "state") == "limited") {
|
||||
fable = std::format("limited{}", fw ? std::format(" ({}%)", Fmt(Dyn::At(*fw, "utilization"))) : std::string());
|
||||
} else if (fw) {
|
||||
fable = std::format("{}%", Fmt(Dyn::At(*fw, "utilization")));
|
||||
} else {
|
||||
fable = Dyn::Truthy(f) ? Dyn::Str(Dyn::At(f, "state")) : std::string("—");
|
||||
}
|
||||
std::vector<std::string> row = {SeatCell(s), w("five_hour"), when("five_hour"), w("seven_day"), when("seven_day"), fable};
|
||||
if (anyInflight) {
|
||||
const std::int64_t n = Inflight(board, keys, s);
|
||||
row.push_back(n ? std::to_string(n) : "—");
|
||||
}
|
||||
rows.push_back(std::move(row));
|
||||
}
|
||||
std::vector<Json> eligible;
|
||||
for (const Json& s : ordered) {
|
||||
if (IsEligible(s, config)) eligible.push_back(s);
|
||||
}
|
||||
std::ranges::stable_sort(eligible, [](const Json& a, const Json& b) { return NumberOr0(Dyn::Get(a, "util")) < NumberOr0(Dyn::Get(b, "util")); });
|
||||
const Json pick = PickNext(seats, config);
|
||||
const std::string next = pick.at("kind") == "seat" ? SeatName({{"seat", pick.at("seat")}}) : std::format("{}: {}", Dyn::Str(pick.at("status")), Dyn::Str(pick.at("reason")));
|
||||
std::vector<std::string> limited;
|
||||
std::vector<std::string> unusable;
|
||||
std::vector<std::string> fableDead;
|
||||
for (const Json& s : ordered) {
|
||||
const Json& state = Dyn::At(s, "state");
|
||||
if (state == "limited") {
|
||||
const Json& resets = Dyn::Get(s, "resetsAt");
|
||||
limited.push_back(std::format("{} until {}", SeatName(s), Dyn::Truthy(resets) ? FormatWhen(AsDouble(resets), now, zone) : std::string("?")));
|
||||
}
|
||||
if (state != "ok" && state != "limited") {
|
||||
unusable.push_back(std::format("{} {}{}", SeatName(s), Dyn::Str(state), Dyn::Truthy(Dyn::Get(s, "error")) ? std::format(" ({})", Dyn::Str(s.at("error"))) : std::string()));
|
||||
}
|
||||
const Json& fableOfSeat = Dyn::Or(Dyn::Get(s, "fable"), Dyn::EmptyDict());
|
||||
const Json& fableStateOfSeat = Dyn::Get(fableOfSeat, "state");
|
||||
if ((state == "ok" || state == "limited") && !fableStateOfSeat.is_null() && fableStateOfSeat != "ok" && fableStateOfSeat != "limited") {
|
||||
fableDead.push_back(std::format("{} {}{}", SeatName(s), Dyn::Str(fableStateOfSeat),
|
||||
Dyn::Truthy(Dyn::Get(fableOfSeat, "error")) ? std::format(" ({})", Dyn::Str(fableOfSeat.at("error"))) : std::string()));
|
||||
}
|
||||
}
|
||||
const bool fableSeen = std::ranges::any_of(seats, [](const Json& s) { return FableWindowOf(s) != nullptr; });
|
||||
std::vector<Json> fableRoom;
|
||||
for (const Json& s : ordered) {
|
||||
if (Dyn::At(s, "state") == "ok" && FableWindowOf(s) && FableOpen(s, config)) fableRoom.push_back(s);
|
||||
}
|
||||
std::ranges::stable_sort(fableRoom, [](const Json& a, const Json& b) { return Less(Dyn::At(*FableWindowOf(a), "utilization"), Dyn::At(*FableWindowOf(b), "utilization")); });
|
||||
const double maxUtil = AsDouble(config.at("maxUtil"));
|
||||
std::vector<std::string> lines = {RenderTable(rows), ""};
|
||||
lines.push_back(std::format("policy {} · ceiling {}% · handover at {}% · a lens is placed at +{}% · 7d cutoff {}%", Dyn::Str(config.at("policy")), Fmt(config.at("maxUtil")),
|
||||
Fmt(config.at("handoverPct")), Fmt(config.at("lensCostPct")), LongWindowCutoff));
|
||||
std::vector<std::string> eligibleText;
|
||||
for (const Json& s : eligible) eligibleText.push_back(std::format("{} ({}% headroom)", SeatName(s), Fmt(Json(maxUtil - NumberOr0(Dyn::Get(s, "util"))))));
|
||||
lines.push_back(std::format("eligible now: {} → next lens: {}", eligible.empty() ? std::string("none") : Dyn::Join(eligibleText, ", "), next));
|
||||
if (fableSeen) {
|
||||
std::vector<std::string> room;
|
||||
for (const Json& s : fableRoom) room.push_back(std::format("{} ({}% fable headroom)", SeatName(s), Fmt(Json(maxUtil - AsDouble(Dyn::At(*FableWindowOf(s), "utilization"))))));
|
||||
lines.push_back(std::format("fable lenses: {}", fableRoom.empty() ? std::string("no seat with room on its Fable window — Fable lenses run on their fallback model") : Dyn::Join(room, ", ")));
|
||||
}
|
||||
if (!limited.empty()) lines.push_back(std::format("limited: {}", Dyn::Join(limited, "; ")));
|
||||
if (!unusable.empty()) lines.push_back(std::format("not usable: {}", Dyn::Join(unusable, "; ")));
|
||||
if (!fableDead.empty()) lines.push_back(std::format("fable probe: {}", Dyn::Join(fableDead, "; ")));
|
||||
return Dyn::Join(lines, "\n");
|
||||
}
|
||||
|
||||
std::string RenderApiReport(const std::vector<Json>& apiSeats, const Json& apiConfig, const Json& board, const std::map<std::int32_t, std::string>& keys) {
|
||||
std::vector<std::vector<std::string>> rows = {{"seat", "state", "driver spend this cycle", "procs", "cycle from", "ledger: out", "in flight"}};
|
||||
for (const Json& s : SortedBySeat(apiSeats)) {
|
||||
const Json& ledger = Dyn::Or(Dyn::Get(s, "ledger"), Dyn::EmptyDict());
|
||||
const double spent = NumberOr0(Dyn::Get(ledger, "spentUsd"));
|
||||
const std::string state = Dyn::Str(Dyn::At(s, "state")) + (Dyn::Truthy(Dyn::Get(s, "error")) && Dyn::At(s, "state") != "ok" ? std::format(" ({})", Dyn::Str(s.at("error"))) : std::string());
|
||||
const Json& outSince = Dyn::Get(ledger, "outSince");
|
||||
const std::int64_t n = Inflight(board, keys, s);
|
||||
rows.push_back({
|
||||
std::format("api{}", Dyn::Str(Dyn::At(s, "seat"))), state, std::format("${:.2f} of ${}", spent, Fmt(apiConfig.at("budgetUsd"))), Dyn::StrOr(Dyn::Get(ledger, "procs"), "0"),
|
||||
Dyn::StrOr(Dyn::Get(ledger, "cycle"), "—"), Dyn::Truthy(outSince) ? std::format("since {}", Dyn::Replace(Dyn::Slice(Dyn::Str(outSince), 0, 16), "T", " ")) : std::string("—"),
|
||||
n ? std::format("{} of {}", n, Dyn::Str(apiConfig.at("concurrency"))) : std::string("—"),
|
||||
});
|
||||
}
|
||||
return Dyn::Join(std::vector<std::string>{
|
||||
RenderTable(rows),
|
||||
std::format("api seats run first (REVIEW_GATES_SEAT_ORDER={}) · budget ${} per seat per cycle · cycle day {} · {} processes per seat · placed at +${} each",
|
||||
Dyn::Str(apiConfig.at("order")), Fmt(apiConfig.at("budgetUsd")), Dyn::Str(apiConfig.at("cycleDay")), Dyn::Str(apiConfig.at("concurrency")), Fmt(apiConfig.at("lensCostUsd"))),
|
||||
"the balance itself is in the Console (Settings > Billing > Promotional credits); the ledger counts only what the review drivers spent",
|
||||
}, "\n");
|
||||
}
|
||||
|
||||
std::string RenderDrivers(const Json& board, double now, const Zone& zone) {
|
||||
const Json& drivers = Dyn::Or(Dyn::Get(Dyn::Or(board, Dyn::EmptyDict()), "drivers"), Dyn::EmptyDict());
|
||||
if (!Dyn::Truthy(drivers)) return std::format("drivers: none running (the board at {})", Dyn::StrOr(Dyn::Get(Dyn::Or(board, Dyn::EmptyDict()), "path"), "?"));
|
||||
std::vector<std::pair<std::string, const Json*>> sorted;
|
||||
for (auto it = drivers.begin(); it != drivers.end(); ++it) sorted.emplace_back(it.key(), &it.value());
|
||||
std::ranges::stable_sort(sorted, [](const auto& a, const auto& b) { return NumberOr0(Dyn::Get(*a.second, "since")) < NumberOr0(Dyn::Get(*b.second, "since")); });
|
||||
std::vector<std::string> parts;
|
||||
std::int64_t total = 0;
|
||||
for (const auto& [key, driver] : sorted) {
|
||||
const Json& d = *driver;
|
||||
const Json& settings = Dyn::Or(Dyn::Get(d, "settings"), Dyn::EmptyDict());
|
||||
std::string held;
|
||||
if (Dyn::Truthy(Dyn::Get(d, "mb"))) held += std::format(", {:.0f} MB", AsDouble(d.at("mb")));
|
||||
if (Dyn::Truthy(Dyn::Get(d, "stacks"))) {
|
||||
std::vector<std::string> stacks;
|
||||
for (const Json& stack : d.at("stacks")) stacks.push_back(Dyn::Str(stack));
|
||||
std::ranges::sort(stacks);
|
||||
held += std::format(", stack(s) {}", Dyn::Join(stacks, ", "));
|
||||
}
|
||||
const Json unknown = "?";
|
||||
const Json zero = 0;
|
||||
parts.push_back(std::format("pid {} ({}, {}, width {}) since {} - {} in flight{}", Dyn::Split(key, ":")[0], Dyn::Str(Dyn::Has(d, "run") ? d.at("run") : unknown),
|
||||
Dyn::Str(Dyn::Has(d, "effort") ? d.at("effort") : unknown), Dyn::Str(Dyn::Has(settings, "concurrency") ? settings.at("concurrency") : unknown),
|
||||
FormatWhen(NumberOr0(Dyn::Get(d, "since")), now, zone), Dyn::Str(Dyn::Has(d, "inflight") ? d.at("inflight") : zero), held));
|
||||
total += static_cast<std::int64_t>(NumberOr0(Dyn::Get(d, "inflight")));
|
||||
}
|
||||
return std::format("drivers ({} in flight on this machine; the width is the machine's cap): {}", total, Dyn::Join(parts, "; "));
|
||||
}
|
||||
}
|
||||
34
interfaces/Crafter.Review-Mcp.cppm
Normal file
34
interfaces/Crafter.Review-Mcp.cppm
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
export module Crafter.Review:Mcp;
|
||||
import std;
|
||||
import :Json;
|
||||
|
||||
// The Playwright MCP server a lens with the browser environment drives the
|
||||
// application with (2026-10-15): one server per pass, launched by the CLI
|
||||
// from --mcp-config (never from the lens's Bash), headless, an in-memory
|
||||
// profile, its screenshots under the pass's own output dir in the scratch
|
||||
// tree. The tools are allowlisted by name (:Tools); browser_run_code_unsafe
|
||||
// runs arbitrary Node code in the server's process and is denied.
|
||||
export namespace Crafter::Review::Mcp {
|
||||
// @playwright/mcp, verified 2026-10-15 with CLI 2.1.300.
|
||||
constexpr std::string_view PlaywrightMcpVersion = "0.0.83";
|
||||
// The Chromium build that version's Playwright drives, as `npx playwright
|
||||
// install chromium` puts it under ~/.cache/ms-playwright: what
|
||||
// `crafter-review browser install` provides.
|
||||
constexpr std::string_view ChromiumRevision = "1247";
|
||||
// --browser chromium: Playwright's own Chromium; without it the server
|
||||
// wants the `chrome` channel, Google Chrome at /opt/google/chrome
|
||||
// (2026-10-15: every navigate of the second firing run failed on it).
|
||||
std::vector<std::string> DefaultArgs();
|
||||
|
||||
// The --mcp-config value for one pass: the consumer's command and args (the
|
||||
// pipeline's pinned default when the config names none) plus this pass's
|
||||
// output dir.
|
||||
Json PlaywrightConfig(const Json& functional, const std::string& outputDir);
|
||||
// The config as compact JSON (DumpCompact), keys in their order.
|
||||
std::string ConfigJson(const Json& config);
|
||||
// <scratch>/playwright/<label with / and spaces as _>, made.
|
||||
std::filesystem::path OutputDir(const std::filesystem::path& scratch, std::string_view label);
|
||||
}
|
||||
88
interfaces/Crafter.Review-Memory.cppm
Normal file
88
interfaces/Crafter.Review-Memory.cppm
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
export module Crafter.Review:Memory;
|
||||
import std;
|
||||
import :Json;
|
||||
|
||||
// Memory (2026-10-22, Jorijn: "make the concurrency limit from free memory,
|
||||
// instead of just guessing"): what the machine has free, what a pass costs,
|
||||
// and the budget the pool admits passes against.
|
||||
//
|
||||
// - Free: the kernel's MemAvailable, read at the start of every gate and
|
||||
// before every launch: the whole machine's, which is right for a budget and
|
||||
// wrong for a cost.
|
||||
// - Cost: the peak PSS of a pass's own process tree (claude, and for the
|
||||
// browser environments the Playwright server and Chromium under it), sampled
|
||||
// by the driver on its own passes and kept per repository and environment
|
||||
// (Calibration); a pass is admitted at the highest of its environment's
|
||||
// recent peaks, or at the measured default before any is recorded.
|
||||
// - Reserve: what is left for the desktop and the page cache —
|
||||
// REVIEW_GATES_MEM_RESERVE_MB, by default 10 % of MemTotal, at least 2 GB.
|
||||
// A policy, not a measurement.
|
||||
export namespace Crafter::Review::Memory {
|
||||
// Recent peaks kept per repository and environment.
|
||||
constexpr std::size_t Keep = 30;
|
||||
|
||||
// MemAvailable in MB; nullopt without /proc/meminfo.
|
||||
std::optional<std::int64_t> Available(const std::filesystem::path& path = "/proc/meminfo");
|
||||
// REVIEW_GATES_MEM_RESERVE_MB, else 10 % of MemTotal, at least 2048.
|
||||
// std::invalid_argument for a value that is no number.
|
||||
std::int64_t Reserve(const std::optional<std::string>& configured, const std::filesystem::path& path = "/proc/meminfo");
|
||||
std::int64_t Reserve();
|
||||
|
||||
// The peak tree PSS of every watched pass, on one thread. A sweep makes one
|
||||
// ppid map, then reads smaps_rollup for every process of every watched tree
|
||||
// — the costly read, it walks the page tables. The pause after a sweep is
|
||||
// `interval` or three times the sweep's own length, whichever is longer, so
|
||||
// sampling holds at most a quarter of one core whatever the number of
|
||||
// passes (2026-10-22). A pass shorter than one pause may go unmeasured.
|
||||
class Sampler {
|
||||
public:
|
||||
using Measure = std::function<double(std::int64_t pid, const std::map<std::int64_t, std::vector<std::int64_t>>& kids)>;
|
||||
using Scan = std::function<std::map<std::int64_t, std::vector<std::int64_t>>()>;
|
||||
explicit Sampler(double interval = 3.0, Measure measure = {}, Scan scan = {});
|
||||
~Sampler();
|
||||
Sampler(const Sampler&) = delete;
|
||||
Sampler& operator=(const Sampler&) = delete;
|
||||
void Watch(const std::string& key, std::int64_t pid);
|
||||
// One sweep over every watched pass; its length in seconds.
|
||||
double Sample();
|
||||
// Stop watching; the peak in MB, rounded to 0.1, or nullopt when
|
||||
// nothing was measured.
|
||||
std::optional<double> Done(const std::string& key);
|
||||
static double Pause(double interval, double sweep);
|
||||
|
||||
private:
|
||||
struct State;
|
||||
std::shared_ptr<State> state;
|
||||
};
|
||||
|
||||
// Recent peaks per repository and environment, in one JSON file next to the
|
||||
// seat ledger: {repo: {env: [MB]}}. A record, never a dependency
|
||||
// (2026-10-22): a damaged file reads as empty, a part of the wrong shape as
|
||||
// absent, and a peak that cannot be written is dropped.
|
||||
class Calibration {
|
||||
public:
|
||||
Calibration(std::filesystem::path path, const std::filesystem::path& repo, std::map<std::string, std::int64_t> defaults);
|
||||
std::vector<Json> Peaks(std::string_view environment) const;
|
||||
struct Cost {
|
||||
// MB, an int or a float as the file holds it.
|
||||
Json megabytes;
|
||||
std::string source;
|
||||
};
|
||||
Cost CostOf(std::string_view environment) const;
|
||||
// Keep a pass's peak; whether it was stored. The file is every
|
||||
// driver's on this machine: the re-read and the write are one section
|
||||
// under its lock (2026-10-31), so a peak another driver wrote between
|
||||
// them is kept.
|
||||
bool Record(std::string_view environment, double megabytes);
|
||||
|
||||
private:
|
||||
std::filesystem::path path;
|
||||
std::string repo;
|
||||
std::map<std::string, std::int64_t> defaults;
|
||||
mutable std::mutex lock;
|
||||
Json Load() const;
|
||||
};
|
||||
}
|
||||
124
interfaces/Crafter.Review-Seats.cppm
Normal file
124
interfaces/Crafter.Review-Seats.cppm
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
export module Crafter.Review:Seats;
|
||||
import std;
|
||||
import :Json;
|
||||
|
||||
// How much usage each Claude seat has left: the dotenv loader, the one-word
|
||||
// probe, the window maths, the ranking and the report. The seat pool
|
||||
// (:SeatPool) places on these; `crafter-review seats` prints the report (its
|
||||
// glue, which needs the ledger and the board, is in :Driver).
|
||||
//
|
||||
// Seats are CLAUDE_CODE_OAUTH_TOKEN1..6 by fixed name (the legacy
|
||||
// CLAUDE_CODE_OAUTH_TOKEN is seat 1 when TOKEN1 is empty), API-credit seats
|
||||
// ANTHROPIC_API_KEY1..6, OpenRouter seats OPENROUTER_KEY[1..6]; never printed:
|
||||
// the table and the JSON carry indices and percentages only. A probe result
|
||||
// and a seat are JSON: the dump and --json show them.
|
||||
export namespace Crafter::Review::Seats {
|
||||
constexpr std::int32_t MaxSeats = 6;
|
||||
constexpr std::string_view ProbeModel = "haiku";
|
||||
// The Fable weekly limit is a window of its own, reported ONLY in the
|
||||
// rate_limit_event of a Fable request (verified 2026-09-08). The CLI has
|
||||
// named it seven_day_overage_included and seven_day_fable; either lands
|
||||
// under the canonical key.
|
||||
constexpr std::string_view FableProbeModel = "claude-fable-5-1";
|
||||
constexpr std::string_view FableWindow = "seven_day_overage_included";
|
||||
constexpr double ProbeTimeoutS = 30;
|
||||
// Fixed rules, not knobs: utilisation is rounded to RankQuantum before
|
||||
// ranking, and a seat whose 7-day window is at or above LongWindowCutoff is
|
||||
// out regardless — its reset is days away. The Fable window is exempt here.
|
||||
constexpr std::int32_t RankQuantum = 5;
|
||||
constexpr std::int32_t LongWindowCutoff = 95;
|
||||
|
||||
// JavaScript's Math.round: halves go up, never to even.
|
||||
double JsRound(double x);
|
||||
// A number the way a JS template prints it: 5 not 5.0, 62.5 as is.
|
||||
std::string Fmt(const Json& value);
|
||||
|
||||
// {maxUtil, lensCostPct, waitMaxMin, policy, handoverPct}: the placement
|
||||
// knobs; garbage leaves the default.
|
||||
Json DefaultConfig();
|
||||
Json ConfigFromEnv(const std::map<std::string, std::string>& env);
|
||||
std::vector<std::pair<std::int32_t, std::string>> SeatTokensFromEnv(const std::map<std::string, std::string>& env);
|
||||
std::vector<std::pair<std::int32_t, std::string>> ApiKeysFromEnv(const std::map<std::string, std::string>& env);
|
||||
std::vector<std::pair<std::int32_t, std::string>> OpenRouterKeysFromEnv(const std::map<std::string, std::string>& env);
|
||||
// Only the seat lines of a dotenv file; `export` and quotes stripped,
|
||||
// everything else ignored.
|
||||
std::map<std::string, std::string> SeatTokensFromEnvFile(std::string_view text);
|
||||
|
||||
// The CLI's fraction as a percent, past 100 for a seat over its limit.
|
||||
Json Pct(const Json& utilization);
|
||||
// {status, rateLimitType, resetsAt, windows, overageReason}, or null.
|
||||
Json ParseRateLimitInfo(const Json& raw);
|
||||
// The last result line, the last rate_limit_event and the session id.
|
||||
Json ParseStream(std::string_view text);
|
||||
|
||||
std::vector<std::string> ProbeArgs(std::string_view model = ProbeModel);
|
||||
// The probe child's whole environment: one token under the name the CLI
|
||||
// reads, an isolated config dir, thinking off, no nonessential traffic, no
|
||||
// updater, and none of the other pool tokens.
|
||||
std::map<std::string, std::string> ProbeEnv(std::string_view token, const std::string& configDir, const std::map<std::string, std::string>& base);
|
||||
// The CLI's own login: no token or API key from the environment.
|
||||
std::map<std::string, std::string> LocalEnv(const std::map<std::string, std::string>& base);
|
||||
// The key's approval in the config dir's .claude.json, once.
|
||||
std::filesystem::path WriteApiKeyApproval(const std::filesystem::path& configDir, std::string_view key);
|
||||
std::map<std::string, std::string> ApiEnv(std::string_view key, const std::string& configDir, const std::map<std::string, std::string>& base);
|
||||
bool LooksLikeCreditExhaustion(const Json& result, std::string_view text);
|
||||
bool LooksLikeAuthFailure(const Json& result, std::string_view text);
|
||||
// One line, at most 160 characters, the seat's own secrets and anything
|
||||
// token-shaped redacted.
|
||||
std::string ClipReason(std::string_view text, std::span<const std::string> secrets = {});
|
||||
|
||||
struct ProbeOptions {
|
||||
std::string model = std::string(ProbeModel);
|
||||
double timeoutS = ProbeTimeoutS;
|
||||
std::string claudeBin;
|
||||
bool local = false;
|
||||
bool api = false;
|
||||
};
|
||||
// One probe of one seat: {seat, probedWith, windows, state: ok | limited |
|
||||
// unknown | invalid | out, util, binding, resetsAt, error, costUsd}.
|
||||
Json ProbeSeat(std::int32_t seat, const std::string& token, const ProbeOptions& options);
|
||||
|
||||
double Quantize(double util);
|
||||
// The most-used window and its utilisation, raw.
|
||||
std::pair<Json, std::string> BindingWindow(const Json& windows);
|
||||
// Quantised for ranking.
|
||||
std::pair<Json, std::string> SeatUtil(const Json& windows);
|
||||
std::string ModelFamily(std::string_view model);
|
||||
bool IsFableWindow(std::string_view key);
|
||||
const Json* FableWindowOf(const Json& seat);
|
||||
bool LongWindowsOk(const Json& seat);
|
||||
bool IsEligible(const Json& seat, const Json& config);
|
||||
bool FableOpen(const Json& seat, const Json& config);
|
||||
Json PickNext(const std::vector<Json>& seats, const Json& config);
|
||||
bool NeedsHaiku(const Json& seat);
|
||||
std::string FableState(const Json& fable);
|
||||
Json MergeFableProbe(Json base, const Json& fable);
|
||||
|
||||
// Where the CLI is: CLAUDE_BIN / REVIEW_GATES_CLAUDE_BIN, then PATH, then
|
||||
// the editor extensions' bundled binary (newest first), then the
|
||||
// standalone installs. The bare name when nothing is found.
|
||||
std::string FindClaudeBin(const std::map<std::string, std::string>& env, const std::string& home);
|
||||
std::string FindClaudeBin();
|
||||
|
||||
// The report's zone: $TZ, else /etc/localtime. A zone by name, as
|
||||
// zoneinfo reads it (this libc++ has no tzdb, so the C library converts,
|
||||
// with TZ set to the name); an empty name is the process's own zone.
|
||||
struct Zone {
|
||||
std::string name;
|
||||
std::string label;
|
||||
};
|
||||
Zone LocalZone();
|
||||
Zone ZoneNamed(std::string_view name);
|
||||
// "02:40 (in 1h 50m)"; more than a day out with its date: "Wed 16 Sept,
|
||||
// 11:00 (in 6d 10h)".
|
||||
std::string FormatWhen(double epoch, double now, const Zone& zone);
|
||||
std::string RenderTable(const std::vector<std::vector<std::string>>& rows);
|
||||
std::string RenderStatusReport(const std::vector<Json>& seats, const Json& config, double now, const Zone& zone, const Json& board = nullptr,
|
||||
const std::map<std::int32_t, std::string>& keys = {});
|
||||
std::string RenderApiReport(const std::vector<Json>& apiSeats, const Json& apiConfig, const Json& board = nullptr,
|
||||
const std::map<std::int32_t, std::string>& keys = {});
|
||||
std::string RenderDrivers(const Json& board, double now, const Zone& zone);
|
||||
}
|
||||
|
|
@ -23,5 +23,8 @@ export import :Totp;
|
|||
export import :Stream;
|
||||
export import :Prompt;
|
||||
export import :Config;
|
||||
export import :Memory;
|
||||
export import :Mcp;
|
||||
export import :Seats;
|
||||
export import :Freeze;
|
||||
export import :Driver;
|
||||
|
|
|
|||
13
project.cpp
13
project.cpp
|
|
@ -13,7 +13,7 @@ using namespace Crafter;
|
|||
namespace {
|
||||
// The umbrella, then the partitions in import order: each after the ones
|
||||
// it imports, which is also the order the module map in AGENTS.md uses.
|
||||
constexpr std::array<std::string_view, 23> Interfaces = {
|
||||
constexpr std::array<std::string_view, 26> Interfaces = {
|
||||
"interfaces/Crafter.Review",
|
||||
"interfaces/Crafter.Review-Unicode",
|
||||
"interfaces/Crafter.Review-Json",
|
||||
|
|
@ -35,10 +35,13 @@ namespace {
|
|||
"interfaces/Crafter.Review-Stream",
|
||||
"interfaces/Crafter.Review-Prompt",
|
||||
"interfaces/Crafter.Review-Config",
|
||||
"interfaces/Crafter.Review-Memory",
|
||||
"interfaces/Crafter.Review-Mcp",
|
||||
"interfaces/Crafter.Review-Seats",
|
||||
"interfaces/Crafter.Review-Freeze",
|
||||
"interfaces/Crafter.Review-Driver",
|
||||
};
|
||||
constexpr std::array<std::string_view, 20> Implementations = {
|
||||
constexpr std::array<std::string_view, 23> Implementations = {
|
||||
"implementations/Crafter.Review-Text",
|
||||
"implementations/Crafter.Review-Crypto",
|
||||
"implementations/Crafter.Review-Platform",
|
||||
|
|
@ -57,6 +60,9 @@ namespace {
|
|||
"implementations/Crafter.Review-Stream",
|
||||
"implementations/Crafter.Review-Prompt",
|
||||
"implementations/Crafter.Review-Config",
|
||||
"implementations/Crafter.Review-Memory",
|
||||
"implementations/Crafter.Review-Mcp",
|
||||
"implementations/Crafter.Review-Seats",
|
||||
"implementations/Crafter.Review-Freeze",
|
||||
"implementations/Crafter.Review-Driver",
|
||||
};
|
||||
|
|
@ -68,7 +74,7 @@ namespace {
|
|||
|
||||
// One per directory under tests/, each an ordinary main() that links the
|
||||
// library: exit 0 passes, nonzero fails, 77 skips.
|
||||
constexpr std::array<std::string_view, 14> Tests = {
|
||||
constexpr std::array<std::string_view, 15> Tests = {
|
||||
"Crypto",
|
||||
"Text",
|
||||
"Platform",
|
||||
|
|
@ -83,6 +89,7 @@ namespace {
|
|||
"CodeKeys",
|
||||
"Freeze",
|
||||
"PgWire",
|
||||
"Seats",
|
||||
};
|
||||
|
||||
// The version the binary reports: CI sets it from the tag, a checkout
|
||||
|
|
|
|||
250
tests/Seats/main.cpp
Normal file
250
tests/Seats/main.cpp
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// Seat usage: the pure functions against reference vectors, numbered in
|
||||
// order, so "check 17" always names the same vector.
|
||||
|
||||
import std;
|
||||
import Crafter.Review;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter::Review;
|
||||
using namespace Crafter::Review::Seats;
|
||||
|
||||
namespace {
|
||||
std::int32_t Failures = 0;
|
||||
std::int32_t N = 0;
|
||||
|
||||
void Eq(const std::string& got, const std::string& want) {
|
||||
++N;
|
||||
if (got != want) {
|
||||
std::println(std::cerr, "check {}:\n got: {}\nwant: {}", N, Dyn::Repr(got), Dyn::Repr(want));
|
||||
++Failures;
|
||||
}
|
||||
}
|
||||
|
||||
void Eq(bool got, bool want) {
|
||||
Eq(std::string(got ? "True" : "False"), std::string(want ? "True" : "False"));
|
||||
}
|
||||
|
||||
void EqJson(const Json& got, const Json& want) {
|
||||
++N;
|
||||
if (!Dyn::Equal(got, want)) {
|
||||
std::println(std::cerr, "check {}:\n got: {}\nwant: {}", N, DumpCompactSorted(got), DumpCompactSorted(want));
|
||||
++Failures;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> Lines(const std::string& text) {
|
||||
return Dyn::Split(text, "\n");
|
||||
}
|
||||
|
||||
Json With(Json value, const Json& changes) {
|
||||
for (auto it = changes.begin(); it != changes.end(); ++it) value[it.key()] = it.value();
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
const Zone utc = ZoneNamed("UTC");
|
||||
const Zone ams = ZoneNamed("Europe/Amsterdam");
|
||||
// 2026-09-07 10:00 UTC
|
||||
constexpr double now = 1788775200.0;
|
||||
const auto nowS = static_cast<std::int64_t>(now);
|
||||
const Json cfg = DefaultConfig();
|
||||
auto seat = [&](std::int32_t k, double five, double seven = 0, const Json& over = Json::object()) {
|
||||
Json windows = {{"five_hour", {{"utilization", five}, {"resetsAt", nowS + 300 * k}}}, {"seven_day", {{"utilization", seven}, {"resetsAt", nowS + 3 * 86400}}}};
|
||||
// An integer stays an integer.
|
||||
for (auto& w : windows) {
|
||||
const double u = w.at("utilization").get<double>();
|
||||
if (u == std::floor(u)) w["utilization"] = static_cast<std::int64_t>(u);
|
||||
}
|
||||
const auto [util, binding] = SeatUtil(windows);
|
||||
Json s = {{"seat", k}, {"state", "ok"}, {"windows", windows}, {"util", util}, {"binding", binding}, {"resetsAt", windows.at("five_hour").at("resetsAt")}};
|
||||
return With(s, over);
|
||||
};
|
||||
const std::string fable(FableWindow);
|
||||
|
||||
// format_when: the clock in the report zone and a countdown; a reset over
|
||||
// a day out carries its date.
|
||||
Eq(FormatWhen(nowS + 45 * 60, now, utc), "10:45 (in 45 min)");
|
||||
Eq(FormatWhen(nowS + 3 * 3600 + 5 * 60, now, ams), "15:05 (in 3h 05m)");
|
||||
Eq(FormatWhen(nowS + 3 * 86400, now, utc), "Thu 10 Sept, 10:00 (in 3d 0h)");
|
||||
Eq(FormatWhen(nowS - 60, now, utc), "09:59 (now)");
|
||||
// The report: a row per seat, columns padded to line up; who is eligible
|
||||
// and where the next lens lands.
|
||||
const Json lim = seat(1, 116, 21, {{"state", "limited"}, {"util", 115}, {"error", "five_hour rejected, overage out_of_credits"}});
|
||||
Json three = seat(3, 62, 16);
|
||||
three["windows"][fable] = {{"utilization", 22}, {"resetsAt", 0}};
|
||||
std::vector<std::string> lines = Lines(RenderStatusReport({lim, seat(2, 61, 18), three}, cfg, now, utc));
|
||||
Eq(lines.at(0), "| seat | 5h | resets | 7d | resets | fable 7d |");
|
||||
Eq(lines.at(1), "|------|------|-------------------|-----|-------------------------------|----------|");
|
||||
Eq(lines.at(2), "| 1 | 116% | 10:05 (in 5 min) | 21% | Thu 10 Sept, 10:00 (in 3d 0h) | — |");
|
||||
Eq(lines.at(3), "| 2 | 61% | 10:10 (in 10 min) | 18% | Thu 10 Sept, 10:00 (in 3d 0h) | — |");
|
||||
Eq(lines.at(4), "| 3 | 62% | 10:15 (in 15 min) | 16% | Thu 10 Sept, 10:00 (in 3d 0h) | 22% |");
|
||||
Eq(lines.at(5), "");
|
||||
Eq(lines.at(6), "policy spread · ceiling 85% · handover at 90% · a lens is placed at +10% · 7d cutoff 95%");
|
||||
Eq(lines.at(7), "eligible now: seat 2 (25% headroom), seat 3 (25% headroom) → next lens: seat 2");
|
||||
Eq(lines.at(8), "fable lenses: seat 3 (63% fable headroom)");
|
||||
Eq(lines.at(9), "limited: seat 1 until 10:05 (in 5 min)");
|
||||
// A seat the probe could not read: an empty row, the reason under the table.
|
||||
const std::string dead = RenderStatusReport({seat(2, 10), {{"seat", 4}, {"state", "unknown"}, {"windows", Json::object()}, {"error", "probe timeout after 20000 ms"}}}, cfg, now, utc);
|
||||
Eq(dead.contains("| 4 | — | — | — | — | — |"), true);
|
||||
// The board: the passes every driver on this machine has in flight on an
|
||||
// account, a column only when there are any.
|
||||
const std::vector<std::string> held = Lines(RenderStatusReport({seat(2, 61, 18), three}, cfg, now, utc, {{"seats", {{"k3", {{"inflight", 4}}}}}}, {{3, "k3"}}));
|
||||
Eq(held.at(0), "| seat | 5h | resets | 7d | resets | fable 7d | in flight |");
|
||||
Eq(held.at(3), "| 3 | 62% | 10:15 (in 15 min) | 16% | Thu 10 Sept, 10:00 (in 3d 0h) | 22% | 4 |");
|
||||
Eq(dead.contains("not usable: seat 4 unknown (probe timeout after 20000 ms)"), true);
|
||||
// A Fable probe refused for the Fable window needs the Haiku stand-in,
|
||||
// which it then lands on: its window, and 'limited' for that window alone.
|
||||
Json fableWindows = seat(2, 10).at("windows");
|
||||
fableWindows[fable] = {{"utilization", 100}, {"resetsAt", nowS + 86400}};
|
||||
const Json fableLimited = With(seat(2, 10), {{"state", "limited"}, {"util", 100}, {"binding", fable}, {"error", std::format("{} rejected", fable)}, {"windows", fableWindows}});
|
||||
Eq(NeedsHaiku(fableLimited), true);
|
||||
const Json merged = MergeFableProbe(seat(2, 10), fableLimited);
|
||||
EqJson(merged.at("fable"), {{"state", "limited"}});
|
||||
EqJson(merged.at("windows").at(fable), {{"utilization", 100}, {"resetsAt", nowS + 86400}});
|
||||
std::string r = RenderStatusReport({merged}, cfg, now, utc);
|
||||
Eq(Lines(r).at(2).contains("| limited (100%) |"), true);
|
||||
Eq(r.contains("fable lenses: no seat with room on its Fable window — Fable lenses run on their fallback model"), true);
|
||||
Eq(r.contains("eligible now: seat 2 (75% headroom)"), true);
|
||||
// A readable Fable probe is the seat itself.
|
||||
Json fullWindows = seat(3, 20, 30).at("windows");
|
||||
fullWindows[fable] = {{"utilization", 44}, {"resetsAt", nowS + 86400}};
|
||||
const Json full = With(seat(3, 20, 30), {{"windows", fullWindows}});
|
||||
Eq(NeedsHaiku(full), false);
|
||||
EqJson(MergeFableProbe(full, full).at("fable"), {{"state", "ok"}});
|
||||
// A drained Fable window makes an Opus seat, never an idle one.
|
||||
Json drainedWindows = seat(4, 10, 20).at("windows");
|
||||
drainedWindows[fable] = {{"utilization", 98}, {"resetsAt", nowS + 86400}};
|
||||
Json drained = With(seat(4, 10, 20), {{"util", 100}, {"binding", fable}, {"windows", drainedWindows}});
|
||||
drained = MergeFableProbe(drained, drained);
|
||||
EqJson(Json::array({drained.at("util"), drained.at("binding"), drained.at("fable")}), Json::array({20, "seven_day", {{"state", "ok"}}}));
|
||||
r = RenderStatusReport({drained, full}, cfg, now, utc);
|
||||
Eq(r.contains("eligible now: seat 4 (65% headroom), seat 3 (55% headroom) → next lens: seat 4"), true);
|
||||
Eq(r.contains("fable lenses: seat 3 (41% fable headroom)"), true);
|
||||
// Reported under the CLI's other name, it lands under the canonical key.
|
||||
Json altWindows = seat(5, 10).at("windows");
|
||||
altWindows["seven_day_fable"] = {{"utilization", 44}, {"resetsAt", nowS + 86400}};
|
||||
Json alt = With(seat(5, 10), {{"windows", altWindows}});
|
||||
alt = MergeFableProbe(alt, alt);
|
||||
EqJson(alt.at("windows").at(fable), {{"utilization", 44}, {"resetsAt", nowS + 86400}});
|
||||
Eq(alt.at("windows").contains("seven_day_fable"), false);
|
||||
Eq(RenderStatusReport({alt}, cfg, now, utc).contains("fable lenses: seat 5 (41% fable headroom)"), true);
|
||||
// A Fable probe refused for the 5-hour window: the seat is limited for
|
||||
// everything, no stand-in, the Fable window still read.
|
||||
Json fiveWindows = seat(2, 110).at("windows");
|
||||
fiveWindows[fable] = {{"utilization", 40}, {"resetsAt", nowS + 86400}};
|
||||
const Json fiveLimited = With(seat(2, 110), {{"state", "limited"}, {"util", 110}, {"binding", "five_hour"}, {"error", "five_hour rejected"}, {"windows", fiveWindows}});
|
||||
Eq(NeedsHaiku(fiveLimited), false);
|
||||
r = RenderStatusReport({MergeFableProbe(fiveLimited, fiveLimited)}, cfg, now, utc);
|
||||
Eq(Lines(r).at(2).contains("| 110% | 10:10 (in 10 min) | 0%") && Lines(r).at(2).ends_with("| 40% |"), true);
|
||||
Eq(r.contains("limited: seat 2 until 10:10 (in 10 min)"), true);
|
||||
Eq(r.contains("fable lenses: seat 2"), false);
|
||||
Eq(NeedsHaiku({{"seat", 1}, {"state", "unknown"}, {"windows", Json::object()}, {"error", "probe timeout"}}), true);
|
||||
Eq(NeedsHaiku(seat(1, 10)), false);
|
||||
// A Fable probe that could not be read: its state in the cell, the reason
|
||||
// under the table.
|
||||
const Json deadFable = MergeFableProbe(seat(2, 10), {{"seat", 2}, {"state", "unknown"}, {"windows", Json::object()}, {"error", "probe timeout after 20000 ms"}});
|
||||
r = RenderStatusReport({deadFable}, cfg, now, utc);
|
||||
Eq(Lines(r).at(2).contains("| unknown |"), true);
|
||||
Eq(r.contains("fable probe: seat 2 unknown (probe timeout after 20000 ms)"), true);
|
||||
const Json revoked = {{"seat", 3}, {"state", "invalid"}, {"windows", Json::object()}, {"error", "token revoked"}};
|
||||
r = RenderStatusReport({MergeFableProbe(revoked, revoked)}, cfg, now, utc);
|
||||
Eq(r.contains("not usable: seat 3 invalid (token revoked)"), true);
|
||||
Eq(r.contains("fable probe:"), false);
|
||||
// The local login is seat 0, named `local`.
|
||||
r = RenderStatusReport({seat(0, 30), seat(2, 50)}, cfg, now, utc);
|
||||
Eq(Lines(r).at(2).starts_with("| local | 30%"), true);
|
||||
Eq(r.contains("eligible now: local (55% headroom), seat 2 (35% headroom) → next lens: local"), true);
|
||||
// The pick: the pipeline's one-lens wave.
|
||||
auto unknown = [](std::int32_t k) { return Json{{"seat", k}, {"state", "unknown"}, {"windows", Json::object()}, {"error", "probe timeout"}}; };
|
||||
auto invalid = [](std::int32_t k) { return Json{{"seat", k}, {"state", "invalid"}, {"windows", Json::object()}, {"error", "authentication failed"}}; };
|
||||
EqJson(PickNext({}, cfg), {{"kind", "stop"}, {"status", "error"}, {"reason", "no seat token configured"}});
|
||||
EqJson(PickNext({invalid(1), invalid(2)}, cfg).at("reason"), "no valid seat token (seats 1, 2 rejected)");
|
||||
EqJson(PickNext({unknown(3), unknown(1)}, cfg), {{"kind", "seat"}, {"seat", 1}, {"source", "round-robin"}});
|
||||
EqJson(PickNext({seat(1, 5), seat(2, 1), seat(3, 0)}, cfg), {{"kind", "seat"}, {"seat", 2}, {"source", "probe"}});
|
||||
EqJson(PickNext({seat(1, 5), seat(2, 1), seat(3, 0)}, With(cfg, {{"policy", "pack"}})).at("seat"), 1);
|
||||
EqJson(PickNext({seat(1, 90, 10), seat(2, 80, 96), seat(3, 86)}, cfg).at("seat"), 3);
|
||||
EqJson(PickNext({seat(1, 50), unknown(2)}, cfg).at("seat"), 1);
|
||||
// Tokens: TOKEN1..6 by fixed name, the legacy name as seat 1, a dotenv
|
||||
// file's token lines only.
|
||||
const std::string text = Dyn::Join(std::vector<std::string>{"MAILOSAUR_API_KEY=abc def", "export CLAUDE_CODE_OAUTH_TOKEN1=\"one\"", "CLAUDE_CODE_OAUTH_TOKEN2='two'",
|
||||
"CLAUDE_CODE_OAUTH_TOKEN3 = three ", "CLAUDE_CODE_OAUTH_TOKEN7=seven", "CLAUDE_CODE_OAUTH_TOKEN=legacy",
|
||||
"# CLAUDE_CODE_OAUTH_TOKEN4=commented"}, "\n");
|
||||
const std::map<std::string, std::string> parsed = SeatTokensFromEnvFile(text);
|
||||
const std::map<std::string, std::string> wantParsed = {{"CLAUDE_CODE_OAUTH_TOKEN1", "one"}, {"CLAUDE_CODE_OAUTH_TOKEN2", "two"}, {"CLAUDE_CODE_OAUTH_TOKEN3", "three"}, {"CLAUDE_CODE_OAUTH_TOKEN", "legacy"}};
|
||||
Eq(parsed == wantParsed, true);
|
||||
std::vector<std::int32_t> ids;
|
||||
for (const auto& [n, token] : SeatTokensFromEnv(parsed)) ids.push_back(n);
|
||||
Eq(ids == std::vector<std::int32_t>{1, 2, 3}, true);
|
||||
using Tokens = std::vector<std::pair<std::int32_t, std::string>>;
|
||||
Eq(SeatTokensFromEnv({{"CLAUDE_CODE_OAUTH_TOKEN1", "a"}, {"CLAUDE_CODE_OAUTH_TOKEN2", " "}, {"CLAUDE_CODE_OAUTH_TOKEN3", "c"}, {"CLAUDE_CODE_OAUTH_TOKEN7", "x"},
|
||||
{"CLAUDE_CODE_OAUTH_TOKENX", "y"}, {"CLAUDE_CODE_OAUTH_TOKEN", "legacy"}}) == Tokens{{1, "a"}, {3, "c"}}, true);
|
||||
Eq(SeatTokensFromEnv({{"CLAUDE_CODE_OAUTH_TOKEN", "legacy"}, {"CLAUDE_CODE_OAUTH_TOKEN2", "b"}}) == Tokens{{1, "legacy"}, {2, "b"}}, true);
|
||||
Eq(SeatTokensFromEnv({}).empty(), true);
|
||||
// Config: repo variables override the defaults, garbage does not.
|
||||
EqJson(ConfigFromEnv({}), {{"maxUtil", 85}, {"lensCostPct", 10}, {"waitMaxMin", 15}, {"policy", "spread"}, {"handoverPct", 90}});
|
||||
EqJson(ConfigFromEnv({{"REVIEW_GATES_SEAT_MAX_UTIL", "70"}, {"REVIEW_GATES_LENS_COST_PCT", "abc"}, {"REVIEW_GATES_SEAT_WAIT_MIN", "0"}, {"REVIEW_GATES_SEAT_HANDOVER_PCT", "80"},
|
||||
{"REVIEW_GATES_SEAT_POLICY", "pack"}}),
|
||||
{{"maxUtil", 70}, {"lensCostPct", 10}, {"waitMaxMin", 0}, {"policy", "pack"}, {"handoverPct", 80}});
|
||||
EqJson(ConfigFromEnv({{"REVIEW_GATES_SEAT_POLICY", "weird"}}).at("policy"), "spread");
|
||||
// The stream: captured from claude 2.1.263, values shortened.
|
||||
const std::string init = R"({"type":"system","subtype":"init","model":"claude-haiku-4-5-20251001","claude_code_version":"2.1.263"})";
|
||||
const std::string event = R"({"type":"rate_limit_event","rate_limit_info":{"status":"allowed","resetsAt":1788789600,"rateLimitType":"five_hour",)"
|
||||
R"("overageStatus":"rejected","overageDisabledReason":"out_of_credits","isUsingOverage":false,"unifiedWindows":{)"
|
||||
R"("five_hour":{"utilization":0.05,"resetsAt":1788789600},"seven_day":{"utilization":0.01,"resetsAt":1789038000}}},)"
|
||||
R"("uuid":"u","session_id":"400f3eb9-4732-42f1-a257-b84d1da95164"})";
|
||||
const std::string assistant = R"({"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"ok"}]}})";
|
||||
const std::string result = R"({"type":"result","subtype":"success","is_error":false,"structured_output":{"ok":true},"result":"ok","modelUsage":{"claude-haiku-4-5-20251001":{"inputTokens":134}}})";
|
||||
for (const std::vector<std::string>& order : {std::vector<std::string>{init, event, assistant, result}, std::vector<std::string>{init, assistant, event, result},
|
||||
std::vector<std::string>{init, assistant, result, event}}) {
|
||||
const Json p = ParseStream(std::format("{}\n", Dyn::Join(order, "\n")));
|
||||
EqJson(p.at("result").at("structured_output"), {{"ok", true}});
|
||||
EqJson(p.at("sessionId"), "400f3eb9-4732-42f1-a257-b84d1da95164");
|
||||
const Json& rl = p.at("rateLimit");
|
||||
EqJson(Json::array({rl.at("status"), rl.at("rateLimitType"), rl.at("resetsAt"), rl.at("overageReason")}), Json::array({"allowed", "five_hour", 1788789600, "out_of_credits"}));
|
||||
EqJson(rl.at("windows").at("five_hour"), {{"utilization", 5}, {"resetsAt", 1788789600}});
|
||||
EqJson(rl.at("windows").at("seven_day"), {{"utilization", 1}, {"resetsAt", 1789038000}});
|
||||
}
|
||||
EqJson(ParseStream(Dyn::Join(std::vector<std::string>{"garbage", event, "more garbage", result}, "\n")).at("result").at("structured_output"), {{"ok", true}});
|
||||
EqJson(ParseStream("{\n \"type\": \"result\",\n \"subtype\": \"success\"\n}").at("result"), nullptr);
|
||||
EqJson(ParseStream("").at("rateLimit"), nullptr);
|
||||
EqJson(Json::array({Pct(1.16), Pct(0.05), Pct(0.625), Pct("x"), Pct(-1)}), Json::array({116, 5, 62.5, 0, 0}));
|
||||
auto pair = [](const std::pair<Json, std::string>& p) { return Json::array({p.first, p.second}); };
|
||||
EqJson(pair(SeatUtil({{"five_hour", {{"utilization", 61}, {"resetsAt", 1}}}, {"seven_day", {{"utilization", 18}, {"resetsAt", 2}}}})), Json::array({60, "five_hour"}));
|
||||
EqJson(pair(SeatUtil({{"five_hour", {{"utilization", 12}, {"resetsAt", 1}}}, {"seven_day_fable", {{"utilization", 97}, {"resetsAt", 2}}}})), Json::array({95, "seven_day_fable"}));
|
||||
Eq(IsEligible(seat(1, 10, 96), With(cfg, {{"maxUtil", 100}})), false);
|
||||
Eq(IsEligible(seat(1, 10, 94), With(cfg, {{"maxUtil", 100}})), true);
|
||||
Eq(IsEligible(seat(1, 10, 94), cfg), false);
|
||||
// Redaction: the seat's own token, token shapes and addresses; auth
|
||||
// failures by status or by text.
|
||||
const std::vector<std::string> secret = {"lit-secret"};
|
||||
Eq(ClipReason("token sk-ant-oat01-abcdefghijklmnopqrstuvwxyz0123 and jorijn@example.com x\n y", secret), "token [redacted:token] and [redacted:email] x y");
|
||||
Eq(ClipReason("bad lit-secret here", secret), "bad [redacted:token] here");
|
||||
Eq(LooksLikeAuthFailure({{"api_error_status", 401}}, ""), true);
|
||||
Eq(LooksLikeAuthFailure(nullptr, "Invalid API key · Please run /login"), true);
|
||||
Eq(LooksLikeAuthFailure({{"result", "ok"}}, ""), false);
|
||||
// find_claude_bin: CLAUDE_BIN, then PATH, then the newest editor-bundled
|
||||
// binary (numerically), else the bare name.
|
||||
const fs::path home = fs::temp_directory_path() / std::format("seats-home-{}", Platform::PublishedPid());
|
||||
fs::remove_all(home);
|
||||
fs::create_directories(home);
|
||||
Eq(FindClaudeBin({{"CLAUDE_BIN", "/x/claude"}, {"PATH", ""}}, home.string()), "/x/claude");
|
||||
const fs::path binDir = home / "bin";
|
||||
fs::create_directories(binDir);
|
||||
const fs::path fake = binDir / "claude";
|
||||
std::ofstream(fake).close();
|
||||
fs::permissions(fake, fs::perms::owner_all | fs::perms::group_read | fs::perms::group_exec | fs::perms::others_read | fs::perms::others_exec);
|
||||
Eq(FindClaudeBin({{"PATH", std::format("/nowhere:{}", binDir.string())}}, home.string()), fake.string());
|
||||
for (const std::string_view v : {"2.1.100-linux-x64", "2.1.99-linux-x64", "2.1.263-linux-x64"}) {
|
||||
const fs::path d = home / ".vscode-oss/extensions" / std::format("anthropic.claude-code-{}", v) / "resources/native-binary";
|
||||
fs::create_directories(d);
|
||||
std::ofstream(d / "claude").close();
|
||||
}
|
||||
Eq(FindClaudeBin({{"PATH", "/nowhere"}}, home.string()), (home / ".vscode-oss/extensions/anthropic.claude-code-2.1.263-linux-x64/resources/native-binary/claude").string());
|
||||
Eq(FindClaudeBin({{"PATH", "/nowhere"}}, (home / "empty").string()), "claude");
|
||||
fs::remove_all(home);
|
||||
std::println("seats: {} checks{}", N, Failures ? std::format(", {} failed", Failures) : std::string(" ok"));
|
||||
return Failures ? 1 : 0;
|
||||
}
|
||||
Loading…
Reference in a new issue