feat(seats): the env file's settings count, and a machine's own seats file
ApiConfig and PoolConfig read the seat order, the budgets and the concurrency from the process environment alone, so a `.env` that said REVIEW_GATES_SEAT_ORDER=oauth-first ordered nothing: the Python driver reads those lines from the file under the process environment (config_env, 2026-10-17: a setting lives beside the seats it orders). SeatPool::SettingsOf does the same - the file's placement settings under the process environment, in a map handed to the placement readers alone (ApiConfig, PoolConfig, Seats::ConfigFromEnv) - and leaves the process environment untouched, so no other reader sees a line of the file: a path's or a binary's line reaches nothing, and two drivers reading one .env keep one board and one ledger. The placement settings are one list, SeatPool::PlacementSettings - the fifteen names those readers read - for the map, the run's log and what makes a working directory's .env a seats file; a REVIEW_GATES_* line of any other name stays out of the map and is named, by the run's log and on `seats`'s stderr. One dotenv grammar serves the tokens and the settings (Seats::DotenvLines). `run` and `seats` resolve the file from their own --env-file (SeatPool::ResolveSeatsFile): the --env-file, else $SEATS_ENV_FILE, else the working directory's .env - the operator's, as the Python driver and `seats` read it, not the repository under review's - when it is a file holding a seat or a placement setting, each with a value, or one that cannot be read as UTF-8 text (its error is then the command's) - a readable application .env, its Vite or Supabase lines, one of blank placeholder lines or one that is no file is passed over, and the run says so - else the machine's own $XDG_CONFIG_HOME/crafter-review/env, else ~/.config/crafter-review/env: one file for every repository on a machine, a repository's seats .env winning over it. Each file is read once and the settings and the tokens are taken from that read (SeatsFile::Text), so a file that can be read only once - a pipe, `--env-file <(gpg -d seats.env.gpg)`, a FIFO - gives its settings and its seats alike, and a failed read raises its own error where the tokens are laid over the environment: SeatPool::WithFileTokens, for `run` and `seats` alike, and LoadSeats takes the merged environment and no longer reads a file by path. An empty `seats --env-file` means no file now, as it did for `run`, and the `seats` report names the side that runs first (it said "api seats run first" whatever the order). Runs, Commands and KernelModes, the tests that run `run` and `seats`, point XDG_CONFIG_HOME, and HOST_XDG_CONFIG_HOME for the Flatpak, at a path of their own that nothing creates, so a global file stays out of them; the grammar, the settings and their precedence, the lookup and the one read have their tests, a run with `--env` (a prefix) and with a repeated --env-file, and a `seats` with `--env`, take tokens and settings from the one file, an application's .env and a venv are passed over, a FIFO is read once, no line of the file reaches `stop`, every listed placement setting moves a reader, another REVIEW_GATES_* line is named and a .env of blank lines passed over, and the README, the environment table and the procedure say the new rule.
This commit is contained in:
parent
0e0983f278
commit
e215ad986e
16 changed files with 483 additions and 91 deletions
|
|
@ -310,7 +310,7 @@ inventory unit, against a frozen commit range.)";
|
|||
{"--models", true, "MODELS", models},
|
||||
{"--scratch", true, "SCRATCH", ""},
|
||||
{"--seats", true, "SEATS", "the seats the lenses run on: auto (every Anthropic seat in the env file, else local), local, api, oauth, openrouter (never in auto), or a list like api1,1,2,local"},
|
||||
{"--env-file", true, "ENV_FILE", "dotenv file with CLAUDE_CODE_OAUTH_TOKEN1..6 / ANTHROPIC_API_KEY1..6 (default $SEATS_ENV_FILE, else the repository .env)"},
|
||||
{"--env-file", true, "ENV_FILE", "dotenv file with CLAUDE_CODE_OAUTH_TOKEN1..6 / ANTHROPIC_API_KEY1..6 and the placement settings (REVIEW_GATES_SEAT_ORDER, the budgets, the concurrency): default $SEATS_ENV_FILE, else the working directory's .env when it holds a seat or a placement setting, else $XDG_CONFIG_HOME/crafter-review/env (~/.config/crafter-review/env); an empty value: none"},
|
||||
{"--no-probe", false, "", "skip the one-request probe of every seat at the start"},
|
||||
{"--api-budget", true, "API_BUDGET", "USD an API-credit seat may spend per cycle (default REVIEW_GATES_API_BUDGET_USD or 200)", Kind::Float},
|
||||
{"--harness", true, "{claude,opencode}",
|
||||
|
|
@ -477,7 +477,7 @@ inventory unit, against a frozen commit range.)";
|
|||
|
||||
// ---- seats: the report, the ledger and the board beside it ------------------
|
||||
constexpr std::array<OptionSpec, 5> SeatsOptions = {{
|
||||
{"--env-file", true, "ENV_FILE", "dotenv file holding the seat tokens (default: $SEATS_ENV_FILE, else ./.env when it exists)"},
|
||||
{"--env-file", true, "ENV_FILE", "dotenv file holding the seat tokens and the placement settings: default $SEATS_ENV_FILE, else the working directory's .env when it holds a seat or a placement setting, else $XDG_CONFIG_HOME/crafter-review/env (~/.config/crafter-review/env); an empty value: none"},
|
||||
{"--json", false, "", "the probe results as JSON instead of the report"},
|
||||
{"--no-fable", false, "", "probe on Haiku instead of Fable (cheaper; the Fable weekly window stays unread)"},
|
||||
{"--local", false, "", "also probe the CLI's own login, shown as the seat `local`"},
|
||||
|
|
@ -541,18 +541,24 @@ inventory unit, against a frozen commit range.)";
|
|||
if (!parsed.error.empty()) return UsageError(UsageLine(Prog, SeatsOptions), Prog, parsed.error);
|
||||
const std::optional<std::string> timeoutText = parsed.Value("timeout");
|
||||
const double timeout = timeoutText ? Dyn::ParseFloat(*timeoutText).value_or(Seats::ProbeTimeoutS) : Seats::ProbeTimeoutS;
|
||||
// The seats file, read once: its placement settings, under the
|
||||
// process environment, for the placement readers alone
|
||||
// (SeatPool::SettingsOf; any other REVIEW_GATES_* line of it is
|
||||
// named on stderr), its tokens over the process environment for
|
||||
// the probes (SeatPool::WithFileTokens).
|
||||
const SeatPool::SeatsFile seatsFile = SeatPool::ResolveSeatsFile(parsed.Value("env-file"), fs::current_path());
|
||||
const std::optional<fs::path> envPath = seatsFile.path;
|
||||
const std::map<std::string, std::string> process = Platform::Environment();
|
||||
const Json cfg = Seats::ConfigFromEnv(process);
|
||||
std::string envFile = parsed.Value("env-file").value_or("");
|
||||
if (envFile.empty()) envFile = Platform::GetEnv("SEATS_ENV_FILE").value_or("");
|
||||
std::error_code error;
|
||||
if (envFile.empty() && fs::exists(".env", error)) envFile = ".env";
|
||||
std::map<std::string, std::string> env = process;
|
||||
if (!envFile.empty()) {
|
||||
// The env file, read as text: one that cannot be read, or is not
|
||||
// UTF-8, ends the command.
|
||||
for (const auto& [name, value] : Seats::SeatTokensFromEnvFile(Dyn::ReadText(envFile))) env[name] = value;
|
||||
const SeatPool::Settings fileSettings = SeatPool::SettingsOf(seatsFile);
|
||||
const std::map<std::string, std::string>& settings = fileSettings.env;
|
||||
if (!fileSettings.ignored.empty() && envPath) {
|
||||
std::println(std::cerr, "seats file: {} in {} - no placement setting, read from the process environment alone", Dyn::Join(fileSettings.ignored, ", "), envPath->string());
|
||||
}
|
||||
const Json cfg = Seats::ConfigFromEnv(settings);
|
||||
std::string envFile = envPath ? envPath->string() : std::string();
|
||||
// The file's one read: one that could not be read, or is not UTF-8,
|
||||
// ends the command with the read's error.
|
||||
std::map<std::string, std::string> env = SeatPool::WithFileTokens(process, seatsFile);
|
||||
std::vector<SeatJob> jobs;
|
||||
for (const auto& [seat, token] : Seats::SeatTokensFromEnv(env)) jobs.push_back({seat, token, false});
|
||||
if (parsed.Flag("local")) jobs.insert(jobs.begin(), SeatJob {0, "", true});
|
||||
|
|
@ -589,7 +595,7 @@ inventory unit, against a frozen commit range.)";
|
|||
seats.push_back(Seats::MergeFableProbe(found != standIns.end() ? found->second : s, s));
|
||||
}
|
||||
}
|
||||
const Json apiConfig = SeatPool::ApiConfig(process);
|
||||
const Json apiConfig = SeatPool::ApiConfig(settings);
|
||||
std::vector<Json> apiSeats;
|
||||
if (!apiJobs.empty()) {
|
||||
std::vector<SeatJob> todo;
|
||||
|
|
|
|||
Loading…
Reference in a new issue