- `seats` reports every seat probed (Fable first, Haiku for a seat that needs it), the API seats beside the driver's ledger, the drivers on the board, or all of it as JSON. In the sandbox a last line names `stop`, since the pids above cannot be signalled from there. - `init` sets a repository up: the config from the template, the artifact and worktree dirs ignored, the prepush skill with a marker that lets a later init refresh it. The old link into the review-gates submodule is replaced by a real directory, never written through. A foreign link, a subdirectory, the home directory and a dir outside git are refused before anything is written. - `doc [--path] prepush|manifest|config` prints a shipped text, or where the host sees it (the deployment's `active` link in the Flatpak). - `browser install` runs the installer of the Playwright the pinned MCP server depends on, and checks the Chromium revision landed. - `stop <scratch>` and `stop --hash <hash8>` write the stop file a running driver checks every 2 s, for live drivers only; the board is read, never written. The option parser takes positionals, in the usage line, the help and the errors. The Commands test runs all of them through this binary, seats on a claude stand-in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
126 lines
7 KiB
C++
126 lines
7 KiB
C++
// 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": the clock in the zone.
|
|
std::string Clock(double epoch, const Zone& zone);
|
|
// "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);
|
|
}
|