diff --git a/AGENTS.md b/AGENTS.md index f280854e3..1e71531ad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ For tests: - `uv run pytest` runs unit and integration, which is what CI runs. - `tests/system/` is OPTIONAL: real agents on real tasks, 5–30 minutes and real tokens per test, and in `tests/system/web/` the web page driven in real browsers (Playwright), never in CI. Unit and integration tests MUST NOT drive a browser. Do NOT run it on every change; run only the file that covers an end-to-end path you changed, e.g. `uv run pytest tests/system/test_flows_ralph.py`. -For `specs/*.md`: +For `specs/**/*.md`: - MUST strictly adhere to specs at `specs/`. - MUST NOT modify any SPEC UNLESS explicitly instructed to do so. diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 4f5013edf..c5aa5e278 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -304,7 +304,7 @@ export default defineConfig({ collapsed: false, items: [ { text: 'Remote execution', link: '/user/remote-execution' }, - { text: 'Where the agent runs (affinity)', link: '/user/remote-execution#where-the-agent-runs' }, + { text: 'Where the agent runs (harness setting)', link: '/user/remote-execution#where-the-agent-runs' }, { text: 'Containers', link: '/user/containers' }, ], }, diff --git a/docs/.vitepress/theme/components/HmzAnchor.vue b/docs/.vitepress/theme/components/HmzAnchor.vue index 13e3885d4..47ecb1036 100644 --- a/docs/.vitepress/theme/components/HmzAnchor.vue +++ b/docs/.vitepress/theme/components/HmzAnchor.vue @@ -1,6 +1,6 @@ @@ -84,15 +78,15 @@ const reading = [ # Many conversations at once When a flow drives several agents, each conversation they hold gets a transcript of its own, -each [outworlder](/user/questions) gets one of what it asks you, and there is one more where -all of it appears together. The screen opens on that one, and goes back to it when a flow -starts. Press shift+tab to read one at a time. +and each [outworlder](/user/questions) gets one of what it asks you. No transcript holds every +agent's lines at once. A run lands on the agent of the first conversation it opens; press +shift+tab to read the others one at a time, and ← for [the +monitor](/user/monitor), where the whole run is watched. ::: info At a glance -- **You will** switch between the transcript of every agent and the transcript of one - conversation, and read the marks that say who is working and who has news. -- **Use it when** a flow drives two or more agents, or one agent opens many conversations, and - the combined transcript is hard to follow. +- **You will** switch between the transcripts of the agents and conversations of a run, and + read the marks that say who is working and who has news. +- **Use it when** a flow drives two or more agents, or one agent opens many conversations. - **You need** a flow running in `hmz`. A flow with two agents, such as [`rlar`](https://humanfia.ai/flows/rlar), shows it best. ::: @@ -109,15 +103,16 @@ decides how many each agent holds: a reviewer that starts fresh every round open each time, and a fan-out holds several at once. Every conversation gets a transcript of its own, and so does each agent, with all of its conversations running down it. -On top of those is the transcript of **every agent**: everything anybody says, in the order it -happens. It is where the screen opens and where a run is usually watched from. +When a run starts, the screen lands on the agent of the first conversation it opens, with what +you started it on at the top, and follows that agent into its later conversations. Before any +conversation opens, the screen shows only the interface's own lines. What you are reading decides more than what is on the screen: - **The transcript**, drawn from the top with a line saying whose it is. - **Where a line you type goes.** See [Talking to a running turn](/user/steering). - **Who answers [`/btw`](/user/btw)**: a side copy of that conversation, or the btw agent - while you read every agent. + on the monitor. - **What `/clear` clears**: only that transcript. ## Example: follow a builder and a tester @@ -129,28 +124,26 @@ transcripts, as shift+tab does: What to look at, by number: -1. **`── builder`.** On the transcript of every agent, a line with the agent's name marks each - place the speaker changes. Without it, two agents' lines would read as one voice. +1. **`─ reading builder ─`.** You are on the builder's transcript, where the run landed, + drawn from the top so it reads as a whole. 2. **One line per agent above the editor.** The name the flow gives it, what it runs as `cli/model:effort`, and `●` while it has a turn open. These stay whatever you read. -3. **`─ reading builder · conversation 1 ─`.** You are on one conversation's own transcript, - drawn from the top, so it reads as a whole. -4. **`· reading`** marks the agent whose transcript is on the screen. -5. **`· unread`** marks an agent that has said something since you last read it: the tester + `· reading` marks the agent whose transcript is on the screen. +3. **`· unread`** marks an agent that has said something since you last read it: the tester has been working while you read the builder. +4. **`─ reading tester · conversation 1 ─`.** One conversation's own transcript. ### Check it worked - The line at the top of the transcript names the conversation you meant to read. - The agent you are reading carries `· reading` above the editor. -- A line you type now goes to that conversation. On every agent's transcript it goes to - whichever turn is open. +- A line you type now goes to that conversation, or to the open turn of the agent you read. ## The keys | Key | Reads | | --- | --- | -| shift+tab | The next conversation that is running, then each outworlder of the flow, then round to every agent again. | +| shift+tab | The next conversation that is running, then each outworlder of the flow, then round again. | | tab | The one before. | | ←, then enter on a node | Any agent or conversation, working or not, picked on [the monitor](/user/monitor). | @@ -174,8 +167,6 @@ then what it is holding. | `reading` | Its transcript is the one on the screen. | | `unread` | It has said something since you last read it. | -Nothing is marked `unread` while you read every agent, since everything is on that screen. - ## One agent, many conversations An agent can hold many conversations. A Ralph loop opens a fresh one every round, and a @@ -206,14 +197,14 @@ or not. ### The transcript I was reading stopped moving You are still on that conversation, and its turn has ended. A Ralph loop's next round is a new -conversation. Press shift+tab to go on to the one that is running, or go back to -every agent. +conversation. Press shift+tab to go on to the one that is running, or read the +agent itself from the monitor, which follows it into each new conversation. ### A line I typed is still on the pin A line typed on one conversation's transcript waits for that conversation's next turn, which a -loop that starts fresh every round never takes. Go to every agent's transcript and type it -again. See [Talking to a running turn](/user/steering#my-line-stays-on-the-pin-and-never-goes). +loop that starts fresh every round never takes. Read the agent itself from the monitor, +which says to whichever of its conversations has a turn open, and type it again. See [Talking to a running turn](/user/steering#my-line-stays-on-the-pin-and-never-goes). ## Next steps diff --git a/docs/user/export.md b/docs/user/export.md index 8a7bd6254..b7439c3f1 100644 --- a/docs/user/export.md +++ b/docs/user/export.md @@ -11,8 +11,9 @@ timeline of the run and what each CLI was, with every credential struck out. ## Try it -Open `/epics`, press enter on the run, choose **export run**, and type where the -archive goes. +Open `/epics`, press enter on the run, choose **export epic**, and type where the +archive goes -- or press **done** as it stands, and it goes to `~/Downloads/` (your home, where +there is no `Downloads`), as a browser's export would. @@ -56,13 +57,13 @@ budget. Open the run in `/epics`: It stopped with 1 agent in 3 sessions. ③ ╭──────────────────────────────────────────────────────────────────────────╮ - │ export run the entire run as an archive, with its trace │ + │ export epic the entire epic as an archive, with its trace │ ╰──────────────────────────────────────────────────────────────────────────╯ enter choose esc back ``` -Choose **export run**. A form asks where the archive goes: a file, or a directory to put it +Choose **export epic**. A form asks where the archive goes: a file, or a directory to put it in under the run's name (made if it is not there yet). The path completes as you type, the way a shell completes one: what it could become is listed under the form, tab finishes as much as those share and then steps through them, and `~` is your home. Here `iss`, @@ -101,7 +102,8 @@ A moment later the list of runs is back, with a line under it: ├── epic.jsonl what happened, a line at a time ├── epic.<flow>_<id>.jsonl the same, for each flow it called ├── resume.jsonl for a flow that can be picked up -├── profile.jsonl for a profiled run +├── profile.jsonl for a run whose environment was profiled +├── flow-profile.jsonl for a run whose flow was profiled ├── traces/ │ └── export.trace.json the whole run as one timeline └── sessions/ @@ -110,7 +112,7 @@ A moment later the list of runs is back, with a line under it: A file the run never wrote is left out: `resume.jsonl` only for a flow that -[can be picked up](/user/resuming), `profile.jsonl` only for a +[can be picked up](/user/resuming), `profile.jsonl` and `flow-profile.jsonl` only for a [profiled](/user/tracing#profiling-a-run) run. **`manifest.json`** is what lets somebody else read the rest: @@ -259,8 +261,9 @@ runs.bundled(epic, output="/tmp/for-the-issue.hmzepic") ### Importing a run Somebody sent you a `.hmzepic`? Read it back in, and it is a run like any other. In `hmz`, -open `/epics`, press `i` (or **Import…**) and give the file; the list shows it with the cursor -on it, turning to this machine's epics when it ran in another directory. From a script: +open `/epics`, press `i` (or **Import…**) and give the file -- type it, press tab to +take each file in `~/Downloads/` in turn, or drop it onto the terminal, which pastes its path; +the list shows it with the cursor on it, turning to this machine's epics when it ran in another directory. From a script: ```python from hmz.sdk import Hmz diff --git a/docs/user/first-run.md b/docs/user/first-run.md index 94d19cba9..9bfa07f13 100644 --- a/docs/user/first-run.md +++ b/docs/user/first-run.md @@ -8,9 +8,9 @@ const running = [ { label: 'round 1', lines: [ - '[dim]❯[/] Fix the bug in calc.py.', + '[dim]─ reading agent ─[/][n]1[/]', '', - '[dim]── agent[/][n]1[/]', + '[dim]❯[/] Fix the bug in calc.py.', '', '[dim]● agent is working[/]', '', @@ -116,7 +116,7 @@ Three words cover what you set up: in a fresh conversation each round, until its budget runs out or you stop it. - A **role** is a slot the flow declares, such as `agent`, `builder` or `reviewer`. You fill each role with an **agent**: a CLI, the account it runs as, a model, and an effort. -- A **budget** is what a run may spend: time, dollars or output tokens. Every flow except +- A **budget** is what an epic may spend: time, dollars or output tokens. Every flow except `chat` needs one, because most flows are loops that never end on their own. You set all three in the `/flow` menu, save, and then type the task. Nothing runs until you @@ -197,24 +197,27 @@ install it; from then on it is listed here, with its version at the end of its r ## 4. Give the role an agent -The flow opens on its **roles**: a row for each agent it drives, then `budget` and -`profiling`, and buttons under them, **Save** last. `ralph_loop` has one role, `agent`. humanize +The flow opens on its **roles**: a row for each agent it drives, then `budget`, +`profile flow` and `profile environment`, and buttons under them, **Save** last. `ralph_loop` has one role, `agent`. humanize fills it with the first CLI it found, so the row already names one.
  hmz › /flow › Installed › ralph_loop                    ● unsaved changes
   Configure each role: an agent (CLI, account, model and effort) or an
-  environment; then what the flow takes and what a run may spend.
+  environment; then what the flow takes and what an epic may spend.
 
   ╭────────────────────────────────────────────────────────────────────────╮
    agent                                     claude/claude-opus-5-5:high ▸  1
      agent
    ────────────────────────────────────────────────────────────────────────
    budget                                                           none ▸ 2
-     what a run may spend: none set; a run needs one
+     what an epic may spend: none set; an epic needs one
+   ────────────────────────────────────────────────────────────────────────
+   profile flow                                                    ○ off ▾ 3
+     traced only
    ────────────────────────────────────────────────────────────────────────
-   profiling                                                       ○ off ▾ 3
+   profile environment                                             ○ off ▾
      traced only
   ╰────────────────────────────────────────────────────────────────────────╯
 
@@ -226,7 +229,8 @@ fills it with the first CLI it found, so the row already names one.
 
 1. **`agent`**, the flow's one role, and the agent filling it as `cli/model:effort`.
 2. **`budget`**: none yet. You set it in step 5.
-3. **`profiling`** stays off here: it is for when you want the programs a run starts in its
+3. **`profile flow`** and **`profile environment`** stay off here: they are for when you want
+   the flow's own Python, or the programs a run starts, in its
    [trace](/user/tracing#profiling-a-run).
 4. **Save**: nothing you change is kept until you press it. `● unsaved changes` says something
    is waiting.
@@ -293,12 +297,12 @@ Press enter on `budget`:
 
 
 
  hmz › /flow › Installed › ralph_loop › Set budget for ralph_loop ● unsaved changes
-  A run stops at whichever limit it reaches first; at least one limit is
+  An epic stops at whichever limit it reaches first; at least one limit is
   required. Leave empty or 0 for no limit.
 
   ╭────────────────────────────────────────────────────────────────────────╮
    duration                                                            20m 1
-     maximum run duration: 1h30m, 90s, PT2H; empty for no limit
+     maximum epic duration: 1h30m, 90s, PT2H; empty for no limit
    ────────────────────────────────────────────────────────────────────────
    cost                                                                0.5  2
      maximum cost in US dollars, 0 for no limit                            
@@ -324,7 +328,7 @@ Press enter on `budget`:
 3. **`graceful`**: on lets the turn that is running finish when a limit is reached; off cuts
    it off. enter or a click drops the two under it, opening on the one it is not.
 4. **Set** keeps all four: tab to it and press enter. The `budget` row
-   then reads `set`, and under it `what a run may spend: stops at 20m, $0.50`.
+   then reads `set`, and under it `what an epic may spend: stops at 20m, $0.50`.
 
 The run stops at whichever limit it reaches first. If your account is billed by the token, set
 a `cost` as well as a `duration`. See [Every run has a budget](/features/allowances).
@@ -351,8 +355,9 @@ it says it:
 
 What to look at, by number:
 
-1. **`── agent`** marks whose words follow. With several agents, a new one appears wherever the
-   speaker changes.
+1. **`─ reading agent ─`** says whose transcript this is: a run lands on the agent of the first
+   conversation it opens, with what you said at the top, and follows that agent into each new
+   conversation.
 2. **`✻ Worked for 7s · agent`** closes a turn: the first round is done, and `calc.py` is
    already fixed.
 3. **`conversation 2 of 2`**: a Ralph loop starts a fresh conversation every round, so the
@@ -428,7 +433,7 @@ remembers, and forgets it.
 
 ### `this flow requires a budget: set the budget first`
 
-Saving refused, because the budget row still reads `none set; a run needs one`. Set at least
+Saving refused, because the budget row still reads `none set; an epic needs one`. Set at least
 one limit in step 5.
 
 ### The cost row reads `0.00.5`
diff --git a/docs/user/installation.md b/docs/user/installation.md
index 55d145187..9450a3028 100644
--- a/docs/user/installation.md
+++ b/docs/user/installation.md
@@ -229,7 +229,7 @@ Qwen Code signed in:
 ──────────────────────────────────────────────────────────────────────
 ❯
 ──────────────────────────────────────────────────────────────────────
-  ◉ chat · ~/src/demo 3   / commands · shift+enter newline · ← monitor · ctrl+c exit
+ ◉ chat · ~/src/demo 3 / commands · shift+enter newline · ctrl+c exit
diff --git a/docs/user/leaving.md b/docs/user/leaving.md index 00bd650c8..510681301 100644 --- a/docs/user/leaving.md +++ b/docs/user/leaving.md @@ -6,8 +6,8 @@ const back = [ { label: 'hmz, later', lines: [ + '[dim]─ reading agent ─[/]', '[dim]❯[/] Fix the bug in calc.py. [m]· by you@tui[/][n]1[/]', - '[dim]── agent[/]', '[dim]● agent is working[/]', '[g]●[/] Found the bug: add() subtracts. Fixing it now.', '[dim]✻ Worked for 7s · agent[/]', diff --git a/docs/user/monitor.md b/docs/user/monitor.md index 056a3ec0d..36e3daf1c 100644 --- a/docs/user/monitor.md +++ b/docs/user/monitor.md @@ -44,7 +44,7 @@ const actor = (here, marks) => box( here, apart('[c]▸ ● actor[/]', '[c]43s[/]'), - '[m]claude/claude-opus-5-5:high · 6 turns · 1.84M tokens[/]', + apart('[m]claude/claude-opus-5-5:high · 6 turns · 1.84M tokens[/]', '[b]reading[/]'), '[m]▤ workspace local[/]', marks, ) @@ -67,8 +67,8 @@ const sessions = [ ' [m] ▤[/] [m]workspace · local · /home/you/shop[/]', ] -const all = (here, n) => - `${here ? ' [b]❯[/] ' : ' '}[c]▣[/] all agents[m] · 1 of 2 working · 11 turns · 7m11s[/][b] · reading[/]${n ? '[n]2[/]' : ''}` +const epic = (n) => + ` [c]◫[/] epic[m] · running · 1 of 2 working · 11 turns · 7m11s[/]${n ? '[n]2[/]' : ''}` const keys = '↑↓ node · enter open · → back · ctrl+t list · / commands' const folds = '↑↓ node · space sessions · enter open · → back · ctrl+t list · / commands' @@ -76,7 +76,7 @@ const shuts = '↑↓ node · space shut · enter open · → back · ctrl+t lis const listed = [ switched(true), - all(false), + epic(false), ' [m]node runs · where turns time tokens[/][n]1[/]', ' [m]▸[/] [c]●[/] [c]actor[/] [m]claude/claude-opus-5-5:high 6 turns[/][c] 43s[/][m] 1.84M[/][n]2[/]', ' [b]❯[/] [m]▾[/] ○ reviewer [c]unread[/] [m]codex/gpt-5.6-sol:high 5 turns idle 1m04s 402.1k[/]', @@ -89,7 +89,7 @@ const listed = [ const place = [ ' [m]monitor ›[/] [b B]workspace[/]', - ' [m]An environment of the run: where its sessions work.[/]', + ' [m]An environment of the epic: where its sessions work.[/]', '', ` [b]╭${'─'.repeat(W + 4)}╮[/]`, ' [m]kind [/]LOCAL[n]1[/]', @@ -142,13 +142,13 @@ const graph = (top, a, r, keys, n) => [ const monitor = [ { label: '1 · ←', - lines: graph(all(true, true), actor(false, ['[n]3[/]', '', '[n]4[/]']), reviewer(false, false), keys, true), + lines: graph(epic(true), actor(true, ['[n]3[/]', '', '[n]4[/]']), reviewer(false, false), folds, true), caption: - 'The monitor opens on all agents: enter here reads every agent at once.', + 'The monitor opens on the agent you were reading: the actor, where the run landed as it opened its first session.', }, { - label: '2 · ↓ ↓', - lines: graph(all(false), actor(false), reviewer(true, false), folds), + label: '2 · ↓', + lines: graph(epic(false), actor(false), reviewer(true, false), folds), caption: 'The cursor is on the reviewer, which has stopped and has something you have not read. The keys offer space sessions.', }, @@ -156,7 +156,7 @@ const monitor = [ label: '3 · space', lines: [ switched(false), - all(false), + epic(false), ...actor(false), ...subagents(false), ...reviewer(true, true), @@ -218,7 +218,10 @@ prompt at the bottom, so every command works on either. The monitor draws the run as **nodes**: -- `▣ all agents`, always first: the log every agent's work appears on. +- `◫ epic`, always first: how far the run has got -- how many agents are working, their turns, + how long it has been going -- and, once it has started, how it stands and its budget against + what it has spent. There is no log every agent's work appears on; enter on it + opens the epic's own page instead. - `◉` an **outworlder**, where the flow talks to you (for example `◉ human · outworlder: messages from the flow to you`). - a **box per agent** that has taken a turn, in the order the flow declares them. An agent is @@ -240,9 +243,10 @@ What to look at in frame 1, by number: 1. **The switch, `graph` `list`.** Which of the two views you are looking at. A click on the other half turns to it, as ctrl+t does. -2. **`▣ all agents`.** Where the cursor starts: how many boxes are working, the run's turns, - and how long it has run. `· reading` says its log is the one you came from. -3. **The actor's box.** The top line says what it is doing now: `▸` (it can be opened out), +2. **`◫ epic`.** How it stands, how many boxes are working, the run's turns, and how long it + has run. enter on it opens the epic's page. +3. **The actor's box.** Where the cursor starts, `reading` saying its log is the one you came + from: a run lands on the agent of its first session. The top line says what it is doing now: `▸` (it can be opened out), `●` working, and `43s`, how long this turn has been going. The second line says what it runs, `cli/model:effort`, its turns and tokens. 4. **`▤ workspace local`.** The environments its sessions work in. @@ -287,11 +291,13 @@ line says `graph` or `list`, and the monitor opens again the way you left it. | the graph | the flow passes work between agents, and you want to see who is waiting on whom | | the list | many agents or sessions, and you want the busy ones at the top | -## The first node +## The first line -`▣ all agents` is always first, and the cursor starts on it. enter on it reads the log -every agent's work appears on, which is where a run is watched from. Its row also says how many -boxes are working, the run's turns, and how long it has run. +`◫ epic` is always first. It says how the run stands, how many boxes are working, the run's +turns, how long it has run, and its budget against what it has spent. No log holds every +agent's work, each node below reading its own: enter on it opens +[the epic's page](#the-epic-s-page) instead. The cursor starts on the node you were reading, or +on its agent. Under it, a run that talks to you has a `◉` node for each outworlder: the role that is you. Enter on it reads what the flow says to you. Where another interface reads the same run, the node says @@ -352,6 +358,17 @@ With the mouse, a click on the left edge of a box, where its `▸` is, opens it and so does a click on the agent the cursor is already on. A click anywhere else on an agent only moves the cursor there, and a double click reads it. +## The epic's page + +enter or a click on `◫ epic` opens the run's own page, the same content as the epic +panel of [the web page](/user/web): `Figures` (time, spend, tokens), `Budget` (each limit with +what has gone of it, as a meter), `Set up` (the flow, task, agents, environments, params, and +whether it is profiled) and `Flows` (the flows called). It stays live. + +Under it, for every flow but a chat, is `Stop` while the run runs and `Force stop` while it +winds down. tab reaches the button; each asks before it acts. See +[Stopping](/user/stopping). + ## Environments Under each session is the environment it works in: `▤`, the environment role the flow gave it, @@ -372,7 +389,7 @@ out: | `image` | What a container for it is started from, where the flow names one. | | `grants` | The capabilities the flow declared for the role: `Shell`, `Files`, `GitWorktree`, … or `nothing beyond running in it`. | | `needs` | What the flow asks of the machine: CPUs, memory, GPUs. | -| `harness` | Where the run put the harnesses of the agents working in it, such as `self: on this environment's machine, with the CLI installed there` or `local: on this machine; what it runs lands here`. Where sessions went different ways, each is named: `self for NeiKos496/1 · local for PoleMos600/1`. See [Remote execution](/user/remote-execution). | +| `harness` | Where the run put the harnesses of the agents working in it, such as `env: on this environment's machine, with the CLI installed there` or `local: on this machine; what it runs lands here`. Where sessions went different ways, each is named: `env for NeiKos496/1 · local for PoleMos600/1`. See [Remote execution](/user/remote-execution). | | `status` | How many of its sessions are working now, such as `1 of 3 sessions working`. | | `Sessions` | Each session working in it. enter or a click on one reads it. | @@ -403,10 +420,11 @@ drawing, above the prompt. | --- | --- | | ↑ ↓ | With nothing typed: move between the nodes. | | space | With nothing typed: open the agent under the cursor out to its sessions, or shut it. | -| enter | With nothing typed: read that node's log. On an environment, open its page. On a line of the board, change it. | +| enter | With nothing typed: read that node's log. On the epic or an environment, open its page. On a line of the board, change it. | | a click | What enter does. On an agent: on its `▸`, or on the one already picked, open it out or shut it; elsewhere, pick it; twice, read it. | | → | With nothing typed: back to the log you were reading. | | ctrl+t | The graph, or the list. | +| ctrl+e | With nothing typed: the run in [Exomyth](/reference/tui#exomyth), as a click on `Exomyth` above the graph does. Not for a chat. | | ← | On the log, with nothing typed: up to the monitor. | The status line offers only the keys that do something on the node under the cursor: diff --git a/docs/user/permissions.md b/docs/user/permissions.md index ec49b3469..eda3ca94f 100644 --- a/docs/user/permissions.md +++ b/docs/user/permissions.md @@ -248,7 +248,10 @@ says yes. The organisation's own `deny` rules still apply. A [container](/user/containers) or an [ssh host](/user/remote-execution) is held to the same grant as this machine. Every command the agent runs lands on the other machine and is fenced -there, around that machine's own workdir and `$HOME`. With `online` of `NONE`, those commands +there, around that machine's own workdir and `$HOME`. Claude Code, Codex, Grok Build, Qwen +Code and pi are started in a home of their own where they run: here, or on that machine where its own CLI is driven, so +neither writes the settings of the machine it runs on +([Security](/user/security#what-a-flow-lets-its-agents-touch)). With `online` of `NONE`, those commands reach no host at all. The agent's temporary directory is kept on the other machine at the path it has here, so a command that names it, as Claude Code's every shell command does, finds it there and ends with its own exit status. diff --git a/docs/user/questions.md b/docs/user/questions.md index fb03714c7..d9ef1f8ee 100644 --- a/docs/user/questions.md +++ b/docs/user/questions.md @@ -172,12 +172,12 @@ hmz exec -f chat -a assistant=claude/claude-haiku-4-5-20251001:low --json "…" ## Answering, in detail -- **Where you answer.** A question is shown on the transcript every agent is on and on the +- **Where you answer.** A question is shown on whichever transcript you are reading and on the asking outworlder's own, which shift+tab reaches and which holds nothing but what it asks you. A line typed on either answers it: the oldest question up on the first, the oldest that outworlder asks on the second. One the agent stopped mid-turn to ask, and - `chat` asking what next, are shown and answered in that agent's conversation as well. On one - agent's transcript any other line goes to the agent instead. See [Many conversations at once](/user/conversations). + `chat` asking what next, are shown and answered in that agent's conversation instead. On one + agent's transcript any other line goes to the agent. See [Many conversations at once](/user/conversations). - **Somebody else's.** Where [another interface](/features/daemon#several-people-on-one-run) reads the same run, a question nobody has claimed goes to whoever answers first, and an answer from somebody else shows ` · by `. One whose outworlder another has claimed diff --git a/docs/user/remote-execution.md b/docs/user/remote-execution.md index 2a0dcb3f9..be3cbe07b 100644 --- a/docs/user/remote-execution.md +++ b/docs/user/remote-execution.md @@ -32,20 +32,32 @@ CLI itself can run. The part of humanize that does this is called **CogAnchor** -That split is what you get when the host has no CLI of the agent's own, which is the usual -case. Where it has one, the agent can run over there instead: see [Where the agent -runs](#where-the-agent-runs). +That split is what you get with the host saved with its harness here, `local`, which is what +a host with no CLI of the agent's own needs. By default the CLI installed on the host runs over +there instead: see [Where the agent runs](#where-the-agent-runs). ## Try it -With `ssh build-box` working and the project at `/home/me/build/myproject` on it, run a flow -that has a role for another machine, such as [`onbox`](#the-flow-used-on-this-page): +With `ssh build-box` working and the project at `/home/me/build/myproject` on it, save the +host with its harness here, in `~/.hmz/settings.yaml` (or at `/settings runtimes`, as +[below](#example-set-it-up-at-the-prompt)): + +```yaml +runtimes: + ssh: + build-box: + host: build-box + harness: local +``` + +Then run a flow that has a role for another machine, such as +[`onbox`](#the-flow-used-on-this-page): ```sh hmz exec -f onbox \ -a builder=claude/claude-haiku-4-5-20251001:low \ -a reviewer=claude/claude-haiku-4-5-20251001:low \ - -e box=ssh@[build-box]/home/me/build/myproject \ + -e box=ssh@build-box/home/me/build/myproject \ -p budget.duration=10m "Make test_calc.py pass." ``` @@ -107,31 +119,32 @@ host's, every command it runs runs there, and the flow's own steps on `box`, suc The agent's CLI, and whatever supervises it, is its **harness**. Where the harness runs decides whose sign-in the agent uses, where its sessions are kept, and which network its model is reached over. Nothing on the command line says where: a host you [saved under a -name](#save-a-host-under-a-name) says it for every run on it, as its **affinity**, a list of -places tried in order. +name](#save-a-host-under-a-name) says it for every run on it, as its **harness** setting, one +place and nowhere else. -| In an affinity | The agent's CLI runs | +| `harness` | The agent's CLI runs | | --- | --- | -| `self` | on the host itself, where the CLI is installed there and can hold the role's fence | +| `env` (the default) | on the host itself, the CLI installed there | | `local` | here, reaching the host's files and commands | | `ssh:`, `docker:`, `swarm:`, `apple-container:`, `slurm:` | on another saved runtime, reaching the host's work from there | -With no affinity, and on a host nobody saved, the CLI runs on the host where it is installed -there and here otherwise. Work in the workspace always has its harness here. The places are -worked through one by one [below](#where-the-agent-runs). +A host nobody saved, such as `ssh@[build-box]`, has `env`. Where the setting cannot hold the +harness (no CLI on the host, say) the run is refused rather than put anywhere else. Work in the +workspace always has its harness here. The places are worked through one by one +[below](#where-the-agent-runs). ## Example: build on a host, review here This is the run from [Try it](#try-it), in full. `build-box` has the project and Python, and no -coding agent CLI. +coding agent CLI, and is saved with its harness `local`. ```text hmz exec -f onbox ① -a builder=claude/claude-haiku-4-5-20251001:low -a reviewer=claude/claude-haiku-4-5-20251001:low - -e box=ssh@[build-box]/home/me/build/myproject ② + -e box=ssh@build-box/home/me/build/myproject ② -p budget.duration=10m ③ "Make test_calc.py pass." ``` @@ -157,11 +170,10 @@ hmz exec -f onbox ① 1. **`-f onbox`** names a flow with an environment role. A flow without one refuses `-e`: `ralph_loop has no environment role 'box'; available roles are none`. -2. **`-e box=ssh@[build-box]/home/me/build/myproject`** puts the `box` role on the host - `build-box`, in that directory. In brackets, `build-box` is whatever `ssh build-box` - reaches, so an alias from your ssh config works as it is; once it is [saved under a - name](#save-a-host-under-a-name), `ssh@build-box` names it. The path is the host's, and has - to exist there. +2. **`-e box=ssh@build-box/home/me/build/myproject`** puts the `box` role on the host saved + as `build-box`, in that directory. In brackets, `ssh@[build-box]` would be whatever + `ssh build-box` reaches with nothing saved, so with the CLI on the host rather than here. + The path is the host's, and has to exist there. 3. **`-p budget.duration=10m`** is the budget, as for any flow. A remote run spends like any other. 4. **The builder's tools name `/home/me/build/myproject`**, the host's path. The agent sees the @@ -172,9 +184,8 @@ hmz exec -f onbox ① 6. **The reviewer works in the workspace**, here, and reads a `git diff` the flow ran on the host. One flow can mix roles here and there. -Nothing said where the builder's CLI runs, and a host nobody saved has no affinity, so with -no `claude` on `build-box` it ran here. At the prompt the transcript says so as the role's -first turn there starts: +`build-box` is saved with its harness `local`, so the builder's CLI ran here. At the prompt +the transcript says so as the role's first turn there starts: ```text ❯ Make test_calc.py pass. @@ -247,10 +258,10 @@ under the list says what pressing it will do: │ falls back to — │ │ runtimes to try in order if this one cannot: docker:box, ssh:gpu2 │ │──────────────────────────────────────────────────────────────────────────│ - │ harness runs on — │ ④ - │ where an agent's harness runs, in order, the next only when one has no │ - │ room: self, local, ssh:, docker:, swarm:; blank for │ - │ self where the CLI is there, else local │ + │ harness runs on local │ ④ + │ where an agent's harness runs, and nowhere else: env (the CLI on this │ + │ runtime's machine), local (here), or ssh:, docker:, │ + │ swarm:, apple-container:, slurm:; blank for env │ ╰──────────────────────────────────────────────────────────────────────────╯ adds ssh/build-box, and checks its resources @@ -307,7 +318,7 @@ leaves it out. Nothing is left to answer, so the focus moves on to **Done**: ```text hmz › @local/onbox ● unsaved changes Configure each role: an agent (CLI, account, model and effort) or an - environment; then what the flow takes and what a run may spend. + environment; then what the flow takes and what an epic may spend. ╭──────────────────────────────────────────────────────────────────────────╮ │ builder claude/claude-haiku-4-5-20251001:high ▸ │ @@ -320,9 +331,12 @@ leaves it out. Nothing is left to answer, so the focus moves on to **Done**: │ environment │ │──────────────────────────────────────────────────────────────────────────│ │ budget set ▸ │ - │ what a run may spend: stops at 10m │ + │ what an epic may spend: stops at 10m │ + │──────────────────────────────────────────────────────────────────────────│ + │ profile flow ○ off ▾ │ + │ traced only │ │──────────────────────────────────────────────────────────────────────────│ - │ profiling ○ off ▾ │ + │ profile environment ○ off ▾ │ │ traced only │ ╰──────────────────────────────────────────────────────────────────────────╯ @@ -338,8 +352,9 @@ leaves it out. Nothing is left to answer, so the focus moves on to **Done**: 2. **`name`** is what `-e` and `/flow` call it from now on: `-e box=ssh@build-box`. 3. **`workdir`** is where a role put on this host works when nothing more is said, so `ssh@build-box` alone means `ssh@build-box/home/me/build/myproject`. -4. **`harness runs on`** is the host's affinity: where the CLI of an agent working on it runs, - [below](#where-the-agent-runs). Blank is the default. +4. **`harness runs on`** is the host's harness setting: where the CLI of an agent working on + it runs, [below](#where-the-agent-runs). `local` keeps it here, which a host with no CLI + needs; blank is the default, `env`, the CLI on the host. 5. **`answers`** is the check: humanize reached the host with nobody there to type a password, and read its home, CPUs and memory. A host that cannot be reached says why on this line. 6. **The `box` row** holds what the form set, as `-e` spells it: `ssh@build-box`, the saved @@ -373,9 +388,10 @@ Where the machines are a Slurm cluster and this machine can `sbatch` to it, let choose the node: `-e box=slurm[@]/` gives the role a job of its own, one task on one node, submitted when the run starts and cancelled when it ends. The job asks for the CPUs, memory and GPUs the flow's role declares, and the cluster keeps it to them; everything the -agent runs lands on the job's node, inside that allocation. The agent's CLI runs here, -supervised, unless the node has one of its own or the runtime's affinity says otherwise -([Where the agent runs](#where-the-agent-runs)), so the nodes need no CLI. +agent runs lands on the job's node, inside that allocation. The agent's CLI runs where the +runtime's harness setting says ([Where the agent runs](#where-the-agent-runs)): by default the +CLI on the node, which the nodes then need; set it to `local` to run it here, supervised, so +that the nodes need no CLI. ```sh hmz exec -f onbox \ @@ -427,53 +443,43 @@ Details: [Machines › Slurm environments](/reference/machines#slurm-environment The harness is the agent's CLI and whatever supervises it. Where it runs is a setting of the runtime the work is on, not of the run: the `harness runs on` row of a saved host's form, its -**affinity**. Write the places in the order to try them, separated by commas; the next is tried -only when the one before has no room: +**harness**. It names one place, and the harness goes there or the run is refused; nothing +else is tried: ```text - │ harness runs on docker:gpubox, local │ - │ where an agent's harness runs, in order, the next only when one has no │ - │ room: self, local, ssh:, docker:, swarm:; blank for │ - │ self where the CLI is there, else local │ + │ harness runs on docker:gpubox │ + │ where an agent's harness runs, and nowhere else: env (the CLI on this │ + │ runtime's machine), local (here), or ssh:, docker:, │ + │ swarm:, apple-container:, slurm:; blank for env │ ``` -| | *(blank)* | `local` | `self` | `ssh:`, `docker:` | -| --- | --- | --- | --- | --- | -| **The CLI runs** | on the host if it can, else here | here | on the host | on that runtime | -| **Signed in as** | whichever it came to | this machine's CLI, or the `@account` | the host's CLI, or the `@account` sent there | that machine's CLI | -| **Sessions kept** | whichever it came to | here, with the run | on the host | on that machine | -| **Needs on the host** | Python 3.12 | Python 3.12 | Python 3.12, the CLI, and Landlock for a fenced role | Python 3.12 | -| **No room when** | never | never | the CLI is not on the host, or it cannot hold the role's fence | it cannot be reached, has no share left, or the role is fenced | +| | `env` *(blank)* | `local` | `ssh:`, `docker:` | +| --- | --- | --- | --- | +| **The CLI runs** | on the host | here | on that runtime | +| **Signed in as** | the host's CLI, or the `@account` sent there | this machine's CLI, or the `@account` | that machine's CLI | +| **Sessions kept** | on the host | here, with the run | on that machine | +| **Needs on the host** | Python 3.12, the CLI, and Landlock for a fenced role | Python 3.12 | Python 3.12 | +| **Refused when** | the CLI is not on the host, it cannot hold the role's fence, or a hook decides whether each tool runs | never | it cannot be reached, has no share left, or the role is fenced | Each role is settled once per machine, as its first turn there starts, and before the flow -is called every role is walked against every machine of the run: where no place in an affinity -has room, the run is refused before anything runs, with the last place's refusal. The -affinity read is the one of the runtime the work is on; a runtime a harness is sent to is used -as it is, its own affinity never walked. - -### No affinity: the default - -The CLI runs on the host when all three hold, and here otherwise: - -- the host has the CLI, on the `PATH` a command run there gets; -- the host can hold the role's [permission](/user/permissions), which takes Landlock there for - any role not granted everything; -- the flow hangs no hook on the role that decides whether each tool runs, such as the builder - of [`humanize1:rlcr`](https://humanfia.ai/flows/humanize1): a CLI on another machine can only report what such a - hook would have decided, so that role stays here. A hook that answers the agent's questions - is not one of those: a question comes back from wherever the CLI runs. +is called every role is settled against every machine of the run: where the setting cannot +hold the harness, the run is refused before anything runs, saying why. The setting read is +the one of the runtime the work is on; a runtime a harness is sent to is used as it is, its +own setting never read. -A host given in brackets and never saved, and docker's default here (`docker/…`), are always -placed this way. Nothing to do when it goes either way: the run is the same run. Give the host an -affinity only to insist. +A host given in brackets and never saved, and docker's default here (`docker/…`), have `env`: +the CLI must be on the host, or in the image. To have it here instead, save the host and set +its harness to `local`. ### `local`: here The CLI runs here, with this machine's sign-in and its sessions kept with the run. It works in a copy of the host's directory that humanize keeps here, and every command it runs is sent to -the host. The transcript says `builder's harness runs here`. Use it when the host is one you -would not give your account to, or when its CLI is signed in as somebody else. `local` always -has room, so nothing after it is ever tried. +the host. The transcript says `builder's harness runs here`. Use it when the host has no CLI, +when it is one you would not give your account to, or when its CLI is signed in as somebody +else. A role with a hook that decides whether each tool runs, such as the builder of +[`humanize1:rlcr`](https://humanfia.ai/flows/humanize1), needs it: the CLI's hooks name a +program on this machine. #### The path is taken here too @@ -493,7 +499,7 @@ A docker environment keeps its copy under `~/.hmz/envs/mirrors/` instead, so its workdir may be your own checkout. ::: -### `self`: on the host +### `env`: on the host The CLI installed on the host runs there, in the host's directory, with nothing copied. The transcript says `builder's harness runs on its environment's machine`. Use it when the host is @@ -506,22 +512,23 @@ where the CLI should live: a machine with its own sign-in, or a network only it while no other turn is using it, and what the host renewed it to is brought back: two copies renewing apart get the login revoked. A turn that finds it in use the other way is refused with `… signs in with a token that refreshes itself …`; give such roles an account signed in - with a key, or put `local` first in the host's affinity + with a key, or set the host's harness to `local` ([more](/user/troubleshooting#this-account-signs-in-with-a-token-that-refreshes-itself)). - **Never copy your own sign-in to the host.** Sign the CLI in there itself. A copy of `~/.codex/auth.json` or `~/.claude/.credentials.json` renews apart from yours and signs this machine out. - **Its sessions are kept on the host**, not with the run here. -- **The flow's own tools are not offered to it**, and a hook that decides whether each tool - runs can only watch. +- **The flow's own tools are not offered to it**, and a role with a hook that decides whether + each tool runs is refused: `a pre_tool_use hook cannot gate the tools of a CLI on another + machine; set the harness of the runtime it is on to local`. A hook that answers the agent's + questions is not one of those: a question comes back from wherever the CLI runs. - **A role not granted everything needs Landlock on the host** to hold its permission. -Where the host has no CLI, or cannot hold the role's permission, `self` has no room and the -next place is tried. Where `self` is the last, the run is refused before the flow starts, with -exit status 2, saying what to do about the first one missing: +Where the host has no CLI, or cannot hold the role's permission, the run is refused before +the flow starts, with exit status 2, saying what to do about it: ```text -hmz exec: error: ssh@build-box: nowhere its affinity (self) names has room for claude's harness; the last: claude is not installed on ssh@build-box: npm i -g @anthropic-ai/claude-code there, or put local in the affinity of the runtime it is on +hmz exec: error: ssh@build-box: claude's harness cannot run where its harness setting (env) says: claude is not installed on ssh@build-box: npm i -g @anthropic-ai/claude-code there, or set the harness of the runtime it is on to local ``` ::: tip Installed is on the `PATH` an ssh command gets @@ -537,31 +544,31 @@ neither this machine nor the host should hold the agent: a machine that is the o in, say, or a docker daemon with containers to spare. The runtime is opened as an environment of its own for the run, probed with the others and taken down with them: -| In the affinity | The CLI runs | +| `harness` | The CLI runs | | --- | --- | | `ssh:gpu-box` | on the host saved as `gpu-box`, in its saved workdir, else the login's home | | `docker:gpubox` | in a container of its own on the daemon saved as `gpubox`, holding its saved workdir; a daemon on this machine saved without one holds an empty `$TMPDIR/humanize-/harness` | -Such a place has no room when it cannot be reached or opened, when a daemon has no share left -for the container (its `max containers` reached, say), and for any role not granted -everything. Its other caveats: +The run is refused when it cannot be reached or opened, when a daemon has no share left for +the container (its `max containers` reached, say), and for any role not granted everything. +Its other caveats: - **Only a role granted everything.** A permission cannot be held around a CLI on another - machine, so a fenced role passes it by: `a fence cannot hold a harness that runs on another + machine, so a fenced role is refused: `a fence cannot hold a harness that runs on another machine`. Most flows' roles run at the default grant; see [Permissions](/user/permissions). - **The CLI must be installed and signed in there.** Nothing checks before the turn: a machine without it fails the turn with `claude: not found on PATH`. No `@account` is sent there. - **It opens a port here, on every interface,** for as long as the run lasts, for the two machines to meet. Set `HUMANIZE_RENDEZVOUS_PORT` to pin it for a firewall. -- **It cannot be the host itself:** an affinity naming its own runtime is refused when it is - saved; that is `self`. +- **It cannot be the host itself:** a harness naming its own runtime is refused when it is + saved; that is `env`. ## What it needs | Where | What it needs | | --- | --- | | **This machine** | Linux on x86-64 or aarch64, and the agent's CLI installed and signed in, unless the harness runs elsewhere | -| **The host** | Linux or macOS on any architecture, Python 3.12 or newer, `ssh` access, and the project already at that path. The CLI too, for `self`. | +| **The host** | Linux or macOS on any architecture, Python 3.12 or newer, `ssh` access, and the project already at that path. The CLI too, for `env`, the default. | | **The flow** | a role for the host, besides its workspace | A Mac gives a remote command no `python3` on its `PATH`, so humanize also looks where Homebrew @@ -583,7 +590,7 @@ and the python.org installer put one, and says what it looked for if it finds no (`ssh:build-2, docker:box`): where `-e box=ssh@build-box/…` cannot reach `build-box`, the role goes to the first of those that it can, and the run says so — `hmz exec: ssh:build-box cannot hold 'box': …; using ssh:build-2`. Where the agent's CLI - runs is still `-H`'s. See [Machines › Falling back](/reference/machines#falling-back). + runs is then `ssh:build-2`'s harness setting. See [Machines › Falling back](/reference/machines#falling-back). ## If it goes wrong {#when-it-refuses} @@ -603,16 +610,17 @@ status 2. | `… 'box' needs 8 GPUs, and the environment given has 0` | The flow asks more of the host than it has. Pick another host. | | `… already contains files and is not an humanize mirror …` | The same path here holds other files. See [the path is taken here too](#the-path-is-taken-here-too). | | `… mirrors ssh://old-box, not ssh://build-box …` | That path here holds humanize's copy of another host. Use another path. | -| `cannot keep the local copy of the work at …` on the first turn | The path cannot be created here. Use one you can create, or put `self` in the host's affinity. | -| `… nowhere its affinity (self) names has room …: claude is not installed on ssh@build-box: …` | `self` and no CLI on the host's `PATH`. Install it there, or add `local` to the affinity. | -| `… nowhere its affinity (docker:gpubox) names has room …: a fence cannot hold a harness that runs on another machine` | A runtime in the affinity for a role not granted everything. Add `local` after it. | -| `build-box: 'somewhere' is not where a harness runs: self, local or :` | Saving the host refused an affinity entry. Spell each as one of those. | +| `cannot keep the local copy of the work at …` on the first turn | The path cannot be created here. Use one you can create, or set the host's harness to `env`. | +| `… claude's harness cannot run where its harness setting (env) says: claude is not installed on ssh@build-box: …` | `env` (the default, and a host nobody saved) and no CLI on the host's `PATH`. Install it there, or save the host with its harness `local`. | +| `… harness cannot run where its harness setting (env) says: a pre_tool_use hook cannot gate the tools of a CLI on another machine; …` | The flow gates the role's tools with a hook. Save the host with its harness `local`. | +| `… harness cannot run where its harness setting (docker:gpubox) says: … a fence cannot hold a harness that runs on another machine` | A harness runtime for a role not granted everything. Set the harness to `local` or `env`. | +| `build-box: 'somewhere' is not where a harness runs: env, local or :` | Saving the host refused its harness. Spell it as one of those. | More, with what causes each, are in [Troubleshooting](/user/troubleshooting). ::: details Good to know - **Network.** With the harness here, the agent's own connection to its model provider stays - here, and the commands it runs use the host's network. With `self` both are the host's. + here, and the commands it runs use the host's network. With `env` both are the host's. - **Timing.** What a command changes on the host is visible to the agent once that command has exited. See [What is not guaranteed](/reference/remote-execution#what-is-not-guaranteed). - **Other ways to reach a machine.** A port left listening, a running container, or a target @@ -675,10 +683,10 @@ async def onbox(task: str, *, agents: Agents, envs: Envs, params: FlowParams, ct - [Containers](/user/containers): a container for a role, reached the same way - [Permissions](/user/permissions): what a role may touch, on the host as here -- [Remote execution reference](/reference/remote-execution): the arrangements behind an - affinity, and where the account lives in each +- [Remote execution reference](/reference/remote-execution): the arrangements behind a + harness setting, and where the account lives in each - [Machines › Runtimes](/reference/machines#runtimes): every field of - a saved host, its affinity among them + a saved host, its harness among them - [humanize in CI](/user/ci): the same `-e` in a scheduled job