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:
Jorijn van der Graaf 2026-10-31 07:32:14 +01:00
commit 8ffbd6cf5d
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
9 changed files with 1764 additions and 3 deletions

View 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);
}

View 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;
};
}

View 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);
}

View file

@ -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;