Harness laws: settings and directors #
Shared rules (what a law secures, the adversary, H1 to H6): harness.md.
S4. Settings as console variables #
Each setting is a console variable (cvar), declared once in one table: id, type, scope, two flags, default. Id = dotted key path in settings.json (retry.enabled).
- Registry holds each id once. Duplicate id, or default not of the cvar's type: eukhe stops at startup (
RegistryError). - Scope Global or Local. Global: one value for every agent, set only by the global config. Local: may differ per agent.
persist: an agent may write its sets of the cvar to its cfg.replicate: clients see the value.
Value layers, higher wins:
default < config < each agent of the family, root first:
its profile < its own sets
-
Config:
settings.json(global, then project), thenconfig.cfg(global<agent dir>/config.cfg, then project.eukhe/config.cfg). Project files obeysettings.jsonproject trust rules; never set a Global cvar. -
Subagent profile:
subagent.cfg, thenagents/<name>.cfgfor a named agent. Root session = agentmain. -
Agent sets: runtime sets (
/set,/toggle, session switches). Session state: conversation documenteukhe.cvar.agent, committed per change; shown in the state tree; restored by rewind or fork. -
Per layer, the last valid set wins. Valid: cvar declared, value of its type; in a profile or agent sets also Local. Invalid: typed
CvarError(Undeclared,BadValue,Global,NotBool). -
Subagent starts from its parent's family (spawn carries parent layers); own profile and sets on top.
-
Session switches (transport, steering mode, follow-up mode, auto retry, auto compaction, service tier): sets of the session's own agent; never change another session.
-
Session cannot set a Global cvar;
/set --globalwrites it to globalconfig.cfg. -
togglein/set --globalflips only the global layer (globalsettings.json, then globalconfig.cfg); a project file never changes what it flips. -
Cfg read-change-write holds the file lock: concurrent sessions lose no set.
-
Existing unreadable cfg file: settings error, never written. Profile file errors and unparsable agent-state aliases: settings errors.
-
eukhe.settingsservice shows a session's settings errors as aneukhe.settings_errorrow of its main conversation (display-only, not in model context): at open, and each new error after reload; each distinct error once per opened session.
One command language for cfg files, the /set family and key binds. Line = commands separated by ;; double quotes keep spaces.
| Command | Does |
|---|---|
set <id> <value> |
Sets the cvar. |
toggle <id> |
Bool cvar to the opposite of its value in the agent (inherited values included). |
alias <name> "<cmds>" |
Names a command list. |
bind <key> "<cmds>" |
Commands a key press runs. |
action <id> |
Runs a built-in client action (in a bind). |
<name> |
Runs alias <name>. |
- Expansion replaces each alias run with its commands; nests at most one level per table alias, plus one. Deeper = repeated alias = cycle (
ExpandError::Cycle). Missing alias:ExpandError::Unknown. Failed expansion runs nothing. - Key binds:
<agent dir>/binds.cfg.- First bind of a key wins; user binds before defaults.
- Unbound key runs nothing; empty bind (
bind <key> "") disables a default. - Default actions sharing a key share one bind; the screen receiving the key runs its own action.
- Press sends
set,toggle,aliasto the session; runsactionin the client. - No
binds.cfg: written once fromkeybindings.json, same actions per key. - Unreadable
binds.cfgor unwritable import: warning at startup and/reload.
/cvar savewrites the agent'spersistsets toagents/<name>.cfg; read back as profile, same values.
Model: bend/cvar.bend (registry, value, written, toggle, expand, press). Laws: laws/cvar.bend (S4). Proofs: proof/cvar/. Free inputs: any declarations (duplicate ids, wrong defaults), config, agent family with any profiles and sets (wrong types, Global cvars, undeclared ids), alias table, binds.
| Law | Says |
|---|---|
cvar_registry_unique |
Each id once in registry (S4.1). |
cvar_registry_typed |
Each default has its type (S4.1). |
cvar_value_typed |
Each value has its cvar's type in each agent; undeclared cvar has no value. |
cvar_inherits |
Subagent without profile or sets has parent's values. |
cvar_set_wins |
Subagent's last valid set beats parent's value. |
cvar_global_uniform |
Global cvar: one value in every agent, config over default (S4.2). |
cvar_written_flagged |
Agent writes to cfg only persist cvars, replicates only replicate cvars (S4.3). |
cvar_persist_roundtrip |
Written cfg read back as profile gives each persisting cvar's value (S4.6). |
cvar_toggle_twice |
Two toggles restore the value. |
alias_expands_fully |
Expansion leaves no alias, or reports cycle or unknown alias (S4.4). |
alias_self_cycle |
Self-running alias is a cycle; the cycle names the alias (S4.4). |
alias_unknown_reported |
Run of a missing alias is reported with its name (S4.4). |
alias_runs_body |
Alias of plain commands runs exactly them (S4.4). |
alias_expands_in_order |
A list expands command by command, in order: two lists expand to the first's commands then the second's; the first error, left to right, is the outcome (S4.4). |
alias_expand_example |
Anchor: the Rust test's table (chain of two aliases, cycle of two, run of a missing alias) (S4.4). |
bind_press_plain |
Key press runs only plain commands (S4.5). |
bind_unbound_runs_nothing |
Unbound key runs nothing (S4.5). |
bind_first_wins |
Key press runs the key's first bind (S4.5). |
Code:
eukhe-types/src/cvar.rs(command language):parse_list,parse_script,Aliases,Binds,ExpandError.eukhe-bend/src/rules/cvar.rs:expand,Presses: expansion and key presses run as the compiled model (entrybend/ffi_cvar.bend, lawslaws/cvar.bend); a press table lives in the core, so a press sends only its key.eukhe-core/src/settings/cvar.rs:Registry,Cvars::value,Cvars::execute,Sets::written,document_sets;settings/cvar/table.rs(declarations).settings/cfg.rs: config layer (settings.json, thenconfig.cfgfiles under trust rules).settings/manager.rs,settings/getters.rs: getters, all throughCvars.eukhe-core/src/durable/cvar_agent.rs(session agent):CvarAgent,eukhe.cvar.agentdocument,run_cvar_line,save_agent_cfg.durable/settings.rs:EukheSettings(session family over its config).eukhe-daemon/src/setting_switches.rs: daemon commandscvar { line, global },cvar_save; refusal answers itsCvarErrorasdata: { kind, id?, message }.- Spawn carries parent family in create config (
cvarFamily). eukhe-tui/src/session_ui/cvar.rs: TUI sends/set,/toggle,/alias,/cvarascvarlines.
Tie: settings::cvar::bridge::the_rust_cvars_equal_the_bend_model vs bridge/bridge_cvar.bend, byte for byte, 300 generated scenarios reaching every rule: registries (duplicate ids, wrong defaults); values over config and family (wrong types, Global cvars, undeclared ids, profiles, sets); toggles of a family's last agent (subagent toggles an inherited value); written sets (persist, replicate). Expansion and presses: no bridge, they run as the model; rules::cvar::tests checks the glue (names and commands to the model's numbers and back).
Regression tests:
a_switch_changes_only_its_own_session(daemon: two workers on one agent dir; one's switches change neither the other norsettings.json)a_setter_never_clobbers_a_global_settings_file_that_failed_to_parse(setter refuses to overwrite an unparsedsettings.json)cvar_rejects_a_global_cvar_from_a_session,a_subagent_inherits_its_parents_family_under_its_profile,a_fork_starts_from_the_agent_state_at_its_fork_point,an_unreadable_cfg_file_is_reported_and_never_overwritten,concurrent_global_sets_both_land,a_global_toggle_flips_the_global_value_not_the_projects,profile_and_agent_alias_errors_reach_the_manager,settings_errors_show_as_a_row_of_the_session,unreadable_binds_cfg_and_failed_import_are_warningskeybindings::tests(binds.cfgimport, presses)
S4b. Config documents #
Documents: settings.json (global, project), mcp-connections.json, auth.json. No input lost silently.
- Writer reads under lock, applies its change, writes. Read fails (unreadable, invalid JSON, not an object, unknown version): writes nothing, returns the error. Only a missing file reads as empty. Untouched keys keep values. Applies to
eukhe mcp add,eukhe mcp remove, every settings setter, connection records,AuthStoragesetandremove(both return the error to the caller). - Settings setter writes the on-disk document, not its loaded copy: keys that failed to load and keys another writer added stay. Refuses a value its setting cannot hold. If a higher layer sets the same setting (global
config.cfg, projectsettings.json, projectconfig.cfg), it names that file (Persisted::Shadowed); client shows it. Daemon shows an unsavable default model or thinking level as a settings error row. - Each
settings.jsonleaf applies or is named by a settings error. Leaf: a non-object value, or a cvar's whole value. Value its setting cannot hold:BadValue; undeclared key:Undeclared(sameCvarErrors and row asconfig.cfg). A bad leaf drops only itself. Known top-level keys come from theSettingstype. - Malformed limit fails closed: non-whole
providerMaxInFlightentry limits its provider to 1; non-map value limits each provider to 1.idleEvictionMinutesand session archive rules: whole number,offornone(NatOrOff); else settings error. - Untrusted project changes no limit, system prompt or credential:
maxToolRounds,subagentMaxConcurrency,subagentStallMs,toolBlockingBudgetMs,autonomous,directors,thinkingBudgets,retry.maxRetries,retry.failover.maxRetrieswithheld with the code-running keys, plus projectSYSTEM.mdandAPPEND_SYSTEM.md; trust warning names them.trustedProjects: global layer only (globalsettings.json, then globalconfig.cfg). mcpServersentry is a unit: a project entry replaces the same-named global entry, never takes its URL or credential.kernelTransportis a unit too.- Sessions directory, one precedence for every reader (CLI, dashboard, gateway, daemon;
settings::resolve_session_dir):--session-dir,EUKHE_SESSION_DIR(or legacyEUKHE_CODING_AGENT_SESSION_DIR),sessionDirsetting,<agent dir>/sessions.
Model: bend/config.bend. Laws: laws/config.bend. Proofs: proof/config/.
| Law | Says |
|---|---|
rmw_refuses_broken |
Unreadable or unparsable document never written (S4b.1). |
rmw_keeps_others |
Write keeps every untouched key (S4b.1). |
rmw_applies_change |
Changed key has the set value. |
settings_save_preserves |
Setter writes the on-disk document with its key changed, whatever copy it loaded (S4b.2). |
leaf_applied_or_reported |
Each leaf applies or is named by a diagnostic (S4b.3). |
leaf_not_both |
No leaf both applies and is named. |
limit_malformed_fails_closed |
Malformed in-flight limit = 1, never "no limit" (S4b.4). |
limit_number_applies |
Whole-number entry is its provider's limit. |
untrusted_changes_nothing_guarded |
Untrusted project changes no limit, prompt or credential (S4b.5). |
trusted_project_applies |
Trusted project's item applies. |
project_entry_never_merges |
Project MCP entry applies whole (S4b.6). |
global_entry_kept |
Global entry the project does not name stays whole. |
Saved default model laws: inference.md ("S6 models.json and the saved default model"), laws/default_model.bend.
Tie: settings::bridge::the_rust_config_code_equals_the_bend_model vs bridge/bridge_config.bend, byte for byte, 360 generated scenarios: each writer over missing, valid, invalid, unreadable, other-version files; setters after another writer's change; documents with good, wrong-typed, undeclared leaves; providerMaxInFlight values; projects under each trust; global and project mcpServers entries.
Regression tests: add_refuses_a_settings_file_that_does_not_parse, a_setter_keeps_a_field_that_failed_to_load_and_a_concurrent_edit, another_version_is_refused_and_never_rewritten, set_over_an_unreadable_auth_file_is_refused_and_writes_nothing, a_bad_leaf_drops_only_itself_and_is_named, a_setting_a_cfg_file_shadows_says_so, a_project_mcp_entry_never_takes_global_credentials.
S5. The Director stack #
After each final answer the agent loop asks a stack of Directors what to do. Director = one layer: a kind and a budget (loop steps it may still request). Kinds: goal, autonomous run, reminder, forced tool call.
- Top layer decides first. Verdicts:
Defer: next layer down decides; layer stays.Again: loop continues; costs 1 budget; no budget = yield.Yield: loop stops; control to user.Stack{l}: pushlon top of itself, loop continues; costs 1 budget; no budget = yield. Needs room: layers above (including deferred, left or led ones) + the layer + layers below < cap. No room: loop fails.Leave: layer removes itself; next layer down decides.Abort: loop stops with failure.Lead: layer leads the step's continuation: loop goes on → it leaves with it; step ends the loop (no lower layer continues, or no step left) → it stays. Either way the next layer down decides.
- No layer decides: control to user.
- Each run's loop has a global step budget; each continuation costs 1; none left: continuation yields.
- Verdict per kind, from session state:
- Goal: active =
Again, unless the progress check stops it (8). Paused, budget-limited, complete or failed = yield. Cleared = leave. Empty reply never reaches the goal: harness fails it as a model error (harness.md H4.7); goal fails with it. - Autonomous: defers while a goal drives continuations (goal keeps priority). Disabled = leave. Else gates and limits give
AgainorYield. - Reminder: its text goes first in the step's continuation and it leads it (
Lead): leaves with it; step not continuing → stays until a continuation comes. Never makes its own continuation; waits for goal or autonomous run. - Forced tool call: each request forces its tool (
stream.forceTool, S6) until a response calls it; layer leaves at the next final answer. Each forced response without the call = miss. Miss aftertriesretries fails the generation withForcedToolError::NotCalled(S6.3), real request count; that miss removes the layer and appends a display row with the error (eukhe.director, not in model context) in one commit, so the next request is unforced. Run ending before any forced request: layer continues (Again) while budget lasts; continuation only names the forced call, the forced request's soft prompt asks for it. No budget: aborts withForcedToolError::NotCalledand the same row.
- Goal: active =
- Step evaluates from the top until one decides. Autonomous gates run before the step commit. One commit: stack, goal and autonomous changes, goal context row, decision. Rerun of the same generation repeats the decision.
- Run with a new first input = new loop: global budget (
directors.maxSteps, default 200) restored; goal layer gets it; autonomous layer gets remaining continuations (autonomous.maxContinuationsminus used). Goal and autonomous run join the stack (goal above autonomous) when driving continuations without a layer, below pushed layers (reminders, forced calls), so pushed layers decide first.directors.stackCap(default 8) caps pushed layers only; 2 extra places (goal, autonomous) always have room: maxdirectors.stackCap + 2layers, each decision gets that cap. Push past cap: refused with error. No layer: final answer writes nothing. - Reminder and forced-call entry points: session slash commands
/remind <text>,/force <tool> [--tries <n>](TUI sends via daemon); kernel host requestdirectors.push(Python REPLgoal.remind(text),goal.force_tool(tool, tries=1)). Forced call: 1 budget,tries1 by default. - Stack = conversation document
eukhe.directors, rewindable: fork or branch move starts from the stack at its fork entry (rewind drops later Directors, restores removed ones). Resume reads the stored stack. Document is a state tree node (S1) under its conversation.
Model: bend/director.bend. Laws: laws/director.bend (S5). Proofs: proof/director/. Every verdict free: the adversary picks each layer's verdict each step.
| Law | Says |
|---|---|
dir_stack_bounded |
Decision never grows stack past cap (S5.1). |
dir_loop_stack_bounded |
Nor does the loop, over any steps (S5.1). |
dir_loop_ends |
Loop ends (control back or failure) within n + 1 steps, n = global budget, whatever layers say (S5.3). |
dir_end_is_final |
Ended loop stays ended (S5.3). |
dir_encode_sound |
Stored stack is a sound tree (S5.8). |
dir_resume_restores |
Resume reads back the stored stack exactly (S5.8). |
dir_rewind_restores |
Rewinding a sound tree to a stack's tree restores that stack exactly (S5.8). |
dir_go_* (empty, unjudged, defer, leave, lead_stays, lead_leaves, yield, abort, again, again_spent, push, push_full, push_spent) |
One decision per verdict: its effect on stack and outcome (S5.1); together they give every decision exactly. |
dir_room, dir_decide_goes_on |
A push has room when the whole stack holds fewer than cap layers; a loop decision is the one where a leading layer leaves (S5.1). |
dir_tick_goes_on, dir_tick_out_of_steps, dir_tick_hands_back, dir_tick_fails |
One loop step by outcome: goes on with one step fewer, or ends with leading layers staying (S5.1, S5.3). |
dir_lead_leaves_with_the_continuation, dir_lead_stays_when_the_loop_ends, dir_lead_goes_example, dir_lead_stays_example |
A step that goes on = the step with every Lead read as Leave; a step that ends = every Lead read as Defer (S5.4, reminder). |
Code:
eukhe-core/src/durable/directors/stack.rs:Loop::tick(one step, run as the compiled model: entrybend/ffi_director.bend, lawslaws/director.bend; kinds cross as numbers, counts saturate at the wire's largest Nat),Layer,LayerKind,Verdict.directors/mod.rs:DIRECTORS_DOC,DirectorState,DirectorLimits(bound),push_director,DIRECTOR_NOTICE_CUSTOM_TYPE.durable/goals/hooks.rs:on_yieldstep (begin,judge,commit_step); forced tool ofbefore_request; call or miss ofafter_response,mark_forced_response.durable/goals/host.rs(directors.push);durable/session_commands.rs(/remind,/force);skills/goal/src/goal/__init__.py(remind,force_tool).eukhe-durable/src/harness/generation.rs:before_requesthooks may set a request's forced tool (RequestMessages::force_tool).eukhe-core/src/settingsdirectors.
Tie: the step is the model itself (no bridge).
Regression tests:
durable::directors::tests: goal layer until completion, global budget, reminder, reminder waiting for a continuation, forced call, forced call never made + unforced next run, goal on full stack, stack after reopen and in a fork.durable::session_commands::tests:/remind,/forceon the stack;/forcefailing once.durable::goals::tests.
Not modelled (tied by tests): session state → verdict map (S5.4), begin (S5.6), layer kind data (model keeps a number).
S5.8. A goal without progress stops #
-
At each continuation of an active goal the progress check counts the session's distinct acts: tool call + result (by tool name, arguments, result text, error flag), or an accepted progress note. Call ids, timestamps, text and thinking around a call: not work. Compared with the work at the last continuation of the same run and goal:
- Work grew: no-progress streak = 0; goal continues.
- Work did not grow: streak + 1. Under the cap: continues; continuation tells the model the streak and that the goal pauses at the cap. At cap (
directors.stallSteps, default 3;0= default): goal pauses with reasonNo progress: <n> goal continuations in a row made no tool call with a new result, run yields, display row (eukhe.director, not in model context) says why and how to go on. - First continuation of a run or new goal starts the count: new user input or resume restores the full cap.
Default 3: continuation 1 may only state a plan, 2 carries the note asking for one concrete step, 3 shows the model did not act. Without the check, an active goal with no token budget got up to
directors.maxSteps(200) answer-only continuations (e.g. "waiting for the build"), each a full-context request. Reply with no text and no tool call = model error (H4.7); the check counts work, not replies.
Model: bend/progress.bend (work, check, goal, judge). Laws (laws/progress.bend): goal_stops_without_progress (cap-many consecutive continuations with only previously seen acts stop the goal from any count), goal_progress_continues (a continuation with a never-seen act clears the count); laws of judge (the check the Rust loop runs on the ledger's counts): goal_first_check_starts, goal_judge_is_check, goal_judge_counts_work (count grew exactly when the session did new work), goal_judge_held, goal_judge_top_stops (count held within the wire's largest Nat; a count there stops a goal with no new work); goal_entry (section core_w6: the core's entry answers the encoded judge on the request Rust writes). Proofs: proof/progress/.
Rust: eukhe-core/src/durable/children/work.rs (WorkLedger), durable/goals/state.rs (stall_check: judge, run as the compiled Bend core, bend/ffi_progress.bend), durable/goals/hooks.rs (goal_check, work read in on_yield, pause and row in commit_step), durable/directors/mod.rs (DEFAULT_STALL_STEPS).
Tie: durable::goals::hooks::bridge::the_rust_progress_check_equals_the_bend_model runs the ledger over real transcripts and goal_check vs bridge/bridge_progress.bend. Regression: durable::goals::tests::a_goal_without_progress_pauses_at_the_stall_cap.