🇫🇷 Version française
Orkeon Studio
Orkeon Studio is the family of graphical and terminal front-ends over the orkeon CLI workflows: edit and validate the very appsettings.json that orkeon init writes, then pick a crew and launch it by running the co-installed orkeon binary. It is a front-end, not a second product — anything Studio does can be done from the terminal, and anything it writes is readable by the CLI, so you can switch between the two at any point.
The four projects
All four live under src/apps/ and none is published on NuGet (IsPackable=false in every csproj — they ship through the release installers instead).
| Project | Target | Output | Role |
|---|---|---|---|
Orkeon.Studio.Core |
net10.0 |
library | UI-agnostic core: everything the screens render lives here. |
Orkeon.Studio.Config |
net10.0 |
exe (orkeon-studio-config) |
Terminal.Gui v2 settings editor: typed section forms, VFS mount editor, LLM presets, validation, orkeon doctor. |
Orkeon.Studio.Run |
net10.0 |
exe (orkeon-studio-run) |
Terminal.Gui v2 crew launcher: points the co-installed orkeon CLI at a crew and streams its output. |
Orkeon.Studio.Wpf |
net10.0-windows |
WinExe, AssemblyName=Orkeon.Studio (orkeon-studio) |
WPF desktop app combining both workflows in one window. |
Orkeon.Studio.Core — the shared core
A library with no UI and no entry point, consumed only by the three front-ends. Its folders map to the behaviours:
- Configuration/ — a lossless
appsettings.jsonediting model (AppSettingsDocument) plus typed sections (LlmSection,LlmLoggingSection,LoggingSection,MountsSection,RagSection,RateLimitingSection, and since STUDIO-21McpSectionandShellToolsSection),LlmProviderDetector, and the tool catalogue (Tools/ToolCatalog: the tools a run exposes, by family, with what each one needs). - FileSystem/ — mount editing:
MountDefinition,MountRights,MountValidator, directory browsing. - Presets/ & Llm/ — the LLM preset catalogue (
LlmPresets,OrkeonCliDefaults) and endpoint probing (ILlmEndpointProbe/HttpLlmEndpointProbe,LlmApiKeyResolver). - Targets/ — run-target detection (
RunTargetDetector): aconfig.yaml, a multi-file crew directory, or a.ork.tsscript. - Launch/ & Process/ — building the
orkeon runcommand line (RunArgumentsBuilder,RunLaunchOptions), locating the binary (OrkeonBinaryLocator— in order: the--cli-dirargument, next to the executable, theORKEON_CLI_DIRenvironment variable,PATH, then the development checkout), running it and streaming output (OrkeonProcessRunner,IProcessLauncher), interpreting exit codes (OrkeonExitCodes,LaunchOutcomeFormatter), and theorkeon doctorreport (DoctorReport). - Forge/ — the typed client of
orkeon forge --events jsonl(the engine behind the creation wizard): a tolerant line parser pinned against the CLI's golden protocol lines, the session projection every front reads (ForgeSessionModel, milestone mapping, the ✔/✘ checklist rules), the child-process driver with the stdin answer channel (ForgeClient, whose start request carries environment overrides — how Studio's assistant profile reaches the engine), the on-disk session catalogue and the resume hydrator. Studio's process never touches an LLM — it only ever sees JSON lines. - Profiles/ — the named model settings of the v3 design (
ModelProfile,ModelProfileSet,ModelProfileFileStore→studio-model-profiles.jsonnext to the settings file): reusable "model settings", a default election mirrored into theLlmsection, the profile Studio's own assistant runs on, and per-profileORKEON_Llm__*environment overrides for launches (model, endpoint, temperature, timeout and the response budgetMaxTokens— empty leaves the cap to the engine, which sends the model's documented maximum, and the hint under the field says what that is for the chosen model, or that the model is unknown to the catalogue and gets 4096 unless pinned — LLM-10; the thinking switch — provider default / on / off — and the reasoning-effort hint,ORKEON_Llm__Thinking__{Enabled,Effort}, and a timeout pre-filled to 600 s when the picked provider's default model reasons before it answers — Kimi, DeepSeek, Z.AI, MiniMax — LLM-11). The profile editor offers the full provider catalogue (LlmPresets.ProviderCatalogFor— the two local runtimes plus every cloud the framework ships a provider for, endpoint/model pre-filled from the drift-pinned runtime defaults), and a novice pastes the API key right in the editor: it lands in a user environment variable (IApiKeyStore/EnvironmentApiKeyStore, the vendor's conventional name such asDEEPSEEK_API_KEY) — the store file only ever carries the name of that variable (ModelProfile.KeyEnvName), and launches lay the resolved value over the child process asORKEON_Llm__ApiKey. The key itself never enters any file. The store reads its file with case-insensitive property names — it is hand-editable, and"profiles"is what people type — and a file that exists but cannot be parsed is reported on the settings screen instead of loading as an empty set indistinguishable from a first run. - Teams/ — the teams directory (
TeamCatalog, default~/Orkeon/teams): every adopted team is an ordinary folder — listed, duplicated, deleted, imported (with an inline-secret scan, and refused before any copy when the launcher's detector cannot resolve it to a crew definition, with the detector's message) — plus thestudio-team.jsonsidecar recording what the crew definition cannot say (name, need, profile, displayed schedule). The recorded profile is not decorative: launching an adopted team resolves it against the profile store and lays it over the run asORKEON_Llm__*. - Run/ — the typed client of a watched
orkeon run --events jsonl(BUS-06):RunClient, ForgeClient's sibling and deliberately its twin — same launcher, same locator, same envelope parser — andRunProgressModel, which folds the stream into what a screen shows (the task in progress and the tool at work, finished tasks, cost, the question the run is waiting on). The client also carries the seat the run's hub gives a watching process: post to an agent, publish, subscribe, reply — and the Launch screen staffs that seat too: an agent'ssend(markedexpectsReply) shows up as a request panel, and the typed reply goes back down stdin. See The run event bus. - Storage/ & History/ — settings locations and resolution chain (
SettingsLocations,AppSettingsFile), launch history (LaunchHistoryStore). - Validation/ —
AppSettingsValidator+ValidationMessageFormatter. - Localization/ — the
IStudioStringsport (below).
Core's dependency list is deliberately slim: only Orkeon.Domain (mounts, LlmDefaults) and Orkeon.Rag.Abstractions (RagProfilePresets, the closed list of RAG profile names the UIs offer). Core is referenced by three self-contained front-ends, so every transitive dependency is paid three times on disk — heavier references were dropped, and the few duplicated constants are pinned against the originals by drift tests in Orkeon.Studio.Core.Tests.
The front-ends
orkeon-studio-config(TUI) — full-screen editor for the settings file: provider presets, model and endpoint, logging, rate limiting, RAG profile, the VFS mount table, a raw-JSON view, and a diagnostic screen runningorkeon doctor. Rendering only: every behaviour comes from Core.orkeon-studio-run(TUI) — pick a target, set the run options (including--validatefor a dry run), watch the output live, cancel if needed.--versionand--helpare answered headlessly before Terminal.Gui initializes, so both TUIs stay scriptable and CI-checkable.orkeon-studio(WPF, Windows) — one desktop window in the v3 "volets" design: a sidebar in team-lifecycle order — Agent teams (Create a team, My teams, Import), Work (Test, Run, History), Environment (Settings, Diagnostic — the doctor report is copyable as plain text) — under a global Novice/Expert switch. Novice explains every step, shows the contextual help and hides the machinery; Expert shows everything: command lines, raw JSON, the technical journal, the expert-only Test screen. The window opens on a startup screen (Kama, the mascot, click to skip), carries an About overlay, a five-stop guided tour, light/dark themes and a hot five-language switch; it holds a 1024×768 minimum and every control is styled — no native Windows chrome. Novice screens follow the v3 mock closely: Run is a team card (sidecar-backed name and meta line), a plain-language progress card with a tone badge and an "open the result" action, and a technical journal folded by default; History is a card list with per-run duration and a localized outcome sentence; Diagnostic opens on a verdict card fed by a silent first doctor run at startup, with plain-language check names; Settings opens on the per-user file (%APPDATA%\Orkeon\appsettings.json, the oneorkeon initwrites), so the folders it declares, itsLlmsection and its raw JSON are on screen from the first frame; it saves itself on every novice edit (the explicit Validate/Save cycle is the expert's), writing that file back with its other keys intact, and the authorized folders are one card per mount, « Allow a folder » opening the OS folder dialog directly (STUDIO-19); the expert limits tab shows the engine's default in every empty field, as a watermark carried by the theme, and its switches are one-click booleans that remove their key when set back to the default (STUDIO-22). It references onlyOrkeon.Studio.Core. Besides--smoke-exit, it accepts--cli-dir <dir>(names the CLI's directory, beating every other lookup) and--capture-screens <dir>: a headless screenshot campaign — the fidelity-remediation reference against the design mock. It builds the window over a seeded scenario (three adopted teams, seven past runs, four forge sessions, four model profiles, a doctor with one warning and one failure, plus a first-run machine with nothing on it and no CLI), then walks every screen and every gated state of it — the four wizard steps, the five modals, the guided tour, the assistant, empty lists beside populated ones — in both modes and both themes, plus a language sweep over the densest screens. Roughly 250 images over eight passes, one PNG per stop under<lang>/<theme>/<mode>/, the same relative path in every pass so comparing two of them is a directory diff, and amanifest.jsoncarrying each image's reason, its SHA-256 and the reason for every stop that failed. The scenario lives in a throwaway temp directory: the campaign never reads or writes the operator's teams, history, settings or preferences. Exit 0, or 1 with each failed stop named on stderr.
The creation wizard is the doctrine at work: "Create a team" walks Describe ▸ Compose ▸ Try ▸ Adopt over orkeon forge --events jsonl launched as a child process — composing runs with --dry, so the engine generates and validates then pauses at the Composer step; the trial is the user's own « Try the team » click, which resumes the session without dry (a session reopened from "My teams" at that pause lands back on Composer the same way) — the stepper is a projection of the engine's milestones, the per-step "instruction + questions" blocks travel down the ordinary user.message channel, the arbitration buttons are generated from the engine's own decision.needed options, and adoption promotes straight into the teams directory with the engine's real schedule grammar (on demand, daily@HH:mm, hourly), then hands the wizard back at a blank step 1 with one line saying the team is in My teams (STUDIO-20). A capability absent from the stream does not exist on the screen — which is exactly what keeps the terminal orkeon forge and the WPF wizard from drifting apart. The wizard is gated until Studio's assistant has a model profile; the unified Settings screen (AI model tab with the named profiles, authorized folders, the tools tab, and the expert limits, MCP and raw-file tabs) is where that election lives. When the click fails — no orkeon binary on the machine, a configuration the engine refuses, a non-zero exit with or without stderr, a refused promotion at step 4 — a card under the status line says so in the user's language, keeps the engine's own text raw (wrapped, bounded, scrollable, never translated) and offers « Copy the report » (command line, exit code, engine error, the whole stderr and journal), « Try again » and, by family, the diagnostic or the settings; it shows in both modes, clears on the next composition, and « Stop » never produces one.
The Launch screen is no longer a terminal
It used to be a twenty-thousand-line list: honest, and a terminal with a theme. The person launching a crew from Studio wants two things scrollback does not give — is it advancing, and is it waiting on me — so the screen now watches the run through the same protocol the creation wizard uses (--events jsonl, on by default; unchecking the option gives the plain argv back).
What it shows: finished tasks with their agent, duration and tokens; a cost line; and the run's question, asked on screen. Before this, a task declared humanInput: true was auto-approved behind the user's back — a defensible fallback for an unattended run, and the wrong answer entirely once a screen is watching.
The raw log is demoted, not removed. A line the panel cannot read falls through to it rather than into nothing, which is the rule the terminal launcher already followed.
Three refusals keep the panel honest, and each is pinned by a test. A silent run says "nothing reported yet" rather than implying progress. A question arriving without a correlation id is not shown as pending, because answering needs an address. And an answer that could not be written leaves the question open instead of pretending it landed.
The first version showed only what had finished. During a seven-minute task the card held a still "running" badge and the rows of the tasks already done, and nothing told a working run from a stalled one — the owner's own words: "on ne sait pas si le processus est en cours". The engine now announces each task as it starts (task.started, every mode), so the card carries a row for what is running now — its agent, a turning glyph, "since HH:mm:ss" from the run's own clock — and, under it, the tool at work, straight from tool.called / tool.returned. The badge pulses while the child process lives and stops the moment it exits. Each finished row also says how many tools the task called: a task meant to write a file and reporting zero is the diagnosis a green run hides. Two smaller things the same review asked for: every journal line leads with the time Studio read it, on screen and in the copied text; and every launch starts from a clean screen — the previous run's journal and verdict go, so nothing on screen can be taken for the run about to start. "Copy" is how a journal survives the next click; "validate first" keeps its dry run and its real run in one journal, because the clean start is per click, not per pass.
Team folders end to end (remediation v2)
An adopted team's folders are part of the team: the sidecar studio-team.json
records them as mount strings (mounts), next to the display name, profile and
schedule. The "My teams" cards show them as chips; « Change the folders »
edits them in the team-mounts modal. A team associates a folder the settings
declare: « Allow another folder… » on an adopted team, and the wizard's « This
team's folders » block under the « Later » policy, open the « Add an allowed
folder » chooser, a checkbox list of the folders held in « Settings › Authorized
folders » (Orkeon:FileSystem:Mounts). The picked entries are carried over
verbatim, rights included: the settings are the single place a folder and its
rights are decided, and a team that could widen them would make that declaration
a suggestion. A row the team already carries, or whose virtual root another
folder already spends, says so and cannot be picked — a team's own list, like the
settings', names each root once. The one gesture that declares from the wizard is
the disk pick of « Existing folders » below, and it declares in the settings on
the way — the settings stay the source of the rights.
A team is a folder one carries. The sidecar records the team's own folders
relative to it: a physical segment that starts with ./ — ./input:/workspace:ro,
./output:/output:rw, ./rapports:/rapports:rw — names a folder inside the team
folder; one segment, / on both OSes, never .., never quoted. An absolute entry
is a folder of the user's, outside the team, and stays what it is; an entry nobody
can parse passes through untouched, both ways. The runtime resolves a relative
physical path against the process cwd and nothing else, so TeamCatalog is the
only place that knows the convention (TeamMountPaths in Core is its one helper):
it resolves relative entries to absolute paths when it describes a team
(Describe, DescribeTarget, hence List — the raw strings stay on
Metadata.Mounts), and relativizes on the way in (SaveMetadata, SaveMounts),
creating each relative folder there — the single point where an in-team folder is
materialised, at adoption and at every later edit alike. The cards, the launcher
and the folders modal receive absolute paths as before and never learn it. A
duplicate, an export and an import copy the entries verbatim and resolve them under
the copy; an older sidecar that recorded absolute paths under its own folder is
rewritten relative on the way — there is no rebase step any more, because a copy is
a safeguard, not a compatibility layer. DeclaredMounts.IsInsideTeam vouches for a
./ entry even before the team folder exists (relative is inside the team, by
construction), and MountValidator skips the existence check of such an entry until
it is told which team folder to look under.
Where the folders live is asked at step 1. A fourth question, « Where are
your folders? », answers the two roots a team can address before it has a
blueprint — /workspace read (« Your documents ») and /output written (« The
results »), the only ones DeriveMounts can produce without one — and composing
never waits for it (FolderPolicy, Later by default). « Existing folders »
shows the two rows; « Choose the folder… » on either opens the disk picker on
the row's rights (read-only for the documents, read-and-write for the results),
and the pick is declared in « Settings › Authorized folders » unless the settings
already hold that folder, saved, then bound behind the row under the row's
rights — one gesture, the status line says which of the two things happened, and
a refused save still binds (MainWindowViewModel.DeclareAndBindAsync). A folder
picked inside the reopened team itself is bound and never declared: it is the
team's own, and the save relativizes it. « Created inside the team » answers both
rows team-relative on the spot — ./input:/workspace:ro, ./output:/output:rw,
read « inside the team: input / output » — and nothing is created on disk before
the adoption. « Later » behaves as before: no rows, the Composer step asks. The
chip moves the two canonical roots and nothing else; composing keeps them
(KeepOnlyStepOneMounts — the rest belonged to the blueprint being replaced),
« Restart », a resume and « Modify » on a card forget them and the policy with
them, so a stale step-1 choice never leaks into the sidecar of another team.
The two rows are a start, not a limit (owner review of 2026-09-19): « Add the
folder » names as many further mount points as the need calls for — a name the
agents will use (/factures, /archives), read or written — and each becomes a
row like the canonical ones, answered the same two ways (« Created inside the
team » answers a new one on the spot), listed on the Composer step next to the
blueprint's own roots, kept across compositions, and created inside the team at
adoption when left unanswered. Which of them the agents address is the
blueprint's business: the need has to name them. A name is normalized the way the
picker derives one from a folder (lowercased, one segment), and a name the runner
reserves or a row already carries cannot be added.
The wizard's block on the Composer step is one line per mount point (lot 3):
the name the agents address, who addresses it — provenance, never permission —
and the folder behind it, or two buttons when there is none yet. « Choose the
folder… » opens the chooser targeted at that virtual path (the disk picker
under « Existing folders »): the picked entry keeps its folder and its rights,
and only the name the agents use for it is the team's to choose. A targeted open
takes ONE folder and judges its rows where the pick will LAND, not on the root
the settings happened to declare — otherwise every folder the team already uses
elsewhere would refuse itself. « Create inside the team » is the other answer,
per row, and « Create every folder inside the team » answers every unanswered
row at once; under the « Created inside the team » policy a root a later
blueprint adds — /rapports beside /output, which step 1 could not foresee —
is answered the same way as it appears (AnswerNewDerivedRoots), unless the
user dropped it. A row inside the team shows the label, never a disk path, and
never reads red: DeclaredMounts.IsVouchedFor — declared in the settings,
or the team's own — is the rule of the wizard, of the "My teams" cards and of
the launcher alike. Without those gestures an agent-implied root could only ever
be answered at adoption, by a folder created inside the team and left empty:
which is why the card also says, while there is still time, that a reading team
with no folder chosen — or one answered « inside the team », whose input/ is
created just as empty — will be given its own empty input/ and that nothing
copies documents into it.
The trial reads where the documents are. orkeon forge … --read <dir>
mounts <dir> as /workspace, read-only, in place of the working directory and
changes nothing else — the session stays under the forge home, the settings still
resolve next to it. The wizard passes the folder bound behind /workspace on
every engine invocation (ForgeStartRequest.ReadDirectory, the seven sites; the
promotion keeps the workspace alone): a real folder as it is, a team-relative
answer resolved under the team folder once there is one — a reopened team
retries on its own input/, filled since — and nothing before the team exists,
where the argv is exactly what it was before the option and step 3 says the trial
runs on an empty folder. An engine that predates --read only ever meets it when
a folder is known, and then refuses it out loud on the failure card.
A team's own folders are vouched for by living inside the team, and never written
to the global settings. Two allow-lists would be one too many: declaring a team's
output/ in Orkeon:FileSystem:Mounts would duplicate the sidecar in a file shared by
every team, and put one team's private folders in the list every other team picks
from. So the rule is implicit — DeclaredMounts.IsVouchedFor: declared in the
settings, or inside the team — and it is the one the launcher answers
(BlockingFolders never counts a team's own /output or /workspace). The settings
screen still has to show these folders, or the one screen that claims to list what
the agents can see would be missing the folders every adopted team writes into. So
« Settings › Authorized folders » ends, in both modes, with a read-only « Team
folders » section (TeamFoldersViewModel): one line per in-team folder of each
adopted team — « Veille concurrentielle · /output → output » with the one-word rights
badge — read from the sidecars' raw entries, the ./ ones and the absolute
under-the-team spelling of an older sidecar alike, never a folder outside the team,
never a disk path. No command, no ✕, nothing written: its hint says these folders
belong to their teams and are changed from « My teams » › « Change the folders ».
The section follows the team list — an adoption, an import, a deletion or a
duplication from a card, a save of the folders modal all end in a rebuild of the
cards, and it re-reads the sidecars on that signal and again when the folders tab
opens — so it never shows a team that is gone, and never misses one adopted a minute
ago.
Declaring is the settings' own gesture, and the chooser's « Declare a new
folder… » is one door to it: it closes and lands on « Settings › Authorized
folders », on that tab and not merely on that screen. One door, so a folder
cannot be declared from two places and drift between them. There is no in-app
picker any more (STUDIO-19): « Allow a folder » on the settings' novice card
opens the OS folder dialog, and the pick lands read-only under a virtual name
derived from the folder's own name (MountDefinition.SuggestVirtualPath, the
first free suggestion when that name is taken or unusable) — the card's rights
badge flips it to read-and-write. The wizard's « Existing folders » rows open
the same dialog on the row's rights and declare the pick under the row's root,
with an id of its own, then bind that very entry (VFS-90, D-01) — a second entry on
a root another folder already claims, told apart by its id, never a rename to
/docs; a writable extra folder at the Composer step goes through a named root.
A team folder that nothing vouches for — neither declared in the settings nor the
team's own — reads red, on the wizard's rows, the "My teams" cards and the
team-mounts modal alike. A team's ./input and ./output are the team's own:
created inside it at adoption, never declared, never red. Red is the one thing a
row cannot say by naming a virtual path, and a team reaching outside the machine's
authorized folders should not have to be discovered by reading a sidecar. Declaring
a folder in the settings un-reds it at once: the cards and the Run screen recompute
their verdicts on every edit of the declared list, and the window reads that list
from the per-user file before it builds them (STUDIO-18).
A team reaching outside the settings does not launch. « Run » refuses a team
carrying a folder that no settings entry allows: the run button is disabled and
the card names the folders and the two ways out (declare them, or take them off
the team), with a button onto « Settings › Authorized folders ». Discovering
that refusal from a run that failed halfway, its reason buried in a log, is the
outcome this replaces. The rule lives in Orkeon.Studio.Core
(DeclaredMounts.BlockingFolders) rather than in the WPF screens, so the TUI
launcher cannot answer it differently — and a team's own /output and /input
never block it: they are the team's plumbing, and counting them would make every
adopted team unlaunchable.
The folders the blueprint implies are removable like any other. They used to be
informative chips with no ✕ — "edit an agent to change them" — which left a team
carrying a root its owner did not want with no way to say so. Dropping one now
sticks: SidecarMounts no longer re-adds it, the same silent undo that method
exists to prevent. The screen warns and names the dropped roots, because nothing
will be bound to them and the agents writing there will fail; a single
« Restore » is the way back from a wrong ✕. What SidecarMounts records is
team-relative for every in-team answer and every root the blueprint addresses
that nothing answered (./output:/output:rw, ./input:/workspace:ro); the save
creates the folders. At launch, the catalog reads the sidecar against the settings
(TeamMountResolution.Resolve) and LaunchMountPlan.For says what reaches the
command line: a settings declaration the team names goes as --mount-id <ulid> —
no path, the machine's own entry as it stands today; the team's own folders and
any copy the settings do not hold as recorded go as --mount, ahead of the
per-launch ones, so the chips and the command cannot disagree; an id this machine
does not declare blocks the launch (below). The --allow-external-mounts flag
follows the sidecar too: a team folder outside the team turns it on, a team whose
folders all resolve under it — the launch's working directory — needs none, and
the expert checkbox stays for the per-launch mounts. The engine places every
--mount by virtual root (RunnerHost): a team folder under a name the
settings spend on another folder replaces every settings entry of that root for
the run, and a folder under a new name is appended after the declared ones. The
Run screen's effective-mounts table predicts exactly that — one row per settings
entry, origin appsettings for what the settings provide, --mount (replaces «…»)
for a replacement, selected by id among N / not mounted for this run for the
entries of a shared root — and the sentence above it states the rule
(MountOverrideSemantics). Before this, the chooser's verbatim copy met its own
twin from the settings and every adopted team that used an allowed folder failed
at kickoff on "Duplicate virtual paths"; the settings' declared folders are also
whitelisted for PathValidator without any flag, so a team reading an allowed
folder outside its own directory is no longer refused file by file. Deliberate
limit: a bare orkeon run in a terminal does not read the sidecar — the crew's
own mounts: block is what it reads (VFS-90); like the profile field, the
sidecar is Studio's comfort, not the engine's contract.
A mount has an identity (VFS-90)
Every entry of « Settings › Authorized folders » carries an id — the
26-character ULID before the | of its mount string,
01J9Z3K4M5N6P7Q8R9S0T1V2W3|C:\data\out:/output:rw — assigned by the editor at
load to an entry that has none and written at the next save, shown on the row
with a one-click copy for a hand-written crew's mounts: block. The id is what a
team's sidecar names its declarations by, and it is why two entries may now
declare one root: the owner's machine keeps experiment 09's …\09\output:/output:rw
and experiment 10's …\10\output:/output:rw side by side, and each team names
its own. The consequences, screen by screen:
- The entry is authoritative (D-01). The chooser opened for one mount point
offers only the entries declared under that root — an entry declared as
/docsreads « declared as /docs — this mount point is /output » and cannot be picked — and records the pick verbatim, id included. The disk pick declares the folder under the row's root (a second/outputentry when one exists) and binds that entry; an equal entry — same folder, root and rights — is reused, never declared twice. Renaming to/docsto dodge a taken root is gone with it. - The sidecar keeps a copy (D-02):
<ulid>|<folder>:/root:rights, portable, the id winning over the copy on the machine that has it. A team's own./outputcarries no id (D-07). A sidecar written before ids resolves its copies by folder, root and rights, and « Change the folders » upgrades an exact copy to the entry itself on save. - The ids travel with the team (D-06). « Duplicate », « Export » and « Import » copy them verbatim. A team naming a declaration this machine does not have — an import, or an entry removed since — reads « declaration … is missing on this machine » on its rows and in the folders tab, and « Run » refuses it naming the ids; the import review offers « Authorize them as recorded », which declares the copies under the same ids (a new id only when this machine already spends it on something else).
- The settings say who depends on an entry: each row reads « Used by … » from
the sidecars, and removing an entry a team names asks first, naming the teams —
they stop starting the moment it is gone.
MountValidatorfiles two entries on one root as information (STUDIO-MOUNT-SHARED), a shared root with an entry that has no id as the error the engine would raise, and one id on two entries asSTUDIO-MOUNT-ID. - Agents never see an id:
list_mounts, the prompt's mount table and the access-denied messages name virtual paths only.
A team folder may hold a single-file crew. Every examples/ crew — and every
crew written by hand — is one config.yaml or crew.yaml carrying agents: and
tasks: inline, not the promoted crew/config.yaml + crew/agents/*.yaml +
crew/tasks/*.yaml layout forge promote writes. The target detector
(RunTargetDetector) resolves a folder holding no layout marker and no script but
one *.yaml/*.yml file as that file: the run path is the file, the selected path
stays the folder, so the sidecar beside it and the launch directory keep working
exactly as for a promoted team, at the folder's root or under its crew/ nesting
alike. Several YAML files resolve to crew.yaml, then config.yaml, and are
otherwise offered as candidates the way scripts are. This is what makes the whole
examples catalogue importable: « Import » runs that detector on the source before
copying anything, and a folder it cannot resolve is refused with the detector's
message in the status line rather than landing in « My teams » as a card nothing
can run.
« Open the folder », at every step, in both modes. The wizard's header offers
it as soon as the engine answered: the working session — which holds the generated
crew/ — before the adoption, the adopted (or reopened) team afterwards, and the
tooltip says which. Same IShellOpener port as the team cards, gated the same way:
no opener wired, no button.
The blueprint, edited by hand
The forge protocol's edit arbitration is real: interactive mode arbitrates
every verdict (a conforming one costs one "accept" click), and
decision.made {edit} followed by blueprint.edited {blueprint} re-renders the
crew deterministically — zero LLM tokens — then re-earns its verdict through the
unchanged validate/test/diagnose path; the engine re-validates everything it
receives (parse, compile, tool catalogue) and answers an invalid edit with a
recoverable FORGE-BLUEPRINT-INVALID, reopening the arbitration. In Studio, the
Composer step shows one card per blueprint agent with « Edit » opening the
agent editor — the name maps to the blueprint's role, "what it does" to its
goal, the capability chips to its tools; « Remove from the team » and
« Add an agent » travel the same path. The buttons are actionable at the
engine's two edit points: while it waits at its arbitration (the live channel —
decision edit, then the amended blueprint), and at the Composer step's dry
pause, where the engine is off — there the apply is a forge resume --edit --dry
child run carrying the amended blueprint as its first stdin line: the engine
validates it in full, re-renders deterministically (zero LLM tokens, same
iteration) and pauses again at the same boundary, so the Composer repaints with
the amended team. While the assistant composes or a trial runs, the buttons wait
with the engine.
The same dry pause carries a second answer beside « Try the team »:
« Adopt without trying » (forge resume --adopt), which moves the session
straight to Ready — offline, no run directory, zero tokens. Ready used to have
exactly one predecessor, an accepted verdict, so keeping the team as generated
required sitting through an execution that nothing downstream consumed:
verdict.json is optional at promotion and the generated FORGE.md already
knows how to say « no verdict recorded ». What the trial buys is evidence, not
permission, so the button's tooltip says exactly that, and the transition gets
its own trigger (TrialSkipped) so the session history never reads as a verdict
that was never earned.
Which engine answered is on screen too, beside the assistant's name (« engine
1.0.0-rc.2 », from session.started). Studio does not embed the CLI — it
launches whichever orkeon its locator finds first, co-installed, on PATH or
built from the checkout — so without that line a session driven by a stale binary
is indistinguishable from a working one that happens to have nothing to report.
What a run costs, on screen (remediation v3)
Wherever a trial or a run finishes, Studio shows what it cost — as mono chips in
one shared recipe (UsageMetricsFormatter): total tokens, the prompt-cache hit
(cache 62 % · 7 980 tokens — the hit/miss pair is a partition of the prompt
tokens, never an addition), and the wall time. The wizard's verdict card reads
them off the enriched verdict.ready (the last trial's own figures, distinct
from the session-cumulative cost.updated meter); the launcher's finish line
reads the enriched run.finished; the history records tokens and the cache pair
per entry (tolerant schema — old files load) and shows them in the meta line.
A metric the providers did not measure produces no chip — never a zero.
Modify, re-try, re-adopt (remediation v3)
Adoption is no longer a one-way door. « Modify » on a team card — resolved by
the reverse lookup from the team folder to the forge session that promoted it
(promotedTo) — reopens the wizard at the Composer step with the whole stepper
reachable: the engine resumes the promoted session into a reopened arbitration
(the stored verdict is re-announced first), so agents are editable again, a new
retry decision re-runs the trial as-is (zero compose tokens, one budget
iteration), and re-adoption updates the same team folder — generated files
(crew/, launchers, FORGE.md, schedule/) are regenerated, the sidecar and
the user's own files survive, and renaming the team only changes its display
name. After an adoption the wizard is a blank step 1 again (STUDIO-20): modifying
an adopted team goes through « Modify » on its card, and the reopened arbitration
offers retry. A team no session points at — imported, or whose session was
deleted — is modifiable too (FORGE-09): the card's « Modify » runs the engine's
forge reopen <team-folder>, which rebuilds a session from the team's own crew/
(brief from the promotion's forge.json, derived from the plan otherwise) and parks it
at the dry pause; the wizard reads the session off session.started /
team.reopened and opens the Composer without an engine, as after --dry — amend
an agent, try the team, or keep it as it is, then re-adopt onto the same folder.
TeamSummary.HasYamlCrew is the gate: only a team with no YAML crew under crew/
(a script crew, a foreign layout) keeps « Modify » disabled, the tooltip saying why;
the tooltip also says when the reopen goes through a rebuilt session. Discarding a
session under « Sessions in progress » while the wizard is open on it ends that
creation as well: the wizard goes back to the blank step 1 of « Restart » (a running
engine is stopped first) rather than keep a Composer over a directory that no longer
exists; a session it is not open on leaves it untouched. « Modify » brings the wizard
forward on the click itself, before any session exists — with none pointing at the team
the engine rebuilds one first, and the screen used to move only once it had (the owner's
« two clicks », 2026-09-21); the rebuild shows as the engine working. A click on « Modify »
or « Resume » while the engine is busy on another creation is refused in words on the
wizard's status line, nothing stopped, instead of being dropped. A forge run's task
completes only once its epilogue has landed on the UI thread, and with it every event
posted before: WPF resumes an await begun in an input handler at Send priority, above the
Normal priority the reader thread's posts travel at, and the rebuild used to read the
session off a model the events had not reached yet — step 1, with the session on disk for
the second click to find. The disk is the fallback when the stream announces nothing, and
a card says so when neither has it.
Tools and MCP in the settings (STUDIO-21)
Two tabs the settings screen lacked. Tools, open to both modes, is three cards. The
tool keys: one row per key a tool needs — the Tavily key of web_search
(ORKEON_TAVILY_API_KEY, the spelling the secret chain reads) and the Brave key of
brave_search (BRAVE_API_KEY, read as-is by the runner host) — on the same rows and
the same store as the API keys of the model tab (SecretRowViewModel, IApiKeyStore: the
value goes to the user environment, never to a file, and the row says where a key is
issued). The catalogue: every tool orkeon run registers, by family, each as a chip, and
under each family one line per tool that needs something — a key remembered above, a
tool present only once its key is (brave_search), a key given at the call by the agent
(image_generation), connection parameters given at the call (the database and graph
tools), an expert setting below (shell_command). The catalogue is declared in Core
(ToolCatalog): the framework carries no "required settings" metadata and its registry
lists names only, so the list is the orkeon run column of the availability matrix in
docs/tools/inventory.md, and a test pins every name against that file. The expert card:
the shell_command allow-list (Orkeon:Tools:Shell), the interpreters switch and the two
command lists, one command per line — never written as an empty array, which the runtime
would read as "block every command".
MCP, expert only, is the MCP section: the switch, and one card per server under
MCP:Servers — identifier, transport (Stdio or Sse, the runtime's spelling), command
and arguments and environment for a stdio server, URL for an HTTP one. Every keystroke
writes in place through McpSection, so a key Studio does not model survives an edit of
the server that carries it, and a rename moves the whole node. Each row says its own
problem the way the validator will refuse the save (STUDIO-MCP-*): an identifier the
binder would mangle, a stdio server without a command, an HTTP server without an absolute
http(s) URL; a value of the environment block that reads as a secret is reported at
information level, since the file is clear text and the server inherits the user
environment. The section is honoured by the runner: orkeon run connects the declared
servers before the crew loads (see MCP integration), which is what makes the
tab worth having — until then nothing read it.
Localization: the IStudioStrings port
Orkeon.Studio.Core defines a localization port, IStudioStrings (Localization/StudioStrings.cs): a key indexer plus a CultureChanged event so ViewModels can re-emit their bindings when the language switches. The English defaults in EnglishStudioStrings are the key registry of record. Each front decides the language: the WPF app bridges the port onto its resx-backed I18n service (I18nStudioStrings, Strings.resx plus the fr, es, de and zh-Hans satellites) with a hot five-language switch relayed via CultureChanged, driven by LanguageSelectorViewModel — the system language is detected and deliberately never persisted, only an explicit pick is recorded; the TUIs keep the English default. Deliberately untranslated, by CLI-contract policy: orkeon doctor check details, LLM probe results, exit-code descriptions and the VALIDATION OK/FAILED verdicts — translating Studio's copy would desynchronize it from what the CLI prints in a terminal. Validator messages and doctor check names follow a revised split: the raw English line stays as the expert detail (tooltip or mono side label), and a per-code plain-language overlay is what the lists show first — Studio.Diagnostics.Code.<code> for validator messages, Studio.Diagnostics.Check.<name> for doctor checks, both under the single Studio.<Screen>.<Label> key convention the parity tests enforce.
Where Studio keeps things
Two roots, one rule: application state lives in the per-user config directory, documents live in the user profile. Nothing is ever generated into the working directory, and API keys live in neither — they stay in the user's environment variables, never in a file.
%APPDATA%\Orkeon\ ($XDG_CONFIG_HOME/Orkeon/ elsewhere) — application state:
| Entry | What it is |
|---|---|
appsettings.json |
the per-user global settings — the durable base every launch composes on |
studio-model-profiles.json |
the named model profiles (provider, model, URL, temperature, response budget, timeout, thinking switch and effort, key env-var name only) |
studio-history.json |
the launch history the History screen and the team cards read |
Studio\ui-preferences.json |
window comfort: mode, language, theme |
.orkeon\forge\<slug>\ |
the atelier sessions — resumable works-in-progress (brief, blueprint, provisional crew/ render, trial runs/), not adopted crews. The dot-name is the engine's workspace-state convention (SPEC §4.1, like .git): Studio hands %APPDATA%\Orkeon to the engine as its forge workspace, so forge resume <slug> works identically from a terminal and from Studio |
%USERPROFILE%\Orkeon\teams\<slug>\ — documents: the adopted teams. Each is an
ordinary, self-contained folder (crew definition, run.cmd/run.sh, the
studio-team.json sidecar with name, need, profile, schedule and mounts) — copiable,
shareable, deletable, runnable with orkeon run <folder> alone. Adoption moves a
session's result from the state root to the documents root; that is the boundary
between a draft and a deliverable. In the sidecar, name is normalized at write
(one line, Markdown stripped, cut at a word under 64 characters — the slug's cap;
import applies the same rule to the copy it makes) while description is the whole
need, untouched: the one-paragraph summary the cards show is derived at read time
(TeamCatalog.Summarize) and never stored.
How Studio ships
Studio is installed next to the CLI by the release packages — see the publication matrix for the exact artifacts and Three ways to run Orkeon for the walkthrough:
- Windows — the
orkeon-cli-<version>-win-x64.zipand the per-user MSI carry the WPF desktop app (orkeon-studio, self-contained); the MSI also registers an "Orkeon Studio" Start-menu shortcut. - Debian/Ubuntu — the
.deband the Linux multi-app archives carry the two terminal apps (orkeon-studio-config,orkeon-studio-run), self-contained. - macOS — CLI-only on the onboarding channel in V1; the multi-app
osx-*archives do carry the two TUIs (only the WPF app has a RID filter), untested on macOS in V1.
The two TUIs target plain net10.0 and are therefore cross-platform builds; only Orkeon.Studio.Wpf is Windows-bound (net10.0-windows, WPF).
What Studio is not
- Not a NuGet package — all four projects set
IsPackable=false; the only distribution channel is the release installers. - Not a separate engine — Studio never re-implements a workflow: it edits the CLI's settings file and spawns the CLI itself (
OrkeonProcessRunner), so its results are exactlyorkeon run's.
See also: Publication matrix · Three ways to run Orkeon · Back to index